Merge branch 'dev' into sqlite

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

View File

@ -14,7 +14,8 @@
("cr" . "chronometrist-report")
("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))

View File

@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 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

View File

@ -1,6 +1,6 @@
.phony: all setup tangle compile lint clean
.phony: all setup tangle compile lint
all: clean clean-tangle manual.md setup tangle compile lint
all: clean-elc manual.md setup tangle compile lint
setup:
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

View File

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

1
README.org Symbolic link
View File

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

176
TODO.org
View File

@ -737,7 +737,9 @@ for <predicate>)"
#+END_SRC
* 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 "<name>"
[<keyword-value pair>] ...
@ -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 "<duration>"= or =:deduct ("<start time>" . "<stop time>")=.
- This will also make it easier to support formats like [[#new-backends-klog][klog]], which support this feature.
- It will probably complicate all data consuming code, though...think of =chronometrist-details= 🤔
- An alternative idea could be to define a custom variable to hold the user's key values. If this variable is defined, it would be used instead of generating suggestions from past key-values. That way, such situations will not affect key-value suggestions.
** "event"/non-interval records :feature:
** "event records" - records with only a timestamp :feature:
:PROPERTIES:
: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
[<keyword-value pair>] ...)
#+END_SRC
** STARTED tagging dates with key-values [0%] :feature:
:PROPERTIES:
:CREATED: 2022-02-10T22:59:45+0530
:CUSTOM_ID: date-key-values
:branch: date-properties
:END:
Straightforward enough for the plist group backend -
#+BEGIN_SRC elisp
("<date>"
[<keyword> <value>]*
<plist>+)
#+END_SRC
Alternatively, this would be easier to work with - easier to select the key-values or the plists, as required -
#+BEGIN_SRC elisp
("<date>"
(key-values
[<keyword> <value>]+)
<plist>+)
#+END_SRC
=to-list= could omit the date key-values, since it has no concept of dates. =to-hash-table= could have them at the start of the lists of plists, i.e. hash values would still be =(cdr <plist group>)=.
The plist backend could theoretically be extended to support it, but I'd rather deprecate that format (since it suffers from performance issues) and not deal with it again.
1. [ ] Update call sites of =chronometrist-latest-date-records= (which may now also contain key-values)
2. [ ] Update code accessing the hash table
3. [ ] Update =plist-pp= to handle
* undesired file watcher created after literate-elisp-load :bug:
: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 youve just worked by 3 (or use your chosen fraction), and add any minutes left over from previous breaks
4. Set an alarm for that long
5. /Break/ until the alarm goes off, or you decide to resume work
6. If you resume early, note how much time was left, to add to your next break
7. Go back to step 1.
Additional rules:
+ If you have to stop work for a /non-work-related interruption/, start a break immediately.
+ You can (optionally) take a /big break/ for lunch and/or dinner, lasting as long as you like. Set an alarm at the start for when youll resume work. A big break uses up any saved break minutes, so you cant carry them over to the afternoon/evening.
+ /Avoid taking other unearned breaks/ if possibleso try to do personal tasks during normal or big breaks, or before/after your work day.
#+END_QUOTE
1. [X] =chronometrist-third-fraction=
2. [X] =chronometrist-third-break-time=
3. [X] on clock out, increment =break-time= and start timed notification
4. [X] on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero.
5. [ ] "big break" command - user enters a time, for which we set an alarm. User must resume working around that time.
6. [ ] Define work hours
* [ ] Custom variable for default work hours.
* [ ] Hook function which inserts default work hours.
* [ ] Hook function which prompts for work hours whenever you first clock in on a date. If work hours are defined in the custom variable, ask whether to use them - on negative answer, prompt for today's work hours. If work hours are not defined, prompt for today's work hours.
7. [ ] Displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time.
** Example flow
1. work for 30m
2. clock out - add 10m to =break-time=
3. clock in after a 5m break - subtract 5m from =break-time=
** Extras
1. persist =break-time= between Emacs sessions
2. audible alerts
3. handle user edits to the database
* last record modified - ?
* remove last record - ?
* example - interval extended
1. work 10 minutes and clock out - +3m break time
2. edit stop time to add +20 minutes of work (30m total)
* compare with old data in hash table - decrement break time added by old plist, increment break time added by new plist
* STARTED Customizable alerts [50%] :feature:code:
:PROPERTIES:
:CREATED: 2022-02-14T08:22:36+0530
:CUSTOM_ID: customizable-alerts
:branch: alert-macro
:END:
=chronometrist-third= and =chronometrist-goal= have nearly identical alert code; additionally, users cannot customize the alert style per-alert without basically rewriting the alert functions.
Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-functions= to customizable alists[fn:4] whose entries take the form =(SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS)=, where
+ =SYMBOL= uniquely identifies the alert,
+ =FUNCTION= is a function calling =chronometrist-*-run-at-time= and =ALERT-FN=, and
+ =ALERT-ARGS= are passed to =ALERT-FN=.
=ALERT-FN= will usually be =alert=, and =ALERT-ARGS= will usually be keyword arguments passed to =alert=.
Similar to how they behave now, these packages will start/stop functions for all entries provided in these alists. (So the user can still control which alerts are run.)
Also define =chronometrist--timer-alist=, which associates =symbol= with a timer object.
1. [X] write macro
2. [ ] use in =chronometrist-goal= and =chronometrist-third=
[fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical.
* DONE Predefined key-values for tasks
:PROPERTIES:
:CREATED: 2022-02-17T23:34:17+0530
:END:
Benefits
1. Speeds up key-value completion for very large data sets.
2. Prevents key-value suggestions from being affected by [[#multiple-intervals-per-record][splitting of tasks across multiple intervals]].
#+BEGIN_SRC emacs-lisp
'(("Task" <plist> ...)
...)
#+END_SRC
* no-goal-alert - "You have spent on <task>" [0%] :bug:
:PROPERTIES:
:CREATED: 2022-02-25T00:19:49+0530
:END:
=chronometrist-goal-no-goal-alert= uses =chronometrist-task-time-one-day=, which calls =chronometrist-task-records-for-date= with the /current/ date.
1. [ ] Use the data for the latest date recorded in the database, rather than the current date.
* better error reporting
:PROPERTIES:
:CREATED: 2022-02-28T11:35:37+0530
:END:
Similar to [[#verify-command][verify command]], perhaps, but this is really about making error messages thrown by the program more meaningful to the user.
** record does not contain intervals
:PROPERTIES:
:CREATED: 2022-02-28T11:35:47+0530
:END:

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -192,6 +192,29 @@ used in `chronometrist-before-out-functions'."
"Add key-values to Chronometrist time intervals."
:group 'chronometrist)
(defcustom chronometrist-key-value-use-database-history t
"If non-nil, use database to generate key-value suggestions.
If nil, only `chronometrist-key-value-preset-alist' is used."
:type 'boolean
:group 'chronometrist-key-value)
(defcustom chronometrist-key-value-preset-alist nil
"Alist of key-value suggestions for `chronometrist-key-value' prompts.
Each element must be in the form (\"TASK\" <KEYWORD> <VALUE> ...)"
:type
'(repeat
(cons
(string :tag "Task name")
(repeat :tag "Property preset"
(plist :tag "Property"
;; :key-type 'keyword :value-type 'sexp
))))
:group 'chronometrist-key-values)
(defun chronometrist-key-value-get-presets (task)
"Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists."
(alist-get task chronometrist-key-value-preset-alist nil nil #'equal))
(defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*"
"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)

View File

@ -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\" <KEYWORD> <VALUE> ...)"
:type
'(repeat
(cons
(string :tag "Task name")
(repeat :tag "Property preset"
(plist :tag "Property"
;; :key-type 'keyword :value-type 'sexp
))))
:group 'chronometrist-key-values)
#+END_SRC
**** get-presets
#+BEGIN_SRC emacs-lisp
(defun chronometrist-key-value-get-presets (task)
"Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists."
(alist-get task chronometrist-key-value-preset-alist nil nil #'equal))
#+END_SRC
*** kv-buffer-name :custom:variable:
#+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:

View File

@ -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:

View File

@ -0,0 +1,178 @@
;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.0.1
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;; Add support for the Third Time system to Chronometrist. In Third
;; Time, you work for any length of time you like, and "earn" a third
;; of the work time as break time. For a more detailed explanation,
;; see
;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md
;;; Code:
(require 'chronometrist)
(require 'alert)
;; [[file:chronometrist-third.org::*group][group:1]]
(defgroup chronometrist-third nil
"Third Time support for Chronometrist."
:group 'chronometrist)
;; group:1 ends here
;; [[file:chronometrist-third.org::*divisor][divisor:1]]
(defcustom chronometrist-third-divisor 3
"Number to determine accumulation of break time relative to work time."
:type 'number)
;; divisor:1 ends here
;; [[file:chronometrist-third.org::*duration-format][duration-format:1]]
(defcustom chronometrist-third-duration-format "%H, %M and %S%z"
"Format string for durations, passed to `format-seconds'."
:type 'string)
;; duration-format:1 ends here
;; [[file:chronometrist-third.org::*break-time][break-time:1]]
(defvar chronometrist-third-break-time 0
"Accumulated break time in seconds.")
;; break-time:1 ends here
;; [[file:chronometrist-third.org::*alert-functions][alert-functions:1]]
(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert)
"List of timed alerts for the Third Time system.
Typically, each function in this list should call
`chronometrist-third-run-at-time' to run another function, which
in turn should call `alert' to notify the user.
All functions in this list are started when the user clocks out,
and stopped when they clock in."
:group 'chronometrist-third
:type 'hook)
;; alert-functions:1 ends here
;; [[file:chronometrist-third.org::*timer-list][timer-list:1]]
(defvar chronometrist-third-timer-list nil)
;; timer-list:1 ends here
;; [[file:chronometrist-third.org::*run-at-time][run-at-time:1]]
(defun chronometrist-third-run-at-time (time repeat function &rest args)
"Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'."
(cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list))
;; run-at-time:1 ends here
;; [[file:chronometrist-third.org::*half-alert][half-alert:1]]
(defun chronometrist-third-half-alert ()
"Display an alert when half the break time is consumed."
(let ((half-time (/ chronometrist-third-break-time 2.0)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
half-time nil
(lambda (half-time)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format half-time))))
half-time))))
;; half-alert:1 ends here
;; [[file:chronometrist-third.org::*quarter-alert][quarter-alert:1]]
(defun chronometrist-third-quarter-alert ()
"Display an alert when 3/4ths of the break time is consumed."
(let ((three-fourths (* chronometrist-third-break-time 7.5)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
three-fourths nil
(lambda (three-fourths)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format
(- chronometrist-third-break-time three-fourths)))))
three-fourths))))
;; quarter-alert:1 ends here
;; [[file:chronometrist-third.org::*break-over-alert][break-over-alert:1]]
(defun chronometrist-third-break-over-alert ()
"Display an alert when break time is over."
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
chronometrist-third-break-time nil
(lambda () (alert (format "Break time is over!"))))))
;; break-over-alert:1 ends here
;; [[file:chronometrist-third.org::*start-alert-timers][start-alert-timers:1]]
(defun chronometrist-third-start-alert-timers ()
"Run functions in `chronometrist-third-alert-functions'."
(mapc #'funcall chronometrist-third-alert-functions))
;; start-alert-timers:1 ends here
;; [[file:chronometrist-third.org::*stop-alert-timers][stop-alert-timers:1]]
(defun chronometrist-third-stop-alert-timers ()
"Stop timers in `chronometrist-third-timer-list'."
(mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list))
;; stop-alert-timers:1 ends here
;; [[file:chronometrist-third.org::*clock-in][clock-in:1]]
(defun chronometrist-third-clock-in (&optional _arg)
"Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time)
(-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend))))
(used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop)))
(used-break-string (format-seconds chronometrist-third-duration-format used-break))
(new-break (- chronometrist-third-break-time used-break))
(old-break chronometrist-third-break-time))
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0))
(alert
(if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break)
used-break-string)
(format "You have used %s of your break time (%s left)"
used-break-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
;; clock-in:1 ends here
;; [[file:chronometrist-third.org::*clock-out][clock-out:1]]
(defun chronometrist-third-clock-out (&optional _arg)
"Update break time based on the latest work interval.
Run `chronometrist-third-alert-functions' to alert user when
break time is up."
(let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))
(break-time-increment (/ latest-work-duration chronometrist-third-divisor)))
(cl-incf chronometrist-third-break-time break-time-increment)
(alert (format "You have gained %s of break time (%s total)"
(format-seconds chronometrist-third-duration-format break-time-increment)
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))
;; start alert timer(s)
(chronometrist-third-start-alert-timers)))
;; clock-out:1 ends here
;; [[file:chronometrist-third.org::*third-minor-mode][third-minor-mode:1]]
;;;###autoload
(define-minor-mode chronometrist-third-minor-mode
nil nil nil nil
(cond (chronometrist-third-minor-mode
(add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))
(t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))))
;; third-minor-mode:1 ends here
(provide 'chronometrist-third)
;;; chronometrist-third.el ends here

View File

@ -0,0 +1,213 @@
#+TITLE: chronometrist-third
#+SUBTITLE: Third Time System extension for Chronometrist
#+PROPERTY: header-args :tangle yes :load yes :comments link
* Program source
** Library headers and commentary
#+BEGIN_SRC emacs-lisp :comments no
;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.0.1
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;; Add support for the Third Time system to Chronometrist. In Third
;; Time, you work for any length of time you like, and "earn" a third
;; of the work time as break time. For a more detailed explanation,
;; see
;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md
#+END_SRC
** Dependencies
#+BEGIN_SRC emacs-lisp :comments no
;;; Code:
(require 'chronometrist)
(require 'alert)
#+END_SRC
** group :custom:group:
#+BEGIN_SRC emacs-lisp
(defgroup chronometrist-third nil
"Third Time support for Chronometrist."
:group 'chronometrist)
#+END_SRC
** divisor :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-divisor 3
"Number to determine accumulation of break time relative to work time."
:type 'number)
#+END_SRC
** duration-format :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-duration-format "%H, %M and %S%z"
"Format string for durations, passed to `format-seconds'."
:type 'string)
#+END_SRC
** break-time :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-third-break-time 0
"Accumulated break time in seconds.")
#+END_SRC
** alert-functions :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert)
"List of timed alerts for the Third Time system.
Typically, each function in this list should call
`chronometrist-third-run-at-time' to run another function, which
in turn should call `alert' to notify the user.
All functions in this list are started when the user clocks out,
and stopped when they clock in."
:group 'chronometrist-third
:type 'hook)
#+END_SRC
** timer-list :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-third-timer-list nil)
#+END_SRC
** run-at-time :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-run-at-time (time repeat function &rest args)
"Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'."
(cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list))
#+END_SRC
** half-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-half-alert ()
"Display an alert when half the break time is consumed."
(let ((half-time (/ chronometrist-third-break-time 2.0)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
half-time nil
(lambda (half-time)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format half-time))))
half-time))))
#+END_SRC
** quarter-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-quarter-alert ()
"Display an alert when 3/4ths of the break time is consumed."
(let ((three-fourths (* chronometrist-third-break-time 7.5)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
three-fourths nil
(lambda (three-fourths)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format
(- chronometrist-third-break-time three-fourths)))))
three-fourths))))
#+END_SRC
** break-over-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-break-over-alert ()
"Display an alert when break time is over."
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
chronometrist-third-break-time nil
(lambda () (alert (format "Break time is over!"))))))
#+END_SRC
** start-alert-timers :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-start-alert-timers ()
"Run functions in `chronometrist-third-alert-functions'."
(mapc #'funcall chronometrist-third-alert-functions))
#+END_SRC
** stop-alert-timers :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-stop-alert-timers ()
"Stop timers in `chronometrist-third-timer-list'."
(mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list))
#+END_SRC
** clock-in :hook:procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-clock-in (&optional _arg)
"Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time)
(-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend))))
(used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop)))
(used-break-string (format-seconds chronometrist-third-duration-format used-break))
(new-break (- chronometrist-third-break-time used-break))
(old-break chronometrist-third-break-time))
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0))
(alert
(if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break)
used-break-string)
(format "You have used %s of your break time (%s left)"
used-break-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
#+END_SRC
** clock-out :hook:procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-clock-out (&optional _arg)
"Update break time based on the latest work interval.
Run `chronometrist-third-alert-functions' to alert user when
break time is up."
(let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))
(break-time-increment (/ latest-work-duration chronometrist-third-divisor)))
(cl-incf chronometrist-third-break-time break-time-increment)
(alert (format "You have gained %s of break time (%s total)"
(format-seconds chronometrist-third-duration-format break-time-increment)
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))
;; start alert timer(s)
(chronometrist-third-start-alert-timers)))
#+END_SRC
** third-minor-mode :minor:mode:
#+BEGIN_SRC emacs-lisp
;;;###autoload
(define-minor-mode chronometrist-third-minor-mode
nil nil nil nil
(cond (chronometrist-third-minor-mode
(add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))
(t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))))
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp :comments no
(provide 'chronometrist-third)
;;; chronometrist-third.el ends here
#+END_SRC
* Local variables :noexport:
# Local Variables:
# my-org-src-default-lang: "emacs-lisp"
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End:

View File

@ -5,7 +5,7 @@
;; Keywords: calendar
;; 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]]

File diff suppressed because it is too large Load Diff

438
manual.md
View File

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

View File

@ -4,14 +4,19 @@
#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="style.css" />
#+BEGIN_EXPORT html
<a href="https://liberapay.com/contrapunctus/donate"><img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay"></a>
<a href="https://liberapay.com/contrapunctus/donate">
<img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay">
</a>
<a href="https://melpa.org/#/chronometrist"><img src="https://melpa.org/packages/chronometrist-badge.svg"></a>
<a href="https://melpa.org/#/chronometrist">
<img src="https://melpa.org/packages/chronometrist-badge.svg">
</a>
#+END_EXPORT
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: