Move definitions from chronometrist-migrate.el to chronometrist.org
This commit is contained in:
parent
909ed00202
commit
9bdf9fe47b
|
@ -47,19 +47,16 @@
|
|||
;; * 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
|
||||
|
||||
;;; Code:
|
||||
(require 'filenotify)
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
(require 'dash)
|
||||
(require 'ts)
|
||||
|
||||
(require 'chronometrist-key-values)
|
||||
(require 'chronometrist-queries)
|
||||
(require 'chronometrist-migrate)
|
||||
(require 'chronometrist-sexp)
|
||||
|
||||
(require 'dash)
|
||||
(require 'cl-lib)
|
||||
(require 'ts)
|
||||
(require 'chronometrist-time)
|
||||
|
||||
(eval-when-compile
|
||||
|
@ -68,24 +65,20 @@
|
|||
(autoload 'chronometrist-maybe-start-timer "chronometrist-timer" nil t)
|
||||
(autoload 'chronometrist-report "chronometrist-report" nil t)
|
||||
(autoload 'chronometrist-statistics "chronometrist-statistics" nil t)
|
||||
|
||||
(defcustom chronometrist-sexp-pretty-print-function #'chronometrist-plist-pp
|
||||
"Function used to pretty print plists in `chronometrist-file'.
|
||||
Like `pp', it must accept an OBJECT and optionally a
|
||||
STREAM (which is the value of `current-buffer')."
|
||||
:type 'function)
|
||||
|
||||
(define-derived-mode chronometrist-sexp-mode
|
||||
;; fundamental-mode
|
||||
emacs-lisp-mode
|
||||
"chronometrist-sexp")
|
||||
|
||||
(defmacro chronometrist-sexp-in-file (file &rest body)
|
||||
"Run BODY in a buffer visiting FILE, restoring point afterwards."
|
||||
(declare (indent defun) (debug t))
|
||||
`(with-current-buffer (find-file-noselect ,file)
|
||||
(save-excursion ,@body)))
|
||||
|
||||
(defmacro chronometrist-loop-file (for expr in file &rest loop-clauses)
|
||||
"`cl-loop' LOOP-CLAUSES over s-expressions in FILE, in reverse.
|
||||
VAR is bound to each s-expression."
|
||||
|
@ -104,27 +97,23 @@ VAR is bound to each s-expression."
|
|||
(setq ,expr (ignore-errors (read (current-buffer))))
|
||||
(backward-list))
|
||||
,@loop-clauses)))
|
||||
|
||||
;;;; Queries
|
||||
(defun chronometrist-sexp-open-log ()
|
||||
"Open `chronometrist-file' in another window."
|
||||
(find-file-other-window chronometrist-file)
|
||||
(goto-char (point-max)))
|
||||
|
||||
(defun chronometrist-sexp-last ()
|
||||
"Return last s-expression from `chronometrist-file'."
|
||||
(chronometrist-sexp-in-file chronometrist-file
|
||||
(goto-char (point-max))
|
||||
(backward-list)
|
||||
(ignore-errors (read (current-buffer)))))
|
||||
|
||||
(defun chronometrist-sexp-current-task ()
|
||||
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
||||
(let ((last-event (chronometrist-sexp-last)))
|
||||
(if (plist-member last-event :stop)
|
||||
nil
|
||||
(plist-get last-event :name))))
|
||||
|
||||
(defun chronometrist-sexp-events-populate ()
|
||||
"Populate hash table `chronometrist-events'.
|
||||
The data is acquired from `chronometrist-file'.
|
||||
|
@ -156,7 +145,6 @@ were none."
|
|||
(list new-value))
|
||||
chronometrist-events)))
|
||||
(unless (zerop index) index))))
|
||||
|
||||
;;;; Modifications
|
||||
(defun chronometrist-sexp-create-file ()
|
||||
"Create `chronometrist-file' if it doesn't already exist."
|
||||
|
@ -165,7 +153,6 @@ were none."
|
|||
(goto-char (point-min))
|
||||
(insert ";;; -*- mode: chronometrist-sexp; -*-")
|
||||
(write-file chronometrist-file))))
|
||||
|
||||
(cl-defun chronometrist-sexp-new (plist)
|
||||
"Add new PLIST at the end of `chronometrist-file'."
|
||||
(chronometrist-sexp-in-file chronometrist-file
|
||||
|
@ -176,13 +163,11 @@ were none."
|
|||
(unless (bolp) (insert "\n"))
|
||||
(funcall chronometrist-sexp-pretty-print-function plist (current-buffer))
|
||||
(save-buffer)))
|
||||
|
||||
(defun chronometrist-sexp-delete-list (&optional arg)
|
||||
"Delete ARG lists after point."
|
||||
(let ((point-1 (point)))
|
||||
(forward-sexp (or arg 1))
|
||||
(delete-region point-1 (point))))
|
||||
|
||||
(defun chronometrist-sexp-replace-last (plist)
|
||||
"Replace the last s-expression in `chronometrist-file' with PLIST."
|
||||
(chronometrist-sexp-in-file chronometrist-file
|
||||
|
@ -192,7 +177,6 @@ were none."
|
|||
(chronometrist-sexp-delete-list)
|
||||
(funcall chronometrist-sexp-pretty-print-function plist (current-buffer))
|
||||
(save-buffer)))
|
||||
|
||||
(defun chronometrist-sexp-reindent-buffer ()
|
||||
"Reindent the current buffer.
|
||||
This is meant to be run in `chronometrist-file' when using the s-expression backend."
|
||||
|
@ -207,13 +191,11 @@ This is meant to be run in `chronometrist-file' when using the s-expression back
|
|||
(funcall chronometrist-sexp-pretty-print-function expr (current-buffer))
|
||||
(insert "\n")
|
||||
(unless (eobp) (insert "\n")))))
|
||||
|
||||
(defvar chronometrist-events (make-hash-table :test #'equal)
|
||||
"Each key is a date in the form (YEAR MONTH DAY).
|
||||
Values are lists containing events, where each event is a list in
|
||||
the form (:name \"NAME\" :tags (TAGS) <key value pairs> ...
|
||||
:start TIME :stop TIME).")
|
||||
|
||||
(defun chronometrist-day-start (timestamp)
|
||||
"Get start of day (according to `chronometrist-day-start-time') for TIMESTAMP.
|
||||
TIMESTAMP must be a time string in the ISO-8601 format.
|
||||
|
@ -230,7 +212,6 @@ Return value is a time value (see `current-time')."
|
|||
(reverse it)
|
||||
(append it timestamp-date-list)
|
||||
(apply #'encode-time it))))
|
||||
|
||||
(defun chronometrist-events-maybe-split (event)
|
||||
"Split EVENT if it spans midnight.
|
||||
Return a list of two events if EVENT was split, else nil."
|
||||
|
@ -252,7 +233,6 @@ Return a list of two events if EVENT was split, else nil."
|
|||
(-> event-2
|
||||
(plist-put :start second-start)
|
||||
(plist-put :stop second-stop))))))))
|
||||
|
||||
(defun chronometrist-events-populate ()
|
||||
"Clear hash table `chronometrist-events' (which see) and populate it.
|
||||
The data is acquired from `chronometrist-file'.
|
||||
|
@ -261,7 +241,6 @@ Return final number of events read from file, or nil if there
|
|||
were none."
|
||||
(clrhash chronometrist-events)
|
||||
(chronometrist-sexp-events-populate))
|
||||
|
||||
(defun chronometrist-events-update (plist &optional replace)
|
||||
"Add PLIST to the end of `chronometrist-events'.
|
||||
If REPLACE is non-nil, replace the last event with PLIST."
|
||||
|
@ -272,7 +251,6 @@ If REPLACE is non-nil, replace the last event with PLIST."
|
|||
(--> (if replace (-drop-last 1 events-today) events-today)
|
||||
(append it (list plist))
|
||||
(puthash date it chronometrist-events))))
|
||||
|
||||
(defun chronometrist-events-subset (start end)
|
||||
"Return a subset of `chronometrist-events'.
|
||||
The subset will contain values between dates START and END (both
|
||||
|
@ -288,11 +266,9 @@ treated as though their time is 00:00:00."
|
|||
(puthash key value subset)))
|
||||
chronometrist-events)
|
||||
subset))
|
||||
|
||||
(defgroup chronometrist nil
|
||||
"A time tracker with a nice UI."
|
||||
:group 'applications)
|
||||
|
||||
(defcustom chronometrist-file
|
||||
(locate-user-emacs-file "chronometrist.sexp")
|
||||
"Default path and name of the Chronometrist database.
|
||||
|
@ -317,35 +293,28 @@ TIME must be an ISO-8601 time string.
|
|||
\(The square brackets here refer to optional elements, not
|
||||
vectors.\)"
|
||||
:type 'file)
|
||||
|
||||
(defcustom chronometrist-buffer-name "*Chronometrist*"
|
||||
"The name of the buffer created by `chronometrist'."
|
||||
:type 'string)
|
||||
|
||||
(defcustom chronometrist-hide-cursor nil
|
||||
"If non-nil, hide the cursor and only highlight the current line in the `chronometrist' buffer."
|
||||
:type 'boolean)
|
||||
|
||||
(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)
|
||||
|
||||
(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))
|
||||
|
||||
(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)
|
||||
|
||||
(defvar chronometrist--point nil)
|
||||
|
||||
(defun chronometrist-open-log (&optional _button)
|
||||
"Open `chronometrist-file' in another window.
|
||||
|
||||
|
@ -353,22 +322,18 @@ Argument _BUTTON is for the purpose of using this command as a
|
|||
button action."
|
||||
(interactive)
|
||||
(chronometrist-sexp-open-log))
|
||||
|
||||
(defun chronometrist-common-create-file ()
|
||||
"Create `chronometrist-file' if it doesn't already exist."
|
||||
(chronometrist-sexp-create-file))
|
||||
|
||||
(defun chronometrist-task-active? (task)
|
||||
"Return t if TASK is currently clocked in, else nil."
|
||||
(equal (chronometrist-current-task) task))
|
||||
|
||||
(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))
|
||||
|
||||
(defun chronometrist-run-transformers (transformers arg)
|
||||
"Run TRANSFORMERS with ARG.
|
||||
TRANSFORMERS should be a list of functions (F₁ ... Fₙ), each of
|
||||
|
@ -382,7 +347,6 @@ Return the value returned by Fₙ."
|
|||
(dolist (fn transformers arg)
|
||||
(setq arg (funcall fn arg)))
|
||||
arg))
|
||||
|
||||
(defun chronometrist-entries ()
|
||||
"Create entries to be displayed in the buffer created by `chronometrist', in the format specified by `tabulated-list-entries'."
|
||||
;; HACK - these calls are commented out, because `chronometrist-entries' is
|
||||
|
@ -400,20 +364,17 @@ Return the value returned by Fₙ."
|
|||
(--> (vector index task-button task-time indicator)
|
||||
(list task it)
|
||||
(chronometrist-run-transformers chronometrist-entry-transformers it))))))
|
||||
|
||||
(defun chronometrist-task-at-point ()
|
||||
"Return the task at point in the `chronometrist' buffer, or nil if there is no task at point."
|
||||
(save-excursion
|
||||
(beginning-of-line)
|
||||
(when (re-search-forward "[0-9]+ +" nil t)
|
||||
(get-text-property (point) 'tabulated-list-id))))
|
||||
|
||||
(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))
|
||||
|
||||
(defun chronometrist-print-keybind (command &optional description firstonly)
|
||||
"Insert the keybindings for COMMAND.
|
||||
If DESCRIPTION is non-nil, insert that too.
|
||||
|
@ -422,7 +383,6 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
(format "\n% 18s - %s"
|
||||
(chronometrist-format-keybinds command chronometrist-mode-map firstonly)
|
||||
(if description description ""))))
|
||||
|
||||
(defun chronometrist-print-non-tabular ()
|
||||
"Print the non-tabular part of the buffer in `chronometrist'."
|
||||
(with-current-buffer chronometrist-buffer-name
|
||||
|
@ -447,7 +407,6 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
(chronometrist-print-keybind 'chronometrist-open-log)
|
||||
(insert-text-button "view/edit log file" 'action #'chronometrist-open-log 'follow-link t)
|
||||
(insert "\n"))))
|
||||
|
||||
(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
|
||||
|
@ -456,7 +415,6 @@ task. N must be a positive integer."
|
|||
(when (re-search-forward (format "^%d" n) nil t)
|
||||
(beginning-of-line)
|
||||
(chronometrist-task-at-point)))
|
||||
|
||||
(defun chronometrist-refresh (&optional _ignore-auto _noconfirm)
|
||||
"Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'.
|
||||
|
||||
|
@ -471,7 +429,6 @@ value of `revert-buffer-function'."
|
|||
(chronometrist-print-non-tabular)
|
||||
(chronometrist-maybe-start-timer)
|
||||
(set-window-point window point)))))
|
||||
|
||||
(defvar chronometrist--file-state nil
|
||||
"List containing the state of `chronometrist-file'.
|
||||
`chronometrist-refresh-file' sets this to a plist in the form
|
||||
|
@ -485,7 +442,6 @@ last s-expression.
|
|||
|
||||
REST-START and REST-END represent the start of the file and the
|
||||
end of the second-last s-expression.")
|
||||
|
||||
(defun chronometrist-file-hash (&optional start end hash)
|
||||
"Calculate hash of `chronometrist-file' between START and END.
|
||||
START can be
|
||||
|
@ -521,7 +477,6 @@ in `chronometrist-file' describing the region for which HASH was calculated."
|
|||
(secure-hash 'sha1 it)
|
||||
(list start end it))
|
||||
(list start end)))))
|
||||
|
||||
(defun chronometrist-read-from (position)
|
||||
(chronometrist-sexp-in-file chronometrist-file
|
||||
(goto-char
|
||||
|
@ -529,7 +484,6 @@ in `chronometrist-file' describing the region for which HASH was calculated."
|
|||
position
|
||||
(funcall position)))
|
||||
(ignore-errors (read (current-buffer)))))
|
||||
|
||||
(defun chronometrist-file-change-type (state)
|
||||
"Determine the type of change made to `chronometrist-file'.
|
||||
STATE must be a plist. (see `chronometrist--file-state')
|
||||
|
@ -565,18 +519,15 @@ Return
|
|||
(progn (goto-char last-start)
|
||||
(forward-list)))))
|
||||
:modify))))
|
||||
|
||||
(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)))
|
||||
|
||||
(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))))
|
||||
|
||||
(defun chronometrist-remove-from-task-list (task)
|
||||
(let ((count (cl-loop with count = 0
|
||||
for intervals being the hash-values of chronometrist-events
|
||||
|
@ -593,55 +544,53 @@ Return
|
|||
(when (and position (= position count))
|
||||
;; The only interval for TASK is the last expression
|
||||
(setq chronometrist-task-list (remove task chronometrist-task-list)))))
|
||||
|
||||
(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)))
|
||||
|
||||
(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)))
|
||||
(defun chronometrist-query-stop ()
|
||||
"Ask the user if they would like to clock out."
|
||||
(let ((task (chronometrist-current-task)))
|
||||
|
@ -649,7 +598,6 @@ Argument _FS-EVENT is ignored."
|
|||
(yes-or-no-p (format "Stop tracking time for %s? " task))
|
||||
(chronometrist-out))
|
||||
t))
|
||||
|
||||
(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."
|
||||
|
@ -657,17 +605,14 @@ TASK is the name of the task, a string. PREFIX is ignored."
|
|||
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
|
||||
(chronometrist-sexp-new plist)
|
||||
(chronometrist-refresh)))
|
||||
|
||||
(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)))
|
||||
|
||||
(defvar chronometrist-mode-hook nil
|
||||
"Normal hook run at the very end of `chronometrist-mode'.")
|
||||
|
||||
(defvar chronometrist-list-format-transformers nil
|
||||
"List of functions to transform `tabulated-list-format' (which see).
|
||||
This is called with `chronometrist-run-transformers' in `chronometrist-mode', which see.
|
||||
|
@ -676,7 +621,6 @@ 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'.")
|
||||
|
||||
(defvar chronometrist-entry-transformers nil
|
||||
"List of functions to transform each entry of `tabulated-list-entries'.
|
||||
This is called with `chronometrist-run-transformers' in `chronometrist-entries', which see.
|
||||
|
@ -685,7 +629,6 @@ 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'.")
|
||||
|
||||
(defvar chronometrist-before-in-functions nil
|
||||
"Functions to run before a task is clocked in.
|
||||
Each function in this hook must accept a single argument, which
|
||||
|
@ -694,7 +637,6 @@ is the name of the task to be clocked-in.
|
|||
The commands `chronometrist-toggle-task-button',
|
||||
`chronometrist-add-new-task-button', `chronometrist-toggle-task',
|
||||
and `chronometrist-add-new-task' will run this hook.")
|
||||
|
||||
(defvar chronometrist-after-in-functions nil
|
||||
"Functions to run after a task is clocked in.
|
||||
Each function in this hook must accept a single argument, which
|
||||
|
@ -703,7 +645,6 @@ is the name of the task to be clocked-in.
|
|||
The commands `chronometrist-toggle-task-button',
|
||||
`chronometrist-add-new-task-button', `chronometrist-toggle-task',
|
||||
and `chronometrist-add-new-task' will run this hook.")
|
||||
|
||||
(defvar chronometrist-before-out-functions nil
|
||||
"Functions to run before a task is clocked out.
|
||||
Each function in this hook must accept a single argument, which
|
||||
|
@ -711,27 +652,22 @@ is the name of the task to be clocked out of.
|
|||
|
||||
The task will be stopped only if all functions in this list
|
||||
return a non-nil value.")
|
||||
|
||||
(defvar chronometrist-after-out-functions nil
|
||||
"Functions to run after a task is clocked out.
|
||||
Each function in this hook must accept a single argument, which
|
||||
is the name of the task to be clocked out of.")
|
||||
|
||||
(defvar chronometrist-file-change-hook nil
|
||||
"Functions to be run after `chronometrist-file' is changed on disk.")
|
||||
|
||||
(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))
|
||||
|
||||
(defun chronometrist-run-functions-and-clock-out (task)
|
||||
"Run hooks and clock out of TASK."
|
||||
(when (run-hook-with-args-until-failure 'chronometrist-before-out-functions task)
|
||||
(chronometrist-out)
|
||||
(run-hook-with-args 'chronometrist-after-out-functions task)))
|
||||
|
||||
(defvar chronometrist-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "RET") #'chronometrist-toggle-task)
|
||||
|
@ -743,7 +679,6 @@ is the name of the task to be clocked out of.")
|
|||
(define-key map (kbd "a") #'chronometrist-add-new-task)
|
||||
map)
|
||||
"Keymap used by `chronometrist-mode'.")
|
||||
|
||||
(define-derived-mode chronometrist-mode tabulated-list-mode "Chronometrist"
|
||||
"Major mode for `chronometrist'."
|
||||
(make-local-variable 'tabulated-list-format)
|
||||
|
@ -757,7 +692,6 @@ is the name of the task to be clocked out of.")
|
|||
(tabulated-list-init-header)
|
||||
(setq revert-buffer-function #'chronometrist-refresh)
|
||||
(run-hooks 'chronometrist-mode-hook))
|
||||
|
||||
(defun chronometrist-toggle-task-button (_button)
|
||||
"Button action to toggle a task.
|
||||
|
||||
|
@ -774,7 +708,6 @@ action, and is ignored."
|
|||
(chronometrist-run-functions-and-clock-out current))
|
||||
(unless (equal at-point current)
|
||||
(chronometrist-run-functions-and-clock-in at-point))))
|
||||
|
||||
(defun chronometrist-add-new-task-button (_button)
|
||||
"Button action to add a new task.
|
||||
|
||||
|
@ -785,7 +718,6 @@ action, and is ignored."
|
|||
(chronometrist-run-functions-and-clock-out current))
|
||||
(let ((task (read-from-minibuffer "New task name: " nil nil nil nil nil t)))
|
||||
(chronometrist-run-functions-and-clock-in task))))
|
||||
|
||||
;; TODO - if clocked in and point not on a task, just clock out
|
||||
(defun chronometrist-toggle-task (&optional prefix inhibit-hooks)
|
||||
"Start or stop the task at point.
|
||||
|
@ -825,7 +757,6 @@ If INHIBIT-HOOKS is non-nil, the hooks
|
|||
(funcall out-function current))
|
||||
(unless (equal target current)
|
||||
(funcall in-function target))))))
|
||||
|
||||
(defun chronometrist-toggle-task-no-hooks (&optional prefix)
|
||||
"Like `chronometrist-toggle-task', but don't run hooks.
|
||||
|
||||
|
@ -833,12 +764,10 @@ With numeric prefix argument PREFIX, toggle the Nth task. If there
|
|||
is no corresponding task, do nothing."
|
||||
(interactive "P")
|
||||
(chronometrist-toggle-task prefix t))
|
||||
|
||||
(defun chronometrist-add-new-task ()
|
||||
"Add a new task."
|
||||
(interactive)
|
||||
(chronometrist-add-new-task-button nil))
|
||||
|
||||
;;;###autoload
|
||||
(defun chronometrist (&optional arg)
|
||||
"Display the user's tasks and the time spent on them today.
|
||||
|
@ -891,16 +820,13 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
|||
|
||||
(defvar chronometrist-task-list nil
|
||||
"List of tasks in `chronometrist-file'.")
|
||||
|
||||
(defvar chronometrist--fs-watch nil
|
||||
"Filesystem watch object.
|
||||
Used to prevent more than one watch being added for the same
|
||||
file.")
|
||||
|
||||
(defun chronometrist-current-task ()
|
||||
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
||||
(chronometrist-sexp-current-task))
|
||||
|
||||
(cl-defun chronometrist-format-time (seconds &optional (blank " "))
|
||||
"Format SECONDS as a string suitable for display in Chronometrist buffers.
|
||||
SECONDS must be a positive integer.
|
||||
|
@ -925,18 +851,15 @@ supplied, 3 spaces are used."
|
|||
(format "%2d" s)
|
||||
(format "%02d" s))))
|
||||
(concat h m s)))))
|
||||
|
||||
(defun chronometrist-common-file-empty-p (file)
|
||||
"Return t if FILE is empty."
|
||||
(let ((size (elt (file-attributes file) 7)))
|
||||
(if (zerop size) t nil)))
|
||||
|
||||
(defun chronometrist-common-clear-buffer (buffer)
|
||||
"Clear the contents of BUFFER."
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-min))
|
||||
(delete-region (point-min) (point-max))))
|
||||
|
||||
(defun chronometrist-format-keybinds (command map &optional firstonly)
|
||||
"Return the keybindings for COMMAND in MAP as a string.
|
||||
If FIRSTONLY is non-nil, return only the first keybinding found."
|
||||
|
@ -948,7 +871,6 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
(-take 2)
|
||||
(-interpose ", ")
|
||||
(apply #'concat))))
|
||||
|
||||
(defun chronometrist-events->ts-pairs (events)
|
||||
"Convert EVENTS to a list of ts struct pairs (see `ts.el').
|
||||
|
||||
|
@ -962,7 +884,6 @@ EVENTS must be a list of valid Chronometrist property lists (see
|
|||
(chronometrist-iso-timestamp->ts stop)
|
||||
(ts-now))))
|
||||
(cons start stop))))
|
||||
|
||||
(defun chronometrist-ts-pairs->durations (ts-pairs)
|
||||
"Return the durations represented by TS-PAIRS.
|
||||
TS-PAIRS is a list of pairs, where each element is a ts struct (see `ts.el').
|
||||
|
@ -972,7 +893,6 @@ Return seconds as an integer, or 0 if TS-PAIRS is nil."
|
|||
(cl-loop for pair in ts-pairs collect
|
||||
(ts-diff (cdr pair) (car pair)))
|
||||
0))
|
||||
|
||||
(defun chronometrist-previous-week-start (ts)
|
||||
"Find the previous `chronometrist-report-week-start-day' from TS.
|
||||
|
||||
|
@ -988,5 +908,99 @@ TS must be a ts struct (see `ts.el')."
|
|||
until (= week-start (ts-dow ts))
|
||||
do (ts-decf (ts-day ts))
|
||||
finally return ts))
|
||||
(defvar chronometrist-migrate-table (make-hash-table))
|
||||
;; TODO - support other timeclock codes (currently only "i" and "o"
|
||||
;; are supported.)
|
||||
(defun chronometrist-migrate-populate (in-file)
|
||||
"Read data from IN-FILE to `chronometrist-migrate-table'.
|
||||
|
||||
IN-FILE should be a file in the format supported by timeclock.el.
|
||||
See `timeclock-log-data' for a description."
|
||||
(clrhash chronometrist-migrate-table)
|
||||
(with-current-buffer (find-file-noselect in-file)
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(let ((key-counter 0))
|
||||
(while (not (eobp))
|
||||
(let* ((event-string (buffer-substring-no-properties (point-at-bol)
|
||||
(point-at-eol)))
|
||||
(event-list (split-string event-string "[ /:]"))
|
||||
(code (cl-first event-list))
|
||||
(date-time (--> event-list
|
||||
(seq-drop it 1)
|
||||
(seq-take it 6)
|
||||
(mapcar #'string-to-number it)
|
||||
(reverse it)
|
||||
(apply #'encode-time it)
|
||||
(chronometrist-format-time-iso8601 it)))
|
||||
(project-or-comment
|
||||
(replace-regexp-in-string
|
||||
(rx (and (or "i" "o") " "
|
||||
(and (= 4 digit) "/" (= 2 digit) "/" (= 2 digit) " ")
|
||||
(and (= 2 digit) ":" (= 2 digit) ":" (= 2 digit))
|
||||
(opt " ")))
|
||||
""
|
||||
event-string)))
|
||||
(pcase code
|
||||
("i"
|
||||
(cl-incf key-counter)
|
||||
(puthash key-counter
|
||||
`(:name ,project-or-comment :start ,date-time)
|
||||
chronometrist-migrate-table))
|
||||
("o"
|
||||
(--> (gethash key-counter chronometrist-migrate-table)
|
||||
(append it
|
||||
`(:stop ,date-time)
|
||||
(when (and (stringp project-or-comment)
|
||||
(not
|
||||
(string= project-or-comment "")))
|
||||
`(:comment ,project-or-comment)))
|
||||
(puthash key-counter it chronometrist-migrate-table)))))
|
||||
(forward-line)
|
||||
(goto-char (point-at-bol))))
|
||||
nil)))
|
||||
(defvar timeclock-file)
|
||||
|
||||
(defun chronometrist-migrate-timelog-file->sexp-file (&optional in-file out-file)
|
||||
"Migrate your existing `timeclock-file' to the Chronometrist file format.
|
||||
|
||||
IN-FILE and OUT-FILE, if provided, are used as input and output
|
||||
file names respectively."
|
||||
(interactive `(,(if (featurep 'timeclock)
|
||||
(read-file-name (concat "timeclock file (default: "
|
||||
timeclock-file
|
||||
"): ")
|
||||
user-emacs-directory
|
||||
timeclock-file t)
|
||||
(read-file-name (concat "timeclock file: ")
|
||||
user-emacs-directory
|
||||
nil t))
|
||||
,(read-file-name (concat "Output file (default: "
|
||||
(locate-user-emacs-file "chronometrist.sexp")
|
||||
"): ")
|
||||
user-emacs-directory
|
||||
(locate-user-emacs-file "chronometrist.sexp"))))
|
||||
(when (if (file-exists-p out-file)
|
||||
(yes-or-no-p (concat "Output file "
|
||||
out-file
|
||||
" already exists - overwrite? "))
|
||||
t)
|
||||
(let ((output (find-file-noselect out-file)))
|
||||
(with-current-buffer output
|
||||
(chronometrist-common-clear-buffer output)
|
||||
(chronometrist-migrate-populate in-file)
|
||||
(maphash (lambda (_key value)
|
||||
(chronometrist-plist-pp value output)
|
||||
(insert "\n\n"))
|
||||
chronometrist-migrate-table)
|
||||
(save-buffer)))))
|
||||
(defun chronometrist-migrate-check ()
|
||||
"Offer to import data from `timeclock-file' if `chronometrist-file' does not exist."
|
||||
(when (and (bound-and-true-p timeclock-file)
|
||||
(not (file-exists-p chronometrist-file)))
|
||||
(if (yes-or-no-p (format (concat "Chronometrist v0.3+ uses a new file format;"
|
||||
" import data from %s ? ")
|
||||
timeclock-file))
|
||||
(chronometrist-migrate-timelog-file->sexp-file timeclock-file chronometrist-file)
|
||||
(message "You can migrate later using `chronometrist-migrate-timelog-file->sexp-file'."))))
|
||||
(provide 'chronometrist)
|
||||
|
|
Reference in New Issue