From a5c709253f785babe924fb6b7528464bcee2d86f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 13:05:02 +0530 Subject: [PATCH 01/21] Add CUSTOM_ID, macro idea --- TODO.org | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index dd5a4dc..81a5db5 100644 --- a/TODO.org +++ b/TODO.org @@ -966,10 +966,11 @@ Additional rules: * Customizable alerts :PROPERTIES: :CREATED: 2022-02-14T08:22:36+0530 +:CUSTOM_ID: customizable-alerts :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 whose entries take the form =(SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS)=, where +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=. @@ -978,3 +979,5 @@ Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-func 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. + +[fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. From 2eed71ad8565169a3639b79bebdad951ffc7c7ff Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 20:47:12 +0530 Subject: [PATCH 02/21] Add 'canned key-values' idea --- TODO.org | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.org b/TODO.org index 81a5db5..b767c5d 100644 --- a/TODO.org +++ b/TODO.org @@ -860,6 +860,7 @@ It doesn't do anything not already possible in the current formats. At best, it + 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 ""= or =:deduct ("" . "")=. - 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= 🤔 + - 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 records" - records with only a timestamp :feature: :PROPERTIES: From d88619b15fd42102aeaa4b86fe4eaefd1f3cb256 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 22:13:00 +0530 Subject: [PATCH 03/21] Update TODO for alert macro --- TODO.org | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index b767c5d..5856815 100644 --- a/TODO.org +++ b/TODO.org @@ -964,10 +964,11 @@ Additional rules: 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 -* Customizable alerts +* STARTED Customizable alerts [50%] :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. @@ -981,4 +982,7 @@ Similar to how they behave now, these packages will start/stop functions for all 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. From 27ebc824fa90a239aab55cbbde3030ede80fbb6f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 01:26:15 +0530 Subject: [PATCH 04/21] Correct :nexport: tag --- elisp/chronometrist-key-values.org | 2 +- elisp/chronometrist-spark.org | 2 +- elisp/chronometrist-third.org | 2 +- elisp/chronometrist.org | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/elisp/chronometrist-key-values.org b/elisp/chronometrist-key-values.org index 37a4767..1d11d78 100644 --- a/elisp/chronometrist-key-values.org +++ b/elisp/chronometrist-key-values.org @@ -741,7 +741,7 @@ Return t, to permit use in `chronometrist-before-out-functions'." ;;; chronometrist-key-values.el ends here #+END_SRC -* Local variables :NOEXPORT: +* Local variables :noexport: # Local Variables: # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # End: diff --git a/elisp/chronometrist-spark.org b/elisp/chronometrist-spark.org index cb645cf..c06ff16 100644 --- a/elisp/chronometrist-spark.org +++ b/elisp/chronometrist-spark.org @@ -171,7 +171,7 @@ SCHEMA should be a vector as specified by `tabulated-list-format'." ;;; chronometrist-spark.el ends here #+END_SRC -* Local variables :NOEXPORT: +* Local variables :noexport: # Local Variables: # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # End: diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 6841004..4c40bb0 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -206,7 +206,7 @@ break time is up." ;;; chronometrist-third.el ends here #+END_SRC -* Local variables :NOEXPORT: +* 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))) diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 176cc47..032fef1 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -2851,7 +2851,7 @@ Return value is either a list in the form collect plist)) #+END_SRC -***** TODO active-days :reader:method:NOEXPORT: +***** TODO active-days :reader:method:noexport: #+BEGIN_SRC emacs-lisp :tangle no (cl-defmethod chronometrist-active-days ((backend chronometrist-plist-group-backend) task &key start end) (cl-check-type task string) @@ -2876,7 +2876,7 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method t)) #+END_SRC -***** count-records :reader:method:NOEXPORT: +***** count-records :reader:method:noexport: #+BEGIN_SRC emacs-lisp :tangle no (cl-defmethod chronometrist-count-records ((backend chronometrist-plist-group-backend))) #+END_SRC @@ -4763,7 +4763,7 @@ Return value is a list as specified by `tabulated-list-entries'." ;;; chronometrist.el ends here #+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). #+BEGIN_SRC emacs-lisp :load no :tangle no From 456fe451abea3e5a53f5470b1bfca34dd45f4dec Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 12:32:07 +0530 Subject: [PATCH 05/21] Add heading tags --- TODO.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 5856815..cefa47e 100644 --- a/TODO.org +++ b/TODO.org @@ -964,7 +964,7 @@ Additional rules: 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%] +* STARTED Customizable alerts [50%] :feature:code: :PROPERTIES: :CREATED: 2022-02-14T08:22:36+0530 :CUSTOM_ID: customizable-alerts From 4638b040ff3d1128b6a7db132cd255c0c45a837d Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 12:35:31 +0530 Subject: [PATCH 06/21] Use headings for new backend items --- TODO.org | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/TODO.org b/TODO.org index cefa47e..9c76ba1 100644 --- a/TODO.org +++ b/TODO.org @@ -799,12 +799,18 @@ protocol implementation progress 20. [ ] [superclass] chronometrist-timer * New backends :feature: -1. Org time tracking -2. timeclock -3. [[https://klog.jotaen.net/][klog]] -4. SQLite -5. https://github.com/projecthamster/hamster - + https://github.com/projecthamster/hamster/wiki/Our-datamodel +** Org time tracking +** timeclock +See docstring of =timeclock-log-data= + +** [[https://klog.jotaen.net/][klog]] +: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: 1. =task-time-one-day= @@ -858,7 +864,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". + 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 ""= or =:deduct ("" . "")=. - - This will also make it easier to support formats like [[https://klog.jotaen.net/][klog]], which support this feature. + - 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. From b222ecc35f4f25c3f2f51f8247f22eb2177fc6c9 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 12:48:19 +0530 Subject: [PATCH 07/21] Update section "customizable task list" and CHANGELOG --- CHANGELOG.md | 2 +- TODO.org | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0046a..50e3151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ 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). 2. New `plist-group` backend, reducing time taken in startup and after changes to the file. 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. 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. diff --git a/TODO.org b/TODO.org index 9c76ba1..43e2a8b 100644 --- a/TODO.org +++ b/TODO.org @@ -818,9 +818,10 @@ https://github.com/projecthamster/hamster 3. =events-to-durations= * Use ISO date for functions operating on dates :time:format: -* STARTED customizable task list :feature: -1. 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? +* STARTED customizable task list [33%] :feature: +1. [X] Make =chronometrist-task-list= customizable +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: Support inputs like "today", "yesterday", "5 days ago", etc. From 9cdc5e5cbbe9dd611a330d1da99b122780d1c5bf Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 18:16:58 +0530 Subject: [PATCH 08/21] Tweak task --- TODO.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 43e2a8b..a2c60aa 100644 --- a/TODO.org +++ b/TODO.org @@ -919,7 +919,7 @@ The plist backend could theoretically be extended to support it, but I'd rather :CREATED: 2022-01-08T23:32:37+0530 :END: 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: From 12e11945a21b68288020f3581dae523a98725669 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 20:47:18 +0530 Subject: [PATCH 09/21] Rename "events" -> "ht" --- elisp/chronometrist.el | 51 +++++++++++++++++++++-------------------- elisp/chronometrist.org | 47 ++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index d6e6688..1cbe6c5 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -315,8 +315,8 @@ Return value is a ts struct (see `ts.el')." (plist-put :stop stop-2)))))))) ;; split-plist:1 ends here -;; [[file:chronometrist.org::*events-update][events-update:1]] -(defun chronometrist-events-update (plist hash-table &optional replace) +;; [[file:chronometrist.org::*ht-update][ht-update:1]] +(defun chronometrist-ht-update (plist hash-table &optional replace) "Return HASH-TABLE with PLIST added as the latest interval. If REPLACE is non-nil, replace the last interval with PLIST." (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)) (puthash date it hash-table)) hash-table)) -;; events-update:1 ends here +;; ht-update:1 ends here -;; [[file:chronometrist.org::*last-date][last-date:1]] -(defun chronometrist-events-last-date (hash-table) +;; [[file:chronometrist.org::*ht-last-date][ht-last-date:1]] +(defun chronometrist-ht-last-date (hash-table) "Return an ISO-8601 date string for the latest date present in `chronometrist-events'." (--> (hash-table-keys hash-table) + (sort it #'string-lessp) (last it) - (car it))) -;; last-date:1 ends here + (cl-first it))) +;; ht-last-date:1 ends here -;; [[file:chronometrist.org::*events-last][events-last:1]] -(cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend))) +;; [[file:chronometrist.org::*ht-last][ht-last:1]] +(cl-defun chronometrist-ht-last (&optional (backend (chronometrist-active-backend))) "Return the last plist from `chronometrist-events'." (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) (last it) (car it)))) -;; events-last:1 ends here +;; ht-last:1 ends here -;; [[file:chronometrist.org::#program-data-structures-events-subset][events-subset:1]] -(defun chronometrist-events-subset (start end hash-table) +;; [[file:chronometrist.org::#program-data-structures-ht-subset][ht-subset:1]] +(defun chronometrist-ht-subset (start end hash-table) "Return a subset of HASH-TABLE. The subset will contain values between dates START and END (both inclusive). @@ -363,7 +364,7 @@ treated as though their time is 00:00:00." (puthash key value subset))) hash-table) subset)) -;; events-subset:1 ends here +;; ht-subset:1 ends here ;; [[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))) @@ -1388,7 +1389,7 @@ STREAM (which is the value of `current-buffer')." (chronometrist-backend-run-assertions backend) (with-slots (hash-table) backend (when-let* - ((latest-date (chronometrist-events-last-date hash-table)) + ((latest-date (chronometrist-ht-last-date hash-table)) (records (gethash latest-date hash-table))) (cons latest-date records)))) ;; latest-date-records:1 ends here @@ -1497,7 +1498,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file." (with-slots (hash-table) 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)))) ;; on-add:1 ends here @@ -1507,8 +1508,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file is modified." (with-slots (hash-table) backend (-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend)) - ((&plist :name old-task) (chronometrist-events-last backend))) - (setf hash-table (chronometrist-events-update new-plist hash-table t)) + ((&plist :name old-task) (chronometrist-ht-last backend))) + (setf hash-table (chronometrist-ht-update new-plist hash-table t)) (chronometrist-remove-from-task-list old-task backend) (chronometrist-add-to-task-list new-task backend)))) ;; on-modify:1 ends here @@ -1518,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 `chronometrist-plist-backend' file is deleted." (with-slots (hash-table) backend - (-let (((&plist :name old-task) (chronometrist-events-last)) - (date (chronometrist-events-last-date hash-table))) + (-let (((&plist :name old-task) (chronometrist-ht-last)) + (date (chronometrist-ht-last-date hash-table))) ;; `chronometrist-remove-from-task-list' checks the hash table to determine ;; if `chronometrist-task-list' is to be updated. Thus, the hash table must ;; not be updated until the task list is. @@ -1788,7 +1789,7 @@ Return value is either a list in the form `chronometrist-plist-group-backend' file is modified." (with-slots (hash-table) 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))) (puthash date plists hash-table) (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 `chronometrist-plist-group-backend' file is deleted." (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))) (cl-loop for plist in old-plists do (chronometrist-remove-from-task-list (plist-get plist :name) backend)) @@ -2946,7 +2947,7 @@ displayed. They must be ts structs (see `ts.el').") It simply operates on the entire hash table TABLE (see `chronometrist-to-hash-table' for table format), so ensure that TABLE is reduced to the desired range using -`chronometrist-events-subset'." +`chronometrist-ht-subset'." (cl-loop for task in (chronometrist-task-list) collect (let* ((active-days (chronometrist-statistics-count-active-days task table)) (active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode) @@ -2975,12 +2976,12 @@ reduced to the desired range using ('week (let* ((start (plist-get chronometrist-statistics--ui-state :start)) (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))) (t ;; `chronometrist-statistics--ui-state' is nil, show current week's data (let* ((start (chronometrist-previous-week-start (chronometrist-date-ts))) (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)) (chronometrist-statistics-rows-internal ht)))))) ;; rows:1 ends here diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 0d9d25c..4348bc2 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -833,9 +833,9 @@ Return value is a ts struct (see `ts.el')." :stop "2021-02-13T00:03:46+0530"))))) #+END_SRC -*** events-update :writer: +*** ht-update :writer: #+BEGIN_SRC emacs-lisp -(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. If REPLACE is non-nil, replace the last interval with PLIST." (let* ((date (->> (plist-get plist :start) @@ -848,33 +848,34 @@ If REPLACE is non-nil, replace the last interval with PLIST." hash-table)) #+END_SRC -*** last-date :reader: +*** ht-last-date :reader: #+BEGIN_SRC emacs-lisp -(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'." (--> (hash-table-keys hash-table) + (sort it #'string-lessp) (last it) - (car it))) + (cl-first it))) #+END_SRC -*** events-last :reader: +*** ht-last :reader: #+BEGIN_SRC emacs-lisp -(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'." (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) (last it) (car it)))) #+END_SRC -*** events-subset :reader: +*** ht-subset :reader: :PROPERTIES: :VALUE: hash table -:CUSTOM_ID: program-data-structures-events-subset +:CUSTOM_ID: program-data-structures-ht-subset :END: #+BEGIN_SRC emacs-lisp -(defun chronometrist-events-subset (start end hash-table) +(defun chronometrist-ht-subset (start end hash-table) "Return a subset of HASH-TABLE. The subset will contain values between dates START and END (both inclusive). @@ -2311,7 +2312,7 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl (chronometrist-backend-run-assertions backend) (with-slots (hash-table) backend (when-let* - ((latest-date (chronometrist-events-last-date hash-table)) + ((latest-date (chronometrist-ht-last-date hash-table)) (records (gethash latest-date hash-table))) (cons latest-date records)))) #+END_SRC @@ -2427,7 +2428,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file." (with-slots (hash-table) 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)))) #+END_SRC @@ -2438,8 +2439,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file is modified." (with-slots (hash-table) backend (-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend)) - ((&plist :name old-task) (chronometrist-events-last backend))) - (setf hash-table (chronometrist-events-update new-plist hash-table t)) + ((&plist :name old-task) (chronometrist-ht-last backend))) + (setf hash-table (chronometrist-ht-update new-plist hash-table t)) (chronometrist-remove-from-task-list old-task backend) (chronometrist-add-to-task-list new-task backend)))) #+END_SRC @@ -2450,8 +2451,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe "Function run when the newest plist in a `chronometrist-plist-backend' file is deleted." (with-slots (hash-table) backend - (-let (((&plist :name old-task) (chronometrist-events-last)) - (date (chronometrist-events-last-date hash-table))) + (-let (((&plist :name old-task) (chronometrist-ht-last)) + (date (chronometrist-ht-last-date hash-table))) ;; `chronometrist-remove-from-task-list' checks the hash table to determine ;; if `chronometrist-task-list' is to be updated. Thus, the hash table must ;; not be updated until the task list is. @@ -2786,7 +2787,7 @@ Return value is either a list in the form `chronometrist-plist-group-backend' file is modified." (with-slots (hash-table) 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))) (puthash date plists hash-table) (cl-loop for plist in old-plists @@ -2801,7 +2802,7 @@ Return value is either a list in the form "Function run when the newest plist-group in a `chronometrist-plist-group-backend' file is deleted." (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))) (cl-loop for plist in old-plists do (chronometrist-remove-from-task-list (plist-get plist :name) backend)) @@ -4102,7 +4103,7 @@ displayed. They must be ts structs (see `ts.el').") It simply operates on the entire hash table TABLE (see `chronometrist-to-hash-table' for table format), so ensure that TABLE is reduced to the desired range using -`chronometrist-events-subset'." +`chronometrist-ht-subset'." (cl-loop for task in (chronometrist-task-list) collect (let* ((active-days (chronometrist-statistics-count-active-days task table)) (active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode) @@ -4132,12 +4133,12 @@ reduced to the desired range using ('week (let* ((start (plist-get chronometrist-statistics--ui-state :start)) (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))) (t ;; `chronometrist-statistics--ui-state' is nil, show current week's data (let* ((start (chronometrist-previous-week-start (chronometrist-date-ts))) (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)) (chronometrist-statistics-rows-internal ht)))))) #+END_SRC @@ -4561,7 +4562,7 @@ range.") #+END_SRC **** intervals-for-range :reader: -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. +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. #+BEGIN_SRC emacs-lisp (defun chronometrist-details-intervals-for-range (range table) From 2c1274147475b552716de7cecd7a9fd46e578e46 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 21:03:25 +0530 Subject: [PATCH 10/21] Update versions for release --- CHANGELOG.md | 4 +++- elisp/chronometrist.el | 2 +- elisp/chronometrist.org | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0046a..ab0ef24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ 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). ## unreleased +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. + +## [0.10.0] - 2022-02-15 ### Changed 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. @@ -17,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. 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 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. diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 1cbe6c5..3bfcc5c 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -5,7 +5,7 @@ ;; Keywords: calendar ;; Homepage: https://tildegit.org/contrapunctus/chronometrist ;; 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. ;; diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 4348bc2..18b2e46 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -412,7 +412,7 @@ But I discovered that if I do that, =package-lint= says - =error: Couldn't parse ;; Keywords: calendar ;; Homepage: https://tildegit.org/contrapunctus/chronometrist ;; 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. ;; From dd8660ee736f0920c771ded8eb4d82112d42c4ee Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 17 Feb 2022 23:33:07 +0530 Subject: [PATCH 11/21] Add tasks for implementing date properties --- TODO.org | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index a2c60aa..631fb15 100644 --- a/TODO.org +++ b/TODO.org @@ -879,7 +879,7 @@ Records not used for time tracking, but to store data associated with a date or [] ...) #+END_SRC -** STARTED tagging dates with key-values :feature: +** STARTED tagging dates with key-values [0%] :feature: :PROPERTIES: :CREATED: 2022-02-10T22:59:45+0530 :CUSTOM_ID: date-key-values @@ -904,6 +904,10 @@ 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. +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: :PROPERTIES: :CREATED: 2021-12-18T15:13:35+0530 From 6b56be318d7363cffd17d90227f903ec2990078d Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 17 Feb 2022 23:33:38 +0530 Subject: [PATCH 12/21] Move tasks into main heading --- TODO.org | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/TODO.org b/TODO.org index 631fb15..b4c2596 100644 --- a/TODO.org +++ b/TODO.org @@ -947,12 +947,6 @@ Additional rules: + /Avoid taking other unearned breaks/ if possible — so try to do personal tasks during normal or big breaks, or before/after your work day. #+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= 2. [X] =chronometrist-third-break-time= 3. [X] on clock out, increment =break-time= and start timed notification @@ -964,6 +958,11 @@ 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. 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 From 08e53cfea3a6957fd7a904d9fce5f6ef93479445 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 17 Feb 2022 23:44:08 +0530 Subject: [PATCH 13/21] Add predefined key-values idea --- TODO.org | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/TODO.org b/TODO.org index b4c2596..72a37e6 100644 --- a/TODO.org +++ b/TODO.org @@ -855,6 +855,9 @@ With different checks, for different levels of speed/thoroughness - * format changes ** multiple intervals per record :feature: +:PROPERTIES: +:CUSTOM_ID: multiple-intervals-per-record +:END: #+BEGIN_SRC emacs-lisp (:name "" [] ... @@ -996,3 +999,16 @@ Also define =chronometrist--timer-alist=, which associates =symbol= with a timer 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. + +* Predefined key-values for tasks [100%] +: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" ...) + ...) +#+END_SRC From 6556ee0bded3c5458ee70eb04039711b3252c887 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 20 Feb 2022 13:33:01 +0530 Subject: [PATCH 14/21] Add screenshot --- doc/2022-02-20 13-26-53.png | Bin 0 -> 57452 bytes manual.md | 30 ++++++++++++++++++------------ manual.org | 14 ++++++++++---- 3 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 doc/2022-02-20 13-26-53.png diff --git a/doc/2022-02-20 13-26-53.png b/doc/2022-02-20 13-26-53.png new file mode 100644 index 0000000000000000000000000000000000000000..4a6098071b5f0335531e1bf90152b743ea6774d6 GIT binary patch literal 57452 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 literal 0 HcmV?d00001 diff --git a/manual.md b/manual.md index e1eb305..565a698 100644 --- a/manual.md +++ b/manual.md @@ -13,7 +13,7 @@ 1. [chronometrist](#usage-chronometrist) 2. [chronometrist-report](#usage-chronometrist-report) 3. [chronometrist-statistics](#usage-chronometrist-statistics) - 4. [chronometrist-details](#org09eaa12) + 4. [chronometrist-details](#org6ee0fb7) 5. [common commands](#usage-common-commands) 6. [Time goals/targets](#time-goals) 6. [How-to](#how-to) @@ -21,26 +21,30 @@ 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) + 5. [How to skip running hooks/attaching tags and key values](#orgfbe4680) 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) +7. [Explanation](#org25cd1c5) 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#org2585c1f) +8. [User's reference](#org5f69f60) 9. [Contributions and contact](#contributions-contact) 10. [License](#license) 11. [Thanks](#thanks) -Donate using Liberapay + + Donate using Liberapay + - + + + -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, [A Time Tracker](https://github.com/netmackan/ATimeTracker) +![img](doc/2022-02-20 13-26-53.png "Screenshot of the main Chronometrist buffer, with the enabled extensions [chronometrist-goal](#time-goals) ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks).") @@ -158,7 +162,7 @@ Run `M-x chronometrist-statistics` (or `chronometrist` with a prefix argument of Press `b` to look at past time ranges, and `f` for future ones. - + ## chronometrist-details @@ -233,7 +237,7 @@ 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. - + ## How to skip running hooks/attaching tags and key values @@ -332,7 +336,7 @@ Or use `vertico-multiform` to disable sorting for only specific commands - (chronometrist-key-values-unified-prompt (vertico-sort-function . nil))))) - + # Explanation @@ -348,7 +352,7 @@ The Org file can also be loaded directly using the [literate-elisp](https://gith `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 @@ -412,6 +416,8 @@ Chronometrist is released under your choice of [Unlicense](https://unlicense.org # Thanks +The main buffer and the report buffer are copied from the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker) + 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 diff --git a/manual.org b/manual.org index 0d8abfa..b7c2174 100644 --- a/manual.org +++ b/manual.org @@ -4,14 +4,19 @@ #+HTML_HEAD: #+BEGIN_EXPORT html -Donate using Liberapay + + Donate using Liberapay + - + + + #+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 :PROPERTIES: @@ -371,6 +376,7 @@ Chronometrist is released under your choice of [[https://unlicense.org/][Unlicen :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 From ba560d68578df308c8ba85dc4f17933a15d839f8 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 20 Feb 2022 15:48:47 +0530 Subject: [PATCH 15/21] Use manual.org as readme --- README.md | 1 - README.org | 1 + manual.md | 434 ----------------------------------------------------- manual.org | 1 - 4 files changed, 1 insertion(+), 436 deletions(-) delete mode 120000 README.md create mode 120000 README.org delete mode 100644 manual.md diff --git a/README.md b/README.md deleted file mode 120000 index 9d0493a..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -manual.md \ No newline at end of file diff --git a/README.org b/README.org new file mode 120000 index 0000000..e3c17fe --- /dev/null +++ b/README.org @@ -0,0 +1 @@ +manual.org \ No newline at end of file diff --git a/manual.md b/manual.md deleted file mode 100644 index 565a698..0000000 --- a/manual.md +++ /dev/null @@ -1,434 +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](#org6ee0fb7) - 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](#orgfbe4680) - 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](#org25cd1c5) - 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#org5f69f60) -9. [Contributions and contact](#contributions-contact) -10. [License](#license) -11. [Thanks](#thanks) - - - Donate using Liberapay - - - - - - -Chronometrist is a friendly and powerful personal time tracker and analyzer for Emacs. - -![img](doc/2022-02-20 13-26-53.png "Screenshot of the main Chronometrist buffer, with the enabled extensions [chronometrist-goal](#time-goals) ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks).") - - - - -# 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 - - - - -# Limitations - -1. No support for concurrent tasks. - - - - -# Comparisons - - - - -## 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 - - - - -## 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. - - - - -# Installation - - - - -## from MELPA - -1. Set up MELPA - - - (Chronometrist uses Semantic Versioning and the developer is accident-prone, so using MELPA Stable is suggested 😏) -2. `M-x package-install RET chronometrist RET` - - - - -## from Git - -You can get `chronometrist` from or - -`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)`. - - - - -# Usage - - - - -## 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 ` 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`) - - - - -## 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. - - - - -## 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. - - - - -## chronometrist-details - - - - -## 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. - - - - -## 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/). - - - - -# How-to - -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 - -Evaluate or add to your init.el the following - -`(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)` - - - - -## How to load the program using literate-elisp - - (add-to-list 'load-path "") - - (require 'literate-elisp) ;; or autoload, use-package, ... - (literate-elisp-load "chronometrist.org") - - - - -## How to attach tags to time intervals - -1. Add `chronometrist-tags-add` to one or more of these hooks 1 - - - (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`. - - - - -## How to attach key-values to time intervals - -1. Add `chronometrist-kv-add` to one or more of these hooks 1 - - - (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. - - - - -## How to skip running hooks/attaching tags and key values - -Use `M-RET` (`chronometrist-toggle-task-no-hooks`) to clock in/out. - - - - -## 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) - - - - -## 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) - - - - -## 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) - - - - -## 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.2 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. - - - - -## 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))))) - - - - -# Explanation - - - - -## 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. - - - - -# 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` - - - - -# Contributions and contact - -Feedback and MRs are very welcome. 🙂 - -- has a long list of tasks -- 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/)) - - - - -# 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)). - - - - -# Thanks - -The main buffer and the report buffer are copied from the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker) - -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 - -1 but not `chronometrist-before-in-functions` - -2 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. diff --git a/manual.org b/manual.org index b7c2174..55733ec 100644 --- a/manual.org +++ b/manual.org @@ -388,6 +388,5 @@ fiete and wu-lee for testing and bug reports * Local variables :noexport: # 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" # End: From c51f4a45609b99d694de6cfad0d99d854b8e4af4 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 20 Feb 2022 16:46:48 +0530 Subject: [PATCH 16/21] Remove MELPA Stable recommendation --- manual.org | 2 -- 1 file changed, 2 deletions(-) diff --git a/manual.org b/manual.org index 55733ec..0de28d9 100644 --- a/manual.org +++ b/manual.org @@ -72,8 +72,6 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti :END: 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= ** from Git From 3d2bc8149e06f5e713de903f5e51967bc8c2f31f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Wed, 23 Feb 2022 14:52:53 +0530 Subject: [PATCH 17/21] Format old-break duration like other durations --- elisp/chronometrist-third.el | 18 +++++++++--------- elisp/chronometrist-third.org | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 14a1062..d443dca 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -131,19 +131,19 @@ and stopped when they clock in." "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-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) - (new-break-time (- chronometrist-third-break-time used-break-duration)) - (old-break-time chronometrist-third-break-time) - (used-break-duration-string (format-seconds chronometrist-third-duration-format - used-break-duration))) - (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0)) + (-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)" - old-break-time used-break-duration-string) + (format-seconds chronometrist-third-duration-format old-break) + used-break-string) (format "You have used %s of your break time (%s left)" - used-break-duration-string + used-break-string (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) ;; clock-in:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 4c40bb0..57ec5de 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -155,19 +155,19 @@ and stopped when they clock in." "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-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) - (new-break-time (- chronometrist-third-break-time used-break-duration)) - (old-break-time chronometrist-third-break-time) - (used-break-duration-string (format-seconds chronometrist-third-duration-format - used-break-duration))) - (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0)) + (-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)" - old-break-time used-break-duration-string) + (format-seconds chronometrist-third-duration-format old-break) + used-break-string) (format "You have used %s of your break time (%s left)" - used-break-duration-string + used-break-string (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) #+END_SRC From 7334be5743f27a965bcf1a2ad06650d7fa8d907b Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Wed, 23 Feb 2022 20:47:46 +0530 Subject: [PATCH 18/21] Add key-value presets Inspired by the Android application "My Expenses" by Michael Totschnig. --- CHANGELOG.md | 3 ++ elisp/chronometrist-key-values.el | 61 +++++++++++++++++------- elisp/chronometrist-key-values.org | 76 +++++++++++++++++++++++------- 3 files changed, 105 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74730c9..4562961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ 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). ## 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 diff --git a/elisp/chronometrist-key-values.el b/elisp/chronometrist-key-values.el index 468c28e..66e10a5 100644 --- a/elisp/chronometrist-key-values.el +++ b/elisp/chronometrist-key-values.el @@ -192,6 +192,29 @@ used in `chronometrist-before-out-functions'." "Add key-values to Chronometrist time intervals." :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\" ...)" + :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*" "Name of buffer in which key-values are entered." :group 'chronometrist-key-values @@ -404,30 +427,34 @@ used in `chronometrist-before-out-functions'." ["Change tags and key-values for active/last interval" 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. Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) + (presets (chronometrist-key-value-get-presets task)) (key-values - (cl-loop for plist in (chronometrist-to-list backend) - when (equal (plist-get plist :name) task) - collect - (let ((plist (chronometrist-plist-remove plist :name :start :stop))) - (when plist (format "%S" plist))) - into key-value-plists - finally return - (--> (seq-filter #'identity key-value-plists) - (cl-remove-duplicates it :test #'equal :from-end t)))) + (when chronometrist-key-value-use-database-history + (cl-loop for plist in (chronometrist-to-list backend) + when (equal (plist-get plist :name) task) + collect + (let ((plist (chronometrist-plist-remove plist :name :start :stop))) + (when plist (format "%S" plist))) + into key-value-plists + finally return + (--> (seq-filter #'identity key-value-plists) + (cl-remove-duplicates it :test #'equal :from-end t))))) (latest (chronometrist-latest-record backend))) - (if (null key-values) + (if (and (null presets) (null key-values)) (progn (chronometrist-tags-add) (chronometrist-kv-add)) - (chronometrist-replace-last - backend - (chronometrist-plist-update - latest - (read (completing-read (format "Key-values for %s: " task) - key-values nil nil nil 'chronometrist-key-values-unified-prompt-history)))))) + (let* ((candidates (append presets key-values)) + (input (completing-read + (format "Key-values for %s: " task) + candidates nil nil nil 'chronometrist-key-values-unified-prompt-history))) + (chronometrist-replace-last backend + (chronometrist-plist-update latest + (read input)))))) t) (provide 'chronometrist-key-values) diff --git a/elisp/chronometrist-key-values.org b/elisp/chronometrist-key-values.org index 1d11d78..389b02f 100644 --- a/elisp/chronometrist-key-values.org +++ b/elisp/chronometrist-key-values.org @@ -330,6 +330,39 @@ used in `chronometrist-before-out-functions'." "Add key-values to Chronometrist time intervals." :group 'chronometrist) #+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\" ...)" + :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: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*" @@ -337,6 +370,7 @@ used in `chronometrist-before-out-functions'." :group 'chronometrist-key-values :type 'string) #+END_SRC + *** key-history :variable: :PROPERTIES: :VALUE: hash table @@ -707,34 +741,39 @@ Return t, to permit use in `chronometrist-before-out-functions'." :CUSTOM_ID: unified-prompt :END: 1. [ ] Improve appearance - is there an easy way to syntax highlight the plists? + #+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. Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) + (presets (chronometrist-key-value-get-presets task)) (key-values - (cl-loop for plist in (chronometrist-to-list backend) - when (equal (plist-get plist :name) task) - collect - (let ((plist (chronometrist-plist-remove plist :name :start :stop))) - (when plist (format "%S" plist))) - into key-value-plists - finally return - (--> (seq-filter #'identity key-value-plists) - (cl-remove-duplicates it :test #'equal :from-end t)))) + (when chronometrist-key-value-use-database-history + (cl-loop for plist in (chronometrist-to-list backend) + when (equal (plist-get plist :name) task) + collect + (let ((plist (chronometrist-plist-remove plist :name :start :stop))) + (when plist (format "%S" plist))) + into key-value-plists + finally return + (--> (seq-filter #'identity key-value-plists) + (cl-remove-duplicates it :test #'equal :from-end t))))) (latest (chronometrist-latest-record backend))) - (if (null key-values) + (if (and (null presets) (null key-values)) (progn (chronometrist-tags-add) (chronometrist-kv-add)) - (chronometrist-replace-last - backend - (chronometrist-plist-update - latest - (read (completing-read (format "Key-values for %s: " task) - key-values nil nil nil 'chronometrist-key-values-unified-prompt-history)))))) + (let* ((candidates (append presets key-values)) + (input (completing-read + (format "Key-values for %s: " task) + candidates nil nil nil 'chronometrist-key-values-unified-prompt-history))) + (chronometrist-replace-last backend + (chronometrist-plist-update latest + (read input)))))) t) - #+END_SRC + * Provide #+BEGIN_SRC emacs-lisp (provide 'chronometrist-key-values) @@ -743,5 +782,6 @@ Return t, to permit use in `chronometrist-before-out-functions'." * 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: From 9ef3337c5a68caf73866a6949bb5783d7e246979 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 24 Feb 2022 15:47:06 +0530 Subject: [PATCH 19/21] Tweak default duration format --- elisp/chronometrist-third.el | 2 +- elisp/chronometrist-third.org | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index d443dca..0995691 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -42,7 +42,7 @@ ;; divisor:1 ends here ;; [[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'." :type 'string) ;; duration-format:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 57ec5de..9eb49ef 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -56,7 +56,7 @@ ** duration-format :custom:variable: #+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'." :type 'string) #+END_SRC From 3962c63f9314ebe7b4d7a1ba048d33d4925ba781 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 25 Feb 2022 00:27:50 +0530 Subject: [PATCH 20/21] Mark task done; document chronometrist-goal bug --- TODO.org | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 72a37e6..928ce00 100644 --- a/TODO.org +++ b/TODO.org @@ -1000,7 +1000,7 @@ Also define =chronometrist--timer-alist=, which associates =symbol= with a timer [fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. -* Predefined key-values for tasks [100%] +* DONE Predefined key-values for tasks :PROPERTIES: :CREATED: 2022-02-17T23:34:17+0530 :END: @@ -1012,3 +1012,11 @@ Benefits '(("Task" ...) ...) #+END_SRC + +* no-goal-alert - "You have spent on " [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. From 7cf2c86afd8f6fb6235320ac9f7ebd76153d8bc6 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 25 Feb 2022 15:20:21 +0530 Subject: [PATCH 21/21] Convert user presets to strings --- elisp/chronometrist-key-values.el | 3 ++- elisp/chronometrist-key-values.org | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/elisp/chronometrist-key-values.el b/elisp/chronometrist-key-values.el index 66e10a5..339fdd5 100644 --- a/elisp/chronometrist-key-values.el +++ b/elisp/chronometrist-key-values.el @@ -433,7 +433,8 @@ used in `chronometrist-before-out-functions'." Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) - (presets (chronometrist-key-value-get-presets task)) + (presets (--map (format "%S" it) + (chronometrist-key-value-get-presets task))) (key-values (when chronometrist-key-value-use-database-history (cl-loop for plist in (chronometrist-to-list backend) diff --git a/elisp/chronometrist-key-values.org b/elisp/chronometrist-key-values.org index 389b02f..3a1d891 100644 --- a/elisp/chronometrist-key-values.org +++ b/elisp/chronometrist-key-values.org @@ -749,7 +749,8 @@ Return t, to permit use in `chronometrist-before-out-functions'." Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) - (presets (chronometrist-key-value-get-presets task)) + (presets (--map (format "%S" it) + (chronometrist-key-value-get-presets task))) (key-values (when chronometrist-key-value-use-database-history (cl-loop for plist in (chronometrist-to-list backend)