This repository has been archived on 2022-05-13. You can view files and clone it, but cannot push or open issues or pull requests.
chronometrist/elisp/chronometrist.org

768 lines
34 KiB
Org Mode
Raw Normal View History

* chronometrist
** Commentary
2021-02-04 03:28:52 +00:00
This is displayed when the user clicks on the package's entry in =M-x list-packages=.
#+BEGIN_SRC emacs-lisp
2019-08-04 19:27:22 +00:00
;;; Commentary:
;;
;; A time tracker in Emacs with a nice interface
2019-01-08 08:03:12 +00:00
;; Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker)
2019-01-08 08:03:12 +00:00
;; * 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
;; * Limitations
;; 1. No support (yet) for adding a task without clocking into it.
;; 2. No support for concurrent tasks.
;; ## Comparisons
;; ### timeclock.el
;; Compared to timeclock.el, Chronometrist
;; * stores data in an s-expression format rather than a line-based one
;; * supports attaching tags and arbitrary key-values to time intervals
;; * has commands to shows useful summaries
;; * has more hooks
;; ### Org time tracking
;; Chronometrist and Org time tracking seem to be equivalent in terms of capabilities, approaching the same ends through different means.
;; * Chronometrist doesn't have a mode line indicator at the moment. (planned)
;; * Chronometrist doesn't have Org's sophisticated querying facilities. (an SQLite backend is planned)
;; * Org does so many things that keybindings seem to necessarily get longer. Chronometrist has far fewer commands than Org, so most of the keybindings are single keys, without modifiers.
;; * Chronometrist's UI 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.
;; For information on usage and customization, see https://github.com/contrapunctus-1/chronometrist/blob/master/README.md
#+END_SRC
** Code
*** Preliminaries
#+BEGIN_SRC emacs-lisp
2019-07-29 10:59:17 +00:00
;;; Code:
(eval-when-compile
(defvar chronometrist-mode-map)
(require 'subr-x))
(autoload 'chronometrist-maybe-start-timer "chronometrist-timer" nil t)
(autoload 'chronometrist-report "chronometrist-report" nil t)
(autoload 'chronometrist-statistics "chronometrist-statistics" nil t)
#+END_SRC
*** Chronometrist
**** custom group :custom:group:
#+BEGIN_SRC emacs-lisp
(defgroup chronometrist nil
"A time tracker with a nice UI."
:group 'applications)
#+END_SRC
**** chronometrist-file :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-file
(locate-user-emacs-file "chronometrist.sexp")
"Default path and name of the Chronometrist database.
It should be a text file containing plists in the form -
\(:name \"task name\"
[:tags TAGS]
[:comment \"comment\"]
[KEY-VALUE-PAIR ...]
:start \"TIME\"
:stop \"TIME\"\)
Where -
TAGS is a list. It can contain any strings and symbols.
KEY-VALUE-PAIR can be any keyword-value pairs. Currently,
Chronometrist ignores them.
TIME must be an ISO-8601 time string.
\(The square brackets here refer to optional elements, not
vectors.\)"
:type 'file)
#+END_SRC
**** buffer-name :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-buffer-name "*Chronometrist*"
"The name of the buffer created by `chronometrist'."
:type 'string)
#+END_SRC
**** hide-cursor :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-hide-cursor nil
"If non-nil, hide the cursor and only highlight the current line in the `chronometrist' buffer."
:type 'boolean)
#+END_SRC
**** update-interval :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-update-interval 5
"How often the `chronometrist' buffer should be updated, in seconds.
This is not guaranteed to be accurate - see (info \"(elisp)Timers\")."
:type 'integer)
#+END_SRC
**** activity-indicator :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-activity-indicator "*"
"How to indicate that a task is active.
Can be a string to be displayed, or a function which returns this string.
The default is \"*\""
:type '(choice string function))
#+END_SRC
**** day-start-time :custom:variable:
#+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\".
The default is midnight, i.e. \"00:00:00\"."
:type 'string)
#+END_SRC
**** point :internal:variable:
#+BEGIN_SRC emacs-lisp
2018-10-03 19:23:37 +00:00
(defvar chronometrist--point nil)
#+END_SRC
**** open-log :command:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-open-log (&optional _button)
"Open `chronometrist-file' in another window.
Argument _BUTTON is for the purpose of using this command as a
button action."
(interactive)
(chronometrist-sexp-open-log))
#+END_SRC
**** create-file :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-common-create-file ()
"Create `chronometrist-file' if it doesn't already exist."
(chronometrist-sexp-create-file))
#+END_SRC
**** task-active? :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-task-active? (task)
"Return t if TASK is currently clocked in, else nil."
(equal (chronometrist-current-task) task))
#+END_SRC
**** activity-indicator :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-activity-indicator ()
"Return a string to indicate that a task is active.
See custom variable `chronometrist-activity-indicator'."
(if (functionp chronometrist-activity-indicator)
(funcall chronometrist-activity-indicator)
chronometrist-activity-indicator))
#+END_SRC
**** run-transformers :function:
#+BEGIN_SRC emacs-lisp
2021-01-04 03:14:48 +00:00
(defun chronometrist-run-transformers (transformers arg)
"Run TRANSFORMERS with ARG.
TRANSFORMERS should be a list of functions (F₁ ... Fₙ), each of
which should accept a single argument.
Call F₁ with ARG, with each following function being called with
the return value of the previous function.
Return the value returned by Fₙ."
(if transformers
(dolist (fn transformers arg)
(setq arg (funcall fn arg)))
arg))
#+END_SRC
**** entries :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-entries ()
"Create entries to be displayed in the buffer created by `chronometrist', in the format specified by `tabulated-list-entries'."
2019-09-05 18:57:48 +00:00
;; HACK - these calls are commented out, because `chronometrist-entries' is
;; called by both `chronometrist-refresh' and `chronometrist-refresh-file', and only the
;; latter should refresh from a file.
;; (chronometrist-events-populate)
;; (chronometrist-events-clean)
2020-09-06 08:35:24 +00:00
(->> (-sort #'string-lessp chronometrist-task-list)
(--map-indexed
2020-04-14 08:53:57 +00:00
(let* ((task it)
(index (number-to-string (1+ it-index)))
2020-09-02 13:54:13 +00:00
(task-button `(,task action chronometrist-toggle-task-button follow-link t))
(task-time (chronometrist-format-time (chronometrist-task-time-one-day task)))
(indicator (if (chronometrist-task-active? task) (chronometrist-activity-indicator) "")))
(--> (vector index task-button task-time indicator)
(list task it)
2021-01-04 03:14:48 +00:00
(chronometrist-run-transformers chronometrist-entry-transformers it))))))
#+END_SRC
**** task-at-point :function:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
(defun chronometrist-task-at-point ()
"Return the task at point in the `chronometrist' buffer, or nil if there is no task at point."
2018-09-02 10:55:25 +00:00
(save-excursion
(beginning-of-line)
(when (re-search-forward "[0-9]+ +" nil t)
(get-text-property (point) 'tabulated-list-id))))
#+END_SRC
**** goto-last-task :function:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
(defun chronometrist-goto-last-task ()
"In the `chronometrist' buffer, move point to the line containing the last active task."
(goto-char (point-min))
(re-search-forward (plist-get (chronometrist-last) :name) nil t)
(beginning-of-line))
#+END_SRC
**** print-keybind :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-print-keybind (command &optional description firstonly)
2019-08-06 11:36:38 +00:00
"Insert the keybindings for COMMAND.
If DESCRIPTION is non-nil, insert that too.
If FIRSTONLY is non-nil, return only the first keybinding found."
(insert
2020-09-02 13:54:13 +00:00
(format "\n% 18s - %s"
(chronometrist-format-keybinds command chronometrist-mode-map firstonly)
(if description description ""))))
#+END_SRC
**** print-non-tabular :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-print-non-tabular ()
"Print the non-tabular part of the buffer in `chronometrist'."
(with-current-buffer chronometrist-buffer-name
(let ((inhibit-read-only t)
(w "\n ")
;; (keybind-start-new (chronometrist-format-keybinds 'chronometrist-add-new-task chronometrist-mode-map))
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
(goto-char (point-max))
2020-09-02 13:54:13 +00:00
(--> (chronometrist-active-time-one-day)
(chronometrist-format-time it)
(format "%s%- 26s%s" w "Total" it)
(insert it))
(insert "\n")
(insert w (format "% 17s" "Keys") w (format "% 17s" "----"))
2019-11-29 04:41:14 +00:00
(chronometrist-print-keybind 'chronometrist-add-new-task)
(insert-text-button "start a new task" 'action #'chronometrist-add-new-task-button 'follow-link t)
(chronometrist-print-keybind 'chronometrist-toggle-task "toggle task at point")
(chronometrist-print-keybind 'chronometrist-toggle-task-no-hooks "toggle without running hooks")
(insert "\n " (format "%s %s - %s" "<numeric argument N>" keybind-toggle "toggle <N>th task"))
(chronometrist-print-keybind 'chronometrist-report)
(insert-text-button "see weekly report" 'action #'chronometrist-report 'follow-link t)
(chronometrist-print-keybind 'chronometrist-open-log)
(insert-text-button "view/edit log file" 'action #'chronometrist-open-log 'follow-link t)
2019-01-08 08:03:54 +00:00
(insert "\n"))))
#+END_SRC
**** goto-nth-task :function:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
(defun chronometrist-goto-nth-task (n)
"Move point to the line containing the Nth task.
Return the task at point, or nil if there is no corresponding
task. N must be a positive integer."
(goto-char (point-min))
(when (re-search-forward (format "^%d" n) nil t)
(beginning-of-line)
2019-11-29 04:41:14 +00:00
(chronometrist-task-at-point)))
#+END_SRC
**** refresh :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-refresh (&optional _ignore-auto _noconfirm)
2019-09-07 07:08:16 +00:00
"Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'.
The optional arguments _IGNORE-AUTO and _NOCONFIRM are ignored,
and are present solely for the sake of using this function as a
value of `revert-buffer-function'."
(let* ((window (get-buffer-window chronometrist-buffer-name t))
(point (window-point window)))
(when window
(with-current-buffer chronometrist-buffer-name
(tabulated-list-print t nil)
(chronometrist-print-non-tabular)
(chronometrist-maybe-start-timer)
(set-window-point window point)))))
#+END_SRC
**** file-state :internal:variable:
#+BEGIN_SRC emacs-lisp
2021-01-03 08:01:13 +00:00
(defvar chronometrist--file-state nil
"List containing the state of `chronometrist-file'.
`chronometrist-refresh-file' sets this to a plist in the form
2021-01-03 08:01:13 +00:00
\(:last (LAST-START LAST-END) :rest (REST-START REST-END HASH))
2021-01-02 16:41:33 +00:00
\(see `chronometrist-file-hash')
2021-01-03 08:01:13 +00:00
LAST-START and LAST-END represent the start and the end of the
last s-expression.
2021-01-03 08:01:13 +00:00
REST-START and REST-END represent the start of the file and the
2021-01-03 07:37:52 +00:00
end of the second-last s-expression.")
#+END_SRC
**** file-hash :function:
#+BEGIN_SRC emacs-lisp
2021-01-03 08:01:13 +00:00
(defun chronometrist-file-hash (&optional start end hash)
"Calculate hash of `chronometrist-file' between START and END.
START can be
a number or marker,
:before-last - the position at the start of the last s-expression
nil or any other value - the value of `point-min'.
END can be
a number or marker,
2021-01-03 07:37:52 +00:00
:before-last - the position at the end of the second-last s-expression,
2021-01-02 16:41:33 +00:00
nil or any other value - the position at the end of the last s-expression.
2021-01-03 08:01:13 +00:00
Return (START END) if HASH is nil, else (START END HASH).
Return a list in the form (A B HASH), where A and B are markers
in `chronometrist-file' describing the region for which HASH was calculated."
(chronometrist-sexp-in-file chronometrist-file
(let* ((start (cond ((number-or-marker-p start) start)
((eq :before-last start)
(goto-char (point-max))
(backward-list))
(t (point-min))))
(end (cond ((number-or-marker-p end) end)
((eq :before-last end)
(goto-char (point-max))
(backward-list 2)
(forward-list))
(t (goto-char (point-max))
(backward-list)
(forward-list)))))
2021-01-03 08:01:13 +00:00
(if hash
(--> (buffer-substring-no-properties start end)
(secure-hash 'sha1 it)
(list start end it))
(list start end)))))
#+END_SRC
**** read-from :function:
#+BEGIN_SRC emacs-lisp
2021-01-03 07:37:52 +00:00
(defun chronometrist-read-from (position)
(chronometrist-sexp-in-file chronometrist-file
(goto-char
(if (number-or-marker-p position)
position
(funcall position)))
(ignore-errors (read (current-buffer)))))
#+END_SRC
**** file-change-type :function:
2021-02-04 03:28:52 +00:00
1. rest-start rest-end last-start last-end
2. :append - rest same, last same, new expr after last-end
3. :modify - rest same, last not same, no expr after last-end
4. :remove - rest same, last not same, no expr after last-start
5. nil - rest same, last same, no expr after last-end
6. t - rest changed
2021-01-03 07:37:52 +00:00
2021-02-04 03:28:52 +00:00
#+BEGIN_SRC emacs-lisp
2021-01-03 08:01:13 +00:00
(defun chronometrist-file-change-type (state)
"Determine the type of change made to `chronometrist-file'.
2021-01-03 08:01:13 +00:00
STATE must be a plist. (see `chronometrist--file-state')
Return
:append if a new s-expression was added to the end,
2021-01-03 07:37:52 +00:00
:modify if the last s-expression was modified,
:remove if the last s-expression was removed,
nil if the contents didn't change, and
t for any other change."
2021-01-03 08:01:13 +00:00
(-let* (((last-start last-end) (plist-get state :last))
((rest-start rest-end rest-hash) (plist-get state :rest))
;; Using a hash to determine if the last expression has
;; changed can cause issues - the expression may shrink, and
;; if we try to compute the hash of the old region again, we
;; will get an args-out-of-range error. A hash will also
;; result in false negatives for whitespace/indentation
2021-01-03 07:37:52 +00:00
;; differences.
(last-same-p (--> (hash-table-keys chronometrist-events) (last it) (car it)
(gethash it chronometrist-events) (last it) (car it)
(equal it (chronometrist-read-from last-start))))
2021-01-03 07:37:52 +00:00
(file-new-length (chronometrist-sexp-in-file chronometrist-file (point-max)))
(rest-same-p (unless (< file-new-length rest-end)
(equal rest-hash
2021-01-27 07:11:01 +00:00
(cl-third (chronometrist-file-hash rest-start rest-end t))))))
2021-01-02 16:41:33 +00:00
(cond ((not rest-same-p) t)
2021-01-03 07:37:52 +00:00
(last-same-p
(when (chronometrist-read-from last-end) :append))
((not (chronometrist-read-from last-start))
:remove)
((not (chronometrist-read-from
(lambda ()
(progn (goto-char last-start)
(forward-list)))))
:modify))))
#+END_SRC
**** task-list :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-task-list ()
"Return a list of tasks from `chronometrist-file'."
(--> (chronometrist-loop-file for plist in chronometrist-file collect (plist-get plist :name))
(cl-remove-duplicates it :test #'equal)
(sort it #'string-lessp)))
#+END_SRC
**** add-to-task-list :function:
#+BEGIN_SRC emacs-lisp
2021-01-27 15:54:23 +00:00
(defun chronometrist-add-to-task-list (task)
(unless (cl-member task chronometrist-task-list :test #'equal)
(setq chronometrist-task-list
(sort (cons task chronometrist-task-list) #'string-lessp))))
#+END_SRC
**** remove-from-task-list :function:
#+BEGIN_SRC emacs-lisp
2021-01-27 15:54:23 +00:00
(defun chronometrist-remove-from-task-list (task)
(let ((count (cl-loop with count = 0
for intervals being the hash-values of chronometrist-events
do (cl-loop for interval in intervals
do (cl-incf count))
finally return count))
(position (cl-loop with count = 0
for intervals being the hash-values of chronometrist-events
when (cl-loop for interval in intervals
do (cl-incf count)
when (equal task (plist-get interval :name))
return t)
return count)))
(when (and position (= position count))
;; The only interval for TASK is the last expression
(setq chronometrist-task-list (remove task chronometrist-task-list)))))
#+END_SRC
**** refresh-file :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-refresh-file (fs-event)
"Re-read `chronometrist-file' and refresh the `chronometrist' buffer.
Argument _FS-EVENT is ignored."
(run-hooks 'chronometrist-file-change-hook)
;; (message "chronometrist - file %s" fs-event)
;; `chronometrist-file-change-type' must be run /before/ we update `chronometrist--file-state'
;; (the latter represents the old state of the file, which
;; `chronometrist-file-change-type' compares with the new one)
(-let* (((descriptor action file ...) fs-event)
(change (when chronometrist--file-state
(chronometrist-file-change-type chronometrist--file-state)))
(reset-watch (or (eq action 'deleted) (eq action 'renamed))))
;; (message "chronometrist - file change type is %s" change)
(cond ((or reset-watch (not chronometrist--file-state) (eq change t))
(when reset-watch
(file-notify-rm-watch chronometrist--fs-watch)
(setq chronometrist--fs-watch nil chronometrist--file-state nil))
(chronometrist-events-populate)
(setq chronometrist-task-list (chronometrist-task-list)))
(chronometrist--file-state
(let ((task (plist-get (chronometrist-last) :name)))
(pcase change
(:append
(chronometrist-events-update (chronometrist-sexp-last))
(chronometrist-add-to-task-list task))
(:modify
(chronometrist-events-update (chronometrist-sexp-last) t)
(chronometrist-remove-from-task-list task)
(chronometrist-add-to-task-list task))
(:remove
(let* ((date (--> (hash-table-keys chronometrist-events)
(last it)
(car it)))
(old-task (--> (gethash date chronometrist-events)
(last it)
(car it)
(plist-get it :name))))
(chronometrist-remove-from-task-list old-task)
(--> (gethash date chronometrist-events)
(-drop-last 1 it)
(puthash date it chronometrist-events))))
((pred null) nil)))))
(setq chronometrist--file-state
(list :last (chronometrist-file-hash :before-last nil)
:rest (chronometrist-file-hash nil :before-last t)))
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
(chronometrist-refresh)))
#+END_SRC
**** query-stop :function:
#+BEGIN_SRC emacs-lisp
2019-11-29 11:08:44 +00:00
(defun chronometrist-query-stop ()
"Ask the user if they would like to clock out."
(let ((task (chronometrist-current-task)))
(and task
(yes-or-no-p (format "Stop tracking time for %s? " task))
2019-11-29 11:08:44 +00:00
(chronometrist-out))
t))
#+END_SRC
**** chronometrist-in :command:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-in (task &optional _prefix)
"Clock in to TASK; record current time in `chronometrist-file'.
TASK is the name of the task, a string. PREFIX is ignored."
(interactive "P")
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
(chronometrist-sexp-new plist)
(chronometrist-refresh)))
#+END_SRC
**** chronometrist-out :command:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-out (&optional _prefix)
"Record current moment as stop time to last s-exp in `chronometrist-file'.
PREFIX is ignored."
(interactive "P")
(let ((plist (plist-put (chronometrist-last) :stop (chronometrist-format-time-iso8601))))
(chronometrist-sexp-replace-last plist)))
#+END_SRC
**** chronometrist-mode-hook :hook:normal:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-mode-hook nil
"Normal hook run at the very end of `chronometrist-mode'.")
#+END_SRC
**** list-format-transformers :extension:variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-list-format-transformers nil
"List of functions to transform `tabulated-list-format' (which see).
2021-01-04 03:14:48 +00:00
This is called with `chronometrist-run-transformers' in `chronometrist-mode', which see.
Extensions using `chronometrist-list-format-transformers' to
increase the number of columns will also need to modify the value
of `tabulated-list-entries' by using
`chronometrist-entry-transformers'.")
#+END_SRC
**** entry-transformers :extension:variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-entry-transformers nil
"List of functions to transform each entry of `tabulated-list-entries'.
2021-01-04 03:14:48 +00:00
This is called with `chronometrist-run-transformers' in `chronometrist-entries', which see.
Extensions using `chronometrist-entry-transformers' to increase
the number of columns will also need to modify the value of
`tabulated-list-format' by using
`chronometrist-list-format-transformers'.")
#+END_SRC
**** before-in-functions :hook:abnormal:
#+BEGIN_SRC emacs-lisp
2019-09-11 18:00:52 +00:00
(defvar chronometrist-before-in-functions nil
2019-11-29 04:41:14 +00:00
"Functions to run before a task is clocked in.
2019-08-06 11:36:38 +00:00
Each function in this hook must accept a single argument, which
2019-11-29 04:41:14 +00:00
is the name of the task to be clocked-in.
2019-11-29 04:41:14 +00:00
The commands `chronometrist-toggle-task-button',
`chronometrist-add-new-task-button', `chronometrist-toggle-task',
and `chronometrist-add-new-task' will run this hook.")
#+END_SRC
**** after-in-functions :hook:abnormal:
#+BEGIN_SRC emacs-lisp
2019-09-11 18:00:52 +00:00
(defvar chronometrist-after-in-functions nil
2019-11-29 04:41:14 +00:00
"Functions to run after a task is clocked in.
2019-09-11 18:00:52 +00:00
Each function in this hook must accept a single argument, which
2019-11-29 04:41:14 +00:00
is the name of the task to be clocked-in.
2019-09-11 18:00:52 +00:00
2019-11-29 04:41:14 +00:00
The commands `chronometrist-toggle-task-button',
`chronometrist-add-new-task-button', `chronometrist-toggle-task',
and `chronometrist-add-new-task' will run this hook.")
#+END_SRC
**** before-out-functions :hook:abnormal:
#+BEGIN_SRC emacs-lisp
2019-09-11 18:00:52 +00:00
(defvar chronometrist-before-out-functions nil
2019-11-29 04:41:14 +00:00
"Functions to run before a task is clocked out.
Each function in this hook must accept a single argument, which
2019-11-29 04:41:14 +00:00
is the name of the task to be clocked out of.
2019-11-29 04:41:14 +00:00
The task will be stopped only if all functions in this list
return a non-nil value.")
#+END_SRC
**** after-out-functions :hook:abnormal:
#+BEGIN_SRC emacs-lisp
2019-09-11 18:00:52 +00:00
(defvar chronometrist-after-out-functions nil
2019-11-29 04:41:14 +00:00
"Functions to run after a task is clocked out.
Each function in this hook must accept a single argument, which
2019-11-29 04:41:14 +00:00
is the name of the task to be clocked out of.")
#+END_SRC
**** file-change-hook :hook:normal:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-file-change-hook nil
"Functions to be run after `chronometrist-file' is changed on disk.")
#+END_SRC
**** run-functions-and-clock-in :function:
#+BEGIN_SRC emacs-lisp
2019-09-11 18:00:52 +00:00
(defun chronometrist-run-functions-and-clock-in (task)
"Run hooks and clock in to TASK."
(run-hook-with-args 'chronometrist-before-in-functions task)
(chronometrist-in task)
(run-hook-with-args 'chronometrist-after-in-functions task))
#+END_SRC
**** run-functions-and-clock-out :function:
#+BEGIN_SRC emacs-lisp
2019-09-11 18:00:52 +00:00
(defun chronometrist-run-functions-and-clock-out (task)
"Run hooks and clock out of TASK."
2019-09-11 18:00:52 +00:00
(when (run-hook-with-args-until-failure 'chronometrist-before-out-functions task)
(chronometrist-out)
(run-hook-with-args 'chronometrist-after-out-functions task)))
#+END_SRC
**** chronometrist-mode-map :keymap:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-mode-map
(let ((map (make-sparse-keymap)))
2019-11-29 04:41:14 +00:00
(define-key map (kbd "RET") #'chronometrist-toggle-task)
(define-key map (kbd "M-RET") #'chronometrist-toggle-task-no-hooks)
(define-key map (kbd "l") #'chronometrist-open-log)
(define-key map (kbd "r") #'chronometrist-report)
2019-11-29 04:41:14 +00:00
(define-key map [mouse-1] #'chronometrist-toggle-task)
(define-key map [mouse-3] #'chronometrist-toggle-task-no-hooks)
2019-11-29 04:41:14 +00:00
(define-key map (kbd "a") #'chronometrist-add-new-task)
map)
"Keymap used by `chronometrist-mode'.")
#+END_SRC
**** chronometrist-mode :major:mode:
#+BEGIN_SRC emacs-lisp
(define-derived-mode chronometrist-mode tabulated-list-mode "Chronometrist"
"Major mode for `chronometrist'."
2018-09-11 12:39:38 +00:00
(make-local-variable 'tabulated-list-format)
(--> [("#" 3 t) ("Task" 25 t) ("Time" 10 t) ("Active" 10 t)]
2021-01-04 03:14:48 +00:00
(chronometrist-run-transformers chronometrist-list-format-transformers it)
(setq tabulated-list-format it))
2018-09-11 12:39:38 +00:00
(make-local-variable 'tabulated-list-entries)
(setq tabulated-list-entries 'chronometrist-entries)
2018-09-11 12:39:38 +00:00
(make-local-variable 'tabulated-list-sort-key)
2019-11-29 04:41:14 +00:00
(setq tabulated-list-sort-key '("Task" . nil))
(tabulated-list-init-header)
(setq revert-buffer-function #'chronometrist-refresh)
(run-hooks 'chronometrist-mode-hook))
#+END_SRC
**** toggle-task-button :function:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
(defun chronometrist-toggle-task-button (_button)
"Button action to toggle a task.
2019-10-31 15:13:50 +00:00
Argument _BUTTON is for the purpose of using this as a button
2019-10-31 15:13:50 +00:00
action, and is ignored."
(when current-prefix-arg
(chronometrist-goto-nth-task (prefix-numeric-value current-prefix-arg)))
(let ((current (chronometrist-current-task))
2019-11-29 04:41:14 +00:00
(at-point (chronometrist-task-at-point)))
;; clocked in + point on current = clock out
2019-11-29 04:41:14 +00:00
;; clocked in + point on some other task = clock out, clock in to task
;; clocked out = clock in
(when current
(chronometrist-run-functions-and-clock-out current))
(unless (equal at-point current)
(chronometrist-run-functions-and-clock-in at-point))))
#+END_SRC
**** add-new-task-button :function:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
(defun chronometrist-add-new-task-button (_button)
"Button action to add a new task.
2019-10-31 15:13:50 +00:00
Argument _BUTTON is for the purpose of using this as a button
2019-10-31 15:13:50 +00:00
action, and is ignored."
(let ((current (chronometrist-current-task)))
(when current
(chronometrist-run-functions-and-clock-out current))
2019-09-07 16:48:19 +00:00
(let ((task (read-from-minibuffer "New task name: " nil nil nil nil nil t)))
(chronometrist-run-functions-and-clock-in task))))
#+END_SRC
**** toggle-task :command:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
;; TODO - if clocked in and point not on a task, just clock out
(defun chronometrist-toggle-task (&optional prefix inhibit-hooks)
2019-11-29 04:41:14 +00:00
"Start or stop the task at point.
2019-11-29 04:41:14 +00:00
If there is no task at point, do nothing.
2019-08-06 11:36:38 +00:00
2019-11-29 04:41:14 +00:00
With numeric prefix argument PREFIX, toggle the Nth task in
the buffer. If there is no corresponding task, do nothing.
If INHIBIT-HOOKS is non-nil, the hooks
`chronometrist-before-in-functions',
`chronometrist-after-in-functions',
`chronometrist-before-out-functions', and
`chronometrist-after-out-functions' will not be run."
(interactive "P")
(let* ((empty-file (chronometrist-common-file-empty-p chronometrist-file))
(nth (when prefix (chronometrist-goto-nth-task prefix)))
(at-point (chronometrist-task-at-point))
(target (or nth at-point))
(current (chronometrist-current-task))
(in-function (if inhibit-hooks
#'chronometrist-in
#'chronometrist-run-functions-and-clock-in))
(out-function (if inhibit-hooks
#'chronometrist-out
#'chronometrist-run-functions-and-clock-out)))
;; do not run hooks - chronometrist-add-new-task will do it
(cond (empty-file (chronometrist-add-new-task))
;; What should we do if the user provides an invalid
;; argument? Currently - nothing.
2018-09-26 17:06:26 +00:00
((and prefix (not nth)))
2019-11-29 04:41:14 +00:00
(target ;; do nothing if there's no task at point
2018-09-26 17:06:26 +00:00
;; clocked in + target is current = clock out
2019-11-29 04:41:14 +00:00
;; clocked in + target is some other task = clock out, clock in to task
2018-09-26 17:06:26 +00:00
;; clocked out = clock in
(when current
(funcall out-function current))
2018-09-26 17:06:26 +00:00
(unless (equal target current)
(funcall in-function target))))))
#+END_SRC
**** toggle-task-no-hooks :command:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-toggle-task-no-hooks (&optional prefix)
2020-06-26 21:17:27 +00:00
"Like `chronometrist-toggle-task', but don't run hooks.
2019-08-06 11:36:38 +00:00
2019-11-29 04:41:14 +00:00
With numeric prefix argument PREFIX, toggle the Nth task. If there
is no corresponding task, do nothing."
(interactive "P")
(chronometrist-toggle-task prefix t))
#+END_SRC
**** add-new-task :command:
#+BEGIN_SRC emacs-lisp
2019-11-29 04:41:14 +00:00
(defun chronometrist-add-new-task ()
"Add a new task."
(interactive)
2019-11-29 04:41:14 +00:00
(chronometrist-add-new-task-button nil))
#+END_SRC
**** chronometrist :command:
#+BEGIN_SRC emacs-lisp
2019-08-04 19:27:22 +00:00
;;;###autoload
(defun chronometrist (&optional arg)
2019-10-31 13:33:18 +00:00
"Display the user's tasks and the time spent on them today.
2019-08-06 11:36:38 +00:00
2019-10-31 13:33:18 +00:00
Based on their timelog file `chronometrist-file'. This is the
2019-08-06 11:36:38 +00:00
'listing command' for `chronometrist-mode'.
2019-08-06 11:36:38 +00:00
If numeric argument ARG is 1, run `chronometrist-report'.
If numeric argument ARG is 2, run `chronometrist-statistics'."
(interactive "P")
2019-10-25 07:47:27 +00:00
(chronometrist-migrate-check)
(let ((buffer (get-buffer-create chronometrist-buffer-name))
2020-05-14 04:18:37 +00:00
(w (save-excursion
(get-buffer-window chronometrist-buffer-name t))))
(cond
(arg (cl-case arg
(1 (chronometrist-report))
(2 (chronometrist-statistics))))
(w (with-current-buffer buffer
(setq chronometrist--point (point))
(kill-buffer chronometrist-buffer-name)))
(t (with-current-buffer buffer
(cond ((or (not (file-exists-p chronometrist-file))
(chronometrist-common-file-empty-p chronometrist-file))
;; first run
2020-05-14 04:17:20 +00:00
(chronometrist-common-create-file)
(let ((inhibit-read-only t))
(chronometrist-common-clear-buffer buffer)
(insert "Welcome to Chronometrist! Hit RET to ")
2019-11-29 04:41:14 +00:00
(insert-text-button "start a new task."
'action #'chronometrist-add-new-task-button
'follow-link t)
(chronometrist-mode)
(switch-to-buffer buffer)))
(t (chronometrist-mode)
(when chronometrist-hide-cursor
(make-local-variable 'cursor-type)
(setq cursor-type nil)
(hl-line-mode))
(switch-to-buffer buffer)
2019-08-08 08:19:06 +00:00
(if (hash-table-keys chronometrist-events)
(chronometrist-refresh)
(chronometrist-refresh-file nil))
(if chronometrist--point
(goto-char chronometrist--point)
2019-11-29 04:41:14 +00:00
(chronometrist-goto-last-task))))
(unless chronometrist--fs-watch
(setq chronometrist--fs-watch
(file-notify-add-watch chronometrist-file '(change) #'chronometrist-refresh-file))))))))
2018-09-11 12:27:34 +00:00
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp
(provide 'chronometrist)
#+END_SRC
2021-02-04 03:28:52 +00:00
# Local Variables:
# eval: (visual-fill-column-mode -1)
# End: