Merge branch 'dev' into sqlite
This commit is contained in:
commit
cd7eb9c726
23
Cask
23
Cask
|
@ -1,23 +0,0 @@
|
|||
(source gnu)
|
||||
(source melpa)
|
||||
|
||||
(package
|
||||
"Chronometrist"
|
||||
"0.9.0"
|
||||
"A time tracker for Emacs with a nice interface")
|
||||
|
||||
(depends-on "cl-lib")
|
||||
(depends-on "literate-elisp" "0.1")
|
||||
(depends-on "dash" "2.16.0")
|
||||
(depends-on "seq" "2.20")
|
||||
(depends-on "ts" "0.2")
|
||||
|
||||
(files "elisp/*.el")
|
||||
|
||||
(development
|
||||
(depends-on "f")
|
||||
(depends-on "ecukes")
|
||||
(depends-on "ert-runner")
|
||||
(depends-on "el-mock")
|
||||
(depends-on "elsa")
|
||||
(depends-on "buttercup"))
|
10
Makefile
10
Makefile
|
@ -65,6 +65,8 @@ lint-package-lint: setup tangle
|
|||
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")' \
|
||||
|
@ -74,16 +76,12 @@ lint-relint: setup tangle
|
|||
lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint
|
||||
|
||||
clean-tangle:
|
||||
-cd elisp/ && \
|
||||
rm elisp/chronometrist.el \
|
||||
elisp/chronometrist-key-values.el \
|
||||
elisp/chronometrist-spark.el \
|
||||
elisp/chronometrist-sqlite.el ; \
|
||||
cd ..
|
||||
elisp/chronometrist-sqlite.el ;
|
||||
|
||||
clean-elc:
|
||||
-cd elisp/ && \
|
||||
rm elisp/*.elc ; \
|
||||
cd ..
|
||||
rm elisp/*.elc
|
||||
|
||||
clean: clean-elc
|
||||
|
|
|
@ -427,7 +427,7 @@ Return t, to permit use in `chronometrist-before-out-functions'."
|
|||
(chronometrist-plist-update
|
||||
latest
|
||||
(read (completing-read (format "Key-values for %s: " task)
|
||||
key-values))))))
|
||||
key-values nil nil nil 'chronometrist-key-values-unified-prompt-history))))))
|
||||
t)
|
||||
|
||||
(provide 'chronometrist-key-values)
|
||||
|
|
|
@ -731,7 +731,7 @@ Return t, to permit use in `chronometrist-before-out-functions'."
|
|||
(chronometrist-plist-update
|
||||
latest
|
||||
(read (completing-read (format "Key-values for %s: " task)
|
||||
key-values))))))
|
||||
key-values nil nil nil 'chronometrist-key-values-unified-prompt-history))))))
|
||||
t)
|
||||
|
||||
#+END_SRC
|
||||
|
@ -740,8 +740,8 @@ Return t, to permit use in `chronometrist-before-out-functions'."
|
|||
(provide 'chronometrist-key-values)
|
||||
;;; chronometrist-key-values.el ends here
|
||||
#+END_SRC
|
||||
|
||||
* Local variables :NOEXPORT:
|
||||
# Local Variables:
|
||||
# nameless-aliases: (("cb" . "chronometrist-backend") ("c" . "chronometrist"))
|
||||
# nameless-current-name: "chronometrist"
|
||||
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
|
||||
# End:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
;;; chronometrist-spark.el --- Show sparklines in Chronometrist -*- lexical-binding: t; -*-
|
||||
;;; chronometrist-spark.el --- Show sparklines in Chronometrist buffers -*- 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 "24.3") (chronometrist "0.7.0") (spark "0.1"))
|
||||
;; Package-Requires: ((emacs "25.1") (chronometrist "0.7.0") (spark "0.1"))
|
||||
;; Version: 0.1.0
|
||||
|
||||
;; This is free and unencumbered software released into the public domain.
|
||||
|
@ -21,6 +21,7 @@
|
|||
;; This package adds a column to Chronometrist displaying sparklines for each task.
|
||||
|
||||
;;; Code:
|
||||
;; This file was automatically generated from chronometrist-spark.org.
|
||||
(require 'chronometrist)
|
||||
(require 'spark)
|
||||
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
* Library headers and commentary
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;;; chronometrist-spark.el --- Show sparklines in Chronometrist -*- lexical-binding: t; -*-
|
||||
;;; chronometrist-spark.el --- Show sparklines in Chronometrist buffers -*- 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 "24.3") (chronometrist "0.7.0") (spark "0.1"))
|
||||
;; Package-Requires: ((emacs "25.1") (chronometrist "0.7.0") (spark "0.1"))
|
||||
;; Version: 0.1.0
|
||||
|
||||
;; This is free and unencumbered software released into the public domain.
|
||||
|
@ -31,12 +31,15 @@
|
|||
;;
|
||||
;; This package adds a column to Chronometrist displaying sparklines for each task.
|
||||
#+END_SRC
|
||||
|
||||
* Dependencies
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;;; Code:
|
||||
;; This file was automatically generated from chronometrist-spark.org.
|
||||
(require 'chronometrist)
|
||||
(require 'spark)
|
||||
#+END_SRC
|
||||
|
||||
* Code
|
||||
** custom group :custom:group:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
|
@ -167,3 +170,8 @@ SCHEMA should be a vector as specified by `tabulated-list-format'."
|
|||
(provide 'chronometrist-spark)
|
||||
;;; chronometrist-spark.el ends here
|
||||
#+END_SRC
|
||||
|
||||
* Local variables :NOEXPORT:
|
||||
# Local Variables:
|
||||
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
|
||||
# End:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
;;; chronometrist.el --- A time tracker with a nice interface -*- lexical-binding: t; -*-
|
||||
;;; chronometrist.el --- Friendly and powerful personal time tracker and analyzer -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
|
||||
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
|
||||
|
@ -18,7 +18,7 @@
|
|||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; A time tracker in Emacs with a nice interface
|
||||
;; Friendly and powerful personal time tracker and analyzer.
|
||||
|
||||
;; Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker)
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
|||
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist or the included manual.org
|
||||
|
||||
;;; Code:
|
||||
;; This file was automatically generated from chronometrist.org
|
||||
;; This file was automatically generated from chronometrist.org.
|
||||
(require 'dash)
|
||||
(require 'ts)
|
||||
|
||||
|
@ -188,7 +188,11 @@ TS must be a ts struct (see `ts.el')."
|
|||
;; plist-p:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*plist type][plist type:1]]
|
||||
(cl-deftype chronometrist-plist () '(satisfies chronometrist-plist-p))
|
||||
(cl-deftype chronometrist-plist ()
|
||||
'(satisfies chronometrist-plist-p)
|
||||
'(satisfies (lambda (plist)
|
||||
(and (plist-get plist :name)
|
||||
(plist-get plist :start)))))
|
||||
;; plist type:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*delete-list][delete-list:1]]
|
||||
|
@ -256,6 +260,7 @@ Return new position of point."
|
|||
FORMAT-STRING and ARGS are passed to `format'."
|
||||
(when chronometrist-debug-enable
|
||||
(with-current-buffer (get-buffer-create chronometrist-debug-buffer)
|
||||
(goto-char (point-max))
|
||||
(chronometrist-debug-log-mode)
|
||||
(let ((inhibit-read-only t))
|
||||
(insert
|
||||
|
@ -270,7 +275,7 @@ FORMAT-STRING and ARGS are passed to `format'."
|
|||
(defun chronometrist-reset ()
|
||||
"Reset Chronometrist's internal state."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: reset")
|
||||
(chronometrist-debug-message "[Command] reset")
|
||||
(chronometrist-reset-backend (chronometrist-active-backend))
|
||||
(chronometrist-refresh))
|
||||
;; reset:1 ends here
|
||||
|
@ -758,7 +763,7 @@ Value must be a keyword corresponding to a key in
|
|||
;; [[file:chronometrist.org::*switch-backend][switch-backend:1]]
|
||||
(defun chronometrist-switch-backend ()
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: switch-backend")
|
||||
(chronometrist-debug-message "[Command] switch-backend")
|
||||
(let* ((prompt (format "Switch to backend (current - %s): "
|
||||
chronometrist-active-backend))
|
||||
(choice (chronometrist-read-backend-name prompt
|
||||
|
@ -864,7 +869,7 @@ a user.")
|
|||
(cl-defgeneric chronometrist-create-file (backend &optional file)
|
||||
"Create file associated with BACKEND.
|
||||
Use FILE as a path, if provided.
|
||||
Return non-nil if FILE is successfully created, and nil if it already exists.")
|
||||
Return path of new file if successfully created, and nil if it already exists.")
|
||||
;; create-file:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*latest-date-records][latest-date-records:1]]
|
||||
|
@ -877,6 +882,10 @@ Return nil if BACKEND contains no records.")
|
|||
(cl-defgeneric chronometrist-insert (backend plist)
|
||||
"Insert PLIST as new record in BACKEND.
|
||||
Return non-nil if record is inserted successfully.")
|
||||
|
||||
(cl-defmethod chronometrist-insert :before ((_backend t) plist &key &allow-other-keys)
|
||||
(unless (cl-typep plist 'chronometrist-plist)
|
||||
(error "Not a valid plist: %S" plist)))
|
||||
;; insert:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*remove-last][remove-last:1]]
|
||||
|
@ -898,11 +907,22 @@ Return value may be active, i.e. it may or may not have a :stop key-value.")
|
|||
DATE-TS must be a `ts.el' struct.
|
||||
|
||||
Return nil if BACKEND contains no records.")
|
||||
|
||||
(cl-defmethod chronometrist-task-records-for-date :before ((_backend t) task date-ts &key &allow-other-keys)
|
||||
(unless (cl-typep task 'string)
|
||||
(error "task %S is not a string" task))
|
||||
(unless (cl-typep date-ts 'ts)
|
||||
(error "date-ts %S is not a `ts' struct" date-ts)))
|
||||
;; task-records-for-date:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*replace-last][replace-last:1]]
|
||||
(cl-defgeneric chronometrist-replace-last (backend plist)
|
||||
"Replace last record in BACKEND with PLIST.")
|
||||
"Replace last record in BACKEND with PLIST.
|
||||
Return non-nil if successful.")
|
||||
|
||||
(cl-defmethod chronometrist-replace-last :before ((_backend t) plist &key &allow-other-keys)
|
||||
(unless (cl-typep plist 'chronometrist-plist)
|
||||
(error "Not a valid plist: %S" plist)))
|
||||
;; replace-last:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*to-file][to-file:1]]
|
||||
|
@ -1057,7 +1077,8 @@ hash table values must be in chronological order.")
|
|||
rest-start rest-end rest-hash
|
||||
file-length last-hash) backend
|
||||
(chronometrist-reset-task-list backend)
|
||||
(file-notify-rm-watch file-watch)
|
||||
(when file-watch
|
||||
(file-notify-rm-watch file-watch))
|
||||
(setf hash-table (chronometrist-to-hash-table backend)
|
||||
file-watch nil
|
||||
rest-start nil
|
||||
|
@ -1097,7 +1118,7 @@ hash table values must be in chronological order.")
|
|||
;; on-file-path-change:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*elisp-sexp-backend][elisp-sexp-backend:1]]
|
||||
(defclass chronometrist-elisp-sexp-backend (chronometrist-backend)
|
||||
(defclass chronometrist-elisp-sexp-backend (chronometrist-backend chronometrist-file-backend-mixin)
|
||||
((rest-start :initarg :rest-start
|
||||
:initform nil
|
||||
:accessor chronometrist-backend-rest-start
|
||||
|
@ -1137,7 +1158,7 @@ hash table values must be in chronological order.")
|
|||
(goto-char (point-min))
|
||||
(insert ";;; -*- mode: chronometrist-sexp; -*-\n\n")
|
||||
(write-file file))
|
||||
t)))
|
||||
file)))
|
||||
;; create-file:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*in-file][in-file:1]]
|
||||
|
@ -1325,7 +1346,7 @@ FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-w
|
|||
(chronometrist-file-change-type backend)))
|
||||
(reset-watch-p (or (eq action 'deleted)
|
||||
(eq action 'renamed))))
|
||||
(chronometrist-debug-message "File change type %s" change)
|
||||
(chronometrist-debug-message "[Method] on-change: file change type %s" change)
|
||||
;; If only the last plist was changed, update hash table and
|
||||
;; task list, otherwise clear and repopulate hash table.
|
||||
(cond ((or reset-watch-p
|
||||
|
@ -1349,7 +1370,7 @@ FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-w
|
|||
;; on-change:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*backend][backend:1]]
|
||||
(defclass chronometrist-plist-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin)
|
||||
(defclass chronometrist-plist-backend (chronometrist-elisp-sexp-backend)
|
||||
((extension :initform "plist"
|
||||
:accessor chronometrist-backend-ext
|
||||
:custom 'string)))
|
||||
|
@ -1408,9 +1429,9 @@ STREAM (which is the value of `current-buffer')."
|
|||
;; to-hash-table:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*insert][insert:1]]
|
||||
(cl-defmethod chronometrist-insert ((backend chronometrist-plist-backend) plist)
|
||||
(cl-defmethod chronometrist-insert ((backend chronometrist-plist-backend) plist &key (save t))
|
||||
(chronometrist-backend-run-assertions backend)
|
||||
(chronometrist-debug-message "Insert plist %s" plist)
|
||||
(chronometrist-debug-message "[Method] insert: %s" plist)
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(goto-char (point-max))
|
||||
;; If we're adding the first s-exp in the file, don't add a
|
||||
|
@ -1418,13 +1439,17 @@ STREAM (which is the value of `current-buffer')."
|
|||
(unless (bobp) (insert "\n"))
|
||||
(unless (bolp) (insert "\n"))
|
||||
(funcall chronometrist-sexp-pretty-print-function plist (current-buffer))
|
||||
(save-buffer)))
|
||||
(when save (save-buffer))
|
||||
t))
|
||||
;; insert:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*remove-last][remove-last:1]]
|
||||
(cl-defmethod chronometrist-remove-last ((backend chronometrist-plist-backend))
|
||||
(chronometrist-debug-message "[Method] remove-last")
|
||||
(chronometrist-backend-run-assertions backend)
|
||||
(chronometrist-debug-message "Remove last plist")
|
||||
(when (chronometrist-backend-empty-p backend)
|
||||
(error "chronometrist-remove-last has nothing to remove in %s"
|
||||
(eieio-object-class-name backend)))
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(goto-char (point-max))
|
||||
;; this condition should never really occur, since we insert a
|
||||
|
@ -1439,7 +1464,7 @@ STREAM (which is the value of `current-buffer')."
|
|||
"Reindent the current buffer.
|
||||
This is meant to be run in `chronometrist-file' when using an s-expression backend."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: reindent-buffer")
|
||||
(chronometrist-debug-message "[Command] reindent-buffer")
|
||||
(let (expr)
|
||||
(goto-char (point-min))
|
||||
(while (setq expr (ignore-errors (read (current-buffer))))
|
||||
|
@ -1531,11 +1556,12 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
|||
|
||||
;; [[file:chronometrist.org::*replace-last][replace-last:1]]
|
||||
(cl-defmethod chronometrist-replace-last ((backend chronometrist-plist-backend) plist)
|
||||
(chronometrist-debug-message "Replace last plist with %s" plist)
|
||||
(chronometrist-debug-message "[Method] replace-last with %s" plist)
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(goto-char (chronometrist-remove-last backend))
|
||||
(funcall chronometrist-sexp-pretty-print-function plist (current-buffer))
|
||||
(save-buffer)))
|
||||
(save-buffer)
|
||||
t))
|
||||
;; replace-last:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*count-records][count-records:1]]
|
||||
|
@ -1549,7 +1575,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
|||
;; count-records:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*backend][backend:1]]
|
||||
(defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin)
|
||||
(defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend)
|
||||
((extension :initform "plg"
|
||||
:accessor chronometrist-backend-ext
|
||||
:custom 'string)))
|
||||
|
@ -1585,7 +1611,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
|||
;; [[file:chronometrist.org::*insert][insert:1]]
|
||||
(cl-defmethod chronometrist-insert ((backend chronometrist-plist-group-backend) plist &key (save t))
|
||||
(cl-check-type plist chronometrist-plist)
|
||||
(chronometrist-debug-message "Insert %S" plist)
|
||||
(chronometrist-debug-message "[Method] insert: %S" plist)
|
||||
(chronometrist-backend-run-assertions backend)
|
||||
(if (not plist)
|
||||
(error "%s" "`chronometrist-insert' was called with an empty plist")
|
||||
|
@ -1828,7 +1854,8 @@ Return value is either a list in the form
|
|||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(chronometrist-remove-last backend :save nil)
|
||||
(chronometrist-insert backend plist :save nil)
|
||||
(save-buffer)))
|
||||
(save-buffer)
|
||||
t))
|
||||
;; replace-last:1 ends here
|
||||
|
||||
;; [[file:chronometrist.org::*remove-prefix][remove-prefix:1]]
|
||||
|
@ -2458,7 +2485,7 @@ If INHIBIT-HOOKS is non-nil, the hooks
|
|||
`chronometrist-before-out-functions', and
|
||||
`chronometrist-after-out-functions' will not be run."
|
||||
(interactive "P")
|
||||
(chronometrist-debug-message "Command: toggle-task %s" (if inhibit-hooks "(without hooks)" ""))
|
||||
(chronometrist-debug-message "[Command] toggle-task %s" (if inhibit-hooks "(without hooks)" ""))
|
||||
(let* ((empty-file (chronometrist-backend-empty-p (chronometrist-active-backend)))
|
||||
(nth (when prefix (chronometrist-goto-nth-task prefix)))
|
||||
(at-point (chronometrist-task-at-point))
|
||||
|
@ -2495,7 +2522,7 @@ there is no corresponding task, do nothing."
|
|||
(defun chronometrist-add-new-task ()
|
||||
"Add a new task."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: add-new-task")
|
||||
(chronometrist-debug-message "[Command] add-new-task")
|
||||
(chronometrist-add-new-task-button nil))
|
||||
;; add-new-task:1 ends here
|
||||
|
||||
|
@ -2508,7 +2535,7 @@ INHIBIT-HOOKS is non-nil or prefix argument is supplied.
|
|||
|
||||
Has no effect if no task is active."
|
||||
(interactive "P")
|
||||
(chronometrist-debug-message "Command: restart-task")
|
||||
(chronometrist-debug-message "[Command] restart-task")
|
||||
(if (chronometrist-current-task)
|
||||
(let* ((latest (chronometrist-latest-record (chronometrist-active-backend)))
|
||||
(plist (plist-put latest :start (chronometrist-format-time-iso8601)))
|
||||
|
@ -2530,7 +2557,7 @@ INHIBIT-HOOKS is non-nil or prefix argument is supplied.
|
|||
|
||||
Has no effect if a task is active."
|
||||
(interactive "P")
|
||||
(chronometrist-debug-message "Command: extend-task")
|
||||
(chronometrist-debug-message "[Command] extend-task")
|
||||
(if (chronometrist-current-task)
|
||||
(message "Cannot extend an active task - use this after clocking out.")
|
||||
(let* ((latest (chronometrist-latest-record (chronometrist-active-backend)))
|
||||
|
@ -2547,7 +2574,7 @@ Has no effect if a task is active."
|
|||
(defun chronometrist-discard-active ()
|
||||
"Remove active interval from the active backend."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: discard-active")
|
||||
(chronometrist-debug-message "[Command] discard-active")
|
||||
(let ((backend (chronometrist-active-backend)))
|
||||
(if (chronometrist-current-task backend)
|
||||
(chronometrist-remove-last backend)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#+TITLE: Chronometrist - an extensible time tracker for Emacs
|
||||
#+SUBTITLE: Developer manual and program source
|
||||
#+TITLE: Chronometrist
|
||||
#+SUBTITLE: Friendly and powerful personal time tracker and analyzer for Emacs
|
||||
#+DESCRIPTION: Developer manual and program source
|
||||
#+AUTHOR: contrapunctus
|
||||
#+TODO: TODO TEST WIP EXTEND CLEANUP FIXME HACK REVIEW |
|
||||
#+PROPERTY: header-args :tangle yes :load yes :comments link
|
||||
|
@ -67,15 +68,15 @@ Thus, I have considered various optimization strategies, and so far implemented
|
|||
|
||||
*** Prevent excess creation of file watchers
|
||||
One of the earliest 'optimizations' of great importance turned out to simply be a bug - turns out, if you run an identical call to [[elisp:(describe-function 'file-notify-add-watch)][=file-notify-add-watch=]] twice, you create /two/ file watchers and your callback will be called /twice./ We were creating a file watcher /each time the =chronometrist= command was run./ 🤦 This was causing humongous slowdowns each time the file changed. 😅
|
||||
+ It was fixed in v0.2.2 by making the watch creation conditional, using [[* fs-watch][=chronometrist--fs-watch=]] to store the watch object.
|
||||
+ It was fixed in v0.2.2 by making the watch creation conditional, using =chronometrist--fs-watch= to store the watch object.
|
||||
|
||||
*** Preserve hash table state for some commands
|
||||
NOTE - this has been replaced with a more general optimization - see next section.
|
||||
|
||||
The next one was released in v0.5. Till then, any time the [[* chronometrist-file][=chronometrist-file=]] was modified, we'd clear the [[* chronometrist-events][=chronometrist-events=]] hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:2] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[#explanation-midnight-spanning-intervals][midnight-spanning events]] is the real performance killer.
|
||||
The next one was released in v0.5. Till then, any time the [[* chronometrist-file][=chronometrist-file=]] was modified, we'd clear the =chronometrist-events= hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:2] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[#explanation-midnight-spanning-intervals][midnight-spanning events]] is the real performance killer.
|
||||
|
||||
After the optimization...
|
||||
1. Two backend functions ([[* new][=chronometrist-sexp-new=]] and [[* replace-last][=chronometrist-sexp-replace-last=]]) were modified to set a flag (=chronometrist--inhibit-read-p=) before saving the file.
|
||||
1. Two backend functions (=chronometrist-sexp-new= and =chronometrist-sexp-replace-last=) were modified to set a flag (=chronometrist--inhibit-read-p=) before saving the file.
|
||||
2. If this flag is non-nil, [[* refresh-file][=chronometrist-refresh-file=]] skips the expensive calls to =chronometrist-events-populate=, =chronometrist-tasks-from-table=, and =chronometrist-tags-history-populate=, and resets the flag.
|
||||
3. Instead, the aforementioned backend functions modify the relevant variables - =chronometrist-events=, =chronometrist-task-list=, and =chronometrist-tags-history= - via...
|
||||
* =chronometrist-events-add= / =chronometrist-events-replace-last=
|
||||
|
@ -384,7 +385,7 @@ Once, for sake of neatness, I made the value of =Package-Requires:= multiline -
|
|||
But I discovered that if I do that, =package-lint= says - =error: Couldn't parse "Package-Requires" header: End of file during parsing=.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :comments no
|
||||
;;; chronometrist.el --- A time tracker with a nice interface -*- lexical-binding: t; -*-
|
||||
;;; chronometrist.el --- Friendly and powerful personal time tracker and analyzer -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
|
||||
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
|
||||
|
@ -408,7 +409,7 @@ But I discovered that if I do that, =package-lint= says - =error: Couldn't parse
|
|||
#+BEGIN_SRC emacs-lisp :comments no
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; A time tracker in Emacs with a nice interface
|
||||
;; Friendly and powerful personal time tracker and analyzer.
|
||||
|
||||
;; Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker)
|
||||
|
||||
|
@ -446,7 +447,7 @@ But I discovered that if I do that, =package-lint= says - =error: Couldn't parse
|
|||
** Dependencies
|
||||
#+BEGIN_SRC emacs-lisp :comments no
|
||||
;;; Code:
|
||||
;; This file was automatically generated from chronometrist.org
|
||||
;; This file was automatically generated from chronometrist.org.
|
||||
(require 'dash)
|
||||
(require 'ts)
|
||||
|
||||
|
@ -512,7 +513,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
(apply #'concat))))
|
||||
#+END_SRC
|
||||
*** day-start-time :custom:variable:
|
||||
[[* events-maybe-split][=chronometrist-events-maybe-split=]] refers to this, but I'm not sure this has the desired effect at the moment—haven't even tried using it.
|
||||
=chronometrist-events-maybe-split= refers to this, but I'm not sure this has the desired effect at the moment—haven't even tried using it.
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defcustom chronometrist-day-start-time "00:00:00"
|
||||
"The time at which a day is considered to start, in \"HH:MM:SS\".
|
||||
|
@ -608,7 +609,10 @@ TS must be a ts struct (see `ts.el')."
|
|||
"Return user key-values from PLIST."
|
||||
(chronometrist-plist-remove plist :name :tags :start :stop))
|
||||
#+END_SRC
|
||||
|
||||
*** plist-p :function:
|
||||
[[file:../tests/chronometrist-tests.org::#tests-common-plist-p][tests]]
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun chronometrist-plist-p (list)
|
||||
"Return non-nil if LIST is a property list, i.e. (:KEYWORD VALUE ...)"
|
||||
|
@ -622,7 +626,11 @@ TS must be a ts struct (see `ts.el')."
|
|||
|
||||
*** plist type :type:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-deftype chronometrist-plist () '(satisfies chronometrist-plist-p))
|
||||
(cl-deftype chronometrist-plist ()
|
||||
'(satisfies chronometrist-plist-p)
|
||||
'(satisfies (lambda (plist)
|
||||
(and (plist-get plist :name)
|
||||
(plist-get plist :start)))))
|
||||
#+END_SRC
|
||||
|
||||
*** delete-list :writer:
|
||||
|
@ -647,6 +655,8 @@ Return new position of point."
|
|||
#+END_SRC
|
||||
|
||||
*** current-task :reader:method:
|
||||
[[file:../tests/chronometrist-tests.org::#tests-common-current-task][tests]]
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defun chronometrist-current-task (&optional (backend (chronometrist-active-backend)))
|
||||
"Return the name of the active task as a string, or nil if not clocked in."
|
||||
|
@ -697,6 +707,7 @@ Return new position of point."
|
|||
FORMAT-STRING and ARGS are passed to `format'."
|
||||
(when chronometrist-debug-enable
|
||||
(with-current-buffer (get-buffer-create chronometrist-debug-buffer)
|
||||
(goto-char (point-max))
|
||||
(chronometrist-debug-log-mode)
|
||||
(let ((inhibit-read-only t))
|
||||
(insert
|
||||
|
@ -721,7 +732,7 @@ The data from =chronometrist-events= is used by most (all?) interval-consuming f
|
|||
(defun chronometrist-reset ()
|
||||
"Reset Chronometrist's internal state."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: reset")
|
||||
(chronometrist-debug-message "[Command] reset")
|
||||
(chronometrist-reset-backend (chronometrist-active-backend))
|
||||
(chronometrist-refresh))
|
||||
#+END_SRC
|
||||
|
@ -1362,7 +1373,7 @@ Value must be a keyword corresponding to a key in
|
|||
#+BEGIN_SRC emacs-lisp
|
||||
(defun chronometrist-switch-backend ()
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: switch-backend")
|
||||
(chronometrist-debug-message "[Command] switch-backend")
|
||||
(let* ((prompt (format "Switch to backend (current - %s): "
|
||||
chronometrist-active-backend))
|
||||
(choice (chronometrist-read-backend-name prompt
|
||||
|
@ -1475,11 +1486,13 @@ a user.")
|
|||
|
||||
**** file operations
|
||||
***** create-file :generic:function:
|
||||
[[file:../tests/chronometrist-tests.org::#tests-backend-create-file][tests]]
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defgeneric chronometrist-create-file (backend &optional file)
|
||||
"Create file associated with BACKEND.
|
||||
Use FILE as a path, if provided.
|
||||
Return non-nil if FILE is successfully created, and nil if it already exists.")
|
||||
Return path of new file if successfully created, and nil if it already exists.")
|
||||
#+END_SRC
|
||||
|
||||
***** latest-date-records :generic:function:
|
||||
|
@ -1494,6 +1507,10 @@ Return nil if BACKEND contains no records.")
|
|||
(cl-defgeneric chronometrist-insert (backend plist)
|
||||
"Insert PLIST as new record in BACKEND.
|
||||
Return non-nil if record is inserted successfully.")
|
||||
|
||||
(cl-defmethod chronometrist-insert :before ((_backend t) plist &key &allow-other-keys)
|
||||
(unless (cl-typep plist 'chronometrist-plist)
|
||||
(error "Not a valid plist: %S" plist)))
|
||||
#+END_SRC
|
||||
|
||||
***** remove-last :generic:function:
|
||||
|
@ -1518,12 +1535,23 @@ Return value may be active, i.e. it may or may not have a :stop key-value.")
|
|||
DATE-TS must be a `ts.el' struct.
|
||||
|
||||
Return nil if BACKEND contains no records.")
|
||||
|
||||
(cl-defmethod chronometrist-task-records-for-date :before ((_backend t) task date-ts &key &allow-other-keys)
|
||||
(unless (cl-typep task 'string)
|
||||
(error "task %S is not a string" task))
|
||||
(unless (cl-typep date-ts 'ts)
|
||||
(error "date-ts %S is not a `ts' struct" date-ts)))
|
||||
#+END_SRC
|
||||
|
||||
***** replace-last :generic:function:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defgeneric chronometrist-replace-last (backend plist)
|
||||
"Replace last record in BACKEND with PLIST.")
|
||||
"Replace last record in BACKEND with PLIST.
|
||||
Return non-nil if successful.")
|
||||
|
||||
(cl-defmethod chronometrist-replace-last :before ((_backend t) plist &key &allow-other-keys)
|
||||
(unless (cl-typep plist 'chronometrist-plist)
|
||||
(error "Not a valid plist: %S" plist)))
|
||||
#+END_SRC
|
||||
|
||||
***** to-file :generic:function:
|
||||
|
@ -1705,7 +1733,8 @@ These can be implemented in terms of the minimal protocol above.
|
|||
rest-start rest-end rest-hash
|
||||
file-length last-hash) backend
|
||||
(chronometrist-reset-task-list backend)
|
||||
(file-notify-rm-watch file-watch)
|
||||
(when file-watch
|
||||
(file-notify-rm-watch file-watch))
|
||||
(setf hash-table (chronometrist-to-hash-table backend)
|
||||
file-watch nil
|
||||
rest-start nil
|
||||
|
@ -1750,7 +1779,7 @@ These can be implemented in terms of the minimal protocol above.
|
|||
|
||||
**** elisp-sexp-backend :class:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defclass chronometrist-elisp-sexp-backend (chronometrist-backend)
|
||||
(defclass chronometrist-elisp-sexp-backend (chronometrist-backend chronometrist-file-backend-mixin)
|
||||
((rest-start :initarg :rest-start
|
||||
:initform nil
|
||||
:accessor chronometrist-backend-rest-start
|
||||
|
@ -1792,7 +1821,7 @@ These can be implemented in terms of the minimal protocol above.
|
|||
(goto-char (point-min))
|
||||
(insert ";;; -*- mode: chronometrist-sexp; -*-\n\n")
|
||||
(write-file file))
|
||||
t)))
|
||||
file)))
|
||||
#+END_SRC
|
||||
|
||||
**** in-file :macro:
|
||||
|
@ -2085,7 +2114,7 @@ FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-w
|
|||
(chronometrist-file-change-type backend)))
|
||||
(reset-watch-p (or (eq action 'deleted)
|
||||
(eq action 'renamed))))
|
||||
(chronometrist-debug-message "File change type %s" change)
|
||||
(chronometrist-debug-message "[Method] on-change: file change type %s" change)
|
||||
;; If only the last plist was changed, update hash table and
|
||||
;; task list, otherwise clear and repopulate hash table.
|
||||
(cond ((or reset-watch-p
|
||||
|
@ -2209,7 +2238,7 @@ Boilerplate for updating state between file operations in tests.
|
|||
#+END_SRC
|
||||
**** backend :class:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defclass chronometrist-plist-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin)
|
||||
(defclass chronometrist-plist-backend (chronometrist-elisp-sexp-backend)
|
||||
((extension :initform "plist"
|
||||
:accessor chronometrist-backend-ext
|
||||
:custom 'string)))
|
||||
|
@ -2274,9 +2303,9 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl
|
|||
|
||||
**** insert :writer:method:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defmethod chronometrist-insert ((backend chronometrist-plist-backend) plist)
|
||||
(cl-defmethod chronometrist-insert ((backend chronometrist-plist-backend) plist &key (save t))
|
||||
(chronometrist-backend-run-assertions backend)
|
||||
(chronometrist-debug-message "Insert plist %s" plist)
|
||||
(chronometrist-debug-message "[Method] insert: %s" plist)
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(goto-char (point-max))
|
||||
;; If we're adding the first s-exp in the file, don't add a
|
||||
|
@ -2284,14 +2313,18 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl
|
|||
(unless (bobp) (insert "\n"))
|
||||
(unless (bolp) (insert "\n"))
|
||||
(funcall chronometrist-sexp-pretty-print-function plist (current-buffer))
|
||||
(save-buffer)))
|
||||
(when save (save-buffer))
|
||||
t))
|
||||
#+END_SRC
|
||||
|
||||
**** remove-last :writer:method:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defmethod chronometrist-remove-last ((backend chronometrist-plist-backend))
|
||||
(chronometrist-debug-message "[Method] remove-last")
|
||||
(chronometrist-backend-run-assertions backend)
|
||||
(chronometrist-debug-message "Remove last plist")
|
||||
(when (chronometrist-backend-empty-p backend)
|
||||
(error "chronometrist-remove-last has nothing to remove in %s"
|
||||
(eieio-object-class-name backend)))
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(goto-char (point-max))
|
||||
;; this condition should never really occur, since we insert a
|
||||
|
@ -2307,7 +2340,7 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl
|
|||
"Reindent the current buffer.
|
||||
This is meant to be run in `chronometrist-file' when using an s-expression backend."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: reindent-buffer")
|
||||
(chronometrist-debug-message "[Command] reindent-buffer")
|
||||
(let (expr)
|
||||
(goto-char (point-min))
|
||||
(while (setq expr (ignore-errors (read (current-buffer))))
|
||||
|
@ -2411,11 +2444,12 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
|||
***** replace-last :writer:method:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defmethod chronometrist-replace-last ((backend chronometrist-plist-backend) plist)
|
||||
(chronometrist-debug-message "Replace last plist with %s" plist)
|
||||
(chronometrist-debug-message "[Method] replace-last with %s" plist)
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(goto-char (chronometrist-remove-last backend))
|
||||
(funcall chronometrist-sexp-pretty-print-function plist (current-buffer))
|
||||
(save-buffer)))
|
||||
(save-buffer)
|
||||
t))
|
||||
#+END_SRC
|
||||
|
||||
***** count-records :reader:method:
|
||||
|
@ -2454,7 +2488,7 @@ Concerns specific to the plist group backend -
|
|||
|
||||
**** backend :class:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin)
|
||||
(defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend)
|
||||
((extension :initform "plg"
|
||||
:accessor chronometrist-backend-ext
|
||||
:custom 'string)))
|
||||
|
@ -2507,7 +2541,7 @@ Situations -
|
|||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defmethod chronometrist-insert ((backend chronometrist-plist-group-backend) plist &key (save t))
|
||||
(cl-check-type plist chronometrist-plist)
|
||||
(chronometrist-debug-message "Insert %S" plist)
|
||||
(chronometrist-debug-message "[Method] insert: %S" plist)
|
||||
(chronometrist-backend-run-assertions backend)
|
||||
(if (not plist)
|
||||
(error "%s" "`chronometrist-insert' was called with an empty plist")
|
||||
|
@ -2555,6 +2589,8 @@ Situations -
|
|||
#+END_SRC
|
||||
|
||||
**** plists-split-p :function:
|
||||
[[file:../tests/chronometrist-tests.org::#tests-common-plists-split-p][tests]]
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun chronometrist-plists-split-p (old-plist new-plist)
|
||||
"Return t if OLD-PLIST and NEW-PLIST are split plists.
|
||||
|
@ -2766,7 +2802,7 @@ Return value is either a list in the form
|
|||
***** replace-last :writer:method:
|
||||
=chronometrist-replace-last= is what is used for clocking out, so we split midnight-spanning intervals in this operation.
|
||||
|
||||
We apply the same hack as in the [[<<hack-note-plist-group-insert>>][insert]] method, removing and inserting the plist group instead of just the specific plist, to avoid having to update the pretty printer.
|
||||
We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method, removing and inserting the plist group instead of just the specific plist, to avoid having to update the pretty printer.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defmethod chronometrist-replace-last ((backend chronometrist-plist-group-backend) plist)
|
||||
|
@ -2776,7 +2812,8 @@ We apply the same hack as in the [[<<hack-note-plist-group-insert>>][insert]] me
|
|||
(chronometrist-sexp-in-file (chronometrist-backend-file backend)
|
||||
(chronometrist-remove-last backend :save nil)
|
||||
(chronometrist-insert backend plist :save nil)
|
||||
(save-buffer)))
|
||||
(save-buffer)
|
||||
t))
|
||||
#+END_SRC
|
||||
|
||||
***** count-records :reader:method:NOEXPORT:
|
||||
|
@ -3474,7 +3511,7 @@ If INHIBIT-HOOKS is non-nil, the hooks
|
|||
`chronometrist-before-out-functions', and
|
||||
`chronometrist-after-out-functions' will not be run."
|
||||
(interactive "P")
|
||||
(chronometrist-debug-message "Command: toggle-task %s" (if inhibit-hooks "(without hooks)" ""))
|
||||
(chronometrist-debug-message "[Command] toggle-task %s" (if inhibit-hooks "(without hooks)" ""))
|
||||
(let* ((empty-file (chronometrist-backend-empty-p (chronometrist-active-backend)))
|
||||
(nth (when prefix (chronometrist-goto-nth-task prefix)))
|
||||
(at-point (chronometrist-task-at-point))
|
||||
|
@ -3513,7 +3550,7 @@ there is no corresponding task, do nothing."
|
|||
(defun chronometrist-add-new-task ()
|
||||
"Add a new task."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: add-new-task")
|
||||
(chronometrist-debug-message "[Command] add-new-task")
|
||||
(chronometrist-add-new-task-button nil))
|
||||
#+END_SRC
|
||||
|
||||
|
@ -3527,7 +3564,7 @@ INHIBIT-HOOKS is non-nil or prefix argument is supplied.
|
|||
|
||||
Has no effect if no task is active."
|
||||
(interactive "P")
|
||||
(chronometrist-debug-message "Command: restart-task")
|
||||
(chronometrist-debug-message "[Command] restart-task")
|
||||
(if (chronometrist-current-task)
|
||||
(let* ((latest (chronometrist-latest-record (chronometrist-active-backend)))
|
||||
(plist (plist-put latest :start (chronometrist-format-time-iso8601)))
|
||||
|
@ -3550,7 +3587,7 @@ INHIBIT-HOOKS is non-nil or prefix argument is supplied.
|
|||
|
||||
Has no effect if a task is active."
|
||||
(interactive "P")
|
||||
(chronometrist-debug-message "Command: extend-task")
|
||||
(chronometrist-debug-message "[Command] extend-task")
|
||||
(if (chronometrist-current-task)
|
||||
(message "Cannot extend an active task - use this after clocking out.")
|
||||
(let* ((latest (chronometrist-latest-record (chronometrist-active-backend)))
|
||||
|
@ -3568,7 +3605,7 @@ Has no effect if a task is active."
|
|||
(defun chronometrist-discard-active ()
|
||||
"Remove active interval from the active backend."
|
||||
(interactive)
|
||||
(chronometrist-debug-message "Command: discard-active")
|
||||
(chronometrist-debug-message "[Command] discard-active")
|
||||
(let ((backend (chronometrist-active-backend)))
|
||||
(if (chronometrist-current-task backend)
|
||||
(chronometrist-remove-last backend)
|
||||
|
@ -4587,9 +4624,13 @@ Return value is a list as specified by `tabulated-list-entries'."
|
|||
;;; chronometrist.el ends here
|
||||
#+END_SRC
|
||||
|
||||
** Local variables :NOEXPORT:
|
||||
* Local variables :NOEXPORT:
|
||||
Evaluate this to be able to insert the package prefix via =nameless-insert-name=, at the cost of having all =nameless-aliases= break (= less readable code).
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :load no :tangle no
|
||||
(setq nameless-current-name "chronometrist")
|
||||
#+END_SRC
|
||||
|
||||
# Local Variables:
|
||||
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
|
||||
# End:
|
||||
|
|
128
manual.md
128
manual.md
|
@ -13,7 +13,7 @@
|
|||
1. [chronometrist](#usage-chronometrist)
|
||||
2. [chronometrist-report](#usage-chronometrist-report)
|
||||
3. [chronometrist-statistics](#usage-chronometrist-statistics)
|
||||
4. [chronometrist-details](#org533cd01)
|
||||
4. [chronometrist-details](#org690c2df)
|
||||
5. [common commands](#usage-common-commands)
|
||||
6. [Time goals/targets](#time-goals)
|
||||
6. [How-to](#how-to)
|
||||
|
@ -21,17 +21,19 @@
|
|||
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](#org0e10cef)
|
||||
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)
|
||||
7. [Explanation](#orgd02a44a)
|
||||
10. [How to configure Vertico for use with Chronometrist](#howto-vertico)
|
||||
7. [Explanation](#org4f83f29)
|
||||
1. [Literate Program](#explanation-literate-program)
|
||||
8. [User's reference](#org215fb52)
|
||||
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>
|
||||
|
||||
|
@ -51,15 +53,14 @@ Largely modelled after the Android application, [A Time Tracker](https://github.
|
|||
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 chronometrist-sparkline extension
|
||||
6. Fancy graphs with the `chronometrist-spark` extension
|
||||
|
||||
|
||||
<a id="limitations"></a>
|
||||
|
||||
# Limitations
|
||||
|
||||
1. No support (yet) for adding a task without clocking into it.
|
||||
2. No support for concurrent tasks.
|
||||
1. No support for concurrent tasks.
|
||||
|
||||
|
||||
<a id="comparisons"></a>
|
||||
|
@ -88,7 +89,6 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
|
|||
- 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 makes keybindings discoverable - they are displayed in the buffers themselves.
|
||||
- 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.
|
||||
|
||||
|
@ -103,7 +103,7 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
|
|||
## 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`
|
||||
|
||||
|
@ -112,7 +112,7 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
|
|||
|
||||
## from Git
|
||||
|
||||
You can get `chronometrist` from <https://tildegit.org/contrapunctus/chronometrist>
|
||||
You can get `chronometrist` from <https://tildegit.org/contrapunctus/chronometrist> or <https://codeberg.org/contrapunctus/chronometrist>
|
||||
|
||||
`chronometrist` requires
|
||||
|
||||
|
@ -159,7 +159,7 @@ Run `M-x chronometrist-statistics` (or `chronometrist` with a prefix argument of
|
|||
Press `b` to look at past time ranges, and `f` for future ones.
|
||||
|
||||
|
||||
<a id="org533cd01"></a>
|
||||
<a id="org690c2df"></a>
|
||||
|
||||
## chronometrist-details
|
||||
|
||||
|
@ -202,7 +202,7 @@ Evaluate or add to your init.el the following -
|
|||
## 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")
|
||||
|
||||
|
@ -211,13 +211,13 @@ Evaluate or add to your init.el the following -
|
|||
|
||||
## 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">1</a></sup> -
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
|
@ -225,16 +225,16 @@ Evaluate or add to your init.el the following -
|
|||
|
||||
## 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">2</a></sup> -
|
||||
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)
|
||||
(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="org0e10cef"></a>
|
||||
<a id="org63b85c6"></a>
|
||||
|
||||
## How to skip running hooks/attaching tags and key values
|
||||
|
||||
|
@ -253,7 +253,7 @@ An idea from the author's own init -
|
|||
(find-file-other-window "~/repertoire.org"))
|
||||
;; ...
|
||||
))
|
||||
|
||||
|
||||
(add-hook 'chronometrist-before-in-functions 'my-start-project)
|
||||
|
||||
|
||||
|
@ -264,19 +264,19 @@ An idea from the author's own init -
|
|||
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)))
|
||||
|
||||
((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)
|
||||
|
||||
|
||||
|
@ -285,14 +285,14 @@ Another one, prompting the user if they have uncommitted changes in a git reposi
|
|||
## How to display the current time interval in the activity indicator
|
||||
|
||||
(defun my-activity-indicator ()
|
||||
(thread-last (plist-put (chronometrist-last)
|
||||
:stop (chronometrist-format-time-iso8601))
|
||||
list
|
||||
chronometrist-events-to-durations
|
||||
(-reduce #'+)
|
||||
truncate
|
||||
chronometrist-format-time))
|
||||
|
||||
(--> (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)
|
||||
|
||||
|
||||
|
@ -300,27 +300,42 @@ Another one, prompting the user if they have uncommitted changes in a git reposi
|
|||
|
||||
## How to back up your Chronometrist data
|
||||
|
||||
I suggest backing up Chronometrist data on each save. Here's how you can do that.
|
||||
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.
|
||||
|
||||
(setq backup-by-copying t
|
||||
kept-new-versions 10
|
||||
kept-old-versions 10
|
||||
version-control t)
|
||||
|
||||
(defun my-force-backup ()
|
||||
(setq buffer-backed-up nil))
|
||||
2. Open your Chronometrist file and add the function to `before-save-hook`.
|
||||
|
||||
|
||||
(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 'before-save-hook #'my-force-backup nil t) RET
|
||||
M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET
|
||||
3. Optionally, configure `backup-directory-alist` to set a specific directory for the backups.
|
||||
|
||||
Adapted from this [StackOverflow answer](https://stackoverflow.com/questions/6916529/how-can-i-make-emacs-backup-every-time-i-save).
|
||||
|
||||
|
||||
<a id="orgd02a44a"></a>
|
||||
<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
|
||||
|
||||
|
@ -331,12 +346,12 @@ Adapted from this [StackOverflow answer](https://stackoverflow.com/questions/691
|
|||
|
||||
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).
|
||||
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="org215fb52"></a>
|
||||
<a id="org07932c3"></a>
|
||||
|
||||
# User's reference
|
||||
|
||||
|
@ -393,7 +408,7 @@ I'd rather make a request - please do everything you can to help that dream come
|
|||
|
||||
Chronometrist is released under your choice of [Unlicense](https://unlicense.org/) or the [WTFPL](http://www.wtfpl.net/).
|
||||
|
||||
(See files <UNLICENSE> and <WTFPL>).
|
||||
(See files [UNLICENSE](UNLICENSE) and [WTFPL](WTFPL)).
|
||||
|
||||
|
||||
<a id="thanks"></a>
|
||||
|
@ -402,15 +417,22 @@ Chronometrist is released under your choice of [Unlicense](https://unlicense.org
|
|||
|
||||
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
|
||||
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.
|
||||
|
|
93
manual.org
93
manual.org
|
@ -1,5 +1,6 @@
|
|||
#+TITLE: Chronometrist - an extensible time tracker for Emacs
|
||||
#+SUBTITLE: User Manual
|
||||
#+SUBTITLE: Friendly and powerful personal time tracker and analyzer for Emacs
|
||||
#+DESCRIPTION: User Manual
|
||||
#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="style.css" />
|
||||
|
||||
#+BEGIN_EXPORT html
|
||||
|
@ -21,14 +22,13 @@ Largely modelled after the Android application, [[https://github.com/netmackan/A
|
|||
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 chronometrist-sparkline extension
|
||||
6. Fancy graphs with the =chronometrist-spark= extension
|
||||
|
||||
* Limitations
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: limitations
|
||||
:END:
|
||||
1. No support (yet) for adding a task without clocking into it.
|
||||
2. No support for concurrent tasks.
|
||||
1. No support for concurrent tasks.
|
||||
|
||||
* Comparisons
|
||||
:PROPERTIES:
|
||||
|
@ -54,7 +54,6 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
|
|||
+ 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 makes keybindings discoverable - they are displayed in the buffers themselves.
|
||||
+ 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.
|
||||
|
||||
|
@ -77,7 +76,7 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti
|
|||
:CUSTOM_ID: install-from-git
|
||||
:END:
|
||||
|
||||
You can get =chronometrist= from https://tildegit.org/contrapunctus/chronometrist
|
||||
You can get =chronometrist= from https://tildegit.org/contrapunctus/chronometrist or https://codeberg.org/contrapunctus/chronometrist
|
||||
|
||||
=chronometrist= requires
|
||||
+ Emacs v25 or higher
|
||||
|
@ -173,7 +172,7 @@ Evaluate or add to your init.el the following -
|
|||
:CUSTOM_ID: how-to-tags
|
||||
:END:
|
||||
|
||||
1. Add =chronometrist-tags-add= to one or more of these hooks [fn:1] -
|
||||
1. Add =chronometrist-tags-add= to one or more of these hooks [fn:2] -
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(add-to-list 'chronometrist-after-in-functions 'chronometrist-tags-add)
|
||||
|
@ -184,24 +183,24 @@ Evaluate or add to your init.el the following -
|
|||
|
||||
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=.
|
||||
|
||||
[fn:1] but not =chronometrist-before-in-functions=
|
||||
[fn:2] but not =chronometrist-before-in-functions=
|
||||
|
||||
** How to attach key-values to time intervals
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: how-to-key-value-pairs
|
||||
:END:
|
||||
|
||||
1. Add =chronometrist-kv-add= to one or more of these hooks [fn:2] -
|
||||
1. Add =chronometrist-kv-add= to one or more of these hooks [fn:3] -
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(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)
|
||||
#+END_SRC
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(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)
|
||||
#+END_SRC
|
||||
|
||||
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:2] but not =chronometrist-before-in-functions=
|
||||
[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.
|
||||
|
@ -256,13 +255,13 @@ Return nil (and run `magit-status') if the user answers no."
|
|||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun my-activity-indicator ()
|
||||
(thread-last (plist-put (chronometrist-last)
|
||||
:stop (chronometrist-format-time-iso8601))
|
||||
list
|
||||
chronometrist-events-to-durations
|
||||
(-reduce #'+)
|
||||
truncate
|
||||
chronometrist-format-time))
|
||||
(--> (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)
|
||||
#+END_SRC
|
||||
|
@ -271,25 +270,43 @@ Return nil (and run `magit-status') if the user answers no."
|
|||
:PROPERTIES:
|
||||
:CUSTOM_ID: how-to-backup
|
||||
:END:
|
||||
I suggest backing up Chronometrist data on each save. Here's how you can do that.
|
||||
I suggest backing up Chronometrist data on each save using the [[https://tildegit.org/contrapunctus/async-backup][async-backup]] package.[fn:1] Here's how you can do that.
|
||||
|
||||
1. Add the following to your init.
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq backup-by-copying t
|
||||
kept-new-versions 10
|
||||
kept-old-versions 10
|
||||
version-control t)
|
||||
|
||||
(defun my-force-backup ()
|
||||
(setq buffer-backed-up nil))
|
||||
(use-package async-backup)
|
||||
#+END_SRC
|
||||
2. Open your Chronometrist file and add the function to =before-save-hook=.
|
||||
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 'before-save-hook #'my-force-backup nil t) RET
|
||||
: M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET
|
||||
3. Optionally, configure =backup-directory-alist= to set a specific directory for the backups.
|
||||
|
||||
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
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: howto-vertico
|
||||
:END:
|
||||
By default, [[https://github.com/minad/vertico][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 -
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq vertico-sort-function nil)
|
||||
#+END_SRC
|
||||
|
||||
Or use =vertico-multiform= to disable sorting for only specific commands -
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(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)))))
|
||||
#+END_SRC
|
||||
|
||||
* Explanation
|
||||
** Literate Program
|
||||
:PROPERTIES:
|
||||
|
@ -297,7 +314,7 @@ Adapted from this [[https://stackoverflow.com/questions/6916529/how-can-i-make-e
|
|||
:END:
|
||||
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 [[https://github.com/jingtaozf/literate-elisp][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-literate-elisp][How to load the program using literate-elisp]].
|
||||
The Org file can also be loaded directly using the [[https://github.com/jingtaozf/literate-elisp][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-literate-elisp][How to load the program using 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.
|
||||
|
||||
|
@ -352,7 +369,7 @@ I'd rather make a request - please do everything you can to help that dream come
|
|||
|
||||
Chronometrist is released under your choice of [[https://unlicense.org/][Unlicense]] or the [[http://www.wtfpl.net/][WTFPL]].
|
||||
|
||||
(See files [[file:UNLICENSE]] and [[file:WTFPL]]).
|
||||
(See files [[file:UNLICENSE][UNLICENSE]] and [[file:WTFPL][WTFPL]]).
|
||||
|
||||
* Thanks
|
||||
:PROPERTIES:
|
||||
|
@ -361,8 +378,14 @@ Chronometrist is released under your choice of [[https://unlicense.org/][Unlicen
|
|||
|
||||
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
|
||||
jwiegley for =timeclock.el=, which we used as a backend in earlier versions
|
||||
|
||||
blandest for helping me with the name
|
||||
|
||||
fiete and wu-lee for testing and bug reports
|
||||
|
||||
* Local variables :NOEXPORT:
|
||||
# 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:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#+PROPERTY: header-args :tangle yes :load yes :comments link
|
||||
#+BEGIN_SRC emacs-lisp :load no :tangle no
|
||||
(setq nameless-current-name "chronometrist")
|
||||
#+END_SRC
|
||||
|
@ -16,9 +17,11 @@
|
|||
(defvar chronometrist-test-backend
|
||||
(make-instance 'chronometrist-plist-group-backend :path chronometrist-test-file-path-stem))
|
||||
|
||||
(defvar chronometrist-test-backends
|
||||
(defun chronometrist-make-test-backends ()
|
||||
(cl-loop for backend in '(chronometrist-plist-backend chronometrist-plist-group-backend)
|
||||
collect (make-instance backend :path chronometrist-test-file-path-stem)))
|
||||
|
||||
(defvar chronometrist-test-backends (chronometrist-make-test-backends))
|
||||
#+END_SRC
|
||||
|
||||
** test-first-record
|
||||
|
@ -90,22 +93,78 @@
|
|||
:stop "2020-05-10T17:10:48+0530")))
|
||||
#+END_SRC
|
||||
|
||||
** cleanup
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defgeneric chronometrist-backend-test-cleanup (backend)
|
||||
"Delete any files created by BACKEND during testing.")
|
||||
|
||||
(cl-defmethod chronometrist-backend-test-cleanup ((backend chronometrist-elisp-sexp-backend))
|
||||
(with-slots (file hash-table) backend
|
||||
(when file
|
||||
(when (file-exists-p file)
|
||||
(delete-file file))
|
||||
(with-current-buffer (get-file-buffer file)
|
||||
;; (erase-buffer)
|
||||
;; Unsuccessful attempt to inhibit "Delete excess backup versions of <file>?" prompts
|
||||
(defvar delete-old-versions)
|
||||
(let ((delete-old-versions t))
|
||||
(set-buffer-modified-p nil)
|
||||
(kill-buffer))))
|
||||
(setf hash-table (clrhash hash-table))))
|
||||
|
||||
(cl-defmethod chronometrist-backend-test-cleanup :after ((backend t))
|
||||
(setf chronometrist-test-backends (chronometrist-make-test-backends)))
|
||||
#+END_SRC
|
||||
|
||||
** ert-deftest :macro:
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: chronometrist-ert-deftest
|
||||
:END:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defmacro chronometrist-ert-deftest (name backend-var &rest test-forms)
|
||||
"Generate test groups containing TEST-FORMS for each backend.
|
||||
BACKEND-VAR is bound to each backend in
|
||||
`chronometrist-test-backends'. TEST-FORMS are passed to
|
||||
`ert-deftest'."
|
||||
(declare (indent defun) (debug t))
|
||||
(cl-loop for backend in chronometrist-test-backends collect
|
||||
(let* ((backend-name (string-remove-suffix
|
||||
"-backend"
|
||||
(string-remove-prefix "chronometrist"
|
||||
(symbol-name
|
||||
(eieio-object-class-name backend)))))
|
||||
(test-name (concat "chronometrist-" (symbol-name name) backend-name)))
|
||||
`(ert-deftest ,(intern test-name) ()
|
||||
(let ((,backend-var ,backend))
|
||||
(unwind-protect
|
||||
(progn ,@test-forms)
|
||||
;; cleanup - remove test backend file
|
||||
(chronometrist-backend-test-cleanup ,backend)))))
|
||||
into test-groups
|
||||
finally return (cons 'progn test-groups)))
|
||||
#+END_SRC
|
||||
|
||||
* Tests
|
||||
** common
|
||||
*** current-task
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: tests-common-current-task
|
||||
:END:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-current-task ()
|
||||
(chronometrist-insert chronometrist-test-backend '(:name "Test"))
|
||||
(should (equal (chronometrist-current-task chronometrist-test-backend) "Test"))
|
||||
(chronometrist-sexp-in-file (chronometrist-backend-file chronometrist-test-backend)
|
||||
(goto-char (point-max))
|
||||
(backward-list)
|
||||
(chronometrist-sexp-delete-list)
|
||||
(save-buffer))
|
||||
(should (not (chronometrist-current-task chronometrist-test-backend))))
|
||||
(chronometrist-ert-deftest current-task b
|
||||
;; (message "current-task test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||||
(chronometrist-create-file b)
|
||||
(should (not (chronometrist-current-task b)))
|
||||
(chronometrist-insert b (list :name "Test" :start (chronometrist-format-time-iso8601)))
|
||||
(should (equal "Test" (chronometrist-current-task b)))
|
||||
(chronometrist-remove-last b)
|
||||
(should (not (chronometrist-current-task b))))
|
||||
#+END_SRC
|
||||
|
||||
*** plist-p
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: tests-common-plist-p
|
||||
:END:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-plist-p ()
|
||||
(should (eq t (chronometrist-plist-p '(:a 1 :b 2))))
|
||||
|
@ -115,6 +174,9 @@
|
|||
#+END_SRC
|
||||
|
||||
*** plists-split-p
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: tests-common-plists-split-p
|
||||
:END:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-plists-split-p ()
|
||||
(should
|
||||
|
@ -163,13 +225,16 @@
|
|||
** data structures
|
||||
*** list-tasks
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-list-tasks ()
|
||||
(let ((task-list (chronometrist-list-tasks chronometrist-test-backend)))
|
||||
(chronometrist-ert-deftest list-tasks b
|
||||
;; (message "list-tasks test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||||
(chronometrist-create-file b)
|
||||
(let ((task-list (chronometrist-list-tasks b)))
|
||||
(should (listp task-list))
|
||||
(should (seq-every-p #'stringp task-list))))
|
||||
#+END_SRC
|
||||
|
||||
** time functions
|
||||
*** format-duration-long :pure:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-format-duration-long ()
|
||||
(should (equal (chronometrist-format-duration-long 5) ""))
|
||||
|
@ -341,98 +406,83 @@ Situations
|
|||
5. multiple records
|
||||
6. +[plist-group] latest plist is split+ (covered in #4)
|
||||
|
||||
*** ert-deftest :macro:
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: chronometrist-ert-deftest
|
||||
:END:
|
||||
#+BEGIN_SRC emacs-lisp :load no :tangle no
|
||||
(defmacro chronometrist-ert-deftest (backend-var &rest deftest-forms)
|
||||
"Generate test groups for each backend in `chronometrist-test-backends'.
|
||||
Each backend object is bound to BACKEND-VAR. DEFTEST-FORMS are
|
||||
passed to `ert-deftest'."
|
||||
(declare (indent defun) (debug t))
|
||||
(cl-loop for backend in chronometrist-test-backends collect
|
||||
(let ((backend-name (symbol-name (eieio-object-class-name backend))))
|
||||
`(ert-deftest ,(intern (concat backend-name "-tests")) ()
|
||||
(let ((,backend-var ,backend))
|
||||
,@deftest-forms)))
|
||||
into test-groups
|
||||
finally return (cons 'progn test-groups)))
|
||||
#+END_SRC
|
||||
|
||||
Tests to be added -
|
||||
1. to-hash-table
|
||||
2. to-file
|
||||
|
||||
The order of these tests is important - the last test for each case is one which moves into the next case.
|
||||
|
||||
*** create-file
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: tests-backend-create-file
|
||||
:END:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-backend-tests ()
|
||||
(let ((plist-1 (car chronometrist-test-records))
|
||||
(chronometrist-ert-deftest create-file b
|
||||
;; (message "create-file test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||||
;; * file does not exist *
|
||||
(should (chronometrist-create-file b))
|
||||
;; * file exists but has no records *
|
||||
(should (not (chronometrist-create-file b))))
|
||||
#+END_SRC
|
||||
|
||||
*** latest-date-records
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(chronometrist-ert-deftest latest-date-records b
|
||||
;; (message "latest-date-records test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||||
(let ((plist-1 (cl-first chronometrist-test-records))
|
||||
(plist-2 (cl-second chronometrist-test-records))
|
||||
(today-ts (chronometrist-date-ts)))
|
||||
(cl-loop for b in chronometrist-test-backends do
|
||||
(unwind-protect
|
||||
(progn
|
||||
;; * file does not exist *
|
||||
(should-error (chronometrist-latest-date-records b))
|
||||
(should-error (chronometrist-insert b plist-1))
|
||||
(should-error (chronometrist-remove-last b))
|
||||
(should-error (chronometrist-to-list b))
|
||||
;; extended protocol
|
||||
(should-error (chronometrist-latest-record b))
|
||||
(should-error (chronometrist-task-records-for-date b "Test" today-ts))
|
||||
(should-error (chronometrist-replace-last b plist-2))
|
||||
(should (chronometrist-create-file b))
|
||||
;; * file does not exist *
|
||||
(should-error (chronometrist-latest-date-records b))
|
||||
(should (chronometrist-create-file b))
|
||||
;; (message "latest-date-records: %S" (chronometrist-latest-date-records b))
|
||||
;; * file exists but has no records *
|
||||
(should (not (chronometrist-latest-date-records b)))
|
||||
(should (chronometrist-insert b plist-1))
|
||||
;; * backend has a single active record *
|
||||
|
||||
;; * file exists but has no records *
|
||||
(should (not (chronometrist-create-file b)))
|
||||
(should (not (chronometrist-latest-date-records b)))
|
||||
(should-error (chronometrist-remove-last b))
|
||||
(should (not (chronometrist-to-list b)))
|
||||
;; extended protocol
|
||||
(should (not (chronometrist-latest-record b)))
|
||||
(should (not (chronometrist-task-records-for-date b "Test" today-ts)))
|
||||
(should-error (chronometrist-replace-last b plist-2))
|
||||
(should (chronometrist-insert b plist-1))
|
||||
;; * backend has a single inactive record *
|
||||
;; * backend has a single active day-crossing record *
|
||||
;; * backend has a single inactive day-crossing record *
|
||||
|
||||
;; * backend has a single active record *
|
||||
|
||||
;; * backend has a single inactive record *
|
||||
;; * backend has a single active day-crossing record *
|
||||
;; * backend has a single inactive day-crossing record *
|
||||
|
||||
;; * backend has two records and one is active *
|
||||
;; * backend has two records and both are inactive *
|
||||
;; * backend has two day-crossing records and one is active *
|
||||
;; * backend has two day-crossing records and both are inactive *
|
||||
)
|
||||
;; cleanup - remove test backend file
|
||||
(let ((file (chronometrist-backend-file b)))
|
||||
(chronometrist-reset-internal b)
|
||||
(when (and file (file-exists-p file))
|
||||
(delete-file file)
|
||||
(kill-buffer (get-file-buffer file))))))))
|
||||
;; * backend has two records and one is active *
|
||||
;; * backend has two records and both are inactive *
|
||||
;; * backend has two day-crossing records and one is active *
|
||||
;; * backend has two day-crossing records and both are inactive *
|
||||
))
|
||||
#+END_SRC
|
||||
|
||||
*** count-records
|
||||
*** insert
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-count-records ()
|
||||
(should (= (chronometrist-count-records chronometrist-test-backend) 12)))
|
||||
(chronometrist-ert-deftest insert b
|
||||
;; (message "insert test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||||
(let* ((plist1 (list :name "Test" :start (chronometrist-format-time-iso8601)))
|
||||
(plist2 (append plist1 (list :stop (chronometrist-format-time-iso8601)))))
|
||||
;; * file does not exist *
|
||||
(should-error (chronometrist-insert b plist1))
|
||||
(should (chronometrist-create-file b))
|
||||
;; * file exists but has no records *
|
||||
(should (chronometrist-insert b plist1))
|
||||
(should (equal (progn (chronometrist-reset-backend b)
|
||||
(chronometrist-latest-date-records b))
|
||||
(list (chronometrist-date-iso) plist1)))
|
||||
;; * backend has a single active record *
|
||||
(should (chronometrist-replace-last b plist2))
|
||||
(should (equal (progn (chronometrist-reset-backend b)
|
||||
(chronometrist-latest-date-records b))
|
||||
(list (chronometrist-date-iso) plist2)))
|
||||
;; * backend has a single inactive record *
|
||||
;; * backend has a single active day-crossing record *
|
||||
;; * backend has a single inactive day-crossing record *
|
||||
|
||||
;; * backend has two records and one is active *
|
||||
;; * backend has two records and both are inactive *
|
||||
;; * backend has two day-crossing records and one is active *
|
||||
;; * backend has two day-crossing records and both are inactive *
|
||||
))
|
||||
#+END_SRC
|
||||
|
||||
*** latest-record
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-latest-record ()
|
||||
(should (equal (chronometrist-latest-record chronometrist-test-backend)
|
||||
chronometrist-test-latest-record)))
|
||||
#+END_SRC
|
||||
|
||||
*** task-records-for-date
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(ert-deftest chronometrist-task-records-for-date ()
|
||||
(should (equal (chronometrist-task-records-for-date chronometrist-test-backend
|
||||
"Programming"
|
||||
(chronometrist-iso-to-ts "2020-05-10"))
|
||||
(list chronometrist-test-latest-record))))
|
||||
#+END_SRC
|
||||
* Local Variables
|
||||
# Local Variables:
|
||||
# delete-old-versions: t
|
||||
# End:
|
||||
|
|
Reference in New Issue