diff --git a/.dir-locals.el b/.dir-locals.el index 33ca69b..f3c9e13 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -14,7 +14,8 @@ ("cr" . "chronometrist-report") ("cs" . "chronometrist-statistics") ("cx" . "chronometrist-sexp") - ("c" . "chronometrist"))))) + ("c" . "chronometrist"))) + (sentence-end-double-space . t))) (org-mode . ((org-html-self-link-headlines . t) (eval . (org-indent-mode)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecd321..4562961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). ## 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 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. @@ -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). 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`. ### 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 ### Added diff --git a/Makefile b/Makefile index 9fa94c2..133695b 100644 --- a/Makefile +++ b/Makefile @@ -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: emacs --batch --eval="(package-initialize)" \ @@ -14,74 +14,69 @@ manual.md: # Org, we want to use it. tangle: cd elisp/ && \ - emacs --batch \ - --eval="(package-initialize)" \ - --eval="(require 'ob-tangle)" \ - --eval='(org-babel-tangle-file "chronometrist.org")' \ - --eval='(org-babel-tangle-file "chronometrist-key-values.org")' \ - --eval='(org-babel-tangle-file "chronometrist-spark.org")' \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'ob-tangle))" \ + --eval='(org-babel-tangle-file "chronometrist.org")' \ + --eval='(org-babel-tangle-file "chronometrist-key-values.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")' ; \ - cd .. ; \ + cd .. compile: tangle cd elisp/ && \ - emacs -q -Q --batch \ - --eval="(progn (package-initialize) (require 'dash) (require 'ts))" \ - --eval='(byte-compile-file "chronometrist.el")' \ - --eval='(byte-compile-file "chronometrist-key-values.el")' \ - --eval='(byte-compile-file "chronometrist-spark.el")' \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'dash) (require 'ts))" \ + --eval='(byte-compile-file "chronometrist.el")' \ + --eval='(byte-compile-file "chronometrist-key-values.el")' \ + --eval='(byte-compile-file "chronometrist-spark.el")' \ + --eval='(byte-compile-file "chronometrist-third.el")' \ --eval='(byte-compile-file "chronometrist-sqlite.el")' ; \ - cd .. + cd .. lint-check-declare: tangle cd elisp/ && \ - emacs -q -Q --batch \ - --eval='(check-declare-file "chronometrist.el")' \ - --eval='(check-declare-file "chronometrist-key-values.el")' \ - --eval='(check-declare-file "chronometrist-spark.el")' \ + emacs -q --batch \ + --eval='(check-declare-file "chronometrist.el")' \ + --eval='(check-declare-file "chronometrist-key-values.el")' \ + --eval='(check-declare-file "chronometrist-spark.el")' \ + --eval='(check-declare-file "chronometrist-third.el")' \ --eval='(check-declare-file "chronometrist-sqlite.el")' ; \ - cd .. + cd .. lint-checkdoc: tangle cd elisp/ && \ emacs -q -Q --batch \ - --eval='(checkdoc-file "chronometrist.el")' \ - --eval='(checkdoc-file "chronometrist-key-values.el")' \ - --eval='(checkdoc-file "chronometrist-spark.el")' \ + --eval='(checkdoc-file "chronometrist.el")' \ + --eval='(checkdoc-file "chronometrist-key-values.el")' \ + --eval='(checkdoc-file "chronometrist-spark.el")' \ + --eval='(checkdoc-file "chronometrist-third.el")' \ --eval='(checkdoc-file "chronometrist-sqlite.el")' ; \ - cd .. + cd .. lint-package-lint: setup tangle cd elisp/ && \ - emacs -q -Q --batch \ - --eval="(progn (package-initialize) (require 'dash) (require 'ts))" \ - --eval="(require 'package-lint)" \ - -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-spark.el \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'dash) (require 'ts) (require 'package-lint))" \ + -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-spark.el \ + -f 'package-lint-batch-and-exit' chronometrist-third.el \ -f 'package-lint-batch-and-exit' chronometrist-sqlite.el ; \ - cd .. + cd .. lint-relint: setup tangle cd elisp/ && \ - emacs -q -Q --batch \ - --eval="(package-initialize)" \ - --eval="(require 'relint)" \ - --eval='(relint-file "chronometrist.el")' \ - --eval='(relint-file "chronometrist-key-values.el")' \ - --eval='(relint-file "chronometrist-spark.el")' \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'relint))" \ + --eval='(relint-file "chronometrist.el")' \ + --eval='(relint-file "chronometrist-key-values.el")' \ + --eval='(relint-file "chronometrist-spark.el")' \ + --eval='(relint-file "chronometrist-third.el")' \ --eval='(relint-file "chronometrist-sqlite.el")' ; \ - cd .. + cd .. lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint -clean-tangle: - rm elisp/chronometrist.el \ - elisp/chronometrist-key-values.el \ - elisp/chronometrist-spark.el \ - elisp/chronometrist-sqlite.el ; - clean-elc: rm elisp/*.elc - -clean: clean-elc 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/TODO.org b/TODO.org index 7384fe0..29969e5 100644 --- a/TODO.org +++ b/TODO.org @@ -737,7 +737,9 @@ for )" #+END_SRC * 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. 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 @@ -797,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= @@ -810,9 +818,10 @@ protocol implementation progress 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. @@ -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: * verify command [20%] :feature: +:PROPERTIES: +:CUSTOM_ID: verify-command +:END: With different checks, for different levels of speed/thoroughness - 1. [X] (plist group) Sequence of plist group dates 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 ** multiple intervals per record :feature: +:PROPERTIES: +:CUSTOM_ID: multiple-intervals-per-record +:END: #+BEGIN_SRC emacs-lisp (:name "" [] ... @@ -855,8 +870,12 @@ With different checks, for different levels of speed/thoroughness - #+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". ++ 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 [[#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: :CREATED: 2021-12-18T11:48:53+0530 :END: @@ -866,6 +885,35 @@ Records not used for time tracking, but to store data associated with a date or [] ...) #+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 +("" + [ ]* + +) +#+END_SRC + +Alternatively, this would be easier to work with - easier to select the key-values or the plists, as required - +#+BEGIN_SRC elisp +("" + (key-values + [ ]+) + +) +#+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 )=. + +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 @@ -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 :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: +: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 you’ve 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 you’ll resume work. A big break uses up any saved break minutes, so you can’t carry them over to the afternoon/evening. ++ /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 + +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" ...) + ...) +#+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. + +* 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: 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 0000000..4a60980 Binary files /dev/null and b/doc/2022-02-20 13-26-53.png differ diff --git a/elisp/chronometrist-key-values.el b/elisp/chronometrist-key-values.el index 468c28e..339fdd5 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,35 @@ 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 (--map (format "%S" it) + (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 37a4767..3a1d891 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,41 +741,48 @@ 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 (--map (format "%S" it) + (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) ;;; chronometrist-key-values.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))) # 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.el b/elisp/chronometrist-third.el new file mode 100644 index 0000000..0995691 --- /dev/null +++ b/elisp/chronometrist-third.el @@ -0,0 +1,178 @@ +;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*- + +;; Author: contrapunctus +;; Maintainer: contrapunctus +;; 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 + +;;; 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 diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org new file mode 100644 index 0000000..9eb49ef --- /dev/null +++ b/elisp/chronometrist-third.org @@ -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 +;; Maintainer: contrapunctus +;; 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 + +;;; 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: diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index c32f324..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. ;; @@ -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))) @@ -371,7 +372,7 @@ treated as though their time is 00:00:00." The return value is seconds, as an integer." (let ((task-events (chronometrist-task-records-for-date backend task date))) (if task-events - (->> (chronometrist-events-to-durations task-events) + (->> (chronometrist-plists-to-durations task-events) (-reduce #'+) (truncate)) ;; 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)))) ;; iso-to-ts:1 ends here -;; [[file:chronometrist.org::*events-to-durations][events-to-durations:1]] -(defun chronometrist-events-to-durations (events) - "Convert EVENTS into a list of durations in seconds. -EVENTS must be a list of valid Chronometrist property lists (see -`chronometrist-file'). - -Return 0 if EVENTS is nil." - (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::*plists-to-durations][plists-to-durations:1]] +(defun chronometrist-plists-to-durations (plists) + "Convert PLISTS into a list of durations in seconds. +PLISTS must be a list of valid Chronometrist property lists (see +`chronometrist-file'). Return 0 if PLISTS is nil." + (if plists (mapcar #'chronometrist-interval plists) 0)) +;; plists-to-durations:1 ends here ;; [[file:chronometrist.org::*date-iso][date-iso:1]] (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 ;; [[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. EVENT should be a plist (see `chronometrist-file')." - (let ((start (plist-get event :start)) - (stop (plist-get event :stop))) - (time-subtract (parse-iso8601-time-string stop) - (parse-iso8601-time-string start)))) + (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))) ;; interval:1 ends here ;; [[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]] (cl-defgeneric chronometrist-latest-record (backend) - "Return the latest entry 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 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. + +If the latest record starts on one day and ends on another, the +entire (unsplit) record must be returned.") ;; latest-record:1 ends here ;; [[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]] (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 (setf path new-path file (concat path "." extension)))) @@ -1394,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 @@ -1503,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 @@ -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." (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 @@ -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 `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. @@ -1658,7 +1653,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe t)))) ;; 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) "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 @@ -1708,8 +1703,14 @@ Return value is either a list in the form (new-plist-wo-time (chronometrist-plist-remove new-plist :start :stop))) (cond ((not (and old-plist new-plist)) nil) ((equal old-plist-wo-time new-plist-wo-time) - (let ((plist (cl-copy-list old-plist))) - (plist-put plist :stop (plist-get new-plist :stop)))) + (let* ((plist (cl-copy-list old-plist)) + (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"))))) ;; 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." (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)) @@ -1831,7 +1832,10 @@ Return value is either a list in the form ;; [[file:chronometrist.org::*latest-record][latest-record:1]] (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 ;; [[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))) (chronometrist-sexp-in-file (chronometrist-backend-file backend) (chronometrist-remove-last backend :save nil) + (delete-trailing-whitespace) (chronometrist-insert backend plist :save nil) (save-buffer) t)) @@ -2306,7 +2311,7 @@ task. N must be a positive integer." (chronometrist-task-at-point))) ;; 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) "Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'. 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.")))) ;; discard-active:1 ends here -;; [[file:chronometrist.org::*chronometrist][chronometrist:1]] +;; [[file:chronometrist.org::#program-frontend-chronometrist-command][chronometrist:1]] ;;;###autoload (defun chronometrist (&optional arg) "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)) ;; report-mode:1 ends here -;; [[file:chronometrist.org::*chronometrist-report][chronometrist-report:1]] +;; [[file:chronometrist.org::#program-frontend-report-command][chronometrist-report:1]] ;;;###autoload (defun chronometrist-report (&optional keep-date) "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)) do (cl-incf days) and collect - (-reduce #'+ (chronometrist-events-to-durations events-in-day)) + (-reduce #'+ (chronometrist-plists-to-durations events-in-day)) into per-day-time-list finally return (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 `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) @@ -2971,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 @@ -3066,7 +3071,7 @@ value of `revert-buffer-function'." (chronometrist-setup-file-watch)) ;; statistics-mode:1 ends here -;; [[file:chronometrist.org::*chronometrist-statistics][chronometrist-statistics:1]] +;; [[file:chronometrist.org::#program-frontend-statistics-command][chronometrist-statistics:1]] ;;;###autoload (defun chronometrist-statistics (&optional preserve-state) "Display statistics for Chronometrist data. @@ -3359,10 +3364,13 @@ TABLE must be a hash table as returned by collect plist) do (ts-adjustf begin-ts 'day 1))))))) -;; (chronometrist-details-intervals-for-range nil chronometrist-events) -;; (chronometrist-details-intervals-for-range "2021-06-01" chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") chronometrist-events) +;; (chronometrist-details-intervals-for-range nil (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range "2021-06-01" +;; (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (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 ;; [[file:chronometrist.org::*input-to-value][input-to-value:1]] diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 3ccc399..11dfd5c 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -48,7 +48,7 @@ Quite recently, after around two years of Chronometrist developement and use, I :PROPERTIES: :DESCRIPTION: Explanation of some terms used later :END: -For lack of a better term, events are how we refer to time intervals. They are stored as plists; each contains at least a =:name ""=, a =:start ""=, and (except in case of an ongoing task) a =:stop ""=. +Chronometrist records /time intervals/ (earlier called "events") as plists. Each plist contains at least a =:name ""=, a =:start ""=, and (except in case of an ongoing task) a =:stop ""=. + row :: a row of a table in a =tabulated-list-mode= buffer; an element of =tabulated-list-entries=. + schema :: the column descriptor of a table in a =tabulated-list-mode= buffer; the value of =tabulated-list-format=. @@ -56,9 +56,14 @@ For lack of a better term, events are how we refer to time intervals. They are s See also [[#explanation-time-formats][Currently-Used Time Formats]] ** Overview -At its most basic, we read data from a [[#program-backend][plain text file]] containing Lisp plists, store it in a [[#program-data-structures][hash table]], and [[#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list-mode")][=tabulated-list-mode=]] buffer. When the file is changed—whether by the program or the user—we [[refresh-file][update the hash table]] and the buffer. +At its most basic, we read data from a [[#program-backend][backend]], store it in a [[#program-data-structures][hash table]], and [[#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list-mode")][=tabulated-list-mode=]] buffer. When the file is changed—whether by the program or the user—we [[refresh-file][update the hash table]] and the [[#program-frontend-chronometrist-refresh][buffer]]. -In addition, we implement a [[#program-pretty-printer][plist pretty-printer]] and some migration commands. This repository also contains an extension for [[file:chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals, and there is a extension for [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]] in a separate repository. +In addition, we implement a [[#program-pretty-printer][plist pretty-printer]] and some [[#program-migration][migration commands]]. + +Extensions exist for - +1. [[file:chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals, and +2. support for the [[file:chronometrist-third.org][Third Time system]] +3. [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]] ** Optimization It is of great importance that Chronometrist be responsive - @@ -113,31 +118,38 @@ Effects on the task list :DESCRIPTION: Events starting on one day and ending on another :CUSTOM_ID: explanation-midnight-spanning-intervals :END: -A unique problem in working with Chronometrist, one I had never foreseen, was tasks which start on one day and end on another. For instance, you start working on something at 2021-01-01T23:00 hours and stop on 2021-01-02T01:00. +A unique problem in working with Chronometrist, one I had never foreseen, was tasks which start on one day and end on another. For instance, you start working on something at =2021-01-01 23:00= hours and stop on =2021-01-02 01:00=. These mess up data consumption in all sorts of unforeseen ways, especially interval calculations and acquiring intervals for a specific date. In case of two of the most common operations throughout the program - -1. finding the intervals recorded on a given date - +1. finding the intervals recorded on a given date 2. finding the time spent on a task on a given day - if the day's intervals used for this contain a midnight-spanning interval, you'll have inaccurate results - it will include yesterday's time from the interval as well as today's. There are a few different approaches of dealing with them. (Currently, Chronometrist uses #3.) + *** Check the code of the first event of the day (timeclock format) :PROPERTIES: :DESCRIPTION: When the code of the first event in the day is "o", it's a midnight-spanning event. :END: + Advantage - very simple to detect + Disadvantage - "in" and "out" events must be represented separately + *** Split them at the file level + Advantage - operation is performed only once for each such event + simpler data-consuming code + reduced post-parsing load. + What happens when the user changes their day-start-time? The split-up events are now split wrongly, and the second event may get split /again./ Possible solutions - -1. Add function to check if, for two events A and B, the :stop of A is the same as the :start of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time. -2. Add a :split tag to split events. It can denote that the next event was originally a part of this one. +1. Add function to check if, for two events A and B, the =:stop= of A is the same as the =:start= of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time. + + Implemented as [[#program-data-structures-plists-split-p][plist-split-p]] +2. Add a =:split= tag to split events. It can denote that the next event was originally a part of this one. 3. Re-check and update the file when the day-start-time changes. - Possible with ~add-variable-watcher~ or ~:custom-set~ in Customize (thanks bpalmer) + +This strategy is implemented in the [[#program-backend-plist-group][plist-group]] backend. Custom day-start time is not yet implemented - if ever implemented, it will probably require exporting the file, with all split intervals being combined and re-split. + *** Split them at the hash-table-level Handled by ~chronometrist-sexp-events-populate~ + Advantage - simpler data-consuming code. + *** Split them at the data-consumer level (e.g. when calculating time for one day/getting events for one day) + Advantage - reduced repetitive post-parsing load. @@ -146,7 +158,7 @@ Handled by ~chronometrist-sexp-events-populate~ :DESCRIPTION: The desired behaviour of point in Chronometrist :END: After hacking, always test for and ensure the following - -1. Toggling the buffer via =chronometrist= / =chronometrist-report= / =chronometrist-statistics= should preserve point +1. Toggling the buffer via [[#program-frontend-chronometrist-command][chronometrist]]/[[#program-frontend-report-command][chronometrist-report]]/[[#program-frontend-statistics-command][chronometrist-statistics]] should preserve point 2. The timer function should preserve point when the buffer is current 3. The timer function should preserve point when the buffer is not current, but is visible in another window 4. The next/previous week keys and buttons should preserve point. @@ -155,8 +167,8 @@ After hacking, always test for and ensure the following - :PROPERTIES: :DESCRIPTION: Deriving dates in the current week :END: -A quick description, starting from the first time [[* chronometrist-report][=chronometrist-report=]] is run in an Emacs session - -1. We get the current date as a ts struct, using =chronometrist-date=. +A quick description, starting from the first time [[#program-frontend-report-command][chronometrist-report]] is run in an Emacs session - +1. We get the current date as a =ts= struct, using chronometrist-date. 2. The variable =chronometrist-report-week-start-day= stores the day we consider the week to start with. The default is "Sunday". We check if the date from #2 is on the week start day, else decrement it till we are, using =(chronometrist-report-previous-week-start)=. @@ -171,6 +183,7 @@ A quick description, starting from the first time [[* chronometrist-report][=chr :CUSTOM_ID: explanation-literate-programming :END: The shift from a bunch of Elisp files to a single Org literate program was born out of frustration with programs stored as text files, which are expensive to restructure (especially in the presence of a VCS). While some dissatisfactions remain, I generally prefer the outcome - tree and source-block folding, tags, properties, and =org-match= have made it trivial to get different views of the program, and literate programming may allow me to express the "explanation" documentation in the same context as the program, without having to try to link between documentation and source. + *** Tangling At first, I tried tangling. Back when I used =benchmark.el= to test it, =org-babel-tangle= took about 30 seconds to tangle this file. Thus, I wrote a little sed one-liner (in the file-local variables) to do the tangling, which was nearly instant. It emitted anything between lines matching the exact strings ="#+BEGIN_SRC emacs-lisp"= and ="#+END_SRC"= - @@ -220,6 +233,7 @@ Further details are stored in properties - 2. :VALUE: list|hash table|... * for functions, this is the return value 3. :STATE: + *** TODO Issues [40%] 1. [X] When opening this file, Emacs may freeze at the prompt for file-local variable values; if so, C-g will quit the prompt, and permanently marking them as safe will make the freezing stop. [fn:3] 2. [ ] I like =visual-fill-column-mode= for natural language, but I don't want it applied to code blocks. =polymode.el= may hold answers. @@ -230,6 +244,7 @@ Further details are stored in properties - * A workaround is to press =M-o M-o= [fn:3] No longer a problem since we switched to =literate-elisp= + ** Currently-Used Time Formats :PROPERTIES: :CUSTOM_ID: explanation-time-formats @@ -238,22 +253,27 @@ Further details are stored in properties - *** ts ts.el struct * Used by nearly all internal functions + *** iso-timestamp ="YYYY-MM-DDTHH:MM:SSZ"= * Used in the s-expression file format * Read by chronometrist-sexp-events-populate * Used in the plists in the chronometrist-events hash table values + *** iso-date ="YYYY-MM-DD"= * Used as hash table keys in chronometrist-events - can't use ts structs for keys, you'd have to make a hash table predicate which uses ts= + *** seconds integer seconds as duration * Used for most durations * May be changed to floating point to allow larger durations. The minimum range of =most-positive-fixnum= is 536870911, which seems to be enough to represent durations of 17 years. * Used for update intervals (chronometrist-update-interval, chronometrist-change-update-interval) + *** minutes integer minutes as duration * Used by [[https://tildegit.org/contrapunctus/chronometrist-goal][chronometrist-goal]] (chronometrist-goals-list, chronometrist-get-goal) - minutes seems like the ideal unit for users to enter + *** list-duration =(hours minute seconds)= * Only returned by =chronometrist-seconds-to-hms=, called by =chronometrist-format-time= @@ -392,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. ;; @@ -491,6 +511,7 @@ supplied, 3 spaces are used." (format "%02d" s)))) (concat h m s))))) #+END_SRC + *** file-empty-p :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-file-empty-p (file) @@ -512,6 +533,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found." (-interpose ", ") (apply #'concat)))) #+END_SRC + *** day-start-time :custom:variable: =chronometrist-events-maybe-split= refers to this, but I'm not sure this has the desired effect at the moment—haven't even tried using it. #+BEGIN_SRC emacs-lisp @@ -521,12 +543,14 @@ If FIRSTONLY is non-nil, return only the first keybinding found." The default is midnight, i.e. \"00:00:00\"." :type 'string) #+END_SRC + *** week-start-day :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-report-week-start-day "Sunday" "The day used for start of week by `chronometrist-report'." :type 'string) #+END_SRC + *** weekday-number-alist :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-report-weekday-number-alist @@ -540,6 +564,7 @@ The default is midnight, i.e. \"00:00:00\"." "Alist in the form (\"NAME\" . NUMBER), where \"NAME\" is the name of a weekday and NUMBER its associated number." :type 'alist) #+END_SRC + *** previous-week-start :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-previous-week-start (ts) @@ -557,6 +582,7 @@ TS must be a ts struct (see `ts.el')." do (ts-decf (ts-day ts)) finally return ts)) #+END_SRC + *** plist-remove :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-plist-remove (plist &rest keys) @@ -727,6 +753,7 @@ Reading directly from the file could be difficult, especially when your most com The data from =chronometrist-events= is used by most (all?) interval-consuming functions, but is never written to the user's file itself. [fn:4] it might be the case that the [[#program-backend][file format]] is not suited to our most frequent operation... + *** reset :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-reset () @@ -750,6 +777,7 @@ Return value is a ts struct (see `ts.el')." (ts-apply :hour h :minute m :second s (chronometrist-iso-to-ts timestamp)))) #+END_SRC + **** tests #+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test (ert-deftest chronometrist-apply-time () @@ -805,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) @@ -820,32 +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). @@ -869,7 +899,7 @@ treated as though their time is 00:00:00." The return value is seconds, as an integer." (let ((task-events (chronometrist-task-records-for-date backend task date))) (if task-events - (->> (chronometrist-events-to-durations task-events) + (->> (chronometrist-plists-to-durations task-events) (-reduce #'+) (truncate)) ;; no events for this task on DATE, i.e. no time spent @@ -933,6 +963,7 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by :dow dow :tz-offset utcoff)))) #+END_SRC + **** tests #+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test (ert-deftest chronometrist-iso-to-ts () @@ -944,26 +975,16 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by :hour 1 :minute 1 :second 1)))) #+END_SRC -*** events-to-durations :function: -#+BEGIN_SRC emacs-lisp -(defun chronometrist-events-to-durations (events) - "Convert EVENTS into a list of durations in seconds. -EVENTS must be a list of valid Chronometrist property lists (see -`chronometrist-file'). -Return 0 if EVENTS is nil." - (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)) +*** plists-to-durations :function: +#+BEGIN_SRC emacs-lisp +(defun chronometrist-plists-to-durations (plists) + "Convert PLISTS into a list of durations in seconds. +PLISTS must be a list of valid Chronometrist property lists (see +`chronometrist-file'). Return 0 if PLISTS is nil." + (if plists (mapcar #'chronometrist-interval plists) 0)) #+END_SRC + *** date-iso :function: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-date-iso (&optional (ts (ts-now))) @@ -990,6 +1011,7 @@ Optional argument UNIX-TIME should be a time value (see ;; Note - this assumes that an event never crosses >1 day. This seems ;; sufficient for all conceivable cases. #+END_SRC + *** FIXME split-time :reader: It does not matter here that the =:stop= dates in the returned plists are different from the =:start=, because =chronometrist-events-populate= uses only the date segment of the =:start= values as hash table keys. (The hash table keys form the rest of the program's notion of "days", and that of which plists belong to which day.) @@ -1059,16 +1081,19 @@ SECONDS must be a positive integer." (h (/ seconds 3600))) (list h m s))) #+END_SRC + *** interval :function: #+BEGIN_SRC emacs-lisp -(defun chronometrist-interval (event) +(defun chronometrist-interval (plist) "Return the period of time covered by EVENT as a time value. EVENT should be a plist (see `chronometrist-file')." - (let ((start (plist-get event :start)) - (stop (plist-get event :stop))) - (time-subtract (parse-iso8601-time-string stop) - (parse-iso8601-time-string start)))) + (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))) #+END_SRC + *** format-duration-long :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-format-duration-long (seconds) @@ -1088,6 +1113,7 @@ SECONDS is less than 60, return a blank string." hours hour-string minutes minute-string))))) #+END_SRC + **** tests #+BEGIN_SRC emacs-lisp :tangle ../tests/chronometrist-tests.el :load test (ert-deftest chronometrist-format-duration-long () @@ -1103,6 +1129,7 @@ SECONDS is less than 60, return a blank string." (should (equal (chronometrist-format-duration-long 7260) "2 hours, 1 minute")) (should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes"))) #+END_SRC + ** Plist pretty-printing :PROPERTIES: :CUSTOM_ID: program-pretty-printer @@ -1129,6 +1156,7 @@ Point is placed at the end of the space." (delete-region (match-beginning 0) (match-end 0)) (insert " "))) #+END_SRC + *** column :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-pp-column () @@ -1136,12 +1164,14 @@ Point is placed at the end of the space." 0 means point is at the beginning of the line." (- (point) (point-at-bol))) #+END_SRC + *** pair-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-pp-pair-p (cons) "Return non-nil if CONS is a pair, i.e. (CAR . CDR)." (and (listp cons) (not (listp (cdr cons))))) #+END_SRC + *** alist-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-pp-alist-p (list) @@ -1152,6 +1182,7 @@ considers it an alist." (when (listp list) (cl-loop for elt in list thereis (chronometrist-pp-pair-p elt)))) #+END_SRC + *** plist-group-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-plist-group-p (list) @@ -1174,6 +1205,7 @@ that point is after the first opening parenthesis." when (keywordp sexp) maximize (length (symbol-name sexp))))) #+END_SRC + *** indent-sexp :function: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-pp-indent-sexp (sexp &optional (right-indent 0)) @@ -1181,6 +1213,7 @@ that point is after the first opening parenthesis." (format (concat "% -" (number-to-string right-indent) "s") sexp)) #+END_SRC + *** buffer :writer: It might help to make =in-sublist= an integer representing depth, instead of a boolean. But at the moment, it's getting the job done. @@ -1312,10 +1345,12 @@ IN-SUBLIST, if non-nil, means point is inside an inner list." (princ (chronometrist-pp-to-string object) (or stream standard-output))) #+END_SRC + ** Backends :PROPERTIES: :CUSTOM_ID: program-backend :END: + *** chronometrist-file :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-file @@ -1524,8 +1559,12 @@ Signal an error if there is no record to remove.") ***** latest-record :generic:function: #+BEGIN_SRC emacs-lisp (cl-defgeneric chronometrist-latest-record (backend) - "Return the latest entry 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 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. + +If the latest record starts on one day and ends on another, the +entire (unsplit) record must be returned.") #+END_SRC ***** task-records-for-date :generic:function: @@ -1665,6 +1704,9 @@ These can be implemented in terms of the minimal protocol above. #+END_SRC *** Common definitions for s-expression backends +:PROPERTIES: +:CUSTOM_ID: program-backend-sexp-common +:END: **** file-backend-mixin :mixin: :PROPERTIES: :CUSTOM_ID: file-backend-mixin @@ -1769,9 +1811,10 @@ These can be implemented in terms of the minimal protocol above. (find-file-noselect file))))) #+END_SRC -**** on-file-path-change :generic:function: +**** on-file-path-change :writer:method: #+BEGIN_SRC emacs-lisp (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 (setf path new-path file (concat path "." extension)))) @@ -1832,6 +1875,7 @@ These can be implemented in terms of the minimal protocol above. `(with-current-buffer (find-file-noselect ,file) (save-excursion ,@body))) #+END_SRC + **** pre-read-check :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-sexp-pre-read-check (buffer) @@ -1912,6 +1956,7 @@ expression first)." (should (= 1256 last-start)) (should (= 1426 last-end)))) #+END_SRC + **** file-change-type :reader: + rest-start - start of first sexp + rest-end - end of second last sexp @@ -2236,6 +2281,7 @@ Boilerplate for updating state between file operations in tests. (list :last (chronometrist-file-hash :before-last nil nil ,file) :rest (chronometrist-file-hash nil :before-last t ,file))))) #+END_SRC + **** backend :class: #+BEGIN_SRC emacs-lisp (defclass chronometrist-plist-backend (chronometrist-elisp-sexp-backend) @@ -2266,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 @@ -2382,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 @@ -2393,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 @@ -2405,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. @@ -2464,6 +2510,9 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe #+END_SRC *** plist group backend +:PROPERTIES: +:CUSTOM_ID: program-backend-plist-group +:END: This is largely like the plist backend, but plists are grouped by date by wrapping them in a tagged list - #+BEGIN_SRC emacs-lisp :tangle no :load no @@ -2589,6 +2638,9 @@ Situations - #+END_SRC **** plists-split-p :function: +:PROPERTIES: +:CUSTOM_ID: program-data-structures-plists-split-p +:END: [[file:../tests/chronometrist-tests.org::#tests-common-plists-split-p][tests]] #+BEGIN_SRC emacs-lisp @@ -2643,8 +2695,14 @@ Return value is either a list in the form (new-plist-wo-time (chronometrist-plist-remove new-plist :start :stop))) (cond ((not (and old-plist new-plist)) nil) ((equal old-plist-wo-time new-plist-wo-time) - (let ((plist (cl-copy-list old-plist))) - (plist-put plist :stop (plist-get new-plist :stop)))) + (let* ((plist (cl-copy-list old-plist)) + (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"))))) #+END_SRC @@ -2729,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 @@ -2744,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)) @@ -2776,7 +2834,10 @@ Return value is either a list in the form ***** latest-record :reader:method: #+BEGIN_SRC emacs-lisp (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)))))) #+END_SRC ***** task-records-for-date :reader:method: @@ -2792,7 +2853,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) @@ -2811,17 +2872,21 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method (error "No record to replace in %s" (eieio-object-class-name backend))) (chronometrist-sexp-in-file (chronometrist-backend-file backend) (chronometrist-remove-last backend :save nil) + (delete-trailing-whitespace) (chronometrist-insert backend plist :save nil) (save-buffer) 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 ** Migration +:PROPERTIES: +:CUSTOM_ID: program-migration +:END: *** remove-prefix #+BEGIN_SRC emacs-lisp (defun chronometrist-remove-prefix (string) @@ -2876,6 +2941,7 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method output-file) (message "Conversion aborted.")))) #+END_SRC + *** table :variable: :PROPERTIES: :VALUE: hash table @@ -2883,6 +2949,7 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method #+BEGIN_SRC emacs-lisp (defvar chronometrist-migrate-table (make-hash-table)) #+END_SRC + *** EXTEND populate :writer: :PROPERTIES: :STATE: chronometrist-migrate-table @@ -2976,6 +3043,7 @@ file names respectively." chronometrist-migrate-table) (save-buffer))))) #+END_SRC + *** check :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-migrate-check () @@ -2988,6 +3056,7 @@ file names respectively." (chronometrist-migrate-timelog-file-to-sexp-file timeclock-file chronometrist-file) (message "You can migrate later using `chronometrist-migrate-timelog-file-to-sexp-file'.")))) #+END_SRC + ** Timer Instead of the Emacs convention of pressing ~g~ to update, we keep buffers updated with a timer. @@ -3001,10 +3070,12 @@ Note - sometimes, when hacking or dealing with errors, timers may result in subt This is not guaranteed to be accurate - see (info \"(elisp)Timers\")." :type 'integer) #+END_SRC + *** timer-object :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist--timer-object nil) #+END_SRC + *** timer-hook :hook:custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-timer-hook nil @@ -3089,6 +3160,7 @@ modified." All four of these use [[info:elisp#Tabulated List Mode][=(info "(elisp)Tabulated List Mode")=]]. Each of them also contains a "-print-non-tabular" function, which prints the non-tabular parts of the buffer. 1. [ ] There is some duplication between the four frontend commands, e.g. all four act as toggles for their respective buffers, point preservation, etc. + *** Chronometrist :PROPERTIES: :CUSTOM_ID: program-frontend-chronometrist @@ -3098,12 +3170,14 @@ All four of these use [[info:elisp#Tabulated List Mode][=(info "(elisp)Tabulated 1. [X] Define hooks with defcustom instead of defvar 2. [ ] Change abnormal hooks to normal hooks 3. [ ] midnight-spanning plist not displayed (may have to do with partial updates) + **** buffer-name :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-buffer-name "*Chronometrist*" "The name of the buffer created by `chronometrist'." :type 'string) #+END_SRC + **** hide-cursor :custom:variable: I have not yet gotten this to work as well as I wanted. @@ -3112,6 +3186,7 @@ I have not yet gotten this to work as well as I wanted. "If non-nil, hide the cursor and only highlight the current line in the `chronometrist' buffer." :type 'boolean) #+END_SRC + **** activity-indicator :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-activity-indicator "*" @@ -3120,10 +3195,12 @@ Can be a string to be displayed, or a function which returns this string. The default is \"*\"" :type '(choice string function)) #+END_SRC + **** point :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist--point nil) #+END_SRC + **** open-log :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-open-log (&optional _button) @@ -3180,11 +3257,13 @@ Return the value returned by Fₙ." See `tabulated-list-format'." :type '(vector)) #+END_SRC + **** chronometrist-mode-hook :hook:normal: #+BEGIN_SRC emacs-lisp (defvar chronometrist-mode-hook nil "Normal hook run at the very end of `chronometrist-mode'.") #+END_SRC + **** schema-transformers :extension:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-schema-transformers nil @@ -3328,6 +3407,9 @@ task. N must be a positive integer." #+END_SRC **** refresh :procedure: +:PROPERTIES: +:CUSTOM_ID: program-frontend-chronometrist-refresh +:END: #+BEGIN_SRC emacs-lisp (defun chronometrist-refresh (&optional _ignore-auto _noconfirm) "Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'. @@ -3368,6 +3450,7 @@ refresh the `chronometrist' buffer." (chronometrist-out)) t)) #+END_SRC + **** chronometrist-in :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-in (task &optional _prefix) @@ -3378,6 +3461,7 @@ TASK is the name of the task, a string. PREFIX is ignored." (chronometrist-insert (chronometrist-active-backend) plist) (chronometrist-refresh))) #+END_SRC + **** chronometrist-out :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-out (&optional _prefix) @@ -3388,6 +3472,7 @@ PREFIX is ignored." (plist (plist-put latest :stop (chronometrist-format-time-iso8601)))) (chronometrist-replace-last (chronometrist-active-backend) plist))) #+END_SRC + **** run-functions-and-clock-in :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-run-functions-and-clock-in (task) @@ -3396,6 +3481,7 @@ PREFIX is ignored." (chronometrist-in task) (run-hook-with-args 'chronometrist-after-in-functions task)) #+END_SRC + **** run-functions-and-clock-out :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-run-functions-and-clock-out (task) @@ -3404,6 +3490,7 @@ PREFIX is ignored." (chronometrist-out) (run-hook-with-args 'chronometrist-after-out-functions task))) #+END_SRC + **** chronometrist-mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-mode-map @@ -3424,6 +3511,7 @@ PREFIX is ignored." map) "Keymap used by `chronometrist-mode'.") #+END_SRC + **** chronometrist-menu :menu: #+BEGIN_SRC emacs-lisp (easy-menu-define chronometrist-menu chronometrist-mode-map @@ -3449,6 +3537,7 @@ PREFIX is ignored." ["Reset state" chronometrist-reset] ["Import/export data" chronometrist-migrate])) #+END_SRC + **** chronometrist-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-mode tabulated-list-mode "Chronometrist" @@ -3464,6 +3553,7 @@ PREFIX is ignored." (setq revert-buffer-function #'chronometrist-refresh) (run-hooks 'chronometrist-mode-hook)) #+END_SRC + **** toggle-task-button :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-toggle-task-button (_button) @@ -3482,6 +3572,7 @@ action, and is ignored." (unless (equal at-point current) (chronometrist-run-functions-and-clock-in at-point)))) #+END_SRC + **** add-new-task-button :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-add-new-task-button (_button) @@ -3494,6 +3585,7 @@ action, and is ignored." (let ((task (read-from-minibuffer "New task name: " nil nil nil nil nil t))) (chronometrist-run-functions-and-clock-in task)))) #+END_SRC + **** toggle-task :command: #+BEGIN_SRC emacs-lisp ;; TODO - if clocked in and point not on a task, just clock out @@ -3613,6 +3705,9 @@ Has no effect if a task is active." #+END_SRC **** chronometrist :command: +:PROPERTIES: +:CUSTOM_ID: program-frontend-chronometrist-command +:END: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist (&optional arg) @@ -3662,18 +3757,21 @@ run `chronometrist-statistics'." *** Report **** TODO [0%] 1. [ ] preserve point when clicking buttons + **** report :custom:group: #+BEGIN_SRC emacs-lisp (defgroup chronometrist-report nil "Weekly report for the `chronometrist' time tracker." :group 'chronometrist) #+END_SRC + **** buffer-name :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-report-buffer-name "*Chronometrist-Report*" "The name of the buffer created by `chronometrist-report'." :type 'string) #+END_SRC + **** ui-date :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report--ui-date nil @@ -3681,16 +3779,19 @@ run `chronometrist-statistics'." A value of nil means the current week. Otherwise, it must be a date in the form \"YYYY-MM-DD\".") #+END_SRC + **** ui-week-dates :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report--ui-week-dates nil "List of dates currently displayed by `chronometrist-report'. Each date is a list containing calendrical information (see (info \"(elisp)Time Conversion\"))") #+END_SRC + **** point :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report--point nil) #+END_SRC + **** date-to-dates-in-week :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-date-to-dates-in-week (first-date-in-week) @@ -3701,6 +3802,7 @@ FIRST-DATE-IN-WEEK must be a ts struct representing the first date." (cl-loop for i from 0 to 6 collect (ts-adjust 'day i first-date-in-week))) #+END_SRC + **** date-to-week-dates :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-date-to-week-dates () @@ -3714,6 +3816,7 @@ The first date is the first occurrence of (chronometrist-previous-week-start) (chronometrist-report-date-to-dates-in-week))) #+END_SRC + **** rows :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-rows () @@ -3735,6 +3838,7 @@ The first date is the first occurrence of duration-strings ;; vconcat converts lists to vectors total-duration))))) #+END_SRC + **** print-keybind :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-print-keybind (command &optional description firstonly) @@ -3747,6 +3851,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found." " - " (if description description ""))) #+END_SRC + **** CLEANUP print-non-tabular :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-print-non-tabular () @@ -3784,6 +3889,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found." (chronometrist-report-print-keybind 'chronometrist-open-log) (insert-text-button "open log file" 'action #'chronometrist-open-log 'follow-link t))) #+END_SRC + **** REVIEW refresh :procedure: Merge this into `chronometrist-refresh-file', while moving the -refresh call to the call site? @@ -3798,6 +3904,7 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to (chronometrist-maybe-start-timer) (set-window-point w p)))) #+END_SRC + **** refresh-file :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-refresh-file (fs-event) @@ -3806,6 +3913,7 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to (chronometrist-on-change (chronometrist-active-backend) fs-event) (chronometrist-report-refresh)) #+END_SRC + **** report-mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report-mode-map @@ -3825,6 +3933,7 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to map) "Keymap used by `chronometrist-report-mode'.") #+END_SRC + **** report-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-report-mode tabulated-list-mode "Chronometrist-Report" @@ -3854,6 +3963,9 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to #+END_SRC **** chronometrist-report :command: +:PROPERTIES: +:CUSTOM_ID: program-frontend-report-command +:END: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist-report (&optional keep-date) @@ -3881,6 +3993,7 @@ current week. Otherwise, display data from the week specified by (chronometrist-report-refresh-file nil) (goto-char (or chronometrist-report--point 1))))))) #+END_SRC + **** report-previous-week :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-previous-week (arg) @@ -3899,6 +4012,7 @@ With prefix argument ARG, move back ARG weeks." (kill-buffer) (chronometrist-report t)) #+END_SRC + **** report-next-week :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-next-week (arg) @@ -3917,6 +4031,7 @@ With prefix argument ARG, move forward ARG weeks." (kill-buffer) (chronometrist-report t))) #+END_SRC + *** Statistics **** statistics :custom:group: #+BEGIN_SRC emacs-lisp @@ -3924,12 +4039,14 @@ With prefix argument ARG, move forward ARG weeks." "Statistics buffer for the `chronometrist' time tracker." :group 'chronometrist) #+END_SRC + **** buffer-name :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-statistics-buffer-name "*Chronometrist-Statistics*" "The name of the buffer created by `chronometrist-statistics'." :type 'string) #+END_SRC + **** ui-state :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics--ui-state nil @@ -3949,14 +4066,17 @@ weekly/monthly/yearly respectively. :START and :END are the start and end of the date range to be displayed. They must be ts structs (see `ts.el').") #+END_SRC + **** point :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics--point nil) #+END_SRC + **** mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics-mode-map) #+END_SRC + **** count-average-time-spent :function: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-statistics-count-average-time-spent (task &optional (backend (chronometrist-active-backend))) @@ -3967,13 +4087,14 @@ displayed. They must be ts structs (see `ts.el').") when (setq events-in-day (chronometrist-task-records-for-date backend task date)) do (cl-incf days) and collect - (-reduce #'+ (chronometrist-events-to-durations events-in-day)) + (-reduce #'+ (chronometrist-plists-to-durations events-in-day)) into per-day-time-list finally return (if per-day-time-list (/ (-reduce #'+ per-day-time-list) days) 0))) #+END_SRC + **** rows-internal :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-rows-internal (table) @@ -3982,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) @@ -4000,6 +4121,7 @@ reduced to the desired range using (content (vector task active-days active-percent average-time))) (list task content)))) #+END_SRC + **** TEST rows :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-rows () @@ -4011,15 +4133,16 @@ 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 + **** print-keybind :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-print-keybind (command &optional description firstonly) @@ -4033,6 +4156,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found." " - " (if description description ""))) #+END_SRC + **** print-non-tabular :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-print-non-tabular () @@ -4051,6 +4175,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found." (ts-format "%F" (plist-get chronometrist-statistics--ui-state :start)) (ts-format "%F" (plist-get chronometrist-statistics--ui-state :end)))))) #+END_SRC + **** refresh :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-refresh (&optional _ignore-auto _noconfirm) @@ -4068,6 +4193,7 @@ value of `revert-buffer-function'." (chronometrist-maybe-start-timer) (set-window-point w p)))) #+END_SRC + **** mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics-mode-map @@ -4078,6 +4204,7 @@ value of `revert-buffer-function'." map) "Keymap used by `chronometrist-statistics-mode'.") #+END_SRC + **** statistics-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-statistics-mode tabulated-list-mode "Chronometrist-Statistics" @@ -4107,6 +4234,9 @@ value of `revert-buffer-function'." #+END_SRC **** chronometrist-statistics :command: +:PROPERTIES: +:CUSTOM_ID: program-frontend-statistics-command +:END: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist-statistics (&optional preserve-state) @@ -4136,6 +4266,7 @@ specified by `chronometrist-statistics--ui-state'." (switch-to-buffer buffer) (chronometrist-statistics-refresh)))))) #+END_SRC + **** previous-range :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-previous-range (arg) @@ -4156,6 +4287,7 @@ If ARG is a numeric argument, go back that many times." (kill-buffer) (chronometrist-statistics t))) #+END_SRC + **** next-range :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-next-range (arg) @@ -4176,6 +4308,7 @@ If ARG is a numeric argument, go forward that many times." (kill-buffer) (chronometrist-statistics t))) #+END_SRC + *** Details =chronometrist= displays the total time spent on a task - but what were the details? That's where =chronometrist-details= comes in - to display details of recorded time intervals for a given day, in a format terser and more informative than the plists in the file. @@ -4210,6 +4343,7 @@ If ARG is a numeric argument, go forward that many times." (format "*%s*" chronometrist-details-buffer-name-base))) #+END_SRC + **** display-tags :custom:variable: If the value of this variable is a function and the string it returns contains a newline, the results may be undesirable...but hardly unrecoverable, so try it and see, if you wish. @@ -4349,6 +4483,7 @@ using `chronometrist-details-schema-transformers'.") map)) #+END_SRC + **** chronometrist-details-menu :menu: #+BEGIN_SRC emacs-lisp (easy-menu-define chronometrist-details-menu chronometrist-details-mode-map @@ -4360,6 +4495,7 @@ using `chronometrist-details-schema-transformers'.") ["View/edit log file" chronometrist-open-log] ["Reset state" chronometrist-reset])) #+END_SRC + **** chronometrist-details-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-details-mode tabulated-list-mode "Details" @@ -4385,6 +4521,7 @@ BUFFER-OR-NAME must be an existing buffer." (tabulated-list-print))) #+END_SRC + **** chronometrist-details :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-details () @@ -4423,8 +4560,9 @@ range.") string)) #+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) @@ -4457,10 +4595,13 @@ TABLE must be a hash table as returned by collect plist) do (ts-adjustf begin-ts 'day 1))))))) -;; (chronometrist-details-intervals-for-range nil chronometrist-events) -;; (chronometrist-details-intervals-for-range "2021-06-01" chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") chronometrist-events) +;; (chronometrist-details-intervals-for-range nil (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range "2021-06-01" +;; (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (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))) #+END_SRC **** input-to-value :function: @@ -4624,7 +4765,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 diff --git a/manual.md b/manual.md deleted file mode 100644 index 4fc0f83..0000000 --- a/manual.md +++ /dev/null @@ -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: - -Donate using Liberapay - - - -A time tracker in Emacs with a nice interface - -Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker) - - - - -# 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 2 - - - (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.3 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). - - - - -## 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 - -wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support - -jwiegley for `timeclock.el`, which we used as a backend in earlier versions - -blandest for helping me with the name - -fiete and wu-lee for testing and bug reports - - - - -# Local variables :NOEXPORT: - - -# Footnotes - -1 but not `chronometrist-before-in-functions` - -2 but not `chronometrist-before-in-functions` - -3 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 cd6795f..0de28d9 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: @@ -67,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 @@ -190,7 +193,7 @@ Evaluate or add to your init.el the following - :CUSTOM_ID: how-to-key-value-pairs :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 (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. -[fn:3] but not =chronometrist-before-in-functions= - ** How to skip running hooks/attaching tags and key values 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 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. ** How to configure Vertico for use with Chronometrist @@ -375,6 +374,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 @@ -384,8 +384,7 @@ blandest for helping me with the name fiete and wu-lee for testing and bug reports -* Local variables :NOEXPORT: +* 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: