Compare commits

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

43 Commits
dev ... sql

Author SHA1 Message Date
contrapunctus 6c96cc4e57 Update more call sites and method signatures 2020-12-30 02:18:12 +05:30
contrapunctus ffe615c1e8 Update call sites for modified backend interface 2020-12-24 17:09:37 +05:30
contrapunctus 1889357b07 TODO - fix botched merge 2020-12-23 02:24:01 +05:30
contrapunctus 3fdc43f334 Move queries to backend interface
Remove FILE argument from backend interface, since it can now be
constructed from the backend object. Results in some loss of
flexibility, but improvement in call site brevity.
2020-12-23 00:51:31 +05:30
contrapunctus a975bf80d7 Merge branch 'dev' into sql 2020-12-22 20:46:53 +05:30
contrapunctus 2ca6c60bf2 chronometrist-query-stop - use aand 2020-12-21 04:02:57 +05:30
contrapunctus 16de1e0837 chronometrist-file - update docstring 2020-12-21 04:02:28 +05:30
contrapunctus 931e5515bf Revisit dependencies
Backends now depend on chronometrist.el, not vice versa.

chronometrist-statistics and chronometrist-report now add functions to
the new chronometrist-timer-hook, so chronometrist-timer.el no longer
depends on them.
2020-12-01 18:47:00 +05:30
contrapunctus e183e463cd Merge branch 'dev' into sql 2020-11-26 02:09:59 +05:30
contrapunctus 3b1f90e8a4 Save backend file path in the backend object itself. 2020-11-18 14:11:53 +05:30
contrapunctus 1ffabf5545 Merge branch 'dev' into sql 2020-11-17 08:29:16 +05:30
contrapunctus ef0eb25dcf Implement some methods for sqlite backend 2020-11-17 07:46:37 +05:30
contrapunctus 922fa41833 Merge branch 'dev' into sql 2020-11-11 01:51:59 +05:30
contrapunctus 108411c82f manual - explain design situation for multiple backends 2020-11-11 01:47:35 +05:30
contrapunctus 9a5c577a3d sqlite3 - implement open-file and new-record methods 2020-11-11 01:46:42 +05:30
contrapunctus f3e23df86e Remove extension from -file; use -file-path instead 2020-11-11 01:44:58 +05:30
contrapunctus 3ada39fb84 Rename backend-add-new -> backend-new-record
The former sounded like it was for adding a new backend, rather than a
new record in a backend.
2020-11-11 01:39:46 +05:30
contrapunctus 74b1206296 Remove migrate-db from migrate.el 2020-11-10 15:40:21 +05:30
contrapunctus 9f8ff1b384 manual - fix footnotes with incorrect and missing markup 2020-11-10 11:47:15 +05:30
contrapunctus 0e9c6b37d4 Start defining sqlite3 backend 2020-11-10 00:13:37 +05:30
contrapunctus 9b570e100f Make code denser 2020-11-10 00:03:52 +05:30
contrapunctus 5b5199476d Fix additional errors 2020-11-09 21:14:47 +05:30
contrapunctus 4227b306b0 TODO - add two items 2020-11-09 21:09:46 +05:30
contrapunctus 388865e58b Improve docstring 2020-11-09 21:09:02 +05:30
contrapunctus b7baba6376 Make code more concise 2020-11-09 21:08:24 +05:30
contrapunctus 6b93d8183b Rename backend generic functions 2020-11-09 21:06:47 +05:30
contrapunctus 3fc7109a95 Fix numerous errors 2020-11-09 18:38:09 +05:30
contrapunctus 0ca1dd96b3 Change nil-returning `if`s to `when` and `unless` 2020-11-09 14:11:07 +05:30
contrapunctus 2c3045e7c1 Remove old function 2020-11-09 14:09:44 +05:30
contrapunctus 1037cacd3c Convert s-expression backend functions to methods
They are also renamed to match the names of the generic functions.
2020-11-09 14:08:42 +05:30
contrapunctus 8acf23b593 Remove -backend from generic function names 2020-11-09 13:36:03 +05:30
contrapunctus 30361027c7 Add initforms, use shorter slot name, define backends as subclasses 2020-11-09 13:35:07 +05:30
contrapunctus f840e8ddb2 Create foundation for multiple backends using EIEIO 2020-11-09 01:24:10 +05:30
contrapunctus 9c503a88eb Fix error introduced in 6b1d554 2020-11-08 18:09:42 +05:30
contrapunctus 1f4f018c70 Write working function to convert from hash table->SQLite 2020-11-08 18:04:13 +05:30
contrapunctus b141b3b2d1 Add scratch code for SQL migration 2020-11-07 14:41:38 +05:30
contrapunctus aa22914fcb Format code to be denser 2020-11-07 14:41:20 +05:30
contrapunctus 74fdfa833c Use `format` instead of `concat` 2020-11-07 14:41:01 +05:30
contrapunctus fa991fdf01 Merge branch 'dev' into sql 2020-11-07 13:25:12 +05:30
contrapunctus 6b1d55493d Remove ->> 2020-11-01 16:52:03 +05:30
contrapunctus 32c976a6f2 Merge branch 'dev' into sql 2020-10-20 12:26:38 +05:30
contrapunctus 956835e2d1 Function to read from s-expression file 2020-03-24 02:35:28 +05:30
contrapunctus 7b5b9b75ad Add initial migration code 2020-03-23 21:41:59 +05:30
17 changed files with 697 additions and 488 deletions

View File

@ -1,7 +1,8 @@
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")
((emacs-lisp-mode . ((nameless-aliases . (("cc" . "chronometrist-common")
((emacs-lisp-mode . ((nameless-aliases . (("cb" . "chronometrist-backend")
("cc" . "chronometrist-common")
("cd" . "chronometrist-diary")
("ce" . "chronometrist-events")
("ck" . "chronometrist-kv")

View File

@ -171,9 +171,8 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
: (foo (1 . bar))
Yikes!
** chronometrist [8%]
1. [ ] Add =:stop= time when we call =chronometrist-kv-accept=, not when we quit the key-value prompt.
2. [ ] Implement undo/redo by running undo-tree commands on chronometrist.sexp
** New features [0%]
1. [ ] 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.
@ -182,9 +181,8 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
4. [ ] *Delete* a project (erasing all records)
5. [ ] *Hide* a project (don't show it in any Chronometrist-* buffer, effectively deleting it non-destructively)
6. [ ] *Reset* current interval - update the =:start= time to the current time.
7. [ ] 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.
** chronometrist [14%]
** chronometrist [12%]
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=.
@ -206,6 +204,15 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
* If the input string can be read in a single call to =read=, treat it as an s-expression; else, use the current heuristics.
6. [ ] 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.
7. [ ] Customizable field widths
8. [ ] 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.
*** Migration [0%]
1. [ ] Implement readers (format -> hash table), emitters (hash table -> format), and predicates for each format. Then implement a single =migrate= command to convert between any supported formats.
* sounds like a job for EIEIO - each format could be defined as an object, with slots for information such as the file extension.
2. [ ] Instead of using =require= for all backends and dependencies, check which backends are loaded, and load dependencies accordingly.
3. [ ] Make migration asynchronous, and use output from the sub-process to make a progress indicator.
4. [ ] Optimization - (SQL) read all keywords once, remove duplicates, and create columns for them, instead of checking =pragma= for each plist.
5. [ ] (SQL) Use transactions.
** chronometrist-report [0%]
1. [ ] Show week counter and max weeks; don't scroll past first/last weeks

View File

@ -32,6 +32,7 @@ The structure of this manual was inspired by @uref{https://documentation.divio.c
How to@dots{}
* How to set up Emacs to contribute::
* How to create a new backend::
Explanation
@ -47,6 +48,7 @@ Explanation
Chronometrist
* Optimization::
* Backends::
Midnight-spanning events
@ -63,18 +65,15 @@ Tags and Key-Values
Reference
* Legend of currently-used time formats::
* chronometrist-backend.el: chronometrist-backendel.
* chronometrist-common.el: chronometrist-commonel.
* chronometrist-custom.el: chronometrist-customel.
* chronometrist-diary-view.el: chronometrist-diary-viewel.
* chronometrist.el: chronometristel.
* chronometrist-events.el: chronometrist-eventsel.
* chronometrist-migrate.el: chronometrist-migrateel.
* chronometrist-plist-pp.el: chronometrist-plist-ppel.
* chronometrist-queries.el: chronometrist-queriesel.
* chronometrist-report-custom.el: chronometrist-report-customel.
* chronometrist-report.el: chronometrist-reportel.
* chronometrist-key-values.el: chronometrist-key-valuesel.
* chronometrist-statistics-custom.el: chronometrist-statistics-customel.
* chronometrist-statistics.el: chronometrist-statisticsel.
* chronometrist-time.el: chronometrist-timeel.
* chronometrist-timer.el: chronometrist-timerel.
@ -98,6 +97,7 @@ Legend of currently-used time formats
@menu
* How to set up Emacs to contribute::
* How to create a new backend::
@end menu
@node How to set up Emacs to contribute
@ -123,6 +123,16 @@ From the project root, you can now run
@end enumerate
@end enumerate
@node How to create a new backend
@section How to create a new backend
@enumerate
@item
Subclass @samp{chronometrist-backend} to define your backend.
@item
Create methods for the eight generic functions in @samp{chronometrist-backend.el}
@end enumerate
@node Explanation
@chapter Explanation
@ -157,7 +167,7 @@ Hooks allow the time tracker to automate tasks and become a useful part of your
Make it easy to edit data using existing, familiar tools
@itemize
@item
We don't use an SQL database, where changing a single field is tricky [1]
We don't use an SQL database, where changing a single field is tricky @footnote{I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?}
@item
We use a text file containing s-expressions (easy for humans to read and write)
@item
@ -171,8 +181,6 @@ Have a useful, informative, interactive interface
Support mouse and keyboard use equally
@end enumerate
[1] I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?
@node Terminology
@section Terminology
@ -214,6 +222,7 @@ Note - sometimes, when hacking or dealing with errors, timers may result in subt
@menu
* Optimization::
* Backends::
@end menu
@node Optimization
@ -248,7 +257,7 @@ The next one was released in v0.5. Till then, any time the @uref{../elisp/chrono
After the optimization@dots{}
@enumerate
@item
Two backend functions (@uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-sexp-new}} and @uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-sexp-replace-last}}) were modified to set a flag (@uref{../elisp/chronometrist.el, @samp{chronometrist--inhibit-read-p}}) before saving the file.
Two backend functions (@uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-new}} and @uref{../elisp/chronometrist-sexp.el, @samp{chronometrist-replace-last}}) were modified to set a flag (@uref{../elisp/chronometrist.el, @samp{chronometrist--inhibit-read-p}}) before saving the file.
@item
If this flag is non-nil, @uref{../elisp/chronometrist.el, @samp{chronometrist-refresh-file}} skips the expensive calls to @samp{chronometrist-events-populate}, @samp{chronometrist-tasks-from-table}, and @samp{chronometrist-tags-history-populate}, and resets the flag.
@item
@ -266,6 +275,45 @@ Instead, the aforementioned backend functions modify the relevant variables - @s
There are still some operations which @uref{../elisp/chronometrist.el, @samp{chronometrist-refresh-file}} runs unconditionally - which is to say there is scope for further optimization, if or when required.
@end enumerate
@node Backends
@subsection Backends
Chronometrist can currently store data in two ways -
@enumerate
@item
as plists in a plain text file, using the s-expression backend.
@item
as an SQL database, using the sqlite3 backend.
@end enumerate
Each of these has areas they excel in - plain text files are VCS-friendly, permit the use of tools like @samp{grep}, and are easy to make small, one-off edits in. SQL databases support a standard and popular query language, ACID guarantees, and are easy to make sweeping changes in.
To try and get the best of both, two approaches were considered -
@enumerate
@item
Support for multiple simultaneous backends
@itemize
@item
@samp{chronometrist-backend-list} could be a custom variable, containing a list of enabled backends
@item
A function like @samp{(call-with-backends generic-function &rest args)} could be created, which calls the generic-function with each backend as the first argument, and use this whenever Chronometrist makes a change to the files.
@item
When the user makes a change to one of the files outside of Chronometrist, we detect the change (via filesystem watchers) and propagate it to the other backends.
@end itemize
Consequently, a user can make quick, one-off edits to the SQL database by editing a text file, or make sweeping, structure-aware changes to the text file using SQL statements.
@item
SQL as the main backend, with viewing/editing UI + auto-export
@enumerate
@item
Write a 'record viewer/editor' for Chronometrist records. Basically, it reads in a record from the SQL database and displays it in an editable buffer. Forward/backward commands would be available, like in @samp{chronometrist-report} and @samp{chronometrist-statistics}. The record could be represented as anything - CSV, Org markup, Lisp data, etc. The user could edit and run a command to accept the changes, which would change the database.
@item
Automatically export from SQL to a plain text (or other) format, whenever there is a change in the database. (probably using hooks)
@end enumerate
These two put together result in the viewing and editing of individual records being easier, and the plain text format can be placed in a VCS, without dealing with any synchronization issues.
@end enumerate
Backends are defined as subclasses of the class @samp{chronometrist-backend} @footnote{I would have liked to define them as instances of @samp{chronometrist-backend}, but EIEIO, unlike CLOS, does not support EQL specialization.}.
@node Midnight-spanning events
@section Midnight-spanning events
@ -425,18 +473,15 @@ Each of these has a corresponding function to clear it and fill it with values -
@menu
* Legend of currently-used time formats::
* chronometrist-backend.el: chronometrist-backendel.
* chronometrist-common.el: chronometrist-commonel.
* chronometrist-custom.el: chronometrist-customel.
* chronometrist-diary-view.el: chronometrist-diary-viewel.
* chronometrist.el: chronometristel.
* chronometrist-events.el: chronometrist-eventsel.
* chronometrist-migrate.el: chronometrist-migrateel.
* chronometrist-plist-pp.el: chronometrist-plist-ppel.
* chronometrist-queries.el: chronometrist-queriesel.
* chronometrist-report-custom.el: chronometrist-report-customel.
* chronometrist-report.el: chronometrist-reportel.
* chronometrist-key-values.el: chronometrist-key-valuesel.
* chronometrist-statistics-custom.el: chronometrist-statistics-customel.
* chronometrist-statistics.el: chronometrist-statisticsel.
* chronometrist-time.el: chronometrist-timeel.
* chronometrist-timer.el: chronometrist-timerel.
@ -518,6 +563,34 @@ Used for goals (chronometrist-goals-list, chronometrist-get-goal) - minutes seem
Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
@end itemize
@node chronometrist-backendel
@section chronometrist-backend.el
@enumerate
@item
Class - chronometrist-backend ()
@item
Variable - chronometrist-backend-current
@item
Variable - chronometrist-backends
@item
Generic Function - chronometrist-backend-to-hash (backend table)
@item
Generic Function - chronometrist-backend-from-hash (backend table)
@item
Generic Function - chronometrist-backend-open-file (backend)
@item
Generic Function - chronometrist-backend-latest-record (backend)
@item
Generic Function - chronometrist-backend-current-task (backend)
@item
Generic Function - chronometrist-backend-create-file (backend)
@item
Generic Function - chronometrist-backend-new-record (backend plist)
@item
Generic Function - chronometrist-backend-replace-last (backend plist)
@end enumerate
@node chronometrist-commonel
@section chronometrist-common.el
@ -568,24 +641,6 @@ ts -> ts
@end itemize
@end enumerate
@node chronometrist-customel
@section chronometrist-custom.el
@enumerate
@item
Custom variable - chronometrist-file
@item
Custom variable - chronometrist-buffer-name
@item
Custom variable - chronometrist-hide-cursor
@item
Custom variable - chronometrist-update-interval
@item
Custom variable - chronometrist-activity-indicator
@item
Custom variable - chronometrist-day-start-time
@end enumerate
@node chronometrist-diary-viewel
@section chronometrist-diary-view.el
@ -611,6 +666,18 @@ Command - chronometrist-diary-view (&optional date)
@enumerate
@item
Custom variable - chronometrist-file
@item
Custom variable - chronometrist-buffer-name
@item
Custom variable - chronometrist-hide-cursor
@item
Custom variable - chronometrist-update-interval
@item
Custom variable - chronometrist-activity-indicator
@item
Custom variable - chronometrist-day-start-time
@item
Internal Variable - chronometrist--task-history
@item
Internal Variable - chronometrist--point
@ -758,36 +825,8 @@ Function - chronometrist-plist-pp-to-string (object)
Function - chronometrist-plist-pp (object &optional stream)
@end enumerate
@node chronometrist-queriesel
@section chronometrist-queries.el
@enumerate
@item
Function - chronometrist-last ()
@itemize
@item
-> plist
@end itemize
@item
Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
@itemize
@item
String &optional ts -> seconds
@end itemize
@item
Function - chronometrist-active-time-one-day (&optional ts)
@itemize
@item
&optional ts -> seconds
@end itemize
@item
Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
@item
Function - chronometrist-task-events-in-day (task ts)
@end enumerate
@node chronometrist-report-customel
@section chronometrist-report-custom.el
@node chronometrist-reportel
@section chronometrist-report.el
@enumerate
@item
@ -796,12 +835,6 @@ Custom variable - chronometrist-report-buffer-name
Custom variable - chronometrist-report-week-start-day
@item
Custom variable - chronometrist-report-weekday-number-alist
@end enumerate
@node chronometrist-reportel
@section chronometrist-report.el
@enumerate
@item
Internal Variable - chronometrist-report--ui-date
@item
@ -912,19 +945,13 @@ Function - chronometrist-skip-query-prompt (task)
Function - chronometrist-skip-query-reset (@math{_task})
@end enumerate
@node chronometrist-statistics-customel
@section chronometrist-statistics-custom.el
@enumerate
@item
Custom variable - chronometrist-statistics-buffer-name
@end enumerate
@node chronometrist-statisticsel
@section chronometrist-statistics.el
@enumerate
@item
Custom variable - chronometrist-statistics-buffer-name
@item
Internal Variable - chronometrist-statistics--ui-state
@item
Internal Variable - chronometrist-statistics--point
@ -1079,31 +1106,59 @@ Function - chronometrist-goal-on-file-change ()
@enumerate
@item
Class - chronometrist-sexp
@item
Object - chronometrist-sexp
@item
Custom variable - chronometrist-sexp-pretty-print-function
@item
Macro - chronometrist-sexp-in-file (file &rest body)
@item
Function - chronometrist-sexp-open-log ()
Method - chronometrist-to-hash ((backend chronometrist-sexp) table)
@item
Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
Method - chronometrist-from-hash ((backend chronometrist-sexp) table)
@item
Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
Method - chronometrist-open-log ((backend chronometrist-sexp))
@item
Function - chronometrist-sexp-last ()
Method - chronometrist-last ((backend chronometrist-sexp))
@itemize
@item
-> plist
@end itemize
@item
Function - chronometrist-sexp-current-task ()
Method - chronometrist-current-task ((backend chronometrist-sexp))
@item
Function - chronometrist-sexp-events-populate ()
Method - chronometrist-create-file ((backend chronometrist-sexp))
@item
Function - chronometrist-sexp-create-file ()
@item
Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
Method - chronometrist-backend-new-record (((backend chronometrist-sexp)) plist)
@item
Function - chronometrist-sexp-delete-list (&optional arg)
@item
Function - chronometrist-sexp-replace-last (plist)
Method - chronometrist-replace-last (((backend chronometrist-sexp)) plist)
@item
Method - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
@itemize
@item
String &optional ts -> seconds
@end itemize
@item
Method - chronometrist-active-time-one-day (&optional ts)
@itemize
@item
&optional ts -> seconds
@end itemize
@item
Method - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
@itemize
@item
String &optional table -> integer
@end itemize
@item
Method - chronometrist-task-events-in-day (task ts)
@itemize
@item
String ts -> events
@end itemize
@item
Command - chronometrist-sexp-reindent-buffer ()
@end enumerate

View File

@ -16,7 +16,9 @@ All of these are optional, but recommended for the best experience.
From the project root, you can now run
1. =cask= to install the project dependencies in a sandbox
2. =cask exec buttercup -L . --traceback pretty= to run tests.
** How to create a new backend
1. Subclass =chronometrist-backend= to define your backend.
2. Create methods for the eight generic functions in =chronometrist-backend.el=
* Explanation
:PROPERTIES:
:DESCRIPTION: The design, the implementation, and a little history
@ -30,14 +32,14 @@ All of these are optional, but recommended for the best experience.
2. Incentivize use
* Hooks allow the time tracker to automate tasks and become a useful part of your workflow
3. Make it easy to edit data using existing, familiar tools
* We don't use an SQL database, where changing a single field is tricky [1]
* We don't use an SQL database, where changing a single field is tricky [fn:1]
* We use a text file containing s-expressions (easy for humans to read and write)
* We use ISO-8601 for timestamps (easy for humans to read and write) rather than UNIX epoch time
4. Reduce human errors in tracking
5. Have a useful, informative, interactive interface
6. Support mouse and keyboard use equally
[1] I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?
[fn:1] I still have doubts about this. Having SQL as a query language would be very useful in perusing the stored data. Maybe we should have tried to create a companion mode to edit SQL databases interactively?
** Terminology
:PROPERTIES:
@ -82,10 +84,10 @@ One of the earliest 'optimizations' of great importance turned out to simply be
+ It was fixed in v0.2.2 by making the watch creation conditional, using [[file:../elisp/chronometrist-common.el::defvar chronometrist--fs-watch ][=chronometrist--fs-watch=]] to store the watch object.
**** Preserve state
The next one was released in v0.5. Till then, any time the [[file:../elisp/chronometrist-custom.el::defcustom chronometrist-file (][=chronometrist-file=]] was modified, we'd clear the [[file:../elisp/chronometrist-events.el::defvar chronometrist-events (][=chronometrist-events=]] hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:1] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[* Midnight-spanning events][midnight-spanning events]] is the real performance killer.
The next one was released in v0.5. Till then, any time the [[file:../elisp/chronometrist-custom.el::defcustom chronometrist-file (][=chronometrist-file=]] was modified, we'd clear the [[file:../elisp/chronometrist-events.el::defvar chronometrist-events (][=chronometrist-events=]] hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:2] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[* Midnight-spanning events][midnight-spanning events]] is the real performance killer.
After the optimization...
1. Two backend functions ([[file:../elisp/chronometrist-sexp.el::cl-defun chronometrist-sexp-new (][=chronometrist-sexp-new=]] and [[file:../elisp/chronometrist-sexp.el::defun chronometrist-sexp-replace-last (][=chronometrist-sexp-replace-last=]]) were modified to set a flag ([[file:../elisp/chronometrist.el::defvar chronometrist--inhibit-read-p ][=chronometrist--inhibit-read-p=]]) before saving the file.
1. Two backend functions ([[file:../elisp/chronometrist-sexp.el::cl-defmethod chronometrist-new (][=chronometrist-new=]] and [[file:../elisp/chronometrist-sexp.el::cl-defmethod chronometrist-replace-last (][=chronometrist-replace-last=]]) were modified to set a flag ([[file:../elisp/chronometrist.el::defvar chronometrist--inhibit-read-p ][=chronometrist--inhibit-read-p=]]) before saving the file.
2. If this flag is non-nil, [[file:../elisp/chronometrist.el::defun chronometrist-refresh-file (][=chronometrist-refresh-file=]] skips the expensive calls to =chronometrist-events-populate=, =chronometrist-tasks-from-table=, and =chronometrist-tags-history-populate=, and resets the flag.
3. Instead, the aforementioned backend functions modify the relevant variables - =chronometrist-events=, =chronometrist-task-list=, and =chronometrist-tags-history= - via...
* =chronometrist-events-add= / =chronometrist-events-replace-last=
@ -94,7 +96,29 @@ After the optimization...
There are still some operations which [[file:../elisp/chronometrist.el::defun chronometrist-refresh-file (][=chronometrist-refresh-file=]] runs unconditionally - which is to say there is scope for further optimization, if or when required.
[fn:1] As indicated by exploratory work in the =parsimonious-reading= branch, where I made a loop to only =read= and collect s-expressions from the file. It was near-instant...until I added event splitting to it.
[fn:2] As indicated by exploratory work in the =parsimonious-reading= branch, where I made a loop to only =read= and collect s-expressions from the file. It was near-instant...until I added event splitting to it.
*** Backends
Chronometrist can currently store data in two ways -
1. as plists in a plain text file, using the s-expression backend.
2. as an SQL database, using the sqlite3 backend.
Each of these has areas they excel in - plain text files are VCS-friendly, permit the use of tools like =grep=, and are easy to make small, one-off edits in. SQL databases support a standard and popular query language, ACID guarantees, and are easy to make sweeping changes in.
To try and get the best of both, two approaches were considered -
1. Support for multiple simultaneous backends
+ =chronometrist-backend-list= could be a custom variable, containing a list of enabled backends
+ A function like =(call-with-backends generic-function &rest args)= could be created, which calls the generic-function with each backend as the first argument, and use this whenever Chronometrist makes a change to the files.
+ When the user makes a change to one of the files outside of Chronometrist, we detect the change (via filesystem watchers) and propagate it to the other backends.
Consequently, a user can make quick, one-off edits to the SQL database by editing a text file, or make sweeping, structure-aware changes to the text file using SQL statements.
2. SQL as the main backend, with viewing/editing UI + auto-export
1. Write a 'record viewer/editor' for Chronometrist records. Basically, it reads in a record from the SQL database and displays it in an editable buffer. Forward/backward commands would be available, like in =chronometrist-report= and =chronometrist-statistics=. The record could be represented as anything - CSV, Org markup, Lisp data, etc. The user could edit and run a command to accept the changes, which would change the database.
2. Automatically export from SQL to a plain text (or other) format, whenever there is a change in the database. (probably using hooks)
These two put together result in the viewing and editing of individual records being easier, and the plain text format can be placed in a VCS, without dealing with any synchronization issues.
Backends are defined as subclasses of the class =chronometrist-backend= [fn:3].
[fn:3] I would have liked to define them as instances of =chronometrist-backend=, but EIEIO, unlike CLOS, does not support EQL specialization.
** Midnight-spanning events
:PROPERTIES:
:DESCRIPTION: Events starting on one day and ending on another
@ -153,11 +177,11 @@ A quick description, starting from the first time [[file:../elisp/chronometrist-
:END:
[[file:../elisp/chronometrist-key-values.el][chronometrist-key-values.el]] deals with adding additional information to events, in the form of key-values and tags.
Key-values are stored as plist keywords and values. The user can add any keywords except =:name=, =:tags=, =:start=, and =:stop=. [fn:2] Values can be any readable Lisp values.
Key-values are stored as plist keywords and values. The user can add any keywords except =:name=, =:tags=, =:start=, and =:stop=. [fn:4] Values can be any readable Lisp values.
Similarly, tags are stored using a =:tags (<tag>*)= keyword-value pair. The tags themselves (the elements of the list) can be any readable Lisp value.
[fn:2] To remove this restriction, I had briefly considered making a keyword called =:user=, whose value would be another plist containing all user-defined keyword-values. But in practice, this hasn't been a big enough issue yet to justify the work.
[fn:4] To remove this restriction, I had briefly considered making a keyword called =:user=, whose value would be another plist containing all user-defined keyword-values. But in practice, this hasn't been a big enough issue yet to justify the work.
*** User input
The entry points are [[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-add (][=chronometrist-kv-add=]] and [[file:../elisp/chronometrist-key-values.el::defun chronometrist-tags-add (][=chronometrist-tags-add=]]. The user adds these to the desired hooks, and they prompt the user for tags/key-values.
@ -207,6 +231,19 @@ Each of these has a corresponding function to clear it and fill it with values -
(hours minute seconds)
* Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
** chronometrist-backend.el
1. Class - chronometrist-backend ()
2. Variable - chronometrist-backend-current
3. Variable - chronometrist-backends
4. Generic Function - chronometrist-backend-to-hash (backend table)
5. Generic Function - chronometrist-backend-from-hash (backend table)
6. Generic Function - chronometrist-backend-open-file (backend)
7. Generic Function - chronometrist-backend-latest-record (backend)
8. Generic Function - chronometrist-backend-current-task (backend)
9. Generic Function - chronometrist-backend-create-file (backend)
10. Generic Function - chronometrist-backend-new-record (backend plist)
11. Generic Function - chronometrist-backend-replace-last (backend plist)
** chronometrist-common.el
1. Variable - chronometrist-empty-time-string
2. Variable - chronometrist-date-re
@ -227,14 +264,6 @@ Each of these has a corresponding function to clear it and fill it with values -
14. Function - chronometrist-previous-week-start (ts)
* ts -> ts
** chronometrist-custom.el
1. Custom variable - chronometrist-file
2. Custom variable - chronometrist-buffer-name
3. Custom variable - chronometrist-hide-cursor
4. Custom variable - chronometrist-update-interval
5. Custom variable - chronometrist-activity-indicator
6. Custom variable - chronometrist-day-start-time
** chronometrist-diary-view.el
1. Variable - chronometrist-diary-buffer-name
2. Internal Variable - chronometrist-diary--current-date
@ -245,41 +274,47 @@ Each of these has a corresponding function to clear it and fill it with values -
7. Command - chronometrist-diary-view (&optional date)
** chronometrist.el
1. Internal Variable - chronometrist--task-history
2. Internal Variable - chronometrist--point
3. Internal Variable - chronometrist--inhibit-read-p
4. Keymap - chronometrist-mode-map
5. Command - chronometrist-open-log (&optional button)
6. Function - chronometrist-common-create-file ()
7. Function - chronometrist-task-active? (task)
1. Custom variable - chronometrist-file
2. Custom variable - chronometrist-buffer-name
3. Custom variable - chronometrist-hide-cursor
4. Custom variable - chronometrist-update-interval
5. Custom variable - chronometrist-activity-indicator
6. Custom variable - chronometrist-day-start-time
7. Internal Variable - chronometrist--task-history
8. Internal Variable - chronometrist--point
9. Internal Variable - chronometrist--inhibit-read-p
10. Keymap - chronometrist-mode-map
11. Command - chronometrist-open-log (&optional button)
12. Function - chronometrist-common-create-file ()
13. Function - chronometrist-task-active? (task)
* String -> Boolean
8. Function - chronometrist-use-goals? ()
9. Function - chronometrist-activity-indicator ()
10. Function - chronometrist-entries ()
11. Function - chronometrist-task-at-point ()
12. Function - chronometrist-goto-last-task ()
13. Function - chronometrist-print-keybind (command &optional description firstonly)
14. Function - chronometrist-print-non-tabular ()
15. Function - chronometrist-goto-nth-task (n)
16. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
17. Function - chronometrist-refresh-file (fs-event)
18. Command - chronometrist-query-stop ()
19. Command - chronometrist-in (task &optional _prefix)
20. Command - chronometrist-out (&optional _prefix)
21. Variable - chronometrist-before-in-functions
22. Variable - chronometrist-after-in-functions
23. Variable - chronometrist-before-out-functions
24. Variable - chronometrist-after-out-functions
25. Function - chronometrist-run-functions-and-clock-in (task)
26. Function - chronometrist-run-functions-and-clock-out (task)
27. Keymap - chronometrist-mode-map
28. Major Mode - chronometrist-mode
29. Function - chronometrist-toggle-task-button (button)
30. Function - chronometrist-add-new-task-button (button)
31. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
32. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
33. Command - chronometrist-add-new-task ()
34. Command - chronometrist (&optional arg)
14. Function - chronometrist-use-goals? ()
15. Function - chronometrist-activity-indicator ()
16. Function - chronometrist-entries ()
17. Function - chronometrist-task-at-point ()
18. Function - chronometrist-goto-last-task ()
19. Function - chronometrist-print-keybind (command &optional description firstonly)
20. Function - chronometrist-print-non-tabular ()
21. Function - chronometrist-goto-nth-task (n)
22. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
23. Function - chronometrist-refresh-file (fs-event)
24. Command - chronometrist-query-stop ()
25. Command - chronometrist-in (task &optional _prefix)
26. Command - chronometrist-out (&optional _prefix)
27. Variable - chronometrist-before-in-functions
28. Variable - chronometrist-after-in-functions
29. Variable - chronometrist-before-out-functions
30. Variable - chronometrist-after-out-functions
31. Function - chronometrist-run-functions-and-clock-in (task)
32. Function - chronometrist-run-functions-and-clock-out (task)
33. Keymap - chronometrist-mode-map
34. Major Mode - chronometrist-mode
35. Function - chronometrist-toggle-task-button (button)
36. Function - chronometrist-add-new-task-button (button)
37. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
38. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
39. Command - chronometrist-add-new-task ()
40. Command - chronometrist (&optional arg)
** chronometrist-events.el
1. Variable - chronometrist-events
@ -311,39 +346,27 @@ Each of these has a corresponding function to clear it and fill it with values -
6. Function - chronometrist-plist-pp-to-string (object)
7. Function - chronometrist-plist-pp (object &optional stream)
** chronometrist-queries.el
1. Function - chronometrist-last ()
* -> plist
2. Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
* String &optional ts -> seconds
3. Function - chronometrist-active-time-one-day (&optional ts)
* &optional ts -> seconds
4. Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
5. Function - chronometrist-task-events-in-day (task ts)
** chronometrist-report-custom.el
** chronometrist-report.el
1. Custom variable - chronometrist-report-buffer-name
2. Custom variable - chronometrist-report-week-start-day
3. Custom variable - chronometrist-report-weekday-number-alist
** chronometrist-report.el
1. Internal Variable - chronometrist-report--ui-date
2. Internal Variable - chronometrist-report--ui-week-dates
3. Internal Variable - chronometrist-report--point
4. Function - chronometrist-report-date ()
5. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
* ts-1 -> (ts-1 ... ts-7)
6. Function - chronometrist-report-date->week-dates ()
7. Function - chronometrist-report-entries ()
8. Function - chronometrist-report-print-keybind (command &optional description firstonly)
9. Function - chronometrist-report-print-non-tabular ()
10. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
11. Function - chronometrist-report-refresh-file (_fs-event)
12. Keymap - chronometrist-report-mode-map
13. Major Mode - chronometrist-report-mode
14. Function - chronometrist-report (&optional keep-date)
15. Function - chronometrist-report-previous-week (arg)
16. Function - chronometrist-report-next-week (arg)
4. Internal Variable - chronometrist-report--ui-date
5. Internal Variable - chronometrist-report--ui-week-dates
6. Internal Variable - chronometrist-report--point
7. Function - chronometrist-report-date ()
8. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
* ts-1 -> (ts-1 ... ts-7)
9. Function - chronometrist-report-date->week-dates ()
10. Function - chronometrist-report-entries ()
11. Function - chronometrist-report-print-keybind (command &optional description firstonly)
12. Function - chronometrist-report-print-non-tabular ()
13. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
14. Function - chronometrist-report-refresh-file (_fs-event)
15. Keymap - chronometrist-report-mode-map
16. Major Mode - chronometrist-report-mode
17. Function - chronometrist-report (&optional keep-date)
18. Function - chronometrist-report-previous-week (arg)
19. Function - chronometrist-report-next-week (arg)
** chronometrist-key-values.el
1. Internal Variable - chronometrist--tag-suggestions
@ -380,24 +403,22 @@ Each of these has a corresponding function to clear it and fill it with values -
32. Function - chronometrist-skip-query-prompt (task)
33. Function - chronometrist-skip-query-reset (_task)
** chronometrist-statistics-custom.el
1. Custom variable - chronometrist-statistics-buffer-name
** chronometrist-statistics.el
1. Internal Variable - chronometrist-statistics--ui-state
2. Internal Variable - chronometrist-statistics--point
3. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
* string &optional hash-table -> seconds
4. Function - chronometrist-statistics-entries-internal (table)
5. Function - chronometrist-statistics-entries ()
6. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
7. Function - chronometrist-statistics-print-non-tabular ()
8. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
9. Keymap - chronometrist-statistics-mode-map
10. Major Mode - chronometrist-statistics-mode
11. Command - chronometrist-statistics (&optional preserve-state)
12. Command - chronometrist-statistics-previous-range (arg)
13. Command - chronometrist-statistics-next-range (arg)
1. Custom variable - chronometrist-statistics-buffer-name
2. Internal Variable - chronometrist-statistics--ui-state
3. Internal Variable - chronometrist-statistics--point
4. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
* string &optional hash-table -> seconds
5. Function - chronometrist-statistics-entries-internal (table)
6. Function - chronometrist-statistics-entries ()
7. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
8. Function - chronometrist-statistics-print-non-tabular ()
9. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
10. Keymap - chronometrist-statistics-mode-map
11. Major Mode - chronometrist-statistics-mode
12. Command - chronometrist-statistics (&optional preserve-state)
13. Command - chronometrist-statistics-previous-range (arg)
14. Command - chronometrist-statistics-next-range (arg)
** chronometrist-time.el
1. Function - chronometrist-iso-timestamp->ts (timestamp)
@ -444,20 +465,29 @@ Each of these has a corresponding function to clear it and fill it with values -
13. Function - chronometrist-goal-on-file-change ()
** chronometrist-sexp
1. Custom variable - chronometrist-sexp-pretty-print-function
2. Macro - chronometrist-sexp-in-file (file &rest body)
3. Function - chronometrist-sexp-open-log ()
4. Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
5. Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
6. Function - chronometrist-sexp-last ()
* -> plist
7. Function - chronometrist-sexp-current-task ()
8. Function - chronometrist-sexp-events-populate ()
9. Function - chronometrist-sexp-create-file ()
10. Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
11. Function - chronometrist-sexp-delete-list (&optional arg)
12. Function - chronometrist-sexp-replace-last (plist)
13. Command - chronometrist-sexp-reindent-buffer ()
1. Class - chronometrist-sexp
2. Object - chronometrist-sexp
3. Custom variable - chronometrist-sexp-pretty-print-function
4. Macro - chronometrist-sexp-in-file (file &rest body)
5. Method - chronometrist-to-hash ((backend chronometrist-sexp) table)
6. Method - chronometrist-from-hash ((backend chronometrist-sexp) table)
7. Method - chronometrist-open-log ((backend chronometrist-sexp))
8. Method - chronometrist-last ((backend chronometrist-sexp))
* -> plist
9. Method - chronometrist-current-task ((backend chronometrist-sexp))
10. Method - chronometrist-create-file ((backend chronometrist-sexp))
11. Method - chronometrist-backend-new-record (((backend chronometrist-sexp)) plist)
12. Function - chronometrist-sexp-delete-list (&optional arg)
13. Method - chronometrist-replace-last (((backend chronometrist-sexp)) plist)
14. Method - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
* String &optional ts -> seconds
15. Method - chronometrist-active-time-one-day (&optional ts)
* &optional ts -> seconds
16. Method - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
* String &optional table -> integer
17. Method - chronometrist-task-events-in-day (task ts)
* String ts -> events
18. Command - chronometrist-sexp-reindent-buffer ()
# Local Variables:
# org-link-file-path-type: relative

View File

@ -0,0 +1,73 @@
;;; chronometrist-backend.el --- backend-related definitions for Chronometrist
;;; Commentary:
;;
(require 'cl)
(require 'eieio)
(defvar chronometrist-file)
(declare-function chronometrist-file-path "chronometrist-common.el")
(defclass chronometrist-backend ()
((name
:initarg :name
:type string
:documentation "The name of the backend."
:initform "")
(ext
:initarg :ext
:type string
:documentation
"The extension used by a file of this backend, without a leading period."
:initform "")
(file :initarg :file)))
(cl-defmethod initialize-instance :after ((backend chronometrist-backend) &rest)
(setf (oref backend :file)
(format "%s.%s" chronometrist-file (oref backend :ext))))
(defvar chronometrist-backend-current nil "The current backend in use.")
(defvar chronometrist-backends nil "List of enabled backends.")
;; # Migration #
(cl-defgeneric chronometrist-backend-to-hash (backend hash-table)
"Clear HASH-TABLE and fill it using BACKEND.
Return final number of intervals read from backend, or nil if
there were none.")
;; If the file for BACKEND exists and is not empty, signal an error or
;; prompt the user?
(cl-defgeneric chronometrist-backend-from-hash (backend hash-table)
"Fill BACKEND using HASH-TABLE.")
;; # Queries #
(cl-defgeneric chronometrist-backend-open-file (backend)
"Open the storage file associated with BACKEND.")
(cl-defgeneric chronometrist-backend-latest-record (backend)
"Return the latest record from BACKEND.")
(cl-defgeneric chronometrist-backend-current-task (backend)
"Return the name of the currently clocked-in task, or nil if not clocked in.")
(cl-defgeneric chronometrist-backend-task-intervals (backend task &optional (ts (ts-now)))
"Return the time intervals for TASK on TS, or today.
TS must be a ts struct (see `ts.el').")
(cl-defgeneric chronometrist-backend-task-time (backend task &optional (ts (ts-now)))
"Return the time tracked for TASK on TS, or today.
TS must be a ts struct (see `ts.el').")
(cl-defgeneric chronometrist-backend-active-time (backend &optional (ts (ts-now)))
"Return the total time tracked on TS, or today.
TS must be a ts struct (see `ts.el').")
(cl-defgeneric chronometrist-backend-active-days (backend task)
"Return the number of days the user spent any time on TASK.")
;; # Modifications #
(cl-defgeneric chronometrist-backend-create-file (backend)
"Create BACKEND file if it does not already exist.")
(cl-defgeneric chronometrist-backend-new-record (backend plist)
"Use PLIST to add a new interval to BACKEND.")
(cl-defgeneric chronometrist-backend-replace-last (backend plist)
"Replace the latest record in BACKEND with PLIST.")
(provide 'chronometrist-backend)
;;; chronometrist-backend.el ends here

View File

@ -19,11 +19,14 @@
(require 'ts)
(require 'chronometrist-time)
(require 'chronometrist-sexp)
;; ## VARIABLES ##
;;; Code:
(defun chronometrist-file-path ()
"Return the full storage path for the current backend."
(concat chronometrist-file "." (oref chronometrist-backend-current :ext)))
(defvar chronometrist-empty-time-string "-")
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
@ -59,7 +62,7 @@ file.")
(defun chronometrist-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(chronometrist-sexp-current-task))
(chronometrist-current-task chronometrist-backend-current))
(cl-defun chronometrist-format-time (seconds &optional (blank " "))
"Format SECONDS as a string suitable for display in Chronometrist buffers.
@ -70,18 +73,12 @@ supplied, 3 spaces are used."
(-let [(h m s) (chronometrist-seconds-to-hms seconds)]
(if (and (zerop h) (zerop m) (zerop s))
" -"
(let ((h (if (zerop h)
blank
(format "%2d:" h)))
(m (cond ((and (zerop h)
(zerop m))
(let ((h (if (zerop h) blank (format "%2d:" h)))
(m (cond ((and (zerop h) (zerop m))
blank)
((zerop h)
(format "%2d:" m))
(t
(format "%02d:" m))))
(s (if (and (zerop h)
(zerop m))
((zerop h) (format "%2d:" m))
(t (format "%02d:" m))))
(s (if (and (zerop h) (zerop m))
(format "%2d" s)
(format "%02d" s))))
(concat h m s)))))
@ -115,13 +112,11 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
EVENTS must be a list of valid Chronometrist property lists (see
`chronometrist-file')."
(cl-loop for plist in events collect
(let* ((start (chronometrist-iso-timestamp->ts
(plist-get plist :start)))
(stop (plist-get plist :stop))
(stop (if stop
(chronometrist-iso-timestamp->ts stop)
(ts-now))))
(cons start stop))))
(cons (chronometrist-iso-timestamp->ts
(plist-get plist :start))
(aif (plist-get plist :stop)
(chronometrist-iso-timestamp->ts it)
(ts-now)))))
(defun chronometrist-ts-pairs->durations (ts-pairs)
"Return the durations represented by TS-PAIRS.

View File

@ -11,9 +11,7 @@
;;
;; For more information, please refer to <https://unlicense.org>
;; (require 'chronometrist-plist-pp)
(require 'chronometrist-common)
(require 'chronometrist-sexp)
(require 'ts)
;; external -
@ -27,7 +25,8 @@
;;; Code:
(defvar chronometrist-events (make-hash-table :test #'equal)
"Each key is a date in the form (YEAR MONTH DAY).
"Hash table containing time intervals.
Each key is a date in the form (YEAR MONTH DAY).
Values are lists containing events, where each event is a list in
the form (:name \"NAME\" :tags (TAGS) <key value pairs> ...
@ -125,7 +124,7 @@ The data is acquired from `chronometrist-file'.
Return final number of events read from file, or nil if there
were none."
(clrhash chronometrist-events)
(chronometrist-sexp-events-populate))
(chronometrist-to-hash chronometrist-backend-current chronometrist-events))
(defun chronometrist-tasks-from-table ()
"Return a list of task names from `chronometrist-events'."

View File

@ -1,19 +1,5 @@
;;; chronometrist-key-values.el --- add key-values to Chronometrist data -*- lexical-binding: t; -*-
(require 'cl-lib)
(require 'subr-x)
(require 'dash)
(require 'seq)
(require 'anaphora)
(require 'chronometrist-migrate)
(require 'chronometrist-events)
(require 'chronometrist-plist-pp)
(require 'chronometrist-common)
(declare-function chronometrist-refresh "chronometrist.el")
(declare-function chronometrist-last "chronometrist-queries.el")
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
@ -28,7 +14,17 @@
;;; Code:
(require 'chronometrist-sexp)
(require 'cl-lib)
(require 'subr-x)
(require 'dash)
(require 'seq)
(require 'anaphora)
(require 'chronometrist-migrate)
(require 'chronometrist-events)
(require 'chronometrist-plist-pp)
(declare-function chronometrist-refresh "chronometrist.el")
(defvar chronometrist--tag-suggestions nil
"Suggestions for tags.
@ -69,7 +65,7 @@ TAGS should be a list of symbols and/or strings.
PLIST should be a property list. Properties reserved by
Chronometrist - currently :name, :tags, :start, and :stop - will
be removed."
(let* ((old-expr (chronometrist-last))
(let* ((old-expr (chronometrist-backend-latest-record chronometrist-backend-current))
(old-name (plist-get old-expr :name))
(old-start (plist-get old-expr :start))
(old-stop (plist-get old-expr :stop))
@ -96,7 +92,7 @@ be removed."
new-kvs
`(:start ,old-start)
(when old-stop `(:stop ,old-stop)))))
(chronometrist-sexp-replace-last plist)))
(chronometrist-backend-replace-last chronometrist-backend-current plist)))
;;;; TAGS ;;;;
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
@ -206,7 +202,7 @@ INITIAL-INPUT is as used in `completing-read'."
_ARGS are ignored. This function always returns t, so it can be
used in `chronometrist-before-out-functions'."
(unless chronometrist--skip-detail-prompts
(let* ((last-expr (chronometrist-last))
(let* ((last-expr (chronometrist-backend-latest-record chronometrist-backend-current))
(last-name (plist-get last-expr :name))
(last-tags (plist-get last-expr :tags))
(input (->> last-tags
@ -353,7 +349,7 @@ It currently supports ido, ido-ubiquitous, ivy, and helm."
"Prompt the user to enter keys.
USED-KEYS are keys they have already added since the invocation
of `chronometrist-kv-add'."
(let ((key-suggestions (--> (chronometrist-last)
(let ((key-suggestions (--> (chronometrist-backend-latest-record chronometrist-backend-current)
(plist-get it :name)
(gethash it chronometrist-key-history))))
(completing-read (format "Key (%s to quit): " (chronometrist-kv-completion-quit-key))
@ -397,7 +393,8 @@ used in `chronometrist-before-out-functions'."
(unless chronometrist--skip-detail-prompts
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
(first-key-p t)
(last-kvs (chronometrist-plist-remove (chronometrist-last) :name :tags :start :stop))
(last-kvs (chronometrist-plist-remove (chronometrist-backend-latest-record chronometrist-backend-current)
:name :tags :start :stop))
(used-keys (->> (seq-filter #'keywordp last-kvs)
(mapcar #'symbol-name)
(--map (s-chop-prefix ":" it)))))
@ -405,7 +402,7 @@ used in `chronometrist-before-out-functions'."
(with-current-buffer buffer
(chronometrist-common-clear-buffer buffer)
(chronometrist-kv-read-mode)
(if (and (chronometrist-current-task) last-kvs)
(if (and (chronometrist-backend-current-task chronometrist-backend-current) last-kvs)
(progn
(funcall chronometrist-sexp-pretty-print-function last-kvs buffer)
(down-list -1)

View File

@ -1,4 +1,4 @@
;;; chronometrist-migrate.el --- Commands to aid in migrating from timeclock to chronometrist s-expr format -*- lexical-binding: t; -*-
;;; chronometrist-migrate.el --- Commands to aid in migrating between event formats -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
@ -23,13 +23,12 @@
(require 'chronometrist-plist-pp)
(defvar chronometrist-file)
(defvar chronometrist-migrate-table (make-hash-table))
(defvar chronometrist-migrate-table (make-hash-table :test #'equal))
;; TODO - support other timeclock codes (currently only "i" and "o"
;; are supported.)
(defun chronometrist-migrate-populate (in-file)
(defun chronometrist-migrate-populate-timelog (in-file)
"Read data from IN-FILE to `chronometrist-migrate-table'.
IN-FILE should be a file in the format supported by timeclock.el.
See `timeclock-log-data' for a description."
(clrhash chronometrist-migrate-table)
@ -80,46 +79,66 @@ See `timeclock-log-data' for a description."
(defun chronometrist-migrate-timelog-file->sexp-file (&optional in-file out-file)
"Migrate your existing `timeclock-file' to the Chronometrist file format.
IN-FILE and OUT-FILE, if provided, are used as input and output
file names respectively."
(interactive `(,(if (featurep 'timeclock)
(read-file-name (concat "timeclock file (default: "
timeclock-file
"): ")
(read-file-name (format "timeclock file (default: %s): "
timeclock-file)
user-emacs-directory
timeclock-file t)
(read-file-name (concat "timeclock file: ")
user-emacs-directory
nil t))
,(read-file-name (concat "Output file (default: "
(locate-user-emacs-file "chronometrist.sexp")
"): ")
(read-file-name "timeclock file: " user-emacs-directory nil t))
,(read-file-name (format "Output file (default: %s): "
(locate-user-emacs-file "chronometrist.sexp"))
user-emacs-directory
(locate-user-emacs-file "chronometrist.sexp"))))
(when (if (file-exists-p out-file)
(yes-or-no-p (concat "Output file "
out-file
" already exists - overwrite? "))
(yes-or-no-p (format "Output file %s already exists - overwrite? " out-file))
t)
(let ((output (find-file-noselect out-file)))
(with-current-buffer output
(chronometrist-common-clear-buffer output)
(chronometrist-migrate-populate in-file)
(chronometrist-migrate-populate-timelog in-file)
(maphash (lambda (_key value)
(chronometrist-plist-pp value output)
(insert "\n\n"))
chronometrist-migrate-table)
(save-buffer)))))
(defun chronometrist-migrate-populate-sexp (in-file)
"Read data from IN-FILE to `chronometrist-migrate-table'.
IN-FILE should be a file in the Chronometrist s-expression format.
See `chronometrist-file' for a description."
(clrhash chronometrist-migrate-table)
(with-current-buffer (find-file-noselect in-file)
(save-excursion
(goto-char (point-min))
(let ((index 0)
expr)
(while (setq expr (ignore-errors (read (current-buffer))))
(let* ((new-date (s-left 10 (plist-get expr :start)))
(existing-value (gethash new-date chronometrist-migrate-table)))
(cl-incf index)
(puthash new-date
(if existing-value
(append existing-value
(list expr))
(list expr))
chronometrist-migrate-table)))
(unless (zerop index) index)))))
(defun chronometrist-migrate (input-format output-format &optional input-file output-file)
"Migrate a Chronometrist file from INPUT-FORMAT to OUTPUT-FORMAT.
Prompt for INPUT-FILE and OUTPUT-FILE if not provided.")
(defun chronometrist-migrate-check ()
"Offer to import data from `timeclock-file' if `chronometrist-file' does not exist."
(when (and (bound-and-true-p timeclock-file)
(not (file-exists-p chronometrist-file)))
(not (file-exists-p (chronometrist-file-path))))
(if (yes-or-no-p (format (concat "Chronometrist v0.3+ uses a new file format;"
" import data from %s ? ")
timeclock-file))
(chronometrist-migrate-timelog-file->sexp-file timeclock-file chronometrist-file)
(chronometrist-migrate-timelog-file->sexp-file timeclock-file (chronometrist-file-path))
(message "You can migrate later using `chronometrist-migrate-timelog-file->sexp-file'."))))
(provide 'chronometrist-migrate)

View File

@ -1,86 +0,0 @@
;;; chronometrist-queries.el --- Functions which query Chronometrist data -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;;
;;; Code:
(require 'dash)
(require 'chronometrist-common)
(require 'chronometrist-events)
(defun chronometrist-last ()
"Return the last entry from `chronometrist-file' as a plist."
(chronometrist-sexp-last))
(cl-defun chronometrist-task-time-one-day (task &optional (ts (ts-now)))
"Return total time spent on TASK today or (if supplied) on timestamp TS.
The data is obtained from `chronometrist-file', via `chronometrist-events'.
TS should be a ts struct (see `ts.el').
The return value is seconds, as an integer."
(let ((task-events (chronometrist-task-events-in-day task ts)))
(if task-events
(->> (chronometrist-events->ts-pairs task-events)
(chronometrist-ts-pairs->durations)
(-reduce #'+)
(truncate))
;; no events for this task on TS, i.e. no time spent
0)))
(defun chronometrist-active-time-one-day (&optional ts)
"Return the total active time on TS (if non-nil) or today.
TS must be a ts struct (see `ts.el')
Return value is seconds as an integer."
(->> chronometrist-task-list
(--map (chronometrist-task-time-one-day it ts))
(-reduce #'+)
(truncate)))
(cl-defun chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
"Return the number of days the user spent any time on TASK.
TABLE must be a hash table - if not supplied, `chronometrist-events' is used.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(let ((count 0))
(maphash (lambda (_date events)
(when (seq-find (lambda (event)
(equal (plist-get event :name) task))
events)
(cl-incf count)))
table)
count))
(defun chronometrist-task-events-in-day (task ts)
"Get events for TASK on TS.
TS should be a ts struct (see `ts.el').
Returns a list of events, where each event is a property list in
the form (:name \"NAME\" :start START :stop STOP ...), where
START and STOP are ISO-8601 time strings.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(->> (gethash (ts-format "%F" ts) chronometrist-events)
(mapcar (lambda (event)
(when (equal task (plist-get event :name))
event)))
(seq-filter #'identity)))
(provide 'chronometrist-queries)
;;; chronometrist-queries.el ends here

View File

@ -17,7 +17,6 @@
(require 'filenotify)
(require 'subr-x)
(require 'chronometrist-common)
(require 'chronometrist-queries)
(require 'chronometrist-timer)
(require 'chronometrist-migrate)
@ -101,18 +100,18 @@ The first date is the first occurrence of
(let* ((week-dates (chronometrist-report-date->week-dates))) ;; uses today if chronometrist-report--ui-date is nil
(setq chronometrist-report--ui-week-dates week-dates)
(cl-loop for task in chronometrist-task-list collect
(let* ((durations (--map (chronometrist-task-time-one-day task (chronometrist-date it))
week-dates))
(duration-strings (mapcar #'chronometrist-format-time
durations))
(total-duration (->> (-reduce #'+ durations)
(chronometrist-format-time)
(vector))))
(list task
(vconcat
(vector task)
duration-strings ;; vconcat converts lists to vectors
total-duration))))))
(let* ((durations (--map (chronometrist-backend-task-time chronometrist-backend-current task (chronometrist-date it))
week-dates))
(duration-strings (mapcar #'chronometrist-format-time
durations))
(total-duration (->> (-reduce #'+ durations)
(chronometrist-format-time)
(vector))))
(list task
(vconcat
(vector task)
duration-strings ;; vconcat converts lists to vectors
total-duration))))))
(defun chronometrist-report-print-keybind (command &optional description firstonly)
"Insert one or more keybindings for COMMAND into the current buffer.
@ -129,11 +128,10 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
"Print the non-tabular part of the buffer in `chronometrist-report'."
(let ((inhibit-read-only t)
(w "\n ")
(total-time-daily (->> chronometrist-report--ui-week-dates
(mapcar #'chronometrist-date)
(mapcar #'chronometrist-active-time-one-day))))
(total-time-daily (->> (mapcar #'chronometrist-date chronometrist-report--ui-week-dates)
(--map (chronometrist-backend-active-time chronometrist-backend-current it)))))
(goto-char (point-min))
(insert " ")
(insert (make-string 25 ?\ ))
(insert (mapconcat (lambda (ts)
(ts-format "%F" ts))
(chronometrist-report-date->week-dates)
@ -176,7 +174,7 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
(defun chronometrist-report-refresh-file (_fs-event)
"Re-read `chronometrist-file' and refresh the `chronometrist-report' buffer.
Argument _FS-EVENT is ignored."
(chronometrist-events-populate)
(chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
;; (chronometrist-events-clean)
(chronometrist-report-refresh))
@ -217,10 +215,14 @@ Argument _FS-EVENT is ignored."
(setq tabulated-list-sort-key '("Task" . nil))
(tabulated-list-init-header)
(chronometrist-maybe-start-timer)
(add-hook 'chronometrist-timer-hook
(lambda ()
(when (get-buffer-window chronometrist-report-buffer-name)
(chronometrist-report-refresh))))
(setq revert-buffer-function #'chronometrist-report-refresh)
(unless chronometrist--fs-watch
(setq chronometrist--fs-watch
(file-notify-add-watch chronometrist-file
(file-notify-add-watch (chronometrist-file-path)
'(change)
#'chronometrist-refresh-file))))
@ -249,7 +251,7 @@ current week. Otherwise, display data from the week specified by
(kill-buffer buffer))
(t (unless keep-date
(setq chronometrist-report--ui-date nil))
(chronometrist-common-create-file)
(chronometrist-backend-create-file chronometrist-backend-current)
(chronometrist-report-mode)
(switch-to-buffer buffer)
(chronometrist-report-refresh-file nil)

View File

@ -8,6 +8,11 @@
;; chronometrist-file (-custom)
;; chronometrist-events, chronometrist-events-maybe-split (-events)
(require 'chronometrist)
(require 'chronometrist-backend)
(defclass chronometrist-sexp (chronometrist-backend) nil)
(defvar chronometrist-sexp-backend (make-instance chronometrist-sexp :name "sexp" :ext "sexp"))
(defcustom chronometrist-sexp-pretty-print-function #'chronometrist-plist-pp
"Function used to pretty print plists in `chronometrist-file'.
Like `pp', it must accept an OBJECT and optionally a
@ -20,33 +25,10 @@ STREAM (which is the value of `current-buffer')."
`(with-current-buffer (find-file-noselect ,file)
(save-excursion ,@body)))
;;;; Queries
(defun chronometrist-sexp-open-log ()
"Open `chronometrist-file' in another window."
(find-file-other-window chronometrist-file)
(goto-char (point-max)))
(defun chronometrist-sexp-last ()
"Return last s-expression from `chronometrist-file'."
(chronometrist-sexp-in-file chronometrist-file
(goto-char (point-max))
(backward-list)
(ignore-errors (read (current-buffer)))))
(defun chronometrist-sexp-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(let ((last-event (chronometrist-sexp-last)))
(if (plist-member last-event :stop)
nil
(plist-get last-event :name))))
(defun chronometrist-sexp-events-populate ()
"Populate hash table `chronometrist-events'.
The data is acquired from `chronometrist-file'.
Return final number of events read from file, or nil if there
were none."
(chronometrist-sexp-in-file chronometrist-file
;; # Migration #
(cl-defmethod chronometrist-backend-to-hash ((backend chronometrist-sexp) table)
(clrhash table)
(chronometrist-sexp-in-file (chronometrist-file-path)
(goto-char (point-min))
(let ((index 0) expr pending-expr)
(while (or pending-expr
@ -60,8 +42,7 @@ were none."
(setq pending-expr (cl-second split-expr))
(cl-first split-expr))
(t expr)))
(new-value-date (->> (plist-get new-value :start)
(s-left 10)))
(new-value-date (s-left 10 (plist-get new-value :start)))
(existing-value (gethash new-value-date chronometrist-events)))
(unless pending-expr (cl-incf index))
(puthash new-value-date
@ -72,16 +53,32 @@ were none."
chronometrist-events)))
(unless (zerop index) index))))
;;;; Modifications
(defun chronometrist-sexp-create-file ()
"Create `chronometrist-file' if it doesn't already exist."
(unless (file-exists-p chronometrist-file)
(with-current-buffer (find-file-noselect chronometrist-file)
(write-file chronometrist-file))))
(cl-defmethod chronometrist-backend-from-hash ((backend chronometrist-sexp) table))
(cl-defun chronometrist-sexp-new (plist)
"Add new PLIST at the end of `chronometrist-file'."
(chronometrist-sexp-in-file chronometrist-file
;; # Queries #
(cl-defmethod chronometrist-backend-open-file ((backend chronometrist-sexp))
(find-file-other-window (chronometrist-file-path))
(goto-char (point-max)))
(cl-defmethod chronometrist-backend-latest-record ((backend chronometrist-sexp))
(chronometrist-sexp-in-file (chronometrist-file-path)
(goto-char (point-max))
(backward-list)
(ignore-errors (read (current-buffer)))))
(cl-defmethod chronometrist-backend-current-task ((backend chronometrist-sexp))
(let ((last-event (chronometrist-backend-latest-record backend)))
(unless (plist-member last-event :stop)
(plist-get last-event :name))))
;; # Modifications #
(cl-defmethod chronometrist-backend-create-file ((backend chronometrist-sexp))
(unless (file-exists-p (chronometrist-file-path))
(with-current-buffer (find-file-noselect (chronometrist-file-path))
(write-file (chronometrist-file-path)))))
(cl-defmethod chronometrist-backend-new-record ((backend chronometrist-sexp) plist)
(chronometrist-sexp-in-file (chronometrist-file-path)
(goto-char (point-max))
;; If we're adding the first s-exp in the file, don't add a
;; newline before it
@ -102,9 +99,8 @@ were none."
(forward-sexp (or arg 1))
(delete-region point-1 (point))))
(defun chronometrist-sexp-replace-last (plist)
"Replace the last s-expression in `chronometrist-file' with PLIST."
(chronometrist-sexp-in-file chronometrist-file
(cl-defmethod chronometrist-backend-replace-last ((backend chronometrist-sexp) plist)
(chronometrist-sexp-in-file (chronometrist-file-path)
(goto-char (point-max))
(unless (and (bobp) (bolp))
(insert "\n"))
@ -119,11 +115,68 @@ were none."
;; :name should be removed from `chronometrist-task-list', but to ascertain
;; that condition we would have to either read the entire file or
;; map over the hash table, defeating the optimization. Thus, we
;; don't update `chronometrist-task-list' here (unlike `chronometrist-sexp-new')
;; don't update `chronometrist-task-list' here (unlike `chronometrist-backend-new-record')
(chronometrist-tags-history-replace-last plist)
(setq chronometrist--inhibit-read-p t)
(save-buffer)))
(cl-defmethod chronometrist-backend-task-intervals ((backend chronometrist-sexp) task &optional (ts (ts-now)))
"Get intervals for TASK on TS.
TS should be a ts struct (see `ts.el').
Returns a list of events, where each event is a property list in
the form (:name \"NAME\" :start START :stop STOP ...), where
START and STOP are ISO-8601 time strings.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(->> (gethash (ts-format "%F" ts) chronometrist-events)
(mapcar (lambda (event)
(when (equal task (plist-get event :name))
event)))
(seq-filter #'identity)))
(cl-defmethod chronometrist-backend-task-time ((backend chronometrist-sexp) task &optional (ts (ts-now)))
"Return total time spent on TASK today or (if supplied) on timestamp TS.
The data is obtained from `chronometrist-file', via `chronometrist-events'.
TS should be a ts struct (see `ts.el').
The return value is seconds, as an integer."
(let ((task-events (chronometrist-backend-task-intervals chronometrist-backend-current task ts)))
(if task-events
(->> (chronometrist-events->ts-pairs task-events)
(chronometrist-ts-pairs->durations)
(-reduce #'+)
(truncate))
;; no events for this task on TS, i.e. no time spent
0)))
(cl-defmethod chronometrist-backend-active-time ((backend chronometrist-sexp) &optional ts)
"Return the total active time on TS (if non-nil) or today.
TS must be a ts struct (see `ts.el')
Return value is seconds as an integer."
(->> chronometrist-task-list
(--map (chronometrist-backend-task-time chronometrist-backend-current it ts))
(-reduce #'+)
(truncate)))
(cl-defmethod chronometrist-backend-active-days ((backend chronometrist-sexp) task &optional (table chronometrist-events))
"Return the number of days the user spent any time on TASK.
TABLE must be a hash table - if not supplied, `chronometrist-events' is used.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(let ((count 0))
(maphash (lambda (_date events)
(when (seq-find (lambda (event)
(equal (plist-get event :name) task))
events)
(cl-incf count)))
table)
count))
(defun chronometrist-sexp-reindent-buffer ()
"Reindent the current buffer.
This is meant to be run in `chronometrist-file' when using the s-expression backend."
@ -134,8 +187,7 @@ This is meant to be run in `chronometrist-file' when using the s-expression back
(backward-list)
(chronometrist-sexp-delete-list)
(when (looking-at "\n*")
(delete-region (match-beginning 0)
(match-end 0)))
(delete-region (match-beginning 0) (match-end 0)))
(funcall chronometrist-sexp-pretty-print-function expr (current-buffer))
(insert "\n")
(unless (eobp)

View File

@ -0,0 +1,93 @@
;;; chronometrist-sqlite3.el --- sqlite3 backend for Chronometrist
;;; Commentary:
;;
(require 'anaphora)
(require 'emacsql-sqlite3)
(require 'chronometrist-backend)
(defclass chronometrist-sqlite3 (chronometrist-backend) nil)
(defvar chronometrist-sqlite3-backend (make-instance chronometrist-sqlite3 :name "sqlite3" :ext "sqlite3"))
(defvar chronometrist-sqlite3-db (chronometrist-backend-create-file chronometrist-sqlite3-backend))
;; # Migration #
(cl-defmethod chronometrist-backend-to-hash ((backend chronometrist-sqlite3) table))
(defun chronometrist-sqlite3-insert-plist (plist db)
"Insert PLIST into DB."
(let* ((keywords (seq-filter #'keywordp event))
(values (seq-remove #'keywordp event))
(columns (mapcar (lambda (keyword)
(--> (symbol-name keyword)
(s-chop-prefix ":" it)
;; emacsql seems to automatically
;; convert dashes in column names
;; to underscores, so we do the
;; same, lest we get a "column
;; already exists" error
(replace-regexp-in-string "-" "_" it)
(intern it)))
keywords)))
;; ensure all keywords in this plist exist as SQL columns
(cl-loop for column in columns do
(let* ((pragma (emacsql db [:pragma (funcall table_info events)]))
(column-exists (cl-loop for column-spec in pragma thereis
(eq column (second column-spec)))))
(unless column-exists
(emacsql db [:alter-table events :add-column $i1] column))))
(emacsql db [:insert-into events [$i1] :values $v2]
(vconcat columns) (vconcat values))))
(cl-defmethod chronometrist-backend-from-hash ((backend chronometrist-sqlite3) hash-table file)
"Save HASH-TABLE as a FILE of BACKEND.
FILE should be a file path without an extension."
(cl-loop with db = (emacsql-sqlite3 (concat file "." (oref backend :ext)))
with count = 0
for events being the hash-values of hash-table do
(cl-loop for event in events do
(chronometrist-sqlite3-insert-plist event db)
(incf count)
(when (zerop (% count 5))
(message "chronometrist-migrate - %s events converted" count)))
finally return count do
(message "chronometrist-migrate - finished converting %s events." count)))
;; # Queries #
(cl-defmethod chronometrist-backend-open-file ((backend chronometrist-sqlite3) file)
(require 'sql)
(switch-to-buffer
(sql-comint-sqlite 'sqlite (list file))))
;; SELECT * FROM TABLE WHERE ID = (SELECT MAX(ID) FROM TABLE);
;; SELECT * FROM tablename ORDER BY column DESC LIMIT 1;
(cl-defmethod chronometrist-backend-latest-record ((backend chronometrist-sqlite3) db)
(emacsql db [:select * :from events :order-by rowid :desc :limit 1]))
(cl-defmethod chronometrist-backend-current-task ((backend chronometrist-sqlite3)))
(cl-defmethod chronometrist-backend-intervals ((backend chronometrist-sqlite3) task &optional (ts (ts-now)))
"Return the time intervals for TASK on TS.")
(cl-defmethod chronometrist-backend-task-time ((backend chronometrist-sqlite3) task &optional (ts (ts-now))))
(cl-defmethod chronometrist-backend-active-time ((backend chronometrist-sqlite3) &optional ts))
(cl-defmethod chronometrist-backend-active-days ((backend chronometrist-sqlite3) task))
;; # Modifications #
(cl-defmethod chronometrist-backend-create-file ((backend chronometrist-sqlite3))
"Create file for BACKEND if it does not already exist.
Return the emacsql-sqlite3 connection object."
(aprog1 (emacsql-sqlite3 (concat chronometrist-file "." (oref backend :ext)))
(emacsql it [:create-table events ([name tags start stop])])))
(cl-defmethod chronometrist-backend-new-record ((backend chronometrist-sqlite3) plist)
(chronometrist-sqlite3-insert-plist plist file))
(cl-defmethod chronometrist-backend-replace-last ((backend chronometrist-sqlite3) plist)
(emacsql db [:delete-from events :where ]))
(provide 'chronometrist-sqlite3)
;;; chronometrist-sqlite3.el ends here

View File

@ -20,10 +20,8 @@
(require 'chronometrist-common)
(require 'chronometrist-time)
(require 'chronometrist-timer)
(require 'chronometrist-events)
(require 'chronometrist-migrate)
(require 'chronometrist-queries)
(declare-function chronometrist-refresh-file "chronometrist.el")
@ -97,12 +95,12 @@ TABLE should be a hash table - if not supplied,
`chronometrist-events' is used."
;; (cl-loop
;; for date being the hash-keys of table
;; (let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
;; (let ((events-in-day (chronometrist-backend-task-intervals task (chronometrist-iso-date->ts key))))
;; (when events-in-day)))
(let ((days 0)
(per-day-time-list))
(maphash (lambda (key _value)
(let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
(let ((events-in-day (chronometrist-backend-task-intervals chronometrist-backend-current task (chronometrist-iso-date->ts key))))
(when events-in-day
(setq days (1+ days))
(->> (chronometrist-events->ts-pairs events-in-day)
@ -125,7 +123,7 @@ It simply operates on the entire hash table TABLE (see
reduced to the desired range using
`chronometrist-events-subset'."
(mapcar (lambda (task)
(let* ((active-days (chronometrist-statistics-count-active-days task table))
(let* ((active-days (chronometrist-count-active-days task table))
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week (* 100 (/ active-days 7.0)))))
(active-percent (if (zerop active-days)
@ -232,10 +230,14 @@ value of `revert-buffer-function'."
(setq tabulated-list-sort-key '("Task" . nil))
(tabulated-list-init-header)
;; (chronometrist-maybe-start-timer)
(add-hook 'chronometrist-timer-hook
(lambda ()
(when (get-buffer-window chronometrist-statistics-buffer-name)
(chronometrist-statistics-refresh))))
(setq revert-buffer-function #'chronometrist-statistics-refresh)
(unless chronometrist--fs-watch
(setq chronometrist--fs-watch
(file-notify-add-watch chronometrist-file
(file-notify-add-watch (chronometrist-file-path)
'(change)
#'chronometrist-refresh-file))))
@ -267,7 +269,7 @@ specified by `chronometrist-statistics--ui-state'."
(setq chronometrist-statistics--ui-state `(:mode week
:start ,week-start
:end ,week-end)))
(chronometrist-common-create-file)
(chronometrist-backend-create-file chronometrist-backend-current)
(chronometrist-statistics-mode)
(switch-to-buffer buffer)
(chronometrist-statistics-refresh))))))

View File

@ -2,12 +2,6 @@
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
(require 'parse-time)
(require 'dash)
(require 's)
(declare-function chronometrist-day-start "chronometrist-events.el")
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
@ -23,6 +17,11 @@
;; which ones those are.
;;; Code:
(require 'parse-time)
(require 'dash)
(require 's)
(declare-function chronometrist-day-start "chronometrist-events.el")
(defun chronometrist-iso-timestamp->ts (timestamp)
"Return new ts struct, parsing TIMESTAMP with `parse-iso8601-time-string'."

View File

@ -15,29 +15,26 @@
;;
;;; Code:
(require 'chronometrist)
(declare-function chronometrist-refresh "chronometrist.el")
(declare-function chronometrist-report-refresh "chronometrist-report.el")
(declare-function chronometrist-statistics-refresh "chronometrist-statistics.el")
(defvar chronometrist--timer-object nil)
(defcustom chronometrist-timer-hook nil
"Functions run by `chronometrist-timer'.")
(defun chronometrist-timer ()
"Refresh Chronometrist and related buffers.
Buffers will be refreshed only if they are visible and the user
is clocked in to a task."
(when (chronometrist-current-task) ;; FIXME - This line is currently
(when (chronometrist-backend-current-task chronometrist-backend-current) ;; FIXME - This line is currently
;; resulting in no refresh at midnight. When `chronometrist-entries' is
;; optimized to consume less CPU and avoid unnecessary parsing,
;; remove this condition.
(when (get-buffer-window chronometrist-buffer-name)
(chronometrist-refresh))
(when (get-buffer-window chronometrist-report-buffer-name)
(chronometrist-report-refresh))
(when (get-buffer-window chronometrist-statistics-buffer-name)
(chronometrist-statistics-refresh))))
(run-hooks 'chronometrist-timer-hook)))
(defun chronometrist-stop-timer ()
"Stop the timer for Chronometrist buffers."

View File

@ -20,9 +20,8 @@
(require 'chronometrist-common)
(require 'chronometrist-key-values)
(require 'chronometrist-queries)
(require 'chronometrist-migrate)
(require 'chronometrist-sexp)
(require 'chronometrist-backend)
;; This is free and unencumbered software released into the public domain.
;;
@ -84,28 +83,8 @@
:group 'applications)
(defcustom chronometrist-file
(locate-user-emacs-file "chronometrist.sexp")
"Default path and name of the Chronometrist database.
It should be a text file containing plists in the form -
\(:name \"task name\"
[:tags TAGS]
[:comment \"comment\"]
[KEY-VALUE-PAIR ...]
:start \"TIME\"
:stop \"TIME\"\)
Where -
TAGS is a list. It can contain any strings and symbols.
KEY-VALUE-PAIR can be any keyword-value pairs. Currently,
Chronometrist ignores them.
TIME must be an ISO-8601 time string.
\(The square brackets here refer to optional elements, not
vectors.\)"
(locate-user-emacs-file "chronometrist")
"Default path and name (without extension) of the Chronometrist database."
:type 'file)
(defcustom chronometrist-buffer-name "*Chronometrist*"
@ -145,15 +124,11 @@ The default is midnight, i.e. \"00:00:00\"."
Argument _BUTTON is for the purpose of using this command as a
button action."
(interactive)
(chronometrist-sexp-open-log))
(defun chronometrist-common-create-file ()
"Create `chronometrist-file' if it doesn't already exist."
(chronometrist-sexp-create-file))
(chronometrist-backend-open-file chronometrist-backend-current))
(defun chronometrist-task-active? (task)
"Return t if TASK is currently clocked in, else nil."
(equal (chronometrist-current-task) task))
(equal (chronometrist-backend-current-task chronometrist-backend-current) task))
(defun chronometrist-activity-indicator ()
"Return a string to indicate that a task is active.
@ -167,14 +142,14 @@ See custom variable `chronometrist-activity-indicator'."
;; HACK - these calls are commented out, because `chronometrist-entries' is
;; called by both `chronometrist-refresh' and `chronometrist-refresh-file', and only the
;; latter should refresh from a file.
;; (chronometrist-events-populate)
;; (chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
;; (chronometrist-events-clean)
(->> (-sort #'string-lessp chronometrist-task-list)
(--map-indexed
(let* ((task it)
(index (number-to-string (1+ it-index)))
(task-button `(,task action chronometrist-toggle-task-button follow-link t))
(task-time (chronometrist-format-time (chronometrist-task-time-one-day task)))
(task-time (chronometrist-format-time (chronometrist-backend-task-time chronometrist-backend-current task)))
(indicator (if (chronometrist-task-active? task) (chronometrist-activity-indicator) "")))
(--> (vector index task-button task-time indicator)
(list task it)
@ -190,7 +165,7 @@ See custom variable `chronometrist-activity-indicator'."
(defun chronometrist-goto-last-task ()
"In the `chronometrist' buffer, move point to the line containing the last active task."
(goto-char (point-min))
(re-search-forward (plist-get (chronometrist-last) :name) nil t)
(re-search-forward (plist-get (chronometrist-backend-latest-record chronometrist-backend-current) :name) nil t)
(beginning-of-line))
(defun chronometrist-print-keybind (command &optional description firstonly)
@ -210,7 +185,7 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
;; (keybind-start-new (chronometrist-format-keybinds 'chronometrist-add-new-task chronometrist-mode-map))
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
(goto-char (point-max))
(--> (chronometrist-active-time-one-day)
(--> (chronometrist-backend-active-time chronometrist-backend-current)
(chronometrist-format-time it)
(format "%s%- 26s%s" w "Total" it)
(insert it))
@ -259,7 +234,7 @@ Argument _FS-EVENT is ignored."
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
(if chronometrist--inhibit-read-p
(setq chronometrist--inhibit-read-p nil)
(chronometrist-events-populate)
(chronometrist-backend-to-hash chronometrist-backend-current chronometrist-events)
(setq chronometrist-task-list (chronometrist-tasks-from-table))
(chronometrist-tags-history-populate chronometrist-events chronometrist-tags-history))
(chronometrist-key-history-populate chronometrist-events chronometrist-key-history)
@ -268,26 +243,25 @@ Argument _FS-EVENT is ignored."
(defun chronometrist-query-stop ()
"Ask the user if they would like to clock out."
(let ((task (chronometrist-current-task)))
(and task
(yes-or-no-p (format "Stop tracking time for %s? " task))
(chronometrist-out))
t))
(aand (chronometrist-backend-current-task chronometrist-backend-current)
(yes-or-no-p (format "Stop tracking time for %s? " it))
(chronometrist-out))
t)
(defun chronometrist-in (task &optional _prefix)
"Clock in to TASK; record current time in `chronometrist-file'.
TASK is the name of the task, a string. PREFIX is ignored."
(interactive "P")
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
(chronometrist-sexp-new plist)
(chronometrist-backend-new-record chronometrist-backend-current plist)
(chronometrist-refresh)))
(defun chronometrist-out (&optional _prefix)
"Record current moment as stop time to last s-exp in `chronometrist-file'.
PREFIX is ignored."
(interactive "P")
(let ((plist (plist-put (chronometrist-last) :stop (chronometrist-format-time-iso8601))))
(chronometrist-sexp-replace-last plist)))
(let ((plist (plist-put (chronometrist-backend-latest-record chronometrist-backend-current) :stop (chronometrist-format-time-iso8601))))
(chronometrist-backend-replace-last chronometrist-backend-current plist)))
;; ## HOOKS ##
(defvar chronometrist-mode-hook nil
@ -393,7 +367,7 @@ Argument _BUTTON is for the purpose of using this as a button
action, and is ignored."
(when current-prefix-arg
(chronometrist-goto-nth-task (prefix-numeric-value current-prefix-arg)))
(let ((current (chronometrist-current-task))
(let ((current (chronometrist-backend-current-task chronometrist-backend-current))
(at-point (chronometrist-task-at-point)))
;; clocked in + point on current = clock out
;; clocked in + point on some other task = clock out, clock in to task
@ -408,7 +382,7 @@ action, and is ignored."
Argument _BUTTON is for the purpose of using this as a button
action, and is ignored."
(let ((current (chronometrist-current-task)))
(let ((current (chronometrist-backend-current-task chronometrist-backend-current)))
(when current
(chronometrist-run-functions-and-clock-out current))
(let ((task (read-from-minibuffer "New task name: " nil nil nil nil nil t)))
@ -431,11 +405,11 @@ If INHIBIT-HOOKS is non-nil, the hooks
`chronometrist-before-out-functions', and
`chronometrist-after-out-functions' will not be run."
(interactive "P")
(let* ((empty-file (chronometrist-common-file-empty-p chronometrist-file))
(let* ((empty-file (chronometrist-common-file-empty-p (chronometrist-file-path)))
(nth (when prefix (chronometrist-goto-nth-task prefix)))
(at-point (chronometrist-task-at-point))
(target (or nth at-point))
(current (chronometrist-current-task))
(current (chronometrist-backend-current-task chronometrist-backend-current))
(in-function (if inhibit-hooks
#'chronometrist-in
#'chronometrist-run-functions-and-clock-in))
@ -491,10 +465,10 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
(setq chronometrist--point (point))
(kill-buffer chronometrist-buffer-name)))
(t (with-current-buffer buffer
(cond ((or (not (file-exists-p chronometrist-file))
(chronometrist-common-file-empty-p chronometrist-file))
(cond ((or (not (file-exists-p (chronometrist-file-path)))
(chronometrist-common-file-empty-p (chronometrist-file-path)))
;; first run
(chronometrist-common-create-file)
(chronometrist-backend-create-file chronometrist-backend-current)
(let ((inhibit-read-only t))
(chronometrist-common-clear-buffer buffer)
(insert "Welcome to Chronometrist! Hit RET to ")
@ -517,7 +491,7 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
(chronometrist-goto-last-task))))
(unless chronometrist--fs-watch
(setq chronometrist--fs-watch
(file-notify-add-watch chronometrist-file '(change) #'chronometrist-refresh-file))))))))
(file-notify-add-watch (chronometrist-file-path) '(change) #'chronometrist-refresh-file))))))))
(provide 'chronometrist)