diff --git a/doc/manual.info b/doc/manual.info index 98f7562..54e695f 100644 --- a/doc/manual.info +++ b/doc/manual.info @@ -275,7 +275,25 @@ Most changes, whether made through user-editing or by Chronometrist commands, ha When @samp{chronometrist-refresh-file} is run by the file system watcher, it uses @samp{chronometrist-file-hash} to assign indices and a hash to @samp{chronometrist--file-state}. The next time the file changes, @samp{chronometrist-file-change-type} compares this state to the current state of the file to determine the type of change made. -A particular concern is when a change results in the addition of a new task, or removal of the last event of a task - i.e. @samp{chronometrist-task-list} should be updated. +Challenges - +@enumerate +@item +Correctly detecting the type of change +@item +Updating @samp{chronometrist-task-list} and the Chronometrist buffer, when a new task is added or the last interval for a task is removed (v0.6.4) +@item +Handling changes made to an active interval after midnight +@itemize +@item +use the date from the plist's @samp{:start} timestamp instead of the date today +@item +:append - normally, add to table; for spanning intervals, invalid operation +@item +:modify - normally, replace in table; for spanning intervals, split and replace +@item +:remove - normally, remove from table; for spanning intervals, split and remove +@end itemize +@end enumerate @end enumerate @node Midnight-spanning events @@ -731,9 +749,7 @@ Function - chronometrist-events-maybe-split (event) @item Function - chronometrist-events-populate () @item -Function - chronometrist-events-add (plist) -@item -Function - chronometrist-events-replace-last (plist) +Function - chronometrist-events-update (plist &optional replace) @item Function - chronometrist-events-subset (start end) @itemize diff --git a/doc/manual.org b/doc/manual.org index 9338817..f994a8d 100644 --- a/doc/manual.org +++ b/doc/manual.org @@ -103,6 +103,15 @@ Most changes, whether made through user-editing or by Chronometrist commands, ha When =chronometrist-refresh-file= is run by the file system watcher, it uses =chronometrist-file-hash= to assign indices and a hash to =chronometrist--file-state=. The next time the file changes, =chronometrist-file-change-type= compares this state to the current state of the file to determine the type of change made. +Challenges - +1. Correctly detecting the type of change +2. Updating =chronometrist-task-list= and the Chronometrist buffer, when a new task is added or the last interval for a task is removed (v0.6.4) +3. Handling changes made to an active interval after midnight + * use the date from the plist's =:start= timestamp instead of the date today + * :append - normally, add to table; for spanning intervals, invalid operation + * :modify - normally, replace in table; for spanning intervals, split and replace + * :remove - normally, remove from table; for spanning intervals, split and remove + ** Midnight-spanning events :PROPERTIES: :DESCRIPTION: Events starting on one day and ending on another @@ -302,9 +311,8 @@ Each of these has a corresponding function to clear it and fill it with values - * commented out, unused 4. Function - chronometrist-events-maybe-split (event) 5. Function - chronometrist-events-populate () -6. Function - chronometrist-events-add (plist) -7. Function - chronometrist-events-replace-last (plist) -8. Function - chronometrist-events-subset (start end) +6. Function - chronometrist-events-update (plist &optional replace) +7. Function - chronometrist-events-subset (start end) * ts ts -> hash-table ** chronometrist-migrate.el diff --git a/elisp/chronometrist-events.el b/elisp/chronometrist-events.el index aa07a15..6942de5 100644 --- a/elisp/chronometrist-events.el +++ b/elisp/chronometrist-events.el @@ -28,14 +28,12 @@ (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) ... :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. Return value is a time value (see `current-time')." @@ -51,45 +49,6 @@ Return value is a time value (see `current-time')." (append it timestamp-date-list) (apply #'encode-time it)))) -;; (defun chronometrist-file-clean () -;; "Clean `chronometrist-file' so that events can be processed accurately. -;; NOTE - currently unused. - -;; This function splits midnight-spanning intervals into two. It -;; must be called before running `chronometrist-populate'. - -;; It returns t if the table was modified, else nil." -;; (let ((buffer (find-file-noselect chronometrist-file)) -;; modified -;; expr) -;; (with-current-buffer buffer -;; (save-excursion -;; (goto-char (point-min)) -;; (while (setq expr (ignore-errors (read (current-buffer)))) -;; (when (plist-get expr :stop) -;; (let ((split-time (chronometrist-midnight-spanning-p (plist-get expr :start) -;; (plist-get expr :stop)))) -;; (when split-time -;; (let ((first-start (plist-get (cl-first split-time) :start)) -;; (first-stop (plist-get (cl-first split-time) :stop)) -;; (second-start (plist-get (cl-second split-time) :start)) -;; (second-stop (plist-get (cl-second split-time) :stop))) -;; (backward-list 1) -;; (chronometrist-sexp-delete-list) -;; (-> expr -;; (plist-put :start first-start) -;; (plist-put :stop first-stop) -;; (chronometrist-plist-pp buffer)) -;; (when (looking-at-p "\n\n") -;; (delete-char 2)) -;; (-> expr -;; (plist-put :start second-start) -;; (plist-put :stop second-stop) -;; (chronometrist-plist-pp buffer)) -;; (setq modified t)))))) -;; (save-buffer))) -;; modified)) - (defun chronometrist-events-maybe-split (event) "Split EVENT if it spans midnight. @@ -127,25 +86,17 @@ were none." (clrhash chronometrist-events) (chronometrist-sexp-events-populate)) -(defun chronometrist-events-add (plist) - "Add new PLIST at the end of `chronometrist-events'." - (let* ((date-today (format-time-string "%Y-%m-%d")) - (events-today (gethash date-today chronometrist-events))) - (--> (list plist) - (append events-today it) - (puthash date-today it chronometrist-events)))) +(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." + (let* ((date (->> (plist-get plist :start) + (chronometrist-iso-timestamp->ts ) + (ts-format "%F" ))) + (events-today (gethash date chronometrist-events))) + (--> (if replace (-drop-last 1 events-today) events-today) + (append it (list plist)) + (puthash date it chronometrist-events)))) -(defun chronometrist-events-replace-last (plist) - "Replace the last plist in `chronometrist-events' with PLIST." - (let* ((date-today (format-time-string "%Y-%m-%d")) - (events-today (gethash date-today chronometrist-events))) - (--> (reverse events-today) - (cdr it) - (append (list plist) it) - (reverse it) - (puthash date-today it chronometrist-events)))) - -;; to be replaced by plist-query (defun chronometrist-events-subset (start end) "Return a subset of `chronometrist-events'. diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 509d8f5..e7730e4 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -395,50 +395,55 @@ Return ;; The only interval for TASK is the last expression (setq chronometrist-task-list (remove task chronometrist-task-list))))) -(defun chronometrist-refresh-file (_fs-event) +(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) - (aif chronometrist--file-state - (let ((change (chronometrist-file-change-type it)) - (task (plist-get (chronometrist-last) :name))) - ;; (message "chronometrist - file change type is %s" change) - (pcase change - (:append - (chronometrist-events-add (chronometrist-sexp-last)) - (chronometrist-add-to-task-list task)) - (:modify - (chronometrist-events-replace-last (chronometrist-sexp-last)) - (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) - (_ (chronometrist-events-populate)))) - (chronometrist-events-populate) - ;; re-create task list - (--> (chronometrist-loop-file for plist in chronometrist-file collect (plist-get plist :name)) - (cl-remove-duplicates it :test #'equal) - (sort it #'string-lessp) - (setq chronometrist-task-list it))) - (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)) + (-let* (((descriptor action file ...) fs-event) + (change (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 + (setq chronometrist--fs-watch nil chronometrist--file-state nil)) + (chronometrist-events-populate) + ;; re-create task list + (--> (chronometrist-loop-file for plist in chronometrist-file collect (plist-get plist :name)) + (cl-remove-duplicates it :test #'equal) + (sort it #'string-lessp) + (setq chronometrist-task-list it))) + (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."