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

78 KiB
Raw Permalink Blame History

The Chronometrist TODO List

  1. Move time operations into a chronometrist-time file
  2. -report - highlight the current day

v0.3.0 [100%]

  1. Release or bundle plist-pp.el
  2. -report - error opening when clocked in

MELPA, hosting [0%]

  1. merge chronometrist-goal into this repository
  2. move project to codeberg.org or tildegit.org
  3. …and then, update the MELPA recipes.

Chronometrist

  1. implement point restore for chronometrist=/=chronometrist-report=/=chronometrist-statistics in a kill-buffer-hook, so it works whenever the buffer is killed, not just when we call the command.

Bugs [57%]

  1. fix -current-task to return correct results if the last one crosses a midnight
  2. Clocking-in/out doesn't result in updated display, sometimes (2019-09-12)
  3. Sorting by clicking the columns makes non-tabular data disappear

    • Advise tabulated-list-col-sort to run either chronometrist-refresh or the relevant -print-non-tabular afterwards
  4. kv-accept with no key values should not modify file
  5. kv-add and tags-add should not remove other keys from last sexp
  6. kv-accept with no key values -> Chronometrist buffer not updated

    • timer will only refresh when clocked in…
  7. regression - no value history
  8. Possible to add the same key-value twice

    • Steps

      1. Ensure that chronometrist-kv-add is present in chronometrist-before-out-functions
      2. Clock in
      3. Clock out
      4. Add key-value and accept
      5. Delete :stop time and save
      6. Clock out again
      7. Accept the already-inserted key-value. Duplicate!

Features [50%]

  1. Hook enhancement - can we supply the whole plist (including tags and key-values) to the task-start hooks, so users can have even smarter hook functions?

    • That would mean ensuring that -kv-read runs before other hooks.
    • Actually, it should be trivial to access the last expression in the file, so maybe this is unnecessary.
  2. Revisit 'no reason' commands - maybe we should ask for tags and key-values with the regular commands, and skip them with the 'no reason' variants?

Key-values [66%]

  1. bug - value-history appears in chronological rather than reverse chronological order
  2. generate history hash table from chronometrist-file.

    • generate value history hash table from chronometrist-file
    • change "press X to quit" in prompt to suggest keys for ido, helm, and ivy
  3. create text button to add key-values to last entry (whether clocked in or out)
  4. insert value as string if it contains spaces and isn't a list

    • Sometimes you want single-word values to be converted to a string, too. Maybe check for capital letters too?
    • Preserve type in history?

      • Might create inconsistency in prompts? We don't want to expect Lisp knowledge from users.
    • Ask user to define types for keys, and handle the syntax for them behind the scenes?
  5. when reading values, add quit keybinding (consistent with output of chronometrist-kv-completion-quit-key) by passing MAP to read-from-minibuffer
  6. bug - missing values in history
  7. enh - remove key-values from suggestions which have already been added
  8. enh - create custom variable to auto-insert key-values used in previous task of same :name in the key-value buffer.
  9. bug - incorrect indenting in -kv-buffer
  10. bug - I think -append-to-last-expr is eating key-values

    • to reproduce - clock-in, add tags, add key-values, clock-out, keep tags - kv-add buffer has (), whereas it should have the earlier key-values.
  11. bug - key-values from clock-in expression not displayed, shows () instead

    • cause - if -kv-add is run after chronometrist-out, chronometrist-current-task will return nil
  12. observe interaction of -kv-add with next function in hook which modifies window configuration

    • code which deletes the window, or switches to a new buffer, interferes with the key-value addition flow
    • add a kv-finished-hook? (run after kv-accept/reject)
  13. how do we handle a blank string as a plist value?

    • some way to require the user to enter a non-empty value (e.g. like the 'require-match' argument to completing-read, except read-from-minibuffer doesn't have that…)
    • discard last-entered key?
  14. optimize - going through key (?) and value histories (M-p/M-n) takes a while. Is this because we're using lists?

    • It doesn't, it's the delay from the hash table update from the fs watch
  15. bug - tag history starts at the beginning (wtf?), value history is empty (wtf?)

    • Does not occur on master, only on dev
    • Does not occur if you disable lexical binding (introduced in 4e89836)
Values

What forms can they take?

  1. Integers, floating point numbers - easy to identify via regexp
  2. If it starts and ends with "(" and ")" or "[" and "]", it's a list or a vector
  3. otherwise - string

Observations

  • This means you can't enter symbols via prompt. Can be added if the demand is there…
  • This also means you can't have multiple atom values for a keyword…but that's irrelevant, because plists can't have multiple values anyway. :)

Tags [100%]

  1. generate history from chronometrist-file

    • narrow it down to the :name
  2. write tags to last expression
  3. show task name in prompt
  4. bug - tags being added twice

Code [28%]

  1. Write tests (in buttercup)
  2. refactor repetitive calls to (format "%04d-%02d-%02d" (elt seq a) (elt seq b) (elt seq c))
  3. See if it is possible to store buttons in a variable, so *-print-non-tabular functions can be made shorter and less imperative. (see make-text-button)
  4. Merge all event-querying functions so that they always operate on an entire hash table (so no 'day' variants),
  5. Use substitute-command-keys instead of chronometrist-format-keybinds
  6. recreate -events-clean, remove splitting code from -events-populate

    • How should we deal with the active event?

      • Earlier, we would add a closing entry and update that on a timer.
  7. Make docstrings consistent - describe inputs and then the return value, in that order.
  8. chronometrist-seconds->alert-string can probably be replaced by org-duration-from-minutes - read the format for FMT
  9. Decouple storage-related code from rest of the program.
  10. See if using iteration constructs (especially loop) can lead to nicer code than nested maps

1. use variables instead of hardcoded numbers to determine spacing

  • Don't see the benefit

6. Timeclock already has hooks! :| Why do we re-implement them?

  • I don't know of a way to know the project being clocked into using timeclock hooks.
  • With v0.2.0 Chronometrist also has a before-project-stop-functions, which runs before the project is stopped, and can control whether the project actually is stopped.

Maybe

  1. Add a new kind of plist - (:name "NAME" :time "TIME" ...) To record events for which the time interval is not relevant. These won't be shown in chronometrist - perhaps in a different buffer.

Optimization

DONE Don't update task list from data when user has set their own

When clocking in/out, do not save file until hooks are run

Currently, the file is probably being saved twice - once when we insert/update the record, and once again when a hook function modifies and saves the file. If this is the case, the file-notify callback is probably called twice.

See branch reduce-saves

Don't save file until exit and/or Emacs idle

A risky strategy - if there is unsaved data during an unclean exit, it will be lost.

DONE Deferred (tag/key/value) history generation

Defer (tag/key/value) history generation from file-change-time to prompt-time, and make it per-task instead of all tasks at once

  • The biggest resource hog is splitting of midnight-spanning intervals, however.
  • Reduce memory use by allowing user to restrict number of s-expressions read.
  • Per-task history generation will create problems - e.g. values for a given key for one task won't be suggested for values for the same key in another 🤦

    • Tags and keys are already task-sensitive; just don't make values task-sensitive.

DONE Hash file contents to optimize for common changes

Compare partial hashes of file to know what has changed - only update memory when necessary.

In-memory cache

Don't store entire file into memory; instead, split midnight-spanning intervals just for the requested data.

  • Will increase load time for each forward/backward command in chronometrist-report and chronometrist-statistics
  • Will reduce memory used by chronometrist-events.

    • Further reductions can take place, if we automatically discard cache entries past a certain limit. (perhaps excluding data for the current day, week, or month)

Split and save midnight-spanning intervals to disk

It will remove the need for an in-memory version of data with split midnight-spanning intervals.

  • Least memory use?
  • Might make the file harder for a user to edit.

Save timestamps as UNIX epoch time

  • Will (probably) greatly speed up time parsing and interval splitting.
  • Will greatly impede human editing of the file, too. 🤔

    • An editing UI could help - pretty sure every timestamp edit I've ever made has been for the last interval, or at most an interval in today's data.

      • The editing UI could have commands for next/previous interval; one could also have a command which, in the file, opens the plist at point for editing.

Use an SQL database instead of a text file

That is, assuming SQL can

  1. find the difference between ISO-8601 timestamps
  2. compare ISO-8601 timestamps, and
  3. do 1 and 2 faster than Elisp.

Change data structure

Instead of storing each plist as-is, split each into two, one with the :start and one with the :end. Now we have the elegance of the one-plist-is-a-complete-interval schema in the file, and the ease and speed of detection of midnight spanning intervals in memory.

So this

(:name "Task" ... :start "<timestamp>" :stop "<timestamp>")

is stored in hash table values as

((:name "Task" ... :start "<timestamp>")
 (:stop "<timestamp>")
 ...)

Change file timestamp format to ("<iso-date>" "<iso-time>")

Change file schema

Pros

  1. Easy to read data for a single date - Chronometrist's most common operation.
  2. No more expensive operations on each startup (date checking, splitting of midnight spanning intervals).
  3. May be easier to check for midnight-spanning intervals - plists are already grouped by date, just check the first and the last plist per date. Which means, if a user neglects to split a midnight-spanning interval, we can do it for them.

    • Make this checking - and also checking for ordering of keys and plists - a command which the user can run at will, rather than something being run automatically.
  4. May reduce memory and disk space used - hash table keys are already dates, so :start and :stop can just be ISO times rather than date-times.

Cons

  1. Opting to remove dates from intervals might make it difficult for users to recover from erroneous edits.

Store plists grouped by date

(:date "YYYY-MM-DD"
       (:name "task 1"
              ...
              :start "HH:MM:SSZ"
              :stop "HH:MM:SSZ")
       ...)
;; or
("YYYY-MM-DD"
 (:name "task 1"
        ...
        :start "HH:MM:SSZ"
        :stop "HH:MM:SSZ")
 ...)

Cons

  1. Users will have to be disciplined while editing, and ensure that events are split on day boundaries. If not, we will have to check each time the file changes, defeating the entire optimization.

    • Not quite - it's less work to test that, when the intervals are grouped by date.

Persist hash table to file

Instead of storing plists in the file, persist the hash table itself.

Pros

  • When editing manually, the whole file can be reindented with a single sp-indent-defun.
  • Stored data can be modified by any Emacs Lisp functions rather than functions specific to our file format.

    • This may also be possible with plists grouped by date, if we serialize the hash table as plists rather than - as we do now - directly adding individual plists to the file.

Cons

  1. Read syntax not portable to other dialects or languages, e.g. if one wants to make an Android frontend in LambdaNative (Gambit Scheme)

  2. Slightly noisier syntax than plain plists.
  3. Users cannot create comments within the hash table expression.

    • Couldn't they use plist keys for comments instead? 🤔
  4. May increase memory consumption - the entire hash table will be loaded into memory.

Cache

  • Lessons from the parsimonious-reading branch - iterating read over the whole file is fast; splitting the intervals is not.
  • Things we need to read the whole file for - task list, tag/key/value history.
  • Fill chronometrist-events only as much as the buffer needing split intervals requires. e.g. for chronometrist, just a day; for chronometrist-report, a week; etc.
  • Anything requiring split intervals will first look in chronometrist-events, and if not found, will read from the file and update chronometrist-events.
  • When the file changes, use the file byte length and hash strategy described below to know whether to keep the cache.
  • Save cache to a file, so that event splitting is avoided by reading from that.

(old) ideas to make -refresh-file faster

  1. Support multiple files, so we read and process lesser data when one of them changes.
  2. Make file writing async
  3. Don't refresh from file when clocking in.
  4. Only write to the file when Emacs is idle or being killed, and store data in memory (in the intervals hash table) in the meantime
  5. What if commands both write to the file and add to the hash table, so we don't have to re-read the file and re-populate the table for commands? The expensive reading+parsing could be avoided for commands, and only take place for the user changing the file.

    • jonasw - store length and hash of previous file, see if the new file has the same hash until old-length bytes.

      • Rather than storing and hashing the full length, we could do it until (before) the last s-expression (or last N s-expressions?). That way, we know if the last expression (or last N expressions) have changed.

        • Or even the first expression of the current date. That way, we just re-read the intervals for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
  6. Don't generate tag/keyword/value history from the entire log, just from the last N days (where N is user-customizable).
  7. Just why are we reading the whole file? chronometrist should not read more than a day; chronometrist-report should not read more than a week at a time, and so on. Make a branch which works on this logic, see if it is faster.

Certain

  1. statistics UI for arbitrary queries

    • user provides a predicate
    • we show buffer with

      • matched unique tag groups, and sparklines for time spent on each
      • matched key-values, and sparklines for time spent on each

plist-pp [66%]

  1. plist-pp - work recursively for plist/alist values
  2. Fix alignment of alist dots

    • While also handling alist members which are proper lists
  3. Add variable (to chronometrist-sexp.el) to set pretty-printing function. Default to ppp.el if found, fallback to internal Emacs pretty printer, and let users set their own pretty printing function.

Bugs [33%]

  1. With tags and key-value query functions in before-out-functions, clock in Task A -> clock in Task B -> prompted for tags and key values for Task A, add some -> they get added to Task B 😱
  2. I clocked into a task -

    (:name  "Arrangement/new edition"
     :tags  (new edition)
     :start "2020-08-17T00:33:24+0530")

    I added some key values to it. What it should have looked like -

    (:name     "Arrangement/new edition"
     :tags     (new edition)
     :composer "Schubert, Franz"
     :song     "Die schöne Müllerin"
     :start    "2020-08-17T00:33:24+0530"
     :stop     "2020-08-17T01:22:40+0530")

    What it actually looked like -

    (:name     "Arrangement/new edition"
     :tags     (new edition)
     :composer "Schubert, Franz"
     :song     "Die schöne Müllerin"
     :start    "2020-08-17T00:33:24+0530"
    ...)

    And of course, that results in an error trying to process it.

  3. When you enter a list containing a string - e.g.

    ("foo" (1 . "bar"))
    

    as a value, when suggested in the history it is presented rather differently -

    (foo (1 . bar))
    

    Yikes!

  4. Midnight spanning intervals currently display the full time when clocked out. e.g. this…

    (:name      "Programming"
     :start     "2020-12-22T23:01:00+0530"
     :stop      "2020-12-23T00:54:52+0530")

    …is displayed as 1:53:52 (rather than 00:54:52) after clocking out. :\

chronometrist [18%]

  1. Add :stop time when we call chronometrist-kv-accept, not when we quit the key-value prompt with a blank input.

    • It might be nice to be able to quit chronometrist-kv-add with C-g instead, actually.

      • C-g stops execution of chronometrist-run-functions-and-clock-in=/=chronometrist-run-functions-and-clock-out, so they can't reach the calls for chronometrist-in=/=chronometrist-out.

        We can make the clock-in/out happen in an unwind-protect, but that means clock-in/out always takes place, e.g. even when a function asks if you'd really like to clock out (like the Magit commit prompt example does), and you respond with "no".

        • What if we call chronometrist-before-out-functions with run-hook-with-args like all other hooks, so it runs all functions unconditionally and any function wishing to abort clocking in/out can use catch/throw? chronometrist-kv-add could quit nonlocally when the user enters a blank input (or hits C-g? Maybe by using unwind-protect?), cancelling the clock in/out, and thereby letting chronometrist-kv-accept resume clock in/out. (It can determine whether to clock in or out using chronometrist-current-task)
  2. Implement undo/redo by running undo-tree commands on chronometrist.sexp

    • Possibly show what changes would be made, and prompt the user to confirm it.
    • How will this work with the SQLite backend? Rollbacks?
    • It might be easier to just have a 'remove last interval' (the operation I use undo for most often), so we don't reimplement an undo for SQLite.
  3. Enhanced tag/key-value prompt - before asking for tags/key-values, if the last occurence of task had tags/key-values, ask if they should be reused. y - yes, n - no (continue to usual prompts).

    • Show what those tags/key-values are, so the user knows what will be added.
  4. chronometrist-details

    • "Explain" command - show intervals for task today
    • "Day summary" - for users who use the "reason" feature to note the specifics of their actual work. Combine the reasons together to create a descriptive overview of the work done in the day.
  5. Switch #4 between intervals and tag-combination breakdown
  6. Magit/other VCS integration

    • Add support for using key-values to point to a commit (commit hash + repo path?)

      • Need some way to extend the key-value prompt, so we can provide completion for commit hashes + commit messages…
    • Add command to open the commit associated with the interval in Magit
    • Make a user-customizable alist of project names and repo locations (local or remote), so shorter project names can be used instead of repo locations, saving space and reducing duplication.
  7. key-values - make detection of Lisp values more robust.

    • If the input string can be read in a single call to read, treat it as an s-expression; else, use the current heuristics.
  8. key-values - create transformer for key-values, to be run before they are added to the file. This will allow users to do cool things like sorting the key-values.
  9. Convert current interval - change the :name of the currently clocked-in interval. Tags and key-values may be re-queried. Clock-in hook functions will be run again with the new task as the argument.
  10. Rename a project (updating all records)
  11. Delete a project (erasing all records)
  12. Hide a project (don't show it in any Chronometrist-* buffer, effectively deleting it non-destructively)
  13. Reset current interval - update the :start time to the current time.
  14. Alternative query function for tags and key-values - a single query. Either with tags and key-values as a single plist, or something like the multi-field query-replace prompt.
  15. Customizable field widths
  16. Ask existing users if they'd like to have a prop line added to chronometrist-file

    • check if chronometrist-sexp-mode is active in the buffer
    • offer to never ask again
  17. Show details for time spent on a project when clicking on a non-zero "time spent" field (in both Chronometrist and Chronometrist-Report buffers).

chronometrist-report [0%]

  1. Show week counter and max weeks; don't scroll past first/last weeks
  2. Highlight column of current day
  3. narrow to specific project(s)
  4. Jump to beginning/end of data (keys B/F)
  5. "Explain" command - show tag-combination-based breakdown

Code quality [25%]

  1. Remove duplication between chronometrist-toggle-task and chronometrist-toggle-task-button
  2. Make functions more test-friendly. Quite a few can get away with returning values instead of writing to a file - this will make it easier to test them. Other functions can handle the file operations for them.
  3. Rewrite using cl-loop

    1. chronometrist-statistics-rows-internal
    2. chronometrist-statistics-count-active-days
  4. Write integration tests using ecukes.

    1. Some feature definitions already exist in features/, write step definitions for them.
  5. Remove duplication - the three chronometrist-*-history-populate functions
  6. Remove duplication - extracting user key-values from a plist seems to be a common operation.
  7. Extensions - redesign chronometrist such that it does not need to check for the availability of extensions (such as chronometrist-goal)

    • Could make two "transformer lists" - chronometrist-list-format-transformers and chronometrist-entry-transformers. The former would be called before tabulated-list-format is set. The latter would be called by chronometrist-entries, with each individual entry as an argument. chronometrist-goal will simply added a function to each of those.
    • Actually, are transformers really necessary? It could be done with a function inserted by chronometrist-goal into chronometrist-mode-hook. The function itself would become a little more complex, but it would remove the two transformer lists + the call-transformers dependency from the code. chronometrist-mode-hook is required either way, to set up chronometrist-goal.

      • Turns out, they are. We set tabulated-list-entries to a function. To modify the value, we must hook into that function in some way. tabulated-list-format could be modified in a regular hook, but it feels more consistent to make it a transformer too 🤔
  8. Ugly code - chronometrist-print-non-tabular; insert-text-button can be replaced with make-text-button

Documentation [0%]

  1. Move usage and customization sections to manual.org (The user may not see the README, if they are installing from MELPA.)

    1. convert README to Org
    2. transclude these sections from the manual to the readme
  2. Make Texinfo documentation

    • setup auto-export of Org to texinfo - git pre-commit hook?
  3. Link identifiers in manual.org to the source.

    • For HTML export, link to GitHub using line number anchors.
    • Try to make describe-function/helpful-at-point work with Org inline code syntax. Then we won't need to make links.

      • Incidentally, a link like [[elisp:(describe-function 'file-notify-add-watch)][file-notify-add-watch]]

        1. if opened from an Org buffer, shows the return value in the echo area, which is ugly
        2. is exported to Info as

          file-notify-add-watch ((describe-function 'file-notify-add-watch))
          

          …yuck :\

    • Currently using file: links with text search - [file:../elisp/file.el::defun identifier (], [file:../elisp/file.el::defvar identifier (], etc.
  4. Fix heading link to "midnight-spanning intervals" - jumps to the correct heading in HTML export, but jumps to its own self in Org mode.
  5. Figure out some way to hide package prefixes in identifiers in Org mode (without actually affecting the contents, a la nameless-mode)

UX [33%]

  1. Optimization - (jonasw) store length and hash of previous file, see if the new file has the same hash until old-length bytes.

    • Check for type of change to file

      • Handle last expression being removed
    • Implement optimized operations
    • BUG - if something was removed from the last expression (thereby decreasing the length of the file), chronometrist-file-change-type returns t instead of :last
    • BUG - args out of range error when last plist is removed
  2. Optimization - generate history before querying, not when the file changes.
  3. When starting a project with time of "-" (i.e. not worked on today until now), immediately set time to 0 instead of waiting for the first timer refresh
  4. Mouse commands should work only on buttons.
  5. Button actions should accept prefix arguments and behave exactly like their keyboard counterparts.
  6. mouse-3 should clock-out without asking for reason.
  7. Some way to ask for the reason just before starting a project. Even when clocking out, the reason is asked before clocking out, which adds time to the project.
  8. Allow calling chronometrist-in/out from anywhere-within-Emacs (a la timeclock) as well as from the chronometrist buffer.
  9. chronometrist-timer - if chronometrist-file is being edited (buffer exists and modified), don't refresh - this will (hopefully) prevent Emacs from going crazy with errors in trying to parse malformed data.

Maybe

New features

  1. Interact with Chronometrist from phone, tablet, or smart watches. (Help needed, I'm a total strange to mobile development and don't own any wearables.)
  2. Task List - a custom variable containing a list of tasks

    ("A Task Name"
     ("Another Task Name" :key-prompt nil)
     ...)

    Elements can be

    1. the task name as a string
    2. a list, with the first element being the task name as a string, followed by keyword-value pairs

    Keywords can be

    1. :tag-prompt, :key-prompt - values can be nil, t (the default), or a function. If nil, don't ask for tags/keys for this task. If t, ask for tags/keys for this task using chronometrist-tags-add=/=chronometrist-key-add. If it's a function, use that as the prompt.

      • Tags and key-values are optional extensions; we don't want Chronometrist to know about them.

        • Well, even with this style of configuration, Chronometrist doesn't necessarily have to…it could use the fields it knows about, ignoring the rest; the extensions could check for the fields they know about.
      • Instead of setting the prompt function, set hooks (chronometrist-before-in-functions=/=chronometrist-after-in-functions=/=chronometrist-before-out-functions=/=chronometrist-after-out-functions) per-task. This is preferable, because if you define a custom prompt function, you probably also want to remove certain functions coming earlier in the hook, such as chronometrist-skip-query-prompt, for that task.
    2. :hide - values can be nil (the default) or t - if t, hide this task from being displayed in chronometrist=/=chronometrist-report=/=chronometrist-statistics buffers. (effectively a non-destructive deletion of all intervals of the task)
    3. :goal
    4. :priority - task priority; could narrow/filter tasks by priority

      • isn't this similar to :hide, though?

    Useful for

    1. Adding tasks without clocking into them (the list is stored in a separate file)
    2. Not asking for tags and/or key-values for a particular task, or having a special behaviour for a task. (e.g. some tasks I use follow certain patterns, which I'd like to automate away)
    3. defining goals (subsume chronometrist-goal-list), priorities, etc
  3. Completion for sub-plists - if the value of a user keyword-value pair is itself a plist, can we reuse the keyword-value prompt for it? 🤔

    • Maybe generate the completion hash table when the plist is created, since this is likely to be less-used.
  4. Create a debug mode
  5. Create a verification command to test chronometrist-file for errors.
  6. Display task's weekly progress using ASCII/sparklines

    • Extension to add new column in chronometrist buffer
    • without goals - show dashes for days on which task was not touched, x for days on which it was done. e.g.

      • - x x x - - * means today is a Saturday, that I did the task on three days (Mo, Tu, We), and that I'm doing it right now.
      • - x - shows that today is a Tuesday and I did it on Monday, but not Sunday or (so far) today.
    • with goals - use sparklines to show how much of the goal was met

      • or, without goals - use sparklines as percent of maximum time spent on that task this week

UX [0%]

  1. Provide a command which tries to auto-configure Chronometrist keys in a way which is consistent with the user's other keymaps.
  2. Do basic checks on values of all customizable variables when they are changed by the user, and provide meaningful errors if they can't be used by the program.
  3. Task-sensitive value suggestions - if you use the key :key for two different tasks, and don't want the values for :key in one task being suggested for :key in another…

    • The problem is that sometimes you do want that, and changing it can lead to duplication of user effort.
    • Maybe make it a switch, enabled by default.
    • …or a list of keys to exclude from task-sensitivity?

      • So chronometrist-value-history will have ("task" . "key") as hash key and ("value" ...) as hash value. Keys which are present in the 'blacklist' are stored the same way as now - "key" as hash key, ("value" ...) as hash value.
    • Can we figure it out automatically, without requiring configuration? 🤔
    • Maybe suggest values for the current task first, and only after that for other tasks? Solves the problem of 'mixed up' value histories, removes the need for a switch to turn it off/have the user configure a blacklist of keys…
  4. Tag-sensitive key suggestions, tag-sensitive value suggestions…?

    • Might complicate things quite a bit.
    • Lack of task-sensitive value suggestions (#3) is an inconsistency, because tags and keys are already task-sensitive. From that perspective, tag-sensitive key and value suggestions are a whole new can of worms.
  5. Change precision of timestamps from seconds to minutes. (like Org)
  6. Use make-thread in v26 or the emacs-async library for chronometrist-entries=/=chronometrist-report-entries
  7. Some way to update buffers every second without making Emacs unusable. (impossible?)

chronometrist-report [0%]

  1. Add support for other locale weeks/weekday names
  2. narrow to specific task - average time spent in $TIMEPERIOD, average days worked on in $TIMEPERIOD, current/longest/last streak, % of $TIMEPERIOD, % of active (tracked) time in $TIMEPERIOD, …
  3. general - most productive $TIMEPERIOD, GitHub-style work heatmap calendar, …
  4. press 1 for weekly stats, 2 for monthly, 3 for yearly

chronometrist-goals [0%]

  1. Colorize times in Chronometrist buffer

    • untouched project with target defined - red
    • target ±5 minutes - green
    • target*2 and above - red

viewing/editing frontends

  1. Some way to use markup (Markdown, Org, etc) for certain plist values
  2. Some way to enter/edit key-values and other data, for people who don't know Lisp

Implementation ideas

"Input frontends"

A way to represent s-expressions as Markdown, Org, etc, so the entire plist can be edited in that mode. As a side-effect, this will permit use of Markdown, Org, etc in keyword-values - e.g. to use markup in comments or notes.

A list of keys whose values are to be edited in a user-specified major mode.

  • Multiple windows - instead of a single key-value buffer, we'll have multiple buffers in multiple visible windows, somewhat like ediff. The accept command will use the data from all involved buffers.

    • The buffer and window will be created when a keyword associated with that mode is selected at the prompt.
  • Alternatively, the whole plist goes into a single buffer of the markup's major mode - the markup bits as markup, the rest of the plist in a code block 🤷
  • poly-mode to mix different modes
  • see swapping text

A binding in the key-value buffer, which will insert the string at point in a buffer of a certain mode.

documentation discoverability   doc

Ensure that the user manual is easily discoverable.

[2021-06-02 13:49:41] rnkn: contrapunctus: have a menu item that opens the org file
[2021-06-02 13:50:49] contrapunctus: rnkn: hm, okay, I'll look into that…any others, for those who always have menus disabled?
[2021-06-02 13:51:43] rgr: why are you shipping as an org and not an info? Not that I have any skin in the game. But just link it in your mode/function docstring maybe.
[2021-06-02 13:52:15] rnkn: not the docstring
[2021-06-02 13:52:24] rnkn: use the :link keyword for the defgroup
[2021-06-02 13:53:22] rnkn: `:link '(file-link FILE)'
[2021-06-02 13:53:35] contrapunctus: rgr: Org […] can do a ton more stuff than [the] info [viewer]. To the point that even HTML export (even with infojs) would be a disgrace to the file ;)
[2021-06-02 13:53:37] rnkn: although you will probably need a function link instead to find the org file
[2021-06-02 13:54:30] contrapunctus: Although I guess the manual.org does not really need those fancy features…info export could work for it. \\

  1. command (and menu item) to launch Org LP
  2. provide :link argument to defgroup
  3. open changelog on update

macro for extensions   code extension

A macro to create new columns for Chronometrist.

Extension writer specifies

  • extension name
  • the new schema entry (an element suitable for tabulated-list-format)
  • the position of your new column in the schema (but what if the user fiddles with the hooks?)
  • a function to emit a single cell of your column; it is called with a single row of table data
  • (optional) :setup and :teardown forms, and (optionally) addendums to the docstrings created for the functions which will be created for them

Macro creates

  • EXTENSION-minor-mode
  • EXTENSION custom group and EXTENSION-cell-function custom variable (with your provided function as the default value)
  • the appropriate EXTENSION-row-transformer and EXTENSION-schema-transformer functions; the former calls EXTENSION-cell-function and inserts the string into the specified position in each row
  • EXTENSION-setup and EXTENSION-teardown functions, which

    • add/remove the generated EXTENSION-row-transformer and EXTENSION-schema-transformer functions to chronometrist-row-transformers and chronometrist-schema-transformers, and
    • run the :setup and :teardown forms if supplied.

Benefits -

  • reduced duplication
  • easier creation of such extensions
  • users can easily replace the function used to generate the cells, without having to deal with how the string is inserted into the row specifier.

Current uses -

  1. chronometrist-goal
  2. chronometrist-spark

macro for frontends   code

A macro to create Chronometrist frontends (based on tabulated-list-mode). If implemented, would shorten code for all four existing and two planned frontends.

Programmer specifies -

  1. frontend command name (a symbol)
  2. name (string, passed to define-derived-mode)
  3. rows function
  4. schema value
  5. name of buffer created by command

Macro creates -

  1. frontend interactive command, which behaves like a toggle
  2. function frontend-rows
  3. custom variables frontend-schema, frontend-row-transformers, frontend-schema-transformers, frontend-buffer-name

unified format-duration function   code customization

Currently we have at least three ways of displaying durations - "HH:MM:SS" , "XhYm" , and X hour(s), Y minutes(s)" . Make a single function similar to format-time-string, but for durations. ts-human-format-duration from ts.el is not nearly as flexible as I'd like. When completed, we can have a single custom variable accepting a format string, which can be used to customize display of durations for the entire application at once.

  • user provides a duration (in seconds), a format string, and an optional separator string
  • "%y" , "%o" , "%w" , "%d" , "%h" , "%m" , "%s" - years (365 days), months (30 days? 4 weeks?), weeks, days, hours, minutes, seconds
  • "%Y" , "%O" , "%W" , "%D" , "%H" , "%M" , "%S" - same as above, but with string units, e.g. "hour(s)" , "minute(s)" , and "second(s)"

    • if the unit is >1, plurals are used
    • separator can be specified like this - "%<SEP><CODE>" , e.g. "%-T"; only entered if there is a value present to the left; if unspecified, it is a space
  • if the value is 0, the value and the unit are ignored even if provided in the input string
  • optional separator string is interspersed between each value

Examples

  • (chronometrist-format-duration "%ss" 1) > ="1s"
  • (chronometrist-format-duration "%S" 1) > ="1 second"
  • (chronometrist-format-duration "%S" 5) > ="5 seconds"
  • (chronometrist-format-duration "%M %S" 5) > ="5 seconds"
  • (chronometrist-format-duration "%M %S" 60) > ="1 minute"
  • (chronometrist-format-duration "%M %S" 125) > ="2 minutes, 5 seconds"
  • ISO-8601 -
  (chronometrist-format-duration "P%yY%oM%wW%dDT%hH%mM%sS"
                     (+ (* 365 24 3600)
                        (* 30 24 3600)
                        (* 7 24 3600)
                        (* 24 3600)
                        (* 10 3600)
                        (* 10 60)
                        10)) ;; => "P1Y1M1W1DT10H10M10S"

Alternative syntax

  • large units are capitals - "Y" , "M" , "W" , "D"
  • small units are lower-case - "h" , "m", "s"
  • to display only values, use "%<code>"
  • to display long units, use "~[<separator>]<code>"

DONE error - min called with nil   spark bug

  1. clock in
  2. change :start of active interval to another time on the same date
  3. error
Debugger entered--Lisp error: (wrong-number-of-arguments #<subr min> 0)
  min()
  apply(min nil)
  (format "(%sm~%sm)" (apply #'min durations-nonzero) (apply #'max duration-minutes))
  (if (= 1 (length durations-nonzero)) (format "(%sm)" (apply #'max duration-minutes)) (format "(%sm~%sm)" (apply #'min durations-nonzero) (apply #'max duration-minutes)))
  (let* ((duration-minutes (mapcar #'(lambda (it) (ignore it) (/ it 60)) durations)) (durations-nonzero (seq-remove #'zerop duration-minutes))) (if (= 1 (length durations-nonzero)) (format "(%sm)" (apply #'max duration-minutes)) (format "(%sm~%sm)" (apply #'min durations-nonzero) (apply #'max duration-minutes))))
  chronometrist-spark-range((0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1) t)
  (format "%s %s" (spark durations) (chronometrist-spark-range durations active-p))
  (if (and active-p chronometrist-spark-show-range) (format "%s %s" (spark durations) (chronometrist-spark-range durations active-p)) (format "%s" (spark durations)))
  (let* ((today (ts-now)) (duration nil) (active-p nil) (day (- (- chronometrist-spark-length 1))) (durations nil)) (while (<= day 0) (setq durations (nconc durations (list (setq duration (chronometrist-task-time-one-day task (ts-adjust ... day today)))))) (if (not (= 0 duration)) (progn (setq active-p t))) (setq day (+ day 1))) (if (and active-p chronometrist-spark-show-range) (format "%s %s" (spark durations) (chronometrist-spark-range durations active-p)) (format "%s" (spark durations))))
  (let* ((--dash-source-12-- row) (task (car-safe (prog1 --dash-source-12-- (setq --dash-source-12-- (cdr --dash-source-12--))))) (vector (car --dash-source-12--)) (sparkline (let* ((today (ts-now)) (duration nil) (active-p nil) (day (- (- chronometrist-spark-length 1))) (durations nil)) (while (<= day 0) (setq durations (nconc durations (list ...))) (if (not (= 0 duration)) (progn (setq active-p t))) (setq day (+ day 1))) (if (and active-p chronometrist-spark-show-range) (format "%s %s" (spark durations) (chronometrist-spark-range durations active-p)) (format "%s" (spark durations)))))) (list task (vconcat vector (vector sparkline))))
  chronometrist-spark-row-transformer(("Performance" ["14" ("Performance" action chronometrist-toggle-task-button follow-link t) "       1" "       1"]))
  funcall(chronometrist-spark-row-transformer ("Performance" ["14" ("Performance" action chronometrist-toggle-task-button follow-link t) "       1" "       1"]))
  (setq arg (funcall fn arg))
  (let ((fn (car --dolist-tail--))) (setq arg (funcall fn arg)) (setq --dolist-tail-- (cdr --dolist-tail--)))
  (while --dolist-tail-- (let ((fn (car --dolist-tail--))) (setq arg (funcall fn arg)) (setq --dolist-tail-- (cdr --dolist-tail--))))
  (let ((--dolist-tail-- transformers)) (while --dolist-tail-- (let ((fn (car --dolist-tail--))) (setq arg (funcall fn arg)) (setq --dolist-tail-- (cdr --dolist-tail--)))) arg)
  (if transformers (let ((--dolist-tail-- transformers)) (while --dolist-tail-- (let ((fn (car --dolist-tail--))) (setq arg (funcall fn arg)) (setq --dolist-tail-- (cdr --dolist-tail--)))) arg) arg)
  chronometrist-run-transformers((chronometrist-spark-row-transformer chronometrist-goal-row-transformer) ("Performance" ["14" ("Performance" action chronometrist-toggle-task-button follow-link t) "       1" "       1"]))
  (let ((it (list task it))) (chronometrist-run-transformers chronometrist-row-transformers it))
  (let ((it (vector index task-button task-time indicator))) (let ((it (list task it))) (chronometrist-run-transformers chronometrist-row-transformers it)))
  (let* ((index (number-to-string index)) (task-button (cons task '(action chronometrist-toggle-task-button follow-link t))) (task-time (chronometrist-format-duration (chronometrist-task-time-one-day task))) (indicator (if (chronometrist-task-active\? task) (chronometrist-activity-indicator) ""))) (let ((it (vector index task-button task-time indicator))) (let ((it (list task it))) (chronometrist-run-transformers chronometrist-row-transformers it))))
  (cons (let* ((index (number-to-string index)) (task-button (cons task '(action chronometrist-toggle-task-button follow-link t))) (task-time (chronometrist-format-duration (chronometrist-task-time-one-day task))) (indicator (if (chronometrist-task-active\? task) (chronometrist-activity-indicator) ""))) (let ((it (vector index task-button task-time indicator))) (let ((it (list task it))) (chronometrist-run-transformers chronometrist-row-transformers it)))) --cl-var--)
  (setq --cl-var-- (cons (let* ((index (number-to-string index)) (task-button (cons task '(action chronometrist-toggle-task-button follow-link t))) (task-time (chronometrist-format-duration (chronometrist-task-time-one-day task))) (indicator (if (chronometrist-task-active\? task) (chronometrist-activity-indicator) ""))) (let ((it (vector index task-button task-time indicator))) (let ((it (list task it))) (chronometrist-run-transformers chronometrist-row-transformers it)))) --cl-var--))
  (while (consp --cl-var--) (setq task (car --cl-var--)) (setq --cl-var-- (cons (let* ((index (number-to-string index)) (task-button (cons task '...)) (task-time (chronometrist-format-duration (chronometrist-task-time-one-day task))) (indicator (if (chronometrist-task-active\? task) (chronometrist-activity-indicator) ""))) (let ((it (vector index task-button task-time indicator))) (let ((it ...)) (chronometrist-run-transformers chronometrist-row-transformers it)))) --cl-var--)) (setq index (1+ index)) (setq --cl-var-- (cdr --cl-var--)))
  (let* ((index 1) (--cl-var-- (-sort #'string-lessp chronometrist-task-list)) (task nil) (--cl-var-- nil)) (while (consp --cl-var--) (setq task (car --cl-var--)) (setq --cl-var-- (cons (let* ((index (number-to-string index)) (task-button (cons task ...)) (task-time (chronometrist-format-duration ...)) (indicator (if ... ... ""))) (let ((it ...)) (let (...) (chronometrist-run-transformers chronometrist-row-transformers it)))) --cl-var--)) (setq index (1+ index)) (setq --cl-var-- (cdr --cl-var--))) (nreverse --cl-var--))
  chronometrist-rows()
  #f(compiled-function (&optional remember-pos update) "Populate the current Tabulated List mode buffer.\nThis sorts the `tabulated-list-entries' list if sorting is\nspecified by `tabulated-list-sort-key'.  It then erases the\nbuffer and inserts the entries with `tabulated-list-printer'.\n\nOptional argument REMEMBER-POS, if non-nil, means to move point\nto the entry with the same ID element as the current line and\nrecenter window line accordingly.\n\nNon-nil UPDATE argument means to use an alternative printing\nmethod which is faster if most entries haven't changed since the\nlast print.  The only difference in outcome is that tags will not\nbe removed from entries that haven't changed (see\n`tabulated-list-put-tag').  Don't use this immediately after\nchanging `tabulated-list-sort-key'." #<bytecode 0x1fdbee9d8f65>)(t nil)
  ad-Advice-tabulated-list-print(#f(compiled-function (&optional remember-pos update) "Populate the current Tabulated List mode buffer.\nThis sorts the `tabulated-list-entries' list if sorting is\nspecified by `tabulated-list-sort-key'.  It then erases the\nbuffer and inserts the entries with `tabulated-list-printer'.\n\nOptional argument REMEMBER-POS, if non-nil, means to move point\nto the entry with the same ID element as the current line and\nrecenter window line accordingly.\n\nNon-nil UPDATE argument means to use an alternative printing\nmethod which is faster if most entries haven't changed since the\nlast print.  The only difference in outcome is that tags will not\nbe removed from entries that haven't changed (see\n`tabulated-list-put-tag').  Don't use this immediately after\nchanging `tabulated-list-sort-key'." #<bytecode 0x1fdbee9d8f65>) t nil)
  apply(ad-Advice-tabulated-list-print #f(compiled-function (&optional remember-pos update) "Populate the current Tabulated List mode buffer.\nThis sorts the `tabulated-list-entries' list if sorting is\nspecified by `tabulated-list-sort-key'.  It then erases the\nbuffer and inserts the entries with `tabulated-list-printer'.\n\nOptional argument REMEMBER-POS, if non-nil, means to move point\nto the entry with the same ID element as the current line and\nrecenter window line accordingly.\n\nNon-nil UPDATE argument means to use an alternative printing\nmethod which is faster if most entries haven't changed since the\nlast print.  The only difference in outcome is that tags will not\nbe removed from entries that haven't changed (see\n`tabulated-list-put-tag').  Don't use this immediately after\nchanging `tabulated-list-sort-key'." #<bytecode 0x1fdbee9d8f65>) (t nil))
  tabulated-list-print(t nil)
  (save-current-buffer (set-buffer chronometrist-buffer-name) (tabulated-list-print t nil) (chronometrist-print-non-tabular) (chronometrist-maybe-start-timer) (set-window-point window point))
  (progn (save-current-buffer (set-buffer chronometrist-buffer-name) (tabulated-list-print t nil) (chronometrist-print-non-tabular) (chronometrist-maybe-start-timer) (set-window-point window point)))
  (if window (progn (save-current-buffer (set-buffer chronometrist-buffer-name) (tabulated-list-print t nil) (chronometrist-print-non-tabular) (chronometrist-maybe-start-timer) (set-window-point window point))))
  (let* ((window (get-buffer-window chronometrist-buffer-name t)) (point (window-point window))) (if window (progn (save-current-buffer (set-buffer chronometrist-buffer-name) (tabulated-list-print t nil) (chronometrist-print-non-tabular) (chronometrist-maybe-start-timer) (set-window-point window point)))))
  chronometrist-refresh()
  (let* ((--dash-source-6-- fs-event) (descriptor (car-safe (prog1 --dash-source-6-- (setq --dash-source-6-- (cdr --dash-source-6--))))) (action (car-safe (prog1 --dash-source-6-- (setq --dash-source-6-- (cdr --dash-source-6--))))) (_ (car (cdr --dash-source-6--))) (change (if chronometrist--file-state (progn (chronometrist-file-change-type chronometrist--file-state)))) (reset-watch (or (eq action 'deleted) (eq action 'renamed)))) (cond ((or reset-watch (not chronometrist--file-state) (eq change t)) (if reset-watch (progn (file-notify-rm-watch chronometrist--fs-watch) (setq chronometrist--fs-watch nil chronometrist--file-state nil))) (chronometrist-events-populate) (chronometrist-reset-task-list)) (chronometrist--file-state (let* ((old-task (plist-get (chronometrist-events-last) :name)) (new-task (plist-get (chronometrist-sexp-last) :name))) (cond ((eq change :append) (chronometrist-events-update (chronometrist-sexp-last)) (chronometrist-add-to-task-list new-task)) ((eq change :modify) (chronometrist-events-update (chronometrist-sexp-last) t) (chronometrist-remove-from-task-list old-task) (chronometrist-add-to-task-list new-task)) ((eq change :remove) (let (...) (chronometrist-remove-from-task-list old-task) (let ... ...))) ((null change) nil) (t nil))))) (setq chronometrist--file-state (list :last (chronometrist-file-hash :before-last nil) :rest (chronometrist-file-hash nil :before-last t))) (chronometrist-refresh))
  chronometrist-refresh-file(((1 . 1) changed "/home/anon/.emacs.d/chronometrist.sexp"))
  file-notify--call-handler(#s(file-notify--watch :directory "/home/anon/.emacs.d" :filename "chronometrist.sexp" :callback chronometrist-refresh-file) (1 . 1) changed "/home/anon/.emacs.d/chronometrist.sexp" nil)
  file-notify--handle-event((1 . 1) (changed) "chronometrist.sexp" 0)
  file-notify--callback-inotify(((1 . 1) (modify) "chronometrist.sexp" 0))
  file-notify-handle-event((file-notify ((1 . 1) (modify) "chronometrist.sexp" 0) file-notify--callback-inotify))
  funcall-interactively(file-notify-handle-event (file-notify ((1 . 1) (modify) "chronometrist.sexp" 0) file-notify--callback-inotify))
  call-interactively(file-notify-handle-event nil [(file-notify ((1 . 1) (modify) "chronometrist.sexp" 0) file-notify--callback-inotify)])
  command-execute(file-notify-handle-event nil [(file-notify ((1 . 1) (modify) "chronometrist.sexp" 0) file-notify--callback-inotify)] t)

STARTED discoverability and mouse-accessibility of commands [33%]   ux

Goals

  1. discoverability of commands
  2. discoverability of default keybindings
  3. mouse accessibility of commands

Strategies

  1. define menu
  2. bind context menu to right click outside of buttons
  3. bind toggle task with/without hooks to left/right-click on buttons

    1. keybindings in tooltips
  4. print keybindings in columns (following the table width)

    • Perhaps I needn't worry too much. menu-bar-mode is enabled by default, and it makes #1 and #2 easy. I think a significant amount of the userbase disables menu-bar-mode, but they also have things like counsel-M-x, describe-.

      • The menu does not make the behavior of the numeric argument discoverable. Doesn't make sense to put it there, either.

querying the file buffer and editing the results   feature

chronometrist-loop-file can be used to run queries against user data. It would be cool to be able to edit the file directly from the query results.

  1. The result data may just be plists, which could be displayed/edited directly from the search results (invisible text or selective display + narrowing?)
  2. The result data may be something which corresponds to the input data, in which case we could jump to the corresponding plist.
  3. The result data may be impossible to trace back to the input data (e.g. a sum of intervals from many plists), in which case we cannot provide direct editing.

error in change type detection   core bug plist_backend

Steps

  1. Clock in
  2. Delete active task plist, but don't save
  3. Clock in to different task. Error.

Might have to do with there being 2 empty lines between the last-but-one plist and the new last plist.

STARTED New frontends and enhancements   feature

Existing frontends

chronometrist (overview for a day)

list of tasks, one day, durations and graphs

  • commands [0%]

    1. previous/next day
    2. set day (blank input to reset to today's date)
    3. set duration format
    4. display sparkline for total time

report

list of tasks, one week, durations only

statistics

list of tasks, one week/month/year 1

details (intervals for a day) [16%]

list of intervals, one day 1

  • commands [50%]

    1. set [task/key-value] filter 2
    2. set range

      1. implement timestamp ranges (e.g. 2021-06-01T12:00+05:30 to 2021-07-03T00:10:29+0530)
    3. previous/next day
    4. set duration format
  • with spark - vertical sparkline for each interval
  • non-tabular text 3
  • when range is a pair with the car or the cdr being blank, set the start/stop date to the earliest/latest date available.

    • not possible with completing-read-multiple, which removes blank strings; the simplest solution was to allow "begin"/"end" as inputs.
  • when range is bigger than a day, you also want to see the date of each interval

Bugs

  1. day-crossing events have the wrong duration and time. e.g. 1 Exercise walking 30 minutes from 23:37 to 00:07

New frontends I want

task-key-values

list of unique key-values for a task, one day 1, durations and graphs

  • commands [0%]

    1. set task
    2. set range

task-graph [0%]

list of days, weeks, or months 1, one task (with optional key-value filter 2), horizontal graph (and durations/stats?)

  • columns

    1. range (ISO date, week, or year-month)
    2. per-range graph (vertical/interals if days, horizontal/days if weeks or months)
  • commands [0%]

    1. toggle week/month
    2. set [task/key-value] filter
    3. set range
  • non-tabular text 3

STARTED customizable duration output   feature

  1. 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

Incorrect time displayed when midnight crossed with a task active   bug

Midnight-spanning intervals are split in the hash table when the file is read, but not when a task is started and not stopped before midnight. Run a function to check at midnight?

Duplication - accessing the latest-record   code

Many functions use the latest record. A (chronometrist-with-latest-record var &rest body) might help…

STARTED Plist-group backend [71%]   feature optimization

See branch grouped-plist-format

  1. Migrate backend code to use CLOS
  2. Implement plist-group backend methods
  3. <<dates-in-timestamps>> Omit dates from plists
  4. Mark split intervals with =:split t=+

    • Not required, since we can detect them with enough certainty for my liking. (See chronometrist-plists-split-p)
  5. make generic cl-loop backend interface - chronometrist-loop-sexp-file
  6. chronometrist-loop-sexp-file - optimize grouped-plist iterator
  7. key-values code is not yet generic
  8. update plist-pp to indent tagged lists differently
  9. optimize program startup and file change callback
  10. remove all references to chronometrist-events (hash table)
  11. create new tagged list if required when clocking in
  12. split midnight-spanning intervals when clocking out
  13. interactive backend-switching command with completion; reset backend state after switching to account for changes made to the file in the meantime
  14. (maybe) handle existing (in-file) split tasks. Currently, if an interval is split, commands such as chronometrist-key-values-unified-prompt operate on the second split interval, but not the first. Instead, plist-group methods for chronometrist-replace-last and chronometrist-remove-last could check if the last interval is split, and operate on both of them. (chronometrist-insert already splits plists when inserting.)

    • Don't modify chronometrist-latest-record; code using it generally only wants the latter half of a split plist.
    • See branch plist-group-handle-split-plists
  15. use file-change-detection optimization

    • fix bugs in change-type detection
  16. fix bug - extra newlines in file

Optional

  1. define generic backend protocol for key-values package

protocol implementation progress

  1. chronometrist-latest-date-records
  2. chronometrist-latest-record
  3. chronometrist-task-records-for-date
  4. chronometrist-active-days
  5. chronometrist-insert
  6. chronometrist-remove-last
  7. chronometrist-replace-last
  8. [superclass] chronometrist-create-file
  9. chronometrist-view-file
  10. [superclass] chronometrist-edit-file
  11. chronometrist-count-records
  12. chronometrist-to-list
  13. chronometrist-to-hash-table
  14. chronometrist-to-file
  15. chronometrist-on-change
  16. [superclass] chronometrist-reset-internal
  17. chronometrist-backend-empty-p
  18. [superclass] chronometrist-memory-layer-empty-p
  19. chronometrist-verify
  20. [superclass] chronometrist-timer

New backends   feature

Org time tracking

timeclock

See docstring of timeclock-log-data

SQLite [0%]

Chronometrist allows the user to add arbitrary key-values. There are a few ways to do this in SQL, but this solution seems well-suited to Chronometrist - put commonly-queried keys (name, start, stop) into SQL columns, and put user key-values in a JSON/s-exp TEXT column. The latter can be queried separately. SQLite has support for JSON columns.

  1. Use string-remove-prefix rather than s-chop-prefix
  2. to-file

    • insert intervals
    • insert date properties
    • insert events

cl-naive-store

Reimplementing the plist/plist-group backends in Common Lisp, without the buffer visualisation and buffer-movement commands provided by Emacs/Elisp, is not something I'm very enthusiastic about. Even if those turn out to not be major factors, implementing these backends in Elisp taught me that making your own database can be a lot of work. Especially if you just care about your application, and writing a database was not what you set out to do (I severely underestimated the work involved).

Yet, some may prefer the plist-group backend to an SQLite database. I trust that a cl-naive-store backend could have the same capabilities as the plist-group backend, but without the maintenance burden of an ad hoc s-expression store.

Functions doing similar things   code

  1. task-time-one-day
  2. interval (unused)
  3. events-to-durations

Use ISO date for functions operating on dates   time format

STARTED customizable task list [33%]   feature

  1. 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.

  • date(1) does something like this, right? Maybe we could shell out to it.

A general library for this could convert between (both to and from) such strings and TS structs.

  • today, yesterday, day before yesterday, tomorrow, day after tomorrow
  • N <seconds/minutes/hours/days/weeks/months/years> (ago|from now/today)

    • plus multiples of those units e.g. "1 year, 5 months, … from now"

DONE Kill/discard command   feature

Command to delete the interval currently being recorded. (bind to 'k')

  • Most conservative option - it will only operate on the project at point, and will only kill for a clocked-in project.
  • Somewhat less conservative option - it will operate on the currently clocked-in project, no matter where point is.
  • It should ask for confirmation.
  • Alternatively, or as a complement - an undo command, which will undo your last action (clock in or clock out).

    • Undo and redo seem like the best bets.

pretty printer

fix handling of tagged alist group values   bug

put each list element on its own line if length of list exceeds fill-column   feature

verify command [20%]   feature

With different checks, for different levels of speed/thoroughness -

  1. (plist group) Sequence of plist group dates
  2. Check that every record (except the last) has a :stop
  3. Intersecting timestamps
  4. Sequence of records
  5. (plist group) Midnight spanning interval check (first and last intervals)
  6. (plist group) Check that plist timestamps have the correct date. Only applicable as long as they have a date.

format changes

multiple intervals per record   feature

(:name "<name>"
 [<keyword-value pair>] ...
 :intervals
 (("<start>" . "<stop>")
  ...))

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 "<duration>" or :deduct ("<start time>" . "<stop time>").

    • This will also make it easier to support formats like 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

Records not used for time tracking, but to store data associated with a date or timestamp. More than one record may exist for the same date.

(event "<date or timestamp>"
 [<keyword-value pair>] ...)

STARTED tagging dates with key-values [0%]   feature

Straightforward enough for the plist group backend -

("<date>"
 [<keyword> <value>]*
 <plist>+)

Alternatively, this would be easier to work with - easier to select the key-values or the plists, as required -

("<date>"
 (key-values
  [<keyword> <value>]+)
 <plist>+)

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 <plist group>).

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

  1. Type M-x chronometrist; (chronometrist-backend-file-watch (chronometrist-active-backend)) probably returns a descriptor
  2. Visit literate source and type M-x literate-elisp-load; (chronometrist-backend-file-watch (chronometrist-active-backend)) returns nil
  3. Type M-x chronometrist; file-notify-descriptors likely contains two descriptors for the same file and the same function (chronometrist-file-refresh)

chronometrist-register-backend removes the old backend object and replaces it with a new one.

backend migration [0%]

  1. Use active backend as the suggested input backend when migrating
  2. Conserve file local variables prop line for text backends

make migrate command async [33%]

chronometrist-migrate / chronometrist-to-file can take a long time to run, freezing Emacs until completion if run synchronously.

  1. basic async export
  2. display message on start and completion
  3. test the INPUT-FILE argument

STARTED Support for the Third Time System [57%]   extension

Third Time: a better way to work

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 youve 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 youll resume work. A big break uses up any saved break minutes, so you cant carry them over to the afternoon/evening.
  • Avoid taking other unearned breaks if possibleso try to do personal tasks during normal or big breaks, or before/after your work day.
  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. "big break" command - user enters a time, for which we set an alarm. User must resume working around that 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.

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
  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

STARTED Customizable alerts [50%]   feature code

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 alists4 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.

  1. write macro
  2. use in chronometrist-goal and chronometrist-third

DONE Predefined key-values for tasks

Benefits

  1. Speeds up key-value completion for very large data sets.
  2. Prevents key-value suggestions from being affected by splitting of tasks across multiple intervals.
'(("Task" <plist> ...)
  ...)

no-goal-alert - "You have spent on <task>" [0%]   bug

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

Similar to 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

User-defined categories

A task can be part of any number of categories. A category may also contain other categories. The categories, and the total time spent on each one, could be displayed in the main view, separate from the tasks.

First run behaviour   ux

  1. Look for any existing Chronometrist database in the default location. If one is found, use it to determine the default active backend. Otherwise, offer to import from one of the supported backends.

    • Some backends may not be installed - "see MELPA for more formats"
  2. Don't suggest nil when asking for first project on first run

STARTED Common Lisp port

DONE Replace add-variable-watcher

Common Lisp does not have Elisp's add-variable-watcher. The Elisp code used that to automatically update the file slot of the active backend object when the user changes the value of chronometrist-file. The new solution is to remove direct accesses to the file slot, define a backend-file method which returns the value of file if non-nil (it's usually nil), or derives a file path from *user-data-file* and the backend's extension.

It seems that the file slot is now unnecessary - backend-file can check path instead of file, and use either path or *user-data-file* to derive the full path.

  1. Remove the file slot

DONE Use pathnames instead of namestrings

CLIM UI design

One-day task-duration table

columns

  1. index (for numeric argument)
  2. task name
  3. task duration today
  4. task activity indicator

    • click to clock in/out (hover to show icon)
  5. graph of daily task durations

    • Click on a point in the graph to view that day in the Details View.

click on row to show taller graph and inline display of either key-value breakdown, or interval breakdown.

commands

  1. clock in, clock out, etc
  2. toggle details for row
  3. toggle details for all rows
  4. next/previous day
  5. set displayed day
  6. cycle graph heights (self/comparative)

    • the height of the graphs can be based on the range of each individual graph's own data ('self') or based on the range of all data ('comparative').

settings

  1. default graph height

One-day category-duration table

  1. category
  2. time duration today

commands

  1. toggle details for row (show tasks in each category)
  2. toggle details for all rows

One-day event-occurrence table

("Useful for recording things like weight, medication, waist circumference, etc.")

  1. index (prefix argument)
  2. event name
  3. time of occurrence
  4. graph of past occurrences

commands

  1. add event
  2. record event

1+ day editable log

Displays tables of day properties, intervals, and events, grouped by day, in date/week/month/year range (default - today)

can select a day property/interval/event to edit its data

buttons to add new day properties, intervals, and events

When there are no intervals/events/day properties, show placeholder text - "No intervals recorded on this day." / "No events recorded on this day." / "No properties for this day."

Commands

  1. filter contents
  2. next/previous day (or range)
  3. set displayed day (or range)

Line chart views

Line chart with one or more (adjustable) metrics -

  • duration for task
  • task-property combinations
  • numeric property value/function with numeric output

…over a time range (adjustable), with points for days/weeks/months/years (adjustable)

User can switch between single graph vs separate graphs, different types of graphs, etc.

Commands

  1. new/open/save/delete view
  2. add/remove metric (a line on the graph)
  3. increase/decrease/set range, next/previous range
  4. increase/decrease/set granularity

Default view (overview for a day)

Default view is a tiled composition of the one-day task-duration table, one-day grouped log, and the one-day category-duration table.

view/edit database as

some way to view/edit the database in a different format, e.g. Lisp plists

Documentation restructuring   doc

  1. Move documentation from LPs to manual.org
  2. Revamp tutorials - https://diataxis.fr/tutorials/#the-language-of-tutorials
  3. Make how-to guides more general?
  4. Move some how-to guide code into a contrib directory?

tutorial and how-to topic revamp

tutorials

  1. installation (MELPA) -> first run
  2. adding the first task
  3. clocking in, clocking out
  4. customization - open a file when starting a task (move from how-to)
  5. customization - warn when exiting with an active task
  6. clocking in/out without running hooks
  7. restarting or discarding an activity
  8. key-value extension - installation -> customization -> testing/results
  9. graph extension - installation -> customization -> testing/results

how-to guides

  1. how to install (Git, MELPA, Quelpa, etc)
  2. how to attach tags and properties to time intervals (cover different commands)
  3. how to configure Emacs for Chronometrist development

Concurrent tasks

I'm not interested in using this myself, so it must be possible to enable/disable it.

  • Enabling/disabling it would just affect how the clock-in command works. Perhaps data verification, too.

How might it affect the data consumer?

  1. Calculation of total time tracked (in any range, e.g. a day) will definitely require a more complex algorithm.

1

variable range

2

keys - show intervals with those keys; key-values - intervals with those values; or predicate

3
"(Showing intervals|No intervals to show)
[(with (<keyword>*|<keyword-value>*)|
matching <predicate>)]
(for <date>|
between (<date>|<datetime>) and (<date>|<datetime>)|
for <predicate>)"
4

Actually, make a macro to define such alists, since the docstrings are likely to be near-identical.