Merge branch 'dev' into sqlite

This commit is contained in:
contrapunctus 2022-03-21 09:19:17 +05:30
commit 239f733dd8
16 changed files with 1015 additions and 688 deletions

View File

@ -14,7 +14,8 @@
("cr" . "chronometrist-report") ("cr" . "chronometrist-report")
("cs" . "chronometrist-statistics") ("cs" . "chronometrist-statistics")
("cx" . "chronometrist-sexp") ("cx" . "chronometrist-sexp")
("c" . "chronometrist"))))) ("c" . "chronometrist")))
(sentence-end-double-space . t)))
(org-mode (org-mode
. ((org-html-self-link-headlines . t) . ((org-html-self-link-headlines . t)
(eval . (org-indent-mode)) (eval . (org-indent-mode))

View File

@ -5,6 +5,12 @@ 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.
@ -14,12 +20,15 @@ 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. 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.
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`.
### Fixed ### Fixed
1. File change detection code has been rewritten, hopefully fixing some uncommon `read` 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.
### Deprecated
1. The plist backend is deprecated and may be removed in a future release. The `plist-group` backend is more performant and extensible - please use `chronometrist-migrate` to convert your data to the `plist-group` backend.
## [0.9.0] - 2021-07-08 ## [0.9.0] - 2021-07-08
### Added ### Added

View File

@ -1,6 +1,6 @@
.phony: all setup tangle compile lint clean .phony: all setup tangle compile lint
all: clean clean-tangle manual.md setup tangle compile lint all: clean-elc manual.md setup tangle compile lint
setup: setup:
emacs --batch --eval="(package-initialize)" \ emacs --batch --eval="(package-initialize)" \
@ -14,74 +14,69 @@ manual.md:
# Org, we want to use it. # Org, we want to use it.
tangle: tangle:
cd elisp/ && \ cd elisp/ && \
emacs --batch \ emacs --batch \
--eval="(package-initialize)" \ --eval="(progn (package-initialize) (require 'ob-tangle))" \
--eval="(require 'ob-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-sqlite.org")' ; \ --eval='(org-babel-tangle-file "chronometrist-sqlite.org")' ; \
cd .. ; \ cd ..
compile: tangle compile: tangle
cd elisp/ && \ cd elisp/ && \
emacs -q -Q --batch \ emacs --batch \
--eval="(progn (package-initialize) (require 'dash) (require 'ts))" \ --eval="(progn (package-initialize) (require 'dash) (require 'ts))" \
--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-sqlite.el")' ; \ --eval='(byte-compile-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint-check-declare: tangle lint-check-declare: tangle
cd elisp/ && \ cd elisp/ && \
emacs -q -Q --batch \ emacs -q --batch \
--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-sqlite.el")' ; \ --eval='(check-declare-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint-checkdoc: tangle lint-checkdoc: tangle
cd elisp/ && \ cd elisp/ && \
emacs -q -Q --batch \ emacs -q -Q --batch \
--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-sqlite.el")' ; \ --eval='(checkdoc-file "chronometrist-sqlite.el")' ; \
cd .. cd ..
lint-package-lint: setup tangle lint-package-lint: setup tangle
cd elisp/ && \ cd elisp/ && \
emacs -q -Q --batch \ emacs --batch \
--eval="(progn (package-initialize) (require 'dash) (require 'ts))" \ --eval="(progn (package-initialize) (require 'dash) (require 'ts) (require 'package-lint))" \
--eval="(require 'package-lint)" \ -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-sqlite.el ; \ -f 'package-lint-batch-and-exit' chronometrist-sqlite.el ; \
cd .. cd ..
lint-relint: setup tangle lint-relint: setup tangle
cd elisp/ && \ cd elisp/ && \
emacs -q -Q --batch \ emacs --batch \
--eval="(package-initialize)" \ --eval="(progn (package-initialize) (require 'relint))" \
--eval="(require 'relint)" \ --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-sqlite.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
clean-tangle:
rm elisp/chronometrist.el \
elisp/chronometrist-key-values.el \
elisp/chronometrist-spark.el \
elisp/chronometrist-sqlite.el ;
clean-elc: clean-elc:
rm elisp/*.elc rm elisp/*.elc
clean: clean-elc

View File

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

1
README.org Symbolic link
View File

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

176
TODO.org
View File

@ -737,7 +737,9 @@ for <predicate>)"
#+END_SRC #+END_SRC
* STARTED customizable duration output :feature: * STARTED customizable duration output :feature:
See branch =format-seconds=. :PROPERTIES:
:branch: format-seconds
:END:
1. [X] define =chronometrist-duration-formats= to hold duration formats for different use cases in Chronometrist. 1. [X] define =chronometrist-duration-formats= to hold duration formats for different use cases in Chronometrist.
2. [ ] define customization type for =chronometrist-duration-formats= to create user-friendly Custom interface 2. [ ] define customization type for =chronometrist-duration-formats= to create user-friendly Custom interface
3. [ ] make non-tabular parts of Chronometrist buffers adapt to column widths, to accomodate changes in duration formats 3. [ ] make non-tabular parts of Chronometrist buffers adapt to column widths, to accomodate changes in duration formats
@ -797,12 +799,18 @@ protocol implementation progress
20. [ ] [superclass] chronometrist-timer 20. [ ] [superclass] chronometrist-timer
* New backends :feature: * New backends :feature:
1. Org time tracking ** Org time tracking
2. timeclock ** timeclock
3. [[https://klog.jotaen.net/][klog]] See docstring of =timeclock-log-data=
4. SQLite
5. https://github.com/projecthamster/hamster ** [[https://klog.jotaen.net/][klog]]
+ https://github.com/projecthamster/hamster/wiki/Our-datamodel :PROPERTIES:
:CUSTOM_ID: new-backends-klog
:END:
** SQLite
** Project Hamster
https://github.com/projecthamster/hamster
+ https://github.com/projecthamster/hamster/wiki/Our-datamodel
* Functions doing similar things :code: * Functions doing similar things :code:
1. =task-time-one-day= 1. =task-time-one-day=
@ -810,9 +818,10 @@ protocol implementation progress
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 :feature: * STARTED customizable task list [33%] :feature:
1. Interactive, buffer-local modification of task list, with completion (=completing-read-multiple=) 1. [X] Make =chronometrist-task-list= customizable
2. Adding a task? We can modify the task list, but how to persist it? 2. [ ] Interactive, buffer-local modification of task list, with completion (=completing-read-multiple=)
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.
@ -836,6 +845,9 @@ Command to delete the interval currently being recorded. (bind to 'k')
** 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=
@ -846,6 +858,9 @@ 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>] ...
@ -855,8 +870,12 @@ With different checks, for different levels of speed/thoroughness -
#+END_SRC #+END_SRC
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>")=.
- This will also make it easier to support formats like [[#new-backends-klog][klog]], which support this feature.
- 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.
** "event"/non-interval records :feature: ** "event records" - records with only a timestamp :feature:
:PROPERTIES: :PROPERTIES:
:CREATED: 2021-12-18T11:48:53+0530 :CREATED: 2021-12-18T11:48:53+0530
:END: :END:
@ -866,6 +885,35 @@ 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:
:PROPERTIES:
:CREATED: 2022-02-10T22:59:45+0530
:CUSTOM_ID: date-key-values
:branch: date-properties
:END:
Straightforward enough for the plist group backend -
#+BEGIN_SRC elisp
("<date>"
[<keyword> <value>]*
<plist>+)
#+END_SRC
Alternatively, this would be easier to work with - easier to select the key-values or the plists, as required -
#+BEGIN_SRC elisp
("<date>"
(key-values
[<keyword> <value>]+)
<plist>+)
#+END_SRC
=to-list= could omit the date key-values, since it has no concept of dates. =to-hash-table= could have them at the start of the lists of plists, i.e. hash values would still be =(cdr <plist group>)=.
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
@ -881,4 +929,108 @@ Records not used for time tracking, but to store data associated with a date or
:CREATED: 2022-01-08T23:32:37+0530 :CREATED: 2022-01-08T23:32:37+0530
:END: :END:
1. [ ] default suggested input backend should be the active backend 1. [ ] default suggested input backend should be the active backend
2. [ ] conserve file local variables prop line 2. [ ] conserve file local variables prop line for text backends
* STARTED Support for the Third Time System [57%] :extension:
:PROPERTIES:
:CREATED: 2022-02-10T14:12:12+0530
:branch: third-time
:END:
[[https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work][Third Time: a better way to work]]
#+BEGIN_QUOTE
Lastly, here in one place are all the steps for using Third Time:
1. Note the time, or start a stopwatch
2. /Work/ until you want or need to break
3. Divide how long youve just worked by 3 (or use your chosen fraction), and add any minutes left over from previous breaks
4. Set an alarm for that long
5. /Break/ until the alarm goes off, or you decide to resume work
6. If you resume early, note how much time was left, to add to your next break
7. Go back to step 1.
Additional rules:
+ If you have to stop work for a /non-work-related interruption/, start a break immediately.
+ You can (optionally) take a /big break/ for lunch and/or dinner, lasting as long as you like. Set an alarm at the start for when youll resume work. A big break uses up any saved break minutes, so you cant carry them over to the afternoon/evening.
+ /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
1. [X] =chronometrist-third-fraction=
2. [X] =chronometrist-third-break-time=
3. [X] on clock out, increment =break-time= and start timed notification
4. [X] on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero.
5. [ ] "big break" command - user enters a time, for which we set an alarm. User must resume working around that time.
6. [ ] Define work hours
* [ ] Custom variable for default work hours.
* [ ] Hook function which inserts default 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.
** 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
1. persist =break-time= between Emacs sessions
2. audible alerts
3. handle user edits to the database
* last record modified - ?
* remove last record - ?
* example - interval extended
1. work 10 minutes and clock out - +3m break time
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
* STARTED Customizable alerts [50%] :feature:code:
:PROPERTIES:
:CREATED: 2022-02-14T08:22:36+0530
:CUSTOM_ID: customizable-alerts
:branch: alert-macro
:END:
=chronometrist-third= and =chronometrist-goal= have nearly identical alert code; additionally, users cannot customize the alert style per-alert without basically rewriting the alert functions.
Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-functions= to customizable alists[fn:4] whose entries take the form =(SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS)=, where
+ =SYMBOL= uniquely identifies the alert,
+ =FUNCTION= is a function calling =chronometrist-*-run-at-time= and =ALERT-FN=, and
+ =ALERT-ARGS= are passed to =ALERT-FN=.
=ALERT-FN= will usually be =alert=, and =ALERT-ARGS= will usually be keyword arguments passed to =alert=.
Similar to how they behave now, these packages will start/stop functions for all entries provided in these alists. (So the user can still control which alerts are run.)
Also define =chronometrist--timer-alist=, which associates =symbol= with a timer object.
1. [X] write macro
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.
* 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:

BIN
doc/2022-02-20 13-26-53.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -192,6 +192,29 @@ 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
@ -404,30 +427,35 @@ 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 (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name))) (cl-defun chronometrist-key-values-unified-prompt
(&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
(cl-loop for plist in (chronometrist-to-list backend) (when chronometrist-key-value-use-database-history
when (equal (plist-get plist :name) task) (cl-loop for plist in (chronometrist-to-list backend)
collect when (equal (plist-get plist :name) task)
(let ((plist (chronometrist-plist-remove plist :name :start :stop))) collect
(when plist (format "%S" plist))) (let ((plist (chronometrist-plist-remove plist :name :start :stop)))
into key-value-plists (when plist (format "%S" plist)))
finally return into key-value-plists
(--> (seq-filter #'identity key-value-plists) finally return
(cl-remove-duplicates it :test #'equal :from-end t)))) (--> (seq-filter #'identity key-value-plists)
(cl-remove-duplicates it :test #'equal :from-end t)))))
(latest (chronometrist-latest-record backend))) (latest (chronometrist-latest-record backend)))
(if (null key-values) (if (and (null presets) (null key-values))
(progn (chronometrist-tags-add) (chronometrist-kv-add)) (progn (chronometrist-tags-add) (chronometrist-kv-add))
(chronometrist-replace-last (let* ((candidates (append presets key-values))
backend (input (completing-read
(chronometrist-plist-update (format "Key-values for %s: " task)
latest candidates nil nil nil 'chronometrist-key-values-unified-prompt-history)))
(read (completing-read (format "Key-values for %s: " task) (chronometrist-replace-last backend
key-values nil nil nil 'chronometrist-key-values-unified-prompt-history)))))) (chronometrist-plist-update latest
(read input))))))
t) t)
(provide 'chronometrist-key-values) (provide 'chronometrist-key-values)

View File

@ -330,6 +330,39 @@ 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*"
@ -337,6 +370,7 @@ used in `chronometrist-before-out-functions'."
: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
@ -707,41 +741,48 @@ 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 (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name))) (cl-defun chronometrist-key-values-unified-prompt
(&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
(cl-loop for plist in (chronometrist-to-list backend) (when chronometrist-key-value-use-database-history
when (equal (plist-get plist :name) task) (cl-loop for plist in (chronometrist-to-list backend)
collect when (equal (plist-get plist :name) task)
(let ((plist (chronometrist-plist-remove plist :name :start :stop))) collect
(when plist (format "%S" plist))) (let ((plist (chronometrist-plist-remove plist :name :start :stop)))
into key-value-plists (when plist (format "%S" plist)))
finally return into key-value-plists
(--> (seq-filter #'identity key-value-plists) finally return
(cl-remove-duplicates it :test #'equal :from-end t)))) (--> (seq-filter #'identity key-value-plists)
(cl-remove-duplicates it :test #'equal :from-end t)))))
(latest (chronometrist-latest-record backend))) (latest (chronometrist-latest-record backend)))
(if (null key-values) (if (and (null presets) (null key-values))
(progn (chronometrist-tags-add) (chronometrist-kv-add)) (progn (chronometrist-tags-add) (chronometrist-kv-add))
(chronometrist-replace-last (let* ((candidates (append presets key-values))
backend (input (completing-read
(chronometrist-plist-update (format "Key-values for %s: " task)
latest candidates nil nil nil 'chronometrist-key-values-unified-prompt-history)))
(read (completing-read (format "Key-values for %s: " task) (chronometrist-replace-last backend
key-values nil nil nil 'chronometrist-key-values-unified-prompt-history)))))) (chronometrist-plist-update latest
(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

@ -0,0 +1,178 @@
;;; chronometrist-third.el --- Third Time support 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 "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.0.1
;; 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:
;; Add support for the Third Time system to Chronometrist. In Third
;; Time, you work for any length of time you like, and "earn" a third
;; of the work time as break time. For a more detailed explanation,
;; see
;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md
;;; Code:
(require 'chronometrist)
(require 'alert)
;; [[file:chronometrist-third.org::*group][group:1]]
(defgroup chronometrist-third nil
"Third Time support for Chronometrist."
:group 'chronometrist)
;; group:1 ends here
;; [[file:chronometrist-third.org::*divisor][divisor:1]]
(defcustom chronometrist-third-divisor 3
"Number to determine accumulation of break time relative to work time."
:type 'number)
;; divisor:1 ends here
;; [[file:chronometrist-third.org::*duration-format][duration-format:1]]
(defcustom chronometrist-third-duration-format "%H, %M and %S%z"
"Format string for durations, passed to `format-seconds'."
:type 'string)
;; duration-format:1 ends here
;; [[file:chronometrist-third.org::*break-time][break-time:1]]
(defvar chronometrist-third-break-time 0
"Accumulated break time in seconds.")
;; break-time:1 ends here
;; [[file:chronometrist-third.org::*alert-functions][alert-functions:1]]
(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert)
"List of timed alerts for the Third Time system.
Typically, each function in this list should call
`chronometrist-third-run-at-time' to run another function, which
in turn should call `alert' to notify the user.
All functions in this list are started when the user clocks out,
and stopped when they clock in."
:group 'chronometrist-third
:type 'hook)
;; alert-functions:1 ends here
;; [[file:chronometrist-third.org::*timer-list][timer-list:1]]
(defvar chronometrist-third-timer-list nil)
;; timer-list:1 ends here
;; [[file:chronometrist-third.org::*run-at-time][run-at-time:1]]
(defun chronometrist-third-run-at-time (time repeat function &rest args)
"Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'."
(cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list))
;; run-at-time:1 ends here
;; [[file:chronometrist-third.org::*half-alert][half-alert:1]]
(defun chronometrist-third-half-alert ()
"Display an alert when half the break time is consumed."
(let ((half-time (/ chronometrist-third-break-time 2.0)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
half-time nil
(lambda (half-time)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format half-time))))
half-time))))
;; half-alert:1 ends here
;; [[file:chronometrist-third.org::*quarter-alert][quarter-alert:1]]
(defun chronometrist-third-quarter-alert ()
"Display an alert when 3/4ths of the break time is consumed."
(let ((three-fourths (* chronometrist-third-break-time 7.5)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
three-fourths nil
(lambda (three-fourths)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format
(- chronometrist-third-break-time three-fourths)))))
three-fourths))))
;; quarter-alert:1 ends here
;; [[file:chronometrist-third.org::*break-over-alert][break-over-alert:1]]
(defun chronometrist-third-break-over-alert ()
"Display an alert when break time is over."
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
chronometrist-third-break-time nil
(lambda () (alert (format "Break time is over!"))))))
;; break-over-alert:1 ends here
;; [[file:chronometrist-third.org::*start-alert-timers][start-alert-timers:1]]
(defun chronometrist-third-start-alert-timers ()
"Run functions in `chronometrist-third-alert-functions'."
(mapc #'funcall chronometrist-third-alert-functions))
;; start-alert-timers:1 ends here
;; [[file:chronometrist-third.org::*stop-alert-timers][stop-alert-timers:1]]
(defun chronometrist-third-stop-alert-timers ()
"Stop timers in `chronometrist-third-timer-list'."
(mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list))
;; stop-alert-timers:1 ends here
;; [[file:chronometrist-third.org::*clock-in][clock-in:1]]
(defun chronometrist-third-clock-in (&optional _arg)
"Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time)
(-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-string (format-seconds chronometrist-third-duration-format used-break))
(new-break (- chronometrist-third-break-time used-break))
(old-break chronometrist-third-break-time))
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0))
(alert
(if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break)
used-break-string)
(format "You have used %s of your break time (%s left)"
used-break-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
;; clock-in:1 ends here
;; [[file:chronometrist-third.org::*clock-out][clock-out:1]]
(defun chronometrist-third-clock-out (&optional _arg)
"Update break time based on the latest work interval.
Run `chronometrist-third-alert-functions' to alert user when
break time is up."
(let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))
(break-time-increment (/ latest-work-duration chronometrist-third-divisor)))
(cl-incf chronometrist-third-break-time break-time-increment)
(alert (format "You have gained %s of break time (%s total)"
(format-seconds chronometrist-third-duration-format break-time-increment)
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))
;; start alert timer(s)
(chronometrist-third-start-alert-timers)))
;; clock-out:1 ends here
;; [[file:chronometrist-third.org::*third-minor-mode][third-minor-mode:1]]
;;;###autoload
(define-minor-mode chronometrist-third-minor-mode
nil nil nil nil
(cond (chronometrist-third-minor-mode
(add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))
(t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))))
;; third-minor-mode:1 ends here
(provide 'chronometrist-third)
;;; chronometrist-third.el ends here

View File

@ -0,0 +1,213 @@
#+TITLE: chronometrist-third
#+SUBTITLE: Third Time System extension for Chronometrist
#+PROPERTY: header-args :tangle yes :load yes :comments link
* Program source
** Library headers and commentary
#+BEGIN_SRC emacs-lisp :comments no
;;; chronometrist-third.el --- Third Time support 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 "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.0.1
;; 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:
;; Add support for the Third Time system to Chronometrist. In Third
;; Time, you work for any length of time you like, and "earn" a third
;; of the work time as break time. For a more detailed explanation,
;; see
;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md
#+END_SRC
** Dependencies
#+BEGIN_SRC emacs-lisp :comments no
;;; Code:
(require 'chronometrist)
(require 'alert)
#+END_SRC
** group :custom:group:
#+BEGIN_SRC emacs-lisp
(defgroup chronometrist-third nil
"Third Time support for Chronometrist."
:group 'chronometrist)
#+END_SRC
** divisor :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-divisor 3
"Number to determine accumulation of break time relative to work time."
:type 'number)
#+END_SRC
** duration-format :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-duration-format "%H, %M and %S%z"
"Format string for durations, passed to `format-seconds'."
:type 'string)
#+END_SRC
** break-time :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-third-break-time 0
"Accumulated break time in seconds.")
#+END_SRC
** alert-functions :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert)
"List of timed alerts for the Third Time system.
Typically, each function in this list should call
`chronometrist-third-run-at-time' to run another function, which
in turn should call `alert' to notify the user.
All functions in this list are started when the user clocks out,
and stopped when they clock in."
:group 'chronometrist-third
:type 'hook)
#+END_SRC
** timer-list :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-third-timer-list nil)
#+END_SRC
** run-at-time :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-run-at-time (time repeat function &rest args)
"Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'."
(cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list))
#+END_SRC
** half-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-half-alert ()
"Display an alert when half the break time is consumed."
(let ((half-time (/ chronometrist-third-break-time 2.0)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
half-time nil
(lambda (half-time)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format half-time))))
half-time))))
#+END_SRC
** quarter-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-quarter-alert ()
"Display an alert when 3/4ths of the break time is consumed."
(let ((three-fourths (* chronometrist-third-break-time 7.5)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
three-fourths nil
(lambda (three-fourths)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format
(- chronometrist-third-break-time three-fourths)))))
three-fourths))))
#+END_SRC
** break-over-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-break-over-alert ()
"Display an alert when break time is over."
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
chronometrist-third-break-time nil
(lambda () (alert (format "Break time is over!"))))))
#+END_SRC
** start-alert-timers :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-start-alert-timers ()
"Run functions in `chronometrist-third-alert-functions'."
(mapc #'funcall chronometrist-third-alert-functions))
#+END_SRC
** stop-alert-timers :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-stop-alert-timers ()
"Stop timers in `chronometrist-third-timer-list'."
(mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list))
#+END_SRC
** clock-in :hook:procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-clock-in (&optional _arg)
"Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time)
(-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-string (format-seconds chronometrist-third-duration-format used-break))
(new-break (- chronometrist-third-break-time used-break))
(old-break chronometrist-third-break-time))
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0))
(alert
(if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break)
used-break-string)
(format "You have used %s of your break time (%s left)"
used-break-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
#+END_SRC
** clock-out :hook:procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-clock-out (&optional _arg)
"Update break time based on the latest work interval.
Run `chronometrist-third-alert-functions' to alert user when
break time is up."
(let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))
(break-time-increment (/ latest-work-duration chronometrist-third-divisor)))
(cl-incf chronometrist-third-break-time break-time-increment)
(alert (format "You have gained %s of break time (%s total)"
(format-seconds chronometrist-third-duration-format break-time-increment)
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))
;; start alert timer(s)
(chronometrist-third-start-alert-timers)))
#+END_SRC
** third-minor-mode :minor:mode:
#+BEGIN_SRC emacs-lisp
;;;###autoload
(define-minor-mode chronometrist-third-minor-mode
nil nil nil nil
(cond (chronometrist-third-minor-mode
(add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))
(t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))))
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp :comments no
(provide 'chronometrist-third)
;;; chronometrist-third.el ends here
#+END_SRC
* Local variables :noexport:
# 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)))
# End:

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.9.0 ;; Version: 0.10.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::*events-update][events-update:1]] ;; [[file:chronometrist.org::*ht-update][ht-update:1]]
(defun chronometrist-events-update (plist hash-table &optional replace) (defun chronometrist-ht-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,28 +327,29 @@ 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))
;; events-update:1 ends here ;; ht-update:1 ends here
;; [[file:chronometrist.org::*last-date][last-date:1]] ;; [[file:chronometrist.org::*ht-last-date][ht-last-date:1]]
(defun chronometrist-events-last-date (hash-table) (defun chronometrist-ht-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)
(car it))) (cl-first it)))
;; last-date:1 ends here ;; ht-last-date:1 ends here
;; [[file:chronometrist.org::*events-last][events-last:1]] ;; [[file:chronometrist.org::*ht-last][ht-last:1]]
(cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend))) (cl-defun chronometrist-ht-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-events-last-date hash-table))) (last-date (chronometrist-ht-last-date hash-table)))
(--> (gethash last-date hash-table) (--> (gethash last-date hash-table)
(last it) (last it)
(car it)))) (car it))))
;; events-last:1 ends here ;; ht-last:1 ends here
;; [[file:chronometrist.org::#program-data-structures-events-subset][events-subset:1]] ;; [[file:chronometrist.org::#program-data-structures-ht-subset][ht-subset:1]]
(defun chronometrist-events-subset (start end hash-table) (defun chronometrist-ht-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).
@ -363,7 +364,7 @@ treated as though their time is 00:00:00."
(puthash key value subset))) (puthash key value subset)))
hash-table) hash-table)
subset)) subset))
;; events-subset:1 ends here ;; ht-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)))
@ -371,7 +372,7 @@ treated as though their time is 00:00:00."
The return value is seconds, as an integer." The return value is seconds, as an integer."
(let ((task-events (chronometrist-task-records-for-date backend task date))) (let ((task-events (chronometrist-task-records-for-date backend task date)))
(if task-events (if task-events
(->> (chronometrist-events-to-durations task-events) (->> (chronometrist-plists-to-durations task-events)
(-reduce #'+) (-reduce #'+)
(truncate)) (truncate))
;; no events for this task on DATE, i.e. no time spent ;; no events for this task on DATE, i.e. no time spent
@ -427,25 +428,13 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by
:dow dow :tz-offset utcoff)))) :dow dow :tz-offset utcoff))))
;; iso-to-ts:1 ends here ;; iso-to-ts:1 ends here
;; [[file:chronometrist.org::*events-to-durations][events-to-durations:1]] ;; [[file:chronometrist.org::*plists-to-durations][plists-to-durations:1]]
(defun chronometrist-events-to-durations (events) (defun chronometrist-plists-to-durations (plists)
"Convert EVENTS into a list of durations in seconds. "Convert PLISTS into a list of durations in seconds.
EVENTS must be a list of valid Chronometrist property lists (see PLISTS must be a list of valid Chronometrist property lists (see
`chronometrist-file'). `chronometrist-file'). Return 0 if PLISTS is nil."
(if plists (mapcar #'chronometrist-interval plists) 0))
Return 0 if EVENTS is nil." ;; plists-to-durations:1 ends here
(if events
(cl-loop for plist in events collect
(let* ((start-ts (chronometrist-iso-to-ts
(plist-get plist :start)))
(stop-iso (plist-get plist :stop))
;; Add a stop time if it does not exist.
(stop-ts (if stop-iso
(chronometrist-iso-to-ts stop-iso)
(ts-now))))
(ts-diff stop-ts start-ts)))
0))
;; events-to-durations:1 ends here
;; [[file:chronometrist.org::*date-iso][date-iso:1]] ;; [[file:chronometrist.org::*date-iso][date-iso:1]]
(cl-defun chronometrist-date-iso (&optional (ts (ts-now))) (cl-defun chronometrist-date-iso (&optional (ts (ts-now)))
@ -510,13 +499,14 @@ SECONDS must be a positive integer."
;; seconds-to-hms:1 ends here ;; seconds-to-hms:1 ends here
;; [[file:chronometrist.org::*interval][interval:1]] ;; [[file:chronometrist.org::*interval][interval:1]]
(defun chronometrist-interval (event) (defun chronometrist-interval (plist)
"Return the period of time covered by EVENT as a time value. "Return the period of time covered by EVENT as a time value.
EVENT should be a plist (see `chronometrist-file')." EVENT should be a plist (see `chronometrist-file')."
(let ((start (plist-get event :start)) (let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start)))
(stop (plist-get event :stop))) (stop-iso (plist-get plist :stop))
(time-subtract (parse-iso8601-time-string stop) ;; Add a stop time if it does not exist.
(parse-iso8601-time-string start)))) (stop-ts (if stop-iso (chronometrist-iso-to-ts stop-iso) (ts-now))))
(ts-diff stop-ts start-ts)))
;; interval:1 ends here ;; interval:1 ends here
;; [[file:chronometrist.org::*format-duration-long][format-duration-long:1]] ;; [[file:chronometrist.org::*format-duration-long][format-duration-long:1]]
@ -897,8 +887,12 @@ Signal an error if there is no record to remove.")
;; [[file:chronometrist.org::*latest-record][latest-record:1]] ;; [[file:chronometrist.org::*latest-record][latest-record:1]]
(cl-defgeneric chronometrist-latest-record (backend) (cl-defgeneric chronometrist-latest-record (backend)
"Return the latest entry from BACKEND as a plist, or nil if BACKEND contains no records. "Return the latest record from BACKEND as a plist, or nil if BACKEND contains no records.
Return value may be active, i.e. it may or may not have a :stop key-value.") Return value may be active, i.e. it may or may not have a `:stop'
key-value.
If the latest record starts on one day and ends on another, the
entire (unsplit) record must be returned.")
;; latest-record:1 ends here ;; latest-record:1 ends here
;; [[file:chronometrist.org::*task-records-for-date][task-records-for-date:1]] ;; [[file:chronometrist.org::*task-records-for-date][task-records-for-date:1]]
@ -1112,6 +1106,7 @@ hash table values must be in chronological order.")
;; [[file:chronometrist.org::*on-file-path-change][on-file-path-change:1]] ;; [[file:chronometrist.org::*on-file-path-change][on-file-path-change:1]]
(cl-defmethod chronometrist-on-file-path-change ((backend chronometrist-file-backend-mixin) _old-path new-path) (cl-defmethod chronometrist-on-file-path-change ((backend chronometrist-file-backend-mixin) _old-path new-path)
"Update path and file slots of BACKEND to use NEW-PATH when `chronometrist-file' is changed."
(with-slots (path extension file) backend (with-slots (path extension file) backend
(setf path new-path (setf path new-path
file (concat path "." extension)))) file (concat path "." extension))))
@ -1394,7 +1389,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-events-last-date hash-table)) ((latest-date (chronometrist-ht-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
@ -1503,7 +1498,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-events-update new-plist hash-table)) (setf hash-table (chronometrist-ht-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
@ -1513,8 +1508,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-events-last backend))) ((&plist :name old-task) (chronometrist-ht-last backend)))
(setf hash-table (chronometrist-events-update new-plist hash-table t)) (setf hash-table (chronometrist-ht-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
@ -1524,8 +1519,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-events-last)) (-let (((&plist :name old-task) (chronometrist-ht-last))
(date (chronometrist-events-last-date hash-table))) (date (chronometrist-ht-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.
@ -1658,7 +1653,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
t)))) t))))
;; insert:1 ends here ;; insert:1 ends here
;; [[file:chronometrist.org::*plists-split-p][plists-split-p:1]] ;; [[file:chronometrist.org::#program-data-structures-plists-split-p][plists-split-p:1]]
(defun chronometrist-plists-split-p (old-plist new-plist) (defun chronometrist-plists-split-p (old-plist new-plist)
"Return t if OLD-PLIST and NEW-PLIST are split plists. "Return t if OLD-PLIST and NEW-PLIST are split plists.
Split plists means the :stop time of old-plist must be the same as Split plists means the :stop time of old-plist must be the same as
@ -1708,8 +1703,14 @@ Return value is either a list in the form
(new-plist-wo-time (chronometrist-plist-remove new-plist :start :stop))) (new-plist-wo-time (chronometrist-plist-remove new-plist :start :stop)))
(cond ((not (and old-plist new-plist)) nil) (cond ((not (and old-plist new-plist)) nil)
((equal old-plist-wo-time new-plist-wo-time) ((equal old-plist-wo-time new-plist-wo-time)
(let ((plist (cl-copy-list old-plist))) (let* ((plist (cl-copy-list old-plist))
(plist-put plist :stop (plist-get new-plist :stop)))) (new-stop (plist-get new-plist :stop)))
;; Usually, a split plist has a `:stop' key. However, a
;; user may clock out and delete the stop time, resulting
;; in a split record without a `:stop' key.
(if new-stop
(plist-put plist :stop new-stop)
(chronometrist-plist-remove plist :stop))))
(t (error "Attempt to unify plists with non-identical key-values"))))) (t (error "Attempt to unify plists with non-identical key-values")))))
;; plist-unify:1 ends here ;; plist-unify:1 ends here
@ -1788,7 +1789,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-events-last-date hash-table)) (old-date (chronometrist-ht-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
@ -1802,7 +1803,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-events-last-date hash-table)) (-let* ((old-date (chronometrist-ht-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))
@ -1831,7 +1832,10 @@ Return value is either a list in the form
;; [[file:chronometrist.org::*latest-record][latest-record:1]] ;; [[file:chronometrist.org::*latest-record][latest-record:1]]
(cl-defmethod chronometrist-latest-record ((backend chronometrist-plist-group-backend)) (cl-defmethod chronometrist-latest-record ((backend chronometrist-plist-group-backend))
(cl-first (last (chronometrist-latest-date-records backend)))) (with-slots (file) backend
(if (chronometrist-last-two-split-p file)
(apply #'chronometrist-plist-unify (chronometrist-last-two-split-p (chronometrist-backend-file (chronometrist-active-backend))))
(cl-first (last (chronometrist-latest-date-records backend))))))
;; latest-record:1 ends here ;; latest-record:1 ends here
;; [[file:chronometrist.org::*task-records-for-date][task-records-for-date:1]] ;; [[file:chronometrist.org::*task-records-for-date][task-records-for-date:1]]
@ -1853,6 +1857,7 @@ Return value is either a list in the form
(error "No record to replace in %s" (eieio-object-class-name backend))) (error "No record to replace in %s" (eieio-object-class-name backend)))
(chronometrist-sexp-in-file (chronometrist-backend-file backend) (chronometrist-sexp-in-file (chronometrist-backend-file backend)
(chronometrist-remove-last backend :save nil) (chronometrist-remove-last backend :save nil)
(delete-trailing-whitespace)
(chronometrist-insert backend plist :save nil) (chronometrist-insert backend plist :save nil)
(save-buffer) (save-buffer)
t)) t))
@ -2306,7 +2311,7 @@ task. N must be a positive integer."
(chronometrist-task-at-point))) (chronometrist-task-at-point)))
;; goto-nth-task:1 ends here ;; goto-nth-task:1 ends here
;; [[file:chronometrist.org::*refresh][refresh:1]] ;; [[file:chronometrist.org::#program-frontend-chronometrist-refresh][refresh:1]]
(defun chronometrist-refresh (&optional _ignore-auto _noconfirm) (defun chronometrist-refresh (&optional _ignore-auto _noconfirm)
"Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'. "Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'.
The optional arguments _IGNORE-AUTO and _NOCONFIRM are ignored, The optional arguments _IGNORE-AUTO and _NOCONFIRM are ignored,
@ -2581,7 +2586,7 @@ Has no effect if a task is active."
(message "Nothing to discard - use this when clocked in.")))) (message "Nothing to discard - use this when clocked in."))))
;; discard-active:1 ends here ;; discard-active:1 ends here
;; [[file:chronometrist.org::*chronometrist][chronometrist:1]] ;; [[file:chronometrist.org::#program-frontend-chronometrist-command][chronometrist:1]]
;;;###autoload ;;;###autoload
(defun chronometrist (&optional arg) (defun chronometrist (&optional arg)
"Display the user's tasks and the time spent on them today. "Display the user's tasks and the time spent on them today.
@ -2815,7 +2820,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
(chronometrist-setup-file-watch)) (chronometrist-setup-file-watch))
;; report-mode:1 ends here ;; report-mode:1 ends here
;; [[file:chronometrist.org::*chronometrist-report][chronometrist-report:1]] ;; [[file:chronometrist.org::#program-frontend-report-command][chronometrist-report:1]]
;;;###autoload ;;;###autoload
(defun chronometrist-report (&optional keep-date) (defun chronometrist-report (&optional keep-date)
"Display a weekly report of the data in `chronometrist-file'. "Display a weekly report of the data in `chronometrist-file'.
@ -2927,7 +2932,7 @@ displayed. They must be ts structs (see `ts.el').")
when (setq events-in-day (chronometrist-task-records-for-date backend task date)) when (setq events-in-day (chronometrist-task-records-for-date backend task date))
do (cl-incf days) and do (cl-incf days) and
collect collect
(-reduce #'+ (chronometrist-events-to-durations events-in-day)) (-reduce #'+ (chronometrist-plists-to-durations events-in-day))
into per-day-time-list into per-day-time-list
finally return finally return
(if per-day-time-list (if per-day-time-list
@ -2942,7 +2947,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-events-subset'." `chronometrist-ht-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)
@ -2971,12 +2976,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-events-subset start end hash-table))) (ht (chronometrist-ht-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-events-subset start end hash-table))) (ht (chronometrist-ht-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
@ -3066,7 +3071,7 @@ value of `revert-buffer-function'."
(chronometrist-setup-file-watch)) (chronometrist-setup-file-watch))
;; statistics-mode:1 ends here ;; statistics-mode:1 ends here
;; [[file:chronometrist.org::*chronometrist-statistics][chronometrist-statistics:1]] ;; [[file:chronometrist.org::#program-frontend-statistics-command][chronometrist-statistics:1]]
;;;###autoload ;;;###autoload
(defun chronometrist-statistics (&optional preserve-state) (defun chronometrist-statistics (&optional preserve-state)
"Display statistics for Chronometrist data. "Display statistics for Chronometrist data.
@ -3359,10 +3364,13 @@ TABLE must be a hash table as returned by
collect plist) collect plist)
do (ts-adjustf begin-ts 'day 1))))))) do (ts-adjustf begin-ts 'day 1)))))))
;; (chronometrist-details-intervals-for-range nil chronometrist-events) ;; (chronometrist-details-intervals-for-range nil (chronometrist-to-hash-table (chronometrist-active-backend)))
;; (chronometrist-details-intervals-for-range "2021-06-01" chronometrist-events) ;; (chronometrist-details-intervals-for-range "2021-06-01"
;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events) ;; (chronometrist-to-hash-table (chronometrist-active-backend)))
;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") chronometrist-events) ;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03")
;; (chronometrist-to-hash-table (chronometrist-active-backend)))
;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30")
;; (chronometrist-to-hash-table (chronometrist-active-backend)))
;; intervals-for-range:1 ends here ;; intervals-for-range:1 ends here
;; [[file:chronometrist.org::*input-to-value][input-to-value:1]] ;; [[file:chronometrist.org::*input-to-value][input-to-value:1]]

File diff suppressed because it is too large Load Diff

438
manual.md
View File

@ -1,438 +0,0 @@
# 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](#org690c2df)
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](#org63b85c6)
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](#org4f83f29)
1. [Literate Program](#explanation-literate-program)
8. [User's reference](#org07932c3)
9. [Contributions and contact](#contributions-contact)
10. [License](#license)
11. [Thanks](#thanks)
12. [Local variables](#org6ca2f77):NOEXPORT:
<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="org690c2df"></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.2" class="footref" href="#fn.2" role="doc-backlink">2</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="org63b85c6"></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.3" class="footref" href="#fn.3" role="doc-backlink">3</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.
Adapted from this [StackOverflow answer](https://stackoverflow.com/questions/6916529/how-can-i-make-emacs-backup-every-time-i-save).
<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="org4f83f29"></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="org07932c3"></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
<a id="org6ca2f77"></a>
# Local variables :NOEXPORT:
# 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> but not `chronometrist-before-in-functions`
<sup><a id="fn.3" href="#fnr.3">3</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

@ -4,14 +4,19 @@
#+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"><img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay"></a> <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 href="https://melpa.org/#/chronometrist">
<img src="https://melpa.org/packages/chronometrist-badge.svg">
</a>
#+END_EXPORT #+END_EXPORT
A time tracker in Emacs with a nice interface Chronometrist is a friendly and powerful personal time tracker and analyzer for Emacs.
Largely modelled after the Android application, [[https://github.com/netmackan/ATimeTracker][A Time Tracker]] #+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).
[[file:doc/2022-02-20 13-26-53.png]]
* Benefits * Benefits
:PROPERTIES: :PROPERTIES:
@ -67,8 +72,6 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
: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
@ -190,7 +193,7 @@ Evaluate or add to your init.el the following -
: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)
@ -200,8 +203,6 @@ Evaluate or add to your init.el the following -
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.
[fn:3] but not =chronometrist-before-in-functions=
** How to skip running hooks/attaching tags and key values ** How to skip running hooks/attaching tags and key values
Use =M-RET= (=chronometrist-toggle-task-no-hooks=) to clock in/out. Use =M-RET= (=chronometrist-toggle-task-no-hooks=) to clock in/out.
@ -281,8 +282,6 @@ I suggest backing up Chronometrist data on each save using the [[https://tildegi
: 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 =backup-directory-alist= to set a specific directory for the backups. 3. Optionally, configure =backup-directory-alist= to set a specific directory for the backups.
Adapted from this [[https://stackoverflow.com/questions/6916529/how-can-i-make-emacs-backup-every-time-i-save][StackOverflow answer]].
[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. [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
@ -375,6 +374,7 @@ Chronometrist is released under your choice of [[https://unlicense.org/][Unlicen
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: thanks :CUSTOM_ID: thanks
:END: :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 wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support
@ -384,8 +384,7 @@ blandest for helping me with the name
fiete and wu-lee for testing and bug reports fiete and wu-lee for testing and bug reports
* Local variables :NOEXPORT: * 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: