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
|
||||
;;; 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")
|
||||
("ce" . "chronometrist-events")
|
||||
("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))
|
||||
Yikes!
|
||||
|
||||
** chronometrist [8%]
|
||||
1. [ ] Add =:stop= time when we call =chronometrist-kv-accept=, not when we quit the key-value prompt.
|
||||
2. [ ] Implement undo/redo by running undo-tree commands on chronometrist.sexp
|
||||
** New features [0%]
|
||||
1. [ ] undo/redo (by running undo-tree commands on chronometrist.sexp)
|
||||
* [ ] Possibly show what changes would be made, and prompt the user to confirm it.
|
||||
* 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.
|
||||
|
@ -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)
|
||||
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.
|
||||
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.
|
||||
* 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=.
|
||||
|
@ -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.
|
||||
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
|
||||
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%]
|
||||
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 set up Emacs to contribute::
|
||||
* How to create a new backend::
|
||||
|
||||
Explanation
|
||||
|
||||
|
@ -47,6 +48,7 @@ Explanation
|
|||
Chronometrist
|
||||
|
||||
* Optimization::
|
||||
* Backends::
|
||||
|
||||
Midnight-spanning events
|
||||
|
||||
|
@ -63,18 +65,15 @@ Tags and Key-Values
|
|||
Reference
|
||||
|
||||
* Legend of currently-used time formats::
|
||||
* chronometrist-backend.el: chronometrist-backendel.
|
||||
* chronometrist-common.el: chronometrist-commonel.
|
||||
* chronometrist-custom.el: chronometrist-customel.
|
||||
* chronometrist-diary-view.el: chronometrist-diary-viewel.
|
||||
* chronometrist.el: chronometristel.
|
||||
* chronometrist-events.el: chronometrist-eventsel.
|
||||
* chronometrist-migrate.el: chronometrist-migrateel.
|
||||
* 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-key-values.el: chronometrist-key-valuesel.
|
||||
* chronometrist-statistics-custom.el: chronometrist-statistics-customel.
|
||||
* chronometrist-statistics.el: chronometrist-statisticsel.
|
||||
* chronometrist-time.el: chronometrist-timeel.
|
||||
* chronometrist-timer.el: chronometrist-timerel.
|
||||
|
@ -98,6 +97,7 @@ Legend of currently-used time formats
|
|||
|
||||
@menu
|
||||
* How to set up Emacs to contribute::
|
||||
* How to create a new backend::
|
||||
@end menu
|
||||
|
||||
@node How to set up Emacs to contribute
|
||||
|
@ -123,6 +123,16 @@ From the project root, you can now run
|
|||
@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
|
||||
@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
|
||||
@itemize
|
||||
@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
|
||||
We use a text file containing s-expressions (easy for humans to read and write)
|
||||
@item
|
||||
|
@ -171,8 +181,6 @@ Have a useful, informative, interactive interface
|
|||
Support mouse and keyboard use equally
|
||||
@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
|
||||
@section Terminology
|
||||
|
||||
|
@ -214,6 +222,7 @@ Note - sometimes, when hacking or dealing with errors, timers may result in subt
|
|||
|
||||
@menu
|
||||
* Optimization::
|
||||
* Backends::
|
||||
@end menu
|
||||
|
||||
@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{}
|
||||
@enumerate
|
||||
@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
|
||||
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
|
||||
|
@ -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.
|
||||
@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
|
||||
@section Midnight-spanning events
|
||||
|
||||
|
@ -425,18 +473,15 @@ Each of these has a corresponding function to clear it and fill it with values -
|
|||
|
||||
@menu
|
||||
* Legend of currently-used time formats::
|
||||
* chronometrist-backend.el: chronometrist-backendel.
|
||||
* chronometrist-common.el: chronometrist-commonel.
|
||||
* chronometrist-custom.el: chronometrist-customel.
|
||||
* chronometrist-diary-view.el: chronometrist-diary-viewel.
|
||||
* chronometrist.el: chronometristel.
|
||||
* chronometrist-events.el: chronometrist-eventsel.
|
||||
* chronometrist-migrate.el: chronometrist-migrateel.
|
||||
* 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-key-values.el: chronometrist-key-valuesel.
|
||||
* chronometrist-statistics-custom.el: chronometrist-statistics-customel.
|
||||
* chronometrist-statistics.el: chronometrist-statisticsel.
|
||||
* chronometrist-time.el: chronometrist-timeel.
|
||||
* 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
|
||||
@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
|
||||
@section chronometrist-common.el
|
||||
|
||||
|
@ -568,24 +641,6 @@ ts -> ts
|
|||
@end itemize
|
||||
@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
|
||||
@section chronometrist-diary-view.el
|
||||
|
||||
|
@ -611,6 +666,18 @@ Command - chronometrist-diary-view (&optional date)
|
|||
|
||||
@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
|
||||
@item
|
||||
Internal Variable - chronometrist--task-history
|
||||
@item
|
||||
Internal Variable - chronometrist--point
|
||||
|
@ -758,36 +825,8 @@ Function - chronometrist-plist-pp-to-string (object)
|
|||
Function - chronometrist-plist-pp (object &optional stream)
|
||||
@end enumerate
|
||||
|
||||
@node chronometrist-queriesel
|
||||
@section chronometrist-queries.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
|
||||
@node chronometrist-reportel
|
||||
@section chronometrist-report.el
|
||||
|
||||
@enumerate
|
||||
@item
|
||||
|
@ -796,12 +835,6 @@ Custom variable - chronometrist-report-buffer-name
|
|||
Custom variable - chronometrist-report-week-start-day
|
||||
@item
|
||||
Custom variable - chronometrist-report-weekday-number-alist
|
||||
@end enumerate
|
||||
|
||||
@node chronometrist-reportel
|
||||
@section chronometrist-report.el
|
||||
|
||||
@enumerate
|
||||
@item
|
||||
Internal Variable - chronometrist-report--ui-date
|
||||
@item
|
||||
|
@ -912,19 +945,13 @@ Function - chronometrist-skip-query-prompt (task)
|
|||
Function - chronometrist-skip-query-reset (@math{_task})
|
||||
@end enumerate
|
||||
|
||||
@node chronometrist-statistics-customel
|
||||
@section chronometrist-statistics-custom.el
|
||||
|
||||
@enumerate
|
||||
@item
|
||||
Custom variable - chronometrist-statistics-buffer-name
|
||||
@end enumerate
|
||||
|
||||
@node chronometrist-statisticsel
|
||||
@section chronometrist-statistics.el
|
||||
|
||||
@enumerate
|
||||
@item
|
||||
Custom variable - chronometrist-statistics-buffer-name
|
||||
@item
|
||||
Internal Variable - chronometrist-statistics--ui-state
|
||||
@item
|
||||
Internal Variable - chronometrist-statistics--point
|
||||
|
@ -1079,31 +1106,59 @@ Function - chronometrist-goal-on-file-change ()
|
|||
|
||||
@enumerate
|
||||
@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)
|
||||
@item
|
||||
Function - chronometrist-sexp-open-log ()
|
||||
Method - chronometrist-to-hash ((backend chronometrist-sexp) table)
|
||||
@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
|
||||
Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
|
||||
Method - chronometrist-open-log ((backend chronometrist-sexp))
|
||||
@item
|
||||
Function - chronometrist-sexp-last ()
|
||||
Method - chronometrist-last ((backend chronometrist-sexp))
|
||||
@itemize
|
||||
@item
|
||||
-> plist
|
||||
@end itemize
|
||||
@item
|
||||
Function - chronometrist-sexp-current-task ()
|
||||
Method - chronometrist-current-task ((backend chronometrist-sexp))
|
||||
@item
|
||||
Function - chronometrist-sexp-events-populate ()
|
||||
Method - chronometrist-create-file ((backend chronometrist-sexp))
|
||||
@item
|
||||
Function - chronometrist-sexp-create-file ()
|
||||
@item
|
||||
Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
|
||||
Method - chronometrist-backend-new-record (((backend chronometrist-sexp)) plist)
|
||||
@item
|
||||
Function - chronometrist-sexp-delete-list (&optional arg)
|
||||
@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
|
||||
Command - chronometrist-sexp-reindent-buffer ()
|
||||
@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
|
||||
1. =cask= to install the project dependencies in a sandbox
|
||||
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
|
||||
:PROPERTIES:
|
||||
: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
|
||||
* 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
|
||||
* 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 ISO-8601 for timestamps (easy for humans to read and write) rather than UNIX epoch time
|
||||
4. Reduce human errors in tracking
|
||||
5. Have a useful, informative, interactive interface
|
||||
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
|
||||
: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.
|
||||
|
||||
**** 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...
|
||||
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.
|
||||
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=
|
||||
|
@ -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.
|
||||
|
||||
[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
|
||||
:PROPERTIES:
|
||||
: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:
|
||||
[[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.
|
||||
|
||||
[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
|
||||
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)
|
||||
* 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
|
||||
1. Variable - chronometrist-empty-time-string
|
||||
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)
|
||||
* 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
|
||||
1. Variable - chronometrist-diary-buffer-name
|
||||
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)
|
||||
|
||||
** chronometrist.el
|
||||
1. Internal Variable - chronometrist--task-history
|
||||
2. Internal Variable - chronometrist--point
|
||||
3. Internal Variable - chronometrist--inhibit-read-p
|
||||
4. Keymap - chronometrist-mode-map
|
||||
5. Command - chronometrist-open-log (&optional button)
|
||||
6. Function - chronometrist-common-create-file ()
|
||||
7. Function - chronometrist-task-active? (task)
|
||||
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
|
||||
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
|
||||
8. Function - chronometrist-use-goals? ()
|
||||
9. Function - chronometrist-activity-indicator ()
|
||||
10. Function - chronometrist-entries ()
|
||||
11. Function - chronometrist-task-at-point ()
|
||||
12. Function - chronometrist-goto-last-task ()
|
||||
13. Function - chronometrist-print-keybind (command &optional description firstonly)
|
||||
14. Function - chronometrist-print-non-tabular ()
|
||||
15. Function - chronometrist-goto-nth-task (n)
|
||||
16. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
|
||||
17. Function - chronometrist-refresh-file (fs-event)
|
||||
18. Command - chronometrist-query-stop ()
|
||||
19. Command - chronometrist-in (task &optional _prefix)
|
||||
20. Command - chronometrist-out (&optional _prefix)
|
||||
21. Variable - chronometrist-before-in-functions
|
||||
22. Variable - chronometrist-after-in-functions
|
||||
23. Variable - chronometrist-before-out-functions
|
||||
24. Variable - chronometrist-after-out-functions
|
||||
25. Function - chronometrist-run-functions-and-clock-in (task)
|
||||
26. Function - chronometrist-run-functions-and-clock-out (task)
|
||||
27. Keymap - chronometrist-mode-map
|
||||
28. Major Mode - chronometrist-mode
|
||||
29. Function - chronometrist-toggle-task-button (button)
|
||||
30. Function - chronometrist-add-new-task-button (button)
|
||||
31. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
|
||||
32. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
|
||||
33. Command - chronometrist-add-new-task ()
|
||||
34. Command - chronometrist (&optional arg)
|
||||
14. Function - chronometrist-use-goals? ()
|
||||
15. Function - chronometrist-activity-indicator ()
|
||||
16. Function - chronometrist-entries ()
|
||||
17. Function - chronometrist-task-at-point ()
|
||||
18. Function - chronometrist-goto-last-task ()
|
||||
19. Function - chronometrist-print-keybind (command &optional description firstonly)
|
||||
20. Function - chronometrist-print-non-tabular ()
|
||||
21. Function - chronometrist-goto-nth-task (n)
|
||||
22. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
|
||||
23. Function - chronometrist-refresh-file (fs-event)
|
||||
24. Command - chronometrist-query-stop ()
|
||||
25. Command - chronometrist-in (task &optional _prefix)
|
||||
26. Command - chronometrist-out (&optional _prefix)
|
||||
27. Variable - chronometrist-before-in-functions
|
||||
28. Variable - chronometrist-after-in-functions
|
||||
29. Variable - chronometrist-before-out-functions
|
||||
30. Variable - chronometrist-after-out-functions
|
||||
31. Function - chronometrist-run-functions-and-clock-in (task)
|
||||
32. Function - chronometrist-run-functions-and-clock-out (task)
|
||||
33. Keymap - chronometrist-mode-map
|
||||
34. Major Mode - chronometrist-mode
|
||||
35. Function - chronometrist-toggle-task-button (button)
|
||||
36. Function - chronometrist-add-new-task-button (button)
|
||||
37. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
|
||||
38. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
|
||||
39. Command - chronometrist-add-new-task ()
|
||||
40. Command - chronometrist (&optional arg)
|
||||
|
||||
** chronometrist-events.el
|
||||
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)
|
||||
7. Function - chronometrist-plist-pp (object &optional stream)
|
||||
|
||||
** chronometrist-queries.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
|
||||
** chronometrist-report.el
|
||||
1. Custom variable - chronometrist-report-buffer-name
|
||||
2. Custom variable - chronometrist-report-week-start-day
|
||||
3. Custom variable - chronometrist-report-weekday-number-alist
|
||||
|
||||
** chronometrist-report.el
|
||||
1. Internal Variable - chronometrist-report--ui-date
|
||||
2. Internal Variable - chronometrist-report--ui-week-dates
|
||||
3. Internal Variable - chronometrist-report--point
|
||||
4. Function - chronometrist-report-date ()
|
||||
5. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
|
||||
* ts-1 -> (ts-1 ... ts-7)
|
||||
6. Function - chronometrist-report-date->week-dates ()
|
||||
7. Function - chronometrist-report-entries ()
|
||||
8. Function - chronometrist-report-print-keybind (command &optional description firstonly)
|
||||
9. Function - chronometrist-report-print-non-tabular ()
|
||||
10. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
|
||||
11. Function - chronometrist-report-refresh-file (_fs-event)
|
||||
12. Keymap - chronometrist-report-mode-map
|
||||
13. Major Mode - chronometrist-report-mode
|
||||
14. Function - chronometrist-report (&optional keep-date)
|
||||
15. Function - chronometrist-report-previous-week (arg)
|
||||
16. Function - chronometrist-report-next-week (arg)
|
||||
4. Internal Variable - chronometrist-report--ui-date
|
||||
5. Internal Variable - chronometrist-report--ui-week-dates
|
||||
6. Internal Variable - chronometrist-report--point
|
||||
7. Function - chronometrist-report-date ()
|
||||
8. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
|
||||
* ts-1 -> (ts-1 ... ts-7)
|
||||
9. Function - chronometrist-report-date->week-dates ()
|
||||
10. Function - chronometrist-report-entries ()
|
||||
11. Function - chronometrist-report-print-keybind (command &optional description firstonly)
|
||||
12. Function - chronometrist-report-print-non-tabular ()
|
||||
13. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
|
||||
14. Function - chronometrist-report-refresh-file (_fs-event)
|
||||
15. Keymap - chronometrist-report-mode-map
|
||||
16. Major Mode - chronometrist-report-mode
|
||||
17. Function - chronometrist-report (&optional keep-date)
|
||||
18. Function - chronometrist-report-previous-week (arg)
|
||||
19. Function - chronometrist-report-next-week (arg)
|
||||
|
||||
** chronometrist-key-values.el
|
||||
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)
|
||||
33. Function - chronometrist-skip-query-reset (_task)
|
||||
|
||||
** chronometrist-statistics-custom.el
|
||||
1. Custom variable - chronometrist-statistics-buffer-name
|
||||
|
||||
** chronometrist-statistics.el
|
||||
1. Internal Variable - chronometrist-statistics--ui-state
|
||||
2. Internal Variable - chronometrist-statistics--point
|
||||
3. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
|
||||
* string &optional hash-table -> seconds
|
||||
4. Function - chronometrist-statistics-entries-internal (table)
|
||||
5. Function - chronometrist-statistics-entries ()
|
||||
6. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
|
||||
7. Function - chronometrist-statistics-print-non-tabular ()
|
||||
8. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
|
||||
9. Keymap - chronometrist-statistics-mode-map
|
||||
10. Major Mode - chronometrist-statistics-mode
|
||||
11. Command - chronometrist-statistics (&optional preserve-state)
|
||||
12. Command - chronometrist-statistics-previous-range (arg)
|
||||
13. Command - chronometrist-statistics-next-range (arg)
|
||||
1. Custom variable - chronometrist-statistics-buffer-name
|
||||
2. Internal Variable - chronometrist-statistics--ui-state
|
||||
3. Internal Variable - chronometrist-statistics--point
|
||||
4. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
|
||||
* string &optional hash-table -> seconds
|
||||
5. Function - chronometrist-statistics-entries-internal (table)
|
||||
6. Function - chronometrist-statistics-entries ()
|
||||
7. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
|
||||
8. Function - chronometrist-statistics-print-non-tabular ()
|
||||
9. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
|
||||
10. Keymap - chronometrist-statistics-mode-map
|
||||
11. Major Mode - chronometrist-statistics-mode
|
||||
12. Command - chronometrist-statistics (&optional preserve-state)
|
||||
13. Command - chronometrist-statistics-previous-range (arg)
|
||||
14. Command - chronometrist-statistics-next-range (arg)
|
||||
|
||||
** chronometrist-time.el
|
||||
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 ()
|
||||
|
||||
** chronometrist-sexp
|
||||
1. Custom variable - chronometrist-sexp-pretty-print-function
|
||||
2. Macro - chronometrist-sexp-in-file (file &rest body)
|
||||
3. Function - chronometrist-sexp-open-log ()
|
||||
4. Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
|
||||
5. Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
|
||||
6. Function - chronometrist-sexp-last ()
|
||||
* -> plist
|
||||
7. Function - chronometrist-sexp-current-task ()
|
||||
8. Function - chronometrist-sexp-events-populate ()
|
||||
9. Function - chronometrist-sexp-create-file ()
|
||||
10. Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
|
||||
11. Function - chronometrist-sexp-delete-list (&optional arg)
|
||||
12. Function - chronometrist-sexp-replace-last (plist)
|
||||
13. Command - chronometrist-sexp-reindent-buffer ()
|
||||
1. Class - chronometrist-sexp
|
||||
2. Object - chronometrist-sexp
|
||||
3. Custom variable - chronometrist-sexp-pretty-print-function
|
||||
4. Macro - chronometrist-sexp-in-file (file &rest body)
|
||||
5. Method - chronometrist-to-hash ((backend chronometrist-sexp) table)
|
||||
6. Method - chronometrist-from-hash ((backend chronometrist-sexp) table)
|
||||
7. Method - chronometrist-open-log ((backend chronometrist-sexp))
|
||||
8. Method - chronometrist-last ((backend chronometrist-sexp))
|
||||
* -> plist
|
||||
9. Method - chronometrist-current-task ((backend chronometrist-sexp))
|
||||
10. Method - chronometrist-create-file ((backend chronometrist-sexp))
|
||||
11. Method - chronometrist-backend-new-record (((backend chronometrist-sexp)) plist)
|
||||
12. Function - chronometrist-sexp-delete-list (&optional arg)
|
||||
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:
|
||||
# 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 'chronometrist-time)
|
||||
(require 'chronometrist-sexp)
|
||||
|
||||
;; ## VARIABLES ##
|
||||
;;; 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-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
|
||||
|
@ -59,7 +62,7 @@ file.")
|
|||
|
||||
(defun chronometrist-current-task ()
|
||||
"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 " "))
|
||||
"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)]
|
||||
(if (and (zerop h) (zerop m) (zerop s))
|
||||
" -"
|
||||
(let ((h (if (zerop h)
|
||||
blank
|
||||
(format "%2d:" h)))
|
||||
(m (cond ((and (zerop h)
|
||||
(zerop m))
|
||||
(let ((h (if (zerop h) blank (format "%2d:" h)))
|
||||
(m (cond ((and (zerop h) (zerop m))
|
||||
blank)
|
||||
((zerop h)
|
||||
(format "%2d:" m))
|
||||
(t
|
||||
(format "%02d:" m))))
|
||||
(s (if (and (zerop h)
|
||||
(zerop m))
|
||||
((zerop h) (format "%2d:" m))
|
||||
(t (format "%02d:" m))))
|
||||
(s (if (and (zerop h) (zerop m))
|
||||
(format "%2d" s)
|
||||
(format "%02d" 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
|
||||
`chronometrist-file')."
|
||||
(cl-loop for plist in events collect
|
||||
(let* ((start (chronometrist-iso-timestamp->ts
|
||||
(plist-get plist :start)))
|
||||
(stop (plist-get plist :stop))
|
||||
(stop (if stop
|
||||
(chronometrist-iso-timestamp->ts stop)
|
||||
(ts-now))))
|
||||
(cons start stop))))
|
||||
(cons (chronometrist-iso-timestamp->ts
|
||||
(plist-get plist :start))
|
||||
(aif (plist-get plist :stop)
|
||||
(chronometrist-iso-timestamp->ts it)
|
||||
(ts-now)))))
|
||||
|
||||
(defun chronometrist-ts-pairs->durations (ts-pairs)
|
||||
"Return the durations represented by TS-PAIRS.
|
||||
|
|
|
@ -11,9 +11,7 @@
|
|||
;;
|
||||
;; For more information, please refer to <https://unlicense.org>
|
||||
|
||||
;; (require 'chronometrist-plist-pp)
|
||||
(require 'chronometrist-common)
|
||||
(require 'chronometrist-sexp)
|
||||
(require 'ts)
|
||||
|
||||
;; external -
|
||||
|
@ -27,7 +25,8 @@
|
|||
;;; Code:
|
||||
|
||||
(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
|
||||
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
|
||||
were none."
|
||||
(clrhash chronometrist-events)
|
||||
(chronometrist-sexp-events-populate))
|
||||
(chronometrist-to-hash chronometrist-backend-current chronometrist-events))
|
||||
|
||||
(defun chronometrist-tasks-from-table ()
|
||||
"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; -*-
|
||||
|
||||
(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.
|
||||
;;
|
||||
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
|
@ -28,7 +14,17 @@
|
|||
|
||||
;;; 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
|
||||
"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
|
||||
Chronometrist - currently :name, :tags, :start, and :stop - will
|
||||
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-start (plist-get old-expr :start))
|
||||
(old-stop (plist-get old-expr :stop))
|
||||
|
@ -96,7 +92,7 @@ be removed."
|
|||
new-kvs
|
||||
`(:start ,old-start)
|
||||
(when old-stop `(:stop ,old-stop)))))
|
||||
(chronometrist-sexp-replace-last plist)))
|
||||
(chronometrist-backend-replace-last chronometrist-backend-current plist)))
|
||||
|
||||
;;;; TAGS ;;;;
|
||||
(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
|
||||
used in `chronometrist-before-out-functions'."
|
||||
(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-tags (plist-get last-expr :tags))
|
||||
(input (->> last-tags
|
||||
|
@ -353,7 +349,7 @@ It currently supports ido, ido-ubiquitous, ivy, and helm."
|
|||
"Prompt the user to enter keys.
|
||||
USED-KEYS are keys they have already added since the invocation
|
||||
of `chronometrist-kv-add'."
|
||||
(let ((key-suggestions (--> (chronometrist-last)
|
||||
(let ((key-suggestions (--> (chronometrist-backend-latest-record chronometrist-backend-current)
|
||||
(plist-get it :name)
|
||||
(gethash it chronometrist-key-history))))
|
||||
(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
|
||||
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
|
||||
(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)
|
||||
(mapcar #'symbol-name)
|
||||
(--map (s-chop-prefix ":" it)))))
|
||||
|
@ -405,7 +402,7 @@ used in `chronometrist-before-out-functions'."
|
|||
(with-current-buffer buffer
|
||||
(chronometrist-common-clear-buffer buffer)
|
||||
(chronometrist-kv-read-mode)
|
||||
(if (and (chronometrist-current-task) last-kvs)
|
||||
(if (and (chronometrist-backend-current-task chronometrist-backend-current) last-kvs)
|
||||
(progn
|
||||
(funcall chronometrist-sexp-pretty-print-function last-kvs buffer)
|
||||
(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>
|
||||
|
||||
|
@ -23,13 +23,12 @@
|
|||
(require 'chronometrist-plist-pp)
|
||||
|
||||
(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"
|
||||
;; are supported.)
|
||||
(defun chronometrist-migrate-populate (in-file)
|
||||
(defun chronometrist-migrate-populate-timelog (in-file)
|
||||
"Read data from IN-FILE to `chronometrist-migrate-table'.
|
||||
|
||||
IN-FILE should be a file in the format supported by timeclock.el.
|
||||
See `timeclock-log-data' for a description."
|
||||
(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)
|
||||
"Migrate your existing `timeclock-file' to the Chronometrist file format.
|
||||
|
||||
IN-FILE and OUT-FILE, if provided, are used as input and output
|
||||
file names respectively."
|
||||
(interactive `(,(if (featurep 'timeclock)
|
||||
(read-file-name (concat "timeclock file (default: "
|
||||
timeclock-file
|
||||
"): ")
|
||||
(read-file-name (format "timeclock file (default: %s): "
|
||||
timeclock-file)
|
||||
user-emacs-directory
|
||||
timeclock-file t)
|
||||
(read-file-name (concat "timeclock file: ")
|
||||
user-emacs-directory
|
||||
nil t))
|
||||
,(read-file-name (concat "Output file (default: "
|
||||
(locate-user-emacs-file "chronometrist.sexp")
|
||||
"): ")
|
||||
(read-file-name "timeclock file: " user-emacs-directory nil t))
|
||||
,(read-file-name (format "Output file (default: %s): "
|
||||
(locate-user-emacs-file "chronometrist.sexp"))
|
||||
user-emacs-directory
|
||||
(locate-user-emacs-file "chronometrist.sexp"))))
|
||||
(when (if (file-exists-p out-file)
|
||||
(yes-or-no-p (concat "Output file "
|
||||
out-file
|
||||
" already exists - overwrite? "))
|
||||
(yes-or-no-p (format "Output file %s already exists - overwrite? " out-file))
|
||||
t)
|
||||
(let ((output (find-file-noselect out-file)))
|
||||
(with-current-buffer output
|
||||
(chronometrist-common-clear-buffer output)
|
||||
(chronometrist-migrate-populate in-file)
|
||||
(chronometrist-migrate-populate-timelog in-file)
|
||||
(maphash (lambda (_key value)
|
||||
(chronometrist-plist-pp value output)
|
||||
(insert "\n\n"))
|
||||
chronometrist-migrate-table)
|
||||
(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 ()
|
||||
"Offer to import data from `timeclock-file' if `chronometrist-file' does not exist."
|
||||
(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;"
|
||||
" import data from %s ? ")
|
||||
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'."))))
|
||||
|
||||
(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 'subr-x)
|
||||
(require 'chronometrist-common)
|
||||
(require 'chronometrist-queries)
|
||||
(require 'chronometrist-timer)
|
||||
(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
|
||||
(setq chronometrist-report--ui-week-dates week-dates)
|
||||
(cl-loop for task in chronometrist-task-list collect
|
||||
(let* ((durations (--map (chronometrist-task-time-one-day task (chronometrist-date it))
|
||||
week-dates))
|
||||
(duration-strings (mapcar #'chronometrist-format-time
|
||||
durations))
|
||||
(total-duration (->> (-reduce #'+ durations)
|
||||
(chronometrist-format-time)
|
||||
(vector))))
|
||||
(list task
|
||||
(vconcat
|
||||
(vector task)
|
||||
duration-strings ;; vconcat converts lists to vectors
|
||||
total-duration))))))
|
||||
(let* ((durations (--map (chronometrist-backend-task-time chronometrist-backend-current task (chronometrist-date it))
|
||||
week-dates))
|
||||
(duration-strings (mapcar #'chronometrist-format-time
|
||||
durations))
|
||||
(total-duration (->> (-reduce #'+ durations)
|
||||
(chronometrist-format-time)
|
||||
(vector))))
|
||||
(list task
|
||||
(vconcat
|
||||
(vector task)
|
||||
duration-strings ;; vconcat converts lists to vectors
|
||||
total-duration))))))
|
||||
|
||||
(defun chronometrist-report-print-keybind (command &optional description firstonly)
|
||||
"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'."
|
||||
(let ((inhibit-read-only t)
|
||||
(w "\n ")
|
||||
(total-time-daily (->> chronometrist-report--ui-week-dates
|
||||
(mapcar #'chronometrist-date)
|
||||
(mapcar #'chronometrist-active-time-one-day))))
|
||||
(total-time-daily (->> (mapcar #'chronometrist-date chronometrist-report--ui-week-dates)
|
||||
(--map (chronometrist-backend-active-time chronometrist-backend-current it)))))
|
||||
(goto-char (point-min))
|
||||
(insert " ")
|
||||
(insert (make-string 25 ?\ ))
|
||||
(insert (mapconcat (lambda (ts)
|
||||
(ts-format "%F" ts))
|
||||
(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)
|
||||
"Re-read `chronometrist-file' and refresh the `chronometrist-report' buffer.
|
||||
Argument _FS-EVENT is ignored."
|
||||
(chronometrist-events-populate)
|
||||
(chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
|
||||
;; (chronometrist-events-clean)
|
||||
(chronometrist-report-refresh))
|
||||
|
||||
|
@ -217,10 +215,14 @@ Argument _FS-EVENT is ignored."
|
|||
(setq tabulated-list-sort-key '("Task" . nil))
|
||||
(tabulated-list-init-header)
|
||||
(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)
|
||||
(unless chronometrist--fs-watch
|
||||
(setq chronometrist--fs-watch
|
||||
(file-notify-add-watch chronometrist-file
|
||||
(file-notify-add-watch (chronometrist-file-path)
|
||||
'(change)
|
||||
#'chronometrist-refresh-file))))
|
||||
|
||||
|
@ -249,7 +251,7 @@ current week. Otherwise, display data from the week specified by
|
|||
(kill-buffer buffer))
|
||||
(t (unless keep-date
|
||||
(setq chronometrist-report--ui-date nil))
|
||||
(chronometrist-common-create-file)
|
||||
(chronometrist-backend-create-file chronometrist-backend-current)
|
||||
(chronometrist-report-mode)
|
||||
(switch-to-buffer buffer)
|
||||
(chronometrist-report-refresh-file nil)
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
;; chronometrist-file (-custom)
|
||||
;; 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
|
||||
"Function used to pretty print plists in `chronometrist-file'.
|
||||
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)
|
||||
(save-excursion ,@body)))
|
||||
|
||||
;;;; Queries
|
||||
(defun chronometrist-sexp-open-log ()
|
||||
"Open `chronometrist-file' in another window."
|
||||
(find-file-other-window chronometrist-file)
|
||||
(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
|
||||
;; # Migration #
|
||||
(cl-defmethod chronometrist-backend-to-hash ((backend chronometrist-sexp) table)
|
||||
(clrhash table)
|
||||
(chronometrist-sexp-in-file (chronometrist-file-path)
|
||||
(goto-char (point-min))
|
||||
(let ((index 0) expr pending-expr)
|
||||
(while (or pending-expr
|
||||
|
@ -60,8 +42,7 @@ were none."
|
|||
(setq pending-expr (cl-second split-expr))
|
||||
(cl-first split-expr))
|
||||
(t expr)))
|
||||
(new-value-date (->> (plist-get new-value :start)
|
||||
(s-left 10)))
|
||||
(new-value-date (s-left 10 (plist-get new-value :start)))
|
||||
(existing-value (gethash new-value-date chronometrist-events)))
|
||||
(unless pending-expr (cl-incf index))
|
||||
(puthash new-value-date
|
||||
|
@ -72,16 +53,32 @@ were none."
|
|||
chronometrist-events)))
|
||||
(unless (zerop index) index))))
|
||||
|
||||
;;;; Modifications
|
||||
(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-defmethod chronometrist-backend-from-hash ((backend chronometrist-sexp) table))
|
||||
|
||||
(cl-defun chronometrist-sexp-new (plist)
|
||||
"Add new PLIST at the end of `chronometrist-file'."
|
||||
(chronometrist-sexp-in-file chronometrist-file
|
||||
;; # Queries #
|
||||
(cl-defmethod chronometrist-backend-open-file ((backend chronometrist-sexp))
|
||||
(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))
|
||||
;; If we're adding the first s-exp in the file, don't add a
|
||||
;; newline before it
|
||||
|
@ -102,9 +99,8 @@ were none."
|
|||
(forward-sexp (or arg 1))
|
||||
(delete-region point-1 (point))))
|
||||
|
||||
(defun chronometrist-sexp-replace-last (plist)
|
||||
"Replace the last s-expression in `chronometrist-file' with PLIST."
|
||||
(chronometrist-sexp-in-file chronometrist-file
|
||||
(cl-defmethod chronometrist-backend-replace-last ((backend chronometrist-sexp) plist)
|
||||
(chronometrist-sexp-in-file (chronometrist-file-path)
|
||||
(goto-char (point-max))
|
||||
(unless (and (bobp) (bolp))
|
||||
(insert "\n"))
|
||||
|
@ -119,11 +115,68 @@ were none."
|
|||
;; :name should be removed from `chronometrist-task-list', but to ascertain
|
||||
;; that condition we would have to either read the entire file or
|
||||
;; 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)
|
||||
(setq chronometrist--inhibit-read-p t)
|
||||
(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 ()
|
||||
"Reindent the current buffer.
|
||||
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)
|
||||
(chronometrist-sexp-delete-list)
|
||||
(when (looking-at "\n*")
|
||||
(delete-region (match-beginning 0)
|
||||
(match-end 0)))
|
||||
(delete-region (match-beginning 0) (match-end 0)))
|
||||
(funcall chronometrist-sexp-pretty-print-function expr (current-buffer))
|
||||
(insert "\n")
|
||||
(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-time)
|
||||
(require 'chronometrist-timer)
|
||||
(require 'chronometrist-events)
|
||||
(require 'chronometrist-migrate)
|
||||
(require 'chronometrist-queries)
|
||||
|
||||
(declare-function chronometrist-refresh-file "chronometrist.el")
|
||||
|
||||
|
@ -97,12 +95,12 @@ TABLE should be a hash table - if not supplied,
|
|||
`chronometrist-events' is used."
|
||||
;; (cl-loop
|
||||
;; 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)))
|
||||
(let ((days 0)
|
||||
(per-day-time-list))
|
||||
(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
|
||||
(setq days (1+ days))
|
||||
(->> (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
|
||||
`chronometrist-events-subset'."
|
||||
(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)
|
||||
('week (* 100 (/ active-days 7.0)))))
|
||||
(active-percent (if (zerop active-days)
|
||||
|
@ -232,10 +230,14 @@ value of `revert-buffer-function'."
|
|||
(setq tabulated-list-sort-key '("Task" . nil))
|
||||
(tabulated-list-init-header)
|
||||
;; (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)
|
||||
(unless chronometrist--fs-watch
|
||||
(setq chronometrist--fs-watch
|
||||
(file-notify-add-watch chronometrist-file
|
||||
(file-notify-add-watch (chronometrist-file-path)
|
||||
'(change)
|
||||
#'chronometrist-refresh-file))))
|
||||
|
||||
|
@ -267,7 +269,7 @@ specified by `chronometrist-statistics--ui-state'."
|
|||
(setq chronometrist-statistics--ui-state `(:mode week
|
||||
:start ,week-start
|
||||
:end ,week-end)))
|
||||
(chronometrist-common-create-file)
|
||||
(chronometrist-backend-create-file chronometrist-backend-current)
|
||||
(chronometrist-statistics-mode)
|
||||
(switch-to-buffer buffer)
|
||||
(chronometrist-statistics-refresh))))))
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
|
||||
;; 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.
|
||||
;;
|
||||
;; Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
|
@ -23,6 +17,11 @@
|
|||
;; which ones those are.
|
||||
|
||||
;;; Code:
|
||||
(require 'parse-time)
|
||||
(require 'dash)
|
||||
(require 's)
|
||||
|
||||
(declare-function chronometrist-day-start "chronometrist-events.el")
|
||||
|
||||
(defun chronometrist-iso-timestamp->ts (timestamp)
|
||||
"Return new ts struct, parsing TIMESTAMP with `parse-iso8601-time-string'."
|
||||
|
|
|
@ -15,29 +15,26 @@
|
|||
;;
|
||||
|
||||
;;; Code:
|
||||
(require 'chronometrist)
|
||||
|
||||
(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)
|
||||
|
||||
(defcustom chronometrist-timer-hook nil
|
||||
"Functions run by `chronometrist-timer'.")
|
||||
|
||||
(defun chronometrist-timer ()
|
||||
"Refresh Chronometrist and related buffers.
|
||||
|
||||
Buffers will be refreshed only if they are visible and the user
|
||||
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
|
||||
;; optimized to consume less CPU and avoid unnecessary parsing,
|
||||
;; remove this condition.
|
||||
(when (get-buffer-window chronometrist-buffer-name)
|
||||
(chronometrist-refresh))
|
||||
(when (get-buffer-window chronometrist-report-buffer-name)
|
||||
(chronometrist-report-refresh))
|
||||
(when (get-buffer-window chronometrist-statistics-buffer-name)
|
||||
(chronometrist-statistics-refresh))))
|
||||
(run-hooks 'chronometrist-timer-hook)))
|
||||
|
||||
(defun chronometrist-stop-timer ()
|
||||
"Stop the timer for Chronometrist buffers."
|
||||
|
|
|
@ -20,9 +20,8 @@
|
|||
|
||||
(require 'chronometrist-common)
|
||||
(require 'chronometrist-key-values)
|
||||
(require 'chronometrist-queries)
|
||||
(require 'chronometrist-migrate)
|
||||
(require 'chronometrist-sexp)
|
||||
(require 'chronometrist-backend)
|
||||
|
||||
;; This is free and unencumbered software released into the public domain.
|
||||
;;
|
||||
|
@ -84,28 +83,8 @@
|
|||
:group 'applications)
|
||||
|
||||
(defcustom chronometrist-file
|
||||
(locate-user-emacs-file "chronometrist.sexp")
|
||||
"Default path and name 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.\)"
|
||||
(locate-user-emacs-file "chronometrist")
|
||||
"Default path and name (without extension) of the Chronometrist database."
|
||||
:type 'file)
|
||||
|
||||
(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
|
||||
button action."
|
||||
(interactive)
|
||||
(chronometrist-sexp-open-log))
|
||||
|
||||
(defun chronometrist-common-create-file ()
|
||||
"Create `chronometrist-file' if it doesn't already exist."
|
||||
(chronometrist-sexp-create-file))
|
||||
(chronometrist-backend-open-file chronometrist-backend-current))
|
||||
|
||||
(defun chronometrist-task-active? (task)
|
||||
"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 ()
|
||||
"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
|
||||
;; called by both `chronometrist-refresh' and `chronometrist-refresh-file', and only the
|
||||
;; latter should refresh from a file.
|
||||
;; (chronometrist-events-populate)
|
||||
;; (chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
|
||||
;; (chronometrist-events-clean)
|
||||
(->> (-sort #'string-lessp chronometrist-task-list)
|
||||
(--map-indexed
|
||||
(let* ((task it)
|
||||
(index (number-to-string (1+ it-index)))
|
||||
(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) "")))
|
||||
(--> (vector index task-button task-time indicator)
|
||||
(list task it)
|
||||
|
@ -190,7 +165,7 @@ See custom variable `chronometrist-activity-indicator'."
|
|||
(defun chronometrist-goto-last-task ()
|
||||
"In the `chronometrist' buffer, move point to the line containing the last active task."
|
||||
(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))
|
||||
|
||||
(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-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
|
||||
(goto-char (point-max))
|
||||
(--> (chronometrist-active-time-one-day)
|
||||
(--> (chronometrist-backend-active-time chronometrist-backend-current)
|
||||
(chronometrist-format-time it)
|
||||
(format "%s%- 26s%s" w "Total" 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'?
|
||||
(if chronometrist--inhibit-read-p
|
||||
(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))
|
||||
(chronometrist-tags-history-populate chronometrist-events chronometrist-tags-history))
|
||||
(chronometrist-key-history-populate chronometrist-events chronometrist-key-history)
|
||||
|
@ -268,26 +243,25 @@ Argument _FS-EVENT is ignored."
|
|||
|
||||
(defun chronometrist-query-stop ()
|
||||
"Ask the user if they would like to clock out."
|
||||
(let ((task (chronometrist-current-task)))
|
||||
(and task
|
||||
(yes-or-no-p (format "Stop tracking time for %s? " task))
|
||||
(chronometrist-out))
|
||||
t))
|
||||
(aand (chronometrist-backend-current-task chronometrist-backend-current)
|
||||
(yes-or-no-p (format "Stop tracking time for %s? " it))
|
||||
(chronometrist-out))
|
||||
t)
|
||||
|
||||
(defun chronometrist-in (task &optional _prefix)
|
||||
"Clock in to TASK; record current time in `chronometrist-file'.
|
||||
TASK is the name of the task, a string. PREFIX is ignored."
|
||||
(interactive "P")
|
||||
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
|
||||
(chronometrist-sexp-new plist)
|
||||
(chronometrist-backend-new-record chronometrist-backend-current plist)
|
||||
(chronometrist-refresh)))
|
||||
|
||||
(defun chronometrist-out (&optional _prefix)
|
||||
"Record current moment as stop time to last s-exp in `chronometrist-file'.
|
||||
PREFIX is ignored."
|
||||
(interactive "P")
|
||||
(let ((plist (plist-put (chronometrist-last) :stop (chronometrist-format-time-iso8601))))
|
||||
(chronometrist-sexp-replace-last plist)))
|
||||
(let ((plist (plist-put (chronometrist-backend-latest-record chronometrist-backend-current) :stop (chronometrist-format-time-iso8601))))
|
||||
(chronometrist-backend-replace-last chronometrist-backend-current plist)))
|
||||
|
||||
;; ## HOOKS ##
|
||||
(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."
|
||||
(when 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)))
|
||||
;; clocked in + point on current = clock out
|
||||
;; 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
|
||||
action, and is ignored."
|
||||
(let ((current (chronometrist-current-task)))
|
||||
(let ((current (chronometrist-backend-current-task chronometrist-backend-current)))
|
||||
(when current
|
||||
(chronometrist-run-functions-and-clock-out current))
|
||||
(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-after-out-functions' will not be run."
|
||||
(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)))
|
||||
(at-point (chronometrist-task-at-point))
|
||||
(target (or nth at-point))
|
||||
(current (chronometrist-current-task))
|
||||
(current (chronometrist-backend-current-task chronometrist-backend-current))
|
||||
(in-function (if inhibit-hooks
|
||||
#'chronometrist-in
|
||||
#'chronometrist-run-functions-and-clock-in))
|
||||
|
@ -491,10 +465,10 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
|||
(setq chronometrist--point (point))
|
||||
(kill-buffer chronometrist-buffer-name)))
|
||||
(t (with-current-buffer buffer
|
||||
(cond ((or (not (file-exists-p chronometrist-file))
|
||||
(chronometrist-common-file-empty-p chronometrist-file))
|
||||
(cond ((or (not (file-exists-p (chronometrist-file-path)))
|
||||
(chronometrist-common-file-empty-p (chronometrist-file-path)))
|
||||
;; first run
|
||||
(chronometrist-common-create-file)
|
||||
(chronometrist-backend-create-file chronometrist-backend-current)
|
||||
(let ((inhibit-read-only t))
|
||||
(chronometrist-common-clear-buffer buffer)
|
||||
(insert "Welcome to Chronometrist! Hit RET to ")
|
||||
|
@ -517,7 +491,7 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
|||
(chronometrist-goto-last-task))))
|
||||
(unless 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)
|
||||
|
||||
|
|
Reference in New Issue