Compare commits

..

3 Commits

Author SHA1 Message Date
contrapunctus 71f5328d41 Merge branch 'doc' into alert-macro 2022-02-14 22:13:22 +05:30
contrapunctus a1ec07c52e Merge branch 'doc' into alert-macro 2022-02-14 22:10:39 +05:30
contrapunctus aee7453ff8 Write alert system macro 2022-02-14 22:10:29 +05:30
23 changed files with 851 additions and 6263 deletions

View File

@ -5,12 +5,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## unreleased ## unreleased
### Added
1. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system.
2. New custom variable `chronometrist-key-value-preset-alist`, to define completion suggestions in advance.
3. New custom variable `chronometrist-key-value-use-database-history`, to control whether database history is used for key-value suggestions.
## [0.10.0] - 2022-02-15
### Changed ### Changed
1. The value of `chronometrist-file` must now be a file path _without extension._ Please update your configurations. 1. The value of `chronometrist-file` must now be a file path _without extension._ Please update your configurations.
2. The existing file format used by Chronometrist is now called the `plist` format. 2. The existing file format used by Chronometrist is now called the `plist` format.
@ -20,9 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1. Multiple backend support - new custom variable `chronometrist-active-backend` to determine active backend, new command `chronometrist-switch-backend` to temporarily select a backend (with completion). 1. Multiple backend support - new custom variable `chronometrist-active-backend` to determine active backend, new command `chronometrist-switch-backend` to temporarily select a backend (with completion).
2. New `plist-group` backend, reducing time taken in startup and after changes to the file. 2. New `plist-group` backend, reducing time taken in startup and after changes to the file.
3. Unified migration interface with command `chronometrist-migrate`. 3. Unified migration interface with command `chronometrist-migrate`.
4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database. Setting it also disables generation of the task list from the database, speeding up many operations. 4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database.
5. New command `chronometrist-discard-active`, to discard the active interval. 5. New command `chronometrist-discard-active`, to discard the active interval.
6. Debug logging messages - to view them, set `chronometrist-debug-enable`. 6. Debug logging messages - to view them, set `chronometrist-debug-enable`.
7. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system.
### Fixed ### Fixed
1. Code to detect the type of change made to the file has been rewritten, hopefully fixing some uncommon `read` errors and `args out of range` errors. 1. Code to detect the type of change made to the file has been rewritten, hopefully fixing some uncommon `read` errors and `args out of range` errors.

View File

@ -19,8 +19,7 @@ tangle:
--eval='(org-babel-tangle-file "chronometrist.org")' \ --eval='(org-babel-tangle-file "chronometrist.org")' \
--eval='(org-babel-tangle-file "chronometrist-key-values.org")' \ --eval='(org-babel-tangle-file "chronometrist-key-values.org")' \
--eval='(org-babel-tangle-file "chronometrist-spark.org")' \ --eval='(org-babel-tangle-file "chronometrist-spark.org")' \
--eval='(org-babel-tangle-file "chronometrist-third.org")' \ --eval='(org-babel-tangle-file "chronometrist-third.org")' ; \
--eval='(org-babel-tangle-file "chronometrist-sqlite.org")' ; \
cd .. cd ..
compile: tangle compile: tangle
@ -30,8 +29,7 @@ compile: tangle
--eval='(byte-compile-file "chronometrist.el")' \ --eval='(byte-compile-file "chronometrist.el")' \
--eval='(byte-compile-file "chronometrist-key-values.el")' \ --eval='(byte-compile-file "chronometrist-key-values.el")' \
--eval='(byte-compile-file "chronometrist-spark.el")' \ --eval='(byte-compile-file "chronometrist-spark.el")' \
--eval='(byte-compile-file "chronometrist-third.el")' \ --eval='(byte-compile-file "chronometrist-third.el")' ; \
--eval='(byte-compile-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint-check-declare: tangle lint-check-declare: tangle
@ -40,8 +38,7 @@ lint-check-declare: tangle
--eval='(check-declare-file "chronometrist.el")' \ --eval='(check-declare-file "chronometrist.el")' \
--eval='(check-declare-file "chronometrist-key-values.el")' \ --eval='(check-declare-file "chronometrist-key-values.el")' \
--eval='(check-declare-file "chronometrist-spark.el")' \ --eval='(check-declare-file "chronometrist-spark.el")' \
--eval='(check-declare-file "chronometrist-third.el")' \ --eval='(check-declare-file "chronometrist-third.el")' ; \
--eval='(check-declare-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint-checkdoc: tangle lint-checkdoc: tangle
@ -50,8 +47,7 @@ lint-checkdoc: tangle
--eval='(checkdoc-file "chronometrist.el")' \ --eval='(checkdoc-file "chronometrist.el")' \
--eval='(checkdoc-file "chronometrist-key-values.el")' \ --eval='(checkdoc-file "chronometrist-key-values.el")' \
--eval='(checkdoc-file "chronometrist-spark.el")' \ --eval='(checkdoc-file "chronometrist-spark.el")' \
--eval='(checkdoc-file "chronometrist-third.el")' \ --eval='(checkdoc-file "chronometrist-third.el")' ; \
--eval='(checkdoc-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint-package-lint: setup tangle lint-package-lint: setup tangle
@ -61,8 +57,7 @@ lint-package-lint: setup tangle
-f 'package-lint-batch-and-exit' chronometrist.el \ -f 'package-lint-batch-and-exit' chronometrist.el \
-f 'package-lint-batch-and-exit' chronometrist-key-values.el \ -f 'package-lint-batch-and-exit' chronometrist-key-values.el \
-f 'package-lint-batch-and-exit' chronometrist-spark.el \ -f 'package-lint-batch-and-exit' chronometrist-spark.el \
-f 'package-lint-batch-and-exit' chronometrist-third.el \ -f 'package-lint-batch-and-exit' chronometrist-third.el ; \
-f 'package-lint-batch-and-exit' chronometrist-sqlite.el ; \
cd .. cd ..
lint-relint: setup tangle lint-relint: setup tangle
@ -72,8 +67,7 @@ lint-relint: setup tangle
--eval='(relint-file "chronometrist.el")' \ --eval='(relint-file "chronometrist.el")' \
--eval='(relint-file "chronometrist-key-values.el")' \ --eval='(relint-file "chronometrist-key-values.el")' \
--eval='(relint-file "chronometrist-spark.el")' \ --eval='(relint-file "chronometrist-spark.el")' \
--eval='(relint-file "chronometrist-third.el")' \ --eval='(relint-file "chronometrist-third.el")' ; \
--eval='(relint-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint

1
README.md Symbolic link
View File

@ -0,0 +1 @@
manual.md

View File

@ -1 +0,0 @@
manual.org

318
TODO.org
View File

@ -8,12 +8,10 @@
* v0.3.0 [100%] * v0.3.0 [100%]
1. [X] +Release or+ bundle plist-pp.el 1. [X] +Release or+ bundle plist-pp.el
2. [X] -report - error opening when clocked in 2. [X] -report - error opening when clocked in
* MELPA, hosting [0%] * MELPA, hosting [0%]
1. [ ] merge =chronometrist-goal= into this repository 1. [ ] merge =chronometrist-goal= into this repository
2. [X] move project to codeberg.org or tildegit.org 2. [X] move project to codeberg.org or tildegit.org
3. [X] ...and then, update the MELPA recipes. 3. [X] ...and then, update the MELPA recipes.
* Chronometrist * Chronometrist
1. implement point restore for =chronometrist=/=chronometrist-report=/=chronometrist-statistics= in a kill-buffer-hook, so it works whenever the buffer is killed, not just when we call the command. 1. implement point restore for =chronometrist=/=chronometrist-report=/=chronometrist-statistics= in a kill-buffer-hook, so it works whenever the buffer is killed, not just when we call the command.
@ -36,13 +34,11 @@
5. Delete =:stop= time and save 5. Delete =:stop= time and save
6. Clock out again 6. Clock out again
7. Accept the already-inserted key-value. Duplicate! 7. Accept the already-inserted key-value. Duplicate!
** Features [50%] ** Features [50%]
1. [ ] Hook enhancement - can we supply the whole plist (including tags and key-values) to the task-start hooks, so users can have even smarter hook functions? 1. [ ] Hook enhancement - can we supply the whole plist (including tags and key-values) to the task-start hooks, so users can have even smarter hook functions?
* That would mean ensuring that -kv-read runs before other hooks. * That would mean ensuring that -kv-read runs before other hooks.
* Actually, it should be trivial to access the last expression in the file, so maybe this is unnecessary. * Actually, it should be trivial to access the last expression in the file, so maybe this is unnecessary.
2. [X] Revisit 'no reason' commands - maybe we should ask for tags and key-values with the regular commands, and skip them with the 'no reason' variants? 2. [X] Revisit 'no reason' commands - maybe we should ask for tags and key-values with the regular commands, and skip them with the 'no reason' variants?
*** Key-values [66%] *** Key-values [66%]
1. [X] bug - value-history appears in chronological rather than reverse chronological order 1. [X] bug - value-history appears in chronological rather than reverse chronological order
2. [X] generate history hash table from chronometrist-file. 2. [X] generate history hash table from chronometrist-file.
@ -74,7 +70,6 @@
15. [X] bug - tag history starts at the beginning (wtf?), value history is empty (wtf?) 15. [X] bug - tag history starts at the beginning (wtf?), value history is empty (wtf?)
* Does not occur on master, only on dev * Does not occur on master, only on dev
* Does not occur if you disable lexical binding (introduced in 4e89836) * Does not occur if you disable lexical binding (introduced in 4e89836)
**** Values **** Values
What forms can they take? What forms can they take?
1. Integers, floating point numbers - easy to identify via regexp 1. Integers, floating point numbers - easy to identify via regexp
@ -114,7 +109,6 @@
** Maybe ** Maybe
1. Add a new kind of plist - =(:name "NAME" :time "TIME" ...)= 1. Add a new kind of plist - =(:name "NAME" :time "TIME" ...)=
To record events for which the time interval is not relevant. These won't be shown in =chronometrist= - perhaps in a different buffer. To record events for which the time interval is not relevant. These won't be shown in =chronometrist= - perhaps in a different buffer.
* Optimization * Optimization
** DONE Don't update task list from data when user has set their own ** DONE Don't update task list from data when user has set their own
:PROPERTIES: :PROPERTIES:
@ -141,7 +135,6 @@ Defer (tag/key/value) history generation from file-change-time to prompt-time, a
+ Reduce memory use by allowing user to restrict number of s-expressions read. + Reduce memory use by allowing user to restrict number of s-expressions read.
+ Per-task history generation will create problems - e.g. values for a given key for one task won't be suggested for values for the same key in another 🤦 + Per-task history generation will create problems - e.g. values for a given key for one task won't be suggested for values for the same key in another 🤦
* Tags and keys are already task-sensitive; just don't make values task-sensitive. * Tags and keys are already task-sensitive; just don't make values task-sensitive.
** DONE Hash file contents to optimize for common changes ** DONE Hash file contents to optimize for common changes
Compare partial hashes of file to know what has changed - only update memory when necessary. Compare partial hashes of file to know what has changed - only update memory when necessary.
@ -150,24 +143,20 @@ Don't store entire file into memory; instead, split midnight-spanning intervals
+ Will increase load time for each forward/backward command in =chronometrist-report= and =chronometrist-statistics= + Will increase load time for each forward/backward command in =chronometrist-report= and =chronometrist-statistics=
+ Will reduce memory used by =chronometrist-events=. + Will reduce memory used by =chronometrist-events=.
* Further reductions can take place, if we automatically discard cache entries past a certain limit. (perhaps excluding data for the current day, week, or month) * Further reductions can take place, if we automatically discard cache entries past a certain limit. (perhaps excluding data for the current day, week, or month)
** Split and save midnight-spanning intervals to disk ** Split and save midnight-spanning intervals to disk
It will remove the need for an in-memory version of data with split midnight-spanning intervals. It will remove the need for an in-memory version of data with split midnight-spanning intervals.
+ Least memory use? + Least memory use?
+ Might make the file harder for a user to edit. + Might make the file harder for a user to edit.
** Save timestamps as UNIX epoch time ** Save timestamps as UNIX epoch time
+ Will (probably) greatly speed up time parsing and interval splitting. + Will (probably) greatly speed up time parsing and interval splitting.
+ Will greatly impede human editing of the file, too. 🤔 + Will greatly impede human editing of the file, too. 🤔
* An editing UI could help - pretty sure every timestamp edit I've ever made has been for the last interval, or at most an interval in today's data. * An editing UI could help - pretty sure every timestamp edit I've ever made has been for the last interval, or at most an interval in today's data.
- The editing UI could have commands for next/previous interval; one could also have a command which, in the file, opens the plist at point for editing. - The editing UI could have commands for next/previous interval; one could also have a command which, in the file, opens the plist at point for editing.
** Use an SQL database instead of a text file ** Use an SQL database instead of a text file
That is, assuming SQL can That is, assuming SQL can
1. find the difference between ISO-8601 timestamps 1. find the difference between ISO-8601 timestamps
2. compare ISO-8601 timestamps, and 2. compare ISO-8601 timestamps, and
3. do 1 and 2 faster than Elisp. 3. do 1 and 2 faster than Elisp.
** Change data structure ** Change data structure
Instead of storing each plist as-is, split each into two, one with the =:start= and one with the =:end=. Now we have the elegance of the one-plist-is-a-complete-interval schema in the file, and the ease and speed of detection of midnight spanning intervals in memory. Instead of storing each plist as-is, split each into two, one with the =:start= and one with the =:end=. Now we have the elegance of the one-plist-is-a-complete-interval schema in the file, and the ease and speed of detection of midnight spanning intervals in memory.
@ -179,7 +168,6 @@ Instead of storing each plist as-is, split each into two, one with the =:start=
(:stop "<timestamp>") (:stop "<timestamp>")
...) ...)
#+END_SRC #+END_SRC
** Change file timestamp format to =("<iso-date>" "<iso-time>")= ** Change file timestamp format to =("<iso-date>" "<iso-time>")=
** Change file schema ** Change file schema
Pros Pros
@ -237,7 +225,6 @@ Cons
+ Anything requiring split intervals will first look in =chronometrist-events=, and if not found, will read from the file and update =chronometrist-events=. + Anything requiring split intervals will first look in =chronometrist-events=, and if not found, will read from the file and update =chronometrist-events=.
+ When the file changes, use the file byte length and hash strategy described below to know whether to keep the cache. + When the file changes, use the file byte length and hash strategy described below to know whether to keep the cache.
+ Save cache to a file, so that event splitting is avoided by reading from that. + Save cache to a file, so that event splitting is avoided by reading from that.
** (old) ideas to make -refresh-file faster ** (old) ideas to make -refresh-file faster
1. Support multiple files, so we read and process lesser data when one of them changes. 1. Support multiple files, so we read and process lesser data when one of them changes.
2. Make file writing async 2. Make file writing async
@ -249,14 +236,12 @@ Cons
* Or even the first expression of the current date. That way, we just re-read the intervals for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates. * Or even the first expression of the current date. That way, we just re-read the intervals for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
6. [ ] Don't generate tag/keyword/value history from the entire log, just from the last N days (where N is user-customizable). 6. [ ] Don't generate tag/keyword/value history from the entire log, just from the last N days (where N is user-customizable).
7. [ ] Just why are we reading the whole file? ~chronometrist~ should not read more than a day; ~chronometrist-report~ should not read more than a week at a time, and so on. Make a branch which works on this logic, see if it is faster. 7. [ ] Just why are we reading the whole file? ~chronometrist~ should not read more than a day; ~chronometrist-report~ should not read more than a week at a time, and so on. Make a branch which works on this logic, see if it is faster.
* Certain * Certain
1. [ ] statistics UI for arbitrary queries 1. [ ] statistics UI for arbitrary queries
* user provides a predicate * user provides a predicate
* we show buffer with * we show buffer with
+ matched unique tag groups, and sparklines for time spent on each + matched unique tag groups, and sparklines for time spent on each
+ matched key-values, and sparklines for time spent on each + matched key-values, and sparklines for time spent on each
** plist-pp [66%] ** plist-pp [66%]
1. [X] plist-pp - work recursively for plist/alist values 1. [X] plist-pp - work recursively for plist/alist values
2. [ ] Fix alignment of alist dots 2. [ ] Fix alignment of alist dots
@ -388,7 +373,7 @@ Cons
4. [ ] Fix heading link to "midnight-spanning intervals" - jumps to the correct heading in HTML export, but jumps to its own self in Org mode. 4. [ ] Fix heading link to "midnight-spanning intervals" - jumps to the correct heading in HTML export, but jumps to its own self in Org mode.
5. [ ] Figure out some way to hide package prefixes in identifiers in Org mode (without actually affecting the contents, a la nameless-mode) 5. [ ] Figure out some way to hide package prefixes in identifiers in Org mode (without actually affecting the contents, a la nameless-mode)
** UX [33%] ** UX [30%]
1. [X] Optimization - (jonasw) store length and hash of previous file, see if the new file has the same hash until old-length bytes. 1. [X] Optimization - (jonasw) store length and hash of previous file, see if the new file has the same hash until old-length bytes.
* [X] Check for type of change to file * [X] Check for type of change to file
+ [X] Handle last expression being removed + [X] Handle last expression being removed
@ -396,13 +381,14 @@ Cons
* [X] BUG - if something was removed from the last expression (thereby decreasing the length of the file), =chronometrist-file-change-type= returns =t= instead of =:last= * [X] BUG - if something was removed from the last expression (thereby decreasing the length of the file), =chronometrist-file-change-type= returns =t= instead of =:last=
* [X] BUG - args out of range error when last plist is removed * [X] BUG - args out of range error when last plist is removed
2. [X] Optimization - generate history before querying, not when the file changes. 2. [X] Optimization - generate history before querying, not when the file changes.
3. [ ] When starting a project with time of "-" (i.e. not worked on today until now), immediately set time to 0 instead of waiting for the first timer refresh 3. [ ] Don't suggest nil when asking for first project on first run
4. [ ] Mouse commands should work only on buttons. 4. [ ] When starting a project with time of "-" (i.e. not worked on today until now), immediately set time to 0 instead of waiting for the first timer refresh
5. [X] Button actions should accept prefix arguments and behave exactly like their keyboard counterparts. 5. [ ] Mouse commands should work only on buttons.
6. [ ] mouse-3 should clock-out without asking for reason. 6. [X] Button actions should accept prefix arguments and behave exactly like their keyboard counterparts.
7. [ ] Some way to ask for the reason just before starting a project. Even when clocking out, the reason is asked /before/ clocking out, which adds time to the project. 7. [ ] mouse-3 should clock-out without asking for reason.
8. [ ] Allow calling chronometrist-in/out from anywhere-within-Emacs (a la timeclock) as well as from the chronometrist buffer. 8. [ ] Some way to ask for the reason just before starting a project. Even when clocking out, the reason is asked /before/ clocking out, which adds time to the project.
9. [ ] =chronometrist-timer= - if =chronometrist-file= is being edited (buffer exists and modified), don't refresh - this will (hopefully) prevent Emacs from going crazy with errors in trying to parse malformed data. 9. [ ] Allow calling chronometrist-in/out from anywhere-within-Emacs (a la timeclock) as well as from the chronometrist buffer.
10. [ ] =chronometrist-timer= - if =chronometrist-file= is being edited (buffer exists and modified), don't refresh - this will (hopefully) prevent Emacs from going crazy with errors in trying to parse malformed data.
* Maybe * Maybe
** New features ** New features
@ -472,7 +458,6 @@ Cons
- untouched project with target defined - red - untouched project with target defined - red
- target ±5 minutes - green - target ±5 minutes - green
- target*2 and above - red - target*2 and above - red
* viewing/editing frontends * viewing/editing frontends
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: viewing-editing-frontends :CUSTOM_ID: viewing-editing-frontends
@ -492,6 +477,7 @@ A way to represent s-expressions as Markdown, Org, etc, so the entire plist can
* see [[info:elisp#Swapping Text][swapping text]] * see [[info:elisp#Swapping Text][swapping text]]
*** A binding in the key-value buffer, which will insert the string at point in a buffer of a certain mode. *** A binding in the key-value buffer, which will insert the string at point in a buffer of a certain mode.
* documentation discoverability :doc: * documentation discoverability :doc:
Ensure that the user manual is easily discoverable. Ensure that the user manual is easily discoverable.
@ -701,13 +687,10 @@ list of tasks, one day, durations and graphs
2. [ ] set day (blank input to reset to today's date) 2. [ ] set day (blank input to reset to today's date)
3. [ ] set duration format 3. [ ] set duration format
4. [ ] display sparkline for total time 4. [ ] display sparkline for total time
*** report *** report
list of tasks, one week, durations only list of tasks, one week, durations only
*** statistics *** statistics
list of tasks, one week/month/year [fn:1] list of tasks, one week/month/year [fn:1]
*** details (intervals for a day) [16%] *** details (intervals for a day) [16%]
list of intervals, one day [fn:1] list of intervals, one day [fn:1]
+ [-] commands [50%] + [-] commands [50%]
@ -724,14 +707,12 @@ list of intervals, one day [fn:1]
Bugs Bugs
1. [ ] day-crossing events have the wrong duration and time. e.g. ~1 Exercise walking 30 minutes from 23:37 to 00:07~ 1. [ ] day-crossing events have the wrong duration and time. e.g. ~1 Exercise walking 30 minutes from 23:37 to 00:07~
** New frontends I want ** New frontends I want
*** task-key-values *** task-key-values
list of unique key-values for a task, one day [fn:1], durations and graphs list of unique key-values for a task, one day [fn:1], durations and graphs
+ commands [0%] + commands [0%]
1. [ ] set task 1. [ ] set task
2. [ ] set range 2. [ ] set range
*** task-graph [0%] *** task-graph [0%]
list of days, weeks, or months [fn:1], one task (with optional key-value filter [fn:2]), horizontal graph (and durations/stats?) list of days, weeks, or months [fn:1], one task (with optional key-value filter [fn:2]), horizontal graph (and durations/stats?)
+ columns + columns
@ -818,35 +799,12 @@ protocol implementation progress
20. [ ] [superclass] chronometrist-timer 20. [ ] [superclass] chronometrist-timer
* New backends :feature: * New backends :feature:
** Org time tracking 1. Org time tracking
** timeclock 2. timeclock
See docstring of =timeclock-log-data= 3. [[https://klog.jotaen.net/][klog]]
4. SQLite
** [[https://klog.jotaen.net/][klog]] 5. https://github.com/projecthamster/hamster
:PROPERTIES: + https://github.com/projecthamster/hamster/wiki/Our-datamodel
:CUSTOM_ID: new-backends-klog
:END:
** SQLite [0%]
Chronometrist allows the user to add arbitrary key-values. There are a few ways to do this in SQL, but [[https://mariadb.com/kb/en/entity-attribute-value-implementation/][this solution]] seems well-suited to Chronometrist - put commonly-queried keys (name, start, stop) into SQL columns, and put user key-values in a JSON/s-exp TEXT column. The latter can be queried separately. SQLite has support for [[https://www.sqlite.org/json1.html][JSON columns]].
1. [ ] Use =string-remove-prefix= rather than =s-chop-prefix=
2. [-] =to-file=
* [X] insert intervals
* [ ] insert date properties
* [ ] insert events
** Project Hamster
https://github.com/projecthamster/hamster
+ https://github.com/projecthamster/hamster/wiki/Our-datamodel
** [[https://gitlab.com/Harag/cl-naive-store][cl-naive-store]]
:PROPERTIES:
:CREATED: 2022-04-16T21:11:07+0530
:END:
Reimplementing the plist/plist-group backends in Common Lisp, without the buffer visualisation and buffer-movement commands provided by Emacs/Elisp, is not something I'm very enthusiastic about. Even if those turn out to not be major factors, implementing these backends in Elisp taught me that making your own database can be a lot of work. Especially if you just care about your application, and writing a database was not what you set out to do (I severely underestimated the work involved).
Yet, some may prefer the plist-group backend to an SQLite database. I trust that a cl-naive-store backend could have the same capabilities as the plist-group backend, but without the maintenance burden of an /ad hoc/ s-expression store.
* Functions doing similar things :code: * Functions doing similar things :code:
1. =task-time-one-day= 1. =task-time-one-day=
@ -854,10 +812,9 @@ Yet, some may prefer the plist-group backend to an SQLite database. I trust that
3. =events-to-durations= 3. =events-to-durations=
* Use ISO date for functions operating on dates :time:format: * Use ISO date for functions operating on dates :time:format:
* STARTED customizable task list [33%] :feature: * STARTED customizable task list :feature:
1. [X] Make =chronometrist-task-list= customizable 1. Interactive, buffer-local modification of task list, with completion (=completing-read-multiple=)
2. [ ] Interactive, buffer-local modification of task list, with completion (=completing-read-multiple=) 2. Adding a task? We can modify the task list, but how to persist it?
3. [ ] Adding a task? We can modify the task list, but how to persist it?
* Extend time range prompt :feature: * Extend time range prompt :feature:
Support inputs like "today", "yesterday", "5 days ago", etc. Support inputs like "today", "yesterday", "5 days ago", etc.
@ -879,10 +836,8 @@ Command to delete the interval currently being recorded. (bind to 'k')
* pretty printer * pretty printer
** fix handling of tagged alist group values :bug: ** fix handling of tagged alist group values :bug:
** put each list element on its own line if length of list exceeds fill-column :feature: ** put each list element on its own line if length of list exceeds fill-column :feature:
* verify command [20%] :feature: * verify command [20%] :feature:
:PROPERTIES:
:CUSTOM_ID: verify-command
:END:
With different checks, for different levels of speed/thoroughness - With different checks, for different levels of speed/thoroughness -
1. [X] (plist group) Sequence of plist group dates 1. [X] (plist group) Sequence of plist group dates
2. [ ] Check that every record (except the last) has a =:stop= 2. [ ] Check that every record (except the last) has a =:stop=
@ -893,9 +848,6 @@ With different checks, for different levels of speed/thoroughness -
* format changes * format changes
** multiple intervals per record :feature: ** multiple intervals per record :feature:
:PROPERTIES:
:CUSTOM_ID: multiple-intervals-per-record
:END:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(:name "<name>" (:name "<name>"
[<keyword-value pair>] ... [<keyword-value pair>] ...
@ -906,7 +858,7 @@ With different checks, for different levels of speed/thoroughness -
It doesn't do anything not already possible in the current formats. At best, it removes some duplication when the same task is "paused" and "resumed". It doesn't do anything not already possible in the current formats. At best, it removes some duplication when the same task is "paused" and "resumed".
+ Sometimes you pause and resume a task and don't want to split your key-values between >1 intervals (to avoid messing up completion suggestions for the future). An alternative means to the same end could be to add a key like =:deduct "<duration>"= or =:deduct ("<start time>" . "<stop time>")=. + Sometimes you pause and resume a task and don't want to split your key-values between >1 intervals (to avoid messing up completion suggestions for the future). An alternative means to the same end could be to add a key like =:deduct "<duration>"= or =:deduct ("<start time>" . "<stop time>")=.
- This will also make it easier to support formats like [[#new-backends-klog][klog]], which support this feature. - This will also make it easier to support formats like [[https://klog.jotaen.net/][klog]], which support this feature.
- It will probably complicate all data consuming code, though...think of =chronometrist-details= 🤔 - It will probably complicate all data consuming code, though...think of =chronometrist-details= 🤔
- An alternative idea could be to define a custom variable to hold the user's key values. If this variable is defined, it would be used instead of generating suggestions from past key-values. That way, such situations will not affect key-value suggestions. - An alternative idea could be to define a custom variable to hold the user's key values. If this variable is defined, it would be used instead of generating suggestions from past key-values. That way, such situations will not affect key-value suggestions.
@ -920,7 +872,7 @@ Records not used for time tracking, but to store data associated with a date or
[<keyword-value pair>] ...) [<keyword-value pair>] ...)
#+END_SRC #+END_SRC
** STARTED tagging dates with key-values [0%] :feature: ** STARTED tagging dates with key-values :feature:
:PROPERTIES: :PROPERTIES:
:CREATED: 2022-02-10T22:59:45+0530 :CREATED: 2022-02-10T22:59:45+0530
:CUSTOM_ID: date-key-values :CUSTOM_ID: date-key-values
@ -945,10 +897,6 @@ Alternatively, this would be easier to work with - easier to select the key-valu
The plist backend could theoretically be extended to support it, but I'd rather deprecate that format (since it suffers from performance issues) and not deal with it again. The plist backend could theoretically be extended to support it, but I'd rather deprecate that format (since it suffers from performance issues) and not deal with it again.
1. [ ] Update call sites of =chronometrist-latest-date-records= (which may now also contain key-values)
2. [ ] Update code accessing the hash table
3. [ ] Update =plist-pp= to handle
* undesired file watcher created after literate-elisp-load :bug: * undesired file watcher created after literate-elisp-load :bug:
:PROPERTIES: :PROPERTIES:
:CREATED: 2021-12-18T15:13:35+0530 :CREATED: 2021-12-18T15:13:35+0530
@ -963,18 +911,8 @@ The plist backend could theoretically be extended to support it, but I'd rather
:PROPERTIES: :PROPERTIES:
:CREATED: 2022-01-08T23:32:37+0530 :CREATED: 2022-01-08T23:32:37+0530
:END: :END:
1. [ ] Use active backend as the suggested input backend when migrating 1. [ ] default suggested input backend should be the active backend
2. [ ] Conserve file local variables prop line for text backends 2. [ ] conserve file local variables prop line
** make migrate command async [33%]
:PROPERTIES:
:CREATED: 2022-04-01T18:02:03+0530
:END:
=chronometrist-migrate= / =chronometrist-to-file= can take a long time to run, freezing Emacs until completion if run synchronously.
1. [X] basic async export
2. [ ] display message on start and completion
3. [ ] test the INPUT-FILE argument
* STARTED Support for the Third Time System [57%] :extension: * STARTED Support for the Third Time System [57%] :extension:
:PROPERTIES: :PROPERTIES:
@ -998,6 +936,12 @@ Additional rules:
+ /Avoid taking other unearned breaks/ if possibleso try to do personal tasks during normal or big breaks, or before/after your work day. + /Avoid taking other unearned breaks/ if possibleso try to do personal tasks during normal or big breaks, or before/after your work day.
#+END_QUOTE #+END_QUOTE
** Example flow
1. work for 30m
2. clock out - add 10m to =break-time=
3. clock in after a 5m break - subtract 5m from =break-time=
** Tasks
1. [X] =chronometrist-third-fraction= 1. [X] =chronometrist-third-fraction=
2. [X] =chronometrist-third-break-time= 2. [X] =chronometrist-third-break-time=
3. [X] on clock out, increment =break-time= and start timed notification 3. [X] on clock out, increment =break-time= and start timed notification
@ -1009,11 +953,6 @@ Additional rules:
* [ ] Hook function which prompts for work hours whenever you first clock in on a date. If work hours are defined in the custom variable, ask whether to use them - on negative answer, prompt for today's work hours. If work hours are not defined, prompt for today's work hours. * [ ] Hook function which prompts for work hours whenever you first clock in on a date. If work hours are defined in the custom variable, ask whether to use them - on negative answer, prompt for today's work hours. If work hours are not defined, prompt for today's work hours.
7. [ ] Displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time. 7. [ ] Displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time.
** Example flow
1. work for 30m
2. clock out - add 10m to =break-time=
3. clock in after a 5m break - subtract 5m from =break-time=
** Extras ** Extras
1. persist =break-time= between Emacs sessions 1. persist =break-time= between Emacs sessions
2. audible alerts 2. audible alerts
@ -1025,7 +964,7 @@ Additional rules:
2. edit stop time to add +20 minutes of work (30m total) 2. edit stop time to add +20 minutes of work (30m total)
* compare with old data in hash table - decrement break time added by old plist, increment break time added by new plist * compare with old data in hash table - decrement break time added by old plist, increment break time added by new plist
* STARTED Customizable alerts [50%] :feature:code: * STARTED Customizable alerts [50%]
:PROPERTIES: :PROPERTIES:
:CREATED: 2022-02-14T08:22:36+0530 :CREATED: 2022-02-14T08:22:36+0530
:CUSTOM_ID: customizable-alerts :CUSTOM_ID: customizable-alerts
@ -1047,198 +986,3 @@ Also define =chronometrist--timer-alist=, which associates =symbol= with a timer
2. [ ] use in =chronometrist-goal= and =chronometrist-third= 2. [ ] use in =chronometrist-goal= and =chronometrist-third=
[fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. [fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical.
* DONE Predefined key-values for tasks
:PROPERTIES:
:CREATED: 2022-02-17T23:34:17+0530
:END:
Benefits
1. Speeds up key-value completion for very large data sets.
2. Prevents key-value suggestions from being affected by [[#multiple-intervals-per-record][splitting of tasks across multiple intervals]].
#+BEGIN_SRC emacs-lisp
'(("Task" <plist> ...)
...)
#+END_SRC
* no-goal-alert - "You have spent on <task>" [0%] :bug:
:PROPERTIES:
:CREATED: 2022-02-25T00:19:49+0530
:END:
=chronometrist-goal-no-goal-alert= uses =chronometrist-task-time-one-day=, which calls =chronometrist-task-records-for-date= with the /current/ date.
1. [ ] Use the data for the latest date recorded in the database, rather than the current date.
* better error reporting
:PROPERTIES:
:CREATED: 2022-02-28T11:35:37+0530
:END:
Similar to [[#verify-command][verify command]], perhaps, but this is really about making error messages thrown by the program more meaningful to the user.
** record does not contain intervals
:PROPERTIES:
:CREATED: 2022-02-28T11:35:47+0530
:END:
* User-defined categories
:PROPERTIES:
:CREATED: 2022-03-31T22:44:34+0530
:END:
A task can be part of any number of categories. A category may also contain other categories. The categories, and the total time spent on each one, could be displayed in the main view, separate from the tasks.
* First run behaviour :ux:
:PROPERTIES:
:CREATED: 2022-04-02T17:28:33+0530
:END:
1. [ ] Look for any existing Chronometrist database in the default location. If one is found, use it to determine the default active backend. Otherwise, offer to import from one of the supported backends.
+ Some backends may not be installed - "see MELPA for more formats"
2. [ ] Don't suggest nil when asking for first project on first run
* STARTED Common Lisp port
:PROPERTIES:
:CREATED: 2022-04-03T09:36:53+0530
:END:
** DONE Replace =add-variable-watcher=
Common Lisp does not have Elisp's =add-variable-watcher=. The Elisp code used that to automatically update the =file= slot of the active backend object when the user changes the value of =chronometrist-file=. The new solution is to remove direct accesses to the =file= slot, define a =backend-file= method which returns the value of =file= if non-nil (it's usually nil), or derives a file path from =*user-data-file*= and the backend's extension.
It seems that the =file= slot is now unnecessary - =backend-file= can check =path= instead of =file=, and use either =path= or =*user-data-file*= to derive the full path.
1. [X] Remove the =file= slot
** DONE Use pathnames instead of namestrings
:PROPERTIES:
:CREATED: 2022-04-03T12:30:24+0530
:END:
** CLIM UI design
*** One-day task-duration table
:PROPERTIES:
:CREATED: 2022-04-10T08:09:18+0530
:END:
columns
1. index (for numeric argument)
2. task name
3. task duration today
4. task activity indicator
* click to clock in/out (hover to show icon)
5. graph of daily task durations
* Click on a point in the graph to view that day in the Details View.
click on row to show taller graph and inline display of either key-value breakdown, or interval breakdown.
commands
1. clock in, clock out, etc
2. toggle details for row
3. toggle details for all rows
4. next/previous day
5. set displayed day
6. cycle graph heights (self/comparative)
* the height of the graphs can be based on the range of each individual graph's own data ('self') or based on the range of all data ('comparative').
settings
1. default graph height
*** One-day category-duration table
:PROPERTIES:
:CREATED: 2022-04-10T08:11:20+0530
:END:
1. category
2. time duration today
commands
1. toggle details for row (show tasks in each category)
2. toggle details for all rows
*** One-day event-occurrence table
:PROPERTIES:
:CREATED: 2022-04-10T08:12:58+0530
:END:
("Useful for recording things like weight, medication, waist circumference, etc.")
1. index (prefix argument)
2. event name
3. time of occurrence
4. graph of past occurrences
commands
1. add event
2. record event
*** 1+ day editable log
:PROPERTIES:
:CREATED: 2022-04-08T18:23:42+0530
:END:
Displays tables of day properties, intervals, and events, grouped by day, in date/week/month/year range (default - today)
can select a day property/interval/event to edit its data
buttons to add new day properties, intervals, and events
When there are no intervals/events/day properties, show placeholder text - "No intervals recorded on this day." / "No events recorded on this day." / "No properties for this day."
Commands
1. filter contents
2. next/previous day (or range)
3. set displayed day (or range)
*** Line chart views
:PROPERTIES:
:CREATED: [2022-04-10 Sun 14:35]
:END:
Line chart with one or more (adjustable) metrics -
* duration for task
* task-property combinations
* numeric property value/function with numeric output
...over a time range (adjustable), with points for days/weeks/months/years (adjustable)
User can switch between single graph vs separate graphs, different types of graphs, etc.
Commands
1. new/open/save/delete view
2. add/remove metric (a line on the graph)
3. increase/decrease/set range, next/previous range
4. increase/decrease/set granularity
*** Default view (overview for a day)
Default view is a tiled composition of the one-day task-duration table, one-day grouped log, and the one-day category-duration table.
*** view/edit database as
:PROPERTIES:
:CREATED: 2022-04-08T19:56:38+0530
:END:
some way to view/edit the database in a different format, e.g. Lisp plists
* Documentation restructuring :doc:
:PROPERTIES:
:CREATED: 2022-04-14T18:04:21+0530
:END:
1. Move documentation from LPs to manual.org
2. Revamp tutorials - https://diataxis.fr/tutorials/#the-language-of-tutorials
3. Make how-to guides more general?
4. Move some how-to guide code into a contrib directory?
** tutorial and how-to topic revamp
tutorials
1. installation (MELPA) -> first run
2. adding the first task
3. clocking in, clocking out
4. customization - open a file when starting a task (move from how-to)
5. customization - warn when exiting with an active task
6. clocking in/out without running hooks
7. restarting or discarding an activity
8. key-value extension - installation -> customization -> testing/results
9. graph extension - installation -> customization -> testing/results
how-to guides
1. how to install (Git, MELPA, Quelpa, etc)
2. how to attach tags and properties to time intervals (cover different commands)
3. how to configure Emacs for Chronometrist development
* Concurrent tasks
:PROPERTIES:
:CREATED: 2022-04-19T22:12:54+0530
:END:
I'm not interested in using this myself, so it must be possible to enable/disable it.
+ Enabling/disabling it would just affect how the clock-in command works. Perhaps data verification, too.
How might it affect the data consumer?
1. Calculation of total time tracked (in any range, e.g. a day) will definitely require a more complex algorithm.

View File

@ -1,9 +0,0 @@
(defsystem chronometrist-sqlite
:version "0.0.1"
:serial t
:license "Unlicense"
:author "contrapunctus <contrapunctus at disroot dot org>"
:description "SQLite backend for Chronometrist"
:defsystem-depends-on ("literate-lisp")
:depends-on (:trivia :clsql-sqlite3)
:components ((:org "chronometrist")))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
(defsystem chronometrist
:version "0.0.1"
:serial t
:license "Unlicense"
:author "contrapunctus <contrapunctus at disroot dot org>"
:description "Friendly and extensible personal time tracker - common library"
:defsystem-depends-on ("literate-lisp")
:depends-on (:trivia)
:components ((:org "chronometrist")))

View File

@ -1,32 +0,0 @@
* Explanation
:PROPERTIES:
:CUSTOM_ID: explanation
:END:
This is a port of Chronometrist to Common Lisp.
Currently, it contains
1. a read-only plist-group backend
2. an incomplete SQLite backend
3. an incomplete CLIM frontend
** Source code overview
:PROPERTIES:
:CUSTOM_ID: source-code-overview
:END:
*** CLIM frontend
:PROPERTIES:
:CUSTOM_ID: clim-frontend
:END:
The CLIM frontend uses CLIM panes for each view of data Chronometrist provides. Each pane has a [[file:chronometrist.org::#display-pane][=display-pane=]] method to display its contents.
Currently, only one CLIM pane has been implemented, called the [[file:chronometrist.org::#task-duration-table-pane][task-duration table]]. By default, it displays a list of tasks, and each of their durations for today.
**** Tables
:PROPERTIES:
:CUSTOM_ID: tables
:END:
The tables in the CLIM frontend are designed with ease of extensibility in mind. Thus -
1. The columns displayed in this table are controlled by [[file:chronometrist.org::#*task-duration-table-spec*][=*task-duration-table-spec*=]], which contains a list of [[file:chronometrist.org::#column-specifier][=column-specifier=]] objects.
2. Each [[file:chronometrist.org::#column-specifier][=column-specifier=]] has two methods specializing on it, a [[file:chronometrist.org::#cell-data][=cell-data=]] method and a [[file:chronometrist.org::#cell-print][=cell-print=]] method. These determine the data contained by the cells of the column, and how that data is printed in the CLIM pane.
The function [[file:chronometrist.org::#task-duration-table-function][=task-duration-table-function=]] uses the [[file:chronometrist.org::#cell-data][=cell-data=]] methods to return the data of the table as a list of lists. This data is used by [[file:chronometrist.org::#display-pane][=display-pane=]] in conjunction with [[file:chronometrist.org::#cell-print][=cell-print=]] methods to display the data in the pane.

View File

@ -1,9 +0,0 @@
(defsystem chronometrist-clim
:version "0.0.1"
:serial t
:license "Unlicense"
:author "contrapunctus <contrapunctus at disroot dot org>"
:description "Friendly and extensible personal time tracker - CLIM GUI"
:defsystem-depends-on ("literate-lisp")
:depends-on (:trivia :mcclim)
:components ((:org "chronometrist")))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -192,29 +192,6 @@ used in `chronometrist-before-out-functions'."
"Add key-values to Chronometrist time intervals." "Add key-values to Chronometrist time intervals."
:group 'chronometrist) :group 'chronometrist)
(defcustom chronometrist-key-value-use-database-history t
"If non-nil, use database to generate key-value suggestions.
If nil, only `chronometrist-key-value-preset-alist' is used."
:type 'boolean
:group 'chronometrist-key-value)
(defcustom chronometrist-key-value-preset-alist nil
"Alist of key-value suggestions for `chronometrist-key-value' prompts.
Each element must be in the form (\"TASK\" <KEYWORD> <VALUE> ...)"
:type
'(repeat
(cons
(string :tag "Task name")
(repeat :tag "Property preset"
(plist :tag "Property"
;; :key-type 'keyword :value-type 'sexp
))))
:group 'chronometrist-key-values)
(defun chronometrist-key-value-get-presets (task)
"Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists."
(alist-get task chronometrist-key-value-preset-alist nil nil #'equal))
(defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*" (defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*"
"Name of buffer in which key-values are entered." "Name of buffer in which key-values are entered."
:group 'chronometrist-key-values :group 'chronometrist-key-values
@ -427,35 +404,30 @@ used in `chronometrist-before-out-functions'."
["Change tags and key-values for active/last interval" ["Change tags and key-values for active/last interval"
chronometrist-key-values-unified-prompt])) chronometrist-key-values-unified-prompt]))
(cl-defun chronometrist-key-values-unified-prompt (cl-defun chronometrist-key-values-unified-prompt (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name)))
(&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name)))
"Query user for tags and key-values to be added for TASK. "Query user for tags and key-values to be added for TASK.
Return t, to permit use in `chronometrist-before-out-functions'." Return t, to permit use in `chronometrist-before-out-functions'."
(interactive) (interactive)
(let* ((backend (chronometrist-active-backend)) (let* ((backend (chronometrist-active-backend))
(presets (--map (format "%S" it)
(chronometrist-key-value-get-presets task)))
(key-values (key-values
(when chronometrist-key-value-use-database-history (cl-loop for plist in (chronometrist-to-list backend)
(cl-loop for plist in (chronometrist-to-list backend) when (equal (plist-get plist :name) task)
when (equal (plist-get plist :name) task) collect
collect (let ((plist (chronometrist-plist-remove plist :name :start :stop)))
(let ((plist (chronometrist-plist-remove plist :name :start :stop))) (when plist (format "%S" plist)))
(when plist (format "%S" plist))) into key-value-plists
into key-value-plists finally return
finally return (--> (seq-filter #'identity key-value-plists)
(--> (seq-filter #'identity key-value-plists) (cl-remove-duplicates it :test #'equal :from-end t))))
(cl-remove-duplicates it :test #'equal :from-end t)))))
(latest (chronometrist-latest-record backend))) (latest (chronometrist-latest-record backend)))
(if (and (null presets) (null key-values)) (if (null key-values)
(progn (chronometrist-tags-add) (chronometrist-kv-add)) (progn (chronometrist-tags-add) (chronometrist-kv-add))
(let* ((candidates (append presets key-values)) (chronometrist-replace-last
(input (completing-read backend
(format "Key-values for %s: " task) (chronometrist-plist-update
candidates nil nil nil 'chronometrist-key-values-unified-prompt-history))) latest
(chronometrist-replace-last backend (read (completing-read (format "Key-values for %s: " task)
(chronometrist-plist-update latest key-values nil nil nil 'chronometrist-key-values-unified-prompt-history))))))
(read input))))))
t) t)
(provide 'chronometrist-key-values) (provide 'chronometrist-key-values)

View File

@ -330,39 +330,6 @@ used in `chronometrist-before-out-functions'."
"Add key-values to Chronometrist time intervals." "Add key-values to Chronometrist time intervals."
:group 'chronometrist) :group 'chronometrist)
#+END_SRC #+END_SRC
*** use-database-history :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-key-value-use-database-history t
"If non-nil, use database to generate key-value suggestions.
If nil, only `chronometrist-key-value-preset-alist' is used."
:type 'boolean
:group 'chronometrist-key-value)
#+END_SRC
*** preset-alist :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-key-value-preset-alist nil
"Alist of key-value suggestions for `chronometrist-key-value' prompts.
Each element must be in the form (\"TASK\" <KEYWORD> <VALUE> ...)"
:type
'(repeat
(cons
(string :tag "Task name")
(repeat :tag "Property preset"
(plist :tag "Property"
;; :key-type 'keyword :value-type 'sexp
))))
:group 'chronometrist-key-values)
#+END_SRC
**** get-presets
#+BEGIN_SRC emacs-lisp
(defun chronometrist-key-value-get-presets (task)
"Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists."
(alist-get task chronometrist-key-value-preset-alist nil nil #'equal))
#+END_SRC
*** kv-buffer-name :custom:variable: *** kv-buffer-name :custom:variable:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*" (defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*"
@ -370,7 +337,6 @@ Each element must be in the form (\"TASK\" <KEYWORD> <VALUE> ...)"
:group 'chronometrist-key-values :group 'chronometrist-key-values
:type 'string) :type 'string)
#+END_SRC #+END_SRC
*** key-history :variable: *** key-history :variable:
:PROPERTIES: :PROPERTIES:
:VALUE: hash table :VALUE: hash table
@ -741,48 +707,41 @@ Return t, to permit use in `chronometrist-before-out-functions'."
:CUSTOM_ID: unified-prompt :CUSTOM_ID: unified-prompt
:END: :END:
1. [ ] Improve appearance - is there an easy way to syntax highlight the plists? 1. [ ] Improve appearance - is there an easy way to syntax highlight the plists?
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-key-values-unified-prompt (cl-defun chronometrist-key-values-unified-prompt (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name)))
(&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name)))
"Query user for tags and key-values to be added for TASK. "Query user for tags and key-values to be added for TASK.
Return t, to permit use in `chronometrist-before-out-functions'." Return t, to permit use in `chronometrist-before-out-functions'."
(interactive) (interactive)
(let* ((backend (chronometrist-active-backend)) (let* ((backend (chronometrist-active-backend))
(presets (--map (format "%S" it)
(chronometrist-key-value-get-presets task)))
(key-values (key-values
(when chronometrist-key-value-use-database-history (cl-loop for plist in (chronometrist-to-list backend)
(cl-loop for plist in (chronometrist-to-list backend) when (equal (plist-get plist :name) task)
when (equal (plist-get plist :name) task) collect
collect (let ((plist (chronometrist-plist-remove plist :name :start :stop)))
(let ((plist (chronometrist-plist-remove plist :name :start :stop))) (when plist (format "%S" plist)))
(when plist (format "%S" plist))) into key-value-plists
into key-value-plists finally return
finally return (--> (seq-filter #'identity key-value-plists)
(--> (seq-filter #'identity key-value-plists) (cl-remove-duplicates it :test #'equal :from-end t))))
(cl-remove-duplicates it :test #'equal :from-end t)))))
(latest (chronometrist-latest-record backend))) (latest (chronometrist-latest-record backend)))
(if (and (null presets) (null key-values)) (if (null key-values)
(progn (chronometrist-tags-add) (chronometrist-kv-add)) (progn (chronometrist-tags-add) (chronometrist-kv-add))
(let* ((candidates (append presets key-values)) (chronometrist-replace-last
(input (completing-read backend
(format "Key-values for %s: " task) (chronometrist-plist-update
candidates nil nil nil 'chronometrist-key-values-unified-prompt-history))) latest
(chronometrist-replace-last backend (read (completing-read (format "Key-values for %s: " task)
(chronometrist-plist-update latest key-values nil nil nil 'chronometrist-key-values-unified-prompt-history))))))
(read input))))))
t) t)
#+END_SRC
#+END_SRC
* Provide * Provide
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(provide 'chronometrist-key-values) (provide 'chronometrist-key-values)
;;; chronometrist-key-values.el ends here ;;; chronometrist-key-values.el ends here
#+END_SRC #+END_SRC
* Local variables :noexport: * Local variables :NOEXPORT:
# Local Variables: # Local Variables:
# my-org-src-default-lang: "emacs-lisp"
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End: # End:

View File

@ -171,7 +171,7 @@ SCHEMA should be a vector as specified by `tabulated-list-format'."
;;; chronometrist-spark.el ends here ;;; chronometrist-spark.el ends here
#+END_SRC #+END_SRC
* Local variables :noexport: * Local variables :NOEXPORT:
# Local Variables: # Local Variables:
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End: # End:

View File

@ -1,210 +0,0 @@
;;; chronometrist-sqlite.el --- SQLite backend for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "24.3") (chronometrist "0.9.0") (emacsql-sqlite "1.0.0"))
;; Version: 0.1.0
;; 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:
;;
;; This package provides an SQLite 3 backend for Chronometrist.
;;; Code:
(require 'chronometrist)
(require 'emacsql-sqlite)
(defclass chronometrist-sqlite-backend (chronometrist-backend chronometrist-file-backend-mixin)
((extension :initform "sqlite"
:accessor chronometrist-backend-ext
:custom 'string)
(connection :initform nil
:initarg :connection
:accessor chronometrist-backend-connection)))
(chronometrist-register-backend
:sqlite "Store records in SQLite database."
(make-instance 'chronometrist-sqlite-backend :path chronometrist-file))
(cl-defmethod initialize-instance :after ((backend chronometrist-sqlite-backend)
&rest _initargs)
"Initialize connection for BACKEND based on its file."
(with-slots (file connection) backend
(when (and file (not connection))
(setf connection (emacsql-sqlite file)))))
(cl-defmethod chronometrist-create-file ((backend chronometrist-sqlite-backend) &optional file)
"Create file for BACKEND if it does not already exist.
Return the connection object from `emacsql-sqlite'."
(let* ((file (or file (chronometrist-backend-file backend)))
(db (or (chronometrist-backend-connection backend)
(setf (chronometrist-backend-connection backend)
(emacsql-sqlite file)))))
(cl-loop
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db)))
(defun chronometrist-iso-to-unix (timestamp)
(truncate (float-time (parse-iso8601-time-string timestamp))))
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(with-slots (connection) backend
(delete-file file)
(when connection (emacsql-close connection))
(setf connection nil)
(chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(emacsql connection [:insert-or-ignore-into dates [date] :values [$s1]]
(chronometrist-iso-to-unix date))
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)))))
(defun chronometrist-sqlite-insert-properties (backend plist)
"Insert properties from PLIST to (SQLite) BACKEND.
Properties are key-values excluding :name, :start, and :stop.
Insert nothing if the properties already exist. Return the
prop-id of the inserted or existing property."
(with-slots (connection) backend
(let* ((plist (chronometrist-plist-key-values plist))
(props (if (functionp chronometrist-sqlite-properties-function)
(funcall chronometrist-sqlite-properties-function plist)
plist)))
(emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]]
props)
(caar (emacsql connection [:select [prop-id]
:from properties
:where (= properties $s1)]
props)))))
(defun chronometrist-sqlite-properties-to-json (plist)
"Return PLIST as a JSON string."
(json-encode
;; `json-encode' throws an error when it thinks
;; it sees "alists" which have numbers as
;; "keys", so we convert any cons cells and any
;; lists starting with a number to vectors
(-tree-map (lambda (elt)
(cond ((chronometrist-pp-pair-p elt)
(vector (car elt) (cdr elt)))
((consp elt)
(vconcat elt))
(t elt)))
plist)))
(defcustom chronometrist-sqlite-properties-function nil
"Function used to control the encoding of user key-values.
The function must accept a single argument, the plist of key-values.
Any non-function value results in key-values being inserted as
s-expressions in a text column."
:type '(choice function (sexp :tag "Insert as s-expressions")))
(cl-defmethod chronometrist-insert ((backend chronometrist-sqlite-backend) plist)
(-let (((plist-1 plist-2) (chronometrist-split-plist plist))
(db (chronometrist-backend-connection backend)))
(cl-loop for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
do
(-let* (((&plist :name name :start start :stop stop) plist)
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))
(start-unix (chronometrist-iso-to-unix start))
(stop-unix (and stop (chronometrist-iso-to-unix stop)))
name-id interval-id prop-id)
;; insert name if it does not exist
(emacsql db [:insert-or-ignore-into interval-names [name]
:values [$s1]]
name)
;; insert interval properties if they do not exist
(setq prop-id (chronometrist-sqlite-insert-properties backend plist))
;; insert interval and associate it with the date
(setq name-id
(caar (emacsql db [:select [name-id]
:from interval-names
:where (= name $s1)]
name)))
(emacsql db [:insert-or-ignore-into intervals
[name-id start-time stop-time prop-id]
:values [$s1 $s2 $s3 $s4]]
name-id start-unix stop-unix prop-id)
(emacsql db [:insert-or-ignore-into dates [date]
:values [$s1]] date-unix)
(setq date-id
(caar (emacsql db [:select [date-id] :from dates
:where (= date $s1)]
date-unix))
interval-id
(caar (emacsql db [:select (funcall max interval-id) :from intervals])))
(emacsql db [:insert-into date-intervals [date-id interval-id]
:values [$s1 $s2]]
date-id interval-id)))))
(cl-defmethod chronometrist-edit-backend ((backend chronometrist-sqlite-backend))
(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-latest-record ((backend chronometrist-sqlite-backend) db)
(emacsql db [:select * :from events :order-by rowid :desc :limit 1]))
(cl-defmethod chronometrist-task-records-for-date ((backend chronometrist-sqlite-backend) task date-ts))
(cl-defmethod chronometrist-active-days ((backend chronometrist-sqlite-backend) task))
(cl-defmethod chronometrist-replace-last ((backend chronometrist-sqlite-backend) plist)
(emacsql db [:delete-from events :where ]))
(provide 'chronometrist-sqlite)
;;; chronometrist-sqlite.el ends here

View File

@ -1,277 +0,0 @@
#+TITLE: chronometrist-sqlite
#+AUTHOR: contrapunctus
#+SUBTITLE: SQLite backend for Chronometrist
#+PROPERTY: header-args :tangle yes :load yes
* Library headers and commentary
#+BEGIN_SRC emacs-lisp
;;; chronometrist-sqlite.el --- SQLite backend for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "24.3") (chronometrist "0.9.0") (emacsql-sqlite "1.0.0"))
;; Version: 0.1.0
;; 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>
#+END_SRC
"Commentary" is displayed when the user clicks on the package's entry in =M-x list-packages=.
#+BEGIN_SRC emacs-lisp
;;; Commentary:
;;
;; This package provides an SQLite 3 backend for Chronometrist.
#+END_SRC
* Dependencies
#+BEGIN_SRC emacs-lisp
;;; Code:
(require 'chronometrist)
(require 'emacsql-sqlite)
#+END_SRC
* Code
** class
#+BEGIN_SRC emacs-lisp
(defclass chronometrist-sqlite-backend (chronometrist-backend chronometrist-file-backend-mixin)
((extension :initform "sqlite"
:accessor chronometrist-backend-ext
:custom 'string)
(connection :initform nil
:initarg :connection
:accessor chronometrist-backend-connection)))
(chronometrist-register-backend
:sqlite "Store records in SQLite database."
(make-instance 'chronometrist-sqlite-backend :path chronometrist-file))
#+END_SRC
** initialize-instance :method:
#+BEGIN_SRC emacs-lisp
(cl-defmethod initialize-instance :after ((backend chronometrist-sqlite-backend)
&rest _initargs)
"Initialize connection for BACKEND based on its file."
(with-slots (file connection) backend
(when (and file (not connection))
(setf connection (emacsql-sqlite file)))))
#+END_SRC
** create-file
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-create-file ((backend chronometrist-sqlite-backend) &optional file)
"Create file for BACKEND if it does not already exist.
Return the connection object from `emacsql-sqlite'."
(let* ((file (or file (chronometrist-backend-file backend)))
(db (or (chronometrist-backend-connection backend)
(setf (chronometrist-backend-connection backend)
(emacsql-sqlite file)))))
(cl-loop
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db)))
#+END_SRC
** iso-to-unix :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-iso-to-unix (timestamp)
(truncate (float-time (parse-iso8601-time-string timestamp))))
#+END_SRC
** to-file :method:
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(with-slots (connection) backend
(delete-file file)
(when connection (emacsql-close connection))
(setf connection nil)
(chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(emacsql connection [:insert-or-ignore-into dates [date] :values [$s1]]
(chronometrist-iso-to-unix date))
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)))))
#+END_SRC
** insert-properties :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-sqlite-insert-properties (backend plist)
"Insert properties from PLIST to (SQLite) BACKEND.
Properties are key-values excluding :name, :start, and :stop.
Insert nothing if the properties already exist. Return the
prop-id of the inserted or existing property."
(with-slots (connection) backend
(let* ((plist (chronometrist-plist-key-values plist))
(props (if (functionp chronometrist-sqlite-properties-function)
(funcall chronometrist-sqlite-properties-function plist)
plist)))
(emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]]
props)
(caar (emacsql connection [:select [prop-id]
:from properties
:where (= properties $s1)]
props)))))
#+END_SRC
*** properties-to-json :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-sqlite-properties-to-json (plist)
"Return PLIST as a JSON string."
(json-encode
;; `json-encode' throws an error when it thinks
;; it sees "alists" which have numbers as
;; "keys", so we convert any cons cells and any
;; lists starting with a number to vectors
(-tree-map (lambda (elt)
(cond ((chronometrist-pp-pair-p elt)
(vector (car elt) (cdr elt)))
((consp elt)
(vconcat elt))
(t elt)))
plist)))
#+END_SRC
*** properties-function :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-sqlite-properties-function nil
"Function used to control the encoding of user key-values.
The function must accept a single argument, the plist of key-values.
Any non-function value results in key-values being inserted as
s-expressions in a text column."
:type '(choice function (sexp :tag "Insert as s-expressions")))
#+END_SRC
** insert
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-insert ((backend chronometrist-sqlite-backend) plist)
(-let (((plist-1 plist-2) (chronometrist-split-plist plist))
(db (chronometrist-backend-connection backend)))
(cl-loop for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
do
(-let* (((&plist :name name :start start :stop stop) plist)
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))
(start-unix (chronometrist-iso-to-unix start))
(stop-unix (and stop (chronometrist-iso-to-unix stop)))
name-id interval-id prop-id)
;; insert name if it does not exist
(emacsql db [:insert-or-ignore-into interval-names [name]
:values [$s1]]
name)
;; insert interval properties if they do not exist
(setq prop-id (chronometrist-sqlite-insert-properties backend plist))
;; insert interval and associate it with the date
(setq name-id
(caar (emacsql db [:select [name-id]
:from interval-names
:where (= name $s1)]
name)))
(emacsql db [:insert-or-ignore-into intervals
[name-id start-time stop-time prop-id]
:values [$s1 $s2 $s3 $s4]]
name-id start-unix stop-unix prop-id)
(emacsql db [:insert-or-ignore-into dates [date]
:values [$s1]] date-unix)
(setq date-id
(caar (emacsql db [:select [date-id] :from dates
:where (= date $s1)]
date-unix))
interval-id
(caar (emacsql db [:select (funcall max interval-id) :from intervals])))
(emacsql db [:insert-into date-intervals [date-id interval-id]
:values [$s1 $s2]]
date-id interval-id)))))
#+END_SRC
** open-file
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-edit-backend ((backend chronometrist-sqlite-backend))
(require 'sql)
(switch-to-buffer
(sql-comint-sqlite 'sqlite (list file))))
#+END_SRC
** latest-record
#+BEGIN_SRC emacs-lisp
;; SELECT * FROM TABLE WHERE ID = (SELECT MAX(ID) FROM TABLE);
;; SELECT * FROM tablename ORDER BY column DESC LIMIT 1;
(cl-defmethod chronometrist-latest-record ((backend chronometrist-sqlite-backend) db)
(emacsql db [:select * :from events :order-by rowid :desc :limit 1]))
#+END_SRC
** task-records-for-date
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-task-records-for-date ((backend chronometrist-sqlite-backend) task date-ts))
#+END_SRC
** active-days
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-active-days ((backend chronometrist-sqlite-backend) task))
#+END_SRC
** replace-last
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-replace-last ((backend chronometrist-sqlite-backend) plist)
(emacsql db [:delete-from events :where ]))
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp
(provide 'chronometrist-sqlite)
;;; chronometrist-sqlite.el ends here
#+END_SRC
* Local variables :noexport:
# Local Variables:
# eval: (when (or (package-installed-p 'emacsql) (featurep 'emacsql)) (require 'emacsql) (emacsql-fix-vector-indentation))
# eval: (when (or (package-installed-p 'literate-elisp) (featurep 'literate-elisp)) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End:

View File

@ -42,7 +42,7 @@
;; divisor:1 ends here ;; divisor:1 ends here
;; [[file:chronometrist-third.org::*duration-format][duration-format:1]] ;; [[file:chronometrist-third.org::*duration-format][duration-format:1]]
(defcustom chronometrist-third-duration-format "%H, %M and %S%z" (defcustom chronometrist-third-duration-format "%H, %M, and %S%z"
"Format string for durations, passed to `format-seconds'." "Format string for durations, passed to `format-seconds'."
:type 'string) :type 'string)
;; duration-format:1 ends here ;; duration-format:1 ends here
@ -131,19 +131,19 @@ and stopped when they clock in."
"Stop alert timers and update break time." "Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers) (chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time) (unless (zerop chronometrist-third-break-time)
(-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend))))
(used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop)))
(used-break-string (format-seconds chronometrist-third-duration-format used-break)) (new-break-time (- chronometrist-third-break-time used-break-duration))
(new-break (- chronometrist-third-break-time used-break)) (old-break-time chronometrist-third-break-time)
(old-break chronometrist-third-break-time)) (used-break-duration-string (format-seconds chronometrist-third-duration-format
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0)) used-break-duration)))
(setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0))
(alert (alert
(if (zerop chronometrist-third-break-time) (if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)" (format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break) old-break-time used-break-duration-string)
used-break-string)
(format "You have used %s of your break time (%s left)" (format "You have used %s of your break time (%s left)"
used-break-string used-break-duration-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
;; clock-in:1 ends here ;; clock-in:1 ends here

View File

@ -56,7 +56,7 @@
** duration-format :custom:variable: ** duration-format :custom:variable:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-duration-format "%H, %M and %S%z" (defcustom chronometrist-third-duration-format "%H, %M, and %S%z"
"Format string for durations, passed to `format-seconds'." "Format string for durations, passed to `format-seconds'."
:type 'string) :type 'string)
#+END_SRC #+END_SRC
@ -155,19 +155,19 @@ and stopped when they clock in."
"Stop alert timers and update break time." "Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers) (chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time) (unless (zerop chronometrist-third-break-time)
(-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend))))
(used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop)))
(used-break-string (format-seconds chronometrist-third-duration-format used-break)) (new-break-time (- chronometrist-third-break-time used-break-duration))
(new-break (- chronometrist-third-break-time used-break)) (old-break-time chronometrist-third-break-time)
(old-break chronometrist-third-break-time)) (used-break-duration-string (format-seconds chronometrist-third-duration-format
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0)) used-break-duration)))
(setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0))
(alert (alert
(if (zerop chronometrist-third-break-time) (if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)" (format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break) old-break-time used-break-duration-string)
used-break-string)
(format "You have used %s of your break time (%s left)" (format "You have used %s of your break time (%s left)"
used-break-string used-break-duration-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
#+END_SRC #+END_SRC
@ -206,7 +206,7 @@ break time is up."
;;; chronometrist-third.el ends here ;;; chronometrist-third.el ends here
#+END_SRC #+END_SRC
* Local variables :noexport: * Local variables :NOEXPORT:
# Local Variables: # Local Variables:
# my-org-src-default-lang: "emacs-lisp" # my-org-src-default-lang: "emacs-lisp"
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))

View File

@ -5,7 +5,7 @@
;; Keywords: calendar ;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist ;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "27.1") (dash "2.16.0") (seq "2.20") (ts "0.2")) ;; Package-Requires: ((emacs "27.1") (dash "2.16.0") (seq "2.20") (ts "0.2"))
;; Version: 0.10.0 ;; Version: 0.9.0
;; This is free and unencumbered software released into the public domain. ;; This is free and unencumbered software released into the public domain.
;; ;;
@ -315,8 +315,8 @@ Return value is a ts struct (see `ts.el')."
(plist-put :stop stop-2)))))))) (plist-put :stop stop-2))))))))
;; split-plist:1 ends here ;; split-plist:1 ends here
;; [[file:chronometrist.org::*ht-update][ht-update:1]] ;; [[file:chronometrist.org::*events-update][events-update:1]]
(defun chronometrist-ht-update (plist hash-table &optional replace) (defun chronometrist-events-update (plist hash-table &optional replace)
"Return HASH-TABLE with PLIST added as the latest interval. "Return HASH-TABLE with PLIST added as the latest interval.
If REPLACE is non-nil, replace the last interval with PLIST." If REPLACE is non-nil, replace the last interval with PLIST."
(let* ((date (->> (plist-get plist :start) (let* ((date (->> (plist-get plist :start)
@ -327,29 +327,28 @@ If REPLACE is non-nil, replace the last interval with PLIST."
(append it (list plist)) (append it (list plist))
(puthash date it hash-table)) (puthash date it hash-table))
hash-table)) hash-table))
;; ht-update:1 ends here ;; events-update:1 ends here
;; [[file:chronometrist.org::*ht-last-date][ht-last-date:1]] ;; [[file:chronometrist.org::*last-date][last-date:1]]
(defun chronometrist-ht-last-date (hash-table) (defun chronometrist-events-last-date (hash-table)
"Return an ISO-8601 date string for the latest date present in `chronometrist-events'." "Return an ISO-8601 date string for the latest date present in `chronometrist-events'."
(--> (hash-table-keys hash-table) (--> (hash-table-keys hash-table)
(sort it #'string-lessp)
(last it) (last it)
(cl-first it))) (car it)))
;; ht-last-date:1 ends here ;; last-date:1 ends here
;; [[file:chronometrist.org::*ht-last][ht-last:1]] ;; [[file:chronometrist.org::*events-last][events-last:1]]
(cl-defun chronometrist-ht-last (&optional (backend (chronometrist-active-backend))) (cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend)))
"Return the last plist from `chronometrist-events'." "Return the last plist from `chronometrist-events'."
(let* ((hash-table (chronometrist-backend-hash-table backend)) (let* ((hash-table (chronometrist-backend-hash-table backend))
(last-date (chronometrist-ht-last-date hash-table))) (last-date (chronometrist-events-last-date hash-table)))
(--> (gethash last-date hash-table) (--> (gethash last-date hash-table)
(last it) (last it)
(car it)))) (car it))))
;; ht-last:1 ends here ;; events-last:1 ends here
;; [[file:chronometrist.org::#program-data-structures-ht-subset][ht-subset:1]] ;; [[file:chronometrist.org::#program-data-structures-events-subset][events-subset:1]]
(defun chronometrist-ht-subset (start end hash-table) (defun chronometrist-events-subset (start end hash-table)
"Return a subset of HASH-TABLE. "Return a subset of HASH-TABLE.
The subset will contain values between dates START and END (both The subset will contain values between dates START and END (both
inclusive). inclusive).
@ -364,7 +363,7 @@ treated as though their time is 00:00:00."
(puthash key value subset))) (puthash key value subset)))
hash-table) hash-table)
subset)) subset))
;; ht-subset:1 ends here ;; events-subset:1 ends here
;; [[file:chronometrist.org::*task-time-one-day][task-time-one-day:1]] ;; [[file:chronometrist.org::*task-time-one-day][task-time-one-day:1]]
(cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend))) (cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend)))
@ -528,11 +527,6 @@ SECONDS is less than 60, return a blank string."
minutes minute-string))))) minutes minute-string)))))
;; format-duration-long:1 ends here ;; format-duration-long:1 ends here
;; [[file:chronometrist.org::*iso-to-date][iso-to-date:1]]
(defun chronometrist-iso-to-date (timestamp)
(cl-first (split-string timestamp "T")))
;; iso-to-date:1 ends here
;; [[file:chronometrist.org::*normalize-whitespace][normalize-whitespace:1]] ;; [[file:chronometrist.org::*normalize-whitespace][normalize-whitespace:1]]
(defun chronometrist-pp-normalize-whitespace () (defun chronometrist-pp-normalize-whitespace ()
"Remove whitespace following point, and insert a space. "Remove whitespace following point, and insert a space.
@ -755,12 +749,6 @@ Value must be a keyword corresponding to a key in
(cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist))) (cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist)))
;; active-backend:1 ends here ;; active-backend:1 ends here
;; [[file:chronometrist.org::*get-backend][get-backend:1]]
(defun chronometrist-get-backend (keyword)
"Return the backend object associated with KEYWORD."
(cl-second (alist-get keyword chronometrist-backends-alist)))
;; get-backend:1 ends here
;; [[file:chronometrist.org::*switch-backend][switch-backend:1]] ;; [[file:chronometrist.org::*switch-backend][switch-backend:1]]
(defun chronometrist-switch-backend () (defun chronometrist-switch-backend ()
(interactive) (interactive)
@ -794,7 +782,8 @@ be replaced."
;; register-backend:1 ends here ;; register-backend:1 ends here
;; [[file:chronometrist.org::*read-backend-name][read-backend-name:1]] ;; [[file:chronometrist.org::*read-backend-name][read-backend-name:1]]
(defun chronometrist-read-backend-name (prompt backend-alist &optional predicate return-keyword) (defun chronometrist-read-backend-name (prompt backend-alist
&optional predicate return-keyword)
"Prompt user for a Chronometrist backend name. "Prompt user for a Chronometrist backend name.
BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'. BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'.
@ -881,9 +870,7 @@ Return nil if BACKEND contains no records.")
;; [[file:chronometrist.org::*insert][insert:1]] ;; [[file:chronometrist.org::*insert][insert:1]]
(cl-defgeneric chronometrist-insert (backend plist) (cl-defgeneric chronometrist-insert (backend plist)
"Insert PLIST as new record in BACKEND. "Insert PLIST as new record in BACKEND.
Return non-nil if record is inserted successfully. Return non-nil if record is inserted successfully.")
PLIST may be an interval which crosses days.")
(cl-defmethod chronometrist-insert :before ((_backend t) plist &key &allow-other-keys) (cl-defmethod chronometrist-insert :before ((_backend t) plist &key &allow-other-keys)
(unless (cl-typep plist 'chronometrist-plist) (unless (cl-typep plist 'chronometrist-plist)
@ -1401,7 +1388,7 @@ STREAM (which is the value of `current-buffer')."
(chronometrist-backend-run-assertions backend) (chronometrist-backend-run-assertions backend)
(with-slots (hash-table) backend (with-slots (hash-table) backend
(when-let* (when-let*
((latest-date (chronometrist-ht-last-date hash-table)) ((latest-date (chronometrist-events-last-date hash-table))
(records (gethash latest-date hash-table))) (records (gethash latest-date hash-table)))
(cons latest-date records)))) (cons latest-date records))))
;; latest-date-records:1 ends here ;; latest-date-records:1 ends here
@ -1510,7 +1497,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
`chronometrist-plist-backend' file." `chronometrist-plist-backend' file."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)] (-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)]
(setf hash-table (chronometrist-ht-update new-plist hash-table)) (setf hash-table (chronometrist-events-update new-plist hash-table))
(chronometrist-add-to-task-list new-task backend)))) (chronometrist-add-to-task-list new-task backend))))
;; on-add:1 ends here ;; on-add:1 ends here
@ -1520,8 +1507,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
`chronometrist-plist-backend' file is modified." `chronometrist-plist-backend' file is modified."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend)) (-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend))
((&plist :name old-task) (chronometrist-ht-last backend))) ((&plist :name old-task) (chronometrist-events-last backend)))
(setf hash-table (chronometrist-ht-update new-plist hash-table t)) (setf hash-table (chronometrist-events-update new-plist hash-table t))
(chronometrist-remove-from-task-list old-task backend) (chronometrist-remove-from-task-list old-task backend)
(chronometrist-add-to-task-list new-task backend)))) (chronometrist-add-to-task-list new-task backend))))
;; on-modify:1 ends here ;; on-modify:1 ends here
@ -1531,8 +1518,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
"Function run when the newest plist in a "Function run when the newest plist in a
`chronometrist-plist-backend' file is deleted." `chronometrist-plist-backend' file is deleted."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let (((&plist :name old-task) (chronometrist-ht-last)) (-let (((&plist :name old-task) (chronometrist-events-last))
(date (chronometrist-ht-last-date hash-table))) (date (chronometrist-events-last-date hash-table)))
;; `chronometrist-remove-from-task-list' checks the hash table to determine ;; `chronometrist-remove-from-task-list' checks the hash table to determine
;; if `chronometrist-task-list' is to be updated. Thus, the hash table must ;; if `chronometrist-task-list' is to be updated. Thus, the hash table must
;; not be updated until the task list is. ;; not be updated until the task list is.
@ -1629,7 +1616,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
(backend-latest-date (cl-first latest-plist-group)) (backend-latest-date (cl-first latest-plist-group))
(date-today (chronometrist-date-iso)) (date-today (chronometrist-date-iso))
(insert-new-group (not (equal date-today backend-latest-date))) (insert-new-group (not (equal date-today backend-latest-date)))
(start-date (chronometrist-iso-to-date (plist-get plist :start))) (start-date (cl-first (split-string (plist-get plist :start) "T")))
(new-plist-group-1 (if latest-plist-group (new-plist-group-1 (if latest-plist-group
(append latest-plist-group (append latest-plist-group
(list (or plist-1 plist))) (list (or plist-1 plist)))
@ -1801,7 +1788,7 @@ Return value is either a list in the form
`chronometrist-plist-group-backend' file is modified." `chronometrist-plist-group-backend' file is modified."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let* (((date . plists) (chronometrist-latest-date-records backend)) (-let* (((date . plists) (chronometrist-latest-date-records backend))
(old-date (chronometrist-ht-last-date hash-table)) (old-date (chronometrist-events-last-date hash-table))
(old-plists (gethash old-date hash-table))) (old-plists (gethash old-date hash-table)))
(puthash date plists hash-table) (puthash date plists hash-table)
(cl-loop for plist in old-plists (cl-loop for plist in old-plists
@ -1815,7 +1802,7 @@ Return value is either a list in the form
"Function run when the newest plist-group in a "Function run when the newest plist-group in a
`chronometrist-plist-group-backend' file is deleted." `chronometrist-plist-group-backend' file is deleted."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let* ((old-date (chronometrist-ht-last-date hash-table)) (-let* ((old-date (chronometrist-events-last-date hash-table))
(old-plists (gethash old-date hash-table))) (old-plists (gethash old-date hash-table)))
(cl-loop for plist in old-plists (cl-loop for plist in old-plists
do (chronometrist-remove-from-task-list (plist-get plist :name) backend)) do (chronometrist-remove-from-task-list (plist-get plist :name) backend))
@ -1881,63 +1868,50 @@ Return value is either a list in the form
;; remove-prefix:1 ends here ;; remove-prefix:1 ends here
;; [[file:chronometrist.org::*migrate][migrate:1]] ;; [[file:chronometrist.org::*migrate][migrate:1]]
(defun chronometrist-migrate (&optional input-backend-keyword input-file (defun chronometrist-migrate ()
output-backend-keyword output-file "Convert from one Chronometrist backend to another."
&rest load-files)
"Convert from one Chronometrist backend to another.
INPUT-BACKEND-KEYWORD and OUTPUT-BACKEND-KEYWORD must be keywords
corresponding to entries in `chronometrist-backends-alist'.
INPUT-FILE and OUTPUT-FILE must be file names. INPUT-FILE must
already exist.
LOAD-FILES must be paths to Elisp files. These files are `load'ed
before conversion. They may be used to point to Chronometrist
source files to be used."
(interactive) (interactive)
(let* ((input-backend-keyword (or input-backend-keyword (let* ((input-backend
(chronometrist-read-backend-name "Backend to convert: " chronometrist-backends-alist nil t))) (chronometrist-read-backend-name "Backend to convert: "
(input-backend (chronometrist-get-backend input-backend-keyword)) chronometrist-backends-alist))
(suggested-input-file (chronometrist-backend-file input-backend)) (input-file-suggestion (chronometrist-backend-file input-backend))
(input-file (or input-file (input-file (read-file-name "File to convert: " nil
(read-file-name "File to convert: " input-file-suggestion t
nil suggested-input-file t suggested-input-file))) input-file-suggestion))
(output-backend-keyword (or output-backend-keyword (output-backend (chronometrist-read-backend-name
(chronometrist-read-backend-name "Backend to write: " "Backend to write: "
chronometrist-backends-alist chronometrist-backends-alist
(lambda (keyword) (lambda (keyword)
(not (equal (cl-second (not (equal (cl-second
(alist-get keyword chronometrist-backends-alist)) (alist-get keyword chronometrist-backends-alist))
input-backend))) input-backend)))))
t))) (output-file-suggestion (chronometrist-backend-file output-backend))
(output-backend (chronometrist-get-backend output-backend-keyword)) (output-file (read-file-name "File to write: " nil nil nil
(suggested-output-file (chronometrist-backend-file output-backend)) output-file-suggestion))
(output-file (or output-file (input-backend-name (chronometrist-remove-prefix
(read-file-name "File to write: " nil nil nil suggested-output-file))) (symbol-name
(input-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name input-backend)))) (eieio-object-class-name input-backend))))
(output-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name output-backend)))) (output-backend-name (chronometrist-remove-prefix
(confirm (yes-or-no-p (format "Convert %s (%s) to %s (%s)? " (symbol-name
input-file input-backend-name (eieio-object-class-name output-backend))))
output-file output-backend-name))) (confirm (yes-or-no-p
(confirm-overwrite (if (and confirm (format "Convert %s (%s) to %s (%s)? "
(file-exists-p output-file) input-file
(not (chronometrist-file-empty-p output-file))) input-backend-name
(yes-or-no-p output-file
(format "Overwrite existing non-empty file %s ?" output-file)) output-backend-name)))
t))) (confirm-exists
(if (and confirm confirm-overwrite) (if (and confirm
(start-process (file-exists-p output-file)
"chronometrist-migrate" (generate-new-buffer-name "chronometrist-migrate") (not (chronometrist-file-empty-p output-file)))
"emacs" "--batch" (yes-or-no-p
"--eval=(package-initialize)" (format "Overwrite existing non-empty file %s ?"
"--eval=(cl-loop for (pkg . desc) in package-alist output-file))
when (string-match-p \"^chronometrist\" (symbol-name pkg)) t)))
do (require pkg))" (if (and confirm confirm-exists)
(format "--eval=(cl-loop for file in (quote %S) do (load file))" load-files) (chronometrist-to-file (chronometrist-backend-hash-table input-backend)
;; the hash table slot may be empty, so we generate the hash table from scratch output-backend
(format "--eval=(chronometrist-to-file (chronometrist-to-hash-table (chronometrist-get-backend %s)) (chronometrist-get-backend %s) %S)" output-file)
input-backend-keyword output-backend-keyword output-file))
(message "Conversion aborted.")))) (message "Conversion aborted."))))
;; migrate:1 ends here ;; migrate:1 ends here
@ -2972,7 +2946,7 @@ displayed. They must be ts structs (see `ts.el').")
It simply operates on the entire hash table TABLE (see It simply operates on the entire hash table TABLE (see
`chronometrist-to-hash-table' for table format), so ensure that TABLE is `chronometrist-to-hash-table' for table format), so ensure that TABLE is
reduced to the desired range using reduced to the desired range using
`chronometrist-ht-subset'." `chronometrist-events-subset'."
(cl-loop for task in (chronometrist-task-list) collect (cl-loop for task in (chronometrist-task-list) collect
(let* ((active-days (chronometrist-statistics-count-active-days task table)) (let* ((active-days (chronometrist-statistics-count-active-days task table))
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode) (active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
@ -3001,12 +2975,12 @@ reduced to the desired range using
('week ('week
(let* ((start (plist-get chronometrist-statistics--ui-state :start)) (let* ((start (plist-get chronometrist-statistics--ui-state :start))
(end (plist-get chronometrist-statistics--ui-state :end)) (end (plist-get chronometrist-statistics--ui-state :end))
(ht (chronometrist-ht-subset start end hash-table))) (ht (chronometrist-events-subset start end hash-table)))
(chronometrist-statistics-rows-internal ht))) (chronometrist-statistics-rows-internal ht)))
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data (t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
(let* ((start (chronometrist-previous-week-start (chronometrist-date-ts))) (let* ((start (chronometrist-previous-week-start (chronometrist-date-ts)))
(end (ts-adjust 'day 7 start)) (end (ts-adjust 'day 7 start))
(ht (chronometrist-ht-subset start end hash-table))) (ht (chronometrist-events-subset start end hash-table)))
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end)) (setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
(chronometrist-statistics-rows-internal ht)))))) (chronometrist-statistics-rows-internal ht))))))
;; rows:1 ends here ;; rows:1 ends here
@ -3545,6 +3519,39 @@ Return value is a list as specified by `tabulated-list-entries'."
do (cl-incf index))) do (cl-incf index)))
;; rows:1 ends here ;; rows:1 ends here
;; [[file:chronometrist.org::*define-alert-system][define-alert-system:1]]
(defmacro chronometrist-define-alert-system (extension initvalue &rest custom-args)
"Define an alerts system for EXTENSION.
Defines a custom variable `chronometrist-EXTENSION-alerts-alist'
with INITVALUE and CUSTOM-ARGS, a variable
`chronometrist-EXTENSION--timer-list', and a procedure
`chronometrist-EXTENSION-run-at-time'."
(let* ((prefix (format "chronometrist-%s" extension))
(custom-var (intern (format "%s-alerts-alist" prefix)))
(list-var (intern (format "%s--timer-alist" prefix)))
(function (intern (format "%s-run-at-time" prefix))))
`(progn
(defcustom ,custom-var ,initvalue
,(let ((docstring (format "Alist of alerts run by `%s'.
Each element must be in the form (SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS),
where SYMBOL uniquely identifies the alert,
FUNCTION is a function calling `%s' and ALERT-FN,
and ALERT-ARGS are passed to ALERT-FN."
prefix function)))
(with-temp-buffer
(insert docstring)
(fill-region (point-min) (point-max))
(buffer-substring-no-properties (point-min) (point-max))))
,@custom-args)
(defvar ,list-var nil ,(format "Alist of timers created for `%s'." prefix))
(defun ,function (time repeat function &rest args)
,(format "Like `run-at-time', but store timer objects in `%s'." ,list-var)
(cl-pushnew (apply #'run-at-time time repeat function args) ,list-var)))))
;; (chronometrist-define-alert-system foo nil)
;; define-alert-system:1 ends here
(provide 'chronometrist) (provide 'chronometrist)
;;; chronometrist.el ends here ;;; chronometrist.el ends here

View File

@ -55,6 +55,16 @@ Chronometrist records /time intervals/ (earlier called "events") as plists. Each
See also [[#explanation-time-formats][Currently-Used Time Formats]] See also [[#explanation-time-formats][Currently-Used Time Formats]]
** Overview
At its most basic, we read data from a [[#program-backend][backend]], store it in a [[#program-data-structures][hash table]], and [[#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list-mode")][=tabulated-list-mode=]] buffer. When the file is changed—whether by the program or the user—we [[refresh-file][update the hash table]] and the [[#program-frontend-chronometrist-refresh][buffer]].
In addition, we implement a [[#program-pretty-printer][plist pretty-printer]] and some [[#program-migration][migration commands]].
Extensions exist for -
1. [[file:chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals, and
2. support for the [[file:chronometrist-third.org][Third Time system]]
3. [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]]
** Optimization ** Optimization
It is of great importance that Chronometrist be responsive - It is of great importance that Chronometrist be responsive -
+ A responsive program is more likely to be used; recall our design goal of 'incentivizing use'. + A responsive program is more likely to be used; recall our design goal of 'incentivizing use'.
@ -402,7 +412,7 @@ But I discovered that if I do that, =package-lint= says - =error: Couldn't parse
;; Keywords: calendar ;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist ;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "27.1") (dash "2.16.0") (seq "2.20") (ts "0.2")) ;; Package-Requires: ((emacs "27.1") (dash "2.16.0") (seq "2.20") (ts "0.2"))
;; Version: 0.10.0 ;; Version: 0.9.0
;; This is free and unencumbered software released into the public domain. ;; This is free and unencumbered software released into the public domain.
;; ;;
@ -823,9 +833,9 @@ Return value is a ts struct (see `ts.el')."
:stop "2021-02-13T00:03:46+0530"))))) :stop "2021-02-13T00:03:46+0530")))))
#+END_SRC #+END_SRC
*** ht-update :writer: *** events-update :writer:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-ht-update (plist hash-table &optional replace) (defun chronometrist-events-update (plist hash-table &optional replace)
"Return HASH-TABLE with PLIST added as the latest interval. "Return HASH-TABLE with PLIST added as the latest interval.
If REPLACE is non-nil, replace the last interval with PLIST." If REPLACE is non-nil, replace the last interval with PLIST."
(let* ((date (->> (plist-get plist :start) (let* ((date (->> (plist-get plist :start)
@ -838,34 +848,33 @@ If REPLACE is non-nil, replace the last interval with PLIST."
hash-table)) hash-table))
#+END_SRC #+END_SRC
*** ht-last-date :reader: *** last-date :reader:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-ht-last-date (hash-table) (defun chronometrist-events-last-date (hash-table)
"Return an ISO-8601 date string for the latest date present in `chronometrist-events'." "Return an ISO-8601 date string for the latest date present in `chronometrist-events'."
(--> (hash-table-keys hash-table) (--> (hash-table-keys hash-table)
(sort it #'string-lessp)
(last it) (last it)
(cl-first it))) (car it)))
#+END_SRC #+END_SRC
*** ht-last :reader: *** events-last :reader:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-ht-last (&optional (backend (chronometrist-active-backend))) (cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend)))
"Return the last plist from `chronometrist-events'." "Return the last plist from `chronometrist-events'."
(let* ((hash-table (chronometrist-backend-hash-table backend)) (let* ((hash-table (chronometrist-backend-hash-table backend))
(last-date (chronometrist-ht-last-date hash-table))) (last-date (chronometrist-events-last-date hash-table)))
(--> (gethash last-date hash-table) (--> (gethash last-date hash-table)
(last it) (last it)
(car it)))) (car it))))
#+END_SRC #+END_SRC
*** ht-subset :reader: *** events-subset :reader:
:PROPERTIES: :PROPERTIES:
:VALUE: hash table :VALUE: hash table
:CUSTOM_ID: program-data-structures-ht-subset :CUSTOM_ID: program-data-structures-events-subset
:END: :END:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-ht-subset (start end hash-table) (defun chronometrist-events-subset (start end hash-table)
"Return a subset of HASH-TABLE. "Return a subset of HASH-TABLE.
The subset will contain values between dates START and END (both The subset will contain values between dates START and END (both
inclusive). inclusive).
@ -1120,12 +1129,6 @@ SECONDS is less than 60, return a blank string."
(should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes"))) (should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes")))
#+END_SRC #+END_SRC
*** iso-to-date :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-iso-to-date (timestamp)
(cl-first (split-string timestamp "T")))
#+END_SRC
** Plist pretty-printing ** Plist pretty-printing
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: program-pretty-printer :CUSTOM_ID: program-pretty-printer
@ -1400,13 +1403,6 @@ Value must be a keyword corresponding to a key in
(cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist))) (cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist)))
#+END_SRC #+END_SRC
**** get-backend :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-get-backend (keyword)
"Return the backend object associated with KEYWORD."
(cl-second (alist-get keyword chronometrist-backends-alist)))
#+END_SRC
**** switch-backend :command: **** switch-backend :command:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-switch-backend () (defun chronometrist-switch-backend ()
@ -1443,7 +1439,8 @@ be replaced."
**** read-backend-name :procedure: **** read-backend-name :procedure:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-read-backend-name (prompt backend-alist &optional predicate return-keyword) (defun chronometrist-read-backend-name (prompt backend-alist
&optional predicate return-keyword)
"Prompt user for a Chronometrist backend name. "Prompt user for a Chronometrist backend name.
BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'. BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'.
@ -1543,9 +1540,7 @@ Return nil if BACKEND contains no records.")
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defgeneric chronometrist-insert (backend plist) (cl-defgeneric chronometrist-insert (backend plist)
"Insert PLIST as new record in BACKEND. "Insert PLIST as new record in BACKEND.
Return non-nil if record is inserted successfully. Return non-nil if record is inserted successfully.")
PLIST may be an interval which crosses days.")
(cl-defmethod chronometrist-insert :before ((_backend t) plist &key &allow-other-keys) (cl-defmethod chronometrist-insert :before ((_backend t) plist &key &allow-other-keys)
(unless (cl-typep plist 'chronometrist-plist) (unless (cl-typep plist 'chronometrist-plist)
@ -2316,7 +2311,7 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl
(chronometrist-backend-run-assertions backend) (chronometrist-backend-run-assertions backend)
(with-slots (hash-table) backend (with-slots (hash-table) backend
(when-let* (when-let*
((latest-date (chronometrist-ht-last-date hash-table)) ((latest-date (chronometrist-events-last-date hash-table))
(records (gethash latest-date hash-table))) (records (gethash latest-date hash-table)))
(cons latest-date records)))) (cons latest-date records))))
#+END_SRC #+END_SRC
@ -2432,7 +2427,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
`chronometrist-plist-backend' file." `chronometrist-plist-backend' file."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)] (-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)]
(setf hash-table (chronometrist-ht-update new-plist hash-table)) (setf hash-table (chronometrist-events-update new-plist hash-table))
(chronometrist-add-to-task-list new-task backend)))) (chronometrist-add-to-task-list new-task backend))))
#+END_SRC #+END_SRC
@ -2443,8 +2438,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
`chronometrist-plist-backend' file is modified." `chronometrist-plist-backend' file is modified."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend)) (-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend))
((&plist :name old-task) (chronometrist-ht-last backend))) ((&plist :name old-task) (chronometrist-events-last backend)))
(setf hash-table (chronometrist-ht-update new-plist hash-table t)) (setf hash-table (chronometrist-events-update new-plist hash-table t))
(chronometrist-remove-from-task-list old-task backend) (chronometrist-remove-from-task-list old-task backend)
(chronometrist-add-to-task-list new-task backend)))) (chronometrist-add-to-task-list new-task backend))))
#+END_SRC #+END_SRC
@ -2455,8 +2450,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
"Function run when the newest plist in a "Function run when the newest plist in a
`chronometrist-plist-backend' file is deleted." `chronometrist-plist-backend' file is deleted."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let (((&plist :name old-task) (chronometrist-ht-last)) (-let (((&plist :name old-task) (chronometrist-events-last))
(date (chronometrist-ht-last-date hash-table))) (date (chronometrist-events-last-date hash-table)))
;; `chronometrist-remove-from-task-list' checks the hash table to determine ;; `chronometrist-remove-from-task-list' checks the hash table to determine
;; if `chronometrist-task-list' is to be updated. Thus, the hash table must ;; if `chronometrist-task-list' is to be updated. Thus, the hash table must
;; not be updated until the task list is. ;; not be updated until the task list is.
@ -2605,7 +2600,7 @@ Situations -
(backend-latest-date (cl-first latest-plist-group)) (backend-latest-date (cl-first latest-plist-group))
(date-today (chronometrist-date-iso)) (date-today (chronometrist-date-iso))
(insert-new-group (not (equal date-today backend-latest-date))) (insert-new-group (not (equal date-today backend-latest-date)))
(start-date (chronometrist-iso-to-date (plist-get plist :start))) (start-date (cl-first (split-string (plist-get plist :start) "T")))
(new-plist-group-1 (if latest-plist-group (new-plist-group-1 (if latest-plist-group
(append latest-plist-group (append latest-plist-group
(list (or plist-1 plist))) (list (or plist-1 plist)))
@ -2791,7 +2786,7 @@ Return value is either a list in the form
`chronometrist-plist-group-backend' file is modified." `chronometrist-plist-group-backend' file is modified."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let* (((date . plists) (chronometrist-latest-date-records backend)) (-let* (((date . plists) (chronometrist-latest-date-records backend))
(old-date (chronometrist-ht-last-date hash-table)) (old-date (chronometrist-events-last-date hash-table))
(old-plists (gethash old-date hash-table))) (old-plists (gethash old-date hash-table)))
(puthash date plists hash-table) (puthash date plists hash-table)
(cl-loop for plist in old-plists (cl-loop for plist in old-plists
@ -2806,7 +2801,7 @@ Return value is either a list in the form
"Function run when the newest plist-group in a "Function run when the newest plist-group in a
`chronometrist-plist-group-backend' file is deleted." `chronometrist-plist-group-backend' file is deleted."
(with-slots (hash-table) backend (with-slots (hash-table) backend
(-let* ((old-date (chronometrist-ht-last-date hash-table)) (-let* ((old-date (chronometrist-events-last-date hash-table))
(old-plists (gethash old-date hash-table))) (old-plists (gethash old-date hash-table)))
(cl-loop for plist in old-plists (cl-loop for plist in old-plists
do (chronometrist-remove-from-task-list (plist-get plist :name) backend)) do (chronometrist-remove-from-task-list (plist-get plist :name) backend))
@ -2857,7 +2852,7 @@ Return value is either a list in the form
collect plist)) collect plist))
#+END_SRC #+END_SRC
***** TODO active-days :reader:method:noexport: ***** TODO active-days :reader:method:NOEXPORT:
#+BEGIN_SRC emacs-lisp :tangle no #+BEGIN_SRC emacs-lisp :tangle no
(cl-defmethod chronometrist-active-days ((backend chronometrist-plist-group-backend) task &key start end) (cl-defmethod chronometrist-active-days ((backend chronometrist-plist-group-backend) task &key start end)
(cl-check-type task string) (cl-check-type task string)
@ -2882,7 +2877,7 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method
t)) t))
#+END_SRC #+END_SRC
***** count-records :reader:method:noexport: ***** count-records :reader:method:NOEXPORT:
#+BEGIN_SRC emacs-lisp :tangle no #+BEGIN_SRC emacs-lisp :tangle no
(cl-defmethod chronometrist-count-records ((backend chronometrist-plist-group-backend))) (cl-defmethod chronometrist-count-records ((backend chronometrist-plist-group-backend)))
#+END_SRC #+END_SRC
@ -2897,65 +2892,52 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method
(replace-regexp-in-string "^chronometrist-" "" string)) (replace-regexp-in-string "^chronometrist-" "" string))
#+END_SRC #+END_SRC
*** migrate :command: *** migrate
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-migrate (&optional input-backend-keyword input-file (defun chronometrist-migrate ()
output-backend-keyword output-file "Convert from one Chronometrist backend to another."
&rest load-files)
"Convert from one Chronometrist backend to another.
INPUT-BACKEND-KEYWORD and OUTPUT-BACKEND-KEYWORD must be keywords
corresponding to entries in `chronometrist-backends-alist'.
INPUT-FILE and OUTPUT-FILE must be file names. INPUT-FILE must
already exist.
LOAD-FILES must be paths to Elisp files. These files are `load'ed
before conversion. They may be used to point to Chronometrist
source files to be used."
(interactive) (interactive)
(let* ((input-backend-keyword (or input-backend-keyword (let* ((input-backend
(chronometrist-read-backend-name "Backend to convert: " chronometrist-backends-alist nil t))) (chronometrist-read-backend-name "Backend to convert: "
(input-backend (chronometrist-get-backend input-backend-keyword)) chronometrist-backends-alist))
(suggested-input-file (chronometrist-backend-file input-backend)) (input-file-suggestion (chronometrist-backend-file input-backend))
(input-file (or input-file (input-file (read-file-name "File to convert: " nil
(read-file-name "File to convert: " input-file-suggestion t
nil suggested-input-file t suggested-input-file))) input-file-suggestion))
(output-backend-keyword (or output-backend-keyword (output-backend (chronometrist-read-backend-name
(chronometrist-read-backend-name "Backend to write: " "Backend to write: "
chronometrist-backends-alist chronometrist-backends-alist
(lambda (keyword) (lambda (keyword)
(not (equal (cl-second (not (equal (cl-second
(alist-get keyword chronometrist-backends-alist)) (alist-get keyword chronometrist-backends-alist))
input-backend))) input-backend)))))
t))) (output-file-suggestion (chronometrist-backend-file output-backend))
(output-backend (chronometrist-get-backend output-backend-keyword)) (output-file (read-file-name "File to write: " nil nil nil
(suggested-output-file (chronometrist-backend-file output-backend)) output-file-suggestion))
(output-file (or output-file (input-backend-name (chronometrist-remove-prefix
(read-file-name "File to write: " nil nil nil suggested-output-file))) (symbol-name
(input-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name input-backend)))) (eieio-object-class-name input-backend))))
(output-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name output-backend)))) (output-backend-name (chronometrist-remove-prefix
(confirm (yes-or-no-p (format "Convert %s (%s) to %s (%s)? " (symbol-name
input-file input-backend-name (eieio-object-class-name output-backend))))
output-file output-backend-name))) (confirm (yes-or-no-p
(confirm-overwrite (if (and confirm (format "Convert %s (%s) to %s (%s)? "
(file-exists-p output-file) input-file
(not (chronometrist-file-empty-p output-file))) input-backend-name
(yes-or-no-p output-file
(format "Overwrite existing non-empty file %s ?" output-file)) output-backend-name)))
t))) (confirm-exists
(if (and confirm confirm-overwrite) (if (and confirm
(start-process (file-exists-p output-file)
"chronometrist-migrate" (generate-new-buffer-name "chronometrist-migrate") (not (chronometrist-file-empty-p output-file)))
"emacs" "--batch" (yes-or-no-p
"--eval=(package-initialize)" (format "Overwrite existing non-empty file %s ?"
"--eval=(cl-loop for (pkg . desc) in package-alist output-file))
when (string-match-p \"^chronometrist\" (symbol-name pkg)) t)))
do (require pkg))" (if (and confirm confirm-exists)
(format "--eval=(cl-loop for file in (quote %S) do (load file))" load-files) (chronometrist-to-file (chronometrist-backend-hash-table input-backend)
;; the hash table slot may be empty, so we generate the hash table from scratch output-backend
(format "--eval=(chronometrist-to-file (chronometrist-to-hash-table (chronometrist-get-backend %s)) (chronometrist-get-backend %s) %S)" output-file)
input-backend-keyword output-backend-keyword output-file))
(message "Conversion aborted.")))) (message "Conversion aborted."))))
#+END_SRC #+END_SRC
@ -4120,7 +4102,7 @@ displayed. They must be ts structs (see `ts.el').")
It simply operates on the entire hash table TABLE (see It simply operates on the entire hash table TABLE (see
`chronometrist-to-hash-table' for table format), so ensure that TABLE is `chronometrist-to-hash-table' for table format), so ensure that TABLE is
reduced to the desired range using reduced to the desired range using
`chronometrist-ht-subset'." `chronometrist-events-subset'."
(cl-loop for task in (chronometrist-task-list) collect (cl-loop for task in (chronometrist-task-list) collect
(let* ((active-days (chronometrist-statistics-count-active-days task table)) (let* ((active-days (chronometrist-statistics-count-active-days task table))
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode) (active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
@ -4150,12 +4132,12 @@ reduced to the desired range using
('week ('week
(let* ((start (plist-get chronometrist-statistics--ui-state :start)) (let* ((start (plist-get chronometrist-statistics--ui-state :start))
(end (plist-get chronometrist-statistics--ui-state :end)) (end (plist-get chronometrist-statistics--ui-state :end))
(ht (chronometrist-ht-subset start end hash-table))) (ht (chronometrist-events-subset start end hash-table)))
(chronometrist-statistics-rows-internal ht))) (chronometrist-statistics-rows-internal ht)))
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data (t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
(let* ((start (chronometrist-previous-week-start (chronometrist-date-ts))) (let* ((start (chronometrist-previous-week-start (chronometrist-date-ts)))
(end (ts-adjust 'day 7 start)) (end (ts-adjust 'day 7 start))
(ht (chronometrist-ht-subset start end hash-table))) (ht (chronometrist-events-subset start end hash-table)))
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end)) (setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
(chronometrist-statistics-rows-internal ht)))))) (chronometrist-statistics-rows-internal ht))))))
#+END_SRC #+END_SRC
@ -4579,7 +4561,7 @@ range.")
#+END_SRC #+END_SRC
**** intervals-for-range :reader: **** intervals-for-range :reader:
This is basically like [[#program-data-structures-ht-subset][chronometrist-ht-subset]], but returns a list instead of a hash table. Might replace one with the other in the future. This is basically like [[#program-data-structures-events-subset][chronometrist-events-subset]], but returns a list instead of a hash table. Might replace one with the other in the future.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-details-intervals-for-range (range table) (defun chronometrist-details-intervals-for-range (range table)
@ -4775,6 +4757,41 @@ Return value is a list as specified by `tabulated-list-entries'."
do (cl-incf index))) do (cl-incf index)))
#+END_SRC #+END_SRC
** Extension utilities
*** define-alert-system
#+BEGIN_SRC emacs-lisp
(defmacro chronometrist-define-alert-system (extension initvalue &rest custom-args)
"Define an alerts system for EXTENSION.
Defines a custom variable `chronometrist-EXTENSION-alerts-alist'
with INITVALUE and CUSTOM-ARGS, a variable
`chronometrist-EXTENSION--timer-list', and a procedure
`chronometrist-EXTENSION-run-at-time'."
(let* ((prefix (format "chronometrist-%s" extension))
(custom-var (intern (format "%s-alerts-alist" prefix)))
(list-var (intern (format "%s--timer-alist" prefix)))
(function (intern (format "%s-run-at-time" prefix))))
`(progn
(defcustom ,custom-var ,initvalue
,(let ((docstring (format "Alist of alerts run by `%s'.
Each element must be in the form (SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS),
where SYMBOL uniquely identifies the alert,
FUNCTION is a function calling `%s' and ALERT-FN,
and ALERT-ARGS are passed to ALERT-FN."
prefix function)))
(with-temp-buffer
(insert docstring)
(fill-region (point-min) (point-max))
(buffer-substring-no-properties (point-min) (point-max))))
,@custom-args)
(defvar ,list-var nil ,(format "Alist of timers created for `%s'." prefix))
(defun ,function (time repeat function &rest args)
,(format "Like `run-at-time', but store timer objects in `%s'." ,list-var)
(cl-pushnew (apply #'run-at-time time repeat function args) ,list-var)))))
;; (chronometrist-define-alert-system foo nil)
#+END_SRC
** Provide ** Provide
#+BEGIN_SRC emacs-lisp :comments no #+BEGIN_SRC emacs-lisp :comments no
(provide 'chronometrist) (provide 'chronometrist)
@ -4782,7 +4799,7 @@ Return value is a list as specified by `tabulated-list-entries'."
;;; chronometrist.el ends here ;;; chronometrist.el ends here
#+END_SRC #+END_SRC
* Local variables :noexport: * Local variables :NOEXPORT:
Evaluate this to be able to insert the package prefix via =nameless-insert-name=, at the cost of having all =nameless-aliases= break (= less readable code). Evaluate this to be able to insert the package prefix via =nameless-insert-name=, at the cost of having all =nameless-aliases= break (= less readable code).
#+BEGIN_SRC emacs-lisp :load no :tangle no #+BEGIN_SRC emacs-lisp :load no :tangle no

428
manual.md Normal file
View File

@ -0,0 +1,428 @@
# Table of Contents
1. [Benefits](#benefits)
2. [Limitations](#limitations)
3. [Comparisons](#comparisons)
1. [timeclock.el](#timeclock.el)
2. [Org time tracking](#org-time-tracking)
4. [Installation](#installation)
1. [from MELPA](#install-from-melpa)
2. [from Git](#install-from-git)
5. [Usage](#usage)
1. [chronometrist](#usage-chronometrist)
2. [chronometrist-report](#usage-chronometrist-report)
3. [chronometrist-statistics](#usage-chronometrist-statistics)
4. [chronometrist-details](#org09eaa12)
5. [common commands](#usage-common-commands)
6. [Time goals/targets](#time-goals)
6. [How-to](#how-to)
1. [How to display a prompt when exiting with an active task](#how-to-prompt-when-exiting-emacs)
2. [How to load the program using literate-elisp](#how-to-literate-elisp)
3. [How to attach tags to time intervals](#how-to-tags)
4. [How to attach key-values to time intervals](#how-to-key-value-pairs)
5. [How to skip running hooks/attaching tags and key values](#orga569ce7)
6. [How to open certain files when you start a task](#how-to-open-files-on-task-start)
7. [How to warn yourself about uncommitted changes](#how-to-warn-uncommitted-changes)
8. [How to display the current time interval in the activity indicator](#how-to-activity-indicator)
9. [How to back up your Chronometrist data](#how-to-backup)
10. [How to configure Vertico for use with Chronometrist](#howto-vertico)
7. [Explanation](#org2a03f3a)
1. [Literate Program](#explanation-literate-program)
8. [User's reference](#org2585c1f)
9. [Contributions and contact](#contributions-contact)
10. [License](#license)
11. [Thanks](#thanks)
<a href="https://liberapay.com/contrapunctus/donate"><img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay"></a>
<a href="https://melpa.org/#/chronometrist"><img src="https://melpa.org/packages/chronometrist-badge.svg"></a>
A time tracker in Emacs with a nice interface
Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker)
<a id="benefits"></a>
# Benefits
1. Extremely simple and efficient to use
2. Displays useful information about your time usage
3. Support for both mouse and keyboard
4. Human errors in tracking are easily fixed by editing a plain text file
5. Hooks to let you perform arbitrary actions when starting/stopping tasks
6. Fancy graphs with the `chronometrist-spark` extension
<a id="limitations"></a>
# Limitations
1. No support for concurrent tasks.
<a id="comparisons"></a>
# Comparisons
<a id="timeclock.el"></a>
## timeclock.el
Compared to timeclock.el, Chronometrist
- stores data in an s-expression format rather than a line-based one
- supports attaching tags and arbitrary key-values to time intervals
- has commands to shows useful summaries
- has more hooks
<a id="org-time-tracking"></a>
## Org time tracking
Chronometrist and Org time tracking seem to be equivalent in terms of capabilities, approaching the same ends through different means.
- Chronometrist doesn't have a mode line indicator at the moment. (planned)
- Chronometrist doesn't have Org's sophisticated querying facilities. (an SQLite backend is planned)
- Org does so many things that keybindings seem to necessarily get longer. Chronometrist has far fewer commands than Org, so most of the keybindings are single keys, without modifiers.
- Chronometrist's UI is cleaner, since the storage is separate from the display. It doesn't show tasks as trees like Org, but it uses tags and key-values to achieve that. Additionally, navigating a flat list takes fewer user operations than navigating a tree.
- Chronometrist data is just s-expressions (plists), and may be easier to parse than a complex text format with numerous use-cases.
<a id="installation"></a>
# Installation
<a id="install-from-melpa"></a>
## from MELPA
1. Set up MELPA - <https://melpa.org/#/getting-started>
(Chronometrist uses Semantic Versioning and the developer is accident-prone, so using MELPA Stable is suggested 😏)
2. `M-x package-install RET chronometrist RET`
<a id="install-from-git"></a>
## from Git
You can get `chronometrist` from <https://tildegit.org/contrapunctus/chronometrist> or <https://codeberg.org/contrapunctus/chronometrist>
`chronometrist` requires
- Emacs v25 or higher
- [dash.el](https://github.com/magnars/dash.el)
- [ts.el](https://github.com/alphapapa/ts.el)
Add the "elisp/" subdirectory to your load-path, and `(require 'chronometrist)`.
<a id="usage"></a>
# Usage
<a id="usage-chronometrist"></a>
## chronometrist
Run `M-x chronometrist` to see your projects, the time you spent on them today, which one is active, and the total time clocked today.
Click or hit `RET` (`chronometrist-toggle-task`) on a project to start tracking time for it. If it's already clocked in, it will be clocked out.
You can also hit `<numeric prefix> RET` anywhere in the buffer to toggle the corresponding project, e.g. =C-1 RET= will toggle the project with index 1.
Press `r` to see a weekly report (see `chronometrist-report`)
<a id="usage-chronometrist-report"></a>
## chronometrist-report
Run `M-x chronometrist-report` (or `chronometrist` with a prefix argument of 1, or press `r` in the `chronometrist` buffer) to see a weekly report.
Press `b` to look at past weeks, and `f` for future weeks.
<a id="usage-chronometrist-statistics"></a>
## chronometrist-statistics
Run `M-x chronometrist-statistics` (or `chronometrist` with a prefix argument of 2) to view statistics.
Press `b` to look at past time ranges, and `f` for future ones.
<a id="org09eaa12"></a>
## chronometrist-details
<a id="usage-common-commands"></a>
## common commands
In the buffers created by the previous three commands, you can press `l` (`chronometrist-open-log`) to view/edit your `chronometrist-file`, which by default is `~/.emacs.d/chronometrist.sexp`.
All of these commands will kill their buffer when run again with the buffer visible, so the keys you bind them to behave as a toggle.
All buffers keep themselves updated via an idle timer - no need to frequently press `g` to update.
<a id="time-goals"></a>
## Time goals/targets
If you wish you could define time goals for some tasks, and have Chronometrist notify you when you're approaching the goal, completing it, or exceeding it, check out the extension [chronometrist-goal.el](https://github.com/contrapunctus-1/chronometrist-goal/).
<a id="how-to"></a>
# How-to
See the Customize groups `chronometrist` and `chronometrist-report` for variables intended to be user-customizable.
<a id="how-to-prompt-when-exiting-emacs"></a>
## How to display a prompt when exiting with an active task
Evaluate or add to your init.el the following -
`(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)`
<a id="how-to-literate-elisp"></a>
## How to load the program using literate-elisp
(add-to-list 'load-path "<directory containing chronometrist.org>")
(require 'literate-elisp) ;; or autoload, use-package, ...
(literate-elisp-load "chronometrist.org")
<a id="how-to-tags"></a>
## How to attach tags to time intervals
1. Add `chronometrist-tags-add` to one or more of these hooks <sup><a id="fnr.1" class="footref" href="#fn.1" role="doc-backlink">1</a></sup> -
(add-to-list 'chronometrist-after-in-functions 'chronometrist-tags-add)
(add-to-list 'chronometrist-before-out-functions 'chronometrist-tags-add)
(add-to-list 'chronometrist-after-out-functions 'chronometrist-tags-add)
2. clock in/clock out to trigger the hook.
The prompt suggests past combinations you used for the current task, which you can browse with `M-p=/=M-n`. You can leave it blank by pressing `RET`.
<a id="how-to-key-value-pairs"></a>
## How to attach key-values to time intervals
1. Add `chronometrist-kv-add` to one or more of these hooks <sup><a id="fnr.1.100" class="footref" href="#fn.1" role="doc-backlink">1</a></sup> -
(add-to-list 'chronometrist-after-in-functions 'chronometrist-kv-add)
(add-to-list 'chronometrist-before-out-functions 'chronometrist-kv-add)
(add-to-list 'chronometrist-after-out-functions 'chronometrist-kv-add)
To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press `C-c C-c` to accept the key-values, or `C-c C-k` to cancel.
<a id="orga569ce7"></a>
## How to skip running hooks/attaching tags and key values
Use `M-RET` (`chronometrist-toggle-task-no-hooks`) to clock in/out.
<a id="how-to-open-files-on-task-start"></a>
## How to open certain files when you start a task
An idea from the author's own init -
(defun my-start-project (project)
(pcase project
("Guitar"
(find-file-other-window "~/repertoire.org"))
;; ...
))
(add-hook 'chronometrist-before-in-functions 'my-start-project)
<a id="how-to-warn-uncommitted-changes"></a>
## How to warn yourself about uncommitted changes
Another one, prompting the user if they have uncommitted changes in a git repository (assuming they use [Magit](https://magit.vc/)) -
(autoload 'magit-anything-modified-p "magit")
(defun my-commit-prompt ()
"Prompt user if `default-directory' is a dirty Git repository.
Return t if the user answers yes, if the repository is clean, or
if there is no Git repository.
Return nil (and run `magit-status') if the user answers no."
(cond ((not (magit-anything-modified-p)) t)
((yes-or-no-p
(format "You have uncommitted changes in %S. Really clock out? "
default-directory)) t)
(t (magit-status) nil)))
(add-hook 'chronometrist-before-out-functions 'my-commit-prompt)
<a id="how-to-activity-indicator"></a>
## How to display the current time interval in the activity indicator
(defun my-activity-indicator ()
(--> (chronometrist-latest-record (chronometrist-active-backend))
(plist-put it :stop (chronometrist-format-time-iso8601))
(list it)
(chronometrist-events-to-durations it)
(-reduce #'+ it)
(truncate it)
(chronometrist-format-duration it)))
(setq chronometrist-activity-indicator #'my-activity-indicator)
<a id="how-to-backup"></a>
## How to back up your Chronometrist data
I suggest backing up Chronometrist data on each save using the [async-backup](https://tildegit.org/contrapunctus/async-backup) package.<sup><a id="fnr.2" class="footref" href="#fn.2" role="doc-backlink">2</a></sup> Here's how you can do that.
1. Add the following to your init.
(use-package async-backup)
2. Open your Chronometrist file and add `async-backup` to a buffer-local `after-save-hook`.
M-x chronometrist-open-log
M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET
3. Optionally, configure `backup-directory-alist` to set a specific directory for the backups.
<a id="howto-vertico"></a>
## How to configure Vertico for use with Chronometrist
By default, [Vertico](https://github.com/minad/vertico) uses its own sorting function - for some commands (such as `chronometrist-key-values-unified-prompt`) this results in *worse* suggestions, since Chronometrist sorts suggestions in most-recent-first order.
You can either disable Vertico's sorting entirely -
(setq vertico-sort-function nil)
Or use `vertico-multiform` to disable sorting for only specific commands -
(use-package vertico-multiform
:init (vertico-multiform-mode)
:config
(setq vertico-multiform-commands
'((chronometrist-toggle-task (vertico-sort-function . nil))
(chronometrist-toggle-task-no-hooks (vertico-sort-function . nil))
(chronometrist-key-values-unified-prompt (vertico-sort-function . nil)))))
<a id="org2a03f3a"></a>
# Explanation
<a id="explanation-literate-program"></a>
## Literate Program
Chronometrist is a literate program, made using Org - the canonical source is the `chronometrist.org` file, which contains source blocks. These are provided to users after *tangling* (extracting the source into an Emacs Lisp file).
The Org file can also be loaded directly using the [literate-elisp](https://github.com/jingtaozf/literate-elisp) package, so that all source links (e.g. `xref`, `describe-function`) lead to the Org file, within the context of the concerned documentation. See [How to load the program using literate-elisp](#how-to-literate-elisp).
`chronometrist.org` is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads.
<a id="org2585c1f"></a>
# User's reference
All variables intended for user customization are listed here. They serve as the public API for this project for the purpose of semantic versioning. Any changes to these which require a user to modify their configuration are considered breaking changes.
1. `chronometrist-file`
2. `chronometrist-buffer-name`
3. `chronometrist-report-buffer-name`
4. `chronometrist-details-buffer-name`
5. `chronometrist-sexp-pretty-print-function`
6. `chronometrist-hide-cursor`
7. `chronometrist-update-interval`
8. `chronometrist-activity-indicator`
Buffer schemas
1. `chronometrist-schema`
2. `chronometrist-details-schema`
Hooks
1. `chronometrist-mode-hook`
2. `chronometrist-schema-transformers`
3. `chronometrist-row-transformers`
4. `chronometrist-before-in-functions`
5. `chronometrist-after-in-functions`
6. `chronometrist-before-out-functions`
7. `chronometrist-after-out-functions`
8. `chronometrist-file-change-hook`
9. `chronometrist-timer-hook`
<a id="contributions-contact"></a>
# Contributions and contact
Feedback and MRs are very welcome. 🙂
- <TODO.md> has a long list of tasks
- <elisp/chronometrist.md> contains all developer-oriented documentation
If you have tried using Chronometrist, I'd love to hear your experiences! Get in touch with the author and other Emacs users in the Emacs channel on the Jabber network - [xmpp:emacs@salas.suchat.org?join](https://conversations.im/j/emacs@salas.suchat.org) ([web chat](https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org))
(For help in getting started with Jabber, [click here](https://xmpp.org/getting-started/))
<a id="license"></a>
# License
I dream of a world where all software is liberated - transparent, trustable, and accessible for anyone to use or improve. But I don't want to make demands or threats (e.g. via legal conditions) to get there.
I'd rather make a request - please do everything you can to help that dream come true. Please Unlicense as much software as you can.
Chronometrist is released under your choice of [Unlicense](https://unlicense.org/) or the [WTFPL](http://www.wtfpl.net/).
(See files [UNLICENSE](UNLICENSE) and [WTFPL](WTFPL)).
<a id="thanks"></a>
# Thanks
wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support
jwiegley for `timeclock.el`, which we used as a backend in earlier versions
blandest for helping me with the name
fiete and wu-lee for testing and bug reports
# Footnotes
<sup><a id="fn.1" href="#fnr.1">1</a></sup> but not `chronometrist-before-in-functions`
<sup><a id="fn.2" href="#fnr.2">2</a></sup> It is possible to use Emacs' built-in backup system to do it, but since it is synchronous, doing so will greatly slow down saving of the Chronometrist file.

View File

@ -1,61 +1,55 @@
#+TITLE: Chronometrist #+TITLE: Chronometrist - an extensible time tracker for Emacs
#+SUBTITLE: Friendly and powerful personal time tracker/analyzer with Emacs and CLIM frontends #+SUBTITLE: Friendly and powerful personal time tracker and analyzer for Emacs
#+DESCRIPTION: User Manual #+DESCRIPTION: User Manual
#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="style.css" /> #+HTML_HEAD: <link rel="stylesheet" type="text/css" href="style.css" />
#+BEGIN_EXPORT html #+BEGIN_EXPORT html
<a href="https://liberapay.com/contrapunctus/donate"> <a href="https://liberapay.com/contrapunctus/donate"><img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay"></a>
<img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay">
</a>
<a href="https://melpa.org/#/chronometrist"> <a href="https://melpa.org/#/chronometrist"><img src="https://melpa.org/packages/chronometrist-badge.svg"></a>
<img src="https://melpa.org/packages/chronometrist-badge.svg">
</a>
#+END_EXPORT #+END_EXPORT
* Explanation A time tracker in Emacs with a nice interface
:PROPERTIES:
:CUSTOM_ID: explanation
:END:
Chronometrist is a friendly and powerful personal time tracker and analyzer. It has frontends for Emacs and [[https://mcclim.common-lisp.dev/][CLIM]].
#+CAPTION: The main Chronometrist buffer, with the enabled extensions [[#time-goals][chronometrist-goal]] ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks). Largely modelled after the Android application, [[https://github.com/netmackan/ATimeTracker][A Time Tracker]]
[[file:doc/2022-02-20 13-26-53.png]]
** Benefits * Benefits
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: benefits :CUSTOM_ID: benefits
:END: :END:
1. Extremely simple and efficient to use 1. Extremely simple and efficient to use
2. Displays useful information about your time usage (including fancy graphs with the =chronometrist-spark= extension) 2. Displays useful information about your time usage
3. Support for both mouse and keyboard 3. Support for both mouse and keyboard
4. Human errors in tracking can be easily fixed by editing a plain text file 4. Human errors in tracking are easily fixed by editing a plain text file
5. Hooks to integrate time tracking into your workflow 5. Hooks to let you perform arbitrary actions when starting/stopping tasks
6. Fancy graphs with the =chronometrist-spark= extension
** Limitations * Limitations
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: limitations :CUSTOM_ID: limitations
:END: :END:
1. No support for concurrent tasks. 1. No support for concurrent tasks.
** Comparisons * Comparisons
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: comparisons :CUSTOM_ID: comparisons
:END: :END:
*** timeclock.el (Emacs built-in) ** timeclock.el
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: timeclock.el :CUSTOM_ID: timeclock.el
:END: :END:
Compared to timeclock.el, Chronometrist Compared to timeclock.el, Chronometrist
+ stores data in an s-expression format rather than a line-based one + stores data in an s-expression format rather than a line-based one
+ supports attaching tags and arbitrary key-values to time intervals + supports attaching tags and arbitrary key-values to time intervals
+ has commands to shows useful summaries + has commands to shows useful summaries
+ has more hooks + has more hooks
*** Org time tracking ** Org time tracking
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: org-time-tracking :CUSTOM_ID: org-time-tracking
:END: :END:
Chronometrist and Org time tracking seem to be equivalent in terms of capabilities, approaching the same ends through different means. Chronometrist and Org time tracking seem to be equivalent in terms of capabilities, approaching the same ends through different means.
+ Chronometrist doesn't have a mode line indicator at the moment. (planned) + Chronometrist doesn't have a mode line indicator at the moment. (planned)
+ Chronometrist doesn't have Org's sophisticated querying facilities. (an SQLite backend is planned) + Chronometrist doesn't have Org's sophisticated querying facilities. (an SQLite backend is planned)
@ -63,126 +57,25 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
+ Chronometrist's UI is cleaner, since the storage is separate from the display. It doesn't show tasks as trees like Org, but it uses tags and key-values to achieve that. Additionally, navigating a flat list takes fewer user operations than navigating a tree. + Chronometrist's UI is cleaner, since the storage is separate from the display. It doesn't show tasks as trees like Org, but it uses tags and key-values to achieve that. Additionally, navigating a flat list takes fewer user operations than navigating a tree.
+ Chronometrist data is just s-expressions (plists), and may be easier to parse than a complex text format with numerous use-cases. + Chronometrist data is just s-expressions (plists), and may be easier to parse than a complex text format with numerous use-cases.
** Common Lisp port * Installation
:PROPERTIES:
:CUSTOM_ID: common-lisp-port
:END:
In March 2022, work began on the long-awaited Common Lisp port of Chronometrist, which aims to create -
1. a greater variety of backends (e.g. SQLite)
2. a common reusable library for frontends to use,
3. a greater variety of frontends, such as -
* a command line interface (CLI), for UNIX scripting;
* a terminal user inteface (TUI), for those so inclined;
* a CLIM (Common Lisp Interface Manager) GUI [fn:1],
* Qt and Android interfaces using [[https://gitlab.com/eql/lqml][LQML]],
* web frontends (possibly via [[https://common-lisp.net/project/parenscript/][Parenscript]] or [[https://github.com/rabbibotton/clog][CLOG]]),
* and perhaps even an interface for wearable devices!
The port was also driven by the desire to have access to Common Lisp's better performance, and features such as namespaces, a /de facto/ standard build system, multithreading, SQLite bindings, a more fully-featured implementation of CLOS and MOP, and type annotations, checking, and inference.
The literate sources for the Common Lisp port may be found in [[file:cl/chronometrist.org][cl/chronometrist.org]]. Currently, this port can -
1. import from a plist-group file and export to an SQLite database
#+BEGIN_SRC lisp
(chronometrist:to-file (chronometrist:to-hash-table
(make-instance 'chronometrist.plist-group:plist-group-backend
:file "/path/to/file.plg"))
(make-instance 'chronometrist.sqlite:sqlite-backend)
"/path/to/file.sqlite")
#+END_SRC
2. display a (WIP) CLIM GUI - =(chronometrist.clim:run-chronometrist)=
The Emacs Lisp codebase will probably become an Emacs frontend to a future Common Lisp CLI client.
[fn:1] McCLIM also has an incomplete ncurses backend - when completed, a CLIM frontend could provide a TUI "for free".
** Literate program
:PROPERTIES:
:CUSTOM_ID: explanation-literate-program
:END:
Chronometrist is written as an Org literate program, which makes it easy to obtain different views of the program source, thanks to tree- and source-block folding, tags, properties, and the =org-match= command.
The canonical source file is [[file:elisp/chronometrist.org][elisp/chronometrist.org]], which contains source blocks. These are provided to users after /tangling/ (extracting the source into an Emacs Lisp file). [fn:2]
The Org literate program can also be loaded directly using the [[https://github.com/jingtaozf/literate-elisp][literate-elisp]] package, so that all source links (e.g. =xref=, =describe-function=) lead to the Org file. See [[#how-to-literate-elisp][How to load the program using literate-elisp]].
[fn:2] the literate source is also included in MELPA installs, although not loaded through =literate-elisp-load= by default, since doing so would interfere with automatic generation of autoloads.
** Source code overview
:PROPERTIES:
:CUSTOM_ID: source-code-overview
:END:
At its most basic, we read data from a [[file:elisp/chronometrist.org::#program-backend][backend]] and [[file:elisp/chronometrist.org::#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list")][=tabulated-list-mode=]] buffer.
The plist and plist-group backends (collectively known as the s-expression backends) =read= a text file containing s-expressions into a [[file:elisp/chronometrist.org::#program-data-structures][hash table]], and query that. When the file is changed—whether by the program or the user—they [[file:elisp/chronometrist.org::refresh-file][update the hash table]] and the [[file:elisp/chronometrist.org::#program-frontend-chronometrist-refresh][buffer]]. The s-expression backends also make use of a [[file:elisp/chronometrist.org::#program-pretty-printer][plist pretty-printer]] of their own.
There are also some [[file:elisp/chronometrist.org::#program-migration][migration commands]].
Extensions exist for -
1. [[file:elisp/chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals,
2. [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]], and
3. support for the [[file:elisp/chronometrist-third.org][Third Time system]]
** Contributions and contact
:PROPERTIES:
:CUSTOM_ID: contributions-contact
:END:
Feedback and MRs are very welcome. 🙂
+ [[file:TODO.org]] has a long list of tasks
+ [[file:elisp/chronometrist.org]] contains all developer-oriented documentation
If you have tried using Chronometrist, I'd love to hear your experiences! Get in touch with the author and other Emacs users in the Emacs channel on the Jabber network - [[https://conversations.im/j/emacs@salas.suchat.org][xmpp:emacs@salas.suchat.org?join]] ([[https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org][web chat]])
(For help in getting started with Jabber, [[https://xmpp.org/getting-started/][click here]])
** License
:PROPERTIES:
:CUSTOM_ID: license
:END:
I'd /like/ for all software to be liberated - transparent, trustable, and accessible for anyone to use, study, or improve.
I'd /like/ anyone using my software to credit me for the work.
I'd /like/ to receive financial support for my efforts, so I can spend all my time doing what I find meaningful.
But I don't want to make demands or threats (e.g. via legal conditions) to accomplish all that, nor restrict my services to only those who can pay.
Thus, Chronometrist is released under your choice of [[https://unlicense.org/][Unlicense]] or the [[http://www.wtfpl.net/][WTFPL]].
(See files [[file:UNLICENSE][UNLICENSE]] and [[file:WTFPL][WTFPL]]).
** Thanks
:PROPERTIES:
:CUSTOM_ID: thanks
:END:
The main buffer and the report buffer are copied from the Android application, [[https://github.com/netmackan/ATimeTracker][A Time Tracker]]
wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support
jwiegley for =timeclock.el=, which we used as a backend in earlier versions
blandest for helping me with the name
fiete and wu-lee for testing and bug reports
* Tutorials
:PROPERTIES:
:CUSTOM_ID: usage
:END:
** Installation
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: installation :CUSTOM_ID: installation
:END: :END:
*** from MELPA ** from MELPA
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: install-from-melpa :CUSTOM_ID: install-from-melpa
:END: :END:
1. Set up MELPA - https://melpa.org/#/getting-started 1. Set up MELPA - https://melpa.org/#/getting-started
(Chronometrist uses Semantic Versioning and the developer is accident-prone, so using MELPA Stable is suggested 😏)
2. =M-x package-install RET chronometrist RET= 2. =M-x package-install RET chronometrist RET=
*** from Git ** from Git
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: install-from-git :CUSTOM_ID: install-from-git
:END: :END:
You can get =chronometrist= from https://tildegit.org/contrapunctus/chronometrist or https://codeberg.org/contrapunctus/chronometrist You can get =chronometrist= from https://tildegit.org/contrapunctus/chronometrist or https://codeberg.org/contrapunctus/chronometrist
=chronometrist= requires =chronometrist= requires
@ -190,12 +83,18 @@ You can get =chronometrist= from https://tildegit.org/contrapunctus/chronometris
+ [[https://github.com/magnars/dash.el][dash.el]] + [[https://github.com/magnars/dash.el][dash.el]]
+ [[https://github.com/alphapapa/ts.el][ts.el]] + [[https://github.com/alphapapa/ts.el][ts.el]]
Add the ="elisp/"= subdirectory to your load-path, and =(require 'chronometrist)=. Add the "elisp/" subdirectory to your load-path, and =(require 'chronometrist)=.
* Usage
:PROPERTIES:
:CUSTOM_ID: usage
:END:
** chronometrist ** chronometrist
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: usage-chronometrist :CUSTOM_ID: usage-chronometrist
:END: :END:
Run =M-x chronometrist= to see your projects, the time you spent on them today, which one is active, and the total time clocked today. Run =M-x chronometrist= to see your projects, the time you spent on them today, which one is active, and the total time clocked today.
Click or hit =RET= (=chronometrist-toggle-task=) on a project to start tracking time for it. If it's already clocked in, it will be clocked out. Click or hit =RET= (=chronometrist-toggle-task=) on a project to start tracking time for it. If it's already clocked in, it will be clocked out.
@ -208,6 +107,7 @@ Press =r= to see a weekly report (see =chronometrist-report=)
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: usage-chronometrist-report :CUSTOM_ID: usage-chronometrist-report
:END: :END:
Run =M-x chronometrist-report= (or =chronometrist= with a prefix argument of 1, or press =r= in the =chronometrist= buffer) to see a weekly report. Run =M-x chronometrist-report= (or =chronometrist= with a prefix argument of 1, or press =r= in the =chronometrist= buffer) to see a weekly report.
Press =b= to look at past weeks, and =f= for future weeks. Press =b= to look at past weeks, and =f= for future weeks.
@ -216,14 +116,12 @@ Press =b= to look at past weeks, and =f= for future weeks.
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: usage-chronometrist-statistics :CUSTOM_ID: usage-chronometrist-statistics
:END: :END:
Run =M-x chronometrist-statistics= (or =chronometrist= with a prefix argument of 2) to view statistics. Run =M-x chronometrist-statistics= (or =chronometrist= with a prefix argument of 2) to view statistics.
Press =b= to look at past time ranges, and =f= for future ones. Press =b= to look at past time ranges, and =f= for future ones.
** chronometrist-details ** chronometrist-details
:PROPERTIES:
:CUSTOM_ID: chronometrist-details
:END:
** common commands ** common commands
:PROPERTIES: :PROPERTIES:
@ -242,16 +140,18 @@ All buffers keep themselves updated via an idle timer - no need to frequently pr
If you wish you could define time goals for some tasks, and have Chronometrist notify you when you're approaching the goal, completing it, or exceeding it, check out the extension [[https://github.com/contrapunctus-1/chronometrist-goal/][chronometrist-goal.el]]. If you wish you could define time goals for some tasks, and have Chronometrist notify you when you're approaching the goal, completing it, or exceeding it, check out the extension [[https://github.com/contrapunctus-1/chronometrist-goal/][chronometrist-goal.el]].
* How-to Guides * How-to
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to :CUSTOM_ID: how-to
:END: :END:
See the Customize groups =chronometrist= and =chronometrist-report= for variables intended to be user-customizable. See the Customize groups =chronometrist= and =chronometrist-report= for variables intended to be user-customizable.
** How to display a prompt when exiting with an active task ** How to display a prompt when exiting with an active task
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-prompt-when-exiting-emacs :CUSTOM_ID: how-to-prompt-when-exiting-emacs
:END: :END:
Evaluate or add to your init.el the following - Evaluate or add to your init.el the following -
=(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)= =(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)=
@ -259,9 +159,7 @@ Evaluate or add to your init.el the following -
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-literate-elisp :CUSTOM_ID: how-to-literate-elisp
:END: :END:
The literate Org document will automatically =literate-elisp-load= itself when opened, if =literate-elisp= is installed via =package.el=.
If you want it to be loaded with =literate-elisp-load= on Emacs startup, add the following to your init.el -
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(add-to-list 'load-path "<directory containing chronometrist.org>") (add-to-list 'load-path "<directory containing chronometrist.org>")
@ -273,7 +171,8 @@ If you want it to be loaded with =literate-elisp-load= on Emacs startup, add the
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-tags :CUSTOM_ID: how-to-tags
:END: :END:
1. Add =chronometrist-tags-add= to one or more of these hooks [fn:3] -
1. Add =chronometrist-tags-add= to one or more of these hooks [fn:2] -
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(add-to-list 'chronometrist-after-in-functions 'chronometrist-tags-add) (add-to-list 'chronometrist-after-in-functions 'chronometrist-tags-add)
@ -284,32 +183,31 @@ If you want it to be loaded with =literate-elisp-load= on Emacs startup, add the
The prompt suggests past combinations you used for the current task, which you can browse with =M-p=/=M-n=. You can leave it blank by pressing =RET=. The prompt suggests past combinations you used for the current task, which you can browse with =M-p=/=M-n=. You can leave it blank by pressing =RET=.
[fn:3] but not =chronometrist-before-in-functions= [fn:2] but not =chronometrist-before-in-functions=
** How to attach key-values to time intervals ** How to attach key-values to time intervals
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-key-value-pairs :CUSTOM_ID: how-to-key-value-pairs
:END: :END:
1. Add =chronometrist-kv-add= to one or more of these hooks [fn:3] -
1. Add =chronometrist-kv-add= to one or more of these hooks [fn:2] -
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(add-to-list 'chronometrist-after-in-functions 'chronometrist-kv-add) (add-to-list 'chronometrist-after-in-functions 'chronometrist-kv-add)
(add-to-list 'chronometrist-before-out-functions 'chronometrist-kv-add) (add-to-list 'chronometrist-before-out-functions 'chronometrist-kv-add)
(add-to-list 'chronometrist-after-out-functions 'chronometrist-kv-add) (add-to-list 'chronometrist-after-out-functions 'chronometrist-kv-add)
#+END_SRC #+END_SRC
To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press =C-c C-c= to accept the key-values, or =C-c C-k= to cancel. To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press =C-c C-c= to accept the key-values, or =C-c C-k= to cancel.
** How to skip running hooks/attaching tags and key values ** How to skip running hooks/attaching tags and key values
:PROPERTIES:
:CUSTOM_ID: how-to-skip-running-hooks/attaching-tags-and-key-values
:END:
Use =M-RET= (=chronometrist-toggle-task-no-hooks=) to clock in/out. Use =M-RET= (=chronometrist-toggle-task-no-hooks=) to clock in/out.
** How to open certain files when you start a task ** How to open certain files when you start a task
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-open-files-on-task-start :CUSTOM_ID: how-to-open-files-on-task-start
:END: :END:
An idea from the author's own init - An idea from the author's own init -
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -327,6 +225,7 @@ An idea from the author's own init -
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-warn-uncommitted-changes :CUSTOM_ID: how-to-warn-uncommitted-changes
:END: :END:
Another one, prompting the user if they have uncommitted changes in a git repository (assuming they use [[https://magit.vc/][Magit]]) - Another one, prompting the user if they have uncommitted changes in a git repository (assuming they use [[https://magit.vc/][Magit]]) -
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -351,6 +250,7 @@ Return nil (and run `magit-status') if the user answers no."
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-activity-indicator :CUSTOM_ID: how-to-activity-indicator
:END: :END:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun my-activity-indicator () (defun my-activity-indicator ()
(--> (chronometrist-latest-record (chronometrist-active-backend)) (--> (chronometrist-latest-record (chronometrist-active-backend))
@ -368,7 +268,7 @@ Return nil (and run `magit-status') if the user answers no."
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: how-to-backup :CUSTOM_ID: how-to-backup
:END: :END:
I suggest backing up Chronometrist data on each save using the [[https://tildegit.org/contrapunctus/async-backup][async-backup]] package.[fn:4] Here's how you can do that. I suggest backing up Chronometrist data on each save using the [[https://tildegit.org/contrapunctus/async-backup][async-backup]] package.[fn:1] Here's how you can do that.
1. Add the following to your init. 1. Add the following to your init.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -377,10 +277,9 @@ I suggest backing up Chronometrist data on each save using the [[https://tildegi
2. Open your Chronometrist file and add =async-backup= to a buffer-local =after-save-hook=. 2. Open your Chronometrist file and add =async-backup= to a buffer-local =after-save-hook=.
: M-x chronometrist-open-log : M-x chronometrist-open-log
: M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET : M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET
3. Optionally, configure =async-backup-location= to set a specific directory for the backups - 3. Optionally, configure =backup-directory-alist= to set a specific directory for the backups.
: (setq async-backup-location "/path/to/backup/dir/")
[fn:4] It is possible to use Emacs' built-in backup system to do it, but since it is synchronous, doing so will greatly slow down saving of the Chronometrist file. [fn:1] It is possible to use Emacs' built-in backup system to do it, but since it is synchronous, doing so will greatly slow down saving of the Chronometrist file.
** How to configure Vertico for use with Chronometrist ** How to configure Vertico for use with Chronometrist
:PROPERTIES: :PROPERTIES:
@ -404,10 +303,18 @@ Or use =vertico-multiform= to disable sorting for only specific commands -
(chronometrist-key-values-unified-prompt (vertico-sort-function . nil))))) (chronometrist-key-values-unified-prompt (vertico-sort-function . nil)))))
#+END_SRC #+END_SRC
* User's reference * Explanation
** Literate Program
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: users-reference :CUSTOM_ID: explanation-literate-program
:END: :END:
Chronometrist is a literate program, made using Org - the canonical source is the =chronometrist.org= file, which contains source blocks. These are provided to users after /tangling/ (extracting the source into an Emacs Lisp file).
The Org file can also be loaded directly using the [[https://github.com/jingtaozf/literate-elisp][literate-elisp]] package, so that all source links (e.g. =xref=, =describe-function=) lead to the Org file, within the context of the concerned documentation. See [[#how-to-literate-elisp][How to load the program using literate-elisp]].
=chronometrist.org= is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads.
* User's reference
All variables intended for user customization are listed here. They serve as the public API for this project for the purpose of semantic versioning. Any changes to these which require a user to modify their configuration are considered breaking changes. All variables intended for user customization are listed here. They serve as the public API for this project for the purpose of semantic versioning. Any changes to these which require a user to modify their configuration are considered breaking changes.
1. =chronometrist-file= 1. =chronometrist-file=
@ -434,10 +341,47 @@ Hooks
8. =chronometrist-file-change-hook= 8. =chronometrist-file-change-hook=
9. =chronometrist-timer-hook= 9. =chronometrist-timer-hook=
* Local variables :noexport: * Contributions and contact
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: local-variables :CUSTOM_ID: contributions-contact
:END: :END:
Feedback and MRs are very welcome. 🙂
+ [[file:TODO.org]] has a long list of tasks
+ [[file:elisp/chronometrist.org]] contains all developer-oriented documentation
If you have tried using Chronometrist, I'd love to hear your experiences! Get in touch with the author and other Emacs users in the Emacs channel on the Jabber network - [[https://conversations.im/j/emacs@salas.suchat.org][xmpp:emacs@salas.suchat.org?join]] ([[https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org][web chat]])
(For help in getting started with Jabber, [[https://xmpp.org/getting-started/][click here]])
* License
:PROPERTIES:
:CUSTOM_ID: license
:END:
I dream of a world where all software is liberated - transparent, trustable, and accessible for anyone to use or improve. But I don't want to make demands or threats (e.g. via legal conditions) to get there.
I'd rather make a request - please do everything you can to help that dream come true. Please Unlicense as much software as you can.
Chronometrist is released under your choice of [[https://unlicense.org/][Unlicense]] or the [[http://www.wtfpl.net/][WTFPL]].
(See files [[file:UNLICENSE][UNLICENSE]] and [[file:WTFPL][WTFPL]]).
* Thanks
:PROPERTIES:
:CUSTOM_ID: thanks
:END:
wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support
jwiegley for =timeclock.el=, which we used as a backend in earlier versions
blandest for helping me with the name
fiete and wu-lee for testing and bug reports
* Local variables :noexport:
# Local Variables: # Local Variables:
# eval: (progn (require 'ox-md) (add-hook 'after-save-hook (lambda () (org-export-to-file 'md "manual.md")) nil t))
# my-org-src-default-lang: "emacs-lisp" # my-org-src-default-lang: "emacs-lisp"
# End: # End: