Compare commits

...

11 Commits

Author SHA1 Message Date
contrapunctus 998ea0f94e Define new classes, begin implementing new protocol 2022-04-17 13:45:31 +05:30
contrapunctus 8f14fbb546 Add cl-naive-store to backends roadmap 2022-04-16 21:38:26 +05:30
contrapunctus bb4b2121c9 Move Overview from LP to manual 2022-04-15 17:43:34 +05:30
contrapunctus dbc7be41d2 manual: Mention plan for Emacs Lisp codebase 2022-04-15 12:02:55 +05:30
contrapunctus a1c79d8faa manual: Mention LP benefits 2022-04-15 10:35:22 +05:30
contrapunctus d63bc11c8a manual: Reword Benefits section 2022-04-15 10:00:34 +05:30
contrapunctus dcca2632fb manual: Update backup location step 2022-04-15 09:54:51 +05:30
contrapunctus bbf5a2e048 manual: Move Contributions to Explanation 2022-04-15 09:46:35 +05:30
contrapunctus 73be9670b7 Mention literate-elisp-load on file open 2022-04-14 22:22:17 +05:30
contrapunctus 1bf960b6e0 TODO: Add tasks for tutorial/how-to revamp 2022-04-14 22:21:50 +05:30
contrapunctus ca20b3edda TODO: Add documentation restructuring tasks 2022-04-14 18:12:56 +05:30
4 changed files with 169 additions and 78 deletions

View File

@ -840,6 +840,14 @@ Chronometrist allows the user to add arbitrary key-values. There are a few ways
https://github.com/projecthamster/hamster
+ https://github.com/projecthamster/hamster/wiki/Our-datamodel
** [[https://gitlab.com/Harag/cl-naive-store][cl-naive-store]]
:PROPERTIES:
:CREATED: 2022-04-16T21:11:07+0530
:END:
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)
@ -1086,11 +1094,10 @@ A task can be part of any number of categories. A category may also contain othe
+ Some backends may not be installed - "see MELPA for more formats"
2. [ ] Don't suggest nil when asking for first project on first run
* Common Lisp port
* STARTED Common Lisp port
:PROPERTIES:
:CREATED: 2022-04-03T09:36:53+0530
:END:
** 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.
@ -1198,3 +1205,29 @@ Default view is a tiled composition of the one-day task-duration table, one-day
:CREATED: 2022-04-08T19:56:38+0530
:END:
some way to view/edit the database in a different format, e.g. Lisp plists
* Documentation restructuring :doc:
:PROPERTIES:
:CREATED: 2022-04-14T18:04:21+0530
:END:
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

View File

@ -672,7 +672,9 @@ treated as though their time is 00:00:00."
*** task-time-one-day :reader:
#+BEGIN_SRC lisp
(defun task-time-one-day (task &optional (date (date-ts)) (backend (chronometrist-active-backend)))
(defun task-time-one-day (task &optional
(date (date-ts))
(backend (chronometrist-active-backend)))
"Return total time spent on TASK today or on DATE, an ISO-8601 date.
The return value is seconds, as an integer."
(let ((task-events (chronometrist-task-records-for-date backend task date)))
@ -680,8 +682,8 @@ The return value is seconds, as an integer."
(->> (plists-to-durations task-events)
(-reduce #'+)
(truncate))
;; no events for this task on DATE, i.e. no time spent
0)))
;; no events for this task on DATE, i.e. no time spent
0)))
#+END_SRC
*** active-time-on :reader:
@ -948,6 +950,56 @@ return a list of tasks from the active backend."
(or *task-list* task-list (setf task-list (list-tasks backend))))))
#+END_SRC
**** day :class:
#+BEGIN_SRC lisp
(defclass day ()
((properties :initarg :properties :accessor properties)
(date :initarg :date
:accessor date
:type integer
:documentation "The date as an integer representing the UNIX epoch time.")
(intervals :initarg :intervals
:accessor intervals
:documentation "The intervals associated with this day.")
(events :initarg :events
:accessor events
:documentation "The events associated with this day.")))
#+END_SRC
**** interval :class:
#+BEGIN_SRC lisp
(defclass interval ()
((properties :initarg :properties :accessor properties)
(activity :initarg :activity
:accessor activity
:type string
:documentation "The name of the task executed during this interval.")
(start :initarg :start
:accessor start
:type integer
:documentation "The time at which this interval started, as an integer representing the UNIX epoch time.")
(stop :initarg :stop
:accessor stop
:type integer
:documentation "The time at which this interval ended, as an integer representing the UNIX epoch time."))
(:documentation "A time range spent on a specific task, with optional properties."))
#+END_SRC
**** event :class:
#+BEGIN_SRC lisp
(defclass event ()
((properties :initarg :properties :accessor properties)
(name :initarg :name
:accessor name
:type string
:documentation "The name of this event.")
(time :initarg :time
:accessor time
:type integer
:documentation "The time at which this interval started, as an integer representing the UNIX epoch time."))
(:documentation "A named timestamp with optional properties."))
#+END_SRC
**** run-assertions :generic:function:
#+BEGIN_SRC lisp
(defgeneric backend-run-assertions (backend)
@ -981,8 +1033,7 @@ For instance, a file-based backend could be undergoing editing by
a user."))
#+END_SRC
**** file operations
***** create-file :generic:function:
**** create-file :generic:function:
[[file:../tests/tests.org::#tests-backend-create-file][tests]]
#+BEGIN_SRC lisp
@ -992,14 +1043,13 @@ Use FILE as a path, if provided.
Return path of new file if successfully created, and nil if it already exists."))
#+END_SRC
***** latest-date-records :generic:function:
**** get :generic:function:
#+BEGIN_SRC lisp
(defgeneric latest-date-records (backend)
(:documentation "Return intervals of latest day in BACKEND as a tagged list (\"DATE\" PLIST*).
Return nil if BACKEND contains no records."))
(defgeneric get (date backend)
(:documentation "Return day associated with DATE from BACKEND, or nil if no such day exists."))
#+END_SRC
***** insert :generic:function:
**** insert :generic:function:
#+BEGIN_SRC lisp
(defgeneric insert (backend plist &key &allow-other-keys)
(:documentation "Insert PLIST as new record in BACKEND.
@ -1013,7 +1063,7 @@ PLIST may be an interval which crosses days."))
(error "Not a valid plist: %S" plist)))
#+END_SRC
***** remove-last :generic:function:
**** remove-last :generic:function:
#+BEGIN_SRC lisp
(defgeneric remove-last (backend &key &allow-other-keys)
(:documentation "Remove last record from BACKEND.
@ -1021,7 +1071,7 @@ Return non-nil if record is successfully removed.
Signal an error if there is no record to remove."))
#+END_SRC
***** latest-record :generic:function:
**** latest-record :generic:function:
#+BEGIN_SRC lisp
(defgeneric latest-record (backend)
(:documentation "Return the latest record from BACKEND as a plist, or nil if BACKEND contains no records.
@ -1032,7 +1082,7 @@ 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:
**** task-records-for-date :generic:function:
#+BEGIN_SRC lisp
(defgeneric task-records-for-date (backend task date-ts &key &allow-other-keys)
(:documentation "From BACKEND, return records for TASK on DATE-TS as a list of plists.
@ -1047,7 +1097,7 @@ Return nil if BACKEND contains no records."))
(error "date-ts %S is not a `ts' struct" date-ts)))
#+END_SRC
***** replace-last :generic:function:
**** replace-last :generic:function:
#+BEGIN_SRC lisp
(defgeneric replace-last (backend plist &key &allow-other-keys)
(:documentation "Replace last record in BACKEND with PLIST.
@ -1058,14 +1108,14 @@ Return non-nil if successful."))
(error "Not a valid plist: %S" plist)))
#+END_SRC
***** to-file :generic:function:
**** to-file :generic:function:
#+BEGIN_SRC lisp
(defgeneric to-file (input-hash-table output-backend output-file)
(:documentation "Save data from INPUT-HASH-TABLE to OUTPUT-FILE, in OUTPUT-BACKEND format.
Any existing data in OUTPUT-FILE is overwritten."))
#+END_SRC
***** on-add :generic:function:
**** on-add :generic:function:
#+BEGIN_SRC lisp
(defgeneric on-add (backend)
(:documentation "Function called when data is added to BACKEND.
@ -1076,7 +1126,7 @@ backend file).
NEW-DATA is the data that was added."))
#+END_SRC
***** on-modify :generic:function:
**** on-modify :generic:function:
#+BEGIN_SRC lisp
(defgeneric on-modify (backend)
(:documentation "Function called when data in BACKEND is modified (rather than added or removed).
@ -1088,7 +1138,7 @@ OLD-DATA and NEW-DATA is the data before and after the changes,
respectively."))
#+END_SRC
***** on-remove :generic:function:
**** on-remove :generic:function:
#+BEGIN_SRC lisp
(defgeneric on-remove (backend)
(:documentation "Function called when data is removed from BACKEND.
@ -1099,7 +1149,7 @@ the backend file).
OLD-DATA is the data that was modified."))
#+END_SRC
***** on-change :generic:function:
**** on-change :generic:function:
#+BEGIN_SRC lisp
(defgeneric on-change (backend &rest args)
(:documentation "Function to be run when BACKEND changes on disk.
@ -1108,7 +1158,7 @@ This may happen within Chronometrist (e.g. via
backend file)."))
#+END_SRC
***** verify :generic:function:
**** verify :generic:function:
#+BEGIN_SRC lisp
(defgeneric verify (backend)
(:documentation "Check BACKEND for errors in data.
@ -1117,7 +1167,7 @@ Return nil if no errors are found.
If an error is found, return (LINE-NUMBER . COLUMN-NUMBER) for file-based backends."))
#+END_SRC
***** on-file-path-change :generic:function:
**** on-file-path-change :generic:function:
#+BEGIN_SRC lisp
(defgeneric on-file-path-change (backend old-path new-path)
(:documentation "Function run when the value of `file' is changed.
@ -1125,14 +1175,13 @@ OLD-PATH and NEW-PATH are the old and new values of
`file', respectively."))
#+END_SRC
**** memory operations
***** reset-backend :generic:function:
**** reset-backend :generic:function:
#+BEGIN_SRC lisp
(defgeneric reset-backend (backend)
(:documentation "Reset data structures for BACKEND."))
#+END_SRC
***** to-hash-table :generic:function:
**** to-hash-table :generic:function:
#+BEGIN_SRC lisp
(defgeneric to-hash-table (backend)
(:documentation "Return data in BACKEND as a hash table in chronological order.
@ -1141,13 +1190,13 @@ lists of records, represented by plists. Both hash table keys and
hash table values must be in chronological order."))
#+END_SRC
***** to-list :generic:function:
**** to-list :generic:function:
#+BEGIN_SRC lisp
(defgeneric to-list (backend)
(:documentation "Return all records in BACKEND as a list of plists."))
#+END_SRC
***** memory-layer-empty-p :generic:function:
**** memory-layer-empty-p :generic:function:
#+BEGIN_SRC lisp
(defgeneric memory-layer-empty-p (backend)
(:documentation "Return non-nil if memory layer of BACKEND contains no records, else nil."))
@ -2419,13 +2468,13 @@ s-expressions in a text column.")
**** table-function :function:
#+BEGIN_SRC lisp
#+(or)
(defun table-function (table-specification)
(loop for col-spec in *table-specification*
for (sym str) in *table-specification*
(loop ;; for col-spec in *table-specification*
;; for (sym str) in *table-specification*
with date = ()
for task in (chronometrist:task-list)
for index from 1
collect (cond ((eq sym 'index) index)
(()))))
collect (list index task)))
#+END_SRC
**** display

View File

@ -55,16 +55,6 @@ Chronometrist records /time intervals/ (earlier called "events") as plists. Each
See also [[#explanation-time-formats][Currently-Used Time Formats]]
** Overview
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 [[#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 -
+ A responsive program is more likely to be used; recall our design goal of 'incentivizing use'.

View File

@ -24,11 +24,10 @@ Chronometrist is a friendly and powerful personal time tracker and analyzer. It
:CUSTOM_ID: benefits
:END:
1. Extremely simple and efficient to use
2. Displays useful information about your time usage
2. Displays useful information about your time usage (including fancy graphs with the =chronometrist-spark= extension)
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
4. Human errors in tracking can be easily fixed by editing a plain text file
5. Hooks to integrate time tracking into your workflow
** Limitations
:PROPERTIES:
@ -75,28 +74,56 @@ In March 2022, work began on the long-awaited Common Lisp port of Chronometrist,
The port was also driven by the desire to have access to Common Lisp's better performance, and features such as namespaces, a /de facto/ standard build system, multithreading, SQLite bindings, a more fully-featured implementation of CLOS and MOP, and type annotations, checking, and inference.
Currently, this port can -
The literate sources for the Common Lisp port may be found in [[file:cl/chronometrist.org][cl/chronometrist.org]]. Currently, this port can -
1. import from a plist-group file and export to an SQLite database
#+BEGIN_SRC lisp
(chronometrist:to-file (chronometrist:to-hash-table
(make-instance 'chronometrist.plist-group:plist-group-backend
:file "/path/to/file.plg"))
(make-instance 'chronometrist.sqlite:sqlite-backend)
"/path/to/file.sqlite")
(chronometrist:to-file (chronometrist:to-hash-table
(make-instance 'chronometrist.plist-group:plist-group-backend
:file "/path/to/file.plg"))
(make-instance 'chronometrist.sqlite:sqlite-backend)
"/path/to/file.sqlite")
#+END_SRC
2. display a (WIP) CLIM GUI - =(chronometrist.clim:run-chronometrist)=
The Emacs Lisp codebase will probably become an Emacs frontend to a future Common Lisp CLI client.
[fn:1] McCLIM also has an incomplete ncurses backend - when completed, a CLIM frontend could provide a TUI "for free".
** Literate Program
** Literate program
:PROPERTIES:
:CUSTOM_ID: explanation-literate-program
:END:
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).
Chronometrist is written as an Org literate program, which makes it easy to obtain different views of the program source, thanks to tree- and source-block folding, tags, properties, and the =org-match= command.
The Org file can also be loaded directly using the [[https://github.com/jingtaozf/literate-elisp][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-literate-elisp][How to load the program using literate-elisp]].
The canonical source file is [[file:elisp/chronometrist.org][elisp/chronometrist.org]], which contains source blocks. These are provided to users after /tangling/ (extracting the source into an Emacs Lisp file). [fn:2]
=chronometrist.org= is also included in MELPA installs, although not used directly by default, since doing so would interfere with automatic generation of autoloads.
The Org literate program can also be loaded directly using the [[https://github.com/jingtaozf/literate-elisp][literate-elisp]] package, so that all source links (e.g. =xref=, =describe-function=) lead to the Org file. See [[#how-to-literate-elisp][How to load the program using literate-elisp]].
[fn:2] the literate source is also included in MELPA installs, although not loaded through =literate-elisp-load= by default, since doing so would interfere with automatic generation of autoloads.
** Source code overview
At its most basic, we read data from a [[file:elisp/chronometrist.org::#program-backend][backend]] and [[file:elisp/chronometrist.org::#program-frontend-chronometrist][display it]] as a [[elisp:(find-library "tabulated-list")][=tabulated-list-mode=]] buffer.
The plist and plist-group backends (collectively known as the s-expression backends) =read= a text file containing s-expressions into a [[file:elisp/chronometrist.org::#program-data-structures][hash table]], and query that. When the file is changed—whether by the program or the user—they [[file:elisp/chronometrist.org::refresh-file][update the hash table]] and the [[file:elisp/chronometrist.org::#program-frontend-chronometrist-refresh][buffer]]. The s-expression backends also make use of a [[file:elisp/chronometrist.org::#program-pretty-printer][plist pretty-printer]] of their own.
There are also some [[file:elisp/chronometrist.org::#program-migration][migration commands]].
Extensions exist for -
1. [[file:elisp/chronometrist-key-values.org][attaching arbitrary metadata]] to time intervals,
2. [[https://tildegit.org/contrapunctus/chronometrist-goal][time goals and alerts]], and
3. support for the [[file:elisp/chronometrist-third.org][Third Time system]]
** Contributions and contact
:PROPERTIES:
:CUSTOM_ID: contributions-contact
:END:
Feedback and MRs are very welcome. 🙂
+ [[file:TODO.org]] has a long list of tasks
+ [[file:elisp/chronometrist.org]] 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 - [[https://conversations.im/j/emacs@salas.suchat.org][xmpp:emacs@salas.suchat.org?join]] ([[https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org][web chat]])
(For help in getting started with Jabber, [[https://xmpp.org/getting-started/][click here]])
** License
:PROPERTIES:
@ -220,6 +247,9 @@ Evaluate or add to your init.el the following -
:PROPERTIES:
:CUSTOM_ID: how-to-literate-elisp
:END:
The literate Org document will automatically =literate-elisp-load= itself when opened, if =literate-elisp= is installed via =package.el=.
If you want it to be loaded with =literate-elisp-load= on Emacs startup, add the following to your init.el -
#+BEGIN_SRC emacs-lisp
(add-to-list 'load-path "<directory containing chronometrist.org>")
@ -231,7 +261,7 @@ Evaluate or add to your init.el the following -
:PROPERTIES:
:CUSTOM_ID: how-to-tags
:END:
1. Add =chronometrist-tags-add= to one or more of these hooks [fn:2] -
1. Add =chronometrist-tags-add= to one or more of these hooks [fn:3] -
#+BEGIN_SRC emacs-lisp
(add-to-list 'chronometrist-after-in-functions 'chronometrist-tags-add)
@ -242,18 +272,18 @@ Evaluate or add to your init.el the following -
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=.
[fn:2] but not =chronometrist-before-in-functions=
[fn:3] but not =chronometrist-before-in-functions=
** How to attach key-values to time intervals
:PROPERTIES:
:CUSTOM_ID: how-to-key-value-pairs
:END:
1. Add =chronometrist-kv-add= to one or more of these hooks [fn:2] -
1. Add =chronometrist-kv-add= to one or more of these hooks [fn:3] -
#+BEGIN_SRC emacs-lisp
(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)
(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)
#+END_SRC
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.
@ -323,7 +353,7 @@ Return nil (and run `magit-status') if the user answers no."
:PROPERTIES:
:CUSTOM_ID: how-to-backup
:END:
I suggest backing up Chronometrist data on each save using the [[https://tildegit.org/contrapunctus/async-backup][async-backup]] package.[fn:3] Here's how you can do that.
I suggest backing up Chronometrist data on each save using the [[https://tildegit.org/contrapunctus/async-backup][async-backup]] package.[fn:4] Here's how you can do that.
1. Add the following to your init.
#+BEGIN_SRC emacs-lisp
@ -332,9 +362,10 @@ I suggest backing up Chronometrist data on each save using the [[https://tildegi
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.
3. Optionally, configure =async-backup-location= to set a specific directory for the backups -
: (setq async-backup-location "/path/to/backup/dir/")
[fn: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.
[fn:4] 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
:PROPERTIES:
@ -385,18 +416,6 @@ Hooks
8. =chronometrist-file-change-hook=
9. =chronometrist-timer-hook=
* Contributions and contact
:PROPERTIES:
:CUSTOM_ID: contributions-contact
:END:
Feedback and MRs are very welcome. 🙂
+ [[file:TODO.org]] has a long list of tasks
+ [[file:elisp/chronometrist.org]] 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 - [[https://conversations.im/j/emacs@salas.suchat.org][xmpp:emacs@salas.suchat.org?join]] ([[https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org][web chat]])
(For help in getting started with Jabber, [[https://xmpp.org/getting-started/][click here]])
* Local variables :noexport:
# Local Variables:
# my-org-src-default-lang: "emacs-lisp"