From a04e9ebfba52e33b87a8f09eab36918b82f04078 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 16:46:24 +0530 Subject: [PATCH 01/56] Add TODO entry for the Third Time system --- TODO.org | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/TODO.org b/TODO.org index e9bd883..dd9b459 100644 --- a/TODO.org +++ b/TODO.org @@ -881,3 +881,37 @@ Records not used for time tracking, but to store data associated with a date or :END: 1. [ ] default suggested input backend should be the active backend 2. [ ] conserve file local variables prop line + +* Support for the Third Time System +:PROPERTIES: +:CREATED: 2022-02-10T14:12:12+0530 +:END: +See branch "third" + +[[https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work][Third Time: a better way to work]] +#+BEGIN_QUOTE +Lastly, here in one place are all the steps for using Third Time: +1. Note the time, or start a stopwatch +2. /Work/ until you want or need to break +3. Divide how long you’ve just worked by 3 (or use your chosen fraction), and add any minutes left over from previous breaks +4. Set an alarm for that long +5. /Break/ until the alarm goes off, or you decide to resume work +6. If you resume early, note how much time was left, to add to your next break +7. Go back to step 1. + +Additional rules: ++ If you have to stop work for a /non-work-related interruption/, start a break immediately. ++ You can (optionally) take a /big break/ for lunch and/or dinner, lasting as long as you like. Set an alarm at the start for when you’ll resume work. A big break uses up any saved break minutes, so you can’t carry them over to the afternoon/evening. ++ /Avoid taking other unearned breaks/ if possible — so try to do personal tasks during normal or big breaks, or before/after your work day. +#+END_QUOTE + +1. =chronometrist-third-fraction= +2. =chronometrist-third-break-time= +3. on clock out, increment =break-time= and start timed notification +4. on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero. +5. persist =break-time= between Emacs sessions + +Example flow +1. work for 30m +2. clock out - add 10m to =break-time= +3. clock in after a 5m break - subtract 5m from =break-time= From 409aa0337e957bea66689b896e68e6c77bfd73bd Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 16:47:28 +0530 Subject: [PATCH 02/56] Add newlines between headings --- elisp/chronometrist.org | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 8142183..c93046e 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -16,6 +16,7 @@ I hope this book—when completed—passes Tim Daly's [[https://www.youtube.com/ :PROPERTIES: :DESCRIPTION: The design, the implementation, and a little history :END: + ** Why I wrote Chronometrist It probably started off with a desire for introspection and self-awareness - what did I do all day? How much time have I spent doing X? It is also a tool to help me stay focused on a single task at a time, and to not go overboard and work on something for 7 hours straight. @@ -119,12 +120,14 @@ These mess up data consumption in all sorts of unforeseen ways, especially inter 2. finding the time spent on a task on a given day - if the day's intervals used for this contain a midnight-spanning interval, you'll have inaccurate results - it will include yesterday's time from the interval as well as today's. There are a few different approaches of dealing with them. (Currently, Chronometrist uses #3.) + *** Check the code of the first event of the day (timeclock format) :PROPERTIES: :DESCRIPTION: When the code of the first event in the day is "o", it's a midnight-spanning event. :END: + Advantage - very simple to detect + Disadvantage - "in" and "out" events must be represented separately + *** Split them at the file level + Advantage - operation is performed only once for each such event + simpler data-consuming code + reduced post-parsing load. + What happens when the user changes their day-start-time? The split-up events are now split wrongly, and the second event may get split /again./ @@ -134,9 +137,11 @@ Possible solutions - 2. Add a :split tag to split events. It can denote that the next event was originally a part of this one. 3. Re-check and update the file when the day-start-time changes. - Possible with ~add-variable-watcher~ or ~:custom-set~ in Customize (thanks bpalmer) + *** Split them at the hash-table-level Handled by ~chronometrist-sexp-events-populate~ + Advantage - simpler data-consuming code. + *** Split them at the data-consumer level (e.g. when calculating time for one day/getting events for one day) + Advantage - reduced repetitive post-parsing load. @@ -170,6 +175,7 @@ A quick description, starting from the first time [[* chronometrist-report][=chr :CUSTOM_ID: explanation-literate-programming :END: The shift from a bunch of Elisp files to a single Org literate program was born out of frustration with programs stored as text files, which are expensive to restructure (especially in the presence of a VCS). While some dissatisfactions remain, I generally prefer the outcome - tree and source-block folding, tags, properties, and =org-match= have made it trivial to get different views of the program, and literate programming may allow me to express the "explanation" documentation in the same context as the program, without having to try to link between documentation and source. + *** Tangling At first, I tried tangling. Back when I used =benchmark.el= to test it, =org-babel-tangle= took about 30 seconds to tangle this file. Thus, I wrote a little sed one-liner (in the file-local variables) to do the tangling, which was nearly instant. It emitted anything between lines matching the exact strings ="#+BEGIN_SRC emacs-lisp"= and ="#+END_SRC"= - @@ -219,6 +225,7 @@ Further details are stored in properties - 2. :VALUE: list|hash table|... * for functions, this is the return value 3. :STATE: + *** TODO Issues [40%] 1. [X] When opening this file, Emacs may freeze at the prompt for file-local variable values; if so, C-g will quit the prompt, and permanently marking them as safe will make the freezing stop. [fn:3] 2. [ ] I like =visual-fill-column-mode= for natural language, but I don't want it applied to code blocks. =polymode.el= may hold answers. @@ -229,6 +236,7 @@ Further details are stored in properties - * A workaround is to press =M-o M-o= [fn:3] No longer a problem since we switched to =literate-elisp= + ** Currently-Used Time Formats :PROPERTIES: :CUSTOM_ID: explanation-time-formats @@ -237,22 +245,27 @@ Further details are stored in properties - *** ts ts.el struct * Used by nearly all internal functions + *** iso-timestamp ="YYYY-MM-DDTHH:MM:SSZ"= * Used in the s-expression file format * Read by chronometrist-sexp-events-populate * Used in the plists in the chronometrist-events hash table values + *** iso-date ="YYYY-MM-DD"= * Used as hash table keys in chronometrist-events - can't use ts structs for keys, you'd have to make a hash table predicate which uses ts= + *** seconds integer seconds as duration * Used for most durations * May be changed to floating point to allow larger durations. The minimum range of =most-positive-fixnum= is 536870911, which seems to be enough to represent durations of 17 years. * Used for update intervals (chronometrist-update-interval, chronometrist-change-update-interval) + *** minutes integer minutes as duration * Used by [[https://tildegit.org/contrapunctus/chronometrist-goal][chronometrist-goal]] (chronometrist-goals-list, chronometrist-get-goal) - minutes seems like the ideal unit for users to enter + *** list-duration =(hours minute seconds)= * Only returned by =chronometrist-seconds-to-hms=, called by =chronometrist-format-time= @@ -490,6 +503,7 @@ supplied, 3 spaces are used." (format "%02d" s)))) (concat h m s))))) #+END_SRC + *** file-empty-p :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-file-empty-p (file) @@ -511,6 +525,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found." (-interpose ", ") (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. #+BEGIN_SRC emacs-lisp @@ -520,12 +535,14 @@ If FIRSTONLY is non-nil, return only the first keybinding found." The default is midnight, i.e. \"00:00:00\"." :type 'string) #+END_SRC + *** week-start-day :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-report-week-start-day "Sunday" "The day used for start of week by `chronometrist-report'." :type 'string) #+END_SRC + *** weekday-number-alist :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-report-weekday-number-alist @@ -539,6 +556,7 @@ The default is midnight, i.e. \"00:00:00\"." "Alist in the form (\"NAME\" . NUMBER), where \"NAME\" is the name of a weekday and NUMBER its associated number." :type 'alist) #+END_SRC + *** previous-week-start :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-previous-week-start (ts) @@ -556,6 +574,7 @@ TS must be a ts struct (see `ts.el')." do (ts-decf (ts-day ts)) finally return ts)) #+END_SRC + *** plist-remove :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-plist-remove (plist &rest keys) @@ -608,6 +627,7 @@ 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: #+BEGIN_SRC emacs-lisp (defun chronometrist-plist-p (list) @@ -705,6 +725,7 @@ Reading directly from the file could be difficult, especially when your most com The data from =chronometrist-events= is used by most (all?) interval-consuming functions, but is never written to the user's file itself. [fn:4] it might be the case that the [[#program-backend][file format]] is not suited to our most frequent operation... + *** reset :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-reset () @@ -728,6 +749,7 @@ Return value is a ts struct (see `ts.el')." (ts-apply :hour h :minute m :second s (chronometrist-iso-to-ts timestamp)))) #+END_SRC + **** tests #+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test (ert-deftest chronometrist-apply-time () @@ -817,6 +839,7 @@ If REPLACE is non-nil, replace the last interval with PLIST." (last it) (car it)))) #+END_SRC + *** events-subset :reader: :PROPERTIES: :VALUE: hash table @@ -839,6 +862,7 @@ treated as though their time is 00:00:00." hash-table) subset)) #+END_SRC + *** task-time-one-day :reader: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend))) @@ -910,6 +934,7 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by :dow dow :tz-offset utcoff)))) #+END_SRC + **** tests #+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test (ert-deftest chronometrist-iso-to-ts () @@ -921,6 +946,7 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by :hour 1 :minute 1 :second 1)))) #+END_SRC + *** events-to-durations :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-events-to-durations (events) @@ -941,6 +967,7 @@ Return 0 if EVENTS is nil." (ts-diff stop-ts start-ts))) 0)) #+END_SRC + *** date-iso :function: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-date-iso (&optional (ts (ts-now))) @@ -967,6 +994,7 @@ Optional argument UNIX-TIME should be a time value (see ;; Note - this assumes that an event never crosses >1 day. This seems ;; sufficient for all conceivable cases. #+END_SRC + *** FIXME split-time :reader: It does not matter here that the =:stop= dates in the returned plists are different from the =:start=, because =chronometrist-events-populate= uses only the date segment of the =:start= values as hash table keys. (The hash table keys form the rest of the program's notion of "days", and that of which plists belong to which day.) @@ -1036,6 +1064,7 @@ SECONDS must be a positive integer." (h (/ seconds 3600))) (list h m s))) #+END_SRC + *** interval :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-interval (event) @@ -1046,6 +1075,7 @@ EVENT should be a plist (see `chronometrist-file')." (time-subtract (parse-iso8601-time-string stop) (parse-iso8601-time-string start)))) #+END_SRC + *** format-duration-long :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-format-duration-long (seconds) @@ -1065,6 +1095,7 @@ SECONDS is less than 60, return a blank string." hours hour-string minutes minute-string))))) #+END_SRC + **** tests #+BEGIN_SRC emacs-lisp :tangle ../tests/chronometrist-tests.el :load test (ert-deftest chronometrist-format-duration-long () @@ -1080,6 +1111,7 @@ SECONDS is less than 60, return a blank string." (should (equal (chronometrist-format-duration-long 7260) "2 hours, 1 minute")) (should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes"))) #+END_SRC + ** Plist pretty-printing :PROPERTIES: :CUSTOM_ID: program-pretty-printer @@ -1106,6 +1138,7 @@ Point is placed at the end of the space." (delete-region (match-beginning 0) (match-end 0)) (insert " "))) #+END_SRC + *** column :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-pp-column () @@ -1113,12 +1146,14 @@ Point is placed at the end of the space." 0 means point is at the beginning of the line." (- (point) (point-at-bol))) #+END_SRC + *** pair-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-pp-pair-p (cons) "Return non-nil if CONS is a pair, i.e. (CAR . CDR)." (and (listp cons) (not (listp (cdr cons))))) #+END_SRC + *** alist-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-pp-alist-p (list) @@ -1129,6 +1164,7 @@ considers it an alist." (when (listp list) (cl-loop for elt in list thereis (chronometrist-pp-pair-p elt)))) #+END_SRC + *** plist-group-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-plist-group-p (list) @@ -1151,6 +1187,7 @@ that point is after the first opening parenthesis." when (keywordp sexp) maximize (length (symbol-name sexp))))) #+END_SRC + *** indent-sexp :function: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-pp-indent-sexp (sexp &optional (right-indent 0)) @@ -1158,6 +1195,7 @@ that point is after the first opening parenthesis." (format (concat "% -" (number-to-string right-indent) "s") sexp)) #+END_SRC + *** buffer :writer: It might help to make =in-sublist= an integer representing depth, instead of a boolean. But at the moment, it's getting the job done. @@ -1289,10 +1327,12 @@ IN-SUBLIST, if non-nil, means point is inside an inner list." (princ (chronometrist-pp-to-string object) (or stream standard-output))) #+END_SRC + ** Backends :PROPERTIES: :CUSTOM_ID: program-backend :END: + *** chronometrist-file :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-file @@ -1788,6 +1828,7 @@ These can be implemented in terms of the minimal protocol above. `(with-current-buffer (find-file-noselect ,file) (save-excursion ,@body))) #+END_SRC + **** pre-read-check :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-sexp-pre-read-check (buffer) @@ -1868,6 +1909,7 @@ expression first)." (should (= 1256 last-start)) (should (= 1426 last-end)))) #+END_SRC + **** file-change-type :reader: + rest-start - start of first sexp + rest-end - end of second last sexp @@ -2189,6 +2231,7 @@ Boilerplate for updating state between file operations in tests. (list :last (chronometrist-file-hash :before-last nil nil ,file) :rest (chronometrist-file-hash nil :before-last t ,file))))) #+END_SRC + **** backend :class: #+BEGIN_SRC emacs-lisp (defclass chronometrist-plist-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin) @@ -2786,6 +2829,7 @@ We apply the same hack as in the [[<>][insert]] me output-file) (message "Conversion aborted.")))) #+END_SRC + *** table :variable: :PROPERTIES: :VALUE: hash table @@ -2793,6 +2837,7 @@ We apply the same hack as in the [[<>][insert]] me #+BEGIN_SRC emacs-lisp (defvar chronometrist-migrate-table (make-hash-table)) #+END_SRC + *** EXTEND populate :writer: :PROPERTIES: :STATE: chronometrist-migrate-table @@ -2886,6 +2931,7 @@ file names respectively." chronometrist-migrate-table) (save-buffer))))) #+END_SRC + *** check :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-migrate-check () @@ -2898,6 +2944,7 @@ file names respectively." (chronometrist-migrate-timelog-file-to-sexp-file timeclock-file chronometrist-file) (message "You can migrate later using `chronometrist-migrate-timelog-file-to-sexp-file'.")))) #+END_SRC + ** Timer Instead of the Emacs convention of pressing ~g~ to update, we keep buffers updated with a timer. @@ -2911,10 +2958,12 @@ Note - sometimes, when hacking or dealing with errors, timers may result in subt This is not guaranteed to be accurate - see (info \"(elisp)Timers\")." :type 'integer) #+END_SRC + *** timer-object :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist--timer-object nil) #+END_SRC + *** timer-hook :hook:custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-timer-hook nil @@ -2999,6 +3048,7 @@ modified." All four of these use [[info:elisp#Tabulated List Mode][=(info "(elisp)Tabulated List Mode")=]]. Each of them also contains a "-print-non-tabular" function, which prints the non-tabular parts of the buffer. 1. [ ] There is some duplication between the four frontend commands, e.g. all four act as toggles for their respective buffers, point preservation, etc. + *** Chronometrist :PROPERTIES: :CUSTOM_ID: program-frontend-chronometrist @@ -3008,12 +3058,14 @@ All four of these use [[info:elisp#Tabulated List Mode][=(info "(elisp)Tabulated 1. [X] Define hooks with defcustom instead of defvar 2. [ ] Change abnormal hooks to normal hooks 3. [ ] midnight-spanning plist not displayed (may have to do with partial updates) + **** 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: I have not yet gotten this to work as well as I wanted. @@ -3022,6 +3074,7 @@ I have not yet gotten this to work as well as I wanted. "If non-nil, hide the cursor and only highlight the current line in the `chronometrist' buffer." :type 'boolean) #+END_SRC + **** activity-indicator :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-activity-indicator "*" @@ -3030,10 +3083,12 @@ Can be a string to be displayed, or a function which returns this string. The default is \"*\"" :type '(choice string function)) #+END_SRC + **** point :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist--point nil) #+END_SRC + **** open-log :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-open-log (&optional _button) @@ -3090,11 +3145,13 @@ Return the value returned by Fₙ." See `tabulated-list-format'." :type '(vector)) #+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 + **** schema-transformers :extension:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-schema-transformers nil @@ -3278,6 +3335,7 @@ refresh the `chronometrist' buffer." (chronometrist-out)) t)) #+END_SRC + **** chronometrist-in :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-in (task &optional _prefix) @@ -3288,6 +3346,7 @@ TASK is the name of the task, a string. PREFIX is ignored." (chronometrist-insert (chronometrist-active-backend) plist) (chronometrist-refresh))) #+END_SRC + **** chronometrist-out :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-out (&optional _prefix) @@ -3298,6 +3357,7 @@ PREFIX is ignored." (plist (plist-put latest :stop (chronometrist-format-time-iso8601)))) (chronometrist-replace-last (chronometrist-active-backend) plist))) #+END_SRC + **** run-functions-and-clock-in :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-run-functions-and-clock-in (task) @@ -3306,6 +3366,7 @@ PREFIX is ignored." (chronometrist-in task) (run-hook-with-args 'chronometrist-after-in-functions task)) #+END_SRC + **** run-functions-and-clock-out :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-run-functions-and-clock-out (task) @@ -3314,6 +3375,7 @@ PREFIX is ignored." (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 @@ -3334,6 +3396,7 @@ PREFIX is ignored." map) "Keymap used by `chronometrist-mode'.") #+END_SRC + **** chronometrist-menu :menu: #+BEGIN_SRC emacs-lisp (easy-menu-define chronometrist-menu chronometrist-mode-map @@ -3359,6 +3422,7 @@ PREFIX is ignored." ["Reset state" chronometrist-reset] ["Import/export data" chronometrist-migrate])) #+END_SRC + **** chronometrist-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-mode tabulated-list-mode "Chronometrist" @@ -3374,6 +3438,7 @@ PREFIX is ignored." (setq revert-buffer-function #'chronometrist-refresh) (run-hooks 'chronometrist-mode-hook)) #+END_SRC + **** toggle-task-button :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-toggle-task-button (_button) @@ -3392,6 +3457,7 @@ action, and is ignored." (unless (equal at-point current) (chronometrist-run-functions-and-clock-in at-point)))) #+END_SRC + **** add-new-task-button :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-add-new-task-button (_button) @@ -3404,6 +3470,7 @@ action, and is ignored." (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 ;; TODO - if clocked in and point not on a task, just clock out @@ -3572,18 +3639,21 @@ run `chronometrist-statistics'." *** Report **** TODO [0%] 1. [ ] preserve point when clicking buttons + **** report :custom:group: #+BEGIN_SRC emacs-lisp (defgroup chronometrist-report nil "Weekly report for the `chronometrist' time tracker." :group 'chronometrist) #+END_SRC + **** buffer-name :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-report-buffer-name "*Chronometrist-Report*" "The name of the buffer created by `chronometrist-report'." :type 'string) #+END_SRC + **** ui-date :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report--ui-date nil @@ -3591,16 +3661,19 @@ run `chronometrist-statistics'." A value of nil means the current week. Otherwise, it must be a date in the form \"YYYY-MM-DD\".") #+END_SRC + **** ui-week-dates :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report--ui-week-dates nil "List of dates currently displayed by `chronometrist-report'. Each date is a list containing calendrical information (see (info \"(elisp)Time Conversion\"))") #+END_SRC + **** point :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report--point nil) #+END_SRC + **** date-to-dates-in-week :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-date-to-dates-in-week (first-date-in-week) @@ -3611,6 +3684,7 @@ FIRST-DATE-IN-WEEK must be a ts struct representing the first date." (cl-loop for i from 0 to 6 collect (ts-adjust 'day i first-date-in-week))) #+END_SRC + **** date-to-week-dates :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-date-to-week-dates () @@ -3624,6 +3698,7 @@ The first date is the first occurrence of (chronometrist-previous-week-start) (chronometrist-report-date-to-dates-in-week))) #+END_SRC + **** rows :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-rows () @@ -3645,6 +3720,7 @@ The first date is the first occurrence of duration-strings ;; vconcat converts lists to vectors total-duration))))) #+END_SRC + **** print-keybind :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-print-keybind (command &optional description firstonly) @@ -3657,6 +3733,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found." " - " (if description description ""))) #+END_SRC + **** CLEANUP print-non-tabular :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-print-non-tabular () @@ -3694,6 +3771,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found." (chronometrist-report-print-keybind 'chronometrist-open-log) (insert-text-button "open log file" 'action #'chronometrist-open-log 'follow-link t))) #+END_SRC + **** REVIEW refresh :procedure: Merge this into `chronometrist-refresh-file', while moving the -refresh call to the call site? @@ -3708,6 +3786,7 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to (chronometrist-maybe-start-timer) (set-window-point w p)))) #+END_SRC + **** refresh-file :writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-refresh-file (fs-event) @@ -3716,6 +3795,7 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to (chronometrist-on-change (chronometrist-active-backend) fs-event) (chronometrist-report-refresh)) #+END_SRC + **** report-mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-report-mode-map @@ -3735,6 +3815,7 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to map) "Keymap used by `chronometrist-report-mode'.") #+END_SRC + **** report-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-report-mode tabulated-list-mode "Chronometrist-Report" @@ -3791,6 +3872,7 @@ current week. Otherwise, display data from the week specified by (chronometrist-report-refresh-file nil) (goto-char (or chronometrist-report--point 1))))))) #+END_SRC + **** report-previous-week :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-previous-week (arg) @@ -3809,6 +3891,7 @@ With prefix argument ARG, move back ARG weeks." (kill-buffer) (chronometrist-report t)) #+END_SRC + **** report-next-week :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-report-next-week (arg) @@ -3827,6 +3910,7 @@ With prefix argument ARG, move forward ARG weeks." (kill-buffer) (chronometrist-report t))) #+END_SRC + *** Statistics **** statistics :custom:group: #+BEGIN_SRC emacs-lisp @@ -3834,12 +3918,14 @@ With prefix argument ARG, move forward ARG weeks." "Statistics buffer for the `chronometrist' time tracker." :group 'chronometrist) #+END_SRC + **** buffer-name :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-statistics-buffer-name "*Chronometrist-Statistics*" "The name of the buffer created by `chronometrist-statistics'." :type 'string) #+END_SRC + **** ui-state :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics--ui-state nil @@ -3859,14 +3945,17 @@ weekly/monthly/yearly respectively. :START and :END are the start and end of the date range to be displayed. They must be ts structs (see `ts.el').") #+END_SRC + **** point :internal:variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics--point nil) #+END_SRC + **** mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics-mode-map) #+END_SRC + **** count-average-time-spent :function: #+BEGIN_SRC emacs-lisp (cl-defun chronometrist-statistics-count-average-time-spent (task &optional (backend (chronometrist-active-backend))) @@ -3884,6 +3973,7 @@ displayed. They must be ts structs (see `ts.el').") (/ (-reduce #'+ per-day-time-list) days) 0))) #+END_SRC + **** rows-internal :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-rows-internal (table) @@ -3910,6 +4000,7 @@ reduced to the desired range using (content (vector task active-days active-percent average-time))) (list task content)))) #+END_SRC + **** TEST rows :reader: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-rows () @@ -3930,6 +4021,7 @@ reduced to the desired range using (setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end)) (chronometrist-statistics-rows-internal ht)))))) #+END_SRC + **** print-keybind :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-print-keybind (command &optional description firstonly) @@ -3943,6 +4035,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found." " - " (if description description ""))) #+END_SRC + **** print-non-tabular :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-print-non-tabular () @@ -3961,6 +4054,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found." (ts-format "%F" (plist-get chronometrist-statistics--ui-state :start)) (ts-format "%F" (plist-get chronometrist-statistics--ui-state :end)))))) #+END_SRC + **** refresh :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-refresh (&optional _ignore-auto _noconfirm) @@ -3978,6 +4072,7 @@ value of `revert-buffer-function'." (chronometrist-maybe-start-timer) (set-window-point w p)))) #+END_SRC + **** mode-map :keymap: #+BEGIN_SRC emacs-lisp (defvar chronometrist-statistics-mode-map @@ -3988,6 +4083,7 @@ value of `revert-buffer-function'." map) "Keymap used by `chronometrist-statistics-mode'.") #+END_SRC + **** statistics-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-statistics-mode tabulated-list-mode "Chronometrist-Statistics" @@ -4046,6 +4142,7 @@ specified by `chronometrist-statistics--ui-state'." (switch-to-buffer buffer) (chronometrist-statistics-refresh)))))) #+END_SRC + **** previous-range :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-previous-range (arg) @@ -4066,6 +4163,7 @@ If ARG is a numeric argument, go back that many times." (kill-buffer) (chronometrist-statistics t))) #+END_SRC + **** next-range :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-statistics-next-range (arg) @@ -4086,6 +4184,7 @@ If ARG is a numeric argument, go forward that many times." (kill-buffer) (chronometrist-statistics t))) #+END_SRC + *** Details =chronometrist= displays the total time spent on a task - but what were the details? That's where =chronometrist-details= comes in - to display details of recorded time intervals for a given day, in a format terser and more informative than the plists in the file. @@ -4120,6 +4219,7 @@ If ARG is a numeric argument, go forward that many times." (format "*%s*" chronometrist-details-buffer-name-base))) #+END_SRC + **** display-tags :custom:variable: If the value of this variable is a function and the string it returns contains a newline, the results may be undesirable...but hardly unrecoverable, so try it and see, if you wish. @@ -4259,6 +4359,7 @@ using `chronometrist-details-schema-transformers'.") map)) #+END_SRC + **** chronometrist-details-menu :menu: #+BEGIN_SRC emacs-lisp (easy-menu-define chronometrist-details-menu chronometrist-details-mode-map @@ -4270,6 +4371,7 @@ using `chronometrist-details-schema-transformers'.") ["View/edit log file" chronometrist-open-log] ["Reset state" chronometrist-reset])) #+END_SRC + **** chronometrist-details-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-details-mode tabulated-list-mode "Details" @@ -4295,6 +4397,7 @@ BUFFER-OR-NAME must be an existing buffer." (tabulated-list-print))) #+END_SRC + **** chronometrist-details :command: #+BEGIN_SRC emacs-lisp (defun chronometrist-details () @@ -4333,6 +4436,7 @@ range.") string)) #+END_SRC + **** intervals-for-range :reader: This is basically like [[#program-data-structures-events-subset][chronometrist-events-subset]], but returns a list instead of a hash table. Might replace one with the other in the future. From f94ab9643baf1fc57ab6d24f210dddfb4b6ce577 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 17:03:08 +0530 Subject: [PATCH 03/56] Add scratch code --- Makefile | 76 +++++++++++++++++++++++++++++++++++ elisp/chronometrist-third.el | 22 ++++++++++ elisp/chronometrist-third.org | 36 +++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 Makefile create mode 100644 elisp/chronometrist-third.el create mode 100644 elisp/chronometrist-third.org diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..461172d --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +.phony: all setup tangle compile lint + +all: clean-elc manual.md setup tangle compile lint + +setup: + emacs --batch --eval="(package-initialize)" \ + --eval="(mapcar #'package-install '(indent-lint package-lint relint))" + +manual.md: + emacs -q -Q --batch --eval="(require 'ox-md)" \ + "manual.org" -f 'org-md-export-to-markdown' + +# No -q or -Q without ORG_PATH - if the user has a newer version of +# Org, we want to use it. +tangle: + cd elisp/ && \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'ob-tangle))" \ + --eval='(org-babel-tangle-file "chronometrist.org")' \ + --eval='(org-babel-tangle-file "chronometrist-key-values.org")' \ + --eval='(org-babel-tangle-file "chronometrist-spark.org")' \ + --eval='(org-babel-tangle-file "chronometrist-third.org")' ; \ + cd .. + +compile: tangle + cd elisp/ && \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'dash) (require 'ts))" \ + --eval='(byte-compile-file "chronometrist.el")' \ + --eval='(byte-compile-file "chronometrist-key-values.el")' \ + --eval='(byte-compile-file "chronometrist-spark.el")' \ + --eval='(byte-compile-file "chronometrist-third.el")' ; \ + cd .. + +lint-check-declare: tangle + cd elisp/ && \ + emacs -q --batch \ + --eval='(check-declare-file "chronometrist.el")' \ + --eval='(check-declare-file "chronometrist-key-values.el")' \ + --eval='(check-declare-file "chronometrist-spark.el")' \ + --eval='(check-declare-file "chronometrist-third.el")' ; \ + cd .. + +lint-checkdoc: tangle + cd elisp/ && \ + emacs -q -Q --batch \ + --eval='(checkdoc-file "chronometrist.el")' \ + --eval='(checkdoc-file "chronometrist-key-values.el")' \ + --eval='(checkdoc-file "chronometrist-spark.el")' \ + --eval='(checkdoc-file "chronometrist-third.el")' ; \ + cd .. + +lint-package-lint: setup tangle + cd elisp/ && \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'dash) (require 'ts) (require 'package-lint))" \ + -f 'package-lint-batch-and-exit' chronometrist.el \ + -f 'package-lint-batch-and-exit' chronometrist-key-values.el \ + -f 'package-lint-batch-and-exit' chronometrist-spark.el \ + -f 'package-lint-batch-and-exit' chronometrist-third.el ; \ + cd .. + +lint-relint: setup tangle + cd elisp/ && \ + emacs --batch \ + --eval="(progn (package-initialize) (require 'relint))" \ + --eval='(relint-file "chronometrist.el")' \ + --eval='(relint-file "chronometrist-key-values.el")' \ + --eval='(relint-file "chronometrist-spark.el")' \ + --eval='(relint-file "chronometrist-third.el")' ; \ + cd .. + +lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint + +clean-elc: + rm elisp/*.elc diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el new file mode 100644 index 0000000..9227f68 --- /dev/null +++ b/elisp/chronometrist-third.el @@ -0,0 +1,22 @@ +;; [[file:chronometrist-third.org::*fraction][fraction:1]] +(defcustom chronometrist-third-divisor 3 + "Number to determine accumulation of break time relative to work time.") +;; fraction:1 ends here + +;; [[file:chronometrist-third.org::*break-time][break-time:1]] +(defvar chronometrist-third-break-time 0 + "Accumulated break time in seconds.") +;; break-time:1 ends here + +;; [[file:chronometrist-third.org::*clock-in][clock-in:1]] +(defun chronometrist-third-clock-in () + (let ((latest-break-duration )) + )) + +;; (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))) +;; clock-in:1 ends here + +;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] +(defun chronometrist-third-clock-out () + (let ((latest-work-duration )))) +;; clock-out:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org new file mode 100644 index 0000000..14fb5fd --- /dev/null +++ b/elisp/chronometrist-third.org @@ -0,0 +1,36 @@ +#+TITLE: chronometrist-third +#+SUBTITLE: Third Time System extension for Chronometrist +#+PROPERTY: header-args :tangle yes :load yes :comments link + +* Program source +** fraction +#+BEGIN_SRC emacs-lisp +(defcustom chronometrist-third-divisor 3 + "Number to determine accumulation of break time relative to work time.") +#+END_SRC + +** break-time +#+BEGIN_SRC emacs-lisp +(defvar chronometrist-third-break-time 0 + "Accumulated break time in seconds.") +#+END_SRC + +** clock-in +#+BEGIN_SRC emacs-lisp +(defun chronometrist-third-clock-in () + (let ((latest-break-duration )) + )) + +;; (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))) +#+END_SRC + +** clock-out +#+BEGIN_SRC emacs-lisp +(defun chronometrist-third-clock-out () + (let ((latest-work-duration )))) +#+END_SRC + +* Local variables :NOEXPORT: +# Local Variables: +# my-org-src-default-lang: "emacs-lisp" +# End: From bbe54ca3bf1acc5678abc3067aff1579a4e472f6 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 17:27:19 +0530 Subject: [PATCH 04/56] Fix undesired empty lines between plist groups --- elisp/chronometrist.el | 1 + elisp/chronometrist.org | 1 + 2 files changed, 2 insertions(+) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index c32f324..7be3004 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -1853,6 +1853,7 @@ Return value is either a list in the form (error "No record to replace in %s" (eieio-object-class-name backend))) (chronometrist-sexp-in-file (chronometrist-backend-file backend) (chronometrist-remove-last backend :save nil) + (delete-trailing-whitespace) (chronometrist-insert backend plist :save nil) (save-buffer) t)) diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 3ccc399..741e306 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -2811,6 +2811,7 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method (error "No record to replace in %s" (eieio-object-class-name backend))) (chronometrist-sexp-in-file (chronometrist-backend-file backend) (chronometrist-remove-last backend :save nil) + (delete-trailing-whitespace) (chronometrist-insert backend plist :save nil) (save-buffer) t)) From bbb9986edf78409f99176c72e9f74b0dffe9881e Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 18:27:01 +0530 Subject: [PATCH 05/56] Rename function, update chronometrist-interval to add :stop times --- elisp/chronometrist.el | 41 +++++++++++++++-------------------------- elisp/chronometrist.org | 39 ++++++++++++++------------------------- 2 files changed, 29 insertions(+), 51 deletions(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 5ba5409..19c7123 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -355,7 +355,7 @@ treated as though their time is 00:00:00." The return value is seconds, as an integer." (let ((task-events (chronometrist-task-records-for-date backend task date))) (if task-events - (->> (chronometrist-events-to-durations task-events) + (->> (chronometrist-plists-to-durations task-events) (-reduce #'+) (truncate)) ;; no events for this task on DATE, i.e. no time spent @@ -411,25 +411,13 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by :dow dow :tz-offset utcoff)))) ;; iso-to-ts:1 ends here -;; [[file:chronometrist.org::*events-to-durations][events-to-durations:1]] -(defun chronometrist-events-to-durations (events) - "Convert EVENTS into a list of durations in seconds. -EVENTS must be a list of valid Chronometrist property lists (see -`chronometrist-file'). - -Return 0 if EVENTS is nil." - (if events - (cl-loop for plist in events collect - (let* ((start-ts (chronometrist-iso-to-ts - (plist-get plist :start))) - (stop-iso (plist-get plist :stop)) - ;; Add a stop time if it does not exist. - (stop-ts (if stop-iso - (chronometrist-iso-to-ts stop-iso) - (ts-now)))) - (ts-diff stop-ts start-ts))) - 0)) -;; events-to-durations:1 ends here +;; [[file:chronometrist.org::*plists-to-durations][plists-to-durations:1]] +(defun chronometrist-plists-to-durations (plists) + "Convert PLISTS into a list of durations in seconds. +PLISTS must be a list of valid Chronometrist property lists (see +`chronometrist-file'). Return 0 if PLISTS is nil." + (if plists (mapcar #'chronometrist-interval plists) 0)) +;; plists-to-durations:1 ends here ;; [[file:chronometrist.org::*date-iso][date-iso:1]] (cl-defun chronometrist-date-iso (&optional (ts (ts-now))) @@ -494,13 +482,14 @@ SECONDS must be a positive integer." ;; seconds-to-hms:1 ends here ;; [[file:chronometrist.org::*interval][interval:1]] -(defun chronometrist-interval (event) +(defun chronometrist-interval (plist) "Return the period of time covered by EVENT as a time value. EVENT should be a plist (see `chronometrist-file')." - (let ((start (plist-get event :start)) - (stop (plist-get event :stop))) - (time-subtract (parse-iso8601-time-string stop) - (parse-iso8601-time-string start)))) + (let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start))) + (stop-iso (plist-get plist :stop)) + ;; Add a stop time if it does not exist. + (stop-ts (if stop-iso (chronometrist-iso-to-ts stop-iso) (ts-now)))) + (ts-diff stop-ts start-ts))) ;; interval:1 ends here ;; [[file:chronometrist.org::*format-duration-long][format-duration-long:1]] @@ -2849,7 +2838,7 @@ displayed. They must be ts structs (see `ts.el').") when (setq events-in-day (chronometrist-task-records-for-date backend task date)) do (cl-incf days) and collect - (-reduce #'+ (chronometrist-events-to-durations events-in-day)) + (-reduce #'+ (chronometrist-plists-to-durations events-in-day)) into per-day-time-list finally return (if per-day-time-list diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index c93046e..f1cad92 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -870,7 +870,7 @@ treated as though their time is 00:00:00." The return value is seconds, as an integer." (let ((task-events (chronometrist-task-records-for-date backend task date))) (if task-events - (->> (chronometrist-events-to-durations task-events) + (->> (chronometrist-plists-to-durations task-events) (-reduce #'+) (truncate)) ;; no events for this task on DATE, i.e. no time spent @@ -947,25 +947,13 @@ TIMESTAMP must be an ISO-8601 timestamp, as handled by #+END_SRC -*** events-to-durations :function: +*** plists-to-durations :function: #+BEGIN_SRC emacs-lisp -(defun chronometrist-events-to-durations (events) - "Convert EVENTS into a list of durations in seconds. -EVENTS must be a list of valid Chronometrist property lists (see -`chronometrist-file'). - -Return 0 if EVENTS is nil." - (if events - (cl-loop for plist in events collect - (let* ((start-ts (chronometrist-iso-to-ts - (plist-get plist :start))) - (stop-iso (plist-get plist :stop)) - ;; Add a stop time if it does not exist. - (stop-ts (if stop-iso - (chronometrist-iso-to-ts stop-iso) - (ts-now)))) - (ts-diff stop-ts start-ts))) - 0)) +(defun chronometrist-plists-to-durations (plists) + "Convert PLISTS into a list of durations in seconds. +PLISTS must be a list of valid Chronometrist property lists (see +`chronometrist-file'). Return 0 if PLISTS is nil." + (if plists (mapcar #'chronometrist-interval plists) 0)) #+END_SRC *** date-iso :function: @@ -1067,13 +1055,14 @@ SECONDS must be a positive integer." *** interval :function: #+BEGIN_SRC emacs-lisp -(defun chronometrist-interval (event) +(defun chronometrist-interval (plist) "Return the period of time covered by EVENT as a time value. EVENT should be a plist (see `chronometrist-file')." - (let ((start (plist-get event :start)) - (stop (plist-get event :stop))) - (time-subtract (parse-iso8601-time-string stop) - (parse-iso8601-time-string start)))) + (let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start))) + (stop-iso (plist-get plist :stop)) + ;; Add a stop time if it does not exist. + (stop-ts (if stop-iso (chronometrist-iso-to-ts stop-iso) (ts-now)))) + (ts-diff stop-ts start-ts))) #+END_SRC *** format-duration-long :function: @@ -3966,7 +3955,7 @@ displayed. They must be ts structs (see `ts.el').") when (setq events-in-day (chronometrist-task-records-for-date backend task date)) do (cl-incf days) and collect - (-reduce #'+ (chronometrist-events-to-durations events-in-day)) + (-reduce #'+ (chronometrist-plists-to-durations events-in-day)) into per-day-time-list finally return (if per-day-time-list From 75d0130df4b6b34edb338c9474a427dda4aecd18 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 18:47:35 +0530 Subject: [PATCH 06/56] Implement clock-in and clock-out functions --- elisp/chronometrist-third.el | 22 +++++++++++++++------- elisp/chronometrist-third.org | 20 ++++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 9227f68..7ff22ee 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -1,7 +1,7 @@ -;; [[file:chronometrist-third.org::*fraction][fraction:1]] +;; [[file:chronometrist-third.org::*divisor][divisor:1]] (defcustom chronometrist-third-divisor 3 "Number to determine accumulation of break time relative to work time.") -;; fraction:1 ends here +;; divisor:1 ends here ;; [[file:chronometrist-third.org::*break-time][break-time:1]] (defvar chronometrist-third-break-time 0 @@ -10,13 +10,21 @@ ;; [[file:chronometrist-third.org::*clock-in][clock-in:1]] (defun chronometrist-third-clock-in () - (let ((latest-break-duration )) - )) - -;; (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))) + (unless (zerop chronometrist-third-break-time) + (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) + (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) + (new-break-time (- chronometrist-third-break-time used-break-duration))) + (setq chronometrist-third-break-time + (if (> new-break-time 0) + new-break-time + 0))))) ;; clock-in:1 ends here ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] (defun chronometrist-third-clock-out () - (let ((latest-work-duration )))) + (let ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))) + (cl-incf chronometrist-third-break-time (/ latest-work-duration chronometrist-third-divisor)) + ;; start notification timer(s) + ;; ... + )) ;; clock-out:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 14fb5fd..837f3bd 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -3,7 +3,7 @@ #+PROPERTY: header-args :tangle yes :load yes :comments link * Program source -** fraction +** divisor #+BEGIN_SRC emacs-lisp (defcustom chronometrist-third-divisor 3 "Number to determine accumulation of break time relative to work time.") @@ -18,16 +18,24 @@ ** clock-in #+BEGIN_SRC emacs-lisp (defun chronometrist-third-clock-in () - (let ((latest-break-duration )) - )) - -;; (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))) + (unless (zerop chronometrist-third-break-time) + (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) + (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) + (new-break-time (- chronometrist-third-break-time used-break-duration))) + (setq chronometrist-third-break-time + (if (> new-break-time 0) + new-break-time + 0))))) #+END_SRC ** clock-out #+BEGIN_SRC emacs-lisp (defun chronometrist-third-clock-out () - (let ((latest-work-duration )))) + (let ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))) + (cl-incf chronometrist-third-break-time (/ latest-work-duration chronometrist-third-divisor)) + ;; start notification timer(s) + ;; ... + )) #+END_SRC * Local variables :NOEXPORT: From 1297f2faa19aa4fb5bc1f2d0c634f231d98cab30 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 18:53:06 +0530 Subject: [PATCH 07/56] Auto-load with literate-elisp --- elisp/chronometrist-third.org | 1 + 1 file changed, 1 insertion(+) diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 837f3bd..1f5e6eb 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -41,4 +41,5 @@ * Local variables :NOEXPORT: # Local Variables: # my-org-src-default-lang: "emacs-lisp" +# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # End: From 8381dc36c5fbe32804b8bc4527923abfa75be3e1 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 19:17:04 +0530 Subject: [PATCH 08/56] Define minor mode --- elisp/chronometrist-third.el | 37 ++++++++++++++++++++++++++++++--- elisp/chronometrist-third.org | 39 ++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 7ff22ee..ee41fe5 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -8,8 +8,29 @@ "Accumulated break time in seconds.") ;; break-time:1 ends here +;; [[file:chronometrist-third.org::*alert-functions][alert-functions:1]] +(defcustom chronometrist-third-alert-functions + '(chronometrist-half-alert chronometrist-quarter-alert) + "List of timed alerts for the Third Time system. + +Typically, each function in this list should call `run-at-time' +to run another function, which in turn should call `alert' to +notify the user." + :group 'chronometrist-third + :type 'hook) +;; alert-functions:1 ends here + +;; [[file:chronometrist-third.org::*alert-functions][alert-functions:2]] +(defun chronometrist-third-half-alert () + (and (not (zerop chronometrist-third-break-time)) + (run-at-time ))) +;; alert-functions:2 ends here + ;; [[file:chronometrist-third.org::*clock-in][clock-in:1]] -(defun chronometrist-third-clock-in () +(defun chronometrist-third-clock-in (&optional arg) + ;; stop alert timer + ;; ... + ;; update break-time (unless (zerop chronometrist-third-break-time) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) @@ -21,10 +42,20 @@ ;; clock-in:1 ends here ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] -(defun chronometrist-third-clock-out () +(defun chronometrist-third-clock-out (&optional arg) (let ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))) (cl-incf chronometrist-third-break-time (/ latest-work-duration chronometrist-third-divisor)) - ;; start notification timer(s) + ;; start alert timer(s) ;; ... )) ;; clock-out:1 ends here + +;; [[file:chronometrist-third.org::*third-minor-mode][third-minor-mode:1]] +(define-minor-mode chronometrist-third-minor-mode + nil nil nil nil + (cond (chronometrist-third-minor-mode + (add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in) + (add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out)) + (t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in) + (remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out)))) +;; third-minor-mode:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 1f5e6eb..e410fc7 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -15,9 +15,31 @@ "Accumulated break time in seconds.") #+END_SRC +** alert-functions +#+BEGIN_SRC emacs-lisp +(defcustom chronometrist-third-alert-functions + '(chronometrist-half-alert chronometrist-quarter-alert) + "List of timed alerts for the Third Time system. + +Typically, each function in this list should call `run-at-time' +to run another function, which in turn should call `alert' to +notify the user." + :group 'chronometrist-third + :type 'hook) +#+END_SRC + +#+BEGIN_SRC emacs-lisp +(defun chronometrist-third-half-alert () + (and (not (zerop chronometrist-third-break-time)) + (run-at-time ))) +#+END_SRC + ** clock-in #+BEGIN_SRC emacs-lisp -(defun chronometrist-third-clock-in () +(defun chronometrist-third-clock-in (&optional arg) + ;; stop alert timer + ;; ... + ;; update break-time (unless (zerop chronometrist-third-break-time) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) @@ -30,14 +52,25 @@ ** clock-out #+BEGIN_SRC emacs-lisp -(defun chronometrist-third-clock-out () +(defun chronometrist-third-clock-out (&optional arg) (let ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))) (cl-incf chronometrist-third-break-time (/ latest-work-duration chronometrist-third-divisor)) - ;; start notification timer(s) + ;; start alert timer(s) ;; ... )) #+END_SRC +** third-minor-mode +#+BEGIN_SRC emacs-lisp +(define-minor-mode chronometrist-third-minor-mode + nil nil nil nil + (cond (chronometrist-third-minor-mode + (add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in) + (add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out)) + (t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in) + (remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out)))) +#+END_SRC + * Local variables :NOEXPORT: # Local Variables: # my-org-src-default-lang: "emacs-lisp" From 276bf64cf7774c33195056a7935b4d5c0ad15e7b Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 21:55:28 +0530 Subject: [PATCH 09/56] Add library headers, define alerts --- elisp/chronometrist-third.el | 112 +++++++++++++++++++++++++++---- elisp/chronometrist-third.org | 122 ++++++++++++++++++++++++++++++---- 2 files changed, 208 insertions(+), 26 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index ee41fe5..fece847 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -1,35 +1,119 @@ +;; [[file:chronometrist-third.org::*Library headers and commentary][Library headers and commentary:1]] +;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*- + +;; Author: contrapunctus +;; Maintainer: contrapunctus +;; Keywords: calendar +;; Homepage: https://tildegit.org/contrapunctus/chronometrist +;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0")) +;; Version: 0.0.1 + +;; This is free and unencumbered software released into the public domain. +;; +;; Anyone is free to copy, modify, publish, use, compile, sell, or +;; distribute this software, either in source code form or as a compiled +;; binary, for any purpose, commercial or non-commercial, and by any +;; means. +;; +;; For more information, please refer to + +;;; Commentary: +;; Add support for the Third Time system to Chronometrist. In Third +;; Time, you work for any length of time you like, and "earn" a third +;; of the work time as break time. For a more detailed explanation, +;; see +;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work + +;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md +;; Library headers and commentary:1 ends here + +;; [[file:chronometrist-third.org::*Dependencies][Dependencies:1]] +;;; Code: +(require 'chronometrist) +(require 'alert) +(declare-function chronometrist-last "chronometrist") +;; Dependencies:1 ends here + +;; [[file:chronometrist-third.org::*group][group:1]] +(defgroup chronometrist-third nil + "Third Time support for Chronometrist." + :group 'chronometrist) +;; group:1 ends here + ;; [[file:chronometrist-third.org::*divisor][divisor:1]] (defcustom chronometrist-third-divisor 3 "Number to determine accumulation of break time relative to work time.") ;; divisor:1 ends here +;; [[file:chronometrist-third.org::*duration-format][duration-format:1]] +(defcustom chronometrist-third-duration-format "%H, %M, and %S%z" + "Format string for durations, passed to `format-seconds'.") +;; duration-format:1 ends here + ;; [[file:chronometrist-third.org::*break-time][break-time:1]] (defvar chronometrist-third-break-time 0 "Accumulated break time in seconds.") ;; break-time:1 ends here ;; [[file:chronometrist-third.org::*alert-functions][alert-functions:1]] -(defcustom chronometrist-third-alert-functions - '(chronometrist-half-alert chronometrist-quarter-alert) +(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert) "List of timed alerts for the Third Time system. -Typically, each function in this list should call `run-at-time' -to run another function, which in turn should call `alert' to -notify the user." +Typically, each function in this list should call +`chronometrist-third-run-at-time' to run another function, which +in turn should call `alert' to notify the user." :group 'chronometrist-third :type 'hook) ;; alert-functions:1 ends here ;; [[file:chronometrist-third.org::*alert-functions][alert-functions:2]] +(defvar chronometrist-third-timer-list nil) + +(defun chronometrist-third-run-at-time (time repeat function &rest args) + (cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list)) + (defun chronometrist-third-half-alert () + "Display an alert when half the break time has been crossed." (and (not (zerop chronometrist-third-break-time)) - (run-at-time ))) + (chronometrist-third-run-at-time + (/ chronometrist-third-break-time 2) nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format (/ chronometrist-third-break-time 2)))))))) + +(defun chronometrist-third-quarter-alert () + "Display an alert when 3/4ths of the break time is consumed." + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + (* chronometrist-third-break-time (/ 3.0 4)) nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format (* chronometrist-third-break-time (/ 3.0 4))))))))) + +(defun chronometrist-third-break-over-alert () + "Display an alert when break time is over." + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + chronometrist-third-break-time nil + (lambda () (alert (format "Break time is over!")))))) ;; alert-functions:2 ends here +;; [[file:chronometrist-third.org::*start-alert-timers][start-alert-timers:1]] +(defun chronometrist-third-start-alert-timers () + (mapc #'funcall chronometrist-third-alert-functions)) +;; start-alert-timers:1 ends here + +;; [[file:chronometrist-third.org::*stop-alert-timers][stop-alert-timers:1]] +(defun chronometrist-third-stop-alert-timers () + (mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list)) +;; stop-alert-timers:1 ends here + ;; [[file:chronometrist-third.org::*clock-in][clock-in:1]] (defun chronometrist-third-clock-in (&optional arg) ;; stop alert timer - ;; ... + (chronometrist-third-stop-alert-timers) ;; update break-time (unless (zerop chronometrist-third-break-time) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) @@ -38,16 +122,20 @@ notify the user." (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time - 0))))) + 0)))) + (alert "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) ;; clock-in:1 ends here ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] (defun chronometrist-third-clock-out (&optional arg) - (let ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))) - (cl-incf chronometrist-third-break-time (/ latest-work-duration chronometrist-third-divisor)) + (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) + (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) + (cl-incf chronometrist-third-break-time break-time-increment) + (alert "%s added to break time (%s total)" + (format-seconds chronometrist-third-duration-format break-time-increment) + (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)) ;; start alert timer(s) - ;; ... - )) + (chronometrist-third-start-alert-timers))) ;; clock-out:1 ends here ;; [[file:chronometrist-third.org::*third-minor-mode][third-minor-mode:1]] diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index e410fc7..f6bc96a 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -3,13 +3,64 @@ #+PROPERTY: header-args :tangle yes :load yes :comments link * Program source -** divisor +** Library headers and commentary +#+BEGIN_SRC emacs-lisp +;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*- + +;; Author: contrapunctus +;; Maintainer: contrapunctus +;; Keywords: calendar +;; Homepage: https://tildegit.org/contrapunctus/chronometrist +;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0")) +;; Version: 0.0.1 + +;; This is free and unencumbered software released into the public domain. +;; +;; Anyone is free to copy, modify, publish, use, compile, sell, or +;; distribute this software, either in source code form or as a compiled +;; binary, for any purpose, commercial or non-commercial, and by any +;; means. +;; +;; For more information, please refer to + +;;; Commentary: +;; Add support for the Third Time system to Chronometrist. In Third +;; Time, you work for any length of time you like, and "earn" a third +;; of the work time as break time. For a more detailed explanation, +;; see +;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work + +;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md +#+END_SRC + +** Dependencies +#+BEGIN_SRC emacs-lisp +;;; Code: +(require 'chronometrist) +(require 'alert) +(declare-function chronometrist-last "chronometrist") +#+END_SRC + +** group :custom:group: +#+BEGIN_SRC emacs-lisp +(defgroup chronometrist-third nil + "Third Time support for Chronometrist." + :group 'chronometrist) +#+END_SRC + +** divisor :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-third-divisor 3 "Number to determine accumulation of break time relative to work time.") #+END_SRC -** break-time +** duration-format :custom:variable: +#+BEGIN_SRC emacs-lisp +(defcustom chronometrist-third-duration-format "%H, %M, and %S%z" + "Format string for durations, passed to `format-seconds'.") +#+END_SRC + +** break-time :variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-third-break-time 0 "Accumulated break time in seconds.") @@ -17,28 +68,67 @@ ** alert-functions #+BEGIN_SRC emacs-lisp -(defcustom chronometrist-third-alert-functions - '(chronometrist-half-alert chronometrist-quarter-alert) +(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert) "List of timed alerts for the Third Time system. -Typically, each function in this list should call `run-at-time' -to run another function, which in turn should call `alert' to -notify the user." +Typically, each function in this list should call +`chronometrist-third-run-at-time' to run another function, which +in turn should call `alert' to notify the user." :group 'chronometrist-third :type 'hook) #+END_SRC #+BEGIN_SRC emacs-lisp +(defvar chronometrist-third-timer-list nil) + +(defun chronometrist-third-run-at-time (time repeat function &rest args) + (cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list)) + (defun chronometrist-third-half-alert () + "Display an alert when half the break time has been crossed." (and (not (zerop chronometrist-third-break-time)) - (run-at-time ))) + (chronometrist-third-run-at-time + (/ chronometrist-third-break-time 2) nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format (/ chronometrist-third-break-time 2)))))))) + +(defun chronometrist-third-quarter-alert () + "Display an alert when 3/4ths of the break time is consumed." + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + (* chronometrist-third-break-time (/ 3.0 4)) nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format (* chronometrist-third-break-time (/ 3.0 4))))))))) + +(defun chronometrist-third-break-over-alert () + "Display an alert when break time is over." + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + chronometrist-third-break-time nil + (lambda () (alert (format "Break time is over!")))))) +#+END_SRC + +** start-alert-timers +#+BEGIN_SRC emacs-lisp +(defun chronometrist-third-start-alert-timers () + (mapc #'funcall chronometrist-third-alert-functions)) +#+END_SRC + +** stop-alert-timers +#+BEGIN_SRC emacs-lisp +(defun chronometrist-third-stop-alert-timers () + (mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list)) #+END_SRC ** clock-in #+BEGIN_SRC emacs-lisp (defun chronometrist-third-clock-in (&optional arg) ;; stop alert timer - ;; ... + (chronometrist-third-stop-alert-timers) ;; update break-time (unless (zerop chronometrist-third-break-time) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) @@ -47,17 +137,21 @@ notify the user." (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time - 0))))) + 0)))) + (alert "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) #+END_SRC ** clock-out #+BEGIN_SRC emacs-lisp (defun chronometrist-third-clock-out (&optional arg) - (let ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))) - (cl-incf chronometrist-third-break-time (/ latest-work-duration chronometrist-third-divisor)) + (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) + (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) + (cl-incf chronometrist-third-break-time break-time-increment) + (alert "%s added to break time (%s total)" + (format-seconds chronometrist-third-duration-format break-time-increment) + (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)) ;; start alert timer(s) - ;; ... - )) + (chronometrist-third-start-alert-timers))) #+END_SRC ** third-minor-mode From 0a1382abd748b0e1855b323666ec2d5c325d3db2 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 22:05:31 +0530 Subject: [PATCH 10/56] Address lint issues --- elisp/chronometrist-third.el | 19 ++++++++++--------- elisp/chronometrist-third.org | 26 +++++++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index fece847..6f800cb 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -1,4 +1,3 @@ -;; [[file:chronometrist-third.org::*Library headers and commentary][Library headers and commentary:1]] ;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*- ;; Author: contrapunctus @@ -25,14 +24,10 @@ ;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work ;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md -;; Library headers and commentary:1 ends here -;; [[file:chronometrist-third.org::*Dependencies][Dependencies:1]] ;;; Code: (require 'chronometrist) (require 'alert) -(declare-function chronometrist-last "chronometrist") -;; Dependencies:1 ends here ;; [[file:chronometrist-third.org::*group][group:1]] (defgroup chronometrist-third nil @@ -42,12 +37,14 @@ ;; [[file:chronometrist-third.org::*divisor][divisor:1]] (defcustom chronometrist-third-divisor 3 - "Number to determine accumulation of break time relative to work time.") + "Number to determine accumulation of break time relative to work time." + :type 'number) ;; divisor:1 ends here ;; [[file:chronometrist-third.org::*duration-format][duration-format:1]] (defcustom chronometrist-third-duration-format "%H, %M, and %S%z" - "Format string for durations, passed to `format-seconds'.") + "Format string for durations, passed to `format-seconds'." + :type 'string) ;; duration-format:1 ends here ;; [[file:chronometrist-third.org::*break-time][break-time:1]] @@ -111,7 +108,7 @@ in turn should call `alert' to notify the user." ;; stop-alert-timers:1 ends here ;; [[file:chronometrist-third.org::*clock-in][clock-in:1]] -(defun chronometrist-third-clock-in (&optional arg) +(defun chronometrist-third-clock-in (&optional _arg) ;; stop alert timer (chronometrist-third-stop-alert-timers) ;; update break-time @@ -127,7 +124,7 @@ in turn should call `alert' to notify the user." ;; clock-in:1 ends here ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] -(defun chronometrist-third-clock-out (&optional arg) +(defun chronometrist-third-clock-out (&optional _arg) (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) @@ -147,3 +144,7 @@ in turn should call `alert' to notify the user." (t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in) (remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out)))) ;; third-minor-mode:1 ends here + +(provide 'chronometrist-third) + +;;; chronometrist-third.el ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index f6bc96a..4ec2c73 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -4,7 +4,7 @@ * Program source ** Library headers and commentary -#+BEGIN_SRC emacs-lisp +#+BEGIN_SRC emacs-lisp :comments no ;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*- ;; Author: contrapunctus @@ -24,9 +24,9 @@ ;; For more information, please refer to ;;; Commentary: -;; Add support for the Third Time system to Chronometrist. In Third +;; Add support for the Third Time system to Chronometrist. In Third ;; Time, you work for any length of time you like, and "earn" a third -;; of the work time as break time. For a more detailed explanation, +;; of the work time as break time. For a more detailed explanation, ;; see ;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work @@ -34,11 +34,10 @@ #+END_SRC ** Dependencies -#+BEGIN_SRC emacs-lisp +#+BEGIN_SRC emacs-lisp :comments no ;;; Code: (require 'chronometrist) (require 'alert) -(declare-function chronometrist-last "chronometrist") #+END_SRC ** group :custom:group: @@ -51,13 +50,15 @@ ** divisor :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-third-divisor 3 - "Number to determine accumulation of break time relative to work time.") + "Number to determine accumulation of break time relative to work time." + :type 'number) #+END_SRC ** duration-format :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-third-duration-format "%H, %M, and %S%z" - "Format string for durations, passed to `format-seconds'.") + "Format string for durations, passed to `format-seconds'." + :type 'string) #+END_SRC ** break-time :variable: @@ -126,7 +127,7 @@ in turn should call `alert' to notify the user." ** clock-in #+BEGIN_SRC emacs-lisp -(defun chronometrist-third-clock-in (&optional arg) +(defun chronometrist-third-clock-in (&optional _arg) ;; stop alert timer (chronometrist-third-stop-alert-timers) ;; update break-time @@ -143,7 +144,7 @@ in turn should call `alert' to notify the user." ** clock-out #+BEGIN_SRC emacs-lisp -(defun chronometrist-third-clock-out (&optional arg) +(defun chronometrist-third-clock-out (&optional _arg) (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) @@ -165,6 +166,13 @@ in turn should call `alert' to notify the user." (remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out)))) #+END_SRC +** Provide +#+BEGIN_SRC emacs-lisp :comments no +(provide 'chronometrist-third) + +;;; chronometrist-third.el ends here +#+END_SRC + * Local variables :NOEXPORT: # Local Variables: # my-org-src-default-lang: "emacs-lisp" From 811b79eb9584978bfdf1f92786bc34dabbf4bc79 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 22:14:25 +0530 Subject: [PATCH 11/56] Add TODO items for Third Time --- TODO.org | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index e556aaa..8892679 100644 --- a/TODO.org +++ b/TODO.org @@ -910,7 +910,11 @@ Additional rules: 2. =chronometrist-third-break-time= 3. on clock out, increment =break-time= and start timed notification 4. on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero. -5. persist =break-time= between Emacs sessions + +Extras +1. persist =break-time= between Emacs sessions +2. audible alerts +3. handle user edits to the database Example flow 1. work for 30m From 64c14137af1596af309b2e949abf3fafb530c726 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 10 Feb 2022 23:19:18 +0530 Subject: [PATCH 12/56] Add tasks for big break command, work hours, and date key-values --- TODO.org | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/TODO.org b/TODO.org index 8892679..53fe149 100644 --- a/TODO.org +++ b/TODO.org @@ -856,7 +856,7 @@ With different checks, for different levels of speed/thoroughness - It doesn't do anything not already possible in the current formats. At best, it removes some duplication when the same task is "paused" and "resumed". -** "event"/non-interval records :feature: +** "event records" - records with only a timestamp :feature: :PROPERTIES: :CREATED: 2021-12-18T11:48:53+0530 :END: @@ -866,6 +866,22 @@ Records not used for time tracking, but to store data associated with a date or [] ...) #+END_SRC +** tagging dates with key-values :feature: +:PROPERTIES: +:CREATED: 2022-02-10T22:59:45+0530 +:CUSTOM_ID: date-key-values +:END: +Straightforward enough for the plist group backend - +#+BEGIN_SRC elisp +("" + [ ]* + +) +#+END_SRC + +=to-list= could omit them, since it has no concept of dates. =to-hash-table= could have them at the start of the lists of plists, i.e. hash values would be =(cdr )=. + +The plist backend could theoretically be extended to support it, but I'd rather deprecate that format (since it suffers from performance issues) and not deal with it again. + * undesired file watcher created after literate-elisp-load :bug: :PROPERTIES: :CREATED: 2021-12-18T15:13:35+0530 @@ -906,15 +922,22 @@ Additional rules: + /Avoid taking other unearned breaks/ if possible — so try to do personal tasks during normal or big breaks, or before/after your work day. #+END_QUOTE -1. =chronometrist-third-fraction= -2. =chronometrist-third-break-time= -3. on clock out, increment =break-time= and start timed notification -4. on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero. +1. [X] =chronometrist-third-fraction= +2. [X] =chronometrist-third-break-time= +3. [X] on clock out, increment =break-time= and start timed notification +4. [X] on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero. +5. [ ] "big break" command - user enters a time, for which we set an alarm. User must resume working around that time. +6. [ ] define work hours? + * As a custom variable - this is easy to implement, but cannot vary over time without affecting (the interpretation of) past data. + * [[#date-key-values][Key values associated with a day?]] The user could define the default in a custom variable. +7. [ ] displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time. Extras 1. persist =break-time= between Emacs sessions 2. audible alerts 3. handle user edits to the database + * last record modified - ? + * remove last record - ? Example flow 1. work for 30m From 2b57dc480786f13347af5ece607272f40a7532a8 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 00:16:49 +0530 Subject: [PATCH 13/56] Reword point, add deprecation notice --- .dir-locals.el | 3 ++- CHANGELOG.md | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.dir-locals.el b/.dir-locals.el index 33ca69b..f3c9e13 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -14,7 +14,8 @@ ("cr" . "chronometrist-report") ("cs" . "chronometrist-statistics") ("cx" . "chronometrist-sexp") - ("c" . "chronometrist"))))) + ("c" . "chronometrist"))) + (sentence-end-double-space . t))) (org-mode . ((org-html-self-link-headlines . t) (eval . (org-indent-mode)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecd321..998f367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 6. Debug logging messages - to view them, set `chronometrist-debug-enable`. ### Fixed -1. File change detection code has been rewritten, hopefully fixing some uncommon `read` and `args out of range` errors. +1. Code to detect the type of change made to the file has been rewritten, hopefully fixing some uncommon `read` errors and `args out of range` errors. + +### Deprecated +1. The plist backend is deprecated and may be removed in a future release. The `plist-group` backend is more performant and extensible - please use `chronometrist-migrate` to convert your data to the `plist-group` backend. ## [0.9.0] - 2021-07-08 ### Added From 863cbcbf52018a1320bcf6b6b95c4300eb441889 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 00:22:22 +0530 Subject: [PATCH 14/56] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 998f367..ed0046a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database. 5. New command `chronometrist-discard-active`, to discard the active interval. 6. Debug logging messages - to view them, set `chronometrist-debug-enable`. +7. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system. ### Fixed 1. Code to detect the type of change made to the file has been rewritten, hopefully fixing some uncommon `read` errors and `args out of range` errors. From 16248f35b3a78c8c6eb1d1c08a2cb9b18a496ae7 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 02:36:46 +0530 Subject: [PATCH 15/56] Add alternative date key-value form --- TODO.org | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 53fe149..b8ecf0b 100644 --- a/TODO.org +++ b/TODO.org @@ -878,7 +878,15 @@ Straightforward enough for the plist group backend - +) #+END_SRC -=to-list= could omit them, since it has no concept of dates. =to-hash-table= could have them at the start of the lists of plists, i.e. hash values would be =(cdr )=. +Alternatively, this would be easier to work with - easier to select the key-values or the plists, as required - +#+BEGIN_SRC elisp +("" + (key-values + [ ]+) + +) +#+END_SRC + +=to-list= could omit the date key-values, since it has no concept of dates. =to-hash-table= could have them at the start of the lists of plists, i.e. hash values would still be =(cdr )=. The plist backend could theoretically be extended to support it, but I'd rather deprecate that format (since it suffers from performance issues) and not deal with it again. From 27e2b17214752480de8053696d92a16a656a41b5 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 11:56:33 +0530 Subject: [PATCH 16/56] Add docstrings --- elisp/chronometrist-third.el | 62 +++++++++++++++++++----------- elisp/chronometrist-third.org | 71 +++++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 46 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 6f800cb..50b0ceb 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -17,9 +17,9 @@ ;; For more information, please refer to ;;; Commentary: -;; Add support for the Third Time system to Chronometrist. In Third +;; Add support for the Third Time system to Chronometrist. In Third ;; Time, you work for any length of time you like, and "earn" a third -;; of the work time as break time. For a more detailed explanation, +;; of the work time as break time. For a more detailed explanation, ;; see ;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work @@ -58,60 +58,75 @@ Typically, each function in this list should call `chronometrist-third-run-at-time' to run another function, which -in turn should call `alert' to notify the user." +in turn should call `alert' to notify the user. + +All functions in this list are started when the user clocks out, +and stopped when they clock in." :group 'chronometrist-third :type 'hook) ;; alert-functions:1 ends here -;; [[file:chronometrist-third.org::*alert-functions][alert-functions:2]] +;; [[file:chronometrist-third.org::*timer-list][timer-list:1]] (defvar chronometrist-third-timer-list nil) +;; timer-list:1 ends here +;; [[file:chronometrist-third.org::*run-at-time][run-at-time:1]] (defun chronometrist-third-run-at-time (time repeat function &rest args) + "Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'." (cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list)) +;; run-at-time:1 ends here +;; [[file:chronometrist-third.org::*half-alert][half-alert:1]] (defun chronometrist-third-half-alert () - "Display an alert when half the break time has been crossed." - (and (not (zerop chronometrist-third-break-time)) - (chronometrist-third-run-at-time - (/ chronometrist-third-break-time 2) nil - (lambda () - (alert - (format "%s left on your break." - (format-seconds chronometrist-third-duration-format (/ chronometrist-third-break-time 2)))))))) + "Display an alert when half the break time is consumed." + (let ((half-time (/ chronometrist-third-break-time 2.0))) + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + half-time nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format half-time)))))))) +;; half-alert:1 ends here +;; [[file:chronometrist-third.org::*quarter-alert][quarter-alert:1]] (defun chronometrist-third-quarter-alert () "Display an alert when 3/4ths of the break time is consumed." - (and (not (zerop chronometrist-third-break-time)) - (chronometrist-third-run-at-time - (* chronometrist-third-break-time (/ 3.0 4)) nil - (lambda () - (alert - (format "%s left on your break." - (format-seconds chronometrist-third-duration-format (* chronometrist-third-break-time (/ 3.0 4))))))))) + (let ((three-fourths (* chronometrist-third-break-time 7.5))) + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + three-fourths nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format three-fourths)))))))) +;; quarter-alert:1 ends here +;; [[file:chronometrist-third.org::*break-over-alert][break-over-alert:1]] (defun chronometrist-third-break-over-alert () "Display an alert when break time is over." (and (not (zerop chronometrist-third-break-time)) (chronometrist-third-run-at-time chronometrist-third-break-time nil (lambda () (alert (format "Break time is over!")))))) -;; alert-functions:2 ends here +;; break-over-alert:1 ends here ;; [[file:chronometrist-third.org::*start-alert-timers][start-alert-timers:1]] (defun chronometrist-third-start-alert-timers () + "Run functions in `chronometrist-third-alert-functions'." (mapc #'funcall chronometrist-third-alert-functions)) ;; start-alert-timers:1 ends here ;; [[file:chronometrist-third.org::*stop-alert-timers][stop-alert-timers:1]] (defun chronometrist-third-stop-alert-timers () + "Stop timers in `chronometrist-third-timer-list'." (mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list)) ;; stop-alert-timers:1 ends here ;; [[file:chronometrist-third.org::*clock-in][clock-in:1]] (defun chronometrist-third-clock-in (&optional _arg) - ;; stop alert timer + "Stop alert timers and update break time." (chronometrist-third-stop-alert-timers) - ;; update break-time (unless (zerop chronometrist-third-break-time) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) @@ -125,6 +140,9 @@ in turn should call `alert' to notify the user." ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] (defun chronometrist-third-clock-out (&optional _arg) + "Update break time based on the latest work interval. +Run `chronometrist-third-alert-functions' to alert user when +break time is up." (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 4ec2c73..a5d030d 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -67,44 +67,63 @@ "Accumulated break time in seconds.") #+END_SRC -** alert-functions +** alert-functions :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert) "List of timed alerts for the Third Time system. Typically, each function in this list should call `chronometrist-third-run-at-time' to run another function, which -in turn should call `alert' to notify the user." +in turn should call `alert' to notify the user. + +All functions in this list are started when the user clocks out, +and stopped when they clock in." :group 'chronometrist-third :type 'hook) #+END_SRC +** timer-list :variable: #+BEGIN_SRC emacs-lisp (defvar chronometrist-third-timer-list nil) +#+END_SRC +** run-at-time :procedure: +#+BEGIN_SRC emacs-lisp (defun chronometrist-third-run-at-time (time repeat function &rest args) + "Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'." (cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list)) +#+END_SRC +** half-alert :procedure: +#+BEGIN_SRC emacs-lisp (defun chronometrist-third-half-alert () - "Display an alert when half the break time has been crossed." - (and (not (zerop chronometrist-third-break-time)) - (chronometrist-third-run-at-time - (/ chronometrist-third-break-time 2) nil - (lambda () - (alert - (format "%s left on your break." - (format-seconds chronometrist-third-duration-format (/ chronometrist-third-break-time 2)))))))) + "Display an alert when half the break time is consumed." + (let ((half-time (/ chronometrist-third-break-time 2.0))) + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + half-time nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format half-time)))))))) +#+END_SRC +** quarter-alert :procedure: +#+BEGIN_SRC emacs-lisp (defun chronometrist-third-quarter-alert () "Display an alert when 3/4ths of the break time is consumed." - (and (not (zerop chronometrist-third-break-time)) - (chronometrist-third-run-at-time - (* chronometrist-third-break-time (/ 3.0 4)) nil - (lambda () - (alert - (format "%s left on your break." - (format-seconds chronometrist-third-duration-format (* chronometrist-third-break-time (/ 3.0 4))))))))) + (let ((three-fourths (* chronometrist-third-break-time 7.5))) + (and (not (zerop chronometrist-third-break-time)) + (chronometrist-third-run-at-time + three-fourths nil + (lambda () + (alert + (format "%s left on your break." + (format-seconds chronometrist-third-duration-format three-fourths)))))))) +#+END_SRC +** break-over-alert :procedure: +#+BEGIN_SRC emacs-lisp (defun chronometrist-third-break-over-alert () "Display an alert when break time is over." (and (not (zerop chronometrist-third-break-time)) @@ -113,24 +132,25 @@ in turn should call `alert' to notify the user." (lambda () (alert (format "Break time is over!")))))) #+END_SRC -** start-alert-timers +** start-alert-timers :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-third-start-alert-timers () + "Run functions in `chronometrist-third-alert-functions'." (mapc #'funcall chronometrist-third-alert-functions)) #+END_SRC -** stop-alert-timers +** stop-alert-timers :procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-third-stop-alert-timers () + "Stop timers in `chronometrist-third-timer-list'." (mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list)) #+END_SRC -** clock-in +** clock-in :hook:procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-third-clock-in (&optional _arg) - ;; stop alert timer + "Stop alert timers and update break time." (chronometrist-third-stop-alert-timers) - ;; update break-time (unless (zerop chronometrist-third-break-time) (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) @@ -142,9 +162,12 @@ in turn should call `alert' to notify the user." (alert "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) #+END_SRC -** clock-out +** clock-out :hook:procedure: #+BEGIN_SRC emacs-lisp (defun chronometrist-third-clock-out (&optional _arg) + "Update break time based on the latest work interval. +Run `chronometrist-third-alert-functions' to alert user when +break time is up." (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) @@ -155,7 +178,7 @@ in turn should call `alert' to notify the user." (chronometrist-third-start-alert-timers))) #+END_SRC -** third-minor-mode +** third-minor-mode :minor:mode: #+BEGIN_SRC emacs-lisp (define-minor-mode chronometrist-third-minor-mode nil nil nil nil From 09203bf5dbed1504ca1c513e7edd2856e6464292 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 12:35:56 +0530 Subject: [PATCH 17/56] Fix incorrect duration bug --- elisp/chronometrist-third.el | 3 ++- elisp/chronometrist-third.org | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 50b0ceb..5036679 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -99,7 +99,8 @@ and stopped when they clock in." (lambda () (alert (format "%s left on your break." - (format-seconds chronometrist-third-duration-format three-fourths)))))))) + (format-seconds chronometrist-third-duration-format + (- chronometrist-third-break-time three-fourths))))))))) ;; quarter-alert:1 ends here ;; [[file:chronometrist-third.org::*break-over-alert][break-over-alert:1]] diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index a5d030d..074ea50 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -119,7 +119,8 @@ and stopped when they clock in." (lambda () (alert (format "%s left on your break." - (format-seconds chronometrist-third-duration-format three-fourths)))))))) + (format-seconds chronometrist-third-duration-format + (- chronometrist-third-break-time three-fourths))))))))) #+END_SRC ** break-over-alert :procedure: From c13241b0a97ba19cf4b0d05fe0e1a3d8cb3f1181 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 12:36:24 +0530 Subject: [PATCH 18/56] Update TODO --- TODO.org | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TODO.org b/TODO.org index b8ecf0b..2a440c1 100644 --- a/TODO.org +++ b/TODO.org @@ -907,12 +907,10 @@ The plist backend could theoretically be extended to support it, but I'd rather 1. [ ] default suggested input backend should be the active backend 2. [ ] conserve file local variables prop line -* Support for the Third Time System +* STARTED Support for the Third Time System [57%] :extension: :PROPERTIES: :CREATED: 2022-02-10T14:12:12+0530 :END: -See branch "third" - [[https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work][Third Time: a better way to work]] #+BEGIN_QUOTE Lastly, here in one place are all the steps for using Third Time: From 66d747d27c952d7b71a5d27f1b353a8a575d1085 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 11 Feb 2022 22:01:24 +0530 Subject: [PATCH 19/56] Add autoload cookie --- elisp/chronometrist-third.el | 1 + elisp/chronometrist-third.org | 1 + 2 files changed, 2 insertions(+) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 5036679..ecb710e 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -155,6 +155,7 @@ break time is up." ;; clock-out:1 ends here ;; [[file:chronometrist-third.org::*third-minor-mode][third-minor-mode:1]] +;;;###autoload (define-minor-mode chronometrist-third-minor-mode nil nil nil nil (cond (chronometrist-third-minor-mode diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 074ea50..15816ff 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -181,6 +181,7 @@ break time is up." ** third-minor-mode :minor:mode: #+BEGIN_SRC emacs-lisp +;;;###autoload (define-minor-mode chronometrist-third-minor-mode nil nil nil nil (cond (chronometrist-third-minor-mode From 6ba6c0996c8cc071cea12d796addee2b68575d86 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sat, 12 Feb 2022 07:39:01 +0530 Subject: [PATCH 20/56] Update explanation --- elisp/chronometrist.el | 11 ++++--- elisp/chronometrist.org | 64 +++++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index fa64dd3..3b44752 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -3349,10 +3349,13 @@ TABLE must be a hash table as returned by collect plist) do (ts-adjustf begin-ts 'day 1))))))) -;; (chronometrist-details-intervals-for-range nil chronometrist-events) -;; (chronometrist-details-intervals-for-range "2021-06-01" chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") chronometrist-events) +;; (chronometrist-details-intervals-for-range nil (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range "2021-06-01" +;; (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") +;; (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") +;; (chronometrist-to-hash-table (chronometrist-active-backend))) ;; intervals-for-range:1 ends here ;; [[file:chronometrist.org::*input-to-value][input-to-value:1]] diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 3c2c418..32edabe 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -17,7 +17,6 @@ I hope this book—when completed—passes Tim Daly's [[https://www.youtube.com/ :PROPERTIES: :DESCRIPTION: The design, the implementation, and a little history :END: - ** Why I wrote Chronometrist It probably started off with a desire for introspection and self-awareness - what did I do all day? How much time have I spent doing X? It is also a tool to help me stay focused on a single task at a time, and to not go overboard and work on something for 7 hours straight. @@ -49,7 +48,7 @@ Quite recently, after around two years of Chronometrist developement and use, I :PROPERTIES: :DESCRIPTION: Explanation of some terms used later :END: -For lack of a better term, events are how we refer to time intervals. They are stored as plists; each contains at least a =:name ""=, a =:start ""=, and (except in case of an ongoing task) a =:stop ""=. +Chronometrist records /time intervals/ (earlier called "events") as plists. Each plist contains at least a =:name ""=, a =:start ""=, and (except in case of an ongoing task) a =:stop ""=. + row :: a row of a table in a =tabulated-list-mode= buffer; an element of =tabulated-list-entries=. + schema :: the column descriptor of a table in a =tabulated-list-mode= buffer; the value of =tabulated-list-format=. @@ -57,9 +56,14 @@ For lack of a better term, events are how we refer to time intervals. They are s See also [[#explanation-time-formats][Currently-Used Time Formats]] ** Overview -At its most basic, we read data from a [[#program-backend][plain text file]] containing Lisp plists, store it in a [[#program-data-structures][hash table]], and [[#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list-mode")][=tabulated-list-mode=]] buffer. When the file is changed—whether by the program or the user—we [[refresh-file][update the hash table]] and the buffer. +At its most basic, we read data from a [[#program-backend][backend]], store it in a [[#program-data-structures][hash table]], and [[#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list-mode")][=tabulated-list-mode=]] buffer. When the file is changed—whether by the program or the user—we [[refresh-file][update the hash table]] and the [[#program-frontend-chronometrist-refresh][buffer]]. -In addition, we implement a [[#program-pretty-printer][plist pretty-printer]] and some migration commands. This repository also contains an extension for [[file:chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals, and there is a extension for [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]] in a separate repository. +In addition, we implement a [[#program-pretty-printer][plist pretty-printer]] and some [[#program-migration][migration commands]]. + +Extensions exist for - +1. [[file:chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals, and +2. support for the [[file:chronometrist-third.org][Third Time system]] +3. [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]] ** Optimization It is of great importance that Chronometrist be responsive - @@ -114,10 +118,10 @@ Effects on the task list :DESCRIPTION: Events starting on one day and ending on another :CUSTOM_ID: explanation-midnight-spanning-intervals :END: -A unique problem in working with Chronometrist, one I had never foreseen, was tasks which start on one day and end on another. For instance, you start working on something at 2021-01-01T23:00 hours and stop on 2021-01-02T01:00. +A unique problem in working with Chronometrist, one I had never foreseen, was tasks which start on one day and end on another. For instance, you start working on something at =2021-01-01 23:00= hours and stop on =2021-01-02 01:00=. These mess up data consumption in all sorts of unforeseen ways, especially interval calculations and acquiring intervals for a specific date. In case of two of the most common operations throughout the program - -1. finding the intervals recorded on a given date - +1. finding the intervals recorded on a given date 2. finding the time spent on a task on a given day - if the day's intervals used for this contain a midnight-spanning interval, you'll have inaccurate results - it will include yesterday's time from the interval as well as today's. There are a few different approaches of dealing with them. (Currently, Chronometrist uses #3.) @@ -134,11 +138,14 @@ There are a few different approaches of dealing with them. (Currently, Chronomet + What happens when the user changes their day-start-time? The split-up events are now split wrongly, and the second event may get split /again./ Possible solutions - -1. Add function to check if, for two events A and B, the :stop of A is the same as the :start of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time. -2. Add a :split tag to split events. It can denote that the next event was originally a part of this one. +1. Add function to check if, for two events A and B, the =:stop= of A is the same as the =:start= of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time. + + Implemented as [[#program-data-structures-plists-split-p][plist-split-p]] +2. Add a =:split= tag to split events. It can denote that the next event was originally a part of this one. 3. Re-check and update the file when the day-start-time changes. - Possible with ~add-variable-watcher~ or ~:custom-set~ in Customize (thanks bpalmer) +This strategy is implemented in the [[#program-backend-plist-group][plist-group]] backend. Custom day-start time is not yet implemented - if ever implemented, it will probably require exporting the file, with all split intervals being combined and re-split. + *** Split them at the hash-table-level Handled by ~chronometrist-sexp-events-populate~ + Advantage - simpler data-consuming code. @@ -151,7 +158,7 @@ Handled by ~chronometrist-sexp-events-populate~ :DESCRIPTION: The desired behaviour of point in Chronometrist :END: After hacking, always test for and ensure the following - -1. Toggling the buffer via =chronometrist= / =chronometrist-report= / =chronometrist-statistics= should preserve point +1. Toggling the buffer via [[#program-frontend-chronometrist-command][chronometrist]]/[[#program-frontend-report-command][chronometrist-report]]/[[#program-frontend-statistics-command][chronometrist-statistics]] should preserve point 2. The timer function should preserve point when the buffer is current 3. The timer function should preserve point when the buffer is not current, but is visible in another window 4. The next/previous week keys and buttons should preserve point. @@ -160,8 +167,8 @@ After hacking, always test for and ensure the following - :PROPERTIES: :DESCRIPTION: Deriving dates in the current week :END: -A quick description, starting from the first time [[* chronometrist-report][=chronometrist-report=]] is run in an Emacs session - -1. We get the current date as a ts struct, using =chronometrist-date=. +A quick description, starting from the first time [[#program-frontend-report-command][chronometrist-report]] is run in an Emacs session - +1. We get the current date as a =ts= struct, using chronometrist-date. 2. The variable =chronometrist-report-week-start-day= stores the day we consider the week to start with. The default is "Sunday". We check if the date from #2 is on the week start day, else decrement it till we are, using =(chronometrist-report-previous-week-start)=. @@ -1692,6 +1699,9 @@ These can be implemented in terms of the minimal protocol above. #+END_SRC *** Common definitions for s-expression backends +:PROPERTIES: +:CUSTOM_ID: program-backend-sexp-common +:END: **** file-backend-mixin :mixin: :PROPERTIES: :CUSTOM_ID: file-backend-mixin @@ -2494,6 +2504,9 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe #+END_SRC *** plist group backend +:PROPERTIES: +:CUSTOM_ID: program-backend-plist-group +:END: This is largely like the plist backend, but plists are grouped by date by wrapping them in a tagged list - #+BEGIN_SRC emacs-lisp :tangle no :load no @@ -2619,6 +2632,9 @@ Situations - #+END_SRC **** plists-split-p :function: +:PROPERTIES: +:CUSTOM_ID: program-data-structures-plists-split-p +:END: [[file:../tests/chronometrist-tests.org::#tests-common-plists-split-p][tests]] #+BEGIN_SRC emacs-lisp @@ -2853,6 +2869,9 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method #+END_SRC ** Migration +:PROPERTIES: +:CUSTOM_ID: program-migration +:END: *** remove-prefix #+BEGIN_SRC emacs-lisp (defun chronometrist-remove-prefix (string) @@ -3373,6 +3392,9 @@ task. N must be a positive integer." #+END_SRC **** refresh :procedure: +:PROPERTIES: +:CUSTOM_ID: program-frontend-chronometrist-refresh +:END: #+BEGIN_SRC emacs-lisp (defun chronometrist-refresh (&optional _ignore-auto _noconfirm) "Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'. @@ -3668,6 +3690,9 @@ Has no effect if a task is active." #+END_SRC **** chronometrist :command: +:PROPERTIES: +:CUSTOM_ID: program-frontend-chronometrist-command +:END: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist (&optional arg) @@ -3923,6 +3948,9 @@ Merge this into `chronometrist-refresh-file', while moving the -refresh call to #+END_SRC **** chronometrist-report :command: +:PROPERTIES: +:CUSTOM_ID: program-frontend-report-command +:END: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist-report (&optional keep-date) @@ -4191,6 +4219,9 @@ value of `revert-buffer-function'." #+END_SRC **** chronometrist-statistics :command: +:PROPERTIES: +:CUSTOM_ID: program-frontend-statistics-command +:END: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist-statistics (&optional preserve-state) @@ -4549,10 +4580,13 @@ TABLE must be a hash table as returned by collect plist) do (ts-adjustf begin-ts 'day 1))))))) -;; (chronometrist-details-intervals-for-range nil chronometrist-events) -;; (chronometrist-details-intervals-for-range "2021-06-01" chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events) -;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") chronometrist-events) +;; (chronometrist-details-intervals-for-range nil (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range "2021-06-01" +;; (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") +;; (chronometrist-to-hash-table (chronometrist-active-backend))) +;; (chronometrist-details-intervals-for-range '("2021-06-02T01:00+05:30" . "2021-06-02T03:00+05:30") +;; (chronometrist-to-hash-table (chronometrist-active-backend))) #+END_SRC **** input-to-value :function: From 1b6c21d52937fa3ff0a8adabda5e95223f7f39ab Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sat, 12 Feb 2022 21:41:15 +0530 Subject: [PATCH 21/56] Fix `alert` errors, blank duration in notification --- elisp/chronometrist-third.el | 10 +++++----- elisp/chronometrist-third.org | 10 +++++----- elisp/chronometrist.el | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index ecb710e..796c3f8 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -135,8 +135,8 @@ and stopped when they clock in." (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time - 0)))) - (alert "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) + 0)) + (alert (format "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))) ;; clock-in:1 ends here ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] @@ -147,9 +147,9 @@ break time is up." (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) - (alert "%s added to break time (%s total)" - (format-seconds chronometrist-third-duration-format break-time-increment) - (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)) + (alert (format "%s added to break time (%s total)" + (format-seconds chronometrist-third-duration-format break-time-increment) + (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) ;; start alert timer(s) (chronometrist-third-start-alert-timers))) ;; clock-out:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 15816ff..548a76c 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -159,8 +159,8 @@ and stopped when they clock in." (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time - 0)))) - (alert "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) + 0)) + (alert (format "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))) #+END_SRC ** clock-out :hook:procedure: @@ -172,9 +172,9 @@ break time is up." (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) - (alert "%s added to break time (%s total)" - (format-seconds chronometrist-third-duration-format break-time-increment) - (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)) + (alert (format "%s added to break time (%s total)" + (format-seconds chronometrist-third-duration-format break-time-increment) + (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) ;; start alert timer(s) (chronometrist-third-start-alert-timers))) #+END_SRC diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index fa64dd3..1a1bb13 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -1647,7 +1647,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe t)))) ;; insert:1 ends here -;; [[file:chronometrist.org::*plists-split-p][plists-split-p:1]] +;; [[file:chronometrist.org::#program-data-structures-plists-split-p][plists-split-p:1]] (defun chronometrist-plists-split-p (old-plist new-plist) "Return t if OLD-PLIST and NEW-PLIST are split plists. Split plists means the :stop time of old-plist must be the same as @@ -2296,7 +2296,7 @@ task. N must be a positive integer." (chronometrist-task-at-point))) ;; goto-nth-task:1 ends here -;; [[file:chronometrist.org::*refresh][refresh:1]] +;; [[file:chronometrist.org::#program-frontend-chronometrist-refresh][refresh:1]] (defun chronometrist-refresh (&optional _ignore-auto _noconfirm) "Refresh the `chronometrist' buffer, without re-reading `chronometrist-file'. The optional arguments _IGNORE-AUTO and _NOCONFIRM are ignored, @@ -2571,7 +2571,7 @@ Has no effect if a task is active." (message "Nothing to discard - use this when clocked in.")))) ;; discard-active:1 ends here -;; [[file:chronometrist.org::*chronometrist][chronometrist:1]] +;; [[file:chronometrist.org::#program-frontend-chronometrist-command][chronometrist:1]] ;;;###autoload (defun chronometrist (&optional arg) "Display the user's tasks and the time spent on them today. @@ -2805,7 +2805,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found." (chronometrist-setup-file-watch)) ;; report-mode:1 ends here -;; [[file:chronometrist.org::*chronometrist-report][chronometrist-report:1]] +;; [[file:chronometrist.org::#program-frontend-report-command][chronometrist-report:1]] ;;;###autoload (defun chronometrist-report (&optional keep-date) "Display a weekly report of the data in `chronometrist-file'. @@ -3056,7 +3056,7 @@ value of `revert-buffer-function'." (chronometrist-setup-file-watch)) ;; statistics-mode:1 ends here -;; [[file:chronometrist.org::*chronometrist-statistics][chronometrist-statistics:1]] +;; [[file:chronometrist.org::#program-frontend-statistics-command][chronometrist-statistics:1]] ;;;###autoload (defun chronometrist-statistics (&optional preserve-state) "Display statistics for Chronometrist data. From 29dee2792cb1783a96d438f106a4c5c420045f84 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sat, 12 Feb 2022 23:41:39 +0530 Subject: [PATCH 22/56] Fix unintentionally exported heading --- manual.md | 22 ++++++++-------------- manual.org | 2 +- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/manual.md b/manual.md index 4fc0f83..67207f3 100644 --- a/manual.md +++ b/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](#org690c2df) + 4. [chronometrist-details](#org0f35db3) 5. [common commands](#usage-common-commands) 6. [Time goals/targets](#time-goals) 6. [How-to](#how-to) @@ -21,19 +21,18 @@ 2. [How to load the program using literate-elisp](#how-to-literate-elisp) 3. [How to attach tags to time intervals](#how-to-tags) 4. [How to attach key-values to time intervals](#how-to-key-value-pairs) - 5. [How to skip running hooks/attaching tags and key values](#org63b85c6) + 5. [How to skip running hooks/attaching tags and key values](#orgdf1f18f) 6. [How to open certain files when you start a task](#how-to-open-files-on-task-start) 7. [How to warn yourself about uncommitted changes](#how-to-warn-uncommitted-changes) 8. [How to display the current time interval in the activity indicator](#how-to-activity-indicator) 9. [How to back up your Chronometrist data](#how-to-backup) 10. [How to configure Vertico for use with Chronometrist](#howto-vertico) -7. [Explanation](#org4f83f29) +7. [Explanation](#org280cb94) 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#org07932c3) +8. [User's reference](#orgf24e40f) 9. [Contributions and contact](#contributions-contact) 10. [License](#license) 11. [Thanks](#thanks) -12. [Local variables](#org6ca2f77):NOEXPORT: Donate using Liberapay @@ -159,7 +158,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. - + ## chronometrist-details @@ -234,7 +233,7 @@ Evaluate or add to your init.el the following - To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press `C-c C-c` to accept the key-values, or `C-c C-k` to cancel. - + ## How to skip running hooks/attaching tags and key values @@ -335,7 +334,7 @@ Or use `vertico-multiform` to disable sorting for only specific commands - (chronometrist-key-values-unified-prompt (vertico-sort-function . nil))))) - + # Explanation @@ -351,7 +350,7 @@ The Org file can also be loaded directly using the [literate-elisp](https://gith `chronometrist.org` is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads. - + # User's reference @@ -424,11 +423,6 @@ blandest for helping me with the name fiete and wu-lee for testing and bug reports - - -# Local variables :NOEXPORT: - - # Footnotes 1 but not `chronometrist-before-in-functions` diff --git a/manual.org b/manual.org index cd6795f..97cb9a7 100644 --- a/manual.org +++ b/manual.org @@ -384,7 +384,7 @@ blandest for helping me with the name fiete and wu-lee for testing and bug reports -* Local variables :NOEXPORT: +* Local variables :noexport: # Local Variables: # eval: (progn (require 'ox-md) (add-hook 'after-save-hook (lambda () (org-export-to-file 'md "manual.md")) nil t)) # my-org-src-default-lang: "emacs-lisp" From bedbf6a8c59b9f52c91f4828395817432ba910c2 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sat, 12 Feb 2022 23:50:34 +0530 Subject: [PATCH 23/56] Pass time as argument to callbacks --- elisp/chronometrist-third.el | 10 ++++++---- elisp/chronometrist-third.org | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 796c3f8..715988a 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -83,10 +83,11 @@ and stopped when they clock in." (and (not (zerop chronometrist-third-break-time)) (chronometrist-third-run-at-time half-time nil - (lambda () + (lambda (half-time) (alert (format "%s left on your break." - (format-seconds chronometrist-third-duration-format half-time)))))))) + (format-seconds chronometrist-third-duration-format half-time)))) + half-time)))) ;; half-alert:1 ends here ;; [[file:chronometrist-third.org::*quarter-alert][quarter-alert:1]] @@ -96,11 +97,12 @@ and stopped when they clock in." (and (not (zerop chronometrist-third-break-time)) (chronometrist-third-run-at-time three-fourths nil - (lambda () + (lambda (three-fourths) (alert (format "%s left on your break." (format-seconds chronometrist-third-duration-format - (- chronometrist-third-break-time three-fourths))))))))) + (- chronometrist-third-break-time three-fourths))))) + three-fourths)))) ;; quarter-alert:1 ends here ;; [[file:chronometrist-third.org::*break-over-alert][break-over-alert:1]] diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 548a76c..0bd1845 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -102,10 +102,11 @@ and stopped when they clock in." (and (not (zerop chronometrist-third-break-time)) (chronometrist-third-run-at-time half-time nil - (lambda () + (lambda (half-time) (alert (format "%s left on your break." - (format-seconds chronometrist-third-duration-format half-time)))))))) + (format-seconds chronometrist-third-duration-format half-time)))) + half-time)))) #+END_SRC ** quarter-alert :procedure: @@ -116,11 +117,12 @@ and stopped when they clock in." (and (not (zerop chronometrist-third-break-time)) (chronometrist-third-run-at-time three-fourths nil - (lambda () + (lambda (three-fourths) (alert (format "%s left on your break." (format-seconds chronometrist-third-duration-format - (- chronometrist-third-break-time three-fourths))))))))) + (- chronometrist-third-break-time three-fourths))))) + three-fourths)))) #+END_SRC ** break-over-alert :procedure: From 28a0f691325347a6a8ce86ebd708819077e2b16f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 00:02:49 +0530 Subject: [PATCH 24/56] Show different message when all break time is used up --- elisp/chronometrist-third.el | 24 +++++++++++++++--------- elisp/chronometrist-third.org | 24 +++++++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 715988a..14a1062 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -131,14 +131,20 @@ and stopped when they clock in." "Stop alert timers and update break time." (chronometrist-third-stop-alert-timers) (unless (zerop chronometrist-third-break-time) - (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) - (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) - (new-break-time (- chronometrist-third-break-time used-break-duration))) - (setq chronometrist-third-break-time - (if (> new-break-time 0) - new-break-time - 0)) - (alert (format "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))) + (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) + (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) + (new-break-time (- chronometrist-third-break-time used-break-duration)) + (old-break-time chronometrist-third-break-time) + (used-break-duration-string (format-seconds chronometrist-third-duration-format + used-break-duration))) + (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0)) + (alert + (if (zerop chronometrist-third-break-time) + (format "You have used up all %s of your break time (%s break)" + old-break-time used-break-duration-string) + (format "You have used %s of your break time (%s left)" + used-break-duration-string + (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) ;; clock-in:1 ends here ;; [[file:chronometrist-third.org::*clock-out][clock-out:1]] @@ -149,7 +155,7 @@ break time is up." (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) - (alert (format "%s added to break time (%s total)" + (alert (format "You have gained %s of break time (%s total)" (format-seconds chronometrist-third-duration-format break-time-increment) (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) ;; start alert timer(s) diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 0bd1845..6841004 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -155,14 +155,20 @@ and stopped when they clock in." "Stop alert timers and update break time." (chronometrist-third-stop-alert-timers) (unless (zerop chronometrist-third-break-time) - (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) - (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) - (new-break-time (- chronometrist-third-break-time used-break-duration))) - (setq chronometrist-third-break-time - (if (> new-break-time 0) - new-break-time - 0)) - (alert (format "%s left on your break" (format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))) + (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) + (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) + (new-break-time (- chronometrist-third-break-time used-break-duration)) + (old-break-time chronometrist-third-break-time) + (used-break-duration-string (format-seconds chronometrist-third-duration-format + used-break-duration))) + (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0)) + (alert + (if (zerop chronometrist-third-break-time) + (format "You have used up all %s of your break time (%s break)" + old-break-time used-break-duration-string) + (format "You have used %s of your break time (%s left)" + used-break-duration-string + (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) #+END_SRC ** clock-out :hook:procedure: @@ -174,7 +180,7 @@ break time is up." (let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend)))) (break-time-increment (/ latest-work-duration chronometrist-third-divisor))) (cl-incf chronometrist-third-break-time break-time-increment) - (alert (format "%s added to break time (%s total)" + (alert (format "You have gained %s of break time (%s total)" (format-seconds chronometrist-third-duration-format break-time-increment) (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))) ;; start alert timer(s) From a7eecc96ab8a9a13e05e411ae306a3bf883e865c Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 00:58:22 +0530 Subject: [PATCH 25/56] Change latest-record to always return a unified record This fixes - * time displayed by activity indicator * calculation of break time for chronometrist-third --- elisp/chronometrist.el | 13 ++++++++++--- elisp/chronometrist.org | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 5406e8b..6ce4176 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -886,8 +886,12 @@ Signal an error if there is no record to remove.") ;; [[file:chronometrist.org::*latest-record][latest-record:1]] (cl-defgeneric chronometrist-latest-record (backend) - "Return the latest entry from BACKEND as a plist, or nil if BACKEND contains no records. -Return value may be active, i.e. it may or may not have a :stop key-value.") + "Return the latest record from BACKEND as a plist, or nil if BACKEND contains no records. +Return value may be active, i.e. it may or may not have a `:stop' +key-value. + +If the latest record starts on one day and ends on another, the +entire (unsplit) record must be returned.") ;; latest-record:1 ends here ;; [[file:chronometrist.org::*task-records-for-date][task-records-for-date:1]] @@ -1820,7 +1824,10 @@ Return value is either a list in the form ;; [[file:chronometrist.org::*latest-record][latest-record:1]] (cl-defmethod chronometrist-latest-record ((backend chronometrist-plist-group-backend)) - (cl-first (last (chronometrist-latest-date-records backend)))) + (with-slots (file) backend + (if (chronometrist-last-two-split-p file) + (apply #'chronometrist-plist-unify (chronometrist-last-two-split-p (chronometrist-backend-file (chronometrist-active-backend)))) + (cl-first (last (chronometrist-latest-date-records backend)))))) ;; latest-record:1 ends here ;; [[file:chronometrist.org::*task-records-for-date][task-records-for-date:1]] diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 32edabe..3cb831d 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -1558,8 +1558,12 @@ Signal an error if there is no record to remove.") ***** latest-record :generic:function: #+BEGIN_SRC emacs-lisp (cl-defgeneric chronometrist-latest-record (backend) - "Return the latest entry from BACKEND as a plist, or nil if BACKEND contains no records. -Return value may be active, i.e. it may or may not have a :stop key-value.") + "Return the latest record from BACKEND as a plist, or nil if BACKEND contains no records. +Return value may be active, i.e. it may or may not have a `:stop' +key-value. + +If the latest record starts on one day and ends on another, the +entire (unsplit) record must be returned.") #+END_SRC ***** task-records-for-date :generic:function: @@ -2822,7 +2826,10 @@ Return value is either a list in the form ***** latest-record :reader:method: #+BEGIN_SRC emacs-lisp (cl-defmethod chronometrist-latest-record ((backend chronometrist-plist-group-backend)) - (cl-first (last (chronometrist-latest-date-records backend)))) + (with-slots (file) backend + (if (chronometrist-last-two-split-p file) + (apply #'chronometrist-plist-unify (chronometrist-last-two-split-p (chronometrist-backend-file (chronometrist-active-backend)))) + (cl-first (last (chronometrist-latest-date-records backend)))))) #+END_SRC ***** task-records-for-date :reader:method: From 90d3f73cdf193136221be52219ee1839c1b6d33c Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 01:00:07 +0530 Subject: [PATCH 26/56] Remove :stop key for active split plists --- elisp/chronometrist.el | 10 ++++++++-- elisp/chronometrist.org | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 6ce4176..61c6387 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -1701,8 +1701,14 @@ Return value is either a list in the form (new-plist-wo-time (chronometrist-plist-remove new-plist :start :stop))) (cond ((not (and old-plist new-plist)) nil) ((equal old-plist-wo-time new-plist-wo-time) - (let ((plist (cl-copy-list old-plist))) - (plist-put plist :stop (plist-get new-plist :stop)))) + (let* ((plist (cl-copy-list old-plist)) + (new-stop (plist-get new-plist :stop))) + ;; Usually, a split plist has a `:stop' key. However, a + ;; user may clock out and delete the stop time, resulting + ;; in a split record without a `:stop' key. + (if new-stop + (plist-put plist :stop new-stop) + (chronometrist-plist-remove plist :stop)))) (t (error "Attempt to unify plists with non-identical key-values"))))) ;; plist-unify:1 ends here diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 3cb831d..176cc47 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -2693,8 +2693,14 @@ Return value is either a list in the form (new-plist-wo-time (chronometrist-plist-remove new-plist :start :stop))) (cond ((not (and old-plist new-plist)) nil) ((equal old-plist-wo-time new-plist-wo-time) - (let ((plist (cl-copy-list old-plist))) - (plist-put plist :stop (plist-get new-plist :stop)))) + (let* ((plist (cl-copy-list old-plist)) + (new-stop (plist-get new-plist :stop))) + ;; Usually, a split plist has a `:stop' key. However, a + ;; user may clock out and delete the stop time, resulting + ;; in a split record without a `:stop' key. + (if new-stop + (plist-put plist :stop new-stop) + (chronometrist-plist-remove plist :stop)))) (t (error "Attempt to unify plists with non-identical key-values"))))) #+END_SRC From 7f4e5e9c7c3ff2223a10a237e4130dc0f17db4c1 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 02:39:22 +0530 Subject: [PATCH 27/56] Remove leftover SO reference --- manual.md | 18 ++++++++---------- manual.org | 2 -- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/manual.md b/manual.md index 67207f3..2528a76 100644 --- a/manual.md +++ b/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](#org0f35db3) + 4. [chronometrist-details](#orga55ac83) 5. [common commands](#usage-common-commands) 6. [Time goals/targets](#time-goals) 6. [How-to](#how-to) @@ -21,15 +21,15 @@ 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](#orgdf1f18f) + 5. [How to skip running hooks/attaching tags and key values](#orgae7b0f6) 6. [How to open certain files when you start a task](#how-to-open-files-on-task-start) 7. [How to warn yourself about uncommitted changes](#how-to-warn-uncommitted-changes) 8. [How to display the current time interval in the activity indicator](#how-to-activity-indicator) 9. [How to back up your Chronometrist data](#how-to-backup) 10. [How to configure Vertico for use with Chronometrist](#howto-vertico) -7. [Explanation](#org280cb94) +7. [Explanation](#org15771af) 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#orgf24e40f) +8. [User's reference](#org020377a) 9. [Contributions and contact](#contributions-contact) 10. [License](#license) 11. [Thanks](#thanks) @@ -158,7 +158,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. - + ## chronometrist-details @@ -233,7 +233,7 @@ Evaluate or add to your init.el the following - To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press `C-c C-c` to accept the key-values, or `C-c C-k` to cancel. - + ## How to skip running hooks/attaching tags and key values @@ -310,8 +310,6 @@ I suggest backing up Chronometrist data on each save using the [async-backup](ht 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). - @@ -334,7 +332,7 @@ Or use `vertico-multiform` to disable sorting for only specific commands - (chronometrist-key-values-unified-prompt (vertico-sort-function . nil))))) - + # Explanation @@ -350,7 +348,7 @@ The Org file can also be loaded directly using the [literate-elisp](https://gith `chronometrist.org` is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads. - + # User's reference diff --git a/manual.org b/manual.org index 97cb9a7..f222612 100644 --- a/manual.org +++ b/manual.org @@ -281,8 +281,6 @@ I suggest backing up Chronometrist data on each save using the [[https://tildegi : M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET 3. Optionally, configure =backup-directory-alist= to set a specific directory for the backups. -Adapted from this [[https://stackoverflow.com/questions/6916529/how-can-i-make-emacs-backup-every-time-i-save][StackOverflow answer]]. - [fn:1] It is possible to use Emacs' built-in backup system to do it, but since it is synchronous, doing so will greatly slow down saving of the Chronometrist file. ** How to configure Vertico for use with Chronometrist From 2c3b9eaa97a187f0990c13b66e3a2a2c3cc3b9ed Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 02:52:25 +0530 Subject: [PATCH 28/56] Add work hour implementation idea --- TODO.org | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/TODO.org b/TODO.org index 2a440c1..dfbae2b 100644 --- a/TODO.org +++ b/TODO.org @@ -933,10 +933,11 @@ Additional rules: 3. [X] on clock out, increment =break-time= and start timed notification 4. [X] on clock in, calculate duration of latest break (duration between last =:stop= and now), and subtract it from =break-time=; if the resulting time is negative, set it to zero. 5. [ ] "big break" command - user enters a time, for which we set an alarm. User must resume working around that time. -6. [ ] define work hours? - * As a custom variable - this is easy to implement, but cannot vary over time without affecting (the interpretation of) past data. - * [[#date-key-values][Key values associated with a day?]] The user could define the default in a custom variable. -7. [ ] displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time. +6. [ ] Define work hours + * [ ] Custom variable for default work hours. + * [ ] Hook function which inserts default work hours. + * [ ] Hook function which prompts for work hours whenever you first clock in on a date. If work hours are defined in the custom variable, ask whether to use them - on negative answer, prompt for today's work hours. If work hours are not defined, prompt for today's work hours. +7. [ ] Displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time. Extras 1. persist =break-time= between Emacs sessions From 005e54f79b4db87799d6f43a3b5c360cacc85054 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 02:52:34 +0530 Subject: [PATCH 29/56] Note branches; mark work on date properties as STARTED --- TODO.org | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index dfbae2b..d2cb667 100644 --- a/TODO.org +++ b/TODO.org @@ -866,10 +866,11 @@ Records not used for time tracking, but to store data associated with a date or [] ...) #+END_SRC -** tagging dates with key-values :feature: +** STARTED tagging dates with key-values :feature: :PROPERTIES: :CREATED: 2022-02-10T22:59:45+0530 :CUSTOM_ID: date-key-values +:branch: date-properties :END: Straightforward enough for the plist group backend - #+BEGIN_SRC elisp @@ -910,6 +911,7 @@ The plist backend could theoretically be extended to support it, but I'd rather * STARTED Support for the Third Time System [57%] :extension: :PROPERTIES: :CREATED: 2022-02-10T14:12:12+0530 +:branch: third-time :END: [[https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work][Third Time: a better way to work]] #+BEGIN_QUOTE From 38f2b6239c0e295416da4f4b8a7d40450c43c717 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 02:59:12 +0530 Subject: [PATCH 30/56] Remove duplicate footnote --- manual.md | 24 +++++++++++------------- manual.org | 4 +--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/manual.md b/manual.md index 2528a76..e1eb305 100644 --- a/manual.md +++ b/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](#orga55ac83) + 4. [chronometrist-details](#org09eaa12) 5. [common commands](#usage-common-commands) 6. [Time goals/targets](#time-goals) 6. [How-to](#how-to) @@ -21,15 +21,15 @@ 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](#orgae7b0f6) + 5. [How to skip running hooks/attaching tags and key values](#orga569ce7) 6. [How to open certain files when you start a task](#how-to-open-files-on-task-start) 7. [How to warn yourself about uncommitted changes](#how-to-warn-uncommitted-changes) 8. [How to display the current time interval in the activity indicator](#how-to-activity-indicator) 9. [How to back up your Chronometrist data](#how-to-backup) 10. [How to configure Vertico for use with Chronometrist](#howto-vertico) -7. [Explanation](#org15771af) +7. [Explanation](#org2a03f3a) 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#org020377a) +8. [User's reference](#org2585c1f) 9. [Contributions and contact](#contributions-contact) 10. [License](#license) 11. [Thanks](#thanks) @@ -158,7 +158,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. - + ## chronometrist-details @@ -224,7 +224,7 @@ 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 2 - +1. Add `chronometrist-kv-add` to one or more of these hooks 1 - (add-to-list 'chronometrist-after-in-functions 'chronometrist-kv-add) (add-to-list 'chronometrist-before-out-functions 'chronometrist-kv-add) @@ -233,7 +233,7 @@ Evaluate or add to your init.el the following - To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press `C-c C-c` to accept the key-values, or `C-c C-k` to cancel. - + ## How to skip running hooks/attaching tags and key values @@ -299,7 +299,7 @@ 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 using the [async-backup](https://tildegit.org/contrapunctus/async-backup) package.3 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.2 Here's how you can do that. 1. Add the following to your init. @@ -332,7 +332,7 @@ Or use `vertico-multiform` to disable sorting for only specific commands - (chronometrist-key-values-unified-prompt (vertico-sort-function . nil))))) - + # Explanation @@ -348,7 +348,7 @@ The Org file can also be loaded directly using the [literate-elisp](https://gith `chronometrist.org` is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads. - + # User's reference @@ -425,6 +425,4 @@ fiete and wu-lee for testing and bug reports 1 but not `chronometrist-before-in-functions` -2 but not `chronometrist-before-in-functions` - -3 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. +2 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. diff --git a/manual.org b/manual.org index f222612..0d8abfa 100644 --- a/manual.org +++ b/manual.org @@ -190,7 +190,7 @@ Evaluate or add to your init.el the following - :CUSTOM_ID: how-to-key-value-pairs :END: -1. Add =chronometrist-kv-add= to one or more of these hooks [fn:3] - +1. Add =chronometrist-kv-add= to one or more of these hooks [fn:2] - #+BEGIN_SRC emacs-lisp (add-to-list 'chronometrist-after-in-functions 'chronometrist-kv-add) @@ -200,8 +200,6 @@ Evaluate or add to your init.el the following - To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press =C-c C-c= to accept the key-values, or =C-c C-k= to cancel. -[fn:3] but not =chronometrist-before-in-functions= - ** How to skip running hooks/attaching tags and key values Use =M-RET= (=chronometrist-toggle-task-no-hooks=) to clock in/out. From d69944b81ce39cc091de2c5e5c3944438bf96dcc Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 16:03:48 +0530 Subject: [PATCH 31/56] Correct tags, add docstring --- elisp/chronometrist.el | 1 + elisp/chronometrist.org | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 61c6387..d6e6688 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -1105,6 +1105,7 @@ hash table values must be in chronological order.") ;; [[file:chronometrist.org::*on-file-path-change][on-file-path-change:1]] (cl-defmethod chronometrist-on-file-path-change ((backend chronometrist-file-backend-mixin) _old-path new-path) + "Update path and file slots of BACKEND to use NEW-PATH when `chronometrist-file' is changed." (with-slots (path extension file) backend (setf path new-path file (concat path "." extension)))) diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 176cc47..0d9d25c 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -1810,9 +1810,10 @@ These can be implemented in terms of the minimal protocol above. (find-file-noselect file))))) #+END_SRC -**** on-file-path-change :generic:function: +**** on-file-path-change :writer:method: #+BEGIN_SRC emacs-lisp (cl-defmethod chronometrist-on-file-path-change ((backend chronometrist-file-backend-mixin) _old-path new-path) + "Update path and file slots of BACKEND to use NEW-PATH when `chronometrist-file' is changed." (with-slots (path extension file) backend (setf path new-path file (concat path "." extension)))) From cdcc059b26e4c922d94bb67521c6c87e84fa5642 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 13 Feb 2022 17:07:18 +0530 Subject: [PATCH 32/56] Add notes on multiple intervals/time deduction --- TODO.org | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.org b/TODO.org index d2cb667..6121749 100644 --- a/TODO.org +++ b/TODO.org @@ -855,6 +855,9 @@ With different checks, for different levels of speed/thoroughness - #+END_SRC It doesn't do anything not already possible in the current formats. At best, it removes some duplication when the same task is "paused" and "resumed". ++ Sometimes you pause and resume a task and don't want to split your key-values between >1 intervals (to avoid messing up completion suggestions for the future). An alternative means to the same end could be to add a key like =:deduct ""= or =:deduct ("" . "")=. + - This will also make it easier to support formats like [[https://klog.jotaen.net/][klog]], which support this feature. + - It will probably complicate all data consuming code, though...think of =chronometrist-details= 🤔 ** "event records" - records with only a timestamp :feature: :PROPERTIES: From fe9ed8c7f01d80abcd1846aa82a0f9df680298d3 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 09:00:31 +0530 Subject: [PATCH 33/56] Add customizable alerts idea --- TODO.org | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/TODO.org b/TODO.org index 6121749..79281d4 100644 --- a/TODO.org +++ b/TODO.org @@ -737,7 +737,9 @@ for )" #+END_SRC * STARTED customizable duration output :feature: -See branch =format-seconds=. +:PROPERTIES: +:branch: format-seconds +:END: 1. [X] define =chronometrist-duration-formats= to hold duration formats for different use cases in Chronometrist. 2. [ ] define customization type for =chronometrist-duration-formats= to create user-friendly Custom interface 3. [ ] make non-tabular parts of Chronometrist buffers adapt to column widths, to accomodate changes in duration formats @@ -933,6 +935,11 @@ Additional rules: + /Avoid taking other unearned breaks/ if possible — so try to do personal tasks during normal or big breaks, or before/after your work day. #+END_QUOTE +Example flow +1. work for 30m +2. clock out - add 10m to =break-time= +3. clock in after a 5m break - subtract 5m from =break-time= + 1. [X] =chronometrist-third-fraction= 2. [X] =chronometrist-third-break-time= 3. [X] on clock out, increment =break-time= and start timed notification @@ -950,8 +957,23 @@ Extras 3. handle user edits to the database * last record modified - ? * remove last record - ? + * example - interval extended + 1. work 10 minutes and clock out - +3m break time + 2. edit stop time to add +20 minutes of work (30m total) + * compare with old data in hash table - decrement break time added by old plist, increment break time added by new plist -Example flow -1. work for 30m -2. clock out - add 10m to =break-time= -3. clock in after a 5m break - subtract 5m from =break-time= +* Customizable alerts +:PROPERTIES: +:CREATED: 2022-02-14T08:22:36+0530 +:END: +=chronometrist-third= and =chronometrist-goal= have nearly identical alert code; additionally, users cannot customize the alert style per-alert without basically rewriting the alert functions. + +Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-functions= to customizable alists whose entries take the form =(SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS)=, where ++ =SYMBOL= uniquely identifies the alert, ++ =FUNCTION= is a function calling =chronometrist-*-run-at-time= and =ALERT-FN=, and ++ =ALERT-ARGS= are passed to =ALERT-FN=. + +=ALERT-FN= will usually be =alert=, and =ALERT-ARGS= will usually be keyword arguments passed to =alert=. +Similar to how they behave now, these packages will start/stop functions for all entries provided in these alists. (So the user can still control which alerts are run.) + +Also define =chronometrist--timer-alist=, which associates =symbol= with a timer object. From e7fd4df27e56611084e9f778aa6e8c3d8fef1948 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 09:00:54 +0530 Subject: [PATCH 34/56] Split Third Time section into headings --- TODO.org | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO.org b/TODO.org index 79281d4..dd5a4dc 100644 --- a/TODO.org +++ b/TODO.org @@ -935,11 +935,12 @@ Additional rules: + /Avoid taking other unearned breaks/ if possible — so try to do personal tasks during normal or big breaks, or before/after your work day. #+END_QUOTE -Example flow +** Example flow 1. work for 30m 2. clock out - add 10m to =break-time= 3. clock in after a 5m break - subtract 5m from =break-time= +** Tasks 1. [X] =chronometrist-third-fraction= 2. [X] =chronometrist-third-break-time= 3. [X] on clock out, increment =break-time= and start timed notification @@ -951,7 +952,7 @@ Example flow * [ ] Hook function which prompts for work hours whenever you first clock in on a date. If work hours are defined in the custom variable, ask whether to use them - on negative answer, prompt for today's work hours. If work hours are not defined, prompt for today's work hours. 7. [ ] Displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time. -Extras +** Extras 1. persist =break-time= between Emacs sessions 2. audible alerts 3. handle user edits to the database From a5c709253f785babe924fb6b7528464bcee2d86f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 13:05:02 +0530 Subject: [PATCH 35/56] Add CUSTOM_ID, macro idea --- TODO.org | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index dd5a4dc..81a5db5 100644 --- a/TODO.org +++ b/TODO.org @@ -966,10 +966,11 @@ Additional rules: * Customizable alerts :PROPERTIES: :CREATED: 2022-02-14T08:22:36+0530 +:CUSTOM_ID: customizable-alerts :END: =chronometrist-third= and =chronometrist-goal= have nearly identical alert code; additionally, users cannot customize the alert style per-alert without basically rewriting the alert functions. -Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-functions= to customizable alists whose entries take the form =(SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS)=, where +Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-functions= to customizable alists[fn:4] whose entries take the form =(SYMBOL FUNCTION ALERT-FN &rest ALERT-ARGS)=, where + =SYMBOL= uniquely identifies the alert, + =FUNCTION= is a function calling =chronometrist-*-run-at-time= and =ALERT-FN=, and + =ALERT-ARGS= are passed to =ALERT-FN=. @@ -978,3 +979,5 @@ Convert =chronometrist-third-alert-functions= and =chronometrist-goal-alert-func Similar to how they behave now, these packages will start/stop functions for all entries provided in these alists. (So the user can still control which alerts are run.) Also define =chronometrist--timer-alist=, which associates =symbol= with a timer object. + +[fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. From 2eed71ad8565169a3639b79bebdad951ffc7c7ff Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 20:47:12 +0530 Subject: [PATCH 36/56] Add 'canned key-values' idea --- TODO.org | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.org b/TODO.org index 81a5db5..b767c5d 100644 --- a/TODO.org +++ b/TODO.org @@ -860,6 +860,7 @@ It doesn't do anything not already possible in the current formats. At best, it + Sometimes you pause and resume a task and don't want to split your key-values between >1 intervals (to avoid messing up completion suggestions for the future). An alternative means to the same end could be to add a key like =:deduct ""= or =:deduct ("" . "")=. - This will also make it easier to support formats like [[https://klog.jotaen.net/][klog]], which support this feature. - It will probably complicate all data consuming code, though...think of =chronometrist-details= 🤔 + - An alternative idea could be to define a custom variable to hold the user's key values. If this variable is defined, it would be used instead of generating suggestions from past key-values. That way, such situations will not affect key-value suggestions. ** "event records" - records with only a timestamp :feature: :PROPERTIES: From d88619b15fd42102aeaa4b86fe4eaefd1f3cb256 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Mon, 14 Feb 2022 22:13:00 +0530 Subject: [PATCH 37/56] Update TODO for alert macro --- TODO.org | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index b767c5d..5856815 100644 --- a/TODO.org +++ b/TODO.org @@ -964,10 +964,11 @@ Additional rules: 2. edit stop time to add +20 minutes of work (30m total) * compare with old data in hash table - decrement break time added by old plist, increment break time added by new plist -* Customizable alerts +* STARTED Customizable alerts [50%] :PROPERTIES: :CREATED: 2022-02-14T08:22:36+0530 :CUSTOM_ID: customizable-alerts +:branch: alert-macro :END: =chronometrist-third= and =chronometrist-goal= have nearly identical alert code; additionally, users cannot customize the alert style per-alert without basically rewriting the alert functions. @@ -981,4 +982,7 @@ Similar to how they behave now, these packages will start/stop functions for all Also define =chronometrist--timer-alist=, which associates =symbol= with a timer object. +1. [X] write macro +2. [ ] use in =chronometrist-goal= and =chronometrist-third= + [fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. From 27ebc824fa90a239aab55cbbde3030ede80fbb6f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 01:26:15 +0530 Subject: [PATCH 38/56] Correct :nexport: tag --- elisp/chronometrist-key-values.org | 2 +- elisp/chronometrist-spark.org | 2 +- elisp/chronometrist-third.org | 2 +- elisp/chronometrist.org | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/elisp/chronometrist-key-values.org b/elisp/chronometrist-key-values.org index 37a4767..1d11d78 100644 --- a/elisp/chronometrist-key-values.org +++ b/elisp/chronometrist-key-values.org @@ -741,7 +741,7 @@ Return t, to permit use in `chronometrist-before-out-functions'." ;;; chronometrist-key-values.el ends here #+END_SRC -* Local variables :NOEXPORT: +* Local variables :noexport: # Local Variables: # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # End: diff --git a/elisp/chronometrist-spark.org b/elisp/chronometrist-spark.org index cb645cf..c06ff16 100644 --- a/elisp/chronometrist-spark.org +++ b/elisp/chronometrist-spark.org @@ -171,7 +171,7 @@ SCHEMA should be a vector as specified by `tabulated-list-format'." ;;; chronometrist-spark.el ends here #+END_SRC -* Local variables :NOEXPORT: +* Local variables :noexport: # Local Variables: # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # End: diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 6841004..4c40bb0 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -206,7 +206,7 @@ break time is up." ;;; chronometrist-third.el ends here #+END_SRC -* Local variables :NOEXPORT: +* Local variables :noexport: # Local Variables: # my-org-src-default-lang: "emacs-lisp" # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 176cc47..032fef1 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -2851,7 +2851,7 @@ Return value is either a list in the form collect plist)) #+END_SRC -***** TODO active-days :reader:method:NOEXPORT: +***** TODO active-days :reader:method:noexport: #+BEGIN_SRC emacs-lisp :tangle no (cl-defmethod chronometrist-active-days ((backend chronometrist-plist-group-backend) task &key start end) (cl-check-type task string) @@ -2876,7 +2876,7 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method t)) #+END_SRC -***** count-records :reader:method:NOEXPORT: +***** count-records :reader:method:noexport: #+BEGIN_SRC emacs-lisp :tangle no (cl-defmethod chronometrist-count-records ((backend chronometrist-plist-group-backend))) #+END_SRC @@ -4763,7 +4763,7 @@ 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 From 456fe451abea3e5a53f5470b1bfca34dd45f4dec Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 12:32:07 +0530 Subject: [PATCH 39/56] Add heading tags --- TODO.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 5856815..cefa47e 100644 --- a/TODO.org +++ b/TODO.org @@ -964,7 +964,7 @@ Additional rules: 2. edit stop time to add +20 minutes of work (30m total) * compare with old data in hash table - decrement break time added by old plist, increment break time added by new plist -* STARTED Customizable alerts [50%] +* STARTED Customizable alerts [50%] :feature:code: :PROPERTIES: :CREATED: 2022-02-14T08:22:36+0530 :CUSTOM_ID: customizable-alerts From 4638b040ff3d1128b6a7db132cd255c0c45a837d Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 12:35:31 +0530 Subject: [PATCH 40/56] Use headings for new backend items --- TODO.org | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/TODO.org b/TODO.org index cefa47e..9c76ba1 100644 --- a/TODO.org +++ b/TODO.org @@ -799,12 +799,18 @@ protocol implementation progress 20. [ ] [superclass] chronometrist-timer * New backends :feature: -1. Org time tracking -2. timeclock -3. [[https://klog.jotaen.net/][klog]] -4. SQLite -5. https://github.com/projecthamster/hamster - + https://github.com/projecthamster/hamster/wiki/Our-datamodel +** Org time tracking +** timeclock +See docstring of =timeclock-log-data= + +** [[https://klog.jotaen.net/][klog]] +:PROPERTIES: +:CUSTOM_ID: new-backends-klog +:END: +** SQLite +** Project Hamster +https://github.com/projecthamster/hamster ++ https://github.com/projecthamster/hamster/wiki/Our-datamodel * Functions doing similar things :code: 1. =task-time-one-day= @@ -858,7 +864,7 @@ With different checks, for different levels of speed/thoroughness - It doesn't do anything not already possible in the current formats. At best, it removes some duplication when the same task is "paused" and "resumed". + Sometimes you pause and resume a task and don't want to split your key-values between >1 intervals (to avoid messing up completion suggestions for the future). An alternative means to the same end could be to add a key like =:deduct ""= or =:deduct ("" . "")=. - - This will also make it easier to support formats like [[https://klog.jotaen.net/][klog]], which support this feature. + - This will also make it easier to support formats like [[#new-backends-klog][klog]], which support this feature. - It will probably complicate all data consuming code, though...think of =chronometrist-details= 🤔 - An alternative idea could be to define a custom variable to hold the user's key values. If this variable is defined, it would be used instead of generating suggestions from past key-values. That way, such situations will not affect key-value suggestions. From b222ecc35f4f25c3f2f51f8247f22eb2177fc6c9 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 12:48:19 +0530 Subject: [PATCH 41/56] Update section "customizable task list" and CHANGELOG --- CHANGELOG.md | 2 +- TODO.org | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0046a..50e3151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 1. Multiple backend support - new custom variable `chronometrist-active-backend` to determine active backend, new command `chronometrist-switch-backend` to temporarily select a backend (with completion). 2. New `plist-group` backend, reducing time taken in startup and after changes to the file. 3. Unified migration interface with command `chronometrist-migrate`. -4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database. +4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database. Setting it also disables generation of the task list from the database, speeding up many operations. 5. New command `chronometrist-discard-active`, to discard the active interval. 6. Debug logging messages - to view them, set `chronometrist-debug-enable`. 7. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system. diff --git a/TODO.org b/TODO.org index 9c76ba1..43e2a8b 100644 --- a/TODO.org +++ b/TODO.org @@ -818,9 +818,10 @@ https://github.com/projecthamster/hamster 3. =events-to-durations= * Use ISO date for functions operating on dates :time:format: -* STARTED customizable task list :feature: -1. Interactive, buffer-local modification of task list, with completion (=completing-read-multiple=) -2. Adding a task? We can modify the task list, but how to persist it? +* STARTED customizable task list [33%] :feature: +1. [X] Make =chronometrist-task-list= customizable +2. [ ] Interactive, buffer-local modification of task list, with completion (=completing-read-multiple=) +3. [ ] Adding a task? We can modify the task list, but how to persist it? * Extend time range prompt :feature: Support inputs like "today", "yesterday", "5 days ago", etc. From 9cdc5e5cbbe9dd611a330d1da99b122780d1c5bf Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 18:16:58 +0530 Subject: [PATCH 42/56] Tweak task --- TODO.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 43e2a8b..a2c60aa 100644 --- a/TODO.org +++ b/TODO.org @@ -919,7 +919,7 @@ The plist backend could theoretically be extended to support it, but I'd rather :CREATED: 2022-01-08T23:32:37+0530 :END: 1. [ ] default suggested input backend should be the active backend -2. [ ] conserve file local variables prop line +2. [ ] conserve file local variables prop line for text backends * STARTED Support for the Third Time System [57%] :extension: :PROPERTIES: From 12e11945a21b68288020f3581dae523a98725669 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 20:47:18 +0530 Subject: [PATCH 43/56] Rename "events" -> "ht" --- elisp/chronometrist.el | 51 +++++++++++++++++++++-------------------- elisp/chronometrist.org | 47 ++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index d6e6688..1cbe6c5 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -315,8 +315,8 @@ Return value is a ts struct (see `ts.el')." (plist-put :stop stop-2)))))))) ;; split-plist:1 ends here -;; [[file:chronometrist.org::*events-update][events-update:1]] -(defun chronometrist-events-update (plist hash-table &optional replace) +;; [[file:chronometrist.org::*ht-update][ht-update:1]] +(defun chronometrist-ht-update (plist hash-table &optional replace) "Return HASH-TABLE with PLIST added as the latest interval. If REPLACE is non-nil, replace the last interval with PLIST." (let* ((date (->> (plist-get plist :start) @@ -327,28 +327,29 @@ If REPLACE is non-nil, replace the last interval with PLIST." (append it (list plist)) (puthash date it hash-table)) hash-table)) -;; events-update:1 ends here +;; ht-update:1 ends here -;; [[file:chronometrist.org::*last-date][last-date:1]] -(defun chronometrist-events-last-date (hash-table) +;; [[file:chronometrist.org::*ht-last-date][ht-last-date:1]] +(defun chronometrist-ht-last-date (hash-table) "Return an ISO-8601 date string for the latest date present in `chronometrist-events'." (--> (hash-table-keys hash-table) + (sort it #'string-lessp) (last it) - (car it))) -;; last-date:1 ends here + (cl-first it))) +;; ht-last-date:1 ends here -;; [[file:chronometrist.org::*events-last][events-last:1]] -(cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend))) +;; [[file:chronometrist.org::*ht-last][ht-last:1]] +(cl-defun chronometrist-ht-last (&optional (backend (chronometrist-active-backend))) "Return the last plist from `chronometrist-events'." (let* ((hash-table (chronometrist-backend-hash-table backend)) - (last-date (chronometrist-events-last-date hash-table))) + (last-date (chronometrist-ht-last-date hash-table))) (--> (gethash last-date hash-table) (last it) (car it)))) -;; events-last:1 ends here +;; ht-last:1 ends here -;; [[file:chronometrist.org::#program-data-structures-events-subset][events-subset:1]] -(defun chronometrist-events-subset (start end hash-table) +;; [[file:chronometrist.org::#program-data-structures-ht-subset][ht-subset:1]] +(defun chronometrist-ht-subset (start end hash-table) "Return a subset of HASH-TABLE. The subset will contain values between dates START and END (both inclusive). @@ -363,7 +364,7 @@ treated as though their time is 00:00:00." (puthash key value subset))) hash-table) subset)) -;; events-subset:1 ends here +;; ht-subset:1 ends here ;; [[file:chronometrist.org::*task-time-one-day][task-time-one-day:1]] (cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend))) @@ -1388,7 +1389,7 @@ STREAM (which is the value of `current-buffer')." (chronometrist-backend-run-assertions backend) (with-slots (hash-table) backend (when-let* - ((latest-date (chronometrist-events-last-date hash-table)) + ((latest-date (chronometrist-ht-last-date hash-table)) (records (gethash latest-date hash-table))) (cons latest-date records)))) ;; latest-date-records:1 ends here @@ -1497,7 +1498,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file." (with-slots (hash-table) backend (-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)] - (setf hash-table (chronometrist-events-update new-plist hash-table)) + (setf hash-table (chronometrist-ht-update new-plist hash-table)) (chronometrist-add-to-task-list new-task backend)))) ;; on-add:1 ends here @@ -1507,8 +1508,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file is modified." (with-slots (hash-table) backend (-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend)) - ((&plist :name old-task) (chronometrist-events-last backend))) - (setf hash-table (chronometrist-events-update new-plist hash-table t)) + ((&plist :name old-task) (chronometrist-ht-last backend))) + (setf hash-table (chronometrist-ht-update new-plist hash-table t)) (chronometrist-remove-from-task-list old-task backend) (chronometrist-add-to-task-list new-task backend)))) ;; on-modify:1 ends here @@ -1518,8 +1519,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe "Function run when the newest plist in a `chronometrist-plist-backend' file is deleted." (with-slots (hash-table) backend - (-let (((&plist :name old-task) (chronometrist-events-last)) - (date (chronometrist-events-last-date hash-table))) + (-let (((&plist :name old-task) (chronometrist-ht-last)) + (date (chronometrist-ht-last-date hash-table))) ;; `chronometrist-remove-from-task-list' checks the hash table to determine ;; if `chronometrist-task-list' is to be updated. Thus, the hash table must ;; not be updated until the task list is. @@ -1788,7 +1789,7 @@ Return value is either a list in the form `chronometrist-plist-group-backend' file is modified." (with-slots (hash-table) backend (-let* (((date . plists) (chronometrist-latest-date-records backend)) - (old-date (chronometrist-events-last-date hash-table)) + (old-date (chronometrist-ht-last-date hash-table)) (old-plists (gethash old-date hash-table))) (puthash date plists hash-table) (cl-loop for plist in old-plists @@ -1802,7 +1803,7 @@ Return value is either a list in the form "Function run when the newest plist-group in a `chronometrist-plist-group-backend' file is deleted." (with-slots (hash-table) backend - (-let* ((old-date (chronometrist-events-last-date hash-table)) + (-let* ((old-date (chronometrist-ht-last-date hash-table)) (old-plists (gethash old-date hash-table))) (cl-loop for plist in old-plists do (chronometrist-remove-from-task-list (plist-get plist :name) backend)) @@ -2946,7 +2947,7 @@ displayed. They must be ts structs (see `ts.el').") It simply operates on the entire hash table TABLE (see `chronometrist-to-hash-table' for table format), so ensure that TABLE is reduced to the desired range using -`chronometrist-events-subset'." +`chronometrist-ht-subset'." (cl-loop for task in (chronometrist-task-list) collect (let* ((active-days (chronometrist-statistics-count-active-days task table)) (active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode) @@ -2975,12 +2976,12 @@ reduced to the desired range using ('week (let* ((start (plist-get chronometrist-statistics--ui-state :start)) (end (plist-get chronometrist-statistics--ui-state :end)) - (ht (chronometrist-events-subset start end hash-table))) + (ht (chronometrist-ht-subset start end hash-table))) (chronometrist-statistics-rows-internal ht))) (t ;; `chronometrist-statistics--ui-state' is nil, show current week's data (let* ((start (chronometrist-previous-week-start (chronometrist-date-ts))) (end (ts-adjust 'day 7 start)) - (ht (chronometrist-events-subset start end hash-table))) + (ht (chronometrist-ht-subset start end hash-table))) (setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end)) (chronometrist-statistics-rows-internal ht)))))) ;; rows:1 ends here diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 0d9d25c..4348bc2 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -833,9 +833,9 @@ Return value is a ts struct (see `ts.el')." :stop "2021-02-13T00:03:46+0530"))))) #+END_SRC -*** events-update :writer: +*** ht-update :writer: #+BEGIN_SRC emacs-lisp -(defun chronometrist-events-update (plist hash-table &optional replace) +(defun chronometrist-ht-update (plist hash-table &optional replace) "Return HASH-TABLE with PLIST added as the latest interval. If REPLACE is non-nil, replace the last interval with PLIST." (let* ((date (->> (plist-get plist :start) @@ -848,33 +848,34 @@ If REPLACE is non-nil, replace the last interval with PLIST." hash-table)) #+END_SRC -*** last-date :reader: +*** ht-last-date :reader: #+BEGIN_SRC emacs-lisp -(defun chronometrist-events-last-date (hash-table) +(defun chronometrist-ht-last-date (hash-table) "Return an ISO-8601 date string for the latest date present in `chronometrist-events'." (--> (hash-table-keys hash-table) + (sort it #'string-lessp) (last it) - (car it))) + (cl-first it))) #+END_SRC -*** events-last :reader: +*** ht-last :reader: #+BEGIN_SRC emacs-lisp -(cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend))) +(cl-defun chronometrist-ht-last (&optional (backend (chronometrist-active-backend))) "Return the last plist from `chronometrist-events'." (let* ((hash-table (chronometrist-backend-hash-table backend)) - (last-date (chronometrist-events-last-date hash-table))) + (last-date (chronometrist-ht-last-date hash-table))) (--> (gethash last-date hash-table) (last it) (car it)))) #+END_SRC -*** events-subset :reader: +*** ht-subset :reader: :PROPERTIES: :VALUE: hash table -:CUSTOM_ID: program-data-structures-events-subset +:CUSTOM_ID: program-data-structures-ht-subset :END: #+BEGIN_SRC emacs-lisp -(defun chronometrist-events-subset (start end hash-table) +(defun chronometrist-ht-subset (start end hash-table) "Return a subset of HASH-TABLE. The subset will contain values between dates START and END (both inclusive). @@ -2311,7 +2312,7 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl (chronometrist-backend-run-assertions backend) (with-slots (hash-table) backend (when-let* - ((latest-date (chronometrist-events-last-date hash-table)) + ((latest-date (chronometrist-ht-last-date hash-table)) (records (gethash latest-date hash-table))) (cons latest-date records)))) #+END_SRC @@ -2427,7 +2428,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file." (with-slots (hash-table) backend (-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)] - (setf hash-table (chronometrist-events-update new-plist hash-table)) + (setf hash-table (chronometrist-ht-update new-plist hash-table)) (chronometrist-add-to-task-list new-task backend)))) #+END_SRC @@ -2438,8 +2439,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe `chronometrist-plist-backend' file is modified." (with-slots (hash-table) backend (-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend)) - ((&plist :name old-task) (chronometrist-events-last backend))) - (setf hash-table (chronometrist-events-update new-plist hash-table t)) + ((&plist :name old-task) (chronometrist-ht-last backend))) + (setf hash-table (chronometrist-ht-update new-plist hash-table t)) (chronometrist-remove-from-task-list old-task backend) (chronometrist-add-to-task-list new-task backend)))) #+END_SRC @@ -2450,8 +2451,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe "Function run when the newest plist in a `chronometrist-plist-backend' file is deleted." (with-slots (hash-table) backend - (-let (((&plist :name old-task) (chronometrist-events-last)) - (date (chronometrist-events-last-date hash-table))) + (-let (((&plist :name old-task) (chronometrist-ht-last)) + (date (chronometrist-ht-last-date hash-table))) ;; `chronometrist-remove-from-task-list' checks the hash table to determine ;; if `chronometrist-task-list' is to be updated. Thus, the hash table must ;; not be updated until the task list is. @@ -2786,7 +2787,7 @@ Return value is either a list in the form `chronometrist-plist-group-backend' file is modified." (with-slots (hash-table) backend (-let* (((date . plists) (chronometrist-latest-date-records backend)) - (old-date (chronometrist-events-last-date hash-table)) + (old-date (chronometrist-ht-last-date hash-table)) (old-plists (gethash old-date hash-table))) (puthash date plists hash-table) (cl-loop for plist in old-plists @@ -2801,7 +2802,7 @@ Return value is either a list in the form "Function run when the newest plist-group in a `chronometrist-plist-group-backend' file is deleted." (with-slots (hash-table) backend - (-let* ((old-date (chronometrist-events-last-date hash-table)) + (-let* ((old-date (chronometrist-ht-last-date hash-table)) (old-plists (gethash old-date hash-table))) (cl-loop for plist in old-plists do (chronometrist-remove-from-task-list (plist-get plist :name) backend)) @@ -4102,7 +4103,7 @@ displayed. They must be ts structs (see `ts.el').") It simply operates on the entire hash table TABLE (see `chronometrist-to-hash-table' for table format), so ensure that TABLE is reduced to the desired range using -`chronometrist-events-subset'." +`chronometrist-ht-subset'." (cl-loop for task in (chronometrist-task-list) collect (let* ((active-days (chronometrist-statistics-count-active-days task table)) (active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode) @@ -4132,12 +4133,12 @@ reduced to the desired range using ('week (let* ((start (plist-get chronometrist-statistics--ui-state :start)) (end (plist-get chronometrist-statistics--ui-state :end)) - (ht (chronometrist-events-subset start end hash-table))) + (ht (chronometrist-ht-subset start end hash-table))) (chronometrist-statistics-rows-internal ht))) (t ;; `chronometrist-statistics--ui-state' is nil, show current week's data (let* ((start (chronometrist-previous-week-start (chronometrist-date-ts))) (end (ts-adjust 'day 7 start)) - (ht (chronometrist-events-subset start end hash-table))) + (ht (chronometrist-ht-subset start end hash-table))) (setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end)) (chronometrist-statistics-rows-internal ht)))))) #+END_SRC @@ -4561,7 +4562,7 @@ range.") #+END_SRC **** intervals-for-range :reader: -This is basically like [[#program-data-structures-events-subset][chronometrist-events-subset]], but returns a list instead of a hash table. Might replace one with the other in the future. +This is basically like [[#program-data-structures-ht-subset][chronometrist-ht-subset]], but returns a list instead of a hash table. Might replace one with the other in the future. #+BEGIN_SRC emacs-lisp (defun chronometrist-details-intervals-for-range (range table) From 2c1274147475b552716de7cecd7a9fd46e578e46 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Tue, 15 Feb 2022 21:03:25 +0530 Subject: [PATCH 44/56] Update versions for release --- CHANGELOG.md | 4 +++- elisp/chronometrist.el | 2 +- elisp/chronometrist.org | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0046a..ab0ef24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## unreleased +1. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system. + +## [0.10.0] - 2022-02-15 ### Changed 1. The value of `chronometrist-file` must now be a file path _without extension._ Please update your configurations. 2. The existing file format used by Chronometrist is now called the `plist` format. @@ -17,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database. 5. New command `chronometrist-discard-active`, to discard the active interval. 6. Debug logging messages - to view them, set `chronometrist-debug-enable`. -7. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system. ### Fixed 1. Code to detect the type of change made to the file has been rewritten, hopefully fixing some uncommon `read` errors and `args out of range` errors. diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 1cbe6c5..3bfcc5c 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -5,7 +5,7 @@ ;; Keywords: calendar ;; Homepage: https://tildegit.org/contrapunctus/chronometrist ;; Package-Requires: ((emacs "27.1") (dash "2.16.0") (seq "2.20") (ts "0.2")) -;; Version: 0.9.0 +;; Version: 0.10.0 ;; This is free and unencumbered software released into the public domain. ;; diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index 4348bc2..18b2e46 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -412,7 +412,7 @@ But I discovered that if I do that, =package-lint= says - =error: Couldn't parse ;; Keywords: calendar ;; Homepage: https://tildegit.org/contrapunctus/chronometrist ;; Package-Requires: ((emacs "27.1") (dash "2.16.0") (seq "2.20") (ts "0.2")) -;; Version: 0.9.0 +;; Version: 0.10.0 ;; This is free and unencumbered software released into the public domain. ;; From dd8660ee736f0920c771ded8eb4d82112d42c4ee Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 17 Feb 2022 23:33:07 +0530 Subject: [PATCH 45/56] Add tasks for implementing date properties --- TODO.org | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index a2c60aa..631fb15 100644 --- a/TODO.org +++ b/TODO.org @@ -879,7 +879,7 @@ Records not used for time tracking, but to store data associated with a date or [] ...) #+END_SRC -** STARTED tagging dates with key-values :feature: +** STARTED tagging dates with key-values [0%] :feature: :PROPERTIES: :CREATED: 2022-02-10T22:59:45+0530 :CUSTOM_ID: date-key-values @@ -904,6 +904,10 @@ Alternatively, this would be easier to work with - easier to select the key-valu The plist backend could theoretically be extended to support it, but I'd rather deprecate that format (since it suffers from performance issues) and not deal with it again. +1. [ ] Update call sites of =chronometrist-latest-date-records= (which may now also contain key-values) +2. [ ] Update code accessing the hash table +3. [ ] Update =plist-pp= to handle + * undesired file watcher created after literate-elisp-load :bug: :PROPERTIES: :CREATED: 2021-12-18T15:13:35+0530 From 6b56be318d7363cffd17d90227f903ec2990078d Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 17 Feb 2022 23:33:38 +0530 Subject: [PATCH 46/56] Move tasks into main heading --- TODO.org | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/TODO.org b/TODO.org index 631fb15..b4c2596 100644 --- a/TODO.org +++ b/TODO.org @@ -947,12 +947,6 @@ Additional rules: + /Avoid taking other unearned breaks/ if possible — so try to do personal tasks during normal or big breaks, or before/after your work day. #+END_QUOTE -** Example flow -1. work for 30m -2. clock out - add 10m to =break-time= -3. clock in after a 5m break - subtract 5m from =break-time= - -** Tasks 1. [X] =chronometrist-third-fraction= 2. [X] =chronometrist-third-break-time= 3. [X] on clock out, increment =break-time= and start timed notification @@ -964,6 +958,11 @@ Additional rules: * [ ] Hook function which prompts for work hours whenever you first clock in on a date. If work hours are defined in the custom variable, ask whether to use them - on negative answer, prompt for today's work hours. If work hours are not defined, prompt for today's work hours. 7. [ ] Displaying/recording breaks. Probably done implicitly - when work hours are defined, any time not spent working can be interpreted as break time. +** Example flow +1. work for 30m +2. clock out - add 10m to =break-time= +3. clock in after a 5m break - subtract 5m from =break-time= + ** Extras 1. persist =break-time= between Emacs sessions 2. audible alerts From 08e53cfea3a6957fd7a904d9fce5f6ef93479445 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 17 Feb 2022 23:44:08 +0530 Subject: [PATCH 47/56] Add predefined key-values idea --- TODO.org | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/TODO.org b/TODO.org index b4c2596..72a37e6 100644 --- a/TODO.org +++ b/TODO.org @@ -855,6 +855,9 @@ With different checks, for different levels of speed/thoroughness - * format changes ** multiple intervals per record :feature: +:PROPERTIES: +:CUSTOM_ID: multiple-intervals-per-record +:END: #+BEGIN_SRC emacs-lisp (:name "" [] ... @@ -996,3 +999,16 @@ Also define =chronometrist--timer-alist=, which associates =symbol= with a timer 2. [ ] use in =chronometrist-goal= and =chronometrist-third= [fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. + +* Predefined key-values for tasks [100%] +:PROPERTIES: +:CREATED: 2022-02-17T23:34:17+0530 +:END: +Benefits +1. Speeds up key-value completion for very large data sets. +2. Prevents key-value suggestions from being affected by [[#multiple-intervals-per-record][splitting of tasks across multiple intervals]]. + +#+BEGIN_SRC emacs-lisp +'(("Task" ...) + ...) +#+END_SRC From 6556ee0bded3c5458ee70eb04039711b3252c887 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 20 Feb 2022 13:33:01 +0530 Subject: [PATCH 48/56] Add screenshot --- doc/2022-02-20 13-26-53.png | Bin 0 -> 57452 bytes manual.md | 30 ++++++++++++++++++------------ manual.org | 14 ++++++++++---- 3 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 doc/2022-02-20 13-26-53.png diff --git a/doc/2022-02-20 13-26-53.png b/doc/2022-02-20 13-26-53.png new file mode 100644 index 0000000000000000000000000000000000000000..4a6098071b5f0335531e1bf90152b743ea6774d6 GIT binary patch literal 57452 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 literal 0 HcmV?d00001 diff --git a/manual.md b/manual.md index e1eb305..565a698 100644 --- a/manual.md +++ b/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](#org09eaa12) + 4. [chronometrist-details](#org6ee0fb7) 5. [common commands](#usage-common-commands) 6. [Time goals/targets](#time-goals) 6. [How-to](#how-to) @@ -21,26 +21,30 @@ 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](#orga569ce7) + 5. [How to skip running hooks/attaching tags and key values](#orgfbe4680) 6. [How to open certain files when you start a task](#how-to-open-files-on-task-start) 7. [How to warn yourself about uncommitted changes](#how-to-warn-uncommitted-changes) 8. [How to display the current time interval in the activity indicator](#how-to-activity-indicator) 9. [How to back up your Chronometrist data](#how-to-backup) 10. [How to configure Vertico for use with Chronometrist](#howto-vertico) -7. [Explanation](#org2a03f3a) +7. [Explanation](#org25cd1c5) 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#org2585c1f) +8. [User's reference](#org5f69f60) 9. [Contributions and contact](#contributions-contact) 10. [License](#license) 11. [Thanks](#thanks) -Donate using Liberapay + + Donate using Liberapay + - + + + -A time tracker in Emacs with a nice interface +Chronometrist is a friendly and powerful personal time tracker and analyzer for Emacs. -Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker) +![img](doc/2022-02-20 13-26-53.png "Screenshot of the main Chronometrist buffer, with the enabled extensions [chronometrist-goal](#time-goals) ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks).") @@ -158,7 +162,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. - + ## chronometrist-details @@ -233,7 +237,7 @@ Evaluate or add to your init.el the following - To exit the prompt, press the key it indicates for quitting - you can then edit the resulting key-values by hand if required. Press `C-c C-c` to accept the key-values, or `C-c C-k` to cancel. - + ## How to skip running hooks/attaching tags and key values @@ -332,7 +336,7 @@ Or use `vertico-multiform` to disable sorting for only specific commands - (chronometrist-key-values-unified-prompt (vertico-sort-function . nil))))) - + # Explanation @@ -348,7 +352,7 @@ The Org file can also be loaded directly using the [literate-elisp](https://gith `chronometrist.org` is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads. - + # User's reference @@ -412,6 +416,8 @@ Chronometrist is released under your choice of [Unlicense](https://unlicense.org # Thanks +The main buffer and the report buffer are copied from the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker) + 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 diff --git a/manual.org b/manual.org index 0d8abfa..b7c2174 100644 --- a/manual.org +++ b/manual.org @@ -4,14 +4,19 @@ #+HTML_HEAD: #+BEGIN_EXPORT html -Donate using Liberapay + + Donate using Liberapay + - + + + #+END_EXPORT -A time tracker in Emacs with a nice interface +Chronometrist is a friendly and powerful personal time tracker and analyzer for Emacs. -Largely modelled after the Android application, [[https://github.com/netmackan/ATimeTracker][A Time Tracker]] +#+CAPTION: The main Chronometrist buffer, with the enabled extensions [[#time-goals][chronometrist-goal]] ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks). +[[file:doc/2022-02-20 13-26-53.png]] * Benefits :PROPERTIES: @@ -371,6 +376,7 @@ Chronometrist is released under your choice of [[https://unlicense.org/][Unlicen :PROPERTIES: :CUSTOM_ID: thanks :END: +The main buffer and the report buffer are copied from the Android application, [[https://github.com/netmackan/ATimeTracker][A Time Tracker]] wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support From ba560d68578df308c8ba85dc4f17933a15d839f8 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 20 Feb 2022 15:48:47 +0530 Subject: [PATCH 49/56] Use manual.org as readme --- README.md | 1 - README.org | 1 + manual.md | 434 ----------------------------------------------------- manual.org | 1 - 4 files changed, 1 insertion(+), 436 deletions(-) delete mode 120000 README.md create mode 120000 README.org delete mode 100644 manual.md diff --git a/README.md b/README.md deleted file mode 120000 index 9d0493a..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -manual.md \ No newline at end of file diff --git a/README.org b/README.org new file mode 120000 index 0000000..e3c17fe --- /dev/null +++ b/README.org @@ -0,0 +1 @@ +manual.org \ No newline at end of file diff --git a/manual.md b/manual.md deleted file mode 100644 index 565a698..0000000 --- a/manual.md +++ /dev/null @@ -1,434 +0,0 @@ - -# Table of Contents - -1. [Benefits](#benefits) -2. [Limitations](#limitations) -3. [Comparisons](#comparisons) - 1. [timeclock.el](#timeclock.el) - 2. [Org time tracking](#org-time-tracking) -4. [Installation](#installation) - 1. [from MELPA](#install-from-melpa) - 2. [from Git](#install-from-git) -5. [Usage](#usage) - 1. [chronometrist](#usage-chronometrist) - 2. [chronometrist-report](#usage-chronometrist-report) - 3. [chronometrist-statistics](#usage-chronometrist-statistics) - 4. [chronometrist-details](#org6ee0fb7) - 5. [common commands](#usage-common-commands) - 6. [Time goals/targets](#time-goals) -6. [How-to](#how-to) - 1. [How to display a prompt when exiting with an active task](#how-to-prompt-when-exiting-emacs) - 2. [How to load the program using literate-elisp](#how-to-literate-elisp) - 3. [How to attach tags to time intervals](#how-to-tags) - 4. [How to attach key-values to time intervals](#how-to-key-value-pairs) - 5. [How to skip running hooks/attaching tags and key values](#orgfbe4680) - 6. [How to open certain files when you start a task](#how-to-open-files-on-task-start) - 7. [How to warn yourself about uncommitted changes](#how-to-warn-uncommitted-changes) - 8. [How to display the current time interval in the activity indicator](#how-to-activity-indicator) - 9. [How to back up your Chronometrist data](#how-to-backup) - 10. [How to configure Vertico for use with Chronometrist](#howto-vertico) -7. [Explanation](#org25cd1c5) - 1. [Literate Program](#explanation-literate-program) -8. [User's reference](#org5f69f60) -9. [Contributions and contact](#contributions-contact) -10. [License](#license) -11. [Thanks](#thanks) - - - Donate using Liberapay - - - - - - -Chronometrist is a friendly and powerful personal time tracker and analyzer for Emacs. - -![img](doc/2022-02-20 13-26-53.png "Screenshot of the main Chronometrist buffer, with the enabled extensions [chronometrist-goal](#time-goals) ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks).") - - - - -# Benefits - -1. Extremely simple and efficient to use -2. Displays useful information about your time usage -3. Support for both mouse and keyboard -4. Human errors in tracking are easily fixed by editing a plain text file -5. Hooks to let you perform arbitrary actions when starting/stopping tasks -6. Fancy graphs with the `chronometrist-spark` extension - - - - -# Limitations - -1. 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 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. - - - - -# Installation - - - - -## from MELPA - -1. Set up MELPA - - - (Chronometrist uses Semantic Versioning and the developer is accident-prone, so using MELPA Stable is suggested 😏) -2. `M-x package-install RET chronometrist RET` - - - - -## from Git - -You can get `chronometrist` from or - -`chronometrist` requires - -- Emacs v25 or higher -- [dash.el](https://github.com/magnars/dash.el) -- [ts.el](https://github.com/alphapapa/ts.el) - -Add the "elisp/" subdirectory to your load-path, and `(require 'chronometrist)`. - - - - -# Usage - - - - -## chronometrist - -Run `M-x chronometrist` to see your projects, the time you spent on them today, which one is active, and the total time clocked today. - -Click or hit `RET` (`chronometrist-toggle-task`) on a project to start tracking time for it. If it's already clocked in, it will be clocked out. - -You can also hit ` RET` anywhere in the buffer to toggle the corresponding project, e.g. =C-1 RET= will toggle the project with index 1. - -Press `r` to see a weekly report (see `chronometrist-report`) - - - - -## chronometrist-report - -Run `M-x chronometrist-report` (or `chronometrist` with a prefix argument of 1, or press `r` in the `chronometrist` buffer) to see a weekly report. - -Press `b` to look at past weeks, and `f` for future weeks. - - - - -## chronometrist-statistics - -Run `M-x chronometrist-statistics` (or `chronometrist` with a prefix argument of 2) to view statistics. - -Press `b` to look at past time ranges, and `f` for future ones. - - - - -## chronometrist-details - - - - -## common commands - -In the buffers created by the previous three commands, you can press `l` (`chronometrist-open-log`) to view/edit your `chronometrist-file`, which by default is `~/.emacs.d/chronometrist.sexp`. - -All of these commands will kill their buffer when run again with the buffer visible, so the keys you bind them to behave as a toggle. - -All buffers keep themselves updated via an idle timer - no need to frequently press `g` to update. - - - - -## Time goals/targets - -If you wish you could define time goals for some tasks, and have Chronometrist notify you when you're approaching the goal, completing it, or exceeding it, check out the extension [chronometrist-goal.el](https://github.com/contrapunctus-1/chronometrist-goal/). - - - - -# How-to - -See the Customize groups `chronometrist` and `chronometrist-report` for variables intended to be user-customizable. - - - - -## How to display a prompt when exiting with an active task - -Evaluate or add to your init.el the following - -`(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)` - - - - -## How to load the program using literate-elisp - - (add-to-list 'load-path "") - - (require 'literate-elisp) ;; or autoload, use-package, ... - (literate-elisp-load "chronometrist.org") - - - - -## How to attach tags to time intervals - -1. Add `chronometrist-tags-add` to one or more of these hooks 1 - - - (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`. - - - - -## How to attach key-values to time intervals - -1. Add `chronometrist-kv-add` to one or more of these hooks 1 - - - (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. - - - - -## How to skip running hooks/attaching tags and key values - -Use `M-RET` (`chronometrist-toggle-task-no-hooks`) to clock in/out. - - - - -## How to open certain files when you start a task - -An idea from the author's own init - - - (defun my-start-project (project) - (pcase project - ("Guitar" - (find-file-other-window "~/repertoire.org")) - ;; ... - )) - - (add-hook 'chronometrist-before-in-functions 'my-start-project) - - - - -## How to warn yourself about uncommitted changes - -Another one, prompting the user if they have uncommitted changes in a git repository (assuming they use [Magit](https://magit.vc/)) - - - (autoload 'magit-anything-modified-p "magit") - - (defun my-commit-prompt () - "Prompt user if `default-directory' is a dirty Git repository. - Return t if the user answers yes, if the repository is clean, or - if there is no Git repository. - - Return nil (and run `magit-status') if the user answers no." - (cond ((not (magit-anything-modified-p)) t) - ((yes-or-no-p - (format "You have uncommitted changes in %S. Really clock out? " - default-directory)) t) - (t (magit-status) nil))) - - (add-hook 'chronometrist-before-out-functions 'my-commit-prompt) - - - - -## How to display the current time interval in the activity indicator - - (defun my-activity-indicator () - (--> (chronometrist-latest-record (chronometrist-active-backend)) - (plist-put it :stop (chronometrist-format-time-iso8601)) - (list it) - (chronometrist-events-to-durations it) - (-reduce #'+ it) - (truncate it) - (chronometrist-format-duration it))) - - (setq chronometrist-activity-indicator #'my-activity-indicator) - - - - -## How to back up your Chronometrist data - -I suggest backing up Chronometrist data on each save using the [async-backup](https://tildegit.org/contrapunctus/async-backup) package.2 Here's how you can do that. - -1. Add the following to your init. - - (use-package async-backup) -2. Open your Chronometrist file and add `async-backup` to a buffer-local `after-save-hook`. - - M-x chronometrist-open-log - M-x add-file-local-variable-prop-line RET eval RET (add-hook 'after-save-hook #'async-backup nil t) RET -3. Optionally, configure `backup-directory-alist` to set a specific directory for the backups. - - - - -## 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))))) - - - - -# Explanation - - - - -## Literate Program - -Chronometrist is a literate program, made using Org - the canonical source is the `chronometrist.org` file, which contains source blocks. These are provided to users after *tangling* (extracting the source into an Emacs Lisp file). - -The Org file can also be loaded directly using the [literate-elisp](https://github.com/jingtaozf/literate-elisp) package, so that all source links (e.g. `xref`, `describe-function`) lead to the Org file, within the context of the concerned documentation. See [How to load the program using literate-elisp](#how-to-literate-elisp). - -`chronometrist.org` is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads. - - - - -# User's reference - -All variables intended for user customization are listed here. They serve as the public API for this project for the purpose of semantic versioning. Any changes to these which require a user to modify their configuration are considered breaking changes. - -1. `chronometrist-file` -2. `chronometrist-buffer-name` -3. `chronometrist-report-buffer-name` -4. `chronometrist-details-buffer-name` -5. `chronometrist-sexp-pretty-print-function` -6. `chronometrist-hide-cursor` -7. `chronometrist-update-interval` -8. `chronometrist-activity-indicator` - -Buffer schemas - -1. `chronometrist-schema` -2. `chronometrist-details-schema` - -Hooks - -1. `chronometrist-mode-hook` -2. `chronometrist-schema-transformers` -3. `chronometrist-row-transformers` -4. `chronometrist-before-in-functions` -5. `chronometrist-after-in-functions` -6. `chronometrist-before-out-functions` -7. `chronometrist-after-out-functions` -8. `chronometrist-file-change-hook` -9. `chronometrist-timer-hook` - - - - -# Contributions and contact - -Feedback and MRs are very welcome. 🙂 - -- has a long list of tasks -- contains all developer-oriented documentation - -If you have tried using Chronometrist, I'd love to hear your experiences! Get in touch with the author and other Emacs users in the Emacs channel on the Jabber network - [xmpp:emacs@salas.suchat.org?join](https://conversations.im/j/emacs@salas.suchat.org) ([web chat](https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org)) - -(For help in getting started with Jabber, [click here](https://xmpp.org/getting-started/)) - - - - -# License - -I dream of a world where all software is liberated - transparent, trustable, and accessible for anyone to use or improve. But I don't want to make demands or threats (e.g. via legal conditions) to get there. - -I'd rather make a request - please do everything you can to help that dream come true. Please Unlicense as much software as you can. - -Chronometrist is released under your choice of [Unlicense](https://unlicense.org/) or the [WTFPL](http://www.wtfpl.net/). - -(See files [UNLICENSE](UNLICENSE) and [WTFPL](WTFPL)). - - - - -# Thanks - -The main buffer and the report buffer are copied from the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker) - -wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support - -jwiegley for `timeclock.el`, which we used as a backend in earlier versions - -blandest for helping me with the name - -fiete and wu-lee for testing and bug reports - - -# Footnotes - -1 but not `chronometrist-before-in-functions` - -2 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. diff --git a/manual.org b/manual.org index b7c2174..55733ec 100644 --- a/manual.org +++ b/manual.org @@ -388,6 +388,5 @@ 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: From c51f4a45609b99d694de6cfad0d99d854b8e4af4 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Sun, 20 Feb 2022 16:46:48 +0530 Subject: [PATCH 50/56] Remove MELPA Stable recommendation --- manual.org | 2 -- 1 file changed, 2 deletions(-) diff --git a/manual.org b/manual.org index 55733ec..0de28d9 100644 --- a/manual.org +++ b/manual.org @@ -72,8 +72,6 @@ Chronometrist and Org time tracking seem to be equivalent in terms of capabiliti :END: 1. Set up MELPA - https://melpa.org/#/getting-started - - (Chronometrist uses Semantic Versioning and the developer is accident-prone, so using MELPA Stable is suggested 😏) 2. =M-x package-install RET chronometrist RET= ** from Git From 3d2bc8149e06f5e713de903f5e51967bc8c2f31f Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Wed, 23 Feb 2022 14:52:53 +0530 Subject: [PATCH 51/56] Format old-break duration like other durations --- elisp/chronometrist-third.el | 18 +++++++++--------- elisp/chronometrist-third.org | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index 14a1062..d443dca 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -131,19 +131,19 @@ and stopped when they clock in." "Stop alert timers and update break time." (chronometrist-third-stop-alert-timers) (unless (zerop chronometrist-third-break-time) - (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) - (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) - (new-break-time (- chronometrist-third-break-time used-break-duration)) - (old-break-time chronometrist-third-break-time) - (used-break-duration-string (format-seconds chronometrist-third-duration-format - used-break-duration))) - (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0)) + (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) + (used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) + (used-break-string (format-seconds chronometrist-third-duration-format used-break)) + (new-break (- chronometrist-third-break-time used-break)) + (old-break chronometrist-third-break-time)) + (setq chronometrist-third-break-time (if (> new-break 0) new-break 0)) (alert (if (zerop chronometrist-third-break-time) (format "You have used up all %s of your break time (%s break)" - old-break-time used-break-duration-string) + (format-seconds chronometrist-third-duration-format old-break) + used-break-string) (format "You have used %s of your break time (%s left)" - used-break-duration-string + used-break-string (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) ;; clock-in:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 4c40bb0..57ec5de 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -155,19 +155,19 @@ and stopped when they clock in." "Stop alert timers and update break time." (chronometrist-third-stop-alert-timers) (unless (zerop chronometrist-third-break-time) - (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) - (used-break-duration (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) - (new-break-time (- chronometrist-third-break-time used-break-duration)) - (old-break-time chronometrist-third-break-time) - (used-break-duration-string (format-seconds chronometrist-third-duration-format - used-break-duration))) - (setq chronometrist-third-break-time (if (> new-break-time 0) new-break-time 0)) + (-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend)))) + (used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop))) + (used-break-string (format-seconds chronometrist-third-duration-format used-break)) + (new-break (- chronometrist-third-break-time used-break)) + (old-break chronometrist-third-break-time)) + (setq chronometrist-third-break-time (if (> new-break 0) new-break 0)) (alert (if (zerop chronometrist-third-break-time) (format "You have used up all %s of your break time (%s break)" - old-break-time used-break-duration-string) + (format-seconds chronometrist-third-duration-format old-break) + used-break-string) (format "You have used %s of your break time (%s left)" - used-break-duration-string + used-break-string (format-seconds chronometrist-third-duration-format chronometrist-third-break-time))))))) #+END_SRC From 7334be5743f27a965bcf1a2ad06650d7fa8d907b Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Wed, 23 Feb 2022 20:47:46 +0530 Subject: [PATCH 52/56] Add key-value presets Inspired by the Android application "My Expenses" by Michael Totschnig. --- CHANGELOG.md | 3 ++ elisp/chronometrist-key-values.el | 61 +++++++++++++++++------- elisp/chronometrist-key-values.org | 76 +++++++++++++++++++++++------- 3 files changed, 105 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74730c9..4562961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## unreleased +### Added 1. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system. +2. New custom variable `chronometrist-key-value-preset-alist`, to define completion suggestions in advance. +3. New custom variable `chronometrist-key-value-use-database-history`, to control whether database history is used for key-value suggestions. ## [0.10.0] - 2022-02-15 ### Changed diff --git a/elisp/chronometrist-key-values.el b/elisp/chronometrist-key-values.el index 468c28e..66e10a5 100644 --- a/elisp/chronometrist-key-values.el +++ b/elisp/chronometrist-key-values.el @@ -192,6 +192,29 @@ used in `chronometrist-before-out-functions'." "Add key-values to Chronometrist time intervals." :group 'chronometrist) +(defcustom chronometrist-key-value-use-database-history t + "If non-nil, use database to generate key-value suggestions. +If nil, only `chronometrist-key-value-preset-alist' is used." + :type 'boolean + :group 'chronometrist-key-value) + +(defcustom chronometrist-key-value-preset-alist nil + "Alist of key-value suggestions for `chronometrist-key-value' prompts. +Each element must be in the form (\"TASK\" ...)" + :type + '(repeat + (cons + (string :tag "Task name") + (repeat :tag "Property preset" + (plist :tag "Property" + ;; :key-type 'keyword :value-type 'sexp + )))) + :group 'chronometrist-key-values) + +(defun chronometrist-key-value-get-presets (task) + "Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists." + (alist-get task chronometrist-key-value-preset-alist nil nil #'equal)) + (defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*" "Name of buffer in which key-values are entered." :group 'chronometrist-key-values @@ -404,30 +427,34 @@ used in `chronometrist-before-out-functions'." ["Change tags and key-values for active/last interval" chronometrist-key-values-unified-prompt])) -(cl-defun chronometrist-key-values-unified-prompt (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name))) +(cl-defun chronometrist-key-values-unified-prompt + (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name))) "Query user for tags and key-values to be added for TASK. Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) + (presets (chronometrist-key-value-get-presets task)) (key-values - (cl-loop for plist in (chronometrist-to-list backend) - when (equal (plist-get plist :name) task) - collect - (let ((plist (chronometrist-plist-remove plist :name :start :stop))) - (when plist (format "%S" plist))) - into key-value-plists - finally return - (--> (seq-filter #'identity key-value-plists) - (cl-remove-duplicates it :test #'equal :from-end t)))) + (when chronometrist-key-value-use-database-history + (cl-loop for plist in (chronometrist-to-list backend) + when (equal (plist-get plist :name) task) + collect + (let ((plist (chronometrist-plist-remove plist :name :start :stop))) + (when plist (format "%S" plist))) + into key-value-plists + finally return + (--> (seq-filter #'identity key-value-plists) + (cl-remove-duplicates it :test #'equal :from-end t))))) (latest (chronometrist-latest-record backend))) - (if (null key-values) + (if (and (null presets) (null key-values)) (progn (chronometrist-tags-add) (chronometrist-kv-add)) - (chronometrist-replace-last - backend - (chronometrist-plist-update - latest - (read (completing-read (format "Key-values for %s: " task) - key-values nil nil nil 'chronometrist-key-values-unified-prompt-history)))))) + (let* ((candidates (append presets key-values)) + (input (completing-read + (format "Key-values for %s: " task) + candidates nil nil nil 'chronometrist-key-values-unified-prompt-history))) + (chronometrist-replace-last backend + (chronometrist-plist-update latest + (read input)))))) t) (provide 'chronometrist-key-values) diff --git a/elisp/chronometrist-key-values.org b/elisp/chronometrist-key-values.org index 1d11d78..389b02f 100644 --- a/elisp/chronometrist-key-values.org +++ b/elisp/chronometrist-key-values.org @@ -330,6 +330,39 @@ used in `chronometrist-before-out-functions'." "Add key-values to Chronometrist time intervals." :group 'chronometrist) #+END_SRC + +*** use-database-history :custom:variable: +#+BEGIN_SRC emacs-lisp +(defcustom chronometrist-key-value-use-database-history t + "If non-nil, use database to generate key-value suggestions. +If nil, only `chronometrist-key-value-preset-alist' is used." + :type 'boolean + :group 'chronometrist-key-value) +#+END_SRC + +*** preset-alist :custom:variable: +#+BEGIN_SRC emacs-lisp +(defcustom chronometrist-key-value-preset-alist nil + "Alist of key-value suggestions for `chronometrist-key-value' prompts. +Each element must be in the form (\"TASK\" ...)" + :type + '(repeat + (cons + (string :tag "Task name") + (repeat :tag "Property preset" + (plist :tag "Property" + ;; :key-type 'keyword :value-type 'sexp + )))) + :group 'chronometrist-key-values) +#+END_SRC + +**** get-presets +#+BEGIN_SRC emacs-lisp +(defun chronometrist-key-value-get-presets (task) + "Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists." + (alist-get task chronometrist-key-value-preset-alist nil nil #'equal)) +#+END_SRC + *** kv-buffer-name :custom:variable: #+BEGIN_SRC emacs-lisp (defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*" @@ -337,6 +370,7 @@ used in `chronometrist-before-out-functions'." :group 'chronometrist-key-values :type 'string) #+END_SRC + *** key-history :variable: :PROPERTIES: :VALUE: hash table @@ -707,34 +741,39 @@ Return t, to permit use in `chronometrist-before-out-functions'." :CUSTOM_ID: unified-prompt :END: 1. [ ] Improve appearance - is there an easy way to syntax highlight the plists? + #+BEGIN_SRC emacs-lisp -(cl-defun chronometrist-key-values-unified-prompt (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name))) +(cl-defun chronometrist-key-values-unified-prompt + (&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name))) "Query user for tags and key-values to be added for TASK. Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) + (presets (chronometrist-key-value-get-presets task)) (key-values - (cl-loop for plist in (chronometrist-to-list backend) - when (equal (plist-get plist :name) task) - collect - (let ((plist (chronometrist-plist-remove plist :name :start :stop))) - (when plist (format "%S" plist))) - into key-value-plists - finally return - (--> (seq-filter #'identity key-value-plists) - (cl-remove-duplicates it :test #'equal :from-end t)))) + (when chronometrist-key-value-use-database-history + (cl-loop for plist in (chronometrist-to-list backend) + when (equal (plist-get plist :name) task) + collect + (let ((plist (chronometrist-plist-remove plist :name :start :stop))) + (when plist (format "%S" plist))) + into key-value-plists + finally return + (--> (seq-filter #'identity key-value-plists) + (cl-remove-duplicates it :test #'equal :from-end t))))) (latest (chronometrist-latest-record backend))) - (if (null key-values) + (if (and (null presets) (null key-values)) (progn (chronometrist-tags-add) (chronometrist-kv-add)) - (chronometrist-replace-last - backend - (chronometrist-plist-update - latest - (read (completing-read (format "Key-values for %s: " task) - key-values nil nil nil 'chronometrist-key-values-unified-prompt-history)))))) + (let* ((candidates (append presets key-values)) + (input (completing-read + (format "Key-values for %s: " task) + candidates nil nil nil 'chronometrist-key-values-unified-prompt-history))) + (chronometrist-replace-last backend + (chronometrist-plist-update latest + (read input)))))) t) - #+END_SRC + * Provide #+BEGIN_SRC emacs-lisp (provide 'chronometrist-key-values) @@ -743,5 +782,6 @@ Return t, to permit use in `chronometrist-before-out-functions'." * Local variables :noexport: # Local Variables: +# my-org-src-default-lang: "emacs-lisp" # eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name))) # End: From 9ef3337c5a68caf73866a6949bb5783d7e246979 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 24 Feb 2022 15:47:06 +0530 Subject: [PATCH 53/56] Tweak default duration format --- elisp/chronometrist-third.el | 2 +- elisp/chronometrist-third.org | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/elisp/chronometrist-third.el b/elisp/chronometrist-third.el index d443dca..0995691 100644 --- a/elisp/chronometrist-third.el +++ b/elisp/chronometrist-third.el @@ -42,7 +42,7 @@ ;; divisor:1 ends here ;; [[file:chronometrist-third.org::*duration-format][duration-format:1]] -(defcustom chronometrist-third-duration-format "%H, %M, and %S%z" +(defcustom chronometrist-third-duration-format "%H, %M and %S%z" "Format string for durations, passed to `format-seconds'." :type 'string) ;; duration-format:1 ends here diff --git a/elisp/chronometrist-third.org b/elisp/chronometrist-third.org index 57ec5de..9eb49ef 100644 --- a/elisp/chronometrist-third.org +++ b/elisp/chronometrist-third.org @@ -56,7 +56,7 @@ ** duration-format :custom:variable: #+BEGIN_SRC emacs-lisp -(defcustom chronometrist-third-duration-format "%H, %M, and %S%z" +(defcustom chronometrist-third-duration-format "%H, %M and %S%z" "Format string for durations, passed to `format-seconds'." :type 'string) #+END_SRC From 3962c63f9314ebe7b4d7a1ba048d33d4925ba781 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 25 Feb 2022 00:27:50 +0530 Subject: [PATCH 54/56] Mark task done; document chronometrist-goal bug --- TODO.org | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 72a37e6..928ce00 100644 --- a/TODO.org +++ b/TODO.org @@ -1000,7 +1000,7 @@ Also define =chronometrist--timer-alist=, which associates =symbol= with a timer [fn:4] Actually, make a macro to define such alists, since the docstrings are likely to be near-identical. -* Predefined key-values for tasks [100%] +* DONE Predefined key-values for tasks :PROPERTIES: :CREATED: 2022-02-17T23:34:17+0530 :END: @@ -1012,3 +1012,11 @@ Benefits '(("Task" ...) ...) #+END_SRC + +* no-goal-alert - "You have spent on " [0%] :bug: +:PROPERTIES: +:CREATED: 2022-02-25T00:19:49+0530 +:END: +=chronometrist-goal-no-goal-alert= uses =chronometrist-task-time-one-day=, which calls =chronometrist-task-records-for-date= with the /current/ date. + +1. [ ] Use the data for the latest date recorded in the database, rather than the current date. From 7cf2c86afd8f6fb6235320ac9f7ebd76153d8bc6 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Fri, 25 Feb 2022 15:20:21 +0530 Subject: [PATCH 55/56] Convert user presets to strings --- elisp/chronometrist-key-values.el | 3 ++- elisp/chronometrist-key-values.org | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/elisp/chronometrist-key-values.el b/elisp/chronometrist-key-values.el index 66e10a5..339fdd5 100644 --- a/elisp/chronometrist-key-values.el +++ b/elisp/chronometrist-key-values.el @@ -433,7 +433,8 @@ used in `chronometrist-before-out-functions'." Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) - (presets (chronometrist-key-value-get-presets task)) + (presets (--map (format "%S" it) + (chronometrist-key-value-get-presets task))) (key-values (when chronometrist-key-value-use-database-history (cl-loop for plist in (chronometrist-to-list backend) diff --git a/elisp/chronometrist-key-values.org b/elisp/chronometrist-key-values.org index 389b02f..3a1d891 100644 --- a/elisp/chronometrist-key-values.org +++ b/elisp/chronometrist-key-values.org @@ -749,7 +749,8 @@ Return t, to permit use in `chronometrist-before-out-functions'." Return t, to permit use in `chronometrist-before-out-functions'." (interactive) (let* ((backend (chronometrist-active-backend)) - (presets (chronometrist-key-value-get-presets task)) + (presets (--map (format "%S" it) + (chronometrist-key-value-get-presets task))) (key-values (when chronometrist-key-value-use-database-history (cl-loop for plist in (chronometrist-to-list backend) From e6c8ed785dd6c749f1812da39be964ae0fec6cc2 Mon Sep 17 00:00:00 2001 From: contrapunctus Date: Thu, 3 Mar 2022 13:11:20 +0530 Subject: [PATCH 56/56] Add task for better error reporting --- TODO.org | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/TODO.org b/TODO.org index 928ce00..29969e5 100644 --- a/TODO.org +++ b/TODO.org @@ -845,6 +845,9 @@ Command to delete the interval currently being recorded. (bind to 'k') ** put each list element on its own line if length of list exceeds fill-column :feature: * verify command [20%] :feature: +:PROPERTIES: +:CUSTOM_ID: verify-command +:END: With different checks, for different levels of speed/thoroughness - 1. [X] (plist group) Sequence of plist group dates 2. [ ] Check that every record (except the last) has a =:stop= @@ -1020,3 +1023,14 @@ Benefits =chronometrist-goal-no-goal-alert= uses =chronometrist-task-time-one-day=, which calls =chronometrist-task-records-for-date= with the /current/ date. 1. [ ] Use the data for the latest date recorded in the database, rather than the current date. + +* better error reporting +:PROPERTIES: +:CREATED: 2022-02-28T11:35:37+0530 +:END: +Similar to [[#verify-command][verify command]], perhaps, but this is really about making error messages thrown by the program more meaningful to the user. + +** record does not contain intervals +:PROPERTIES: +:CREATED: 2022-02-28T11:35:47+0530 +:END: