Merge branch 'dev' into sqlite
This commit is contained in:
commit
239f733dd8
|
@ -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))
|
||||
|
|
13
CHANGELOG.md
13
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
|
||||
|
|
37
Makefile
37
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)" \
|
||||
|
@ -15,30 +15,32 @@ manual.md:
|
|||
tangle:
|
||||
cd elisp/ && \
|
||||
emacs --batch \
|
||||
--eval="(package-initialize)" \
|
||||
--eval="(require 'ob-tangle)" \
|
||||
--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 \
|
||||
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 ..
|
||||
|
||||
lint-check-declare: tangle
|
||||
cd elisp/ && \
|
||||
emacs -q -Q --batch \
|
||||
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 ..
|
||||
|
||||
|
@ -48,40 +50,33 @@ lint-checkdoc: tangle
|
|||
--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 ..
|
||||
|
||||
lint-package-lint: setup tangle
|
||||
cd elisp/ && \
|
||||
emacs -q -Q --batch \
|
||||
--eval="(progn (package-initialize) (require 'dash) (require 'ts))" \
|
||||
--eval="(require 'package-lint)" \
|
||||
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 ..
|
||||
|
||||
lint-relint: setup tangle
|
||||
cd elisp/ && \
|
||||
emacs -q -Q --batch \
|
||||
--eval="(package-initialize)" \
|
||||
--eval="(require 'relint)" \
|
||||
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 ..
|
||||
|
||||
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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
manual.org
|
176
TODO.org
176
TODO.org
|
@ -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 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" <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:
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
|
@ -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,12 +427,16 @@ 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
|
||||
(when chronometrist-key-value-use-database-history
|
||||
(cl-loop for plist in (chronometrist-to-list backend)
|
||||
when (equal (plist-get plist :name) task)
|
||||
collect
|
||||
|
@ -418,16 +445,17 @@ Return t, to permit use in `chronometrist-before-out-functions'."
|
|||
into key-value-plists
|
||||
finally return
|
||||
(--> (seq-filter #'identity key-value-plists)
|
||||
(cl-remove-duplicates it :test #'equal :from-end t))))
|
||||
(cl-remove-duplicates it :test #'equal :from-end t)))))
|
||||
(latest (chronometrist-latest-record backend)))
|
||||
(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)
|
||||
|
|
|
@ -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,13 +741,18 @@ 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
|
||||
(when chronometrist-key-value-use-database-history
|
||||
(cl-loop for plist in (chronometrist-to-list backend)
|
||||
when (equal (plist-get plist :name) task)
|
||||
collect
|
||||
|
@ -722,26 +761,28 @@ Return t, to permit use in `chronometrist-before-out-functions'."
|
|||
into key-value-plists
|
||||
finally return
|
||||
(--> (seq-filter #'identity key-value-plists)
|
||||
(cl-remove-duplicates it :test #'equal :from-end t))))
|
||||
(cl-remove-duplicates it :test #'equal :from-end t)))))
|
||||
(latest (chronometrist-latest-record backend)))
|
||||
(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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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:
|
|
@ -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
438
manual.md
|
@ -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.
|
25
manual.org
25
manual.org
|
@ -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:
|
||||
|
|
Reference in New Issue