Compare commits
43 Commits
Author | SHA1 | Date |
---|---|---|
contrapunctus | 6c96cc4e57 | |
contrapunctus | ffe615c1e8 | |
contrapunctus | 1889357b07 | |
contrapunctus | 3fdc43f334 | |
contrapunctus | a975bf80d7 | |
contrapunctus | 2ca6c60bf2 | |
contrapunctus | 16de1e0837 | |
contrapunctus | 931e5515bf | |
contrapunctus | e183e463cd | |
contrapunctus | 3b1f90e8a4 | |
contrapunctus | 1ffabf5545 | |
contrapunctus | ef0eb25dcf | |
contrapunctus | 922fa41833 | |
contrapunctus | 108411c82f | |
contrapunctus | 9a5c577a3d | |
contrapunctus | f3e23df86e | |
contrapunctus | 3ada39fb84 | |
contrapunctus | 74b1206296 | |
contrapunctus | 9f8ff1b384 | |
contrapunctus | 0e9c6b37d4 | |
contrapunctus | 9b570e100f | |
contrapunctus | 5b5199476d | |
contrapunctus | 4227b306b0 | |
contrapunctus | 388865e58b | |
contrapunctus | b7baba6376 | |
contrapunctus | 6b93d8183b | |
contrapunctus | 3fc7109a95 | |
contrapunctus | 0ca1dd96b3 | |
contrapunctus | 2c3045e7c1 | |
contrapunctus | 1037cacd3c | |
contrapunctus | 8acf23b593 | |
contrapunctus | 30361027c7 | |
contrapunctus | f840e8ddb2 | |
contrapunctus | 9c503a88eb | |
contrapunctus | 1f4f018c70 | |
contrapunctus | b141b3b2d1 | |
contrapunctus | aa22914fcb | |
contrapunctus | 74fdfa833c | |
contrapunctus | fa991fdf01 | |
contrapunctus | 6b1d55493d | |
contrapunctus | 32c976a6f2 | |
contrapunctus | 956835e2d1 | |
contrapunctus | 7b5b9b75ad |
|
@ -1,7 +1,8 @@
|
||||||
;;; Directory Local Variables
|
;;; Directory Local Variables
|
||||||
;;; For more information see (info "(emacs) Directory Variables")
|
;;; For more information see (info "(emacs) Directory Variables")
|
||||||
|
|
||||||
((emacs-lisp-mode . ((nameless-aliases . (("cc" . "chronometrist-common")
|
((emacs-lisp-mode . ((nameless-aliases . (("cb" . "chronometrist-backend")
|
||||||
|
("cc" . "chronometrist-common")
|
||||||
("cd" . "chronometrist-diary")
|
("cd" . "chronometrist-diary")
|
||||||
("ce" . "chronometrist-events")
|
("ce" . "chronometrist-events")
|
||||||
("ck" . "chronometrist-kv")
|
("ck" . "chronometrist-kv")
|
||||||
|
|
17
TODO.org
17
TODO.org
|
@ -171,9 +171,8 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
||||||
: (foo (1 . bar))
|
: (foo (1 . bar))
|
||||||
Yikes!
|
Yikes!
|
||||||
|
|
||||||
** chronometrist [8%]
|
** New features [0%]
|
||||||
1. [ ] Add =:stop= time when we call =chronometrist-kv-accept=, not when we quit the key-value prompt.
|
1. [ ] undo/redo (by running undo-tree commands on chronometrist.sexp)
|
||||||
2. [ ] Implement undo/redo by running undo-tree commands on chronometrist.sexp
|
|
||||||
* [ ] Possibly show what changes would be made, and prompt the user to confirm it.
|
* [ ] Possibly show what changes would be made, and prompt the user to confirm it.
|
||||||
* How will this work with the SQLite backend? Rollbacks?
|
* How will this work with the SQLite backend? Rollbacks?
|
||||||
* It might be easier to just have a 'remove last interval' (the operation I use undo for most often), so we don't reimplement an undo for SQLite.
|
* It might be easier to just have a 'remove last interval' (the operation I use undo for most often), so we don't reimplement an undo for SQLite.
|
||||||
|
@ -182,9 +181,8 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
||||||
4. [ ] *Delete* a project (erasing all records)
|
4. [ ] *Delete* a project (erasing all records)
|
||||||
5. [ ] *Hide* a project (don't show it in any Chronometrist-* buffer, effectively deleting it non-destructively)
|
5. [ ] *Hide* a project (don't show it in any Chronometrist-* buffer, effectively deleting it non-destructively)
|
||||||
6. [ ] *Reset* current interval - update the =:start= time to the current time.
|
6. [ ] *Reset* current interval - update the =:start= time to the current time.
|
||||||
7. [ ] Alternative query function for tags and key-values - a single query. Either with tags and key-values as a single plist, or something like the multi-field query-replace prompt.
|
|
||||||
|
|
||||||
** chronometrist [14%]
|
** chronometrist [12%]
|
||||||
1. [ ] Add =:stop= time when we call =chronometrist-kv-accept=, not when we quit the key-value prompt with a blank input.
|
1. [ ] Add =:stop= time when we call =chronometrist-kv-accept=, not when we quit the key-value prompt with a blank input.
|
||||||
* It might be nice to be able to quit =chronometrist-kv-add= with C-g instead, actually.
|
* It might be nice to be able to quit =chronometrist-kv-add= with C-g instead, actually.
|
||||||
+ =C-g= stops execution of =chronometrist-run-functions-and-clock-in=/=chronometrist-run-functions-and-clock-out=, so they can't reach the calls for =chronometrist-in=/=chronometrist-out=.
|
+ =C-g= stops execution of =chronometrist-run-functions-and-clock-in=/=chronometrist-run-functions-and-clock-out=, so they can't reach the calls for =chronometrist-in=/=chronometrist-out=.
|
||||||
|
@ -206,6 +204,15 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
||||||
* If the input string can be read in a single call to =read=, treat it as an s-expression; else, use the current heuristics.
|
* If the input string can be read in a single call to =read=, treat it as an s-expression; else, use the current heuristics.
|
||||||
6. [ ] key-values - create transformer for key-values, to be run before they are added to the file. This will allow users to do cool things like sorting the key-values.
|
6. [ ] key-values - create transformer for key-values, to be run before they are added to the file. This will allow users to do cool things like sorting the key-values.
|
||||||
7. [ ] Customizable field widths
|
7. [ ] Customizable field widths
|
||||||
|
8. [ ] Alternative query function for tags and key-values - a single query. Either with tags and key-values as a single plist, or something like the multi-field query-replace prompt.
|
||||||
|
|
||||||
|
*** Migration [0%]
|
||||||
|
1. [ ] Implement readers (format -> hash table), emitters (hash table -> format), and predicates for each format. Then implement a single =migrate= command to convert between any supported formats.
|
||||||
|
* sounds like a job for EIEIO - each format could be defined as an object, with slots for information such as the file extension.
|
||||||
|
2. [ ] Instead of using =require= for all backends and dependencies, check which backends are loaded, and load dependencies accordingly.
|
||||||
|
3. [ ] Make migration asynchronous, and use output from the sub-process to make a progress indicator.
|
||||||
|
4. [ ] Optimization - (SQL) read all keywords once, remove duplicates, and create columns for them, instead of checking =pragma= for each plist.
|
||||||
|
5. [ ] (SQL) Use transactions.
|
||||||
|
|
||||||
** chronometrist-report [0%]
|
** chronometrist-report [0%]
|
||||||
1. [ ] Show week counter and max weeks; don't scroll past first/last weeks
|
1. [ ] Show week counter and max weeks; don't scroll past first/last weeks
|
||||||
|
|
223
doc/manual.info
223
doc/manual.info
|
@ -32,6 +32,7 @@ The structure of this manual was inspired by @uref{https://documentation.divio.c
|
||||||
How to@dots{}
|
How to@dots{}
|
||||||
|
|
||||||
* How to set up Emacs to contribute::
|
* How to set up Emacs to contribute::
|
||||||
|
* How to create a new backend::
|
||||||
|
|
||||||
Explanation
|
Explanation
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ Explanation
|
||||||
Chronometrist
|
Chronometrist
|
||||||
|
|
||||||
* Optimization::
|
* Optimization::
|
||||||
|
* Backends::
|
||||||
|
|
||||||
Midnight-spanning events
|
Midnight-spanning events
|
||||||
|
|
||||||
|
@ -63,18 +65,15 @@ Tags and Key-Values
|
||||||
Reference
|
Reference
|
||||||
|
|
||||||
* Legend of currently-used time formats::
|
* Legend of currently-used time formats::
|
||||||
|
* chronometrist-backend.el: chronometrist-backendel.
|
||||||
* chronometrist-common.el: chronometrist-commonel.
|
* chronometrist-common.el: chronometrist-commonel.
|
||||||
* chronometrist-custom.el: chronometrist-customel.
|
|
||||||
* chronometrist-diary-view.el: chronometrist-diary-viewel.
|
* chronometrist-diary-view.el: chronometrist-diary-viewel.
|
||||||
* chronometrist.el: chronometristel.
|
* chronometrist.el: chronometristel.
|
||||||
* chronometrist-events.el: chronometrist-eventsel.
|
* chronometrist-events.el: chronometrist-eventsel.
|
||||||
* chronometrist-migrate.el: chronometrist-migrateel.
|
* chronometrist-migrate.el: chronometrist-migrateel.
|
||||||
* chronometrist-plist-pp.el: chronometrist-plist-ppel.
|
* chronometrist-plist-pp.el: chronometrist-plist-ppel.
|
||||||
* chronometrist-queries.el: chronometrist-queriesel.
|
|
||||||
* chronometrist-report-custom.el: chronometrist-report-customel.
|
|
||||||
* chronometrist-report.el: chronometrist-reportel.
|
* chronometrist-report.el: chronometrist-reportel.
|
||||||
* chronometrist-key-values.el: chronometrist-key-valuesel.
|
* chronometrist-key-values.el: chronometrist-key-valuesel.
|
||||||
* chronometrist-statistics-custom.el: chronometrist-statistics-customel.
|
|
||||||
* chronometrist-statistics.el: chronometrist-statisticsel.
|
* chronometrist-statistics.el: chronometrist-statisticsel.
|
||||||
* chronometrist-time.el: chronometrist-timeel.
|
* chronometrist-time.el: chronometrist-timeel.
|
||||||
* chronometrist-timer.el: chronometrist-timerel.
|
* chronometrist-timer.el: chronometrist-timerel.
|
||||||
|
@ -98,6 +97,7 @@ Legend of currently-used time formats
|
||||||
|
|
||||||
@menu
|
@menu
|
||||||
* How to set up Emacs to contribute::
|
* How to set up Emacs to contribute::
|
||||||
|
* How to create a new backend::
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@node How to set up Emacs to contribute
|
@node How to set up Emacs to contribute
|
||||||
|
@ -123,6 +123,16 @@ From the project root, you can now run
|
||||||
@end enumerate
|
@end enumerate
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
||||||
|
@node How to create a new backend
|
||||||
|
@section How to create a new backend
|
||||||
|
|
||||||
|
@enumerate
|
||||||
|
@item
|
||||||
|
Subclass @samp{chronometrist-backend} to define your backend.
|
||||||
|
@item
|
||||||
|
Create methods for the eight generic functions in @samp{chronometrist-backend.el}
|
||||||
|
@end enumerate
|
||||||
|
|
||||||
@node Explanation
|
@node Explanation
|
||||||
@chapter Explanation
|
@chapter Explanation
|
||||||
|
|
||||||
|
@ -157,7 +167,7 @@ Hooks allow the time tracker to automate tasks and become a useful part of your
|
||||||
Make it easy to edit data using existing, familiar tools
|
Make it easy to edit data using existing, familiar tools
|
||||||
@itemize
|
@itemize
|
||||||
@item
|
@item
|
||||||
We don't use an SQL database, where changing a single field is tricky [1]
|
We don't use an SQL database, where changing a single field is tricky @footnote{I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?}
|
||||||
@item
|
@item
|
||||||
We use a text file containing s-expressions (easy for humans to read and write)
|
We use a text file containing s-expressions (easy for humans to read and write)
|
||||||
@item
|
@item
|
||||||
|
@ -171,8 +181,6 @@ Have a useful, informative, interactive interface
|
||||||
Support mouse and keyboard use equally
|
Support mouse and keyboard use equally
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
||||||
[1] I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?
|
|
||||||
|
|
||||||
@node Terminology
|
@node Terminology
|
||||||
@section Terminology
|
@section Terminology
|
||||||
|
|
||||||
|
@ -214,6 +222,7 @@ Note - sometimes, when hacking or dealing with errors, timers may result in subt
|
||||||
|
|
||||||
@menu
|
@menu
|
||||||
* Optimization::
|
* Optimization::
|
||||||
|
* Backends::
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@node Optimization
|
@node Optimization
|
||||||
|
@ -248,7 +257,7 @@ The next one was released in v0.5. Till then, any time the @uref{../elisp/chrono
|
||||||
After the optimization@dots{}
|
After the optimization@dots{}
|
||||||
@enumerate
|
@enumerate
|
||||||
@item
|
@item
|
||||||
Two backend functions (@uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-sexp-new}} and @uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-sexp-replace-last}}) were modified to set a flag (@uref{../elisp/chronometrist.el, @samp{chronometrist--inhibit-read-p}}) before saving the file.
|
Two backend functions (@uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-new}} and @uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-replace-last}}) were modified to set a flag (@uref{../elisp/chronometrist.el, @samp{chronometrist--inhibit-read-p}}) before saving the file.
|
||||||
@item
|
@item
|
||||||
If this flag is non-nil, @uref{../elisp/chronometrist.el, @samp{chronometrist-refresh-file}} skips the expensive calls to @samp{chronometrist-events-populate}, @samp{chronometrist-tasks-from-table}, and @samp{chronometrist-tags-history-populate}, and resets the flag.
|
If this flag is non-nil, @uref{../elisp/chronometrist.el, @samp{chronometrist-refresh-file}} skips the expensive calls to @samp{chronometrist-events-populate}, @samp{chronometrist-tasks-from-table}, and @samp{chronometrist-tags-history-populate}, and resets the flag.
|
||||||
@item
|
@item
|
||||||
|
@ -266,6 +275,45 @@ Instead, the aforementioned backend functions modify the relevant variables - @s
|
||||||
There are still some operations which @uref{../elisp/chronometrist.el, @samp{chronometrist-refresh-file}} runs unconditionally - which is to say there is scope for further optimization, if or when required.
|
There are still some operations which @uref{../elisp/chronometrist.el, @samp{chronometrist-refresh-file}} runs unconditionally - which is to say there is scope for further optimization, if or when required.
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
||||||
|
@node Backends
|
||||||
|
@subsection Backends
|
||||||
|
|
||||||
|
Chronometrist can currently store data in two ways -
|
||||||
|
@enumerate
|
||||||
|
@item
|
||||||
|
as plists in a plain text file, using the s-expression backend.
|
||||||
|
@item
|
||||||
|
as an SQL database, using the sqlite3 backend.
|
||||||
|
@end enumerate
|
||||||
|
|
||||||
|
Each of these has areas they excel in - plain text files are VCS-friendly, permit the use of tools like @samp{grep}, and are easy to make small, one-off edits in. SQL databases support a standard and popular query language, ACID guarantees, and are easy to make sweeping changes in.
|
||||||
|
|
||||||
|
To try and get the best of both, two approaches were considered -
|
||||||
|
@enumerate
|
||||||
|
@item
|
||||||
|
Support for multiple simultaneous backends
|
||||||
|
@itemize
|
||||||
|
@item
|
||||||
|
@samp{chronometrist-backend-list} could be a custom variable, containing a list of enabled backends
|
||||||
|
@item
|
||||||
|
A function like @samp{(call-with-backends generic-function &rest args)} could be created, which calls the generic-function with each backend as the first argument, and use this whenever Chronometrist makes a change to the files.
|
||||||
|
@item
|
||||||
|
When the user makes a change to one of the files outside of Chronometrist, we detect the change (via filesystem watchers) and propagate it to the other backends.
|
||||||
|
@end itemize
|
||||||
|
Consequently, a user can make quick, one-off edits to the SQL database by editing a text file, or make sweeping, structure-aware changes to the text file using SQL statements.
|
||||||
|
@item
|
||||||
|
SQL as the main backend, with viewing/editing UI + auto-export
|
||||||
|
@enumerate
|
||||||
|
@item
|
||||||
|
Write a 'record viewer/editor' for Chronometrist records. Basically, it reads in a record from the SQL database and displays it in an editable buffer. Forward/backward commands would be available, like in @samp{chronometrist-report} and @samp{chronometrist-statistics}. The record could be represented as anything - CSV, Org markup, Lisp data, etc. The user could edit and run a command to accept the changes, which would change the database.
|
||||||
|
@item
|
||||||
|
Automatically export from SQL to a plain text (or other) format, whenever there is a change in the database. (probably using hooks)
|
||||||
|
@end enumerate
|
||||||
|
These two put together result in the viewing and editing of individual records being easier, and the plain text format can be placed in a VCS, without dealing with any synchronization issues.
|
||||||
|
@end enumerate
|
||||||
|
|
||||||
|
Backends are defined as subclasses of the class @samp{chronometrist-backend} @footnote{I would have liked to define them as instances of @samp{chronometrist-backend}, but EIEIO, unlike CLOS, does not support EQL specialization.}.
|
||||||
|
|
||||||
@node Midnight-spanning events
|
@node Midnight-spanning events
|
||||||
@section Midnight-spanning events
|
@section Midnight-spanning events
|
||||||
|
|
||||||
|
@ -425,18 +473,15 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
|
|
||||||
@menu
|
@menu
|
||||||
* Legend of currently-used time formats::
|
* Legend of currently-used time formats::
|
||||||
|
* chronometrist-backend.el: chronometrist-backendel.
|
||||||
* chronometrist-common.el: chronometrist-commonel.
|
* chronometrist-common.el: chronometrist-commonel.
|
||||||
* chronometrist-custom.el: chronometrist-customel.
|
|
||||||
* chronometrist-diary-view.el: chronometrist-diary-viewel.
|
* chronometrist-diary-view.el: chronometrist-diary-viewel.
|
||||||
* chronometrist.el: chronometristel.
|
* chronometrist.el: chronometristel.
|
||||||
* chronometrist-events.el: chronometrist-eventsel.
|
* chronometrist-events.el: chronometrist-eventsel.
|
||||||
* chronometrist-migrate.el: chronometrist-migrateel.
|
* chronometrist-migrate.el: chronometrist-migrateel.
|
||||||
* chronometrist-plist-pp.el: chronometrist-plist-ppel.
|
* chronometrist-plist-pp.el: chronometrist-plist-ppel.
|
||||||
* chronometrist-queries.el: chronometrist-queriesel.
|
|
||||||
* chronometrist-report-custom.el: chronometrist-report-customel.
|
|
||||||
* chronometrist-report.el: chronometrist-reportel.
|
* chronometrist-report.el: chronometrist-reportel.
|
||||||
* chronometrist-key-values.el: chronometrist-key-valuesel.
|
* chronometrist-key-values.el: chronometrist-key-valuesel.
|
||||||
* chronometrist-statistics-custom.el: chronometrist-statistics-customel.
|
|
||||||
* chronometrist-statistics.el: chronometrist-statisticsel.
|
* chronometrist-statistics.el: chronometrist-statisticsel.
|
||||||
* chronometrist-time.el: chronometrist-timeel.
|
* chronometrist-time.el: chronometrist-timeel.
|
||||||
* chronometrist-timer.el: chronometrist-timerel.
|
* chronometrist-timer.el: chronometrist-timerel.
|
||||||
|
@ -518,6 +563,34 @@ Used for goals (chronometrist-goals-list, chronometrist-get-goal) - minutes seem
|
||||||
Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
|
Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
|
||||||
@end itemize
|
@end itemize
|
||||||
|
|
||||||
|
@node chronometrist-backendel
|
||||||
|
@section chronometrist-backend.el
|
||||||
|
|
||||||
|
@enumerate
|
||||||
|
@item
|
||||||
|
Class - chronometrist-backend ()
|
||||||
|
@item
|
||||||
|
Variable - chronometrist-backend-current
|
||||||
|
@item
|
||||||
|
Variable - chronometrist-backends
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-to-hash (backend table)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-from-hash (backend table)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-open-file (backend)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-latest-record (backend)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-current-task (backend)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-create-file (backend)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-new-record (backend plist)
|
||||||
|
@item
|
||||||
|
Generic Function - chronometrist-backend-replace-last (backend plist)
|
||||||
|
@end enumerate
|
||||||
|
|
||||||
@node chronometrist-commonel
|
@node chronometrist-commonel
|
||||||
@section chronometrist-common.el
|
@section chronometrist-common.el
|
||||||
|
|
||||||
|
@ -568,24 +641,6 @@ ts -> ts
|
||||||
@end itemize
|
@end itemize
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
||||||
@node chronometrist-customel
|
|
||||||
@section chronometrist-custom.el
|
|
||||||
|
|
||||||
@enumerate
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-file
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-buffer-name
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-hide-cursor
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-update-interval
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-activity-indicator
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-day-start-time
|
|
||||||
@end enumerate
|
|
||||||
|
|
||||||
@node chronometrist-diary-viewel
|
@node chronometrist-diary-viewel
|
||||||
@section chronometrist-diary-view.el
|
@section chronometrist-diary-view.el
|
||||||
|
|
||||||
|
@ -611,6 +666,18 @@ Command - chronometrist-diary-view (&optional date)
|
||||||
|
|
||||||
@enumerate
|
@enumerate
|
||||||
@item
|
@item
|
||||||
|
Custom variable - chronometrist-file
|
||||||
|
@item
|
||||||
|
Custom variable - chronometrist-buffer-name
|
||||||
|
@item
|
||||||
|
Custom variable - chronometrist-hide-cursor
|
||||||
|
@item
|
||||||
|
Custom variable - chronometrist-update-interval
|
||||||
|
@item
|
||||||
|
Custom variable - chronometrist-activity-indicator
|
||||||
|
@item
|
||||||
|
Custom variable - chronometrist-day-start-time
|
||||||
|
@item
|
||||||
Internal Variable - chronometrist--task-history
|
Internal Variable - chronometrist--task-history
|
||||||
@item
|
@item
|
||||||
Internal Variable - chronometrist--point
|
Internal Variable - chronometrist--point
|
||||||
|
@ -758,36 +825,8 @@ Function - chronometrist-plist-pp-to-string (object)
|
||||||
Function - chronometrist-plist-pp (object &optional stream)
|
Function - chronometrist-plist-pp (object &optional stream)
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
||||||
@node chronometrist-queriesel
|
@node chronometrist-reportel
|
||||||
@section chronometrist-queries.el
|
@section chronometrist-report.el
|
||||||
|
|
||||||
@enumerate
|
|
||||||
@item
|
|
||||||
Function - chronometrist-last ()
|
|
||||||
@itemize
|
|
||||||
@item
|
|
||||||
-> plist
|
|
||||||
@end itemize
|
|
||||||
@item
|
|
||||||
Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
|
|
||||||
@itemize
|
|
||||||
@item
|
|
||||||
String &optional ts -> seconds
|
|
||||||
@end itemize
|
|
||||||
@item
|
|
||||||
Function - chronometrist-active-time-one-day (&optional ts)
|
|
||||||
@itemize
|
|
||||||
@item
|
|
||||||
&optional ts -> seconds
|
|
||||||
@end itemize
|
|
||||||
@item
|
|
||||||
Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
|
||||||
@item
|
|
||||||
Function - chronometrist-task-events-in-day (task ts)
|
|
||||||
@end enumerate
|
|
||||||
|
|
||||||
@node chronometrist-report-customel
|
|
||||||
@section chronometrist-report-custom.el
|
|
||||||
|
|
||||||
@enumerate
|
@enumerate
|
||||||
@item
|
@item
|
||||||
|
@ -796,12 +835,6 @@ Custom variable - chronometrist-report-buffer-name
|
||||||
Custom variable - chronometrist-report-week-start-day
|
Custom variable - chronometrist-report-week-start-day
|
||||||
@item
|
@item
|
||||||
Custom variable - chronometrist-report-weekday-number-alist
|
Custom variable - chronometrist-report-weekday-number-alist
|
||||||
@end enumerate
|
|
||||||
|
|
||||||
@node chronometrist-reportel
|
|
||||||
@section chronometrist-report.el
|
|
||||||
|
|
||||||
@enumerate
|
|
||||||
@item
|
@item
|
||||||
Internal Variable - chronometrist-report--ui-date
|
Internal Variable - chronometrist-report--ui-date
|
||||||
@item
|
@item
|
||||||
|
@ -912,19 +945,13 @@ Function - chronometrist-skip-query-prompt (task)
|
||||||
Function - chronometrist-skip-query-reset (@math{_task})
|
Function - chronometrist-skip-query-reset (@math{_task})
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
||||||
@node chronometrist-statistics-customel
|
|
||||||
@section chronometrist-statistics-custom.el
|
|
||||||
|
|
||||||
@enumerate
|
|
||||||
@item
|
|
||||||
Custom variable - chronometrist-statistics-buffer-name
|
|
||||||
@end enumerate
|
|
||||||
|
|
||||||
@node chronometrist-statisticsel
|
@node chronometrist-statisticsel
|
||||||
@section chronometrist-statistics.el
|
@section chronometrist-statistics.el
|
||||||
|
|
||||||
@enumerate
|
@enumerate
|
||||||
@item
|
@item
|
||||||
|
Custom variable - chronometrist-statistics-buffer-name
|
||||||
|
@item
|
||||||
Internal Variable - chronometrist-statistics--ui-state
|
Internal Variable - chronometrist-statistics--ui-state
|
||||||
@item
|
@item
|
||||||
Internal Variable - chronometrist-statistics--point
|
Internal Variable - chronometrist-statistics--point
|
||||||
|
@ -1079,31 +1106,59 @@ Function - chronometrist-goal-on-file-change ()
|
||||||
|
|
||||||
@enumerate
|
@enumerate
|
||||||
@item
|
@item
|
||||||
|
Class - chronometrist-sexp
|
||||||
|
@item
|
||||||
|
Object - chronometrist-sexp
|
||||||
|
@item
|
||||||
|
Custom variable - chronometrist-sexp-pretty-print-function
|
||||||
|
@item
|
||||||
Macro - chronometrist-sexp-in-file (file &rest body)
|
Macro - chronometrist-sexp-in-file (file &rest body)
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-open-log ()
|
Method - chronometrist-to-hash ((backend chronometrist-sexp) table)
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
|
Method - chronometrist-from-hash ((backend chronometrist-sexp) table)
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
|
Method - chronometrist-open-log ((backend chronometrist-sexp))
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-last ()
|
Method - chronometrist-last ((backend chronometrist-sexp))
|
||||||
@itemize
|
@itemize
|
||||||
@item
|
@item
|
||||||
-> plist
|
-> plist
|
||||||
@end itemize
|
@end itemize
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-current-task ()
|
Method - chronometrist-current-task ((backend chronometrist-sexp))
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-events-populate ()
|
Method - chronometrist-create-file ((backend chronometrist-sexp))
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-create-file ()
|
Method - chronometrist-backend-new-record (((backend chronometrist-sexp)) plist)
|
||||||
@item
|
|
||||||
Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
|
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-delete-list (&optional arg)
|
Function - chronometrist-sexp-delete-list (&optional arg)
|
||||||
@item
|
@item
|
||||||
Function - chronometrist-sexp-replace-last (plist)
|
Method - chronometrist-replace-last (((backend chronometrist-sexp)) plist)
|
||||||
|
@item
|
||||||
|
Method - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
|
||||||
|
@itemize
|
||||||
|
@item
|
||||||
|
String &optional ts -> seconds
|
||||||
|
@end itemize
|
||||||
|
@item
|
||||||
|
Method - chronometrist-active-time-one-day (&optional ts)
|
||||||
|
@itemize
|
||||||
|
@item
|
||||||
|
&optional ts -> seconds
|
||||||
|
@end itemize
|
||||||
|
@item
|
||||||
|
Method - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
||||||
|
@itemize
|
||||||
|
@item
|
||||||
|
String &optional table -> integer
|
||||||
|
@end itemize
|
||||||
|
@item
|
||||||
|
Method - chronometrist-task-events-in-day (task ts)
|
||||||
|
@itemize
|
||||||
|
@item
|
||||||
|
String ts -> events
|
||||||
|
@end itemize
|
||||||
@item
|
@item
|
||||||
Command - chronometrist-sexp-reindent-buffer ()
|
Command - chronometrist-sexp-reindent-buffer ()
|
||||||
@end enumerate
|
@end enumerate
|
||||||
|
|
252
doc/manual.org
252
doc/manual.org
|
@ -16,7 +16,9 @@ All of these are optional, but recommended for the best experience.
|
||||||
From the project root, you can now run
|
From the project root, you can now run
|
||||||
1. =cask= to install the project dependencies in a sandbox
|
1. =cask= to install the project dependencies in a sandbox
|
||||||
2. =cask exec buttercup -L . --traceback pretty= to run tests.
|
2. =cask exec buttercup -L . --traceback pretty= to run tests.
|
||||||
|
** How to create a new backend
|
||||||
|
1. Subclass =chronometrist-backend= to define your backend.
|
||||||
|
2. Create methods for the eight generic functions in =chronometrist-backend.el=
|
||||||
* Explanation
|
* Explanation
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:DESCRIPTION: The design, the implementation, and a little history
|
:DESCRIPTION: The design, the implementation, and a little history
|
||||||
|
@ -30,14 +32,14 @@ All of these are optional, but recommended for the best experience.
|
||||||
2. Incentivize use
|
2. Incentivize use
|
||||||
* Hooks allow the time tracker to automate tasks and become a useful part of your workflow
|
* Hooks allow the time tracker to automate tasks and become a useful part of your workflow
|
||||||
3. Make it easy to edit data using existing, familiar tools
|
3. Make it easy to edit data using existing, familiar tools
|
||||||
* We don't use an SQL database, where changing a single field is tricky [1]
|
* We don't use an SQL database, where changing a single field is tricky [fn:1]
|
||||||
* We use a text file containing s-expressions (easy for humans to read and write)
|
* We use a text file containing s-expressions (easy for humans to read and write)
|
||||||
* We use ISO-8601 for timestamps (easy for humans to read and write) rather than UNIX epoch time
|
* We use ISO-8601 for timestamps (easy for humans to read and write) rather than UNIX epoch time
|
||||||
4. Reduce human errors in tracking
|
4. Reduce human errors in tracking
|
||||||
5. Have a useful, informative, interactive interface
|
5. Have a useful, informative, interactive interface
|
||||||
6. Support mouse and keyboard use equally
|
6. Support mouse and keyboard use equally
|
||||||
|
|
||||||
[1] I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?
|
[fn:1] I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?
|
||||||
|
|
||||||
** Terminology
|
** Terminology
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
@ -82,10 +84,10 @@ One of the earliest 'optimizations' of great importance turned out to simply be
|
||||||
+ It was fixed in v0.2.2 by making the watch creation conditional, using [[file:../elisp/chronometrist-common.el::defvar chronometrist--fs-watch ][=chronometrist--fs-watch=]] to store the watch object.
|
+ It was fixed in v0.2.2 by making the watch creation conditional, using [[file:../elisp/chronometrist-common.el::defvar chronometrist--fs-watch ][=chronometrist--fs-watch=]] to store the watch object.
|
||||||
|
|
||||||
**** Preserve state
|
**** Preserve state
|
||||||
The next one was released in v0.5. Till then, any time the [[file:../elisp/chronometrist-custom.el::defcustom chronometrist-file (][=chronometrist-file=]] was modified, we'd clear the [[file:../elisp/chronometrist-events.el::defvar chronometrist-events (][=chronometrist-events=]] hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:1] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[* Midnight-spanning events][midnight-spanning events]] is the real performance killer.
|
The next one was released in v0.5. Till then, any time the [[file:../elisp/chronometrist-custom.el::defcustom chronometrist-file (][=chronometrist-file=]] was modified, we'd clear the [[file:../elisp/chronometrist-events.el::defvar chronometrist-events (][=chronometrist-events=]] hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:2] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[* Midnight-spanning events][midnight-spanning events]] is the real performance killer.
|
||||||
|
|
||||||
After the optimization...
|
After the optimization...
|
||||||
1. Two backend functions ([[file:../elisp/chronometrist-sexp.el::cl-defun chronometrist-sexp-new (][=chronometrist-sexp-new=]] and [[file:../elisp/chronometrist-sexp.el::defun chronometrist-sexp-replace-last (][=chronometrist-sexp-replace-last=]]) were modified to set a flag ([[file:../elisp/chronometrist.el::defvar chronometrist--inhibit-read-p ][=chronometrist--inhibit-read-p=]]) before saving the file.
|
1. Two backend functions ([[file:../elisp/chronometrist-sexp.el::cl-defmethod chronometrist-new (][=chronometrist-new=]] and [[file:../elisp/chronometrist-sexp.el::cl-defmethod chronometrist-replace-last (][=chronometrist-replace-last=]]) were modified to set a flag ([[file:../elisp/chronometrist.el::defvar chronometrist--inhibit-read-p ][=chronometrist--inhibit-read-p=]]) before saving the file.
|
||||||
2. If this flag is non-nil, [[file:../elisp/chronometrist.el::defun chronometrist-refresh-file (][=chronometrist-refresh-file=]] skips the expensive calls to =chronometrist-events-populate=, =chronometrist-tasks-from-table=, and =chronometrist-tags-history-populate=, and resets the flag.
|
2. If this flag is non-nil, [[file:../elisp/chronometrist.el::defun chronometrist-refresh-file (][=chronometrist-refresh-file=]] skips the expensive calls to =chronometrist-events-populate=, =chronometrist-tasks-from-table=, and =chronometrist-tags-history-populate=, and resets the flag.
|
||||||
3. Instead, the aforementioned backend functions modify the relevant variables - =chronometrist-events=, =chronometrist-task-list=, and =chronometrist-tags-history= - via...
|
3. Instead, the aforementioned backend functions modify the relevant variables - =chronometrist-events=, =chronometrist-task-list=, and =chronometrist-tags-history= - via...
|
||||||
* =chronometrist-events-add= / =chronometrist-events-replace-last=
|
* =chronometrist-events-add= / =chronometrist-events-replace-last=
|
||||||
|
@ -94,7 +96,29 @@ After the optimization...
|
||||||
|
|
||||||
There are still some operations which [[file:../elisp/chronometrist.el::defun chronometrist-refresh-file (][=chronometrist-refresh-file=]] runs unconditionally - which is to say there is scope for further optimization, if or when required.
|
There are still some operations which [[file:../elisp/chronometrist.el::defun chronometrist-refresh-file (][=chronometrist-refresh-file=]] runs unconditionally - which is to say there is scope for further optimization, if or when required.
|
||||||
|
|
||||||
[fn:1] As indicated by exploratory work in the =parsimonious-reading= branch, where I made a loop to only =read= and collect s-expressions from the file. It was near-instant...until I added event splitting to it.
|
[fn:2] As indicated by exploratory work in the =parsimonious-reading= branch, where I made a loop to only =read= and collect s-expressions from the file. It was near-instant...until I added event splitting to it.
|
||||||
|
|
||||||
|
*** Backends
|
||||||
|
Chronometrist can currently store data in two ways -
|
||||||
|
1. as plists in a plain text file, using the s-expression backend.
|
||||||
|
2. as an SQL database, using the sqlite3 backend.
|
||||||
|
|
||||||
|
Each of these has areas they excel in - plain text files are VCS-friendly, permit the use of tools like =grep=, and are easy to make small, one-off edits in. SQL databases support a standard and popular query language, ACID guarantees, and are easy to make sweeping changes in.
|
||||||
|
|
||||||
|
To try and get the best of both, two approaches were considered -
|
||||||
|
1. Support for multiple simultaneous backends
|
||||||
|
+ =chronometrist-backend-list= could be a custom variable, containing a list of enabled backends
|
||||||
|
+ A function like =(call-with-backends generic-function &rest args)= could be created, which calls the generic-function with each backend as the first argument, and use this whenever Chronometrist makes a change to the files.
|
||||||
|
+ When the user makes a change to one of the files outside of Chronometrist, we detect the change (via filesystem watchers) and propagate it to the other backends.
|
||||||
|
Consequently, a user can make quick, one-off edits to the SQL database by editing a text file, or make sweeping, structure-aware changes to the text file using SQL statements.
|
||||||
|
2. SQL as the main backend, with viewing/editing UI + auto-export
|
||||||
|
1. Write a 'record viewer/editor' for Chronometrist records. Basically, it reads in a record from the SQL database and displays it in an editable buffer. Forward/backward commands would be available, like in =chronometrist-report= and =chronometrist-statistics=. The record could be represented as anything - CSV, Org markup, Lisp data, etc. The user could edit and run a command to accept the changes, which would change the database.
|
||||||
|
2. Automatically export from SQL to a plain text (or other) format, whenever there is a change in the database. (probably using hooks)
|
||||||
|
These two put together result in the viewing and editing of individual records being easier, and the plain text format can be placed in a VCS, without dealing with any synchronization issues.
|
||||||
|
|
||||||
|
Backends are defined as subclasses of the class =chronometrist-backend= [fn:3].
|
||||||
|
|
||||||
|
[fn:3] I would have liked to define them as instances of =chronometrist-backend=, but EIEIO, unlike CLOS, does not support EQL specialization.
|
||||||
** Midnight-spanning events
|
** Midnight-spanning events
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:DESCRIPTION: Events starting on one day and ending on another
|
:DESCRIPTION: Events starting on one day and ending on another
|
||||||
|
@ -153,11 +177,11 @@ A quick description, starting from the first time [[file:../elisp/chronometrist-
|
||||||
:END:
|
:END:
|
||||||
[[file:../elisp/chronometrist-key-values.el][chronometrist-key-values.el]] deals with adding additional information to events, in the form of key-values and tags.
|
[[file:../elisp/chronometrist-key-values.el][chronometrist-key-values.el]] deals with adding additional information to events, in the form of key-values and tags.
|
||||||
|
|
||||||
Key-values are stored as plist keywords and values. The user can add any keywords except =:name=, =:tags=, =:start=, and =:stop=. [fn:2] Values can be any readable Lisp values.
|
Key-values are stored as plist keywords and values. The user can add any keywords except =:name=, =:tags=, =:start=, and =:stop=. [fn:4] Values can be any readable Lisp values.
|
||||||
|
|
||||||
Similarly, tags are stored using a =:tags (<tag>*)= keyword-value pair. The tags themselves (the elements of the list) can be any readable Lisp value.
|
Similarly, tags are stored using a =:tags (<tag>*)= keyword-value pair. The tags themselves (the elements of the list) can be any readable Lisp value.
|
||||||
|
|
||||||
[fn:2] To remove this restriction, I had briefly considered making a keyword called =:user=, whose value would be another plist containing all user-defined keyword-values. But in practice, this hasn't been a big enough issue yet to justify the work.
|
[fn:4] To remove this restriction, I had briefly considered making a keyword called =:user=, whose value would be another plist containing all user-defined keyword-values. But in practice, this hasn't been a big enough issue yet to justify the work.
|
||||||
*** User input
|
*** User input
|
||||||
The entry points are [[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-add (][=chronometrist-kv-add=]] and [[file:../elisp/chronometrist-key-values.el::defun chronometrist-tags-add (][=chronometrist-tags-add=]]. The user adds these to the desired hooks, and they prompt the user for tags/key-values.
|
The entry points are [[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-add (][=chronometrist-kv-add=]] and [[file:../elisp/chronometrist-key-values.el::defun chronometrist-tags-add (][=chronometrist-tags-add=]]. The user adds these to the desired hooks, and they prompt the user for tags/key-values.
|
||||||
|
|
||||||
|
@ -207,6 +231,19 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
(hours minute seconds)
|
(hours minute seconds)
|
||||||
* Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
|
* Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
|
||||||
|
|
||||||
|
** chronometrist-backend.el
|
||||||
|
1. Class - chronometrist-backend ()
|
||||||
|
2. Variable - chronometrist-backend-current
|
||||||
|
3. Variable - chronometrist-backends
|
||||||
|
4. Generic Function - chronometrist-backend-to-hash (backend table)
|
||||||
|
5. Generic Function - chronometrist-backend-from-hash (backend table)
|
||||||
|
6. Generic Function - chronometrist-backend-open-file (backend)
|
||||||
|
7. Generic Function - chronometrist-backend-latest-record (backend)
|
||||||
|
8. Generic Function - chronometrist-backend-current-task (backend)
|
||||||
|
9. Generic Function - chronometrist-backend-create-file (backend)
|
||||||
|
10. Generic Function - chronometrist-backend-new-record (backend plist)
|
||||||
|
11. Generic Function - chronometrist-backend-replace-last (backend plist)
|
||||||
|
|
||||||
** chronometrist-common.el
|
** chronometrist-common.el
|
||||||
1. Variable - chronometrist-empty-time-string
|
1. Variable - chronometrist-empty-time-string
|
||||||
2. Variable - chronometrist-date-re
|
2. Variable - chronometrist-date-re
|
||||||
|
@ -227,14 +264,6 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
14. Function - chronometrist-previous-week-start (ts)
|
14. Function - chronometrist-previous-week-start (ts)
|
||||||
* ts -> ts
|
* ts -> ts
|
||||||
|
|
||||||
** chronometrist-custom.el
|
|
||||||
1. Custom variable - chronometrist-file
|
|
||||||
2. Custom variable - chronometrist-buffer-name
|
|
||||||
3. Custom variable - chronometrist-hide-cursor
|
|
||||||
4. Custom variable - chronometrist-update-interval
|
|
||||||
5. Custom variable - chronometrist-activity-indicator
|
|
||||||
6. Custom variable - chronometrist-day-start-time
|
|
||||||
|
|
||||||
** chronometrist-diary-view.el
|
** chronometrist-diary-view.el
|
||||||
1. Variable - chronometrist-diary-buffer-name
|
1. Variable - chronometrist-diary-buffer-name
|
||||||
2. Internal Variable - chronometrist-diary--current-date
|
2. Internal Variable - chronometrist-diary--current-date
|
||||||
|
@ -245,41 +274,47 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
7. Command - chronometrist-diary-view (&optional date)
|
7. Command - chronometrist-diary-view (&optional date)
|
||||||
|
|
||||||
** chronometrist.el
|
** chronometrist.el
|
||||||
1. Internal Variable - chronometrist--task-history
|
1. Custom variable - chronometrist-file
|
||||||
2. Internal Variable - chronometrist--point
|
2. Custom variable - chronometrist-buffer-name
|
||||||
3. Internal Variable - chronometrist--inhibit-read-p
|
3. Custom variable - chronometrist-hide-cursor
|
||||||
4. Keymap - chronometrist-mode-map
|
4. Custom variable - chronometrist-update-interval
|
||||||
5. Command - chronometrist-open-log (&optional button)
|
5. Custom variable - chronometrist-activity-indicator
|
||||||
6. Function - chronometrist-common-create-file ()
|
6. Custom variable - chronometrist-day-start-time
|
||||||
7. Function - chronometrist-task-active? (task)
|
7. Internal Variable - chronometrist--task-history
|
||||||
|
8. Internal Variable - chronometrist--point
|
||||||
|
9. Internal Variable - chronometrist--inhibit-read-p
|
||||||
|
10. Keymap - chronometrist-mode-map
|
||||||
|
11. Command - chronometrist-open-log (&optional button)
|
||||||
|
12. Function - chronometrist-common-create-file ()
|
||||||
|
13. Function - chronometrist-task-active? (task)
|
||||||
* String -> Boolean
|
* String -> Boolean
|
||||||
8. Function - chronometrist-use-goals? ()
|
14. Function - chronometrist-use-goals? ()
|
||||||
9. Function - chronometrist-activity-indicator ()
|
15. Function - chronometrist-activity-indicator ()
|
||||||
10. Function - chronometrist-entries ()
|
16. Function - chronometrist-entries ()
|
||||||
11. Function - chronometrist-task-at-point ()
|
17. Function - chronometrist-task-at-point ()
|
||||||
12. Function - chronometrist-goto-last-task ()
|
18. Function - chronometrist-goto-last-task ()
|
||||||
13. Function - chronometrist-print-keybind (command &optional description firstonly)
|
19. Function - chronometrist-print-keybind (command &optional description firstonly)
|
||||||
14. Function - chronometrist-print-non-tabular ()
|
20. Function - chronometrist-print-non-tabular ()
|
||||||
15. Function - chronometrist-goto-nth-task (n)
|
21. Function - chronometrist-goto-nth-task (n)
|
||||||
16. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
|
22. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
|
||||||
17. Function - chronometrist-refresh-file (fs-event)
|
23. Function - chronometrist-refresh-file (fs-event)
|
||||||
18. Command - chronometrist-query-stop ()
|
24. Command - chronometrist-query-stop ()
|
||||||
19. Command - chronometrist-in (task &optional _prefix)
|
25. Command - chronometrist-in (task &optional _prefix)
|
||||||
20. Command - chronometrist-out (&optional _prefix)
|
26. Command - chronometrist-out (&optional _prefix)
|
||||||
21. Variable - chronometrist-before-in-functions
|
27. Variable - chronometrist-before-in-functions
|
||||||
22. Variable - chronometrist-after-in-functions
|
28. Variable - chronometrist-after-in-functions
|
||||||
23. Variable - chronometrist-before-out-functions
|
29. Variable - chronometrist-before-out-functions
|
||||||
24. Variable - chronometrist-after-out-functions
|
30. Variable - chronometrist-after-out-functions
|
||||||
25. Function - chronometrist-run-functions-and-clock-in (task)
|
31. Function - chronometrist-run-functions-and-clock-in (task)
|
||||||
26. Function - chronometrist-run-functions-and-clock-out (task)
|
32. Function - chronometrist-run-functions-and-clock-out (task)
|
||||||
27. Keymap - chronometrist-mode-map
|
33. Keymap - chronometrist-mode-map
|
||||||
28. Major Mode - chronometrist-mode
|
34. Major Mode - chronometrist-mode
|
||||||
29. Function - chronometrist-toggle-task-button (button)
|
35. Function - chronometrist-toggle-task-button (button)
|
||||||
30. Function - chronometrist-add-new-task-button (button)
|
36. Function - chronometrist-add-new-task-button (button)
|
||||||
31. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
|
37. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
|
||||||
32. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
|
38. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
|
||||||
33. Command - chronometrist-add-new-task ()
|
39. Command - chronometrist-add-new-task ()
|
||||||
34. Command - chronometrist (&optional arg)
|
40. Command - chronometrist (&optional arg)
|
||||||
|
|
||||||
** chronometrist-events.el
|
** chronometrist-events.el
|
||||||
1. Variable - chronometrist-events
|
1. Variable - chronometrist-events
|
||||||
|
@ -311,39 +346,27 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
6. Function - chronometrist-plist-pp-to-string (object)
|
6. Function - chronometrist-plist-pp-to-string (object)
|
||||||
7. Function - chronometrist-plist-pp (object &optional stream)
|
7. Function - chronometrist-plist-pp (object &optional stream)
|
||||||
|
|
||||||
** chronometrist-queries.el
|
** chronometrist-report.el
|
||||||
1. Function - chronometrist-last ()
|
|
||||||
* -> plist
|
|
||||||
2. Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
|
|
||||||
* String &optional ts -> seconds
|
|
||||||
3. Function - chronometrist-active-time-one-day (&optional ts)
|
|
||||||
* &optional ts -> seconds
|
|
||||||
4. Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
|
||||||
5. Function - chronometrist-task-events-in-day (task ts)
|
|
||||||
|
|
||||||
** chronometrist-report-custom.el
|
|
||||||
1. Custom variable - chronometrist-report-buffer-name
|
1. Custom variable - chronometrist-report-buffer-name
|
||||||
2. Custom variable - chronometrist-report-week-start-day
|
2. Custom variable - chronometrist-report-week-start-day
|
||||||
3. Custom variable - chronometrist-report-weekday-number-alist
|
3. Custom variable - chronometrist-report-weekday-number-alist
|
||||||
|
4. Internal Variable - chronometrist-report--ui-date
|
||||||
** chronometrist-report.el
|
5. Internal Variable - chronometrist-report--ui-week-dates
|
||||||
1. Internal Variable - chronometrist-report--ui-date
|
6. Internal Variable - chronometrist-report--point
|
||||||
2. Internal Variable - chronometrist-report--ui-week-dates
|
7. Function - chronometrist-report-date ()
|
||||||
3. Internal Variable - chronometrist-report--point
|
8. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
|
||||||
4. Function - chronometrist-report-date ()
|
* ts-1 -> (ts-1 ... ts-7)
|
||||||
5. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
|
9. Function - chronometrist-report-date->week-dates ()
|
||||||
* ts-1 -> (ts-1 ... ts-7)
|
10. Function - chronometrist-report-entries ()
|
||||||
6. Function - chronometrist-report-date->week-dates ()
|
11. Function - chronometrist-report-print-keybind (command &optional description firstonly)
|
||||||
7. Function - chronometrist-report-entries ()
|
12. Function - chronometrist-report-print-non-tabular ()
|
||||||
8. Function - chronometrist-report-print-keybind (command &optional description firstonly)
|
13. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
|
||||||
9. Function - chronometrist-report-print-non-tabular ()
|
14. Function - chronometrist-report-refresh-file (_fs-event)
|
||||||
10. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
|
15. Keymap - chronometrist-report-mode-map
|
||||||
11. Function - chronometrist-report-refresh-file (_fs-event)
|
16. Major Mode - chronometrist-report-mode
|
||||||
12. Keymap - chronometrist-report-mode-map
|
17. Function - chronometrist-report (&optional keep-date)
|
||||||
13. Major Mode - chronometrist-report-mode
|
18. Function - chronometrist-report-previous-week (arg)
|
||||||
14. Function - chronometrist-report (&optional keep-date)
|
19. Function - chronometrist-report-next-week (arg)
|
||||||
15. Function - chronometrist-report-previous-week (arg)
|
|
||||||
16. Function - chronometrist-report-next-week (arg)
|
|
||||||
|
|
||||||
** chronometrist-key-values.el
|
** chronometrist-key-values.el
|
||||||
1. Internal Variable - chronometrist--tag-suggestions
|
1. Internal Variable - chronometrist--tag-suggestions
|
||||||
|
@ -380,24 +403,22 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
32. Function - chronometrist-skip-query-prompt (task)
|
32. Function - chronometrist-skip-query-prompt (task)
|
||||||
33. Function - chronometrist-skip-query-reset (_task)
|
33. Function - chronometrist-skip-query-reset (_task)
|
||||||
|
|
||||||
** chronometrist-statistics-custom.el
|
|
||||||
1. Custom variable - chronometrist-statistics-buffer-name
|
|
||||||
|
|
||||||
** chronometrist-statistics.el
|
** chronometrist-statistics.el
|
||||||
1. Internal Variable - chronometrist-statistics--ui-state
|
1. Custom variable - chronometrist-statistics-buffer-name
|
||||||
2. Internal Variable - chronometrist-statistics--point
|
2. Internal Variable - chronometrist-statistics--ui-state
|
||||||
3. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
|
3. Internal Variable - chronometrist-statistics--point
|
||||||
* string &optional hash-table -> seconds
|
4. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
|
||||||
4. Function - chronometrist-statistics-entries-internal (table)
|
* string &optional hash-table -> seconds
|
||||||
5. Function - chronometrist-statistics-entries ()
|
5. Function - chronometrist-statistics-entries-internal (table)
|
||||||
6. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
|
6. Function - chronometrist-statistics-entries ()
|
||||||
7. Function - chronometrist-statistics-print-non-tabular ()
|
7. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
|
||||||
8. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
|
8. Function - chronometrist-statistics-print-non-tabular ()
|
||||||
9. Keymap - chronometrist-statistics-mode-map
|
9. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
|
||||||
10. Major Mode - chronometrist-statistics-mode
|
10. Keymap - chronometrist-statistics-mode-map
|
||||||
11. Command - chronometrist-statistics (&optional preserve-state)
|
11. Major Mode - chronometrist-statistics-mode
|
||||||
12. Command - chronometrist-statistics-previous-range (arg)
|
12. Command - chronometrist-statistics (&optional preserve-state)
|
||||||
13. Command - chronometrist-statistics-next-range (arg)
|
13. Command - chronometrist-statistics-previous-range (arg)
|
||||||
|
14. Command - chronometrist-statistics-next-range (arg)
|
||||||
|
|
||||||
** chronometrist-time.el
|
** chronometrist-time.el
|
||||||
1. Function - chronometrist-iso-timestamp->ts (timestamp)
|
1. Function - chronometrist-iso-timestamp->ts (timestamp)
|
||||||
|
@ -444,20 +465,29 @@ Each of these has a corresponding function to clear it and fill it with values -
|
||||||
13. Function - chronometrist-goal-on-file-change ()
|
13. Function - chronometrist-goal-on-file-change ()
|
||||||
|
|
||||||
** chronometrist-sexp
|
** chronometrist-sexp
|
||||||
1. Custom variable - chronometrist-sexp-pretty-print-function
|
1. Class - chronometrist-sexp
|
||||||
2. Macro - chronometrist-sexp-in-file (file &rest body)
|
2. Object - chronometrist-sexp
|
||||||
3. Function - chronometrist-sexp-open-log ()
|
3. Custom variable - chronometrist-sexp-pretty-print-function
|
||||||
4. Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
|
4. Macro - chronometrist-sexp-in-file (file &rest body)
|
||||||
5. Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
|
5. Method - chronometrist-to-hash ((backend chronometrist-sexp) table)
|
||||||
6. Function - chronometrist-sexp-last ()
|
6. Method - chronometrist-from-hash ((backend chronometrist-sexp) table)
|
||||||
* -> plist
|
7. Method - chronometrist-open-log ((backend chronometrist-sexp))
|
||||||
7. Function - chronometrist-sexp-current-task ()
|
8. Method - chronometrist-last ((backend chronometrist-sexp))
|
||||||
8. Function - chronometrist-sexp-events-populate ()
|
* -> plist
|
||||||
9. Function - chronometrist-sexp-create-file ()
|
9. Method - chronometrist-current-task ((backend chronometrist-sexp))
|
||||||
10. Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
|
10. Method - chronometrist-create-file ((backend chronometrist-sexp))
|
||||||
11. Function - chronometrist-sexp-delete-list (&optional arg)
|
11. Method - chronometrist-backend-new-record (((backend chronometrist-sexp)) plist)
|
||||||
12. Function - chronometrist-sexp-replace-last (plist)
|
12. Function - chronometrist-sexp-delete-list (&optional arg)
|
||||||
13. Command - chronometrist-sexp-reindent-buffer ()
|
13. Method - chronometrist-replace-last (((backend chronometrist-sexp)) plist)
|
||||||
|
14. Method - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
|
||||||
|
* String &optional ts -> seconds
|
||||||
|
15. Method - chronometrist-active-time-one-day (&optional ts)
|
||||||
|
* &optional ts -> seconds
|
||||||
|
16. Method - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
||||||
|
* String &optional table -> integer
|
||||||
|
17. Method - chronometrist-task-events-in-day (task ts)
|
||||||
|
* String ts -> events
|
||||||
|
18. Command - chronometrist-sexp-reindent-buffer ()
|
||||||
|
|
||||||
# Local Variables:
|
# Local Variables:
|
||||||
# org-link-file-path-type: relative
|
# org-link-file-path-type: relative
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
;;; chronometrist-backend.el --- backend-related definitions for Chronometrist
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;;
|
||||||
|
|
||||||
|
(require 'cl)
|
||||||
|
(require 'eieio)
|
||||||
|
|
||||||
|
(defvar chronometrist-file)
|
||||||
|
(declare-function chronometrist-file-path "chronometrist-common.el")
|
||||||
|
|
||||||
|
(defclass chronometrist-backend ()
|
||||||
|
((name
|
||||||
|
:initarg :name
|
||||||
|
:type string
|
||||||
|
:documentation "The name of the backend."
|
||||||
|
:initform "")
|
||||||
|
(ext
|
||||||
|
:initarg :ext
|
||||||
|
:type string
|
||||||
|
:documentation
|
||||||
|
"The extension used by a file of this backend, without a leading period."
|
||||||
|
:initform "")
|
||||||
|
(file :initarg :file)))
|
||||||
|
|
||||||
|
(cl-defmethod initialize-instance :after ((backend chronometrist-backend) &rest)
|
||||||
|
(setf (oref backend :file)
|
||||||
|
(format "%s.%s" chronometrist-file (oref backend :ext))))
|
||||||
|
|
||||||
|
(defvar chronometrist-backend-current nil "The current backend in use.")
|
||||||
|
(defvar chronometrist-backends nil "List of enabled backends.")
|
||||||
|
|
||||||
|
;; # Migration #
|
||||||
|
(cl-defgeneric chronometrist-backend-to-hash (backend hash-table)
|
||||||
|
"Clear HASH-TABLE and fill it using BACKEND.
|
||||||
|
Return final number of intervals read from backend, or nil if
|
||||||
|
there were none.")
|
||||||
|
;; If the file for BACKEND exists and is not empty, signal an error or
|
||||||
|
;; prompt the user?
|
||||||
|
(cl-defgeneric chronometrist-backend-from-hash (backend hash-table)
|
||||||
|
"Fill BACKEND using HASH-TABLE.")
|
||||||
|
|
||||||
|
;; # Queries #
|
||||||
|
(cl-defgeneric chronometrist-backend-open-file (backend)
|
||||||
|
"Open the storage file associated with BACKEND.")
|
||||||
|
(cl-defgeneric chronometrist-backend-latest-record (backend)
|
||||||
|
"Return the latest record from BACKEND.")
|
||||||
|
(cl-defgeneric chronometrist-backend-current-task (backend)
|
||||||
|
"Return the name of the currently clocked-in task, or nil if not clocked in.")
|
||||||
|
|
||||||
|
(cl-defgeneric chronometrist-backend-task-intervals (backend task &optional (ts (ts-now)))
|
||||||
|
"Return the time intervals for TASK on TS, or today.
|
||||||
|
TS must be a ts struct (see `ts.el').")
|
||||||
|
(cl-defgeneric chronometrist-backend-task-time (backend task &optional (ts (ts-now)))
|
||||||
|
"Return the time tracked for TASK on TS, or today.
|
||||||
|
TS must be a ts struct (see `ts.el').")
|
||||||
|
(cl-defgeneric chronometrist-backend-active-time (backend &optional (ts (ts-now)))
|
||||||
|
"Return the total time tracked on TS, or today.
|
||||||
|
TS must be a ts struct (see `ts.el').")
|
||||||
|
(cl-defgeneric chronometrist-backend-active-days (backend task)
|
||||||
|
"Return the number of days the user spent any time on TASK.")
|
||||||
|
|
||||||
|
;; # Modifications #
|
||||||
|
(cl-defgeneric chronometrist-backend-create-file (backend)
|
||||||
|
"Create BACKEND file if it does not already exist.")
|
||||||
|
(cl-defgeneric chronometrist-backend-new-record (backend plist)
|
||||||
|
"Use PLIST to add a new interval to BACKEND.")
|
||||||
|
(cl-defgeneric chronometrist-backend-replace-last (backend plist)
|
||||||
|
"Replace the latest record in BACKEND with PLIST.")
|
||||||
|
|
||||||
|
(provide 'chronometrist-backend)
|
||||||
|
|
||||||
|
;;; chronometrist-backend.el ends here
|
|
@ -19,11 +19,14 @@
|
||||||
(require 'ts)
|
(require 'ts)
|
||||||
|
|
||||||
(require 'chronometrist-time)
|
(require 'chronometrist-time)
|
||||||
(require 'chronometrist-sexp)
|
|
||||||
|
|
||||||
;; ## VARIABLES ##
|
;; ## VARIABLES ##
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
|
(defun chronometrist-file-path ()
|
||||||
|
"Return the full storage path for the current backend."
|
||||||
|
(concat chronometrist-file "." (oref chronometrist-backend-current :ext)))
|
||||||
|
|
||||||
(defvar chronometrist-empty-time-string "-")
|
(defvar chronometrist-empty-time-string "-")
|
||||||
|
|
||||||
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
|
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
|
||||||
|
@ -59,7 +62,7 @@ file.")
|
||||||
|
|
||||||
(defun chronometrist-current-task ()
|
(defun chronometrist-current-task ()
|
||||||
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
||||||
(chronometrist-sexp-current-task))
|
(chronometrist-current-task chronometrist-backend-current))
|
||||||
|
|
||||||
(cl-defun chronometrist-format-time (seconds &optional (blank " "))
|
(cl-defun chronometrist-format-time (seconds &optional (blank " "))
|
||||||
"Format SECONDS as a string suitable for display in Chronometrist buffers.
|
"Format SECONDS as a string suitable for display in Chronometrist buffers.
|
||||||
|
@ -70,18 +73,12 @@ supplied, 3 spaces are used."
|
||||||
(-let [(h m s) (chronometrist-seconds-to-hms seconds)]
|
(-let [(h m s) (chronometrist-seconds-to-hms seconds)]
|
||||||
(if (and (zerop h) (zerop m) (zerop s))
|
(if (and (zerop h) (zerop m) (zerop s))
|
||||||
" -"
|
" -"
|
||||||
(let ((h (if (zerop h)
|
(let ((h (if (zerop h) blank (format "%2d:" h)))
|
||||||
blank
|
(m (cond ((and (zerop h) (zerop m))
|
||||||
(format "%2d:" h)))
|
|
||||||
(m (cond ((and (zerop h)
|
|
||||||
(zerop m))
|
|
||||||
blank)
|
blank)
|
||||||
((zerop h)
|
((zerop h) (format "%2d:" m))
|
||||||
(format "%2d:" m))
|
(t (format "%02d:" m))))
|
||||||
(t
|
(s (if (and (zerop h) (zerop m))
|
||||||
(format "%02d:" m))))
|
|
||||||
(s (if (and (zerop h)
|
|
||||||
(zerop m))
|
|
||||||
(format "%2d" s)
|
(format "%2d" s)
|
||||||
(format "%02d" s))))
|
(format "%02d" s))))
|
||||||
(concat h m s)))))
|
(concat h m s)))))
|
||||||
|
@ -115,13 +112,11 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
||||||
EVENTS must be a list of valid Chronometrist property lists (see
|
EVENTS must be a list of valid Chronometrist property lists (see
|
||||||
`chronometrist-file')."
|
`chronometrist-file')."
|
||||||
(cl-loop for plist in events collect
|
(cl-loop for plist in events collect
|
||||||
(let* ((start (chronometrist-iso-timestamp->ts
|
(cons (chronometrist-iso-timestamp->ts
|
||||||
(plist-get plist :start)))
|
(plist-get plist :start))
|
||||||
(stop (plist-get plist :stop))
|
(aif (plist-get plist :stop)
|
||||||
(stop (if stop
|
(chronometrist-iso-timestamp->ts it)
|
||||||
(chronometrist-iso-timestamp->ts stop)
|
(ts-now)))))
|
||||||
(ts-now))))
|
|
||||||
(cons start stop))))
|
|
||||||
|
|
||||||
(defun chronometrist-ts-pairs->durations (ts-pairs)
|
(defun chronometrist-ts-pairs->durations (ts-pairs)
|
||||||
"Return the durations represented by TS-PAIRS.
|
"Return the durations represented by TS-PAIRS.
|
||||||
|
|
|
@ -11,9 +11,7 @@
|
||||||
;;
|
;;
|
||||||
;; For more information, please refer to <https://unlicense.org>
|
;; For more information, please refer to <https://unlicense.org>
|
||||||
|
|
||||||
;; (require 'chronometrist-plist-pp)
|
|
||||||
(require 'chronometrist-common)
|
(require 'chronometrist-common)
|
||||||
(require 'chronometrist-sexp)
|
|
||||||
(require 'ts)
|
(require 'ts)
|
||||||
|
|
||||||
;; external -
|
;; external -
|
||||||
|
@ -27,7 +25,8 @@
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
(defvar chronometrist-events (make-hash-table :test #'equal)
|
(defvar chronometrist-events (make-hash-table :test #'equal)
|
||||||
"Each key is a date in the form (YEAR MONTH DAY).
|
"Hash table containing time intervals.
|
||||||
|
Each key is a date in the form (YEAR MONTH DAY).
|
||||||
|
|
||||||
Values are lists containing events, where each event is a list in
|
Values are lists containing events, where each event is a list in
|
||||||
the form (:name \"NAME\" :tags (TAGS) <key value pairs> ...
|
the form (:name \"NAME\" :tags (TAGS) <key value pairs> ...
|
||||||
|
@ -125,7 +124,7 @@ The data is acquired from `chronometrist-file'.
|
||||||
Return final number of events read from file, or nil if there
|
Return final number of events read from file, or nil if there
|
||||||
were none."
|
were none."
|
||||||
(clrhash chronometrist-events)
|
(clrhash chronometrist-events)
|
||||||
(chronometrist-sexp-events-populate))
|
(chronometrist-to-hash chronometrist-backend-current chronometrist-events))
|
||||||
|
|
||||||
(defun chronometrist-tasks-from-table ()
|
(defun chronometrist-tasks-from-table ()
|
||||||
"Return a list of task names from `chronometrist-events'."
|
"Return a list of task names from `chronometrist-events'."
|
||||||
|
|
|
@ -1,19 +1,5 @@
|
||||||
;;; chronometrist-key-values.el --- add key-values to Chronometrist data -*- lexical-binding: t; -*-
|
;;; chronometrist-key-values.el --- add key-values to Chronometrist data -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
(require 'cl-lib)
|
|
||||||
(require 'subr-x)
|
|
||||||
(require 'dash)
|
|
||||||
(require 'seq)
|
|
||||||
(require 'anaphora)
|
|
||||||
|
|
||||||
(require 'chronometrist-migrate)
|
|
||||||
(require 'chronometrist-events)
|
|
||||||
(require 'chronometrist-plist-pp)
|
|
||||||
(require 'chronometrist-common)
|
|
||||||
|
|
||||||
(declare-function chronometrist-refresh "chronometrist.el")
|
|
||||||
(declare-function chronometrist-last "chronometrist-queries.el")
|
|
||||||
|
|
||||||
;; This is free and unencumbered software released into the public domain.
|
;; This is free and unencumbered software released into the public domain.
|
||||||
;;
|
;;
|
||||||
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
@ -28,7 +14,17 @@
|
||||||
|
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
(require 'chronometrist-sexp)
|
(require 'cl-lib)
|
||||||
|
(require 'subr-x)
|
||||||
|
(require 'dash)
|
||||||
|
(require 'seq)
|
||||||
|
(require 'anaphora)
|
||||||
|
|
||||||
|
(require 'chronometrist-migrate)
|
||||||
|
(require 'chronometrist-events)
|
||||||
|
(require 'chronometrist-plist-pp)
|
||||||
|
|
||||||
|
(declare-function chronometrist-refresh "chronometrist.el")
|
||||||
|
|
||||||
(defvar chronometrist--tag-suggestions nil
|
(defvar chronometrist--tag-suggestions nil
|
||||||
"Suggestions for tags.
|
"Suggestions for tags.
|
||||||
|
@ -69,7 +65,7 @@ TAGS should be a list of symbols and/or strings.
|
||||||
PLIST should be a property list. Properties reserved by
|
PLIST should be a property list. Properties reserved by
|
||||||
Chronometrist - currently :name, :tags, :start, and :stop - will
|
Chronometrist - currently :name, :tags, :start, and :stop - will
|
||||||
be removed."
|
be removed."
|
||||||
(let* ((old-expr (chronometrist-last))
|
(let* ((old-expr (chronometrist-backend-latest-record chronometrist-backend-current))
|
||||||
(old-name (plist-get old-expr :name))
|
(old-name (plist-get old-expr :name))
|
||||||
(old-start (plist-get old-expr :start))
|
(old-start (plist-get old-expr :start))
|
||||||
(old-stop (plist-get old-expr :stop))
|
(old-stop (plist-get old-expr :stop))
|
||||||
|
@ -96,7 +92,7 @@ be removed."
|
||||||
new-kvs
|
new-kvs
|
||||||
`(:start ,old-start)
|
`(:start ,old-start)
|
||||||
(when old-stop `(:stop ,old-stop)))))
|
(when old-stop `(:stop ,old-stop)))))
|
||||||
(chronometrist-sexp-replace-last plist)))
|
(chronometrist-backend-replace-last chronometrist-backend-current plist)))
|
||||||
|
|
||||||
;;;; TAGS ;;;;
|
;;;; TAGS ;;;;
|
||||||
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
|
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
|
||||||
|
@ -206,7 +202,7 @@ INITIAL-INPUT is as used in `completing-read'."
|
||||||
_ARGS are ignored. This function always returns t, so it can be
|
_ARGS are ignored. This function always returns t, so it can be
|
||||||
used in `chronometrist-before-out-functions'."
|
used in `chronometrist-before-out-functions'."
|
||||||
(unless chronometrist--skip-detail-prompts
|
(unless chronometrist--skip-detail-prompts
|
||||||
(let* ((last-expr (chronometrist-last))
|
(let* ((last-expr (chronometrist-backend-latest-record chronometrist-backend-current))
|
||||||
(last-name (plist-get last-expr :name))
|
(last-name (plist-get last-expr :name))
|
||||||
(last-tags (plist-get last-expr :tags))
|
(last-tags (plist-get last-expr :tags))
|
||||||
(input (->> last-tags
|
(input (->> last-tags
|
||||||
|
@ -353,7 +349,7 @@ It currently supports ido, ido-ubiquitous, ivy, and helm."
|
||||||
"Prompt the user to enter keys.
|
"Prompt the user to enter keys.
|
||||||
USED-KEYS are keys they have already added since the invocation
|
USED-KEYS are keys they have already added since the invocation
|
||||||
of `chronometrist-kv-add'."
|
of `chronometrist-kv-add'."
|
||||||
(let ((key-suggestions (--> (chronometrist-last)
|
(let ((key-suggestions (--> (chronometrist-backend-latest-record chronometrist-backend-current)
|
||||||
(plist-get it :name)
|
(plist-get it :name)
|
||||||
(gethash it chronometrist-key-history))))
|
(gethash it chronometrist-key-history))))
|
||||||
(completing-read (format "Key (%s to quit): " (chronometrist-kv-completion-quit-key))
|
(completing-read (format "Key (%s to quit): " (chronometrist-kv-completion-quit-key))
|
||||||
|
@ -397,7 +393,8 @@ used in `chronometrist-before-out-functions'."
|
||||||
(unless chronometrist--skip-detail-prompts
|
(unless chronometrist--skip-detail-prompts
|
||||||
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
|
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
|
||||||
(first-key-p t)
|
(first-key-p t)
|
||||||
(last-kvs (chronometrist-plist-remove (chronometrist-last) :name :tags :start :stop))
|
(last-kvs (chronometrist-plist-remove (chronometrist-backend-latest-record chronometrist-backend-current)
|
||||||
|
:name :tags :start :stop))
|
||||||
(used-keys (->> (seq-filter #'keywordp last-kvs)
|
(used-keys (->> (seq-filter #'keywordp last-kvs)
|
||||||
(mapcar #'symbol-name)
|
(mapcar #'symbol-name)
|
||||||
(--map (s-chop-prefix ":" it)))))
|
(--map (s-chop-prefix ":" it)))))
|
||||||
|
@ -405,7 +402,7 @@ used in `chronometrist-before-out-functions'."
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(chronometrist-common-clear-buffer buffer)
|
(chronometrist-common-clear-buffer buffer)
|
||||||
(chronometrist-kv-read-mode)
|
(chronometrist-kv-read-mode)
|
||||||
(if (and (chronometrist-current-task) last-kvs)
|
(if (and (chronometrist-backend-current-task chronometrist-backend-current) last-kvs)
|
||||||
(progn
|
(progn
|
||||||
(funcall chronometrist-sexp-pretty-print-function last-kvs buffer)
|
(funcall chronometrist-sexp-pretty-print-function last-kvs buffer)
|
||||||
(down-list -1)
|
(down-list -1)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
;;; chronometrist-migrate.el --- Commands to aid in migrating from timeclock to chronometrist s-expr format -*- lexical-binding: t; -*-
|
;;; chronometrist-migrate.el --- Commands to aid in migrating between event formats -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
|
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
|
||||||
|
|
||||||
|
@ -23,13 +23,12 @@
|
||||||
(require 'chronometrist-plist-pp)
|
(require 'chronometrist-plist-pp)
|
||||||
|
|
||||||
(defvar chronometrist-file)
|
(defvar chronometrist-file)
|
||||||
(defvar chronometrist-migrate-table (make-hash-table))
|
(defvar chronometrist-migrate-table (make-hash-table :test #'equal))
|
||||||
|
|
||||||
;; TODO - support other timeclock codes (currently only "i" and "o"
|
;; TODO - support other timeclock codes (currently only "i" and "o"
|
||||||
;; are supported.)
|
;; are supported.)
|
||||||
(defun chronometrist-migrate-populate (in-file)
|
(defun chronometrist-migrate-populate-timelog (in-file)
|
||||||
"Read data from IN-FILE to `chronometrist-migrate-table'.
|
"Read data from IN-FILE to `chronometrist-migrate-table'.
|
||||||
|
|
||||||
IN-FILE should be a file in the format supported by timeclock.el.
|
IN-FILE should be a file in the format supported by timeclock.el.
|
||||||
See `timeclock-log-data' for a description."
|
See `timeclock-log-data' for a description."
|
||||||
(clrhash chronometrist-migrate-table)
|
(clrhash chronometrist-migrate-table)
|
||||||
|
@ -80,46 +79,66 @@ See `timeclock-log-data' for a description."
|
||||||
|
|
||||||
(defun chronometrist-migrate-timelog-file->sexp-file (&optional in-file out-file)
|
(defun chronometrist-migrate-timelog-file->sexp-file (&optional in-file out-file)
|
||||||
"Migrate your existing `timeclock-file' to the Chronometrist file format.
|
"Migrate your existing `timeclock-file' to the Chronometrist file format.
|
||||||
|
|
||||||
IN-FILE and OUT-FILE, if provided, are used as input and output
|
IN-FILE and OUT-FILE, if provided, are used as input and output
|
||||||
file names respectively."
|
file names respectively."
|
||||||
(interactive `(,(if (featurep 'timeclock)
|
(interactive `(,(if (featurep 'timeclock)
|
||||||
(read-file-name (concat "timeclock file (default: "
|
(read-file-name (format "timeclock file (default: %s): "
|
||||||
timeclock-file
|
timeclock-file)
|
||||||
"): ")
|
|
||||||
user-emacs-directory
|
user-emacs-directory
|
||||||
timeclock-file t)
|
timeclock-file t)
|
||||||
(read-file-name (concat "timeclock file: ")
|
(read-file-name "timeclock file: " user-emacs-directory nil t))
|
||||||
user-emacs-directory
|
,(read-file-name (format "Output file (default: %s): "
|
||||||
nil t))
|
(locate-user-emacs-file "chronometrist.sexp"))
|
||||||
,(read-file-name (concat "Output file (default: "
|
|
||||||
(locate-user-emacs-file "chronometrist.sexp")
|
|
||||||
"): ")
|
|
||||||
user-emacs-directory
|
user-emacs-directory
|
||||||
(locate-user-emacs-file "chronometrist.sexp"))))
|
(locate-user-emacs-file "chronometrist.sexp"))))
|
||||||
(when (if (file-exists-p out-file)
|
(when (if (file-exists-p out-file)
|
||||||
(yes-or-no-p (concat "Output file "
|
(yes-or-no-p (format "Output file %s already exists - overwrite? " out-file))
|
||||||
out-file
|
|
||||||
" already exists - overwrite? "))
|
|
||||||
t)
|
t)
|
||||||
(let ((output (find-file-noselect out-file)))
|
(let ((output (find-file-noselect out-file)))
|
||||||
(with-current-buffer output
|
(with-current-buffer output
|
||||||
(chronometrist-common-clear-buffer output)
|
(chronometrist-common-clear-buffer output)
|
||||||
(chronometrist-migrate-populate in-file)
|
(chronometrist-migrate-populate-timelog in-file)
|
||||||
(maphash (lambda (_key value)
|
(maphash (lambda (_key value)
|
||||||
(chronometrist-plist-pp value output)
|
(chronometrist-plist-pp value output)
|
||||||
(insert "\n\n"))
|
(insert "\n\n"))
|
||||||
chronometrist-migrate-table)
|
chronometrist-migrate-table)
|
||||||
(save-buffer)))))
|
(save-buffer)))))
|
||||||
|
|
||||||
|
(defun chronometrist-migrate-populate-sexp (in-file)
|
||||||
|
"Read data from IN-FILE to `chronometrist-migrate-table'.
|
||||||
|
|
||||||
|
IN-FILE should be a file in the Chronometrist s-expression format.
|
||||||
|
See `chronometrist-file' for a description."
|
||||||
|
(clrhash chronometrist-migrate-table)
|
||||||
|
(with-current-buffer (find-file-noselect in-file)
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (point-min))
|
||||||
|
(let ((index 0)
|
||||||
|
expr)
|
||||||
|
(while (setq expr (ignore-errors (read (current-buffer))))
|
||||||
|
(let* ((new-date (s-left 10 (plist-get expr :start)))
|
||||||
|
(existing-value (gethash new-date chronometrist-migrate-table)))
|
||||||
|
(cl-incf index)
|
||||||
|
(puthash new-date
|
||||||
|
(if existing-value
|
||||||
|
(append existing-value
|
||||||
|
(list expr))
|
||||||
|
(list expr))
|
||||||
|
chronometrist-migrate-table)))
|
||||||
|
(unless (zerop index) index)))))
|
||||||
|
|
||||||
|
(defun chronometrist-migrate (input-format output-format &optional input-file output-file)
|
||||||
|
"Migrate a Chronometrist file from INPUT-FORMAT to OUTPUT-FORMAT.
|
||||||
|
Prompt for INPUT-FILE and OUTPUT-FILE if not provided.")
|
||||||
|
|
||||||
(defun chronometrist-migrate-check ()
|
(defun chronometrist-migrate-check ()
|
||||||
"Offer to import data from `timeclock-file' if `chronometrist-file' does not exist."
|
"Offer to import data from `timeclock-file' if `chronometrist-file' does not exist."
|
||||||
(when (and (bound-and-true-p timeclock-file)
|
(when (and (bound-and-true-p timeclock-file)
|
||||||
(not (file-exists-p chronometrist-file)))
|
(not (file-exists-p (chronometrist-file-path))))
|
||||||
(if (yes-or-no-p (format (concat "Chronometrist v0.3+ uses a new file format;"
|
(if (yes-or-no-p (format (concat "Chronometrist v0.3+ uses a new file format;"
|
||||||
" import data from %s ? ")
|
" import data from %s ? ")
|
||||||
timeclock-file))
|
timeclock-file))
|
||||||
(chronometrist-migrate-timelog-file->sexp-file timeclock-file chronometrist-file)
|
(chronometrist-migrate-timelog-file->sexp-file timeclock-file (chronometrist-file-path))
|
||||||
(message "You can migrate later using `chronometrist-migrate-timelog-file->sexp-file'."))))
|
(message "You can migrate later using `chronometrist-migrate-timelog-file->sexp-file'."))))
|
||||||
|
|
||||||
(provide 'chronometrist-migrate)
|
(provide 'chronometrist-migrate)
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
;;; chronometrist-queries.el --- Functions which query Chronometrist data -*- lexical-binding: t; -*-
|
|
||||||
|
|
||||||
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
|
|
||||||
|
|
||||||
;; This is free and unencumbered software released into the public domain.
|
|
||||||
;;
|
|
||||||
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
||||||
;; distribute this software, either in source code form or as a compiled
|
|
||||||
;; binary, for any purpose, commercial or non-commercial, and by any
|
|
||||||
;; means.
|
|
||||||
;;
|
|
||||||
;; For more information, please refer to <https://unlicense.org>
|
|
||||||
|
|
||||||
;;; Commentary:
|
|
||||||
;;
|
|
||||||
|
|
||||||
;;; Code:
|
|
||||||
|
|
||||||
(require 'dash)
|
|
||||||
(require 'chronometrist-common)
|
|
||||||
(require 'chronometrist-events)
|
|
||||||
|
|
||||||
(defun chronometrist-last ()
|
|
||||||
"Return the last entry from `chronometrist-file' as a plist."
|
|
||||||
(chronometrist-sexp-last))
|
|
||||||
|
|
||||||
(cl-defun chronometrist-task-time-one-day (task &optional (ts (ts-now)))
|
|
||||||
"Return total time spent on TASK today or (if supplied) on timestamp TS.
|
|
||||||
The data is obtained from `chronometrist-file', via `chronometrist-events'.
|
|
||||||
|
|
||||||
TS should be a ts struct (see `ts.el').
|
|
||||||
|
|
||||||
The return value is seconds, as an integer."
|
|
||||||
(let ((task-events (chronometrist-task-events-in-day task ts)))
|
|
||||||
(if task-events
|
|
||||||
(->> (chronometrist-events->ts-pairs task-events)
|
|
||||||
(chronometrist-ts-pairs->durations)
|
|
||||||
(-reduce #'+)
|
|
||||||
(truncate))
|
|
||||||
;; no events for this task on TS, i.e. no time spent
|
|
||||||
0)))
|
|
||||||
|
|
||||||
(defun chronometrist-active-time-one-day (&optional ts)
|
|
||||||
"Return the total active time on TS (if non-nil) or today.
|
|
||||||
TS must be a ts struct (see `ts.el')
|
|
||||||
|
|
||||||
Return value is seconds as an integer."
|
|
||||||
(->> chronometrist-task-list
|
|
||||||
(--map (chronometrist-task-time-one-day it ts))
|
|
||||||
(-reduce #'+)
|
|
||||||
(truncate)))
|
|
||||||
|
|
||||||
(cl-defun chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
|
||||||
"Return the number of days the user spent any time on TASK.
|
|
||||||
TABLE must be a hash table - if not supplied, `chronometrist-events' is used.
|
|
||||||
|
|
||||||
This will not return correct results if TABLE contains records
|
|
||||||
which span midnights. (see `chronometrist-events-clean')"
|
|
||||||
(let ((count 0))
|
|
||||||
(maphash (lambda (_date events)
|
|
||||||
(when (seq-find (lambda (event)
|
|
||||||
(equal (plist-get event :name) task))
|
|
||||||
events)
|
|
||||||
(cl-incf count)))
|
|
||||||
table)
|
|
||||||
count))
|
|
||||||
|
|
||||||
(defun chronometrist-task-events-in-day (task ts)
|
|
||||||
"Get events for TASK on TS.
|
|
||||||
TS should be a ts struct (see `ts.el').
|
|
||||||
|
|
||||||
Returns a list of events, where each event is a property list in
|
|
||||||
the form (:name \"NAME\" :start START :stop STOP ...), where
|
|
||||||
START and STOP are ISO-8601 time strings.
|
|
||||||
|
|
||||||
This will not return correct results if TABLE contains records
|
|
||||||
which span midnights. (see `chronometrist-events-clean')"
|
|
||||||
(->> (gethash (ts-format "%F" ts) chronometrist-events)
|
|
||||||
(mapcar (lambda (event)
|
|
||||||
(when (equal task (plist-get event :name))
|
|
||||||
event)))
|
|
||||||
(seq-filter #'identity)))
|
|
||||||
|
|
||||||
(provide 'chronometrist-queries)
|
|
||||||
|
|
||||||
;;; chronometrist-queries.el ends here
|
|
|
@ -17,7 +17,6 @@
|
||||||
(require 'filenotify)
|
(require 'filenotify)
|
||||||
(require 'subr-x)
|
(require 'subr-x)
|
||||||
(require 'chronometrist-common)
|
(require 'chronometrist-common)
|
||||||
(require 'chronometrist-queries)
|
|
||||||
(require 'chronometrist-timer)
|
(require 'chronometrist-timer)
|
||||||
(require 'chronometrist-migrate)
|
(require 'chronometrist-migrate)
|
||||||
|
|
||||||
|
@ -101,18 +100,18 @@ The first date is the first occurrence of
|
||||||
(let* ((week-dates (chronometrist-report-date->week-dates))) ;; uses today if chronometrist-report--ui-date is nil
|
(let* ((week-dates (chronometrist-report-date->week-dates))) ;; uses today if chronometrist-report--ui-date is nil
|
||||||
(setq chronometrist-report--ui-week-dates week-dates)
|
(setq chronometrist-report--ui-week-dates week-dates)
|
||||||
(cl-loop for task in chronometrist-task-list collect
|
(cl-loop for task in chronometrist-task-list collect
|
||||||
(let* ((durations (--map (chronometrist-task-time-one-day task (chronometrist-date it))
|
(let* ((durations (--map (chronometrist-backend-task-time chronometrist-backend-current task (chronometrist-date it))
|
||||||
week-dates))
|
week-dates))
|
||||||
(duration-strings (mapcar #'chronometrist-format-time
|
(duration-strings (mapcar #'chronometrist-format-time
|
||||||
durations))
|
durations))
|
||||||
(total-duration (->> (-reduce #'+ durations)
|
(total-duration (->> (-reduce #'+ durations)
|
||||||
(chronometrist-format-time)
|
(chronometrist-format-time)
|
||||||
(vector))))
|
(vector))))
|
||||||
(list task
|
(list task
|
||||||
(vconcat
|
(vconcat
|
||||||
(vector task)
|
(vector task)
|
||||||
duration-strings ;; vconcat converts lists to vectors
|
duration-strings ;; vconcat converts lists to vectors
|
||||||
total-duration))))))
|
total-duration))))))
|
||||||
|
|
||||||
(defun chronometrist-report-print-keybind (command &optional description firstonly)
|
(defun chronometrist-report-print-keybind (command &optional description firstonly)
|
||||||
"Insert one or more keybindings for COMMAND into the current buffer.
|
"Insert one or more keybindings for COMMAND into the current buffer.
|
||||||
|
@ -129,11 +128,10 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
|
||||||
"Print the non-tabular part of the buffer in `chronometrist-report'."
|
"Print the non-tabular part of the buffer in `chronometrist-report'."
|
||||||
(let ((inhibit-read-only t)
|
(let ((inhibit-read-only t)
|
||||||
(w "\n ")
|
(w "\n ")
|
||||||
(total-time-daily (->> chronometrist-report--ui-week-dates
|
(total-time-daily (->> (mapcar #'chronometrist-date chronometrist-report--ui-week-dates)
|
||||||
(mapcar #'chronometrist-date)
|
(--map (chronometrist-backend-active-time chronometrist-backend-current it)))))
|
||||||
(mapcar #'chronometrist-active-time-one-day))))
|
|
||||||
(goto-char (point-min))
|
(goto-char (point-min))
|
||||||
(insert " ")
|
(insert (make-string 25 ?\ ))
|
||||||
(insert (mapconcat (lambda (ts)
|
(insert (mapconcat (lambda (ts)
|
||||||
(ts-format "%F" ts))
|
(ts-format "%F" ts))
|
||||||
(chronometrist-report-date->week-dates)
|
(chronometrist-report-date->week-dates)
|
||||||
|
@ -176,7 +174,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
|
||||||
(defun chronometrist-report-refresh-file (_fs-event)
|
(defun chronometrist-report-refresh-file (_fs-event)
|
||||||
"Re-read `chronometrist-file' and refresh the `chronometrist-report' buffer.
|
"Re-read `chronometrist-file' and refresh the `chronometrist-report' buffer.
|
||||||
Argument _FS-EVENT is ignored."
|
Argument _FS-EVENT is ignored."
|
||||||
(chronometrist-events-populate)
|
(chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
|
||||||
;; (chronometrist-events-clean)
|
;; (chronometrist-events-clean)
|
||||||
(chronometrist-report-refresh))
|
(chronometrist-report-refresh))
|
||||||
|
|
||||||
|
@ -217,10 +215,14 @@ Argument _FS-EVENT is ignored."
|
||||||
(setq tabulated-list-sort-key '("Task" . nil))
|
(setq tabulated-list-sort-key '("Task" . nil))
|
||||||
(tabulated-list-init-header)
|
(tabulated-list-init-header)
|
||||||
(chronometrist-maybe-start-timer)
|
(chronometrist-maybe-start-timer)
|
||||||
|
(add-hook 'chronometrist-timer-hook
|
||||||
|
(lambda ()
|
||||||
|
(when (get-buffer-window chronometrist-report-buffer-name)
|
||||||
|
(chronometrist-report-refresh))))
|
||||||
(setq revert-buffer-function #'chronometrist-report-refresh)
|
(setq revert-buffer-function #'chronometrist-report-refresh)
|
||||||
(unless chronometrist--fs-watch
|
(unless chronometrist--fs-watch
|
||||||
(setq chronometrist--fs-watch
|
(setq chronometrist--fs-watch
|
||||||
(file-notify-add-watch chronometrist-file
|
(file-notify-add-watch (chronometrist-file-path)
|
||||||
'(change)
|
'(change)
|
||||||
#'chronometrist-refresh-file))))
|
#'chronometrist-refresh-file))))
|
||||||
|
|
||||||
|
@ -249,7 +251,7 @@ current week. Otherwise, display data from the week specified by
|
||||||
(kill-buffer buffer))
|
(kill-buffer buffer))
|
||||||
(t (unless keep-date
|
(t (unless keep-date
|
||||||
(setq chronometrist-report--ui-date nil))
|
(setq chronometrist-report--ui-date nil))
|
||||||
(chronometrist-common-create-file)
|
(chronometrist-backend-create-file chronometrist-backend-current)
|
||||||
(chronometrist-report-mode)
|
(chronometrist-report-mode)
|
||||||
(switch-to-buffer buffer)
|
(switch-to-buffer buffer)
|
||||||
(chronometrist-report-refresh-file nil)
|
(chronometrist-report-refresh-file nil)
|
||||||
|
|
|
@ -8,6 +8,11 @@
|
||||||
;; chronometrist-file (-custom)
|
;; chronometrist-file (-custom)
|
||||||
;; chronometrist-events, chronometrist-events-maybe-split (-events)
|
;; chronometrist-events, chronometrist-events-maybe-split (-events)
|
||||||
|
|
||||||
|
(require 'chronometrist)
|
||||||
|
(require 'chronometrist-backend)
|
||||||
|
(defclass chronometrist-sexp (chronometrist-backend) nil)
|
||||||
|
(defvar chronometrist-sexp-backend (make-instance chronometrist-sexp :name "sexp" :ext "sexp"))
|
||||||
|
|
||||||
(defcustom chronometrist-sexp-pretty-print-function #'chronometrist-plist-pp
|
(defcustom chronometrist-sexp-pretty-print-function #'chronometrist-plist-pp
|
||||||
"Function used to pretty print plists in `chronometrist-file'.
|
"Function used to pretty print plists in `chronometrist-file'.
|
||||||
Like `pp', it must accept an OBJECT and optionally a
|
Like `pp', it must accept an OBJECT and optionally a
|
||||||
|
@ -20,33 +25,10 @@ STREAM (which is the value of `current-buffer')."
|
||||||
`(with-current-buffer (find-file-noselect ,file)
|
`(with-current-buffer (find-file-noselect ,file)
|
||||||
(save-excursion ,@body)))
|
(save-excursion ,@body)))
|
||||||
|
|
||||||
;;;; Queries
|
;; # Migration #
|
||||||
(defun chronometrist-sexp-open-log ()
|
(cl-defmethod chronometrist-backend-to-hash ((backend chronometrist-sexp) table)
|
||||||
"Open `chronometrist-file' in another window."
|
(clrhash table)
|
||||||
(find-file-other-window chronometrist-file)
|
(chronometrist-sexp-in-file (chronometrist-file-path)
|
||||||
(goto-char (point-max)))
|
|
||||||
|
|
||||||
(defun chronometrist-sexp-last ()
|
|
||||||
"Return last s-expression from `chronometrist-file'."
|
|
||||||
(chronometrist-sexp-in-file chronometrist-file
|
|
||||||
(goto-char (point-max))
|
|
||||||
(backward-list)
|
|
||||||
(ignore-errors (read (current-buffer)))))
|
|
||||||
|
|
||||||
(defun chronometrist-sexp-current-task ()
|
|
||||||
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
|
||||||
(let ((last-event (chronometrist-sexp-last)))
|
|
||||||
(if (plist-member last-event :stop)
|
|
||||||
nil
|
|
||||||
(plist-get last-event :name))))
|
|
||||||
|
|
||||||
(defun chronometrist-sexp-events-populate ()
|
|
||||||
"Populate hash table `chronometrist-events'.
|
|
||||||
The data is acquired from `chronometrist-file'.
|
|
||||||
|
|
||||||
Return final number of events read from file, or nil if there
|
|
||||||
were none."
|
|
||||||
(chronometrist-sexp-in-file chronometrist-file
|
|
||||||
(goto-char (point-min))
|
(goto-char (point-min))
|
||||||
(let ((index 0) expr pending-expr)
|
(let ((index 0) expr pending-expr)
|
||||||
(while (or pending-expr
|
(while (or pending-expr
|
||||||
|
@ -60,8 +42,7 @@ were none."
|
||||||
(setq pending-expr (cl-second split-expr))
|
(setq pending-expr (cl-second split-expr))
|
||||||
(cl-first split-expr))
|
(cl-first split-expr))
|
||||||
(t expr)))
|
(t expr)))
|
||||||
(new-value-date (->> (plist-get new-value :start)
|
(new-value-date (s-left 10 (plist-get new-value :start)))
|
||||||
(s-left 10)))
|
|
||||||
(existing-value (gethash new-value-date chronometrist-events)))
|
(existing-value (gethash new-value-date chronometrist-events)))
|
||||||
(unless pending-expr (cl-incf index))
|
(unless pending-expr (cl-incf index))
|
||||||
(puthash new-value-date
|
(puthash new-value-date
|
||||||
|
@ -72,16 +53,32 @@ were none."
|
||||||
chronometrist-events)))
|
chronometrist-events)))
|
||||||
(unless (zerop index) index))))
|
(unless (zerop index) index))))
|
||||||
|
|
||||||
;;;; Modifications
|
(cl-defmethod chronometrist-backend-from-hash ((backend chronometrist-sexp) table))
|
||||||
(defun chronometrist-sexp-create-file ()
|
|
||||||
"Create `chronometrist-file' if it doesn't already exist."
|
|
||||||
(unless (file-exists-p chronometrist-file)
|
|
||||||
(with-current-buffer (find-file-noselect chronometrist-file)
|
|
||||||
(write-file chronometrist-file))))
|
|
||||||
|
|
||||||
(cl-defun chronometrist-sexp-new (plist)
|
;; # Queries #
|
||||||
"Add new PLIST at the end of `chronometrist-file'."
|
(cl-defmethod chronometrist-backend-open-file ((backend chronometrist-sexp))
|
||||||
(chronometrist-sexp-in-file chronometrist-file
|
(find-file-other-window (chronometrist-file-path))
|
||||||
|
(goto-char (point-max)))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-latest-record ((backend chronometrist-sexp))
|
||||||
|
(chronometrist-sexp-in-file (chronometrist-file-path)
|
||||||
|
(goto-char (point-max))
|
||||||
|
(backward-list)
|
||||||
|
(ignore-errors (read (current-buffer)))))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-current-task ((backend chronometrist-sexp))
|
||||||
|
(let ((last-event (chronometrist-backend-latest-record backend)))
|
||||||
|
(unless (plist-member last-event :stop)
|
||||||
|
(plist-get last-event :name))))
|
||||||
|
|
||||||
|
;; # Modifications #
|
||||||
|
(cl-defmethod chronometrist-backend-create-file ((backend chronometrist-sexp))
|
||||||
|
(unless (file-exists-p (chronometrist-file-path))
|
||||||
|
(with-current-buffer (find-file-noselect (chronometrist-file-path))
|
||||||
|
(write-file (chronometrist-file-path)))))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-new-record ((backend chronometrist-sexp) plist)
|
||||||
|
(chronometrist-sexp-in-file (chronometrist-file-path)
|
||||||
(goto-char (point-max))
|
(goto-char (point-max))
|
||||||
;; If we're adding the first s-exp in the file, don't add a
|
;; If we're adding the first s-exp in the file, don't add a
|
||||||
;; newline before it
|
;; newline before it
|
||||||
|
@ -102,9 +99,8 @@ were none."
|
||||||
(forward-sexp (or arg 1))
|
(forward-sexp (or arg 1))
|
||||||
(delete-region point-1 (point))))
|
(delete-region point-1 (point))))
|
||||||
|
|
||||||
(defun chronometrist-sexp-replace-last (plist)
|
(cl-defmethod chronometrist-backend-replace-last ((backend chronometrist-sexp) plist)
|
||||||
"Replace the last s-expression in `chronometrist-file' with PLIST."
|
(chronometrist-sexp-in-file (chronometrist-file-path)
|
||||||
(chronometrist-sexp-in-file chronometrist-file
|
|
||||||
(goto-char (point-max))
|
(goto-char (point-max))
|
||||||
(unless (and (bobp) (bolp))
|
(unless (and (bobp) (bolp))
|
||||||
(insert "\n"))
|
(insert "\n"))
|
||||||
|
@ -119,11 +115,68 @@ were none."
|
||||||
;; :name should be removed from `chronometrist-task-list', but to ascertain
|
;; :name should be removed from `chronometrist-task-list', but to ascertain
|
||||||
;; that condition we would have to either read the entire file or
|
;; that condition we would have to either read the entire file or
|
||||||
;; map over the hash table, defeating the optimization. Thus, we
|
;; map over the hash table, defeating the optimization. Thus, we
|
||||||
;; don't update `chronometrist-task-list' here (unlike `chronometrist-sexp-new')
|
;; don't update `chronometrist-task-list' here (unlike `chronometrist-backend-new-record')
|
||||||
(chronometrist-tags-history-replace-last plist)
|
(chronometrist-tags-history-replace-last plist)
|
||||||
(setq chronometrist--inhibit-read-p t)
|
(setq chronometrist--inhibit-read-p t)
|
||||||
(save-buffer)))
|
(save-buffer)))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-task-intervals ((backend chronometrist-sexp) task &optional (ts (ts-now)))
|
||||||
|
"Get intervals for TASK on TS.
|
||||||
|
TS should be a ts struct (see `ts.el').
|
||||||
|
|
||||||
|
Returns a list of events, where each event is a property list in
|
||||||
|
the form (:name \"NAME\" :start START :stop STOP ...), where
|
||||||
|
START and STOP are ISO-8601 time strings.
|
||||||
|
|
||||||
|
This will not return correct results if TABLE contains records
|
||||||
|
which span midnights. (see `chronometrist-events-clean')"
|
||||||
|
(->> (gethash (ts-format "%F" ts) chronometrist-events)
|
||||||
|
(mapcar (lambda (event)
|
||||||
|
(when (equal task (plist-get event :name))
|
||||||
|
event)))
|
||||||
|
(seq-filter #'identity)))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-task-time ((backend chronometrist-sexp) task &optional (ts (ts-now)))
|
||||||
|
"Return total time spent on TASK today or (if supplied) on timestamp TS.
|
||||||
|
The data is obtained from `chronometrist-file', via `chronometrist-events'.
|
||||||
|
|
||||||
|
TS should be a ts struct (see `ts.el').
|
||||||
|
|
||||||
|
The return value is seconds, as an integer."
|
||||||
|
(let ((task-events (chronometrist-backend-task-intervals chronometrist-backend-current task ts)))
|
||||||
|
(if task-events
|
||||||
|
(->> (chronometrist-events->ts-pairs task-events)
|
||||||
|
(chronometrist-ts-pairs->durations)
|
||||||
|
(-reduce #'+)
|
||||||
|
(truncate))
|
||||||
|
;; no events for this task on TS, i.e. no time spent
|
||||||
|
0)))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-active-time ((backend chronometrist-sexp) &optional ts)
|
||||||
|
"Return the total active time on TS (if non-nil) or today.
|
||||||
|
TS must be a ts struct (see `ts.el')
|
||||||
|
|
||||||
|
Return value is seconds as an integer."
|
||||||
|
(->> chronometrist-task-list
|
||||||
|
(--map (chronometrist-backend-task-time chronometrist-backend-current it ts))
|
||||||
|
(-reduce #'+)
|
||||||
|
(truncate)))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-active-days ((backend chronometrist-sexp) task &optional (table chronometrist-events))
|
||||||
|
"Return the number of days the user spent any time on TASK.
|
||||||
|
TABLE must be a hash table - if not supplied, `chronometrist-events' is used.
|
||||||
|
|
||||||
|
This will not return correct results if TABLE contains records
|
||||||
|
which span midnights. (see `chronometrist-events-clean')"
|
||||||
|
(let ((count 0))
|
||||||
|
(maphash (lambda (_date events)
|
||||||
|
(when (seq-find (lambda (event)
|
||||||
|
(equal (plist-get event :name) task))
|
||||||
|
events)
|
||||||
|
(cl-incf count)))
|
||||||
|
table)
|
||||||
|
count))
|
||||||
|
|
||||||
(defun chronometrist-sexp-reindent-buffer ()
|
(defun chronometrist-sexp-reindent-buffer ()
|
||||||
"Reindent the current buffer.
|
"Reindent the current buffer.
|
||||||
This is meant to be run in `chronometrist-file' when using the s-expression backend."
|
This is meant to be run in `chronometrist-file' when using the s-expression backend."
|
||||||
|
@ -134,8 +187,7 @@ This is meant to be run in `chronometrist-file' when using the s-expression back
|
||||||
(backward-list)
|
(backward-list)
|
||||||
(chronometrist-sexp-delete-list)
|
(chronometrist-sexp-delete-list)
|
||||||
(when (looking-at "\n*")
|
(when (looking-at "\n*")
|
||||||
(delete-region (match-beginning 0)
|
(delete-region (match-beginning 0) (match-end 0)))
|
||||||
(match-end 0)))
|
|
||||||
(funcall chronometrist-sexp-pretty-print-function expr (current-buffer))
|
(funcall chronometrist-sexp-pretty-print-function expr (current-buffer))
|
||||||
(insert "\n")
|
(insert "\n")
|
||||||
(unless (eobp)
|
(unless (eobp)
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
;;; chronometrist-sqlite3.el --- sqlite3 backend for Chronometrist
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;;
|
||||||
|
|
||||||
|
(require 'anaphora)
|
||||||
|
(require 'emacsql-sqlite3)
|
||||||
|
(require 'chronometrist-backend)
|
||||||
|
|
||||||
|
(defclass chronometrist-sqlite3 (chronometrist-backend) nil)
|
||||||
|
(defvar chronometrist-sqlite3-backend (make-instance chronometrist-sqlite3 :name "sqlite3" :ext "sqlite3"))
|
||||||
|
(defvar chronometrist-sqlite3-db (chronometrist-backend-create-file chronometrist-sqlite3-backend))
|
||||||
|
|
||||||
|
;; # Migration #
|
||||||
|
(cl-defmethod chronometrist-backend-to-hash ((backend chronometrist-sqlite3) table))
|
||||||
|
|
||||||
|
(defun chronometrist-sqlite3-insert-plist (plist db)
|
||||||
|
"Insert PLIST into DB."
|
||||||
|
(let* ((keywords (seq-filter #'keywordp event))
|
||||||
|
(values (seq-remove #'keywordp event))
|
||||||
|
(columns (mapcar (lambda (keyword)
|
||||||
|
(--> (symbol-name keyword)
|
||||||
|
(s-chop-prefix ":" it)
|
||||||
|
;; emacsql seems to automatically
|
||||||
|
;; convert dashes in column names
|
||||||
|
;; to underscores, so we do the
|
||||||
|
;; same, lest we get a "column
|
||||||
|
;; already exists" error
|
||||||
|
(replace-regexp-in-string "-" "_" it)
|
||||||
|
(intern it)))
|
||||||
|
keywords)))
|
||||||
|
;; ensure all keywords in this plist exist as SQL columns
|
||||||
|
(cl-loop for column in columns do
|
||||||
|
(let* ((pragma (emacsql db [:pragma (funcall table_info events)]))
|
||||||
|
(column-exists (cl-loop for column-spec in pragma thereis
|
||||||
|
(eq column (second column-spec)))))
|
||||||
|
(unless column-exists
|
||||||
|
(emacsql db [:alter-table events :add-column $i1] column))))
|
||||||
|
(emacsql db [:insert-into events [$i1] :values $v2]
|
||||||
|
(vconcat columns) (vconcat values))))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-from-hash ((backend chronometrist-sqlite3) hash-table file)
|
||||||
|
"Save HASH-TABLE as a FILE of BACKEND.
|
||||||
|
FILE should be a file path without an extension."
|
||||||
|
(cl-loop with db = (emacsql-sqlite3 (concat file "." (oref backend :ext)))
|
||||||
|
with count = 0
|
||||||
|
for events being the hash-values of hash-table do
|
||||||
|
(cl-loop for event in events do
|
||||||
|
(chronometrist-sqlite3-insert-plist event db)
|
||||||
|
(incf count)
|
||||||
|
(when (zerop (% count 5))
|
||||||
|
(message "chronometrist-migrate - %s events converted" count)))
|
||||||
|
finally return count do
|
||||||
|
(message "chronometrist-migrate - finished converting %s events." count)))
|
||||||
|
|
||||||
|
;; # Queries #
|
||||||
|
(cl-defmethod chronometrist-backend-open-file ((backend chronometrist-sqlite3) file)
|
||||||
|
(require 'sql)
|
||||||
|
(switch-to-buffer
|
||||||
|
(sql-comint-sqlite 'sqlite (list file))))
|
||||||
|
|
||||||
|
;; SELECT * FROM TABLE WHERE ID = (SELECT MAX(ID) FROM TABLE);
|
||||||
|
;; SELECT * FROM tablename ORDER BY column DESC LIMIT 1;
|
||||||
|
(cl-defmethod chronometrist-backend-latest-record ((backend chronometrist-sqlite3) db)
|
||||||
|
(emacsql db [:select * :from events :order-by rowid :desc :limit 1]))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-current-task ((backend chronometrist-sqlite3)))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-intervals ((backend chronometrist-sqlite3) task &optional (ts (ts-now)))
|
||||||
|
"Return the time intervals for TASK on TS.")
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-task-time ((backend chronometrist-sqlite3) task &optional (ts (ts-now))))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-active-time ((backend chronometrist-sqlite3) &optional ts))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-active-days ((backend chronometrist-sqlite3) task))
|
||||||
|
|
||||||
|
;; # Modifications #
|
||||||
|
(cl-defmethod chronometrist-backend-create-file ((backend chronometrist-sqlite3))
|
||||||
|
"Create file for BACKEND if it does not already exist.
|
||||||
|
Return the emacsql-sqlite3 connection object."
|
||||||
|
(aprog1 (emacsql-sqlite3 (concat chronometrist-file "." (oref backend :ext)))
|
||||||
|
(emacsql it [:create-table events ([name tags start stop])])))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-new-record ((backend chronometrist-sqlite3) plist)
|
||||||
|
(chronometrist-sqlite3-insert-plist plist file))
|
||||||
|
|
||||||
|
(cl-defmethod chronometrist-backend-replace-last ((backend chronometrist-sqlite3) plist)
|
||||||
|
(emacsql db [:delete-from events :where ]))
|
||||||
|
|
||||||
|
(provide 'chronometrist-sqlite3)
|
||||||
|
|
||||||
|
;;; chronometrist-sqlite3.el ends here
|
|
@ -20,10 +20,8 @@
|
||||||
|
|
||||||
(require 'chronometrist-common)
|
(require 'chronometrist-common)
|
||||||
(require 'chronometrist-time)
|
(require 'chronometrist-time)
|
||||||
(require 'chronometrist-timer)
|
|
||||||
(require 'chronometrist-events)
|
(require 'chronometrist-events)
|
||||||
(require 'chronometrist-migrate)
|
(require 'chronometrist-migrate)
|
||||||
(require 'chronometrist-queries)
|
|
||||||
|
|
||||||
(declare-function chronometrist-refresh-file "chronometrist.el")
|
(declare-function chronometrist-refresh-file "chronometrist.el")
|
||||||
|
|
||||||
|
@ -97,12 +95,12 @@ TABLE should be a hash table - if not supplied,
|
||||||
`chronometrist-events' is used."
|
`chronometrist-events' is used."
|
||||||
;; (cl-loop
|
;; (cl-loop
|
||||||
;; for date being the hash-keys of table
|
;; for date being the hash-keys of table
|
||||||
;; (let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
|
;; (let ((events-in-day (chronometrist-backend-task-intervals task (chronometrist-iso-date->ts key))))
|
||||||
;; (when events-in-day)))
|
;; (when events-in-day)))
|
||||||
(let ((days 0)
|
(let ((days 0)
|
||||||
(per-day-time-list))
|
(per-day-time-list))
|
||||||
(maphash (lambda (key _value)
|
(maphash (lambda (key _value)
|
||||||
(let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
|
(let ((events-in-day (chronometrist-backend-task-intervals chronometrist-backend-current task (chronometrist-iso-date->ts key))))
|
||||||
(when events-in-day
|
(when events-in-day
|
||||||
(setq days (1+ days))
|
(setq days (1+ days))
|
||||||
(->> (chronometrist-events->ts-pairs events-in-day)
|
(->> (chronometrist-events->ts-pairs events-in-day)
|
||||||
|
@ -125,7 +123,7 @@ It simply operates on the entire hash table TABLE (see
|
||||||
reduced to the desired range using
|
reduced to the desired range using
|
||||||
`chronometrist-events-subset'."
|
`chronometrist-events-subset'."
|
||||||
(mapcar (lambda (task)
|
(mapcar (lambda (task)
|
||||||
(let* ((active-days (chronometrist-statistics-count-active-days task table))
|
(let* ((active-days (chronometrist-count-active-days task table))
|
||||||
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
||||||
('week (* 100 (/ active-days 7.0)))))
|
('week (* 100 (/ active-days 7.0)))))
|
||||||
(active-percent (if (zerop active-days)
|
(active-percent (if (zerop active-days)
|
||||||
|
@ -232,10 +230,14 @@ value of `revert-buffer-function'."
|
||||||
(setq tabulated-list-sort-key '("Task" . nil))
|
(setq tabulated-list-sort-key '("Task" . nil))
|
||||||
(tabulated-list-init-header)
|
(tabulated-list-init-header)
|
||||||
;; (chronometrist-maybe-start-timer)
|
;; (chronometrist-maybe-start-timer)
|
||||||
|
(add-hook 'chronometrist-timer-hook
|
||||||
|
(lambda ()
|
||||||
|
(when (get-buffer-window chronometrist-statistics-buffer-name)
|
||||||
|
(chronometrist-statistics-refresh))))
|
||||||
(setq revert-buffer-function #'chronometrist-statistics-refresh)
|
(setq revert-buffer-function #'chronometrist-statistics-refresh)
|
||||||
(unless chronometrist--fs-watch
|
(unless chronometrist--fs-watch
|
||||||
(setq chronometrist--fs-watch
|
(setq chronometrist--fs-watch
|
||||||
(file-notify-add-watch chronometrist-file
|
(file-notify-add-watch (chronometrist-file-path)
|
||||||
'(change)
|
'(change)
|
||||||
#'chronometrist-refresh-file))))
|
#'chronometrist-refresh-file))))
|
||||||
|
|
||||||
|
@ -267,7 +269,7 @@ specified by `chronometrist-statistics--ui-state'."
|
||||||
(setq chronometrist-statistics--ui-state `(:mode week
|
(setq chronometrist-statistics--ui-state `(:mode week
|
||||||
:start ,week-start
|
:start ,week-start
|
||||||
:end ,week-end)))
|
:end ,week-end)))
|
||||||
(chronometrist-common-create-file)
|
(chronometrist-backend-create-file chronometrist-backend-current)
|
||||||
(chronometrist-statistics-mode)
|
(chronometrist-statistics-mode)
|
||||||
(switch-to-buffer buffer)
|
(switch-to-buffer buffer)
|
||||||
(chronometrist-statistics-refresh))))))
|
(chronometrist-statistics-refresh))))))
|
||||||
|
|
|
@ -2,12 +2,6 @@
|
||||||
|
|
||||||
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
|
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
|
||||||
|
|
||||||
(require 'parse-time)
|
|
||||||
(require 'dash)
|
|
||||||
(require 's)
|
|
||||||
|
|
||||||
(declare-function chronometrist-day-start "chronometrist-events.el")
|
|
||||||
|
|
||||||
;; This is free and unencumbered software released into the public domain.
|
;; This is free and unencumbered software released into the public domain.
|
||||||
;;
|
;;
|
||||||
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
@ -23,6 +17,11 @@
|
||||||
;; which ones those are.
|
;; which ones those are.
|
||||||
|
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
(require 'parse-time)
|
||||||
|
(require 'dash)
|
||||||
|
(require 's)
|
||||||
|
|
||||||
|
(declare-function chronometrist-day-start "chronometrist-events.el")
|
||||||
|
|
||||||
(defun chronometrist-iso-timestamp->ts (timestamp)
|
(defun chronometrist-iso-timestamp->ts (timestamp)
|
||||||
"Return new ts struct, parsing TIMESTAMP with `parse-iso8601-time-string'."
|
"Return new ts struct, parsing TIMESTAMP with `parse-iso8601-time-string'."
|
||||||
|
|
|
@ -15,29 +15,26 @@
|
||||||
;;
|
;;
|
||||||
|
|
||||||
;;; Code:
|
;;; Code:
|
||||||
(require 'chronometrist)
|
|
||||||
|
|
||||||
(declare-function chronometrist-refresh "chronometrist.el")
|
(declare-function chronometrist-refresh "chronometrist.el")
|
||||||
(declare-function chronometrist-report-refresh "chronometrist-report.el")
|
|
||||||
(declare-function chronometrist-statistics-refresh "chronometrist-statistics.el")
|
|
||||||
|
|
||||||
(defvar chronometrist--timer-object nil)
|
(defvar chronometrist--timer-object nil)
|
||||||
|
|
||||||
|
(defcustom chronometrist-timer-hook nil
|
||||||
|
"Functions run by `chronometrist-timer'.")
|
||||||
|
|
||||||
(defun chronometrist-timer ()
|
(defun chronometrist-timer ()
|
||||||
"Refresh Chronometrist and related buffers.
|
"Refresh Chronometrist and related buffers.
|
||||||
|
|
||||||
Buffers will be refreshed only if they are visible and the user
|
Buffers will be refreshed only if they are visible and the user
|
||||||
is clocked in to a task."
|
is clocked in to a task."
|
||||||
(when (chronometrist-current-task) ;; FIXME - This line is currently
|
(when (chronometrist-backend-current-task chronometrist-backend-current) ;; FIXME - This line is currently
|
||||||
;; resulting in no refresh at midnight. When `chronometrist-entries' is
|
;; resulting in no refresh at midnight. When `chronometrist-entries' is
|
||||||
;; optimized to consume less CPU and avoid unnecessary parsing,
|
;; optimized to consume less CPU and avoid unnecessary parsing,
|
||||||
;; remove this condition.
|
;; remove this condition.
|
||||||
(when (get-buffer-window chronometrist-buffer-name)
|
(when (get-buffer-window chronometrist-buffer-name)
|
||||||
(chronometrist-refresh))
|
(chronometrist-refresh))
|
||||||
(when (get-buffer-window chronometrist-report-buffer-name)
|
(run-hooks 'chronometrist-timer-hook)))
|
||||||
(chronometrist-report-refresh))
|
|
||||||
(when (get-buffer-window chronometrist-statistics-buffer-name)
|
|
||||||
(chronometrist-statistics-refresh))))
|
|
||||||
|
|
||||||
(defun chronometrist-stop-timer ()
|
(defun chronometrist-stop-timer ()
|
||||||
"Stop the timer for Chronometrist buffers."
|
"Stop the timer for Chronometrist buffers."
|
||||||
|
|
|
@ -20,9 +20,8 @@
|
||||||
|
|
||||||
(require 'chronometrist-common)
|
(require 'chronometrist-common)
|
||||||
(require 'chronometrist-key-values)
|
(require 'chronometrist-key-values)
|
||||||
(require 'chronometrist-queries)
|
|
||||||
(require 'chronometrist-migrate)
|
(require 'chronometrist-migrate)
|
||||||
(require 'chronometrist-sexp)
|
(require 'chronometrist-backend)
|
||||||
|
|
||||||
;; This is free and unencumbered software released into the public domain.
|
;; This is free and unencumbered software released into the public domain.
|
||||||
;;
|
;;
|
||||||
|
@ -84,28 +83,8 @@
|
||||||
:group 'applications)
|
:group 'applications)
|
||||||
|
|
||||||
(defcustom chronometrist-file
|
(defcustom chronometrist-file
|
||||||
(locate-user-emacs-file "chronometrist.sexp")
|
(locate-user-emacs-file "chronometrist")
|
||||||
"Default path and name of the Chronometrist database.
|
"Default path and name (without extension) of the Chronometrist database."
|
||||||
|
|
||||||
It should be a text file containing plists in the form -
|
|
||||||
\(:name \"task name\"
|
|
||||||
[:tags TAGS]
|
|
||||||
[:comment \"comment\"]
|
|
||||||
[KEY-VALUE-PAIR ...]
|
|
||||||
:start \"TIME\"
|
|
||||||
:stop \"TIME\"\)
|
|
||||||
|
|
||||||
Where -
|
|
||||||
|
|
||||||
TAGS is a list. It can contain any strings and symbols.
|
|
||||||
|
|
||||||
KEY-VALUE-PAIR can be any keyword-value pairs. Currently,
|
|
||||||
Chronometrist ignores them.
|
|
||||||
|
|
||||||
TIME must be an ISO-8601 time string.
|
|
||||||
|
|
||||||
\(The square brackets here refer to optional elements, not
|
|
||||||
vectors.\)"
|
|
||||||
:type 'file)
|
:type 'file)
|
||||||
|
|
||||||
(defcustom chronometrist-buffer-name "*Chronometrist*"
|
(defcustom chronometrist-buffer-name "*Chronometrist*"
|
||||||
|
@ -145,15 +124,11 @@ The default is midnight, i.e. \"00:00:00\"."
|
||||||
Argument _BUTTON is for the purpose of using this command as a
|
Argument _BUTTON is for the purpose of using this command as a
|
||||||
button action."
|
button action."
|
||||||
(interactive)
|
(interactive)
|
||||||
(chronometrist-sexp-open-log))
|
(chronometrist-backend-open-file chronometrist-backend-current))
|
||||||
|
|
||||||
(defun chronometrist-common-create-file ()
|
|
||||||
"Create `chronometrist-file' if it doesn't already exist."
|
|
||||||
(chronometrist-sexp-create-file))
|
|
||||||
|
|
||||||
(defun chronometrist-task-active? (task)
|
(defun chronometrist-task-active? (task)
|
||||||
"Return t if TASK is currently clocked in, else nil."
|
"Return t if TASK is currently clocked in, else nil."
|
||||||
(equal (chronometrist-current-task) task))
|
(equal (chronometrist-backend-current-task chronometrist-backend-current) task))
|
||||||
|
|
||||||
(defun chronometrist-activity-indicator ()
|
(defun chronometrist-activity-indicator ()
|
||||||
"Return a string to indicate that a task is active.
|
"Return a string to indicate that a task is active.
|
||||||
|
@ -167,14 +142,14 @@ See custom variable `chronometrist-activity-indicator'."
|
||||||
;; HACK - these calls are commented out, because `chronometrist-entries' is
|
;; HACK - these calls are commented out, because `chronometrist-entries' is
|
||||||
;; called by both `chronometrist-refresh' and `chronometrist-refresh-file', and only the
|
;; called by both `chronometrist-refresh' and `chronometrist-refresh-file', and only the
|
||||||
;; latter should refresh from a file.
|
;; latter should refresh from a file.
|
||||||
;; (chronometrist-events-populate)
|
;; (chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
|
||||||
;; (chronometrist-events-clean)
|
;; (chronometrist-events-clean)
|
||||||
(->> (-sort #'string-lessp chronometrist-task-list)
|
(->> (-sort #'string-lessp chronometrist-task-list)
|
||||||
(--map-indexed
|
(--map-indexed
|
||||||
(let* ((task it)
|
(let* ((task it)
|
||||||
(index (number-to-string (1+ it-index)))
|
(index (number-to-string (1+ it-index)))
|
||||||
(task-button `(,task action chronometrist-toggle-task-button follow-link t))
|
(task-button `(,task action chronometrist-toggle-task-button follow-link t))
|
||||||
(task-time (chronometrist-format-time (chronometrist-task-time-one-day task)))
|
(task-time (chronometrist-format-time (chronometrist-backend-task-time chronometrist-backend-current task)))
|
||||||
(indicator (if (chronometrist-task-active? task) (chronometrist-activity-indicator) "")))
|
(indicator (if (chronometrist-task-active? task) (chronometrist-activity-indicator) "")))
|
||||||
(--> (vector index task-button task-time indicator)
|
(--> (vector index task-button task-time indicator)
|
||||||
(list task it)
|
(list task it)
|
||||||
|
@ -190,7 +165,7 @@ See custom variable `chronometrist-activity-indicator'."
|
||||||
(defun chronometrist-goto-last-task ()
|
(defun chronometrist-goto-last-task ()
|
||||||
"In the `chronometrist' buffer, move point to the line containing the last active task."
|
"In the `chronometrist' buffer, move point to the line containing the last active task."
|
||||||
(goto-char (point-min))
|
(goto-char (point-min))
|
||||||
(re-search-forward (plist-get (chronometrist-last) :name) nil t)
|
(re-search-forward (plist-get (chronometrist-backend-latest-record chronometrist-backend-current) :name) nil t)
|
||||||
(beginning-of-line))
|
(beginning-of-line))
|
||||||
|
|
||||||
(defun chronometrist-print-keybind (command &optional description firstonly)
|
(defun chronometrist-print-keybind (command &optional description firstonly)
|
||||||
|
@ -210,7 +185,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
||||||
;; (keybind-start-new (chronometrist-format-keybinds 'chronometrist-add-new-task chronometrist-mode-map))
|
;; (keybind-start-new (chronometrist-format-keybinds 'chronometrist-add-new-task chronometrist-mode-map))
|
||||||
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
|
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
|
||||||
(goto-char (point-max))
|
(goto-char (point-max))
|
||||||
(--> (chronometrist-active-time-one-day)
|
(--> (chronometrist-backend-active-time chronometrist-backend-current)
|
||||||
(chronometrist-format-time it)
|
(chronometrist-format-time it)
|
||||||
(format "%s%- 26s%s" w "Total" it)
|
(format "%s%- 26s%s" w "Total" it)
|
||||||
(insert it))
|
(insert it))
|
||||||
|
@ -259,7 +234,7 @@ Argument _FS-EVENT is ignored."
|
||||||
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
|
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
|
||||||
(if chronometrist--inhibit-read-p
|
(if chronometrist--inhibit-read-p
|
||||||
(setq chronometrist--inhibit-read-p nil)
|
(setq chronometrist--inhibit-read-p nil)
|
||||||
(chronometrist-events-populate)
|
(chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
|
||||||
(setq chronometrist-task-list (chronometrist-tasks-from-table))
|
(setq chronometrist-task-list (chronometrist-tasks-from-table))
|
||||||
(chronometrist-tags-history-populate chronometrist-events chronometrist-tags-history))
|
(chronometrist-tags-history-populate chronometrist-events chronometrist-tags-history))
|
||||||
(chronometrist-key-history-populate chronometrist-events chronometrist-key-history)
|
(chronometrist-key-history-populate chronometrist-events chronometrist-key-history)
|
||||||
|
@ -268,26 +243,25 @@ Argument _FS-EVENT is ignored."
|
||||||
|
|
||||||
(defun chronometrist-query-stop ()
|
(defun chronometrist-query-stop ()
|
||||||
"Ask the user if they would like to clock out."
|
"Ask the user if they would like to clock out."
|
||||||
(let ((task (chronometrist-current-task)))
|
(aand (chronometrist-backend-current-task chronometrist-backend-current)
|
||||||
(and task
|
(yes-or-no-p (format "Stop tracking time for %s? " it))
|
||||||
(yes-or-no-p (format "Stop tracking time for %s? " task))
|
(chronometrist-out))
|
||||||
(chronometrist-out))
|
t)
|
||||||
t))
|
|
||||||
|
|
||||||
(defun chronometrist-in (task &optional _prefix)
|
(defun chronometrist-in (task &optional _prefix)
|
||||||
"Clock in to TASK; record current time in `chronometrist-file'.
|
"Clock in to TASK; record current time in `chronometrist-file'.
|
||||||
TASK is the name of the task, a string. PREFIX is ignored."
|
TASK is the name of the task, a string. PREFIX is ignored."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
|
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
|
||||||
(chronometrist-sexp-new plist)
|
(chronometrist-backend-new-record chronometrist-backend-current plist)
|
||||||
(chronometrist-refresh)))
|
(chronometrist-refresh)))
|
||||||
|
|
||||||
(defun chronometrist-out (&optional _prefix)
|
(defun chronometrist-out (&optional _prefix)
|
||||||
"Record current moment as stop time to last s-exp in `chronometrist-file'.
|
"Record current moment as stop time to last s-exp in `chronometrist-file'.
|
||||||
PREFIX is ignored."
|
PREFIX is ignored."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(let ((plist (plist-put (chronometrist-last) :stop (chronometrist-format-time-iso8601))))
|
(let ((plist (plist-put (chronometrist-backend-latest-record chronometrist-backend-current) :stop (chronometrist-format-time-iso8601))))
|
||||||
(chronometrist-sexp-replace-last plist)))
|
(chronometrist-backend-replace-last chronometrist-backend-current plist)))
|
||||||
|
|
||||||
;; ## HOOKS ##
|
;; ## HOOKS ##
|
||||||
(defvar chronometrist-mode-hook nil
|
(defvar chronometrist-mode-hook nil
|
||||||
|
@ -393,7 +367,7 @@ Argument _BUTTON is for the purpose of using this as a button
|
||||||
action, and is ignored."
|
action, and is ignored."
|
||||||
(when current-prefix-arg
|
(when current-prefix-arg
|
||||||
(chronometrist-goto-nth-task (prefix-numeric-value current-prefix-arg)))
|
(chronometrist-goto-nth-task (prefix-numeric-value current-prefix-arg)))
|
||||||
(let ((current (chronometrist-current-task))
|
(let ((current (chronometrist-backend-current-task chronometrist-backend-current))
|
||||||
(at-point (chronometrist-task-at-point)))
|
(at-point (chronometrist-task-at-point)))
|
||||||
;; clocked in + point on current = clock out
|
;; clocked in + point on current = clock out
|
||||||
;; clocked in + point on some other task = clock out, clock in to task
|
;; clocked in + point on some other task = clock out, clock in to task
|
||||||
|
@ -408,7 +382,7 @@ action, and is ignored."
|
||||||
|
|
||||||
Argument _BUTTON is for the purpose of using this as a button
|
Argument _BUTTON is for the purpose of using this as a button
|
||||||
action, and is ignored."
|
action, and is ignored."
|
||||||
(let ((current (chronometrist-current-task)))
|
(let ((current (chronometrist-backend-current-task chronometrist-backend-current)))
|
||||||
(when current
|
(when current
|
||||||
(chronometrist-run-functions-and-clock-out current))
|
(chronometrist-run-functions-and-clock-out current))
|
||||||
(let ((task (read-from-minibuffer "New task name: " nil nil nil nil nil t)))
|
(let ((task (read-from-minibuffer "New task name: " nil nil nil nil nil t)))
|
||||||
|
@ -431,11 +405,11 @@ If INHIBIT-HOOKS is non-nil, the hooks
|
||||||
`chronometrist-before-out-functions', and
|
`chronometrist-before-out-functions', and
|
||||||
`chronometrist-after-out-functions' will not be run."
|
`chronometrist-after-out-functions' will not be run."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(let* ((empty-file (chronometrist-common-file-empty-p chronometrist-file))
|
(let* ((empty-file (chronometrist-common-file-empty-p (chronometrist-file-path)))
|
||||||
(nth (when prefix (chronometrist-goto-nth-task prefix)))
|
(nth (when prefix (chronometrist-goto-nth-task prefix)))
|
||||||
(at-point (chronometrist-task-at-point))
|
(at-point (chronometrist-task-at-point))
|
||||||
(target (or nth at-point))
|
(target (or nth at-point))
|
||||||
(current (chronometrist-current-task))
|
(current (chronometrist-backend-current-task chronometrist-backend-current))
|
||||||
(in-function (if inhibit-hooks
|
(in-function (if inhibit-hooks
|
||||||
#'chronometrist-in
|
#'chronometrist-in
|
||||||
#'chronometrist-run-functions-and-clock-in))
|
#'chronometrist-run-functions-and-clock-in))
|
||||||
|
@ -491,10 +465,10 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
||||||
(setq chronometrist--point (point))
|
(setq chronometrist--point (point))
|
||||||
(kill-buffer chronometrist-buffer-name)))
|
(kill-buffer chronometrist-buffer-name)))
|
||||||
(t (with-current-buffer buffer
|
(t (with-current-buffer buffer
|
||||||
(cond ((or (not (file-exists-p chronometrist-file))
|
(cond ((or (not (file-exists-p (chronometrist-file-path)))
|
||||||
(chronometrist-common-file-empty-p chronometrist-file))
|
(chronometrist-common-file-empty-p (chronometrist-file-path)))
|
||||||
;; first run
|
;; first run
|
||||||
(chronometrist-common-create-file)
|
(chronometrist-backend-create-file chronometrist-backend-current)
|
||||||
(let ((inhibit-read-only t))
|
(let ((inhibit-read-only t))
|
||||||
(chronometrist-common-clear-buffer buffer)
|
(chronometrist-common-clear-buffer buffer)
|
||||||
(insert "Welcome to Chronometrist! Hit RET to ")
|
(insert "Welcome to Chronometrist! Hit RET to ")
|
||||||
|
@ -517,7 +491,7 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
||||||
(chronometrist-goto-last-task))))
|
(chronometrist-goto-last-task))))
|
||||||
(unless chronometrist--fs-watch
|
(unless chronometrist--fs-watch
|
||||||
(setq chronometrist--fs-watch
|
(setq chronometrist--fs-watch
|
||||||
(file-notify-add-watch chronometrist-file '(change) #'chronometrist-refresh-file))))))))
|
(file-notify-add-watch (chronometrist-file-path) '(change) #'chronometrist-refresh-file))))))))
|
||||||
|
|
||||||
(provide 'chronometrist)
|
(provide 'chronometrist)
|
||||||
|
|
||||||
|
|
Reference in New Issue