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.

912 Commits

Author SHA1 Message Date
contrapunctus f7b2defcee Update CL port manual 2022-05-03 10:45:18 +05:30
contrapunctus be32efd73b Update custom IDs 2022-05-03 10:45:10 +05:30
contrapunctus 339d5da1e7 Display durations using format-seconds 2022-05-03 10:43:50 +05:30
contrapunctus db965525b0 Extend CLIM frontend documentation 2022-04-27 21:12:27 +05:30
contrapunctus e2d32232df CLIM frontend: Write some documentation 2022-04-27 16:43:22 +05:30
contrapunctus 4f2d986fe4 Add CUSTOM_IDs 2022-04-27 16:26:50 +05:30
contrapunctus e641139497 TODO: Add commands, rephrase effect of setting 2022-04-25 19:29:10 +05:30
contrapunctus c5d5ba419e CLIM: Specify table columns using classes 2022-04-23 08:19:56 +05:30
contrapunctus 5d93dd48bc CLIM: Display durations 2022-04-22 22:30:13 +05:30
contrapunctus e57f88a935 Make task-time-one-day work with interval objects 2022-04-21 11:03:06 +05:30
contrapunctus 3f8253daa4 Define default method for task-records-for-date 2022-04-21 09:52:53 +05:30
contrapunctus ecaf35f842 Correct type declarations 2022-04-21 09:38:27 +05:30
contrapunctus 900dbd9742 Have task-records-for-date return intervals 2022-04-21 09:37:22 +05:30
contrapunctus 07498b39c5 (WIP) Implement task-records-for-date (SQLite) 2022-04-20 12:08:09 +05:30
contrapunctus 44c29317ec Add task for concurrent intervals 2022-04-20 10:38:51 +05:30
contrapunctus c74d83908c Finish implementing get-day (SQLite) 2022-04-19 09:48:11 +05:30
contrapunctus 07609d4813 Rename slot activity -> name 2022-04-18 23:15:16 +05:30
contrapunctus cf50c5beaf Refactor get-day (SQLite) 2022-04-18 22:45:08 +05:30
contrapunctus 0021f92e29 Create interval objects from query 2022-04-18 20:09:13 +05:30
contrapunctus c9ab8b4f95 get-day (SQLite): write query for intervals 2022-04-18 19:19:25 +05:30
contrapunctus 94e7a78404 Rename `get` to `get-day`, implement SQLite method (WIP) 2022-04-18 03:54:51 +05:30
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
contrapunctus 88aa54c082 Restore subtitle 2022-04-14 14:13:33 +05:30
contrapunctus f25c586232 Explain Common Lisp port 2022-04-14 14:08:35 +05:30
contrapunctus 30bde518a0 Update license explanation 2022-04-14 13:25:15 +05:30
contrapunctus ea704f8c00 Reorganize documentation, mention CLIM frontend 2022-04-14 13:15:19 +05:30
contrapunctus 8dbba77d12 Merge branch 'dev' into doc 2022-04-14 12:56:16 +05:30
contrapunctus 3a68a65156 TODO: add command, configuration 2022-04-14 12:40:26 +05:30
contrapunctus 1465f5da33 Fix errors about unknown types 2022-04-14 12:34:28 +05:30
contrapunctus 38e4997269 Add chronometrist: to method names 2022-04-13 10:48:24 +05:30
contrapunctus 0831ae82d5 Display tasks as presentation types 2022-04-11 22:42:47 +05:30
contrapunctus b9417d6a25 Rename views, list more commands 2022-04-11 17:54:01 +05:30
contrapunctus f3f96a8066 doc(TODO): Note more commands 2022-04-11 12:54:26 +05:30
contrapunctus b9ba7fe9b2 Make background black and foreground white 2022-04-10 23:29:20 +05:30
contrapunctus 90b68d6671 List commands available in views 2022-04-10 21:47:00 +05:30
contrapunctus 80f08aabdf Combine tabular views into more general graph view 2022-04-10 18:54:26 +05:30
contrapunctus 1a7ebecb2e Add missing views; rethink default view 2022-04-10 08:30:12 +05:30
contrapunctus 8d65dd8a13 Document the CL port 2022-04-08 21:34:10 +05:30
contrapunctus b70b5eac5c Make CLIM UI design notes 2022-04-08 20:05:25 +05:30
contrapunctus 0f0dab74bc Display index column 2022-04-08 11:41:50 +05:30
contrapunctus 162f005507 Do not :use the chronometrist package 2022-04-08 09:13:02 +05:30
contrapunctus 71d116480d CLIM UI - display task list 2022-04-08 09:13:02 +05:30
contrapunctus 53f650678b Implement list-tasks differently for SQLite backend 2022-04-07 18:01:34 +05:30
contrapunctus 9df00858f4 Use SxQL for table creation 2022-04-07 16:52:59 +05:30
contrapunctus 33ac4243f4 Replace clsql with cl-sqlite 2022-04-06 07:24:26 +05:30
contrapunctus e21c85e543 Try to fix empty prop_id 2022-04-05 10:33:05 +05:30
contrapunctus aec8f82c60 Fix most remaining errors in import-export 2022-04-05 07:48:00 +05:30
contrapunctus 98f8bcd7b8 Attempt to fix quoting of plists 2022-04-04 21:33:02 +05:30
contrapunctus 88c6e11b9f Update plist-remove and emacsql calls, fix let-match patterns 2022-04-04 20:03:08 +05:30
contrapunctus 031f3b9640 Convert emacsql to CLSQL, reinstate some helpers 2022-04-04 07:14:43 +05:30
contrapunctus 63f60ccde5 Address still more errors 2022-04-04 06:50:50 +05:30
contrapunctus a04447f745 Replace iso-to-ts -> parse-timestring, ts-adjust -> adjust-timestamp
And split-string with uiop:split-string
2022-04-04 01:10:35 +05:30
contrapunctus e4ff298ba4 Update some call sites for timestamp, plist, hash table, and database APIs 2022-04-03 23:00:07 +05:30
contrapunctus a0107ccb91 Correct class option syntax 2022-04-03 20:17:10 +05:30
contrapunctus 5a64f7ce93 Fix make-hash-table-1 error, backend-file bug
backend-file uncovered an issue with redefining slots - we now use
:default-initargs instead.
2022-04-03 13:52:57 +05:30
contrapunctus 5d0910e4a0 Remove `path` slot 2022-04-03 10:20:40 +05:30
contrapunctus 11ba9d14c9 Remove on-file-path-change 2022-04-03 09:53:10 +05:30
contrapunctus f18c71453b Disable verify method 2022-04-03 09:30:36 +05:30
contrapunctus ebce220bd5 Create plist-group package 2022-04-03 09:26:36 +05:30
contrapunctus 7a1cf0d66e Create XDG directories on startup 2022-04-02 22:36:45 +05:30
contrapunctus e45064be44 Export external symbols 2022-04-02 22:13:47 +05:30
contrapunctus 131e157239 Replace -let with let-match 2022-04-02 22:13:26 +05:30
contrapunctus fdc2b1a964 Fix method arity issues 2022-04-02 22:13:06 +05:30
contrapunctus 3f50c1ba55 Remove old to-file method 2022-04-02 22:12:46 +05:30
contrapunctus 0ea1dee847 Update some references to old chronometrist-file variable 2022-04-02 21:49:54 +05:30
contrapunctus ad6cf91a53 Reinstate data structures and time functions 2022-04-02 21:45:15 +05:30
contrapunctus 8fc14e6686 Convert Elisp alist functions to CL 2022-04-02 21:43:59 +05:30
contrapunctus 6e38c56f0e doc: Add first-run behaviour tasks 2022-04-02 17:30:45 +05:30
contrapunctus 493d7d34ba Address load errors
Remove frontend command `switch-backend` and function `read-backend-name`
Replace `alist-get` with `assoc`
Add earmuffs to *backends-alist*
2022-04-02 17:23:54 +05:30
contrapunctus c4666ee770 Remove `provide` form 2022-04-02 16:58:21 +05:30
contrapunctus b5d71bd4d5 Address load errors 2022-04-02 16:58:11 +05:30
contrapunctus c0c6c20e2a Correct view statement 2022-04-02 16:37:35 +05:30
contrapunctus 48ea058ce3 Define file slot accessor and update some call sites
Since we don't have Elisp's add-variable-watcher in Common Lisp, this
seemed like the next best way (or an even better way?) to
automatically update the database path as soon as the user changes its
location.
2022-04-02 14:57:26 +05:30
contrapunctus a77eaac963 Use XDG directories for configuration and storage locations 2022-04-02 12:39:47 +05:30
contrapunctus 0e52e04491 Address compiler errors, reinstate make-hash-table-1 2022-04-02 12:00:02 +05:30
contrapunctus 1a7d70b8b7 Write SXQL alternative 2022-04-02 11:06:04 +05:30
contrapunctus 35d9e81c78 Create view 2022-04-02 10:58:35 +05:30
contrapunctus 10b8328474 Try using queries in CLSQL symbolic syntax 2022-04-02 10:58:09 +05:30
contrapunctus cf80e12ce6 Migrate create-file from emacsql to CLSQL 2022-04-02 09:12:12 +05:30
contrapunctus 125cba8fc3 Replace emacsql-sqlite with clsql:connect 2022-04-02 08:13:43 +05:30
contrapunctus 1e00f8e930 Add tags 2022-04-02 08:13:36 +05:30
contrapunctus 0722b10a32 Define package chronometrist.sqlite 2022-04-02 08:10:39 +05:30
contrapunctus cfbe5a6209 Remove plist backend code 2022-04-02 08:07:21 +05:30
contrapunctus 01deba0c45 Convert or remove Elisp-isms 2022-04-02 08:07:16 +05:30
contrapunctus 987e82cbb9 Replace -let with let-match, comment out square bracket syntax
We also remove code which is not likely to be necessary for some time,
in a bid to be able to load the file without errors.
2022-04-02 00:41:47 +05:30
contrapunctus 605fc63b49 Remove migration and timeclock code 2022-04-02 00:12:37 +05:30
contrapunctus 04ede947b4 Merge branch 'dev' into cl-port 2022-04-01 22:42:10 +05:30
contrapunctus c47906f0bf Remove unused code 2022-04-01 21:31:12 +05:30
contrapunctus 059579e841 TODO: Remove async.el discussion, add tasks 2022-04-01 20:26:07 +05:30
contrapunctus 98650443c7 Merge branch 'doc' into async-migrate 2022-04-01 20:23:51 +05:30
contrapunctus cbf302a7d1 Fix empty output and emacsql-close error 2022-04-01 20:23:47 +05:30
contrapunctus 8c02c3c397 Accept optional arguments, make code async (WIP)
The optional arguments enable partially-non-interactive use and ease
testing.
2022-04-01 19:47:45 +05:30
contrapunctus 8f11adb3bc doc: Add async migration task and attempted implementation 2022-04-01 18:11:15 +05:30
contrapunctus 25dd20f170 Add task for first-launch migration 2022-04-01 12:20:23 +05:30
contrapunctus dbbc25d127 Merge branch 'doc' into dev 2022-04-01 12:02:59 +05:30
contrapunctus ea61ed6d2a Insert prop-id when inserting intervals 2022-04-01 12:02:21 +05:30
contrapunctus f7285c2ddc doc: Change 'custom fields' idea to 'categories' 2022-03-31 22:54:52 +05:30
contrapunctus ad07f401e2 doc: Add customizable total duration fields idea 2022-03-31 17:32:45 +05:30
contrapunctus 8ede73dbf5 Define backend and frontend systems 2022-03-30 20:26:50 +05:30
contrapunctus 769495ca5e Write incomplete to-list method for SQLite backend 2022-03-29 20:52:39 +05:30
contrapunctus b5501afc9d Change source block language 2022-03-29 20:29:59 +05:30
contrapunctus 6de9e473fd Use earmuffs and defvar for custom variables 2022-03-29 20:29:59 +05:30
contrapunctus 985969f867 Remove cl- and chronometrist- prefixes 2022-03-29 20:29:59 +05:30
contrapunctus b902da07ee Copy SQLite backend code 2022-03-29 20:05:33 +05:30
contrapunctus 691c8c4089 Export symbols for custom variables 2022-03-28 03:11:05 +05:30
contrapunctus e4a3cf4cfd Create CLIM GUI file from Elisp frontend codebase 2022-03-28 02:52:06 +05:30
contrapunctus fc7ed02df7 Remove report- prefix 2022-03-28 02:38:41 +05:30
contrapunctus 236dbfda8f Remove remaining cl- prefixes 2022-03-27 20:17:13 +05:30
contrapunctus e50b2813b3 Remove pretty-printer 2022-03-27 20:15:38 +05:30
contrapunctus 9ff6dd7edf Address package lock errors 2022-03-26 23:33:02 +05:30
contrapunctus 0cc451fa03 Change source block language 2022-03-26 23:24:42 +05:30
contrapunctus cd657af021 Remove frontend and timer code 2022-03-26 23:09:32 +05:30
contrapunctus d7da24b7cb Remove chronometrist- prefix 2022-03-26 23:02:39 +05:30
contrapunctus 7c85867aa7 Remove cl- prefixes 2022-03-26 23:00:58 +05:30
contrapunctus 6b30afb90e Change cl-defun to defun; remove custom groups 2022-03-26 22:58:17 +05:30
contrapunctus 0147bc3650 Add system definition, copy Elisp LP 2022-03-26 20:57:33 +05:30
contrapunctus 3f52469f31 Disable JSON pretty-printing 2022-03-26 20:09:49 +05:30
contrapunctus 1eaf2c099a Fix emacsql setup form; setup automatic literate-elisp-load 2022-03-25 23:00:20 +05:30
contrapunctus c22be59b51 Reinstate encoding of properties as JSON (now customizable) 2022-03-25 22:39:12 +05:30
contrapunctus 0d4dde2a3f Use INSERT OR IGNORE INTO; move tasks to TODO 2022-03-25 20:27:27 +05:30
contrapunctus 8c23ac65f0 Remove JSON encoding of user key-values 2022-03-25 20:13:16 +05:30
contrapunctus ab1dc18795 Fix "Too many open files" error 2022-03-25 19:53:58 +05:30
contrapunctus fc47683184 Fix json-encode error with cons pairs 2022-03-25 09:14:06 +05:30
contrapunctus a2c483f1ee Correct create-file return value 2022-03-25 08:38:42 +05:30
contrapunctus 98dda890d4 Add optional file argument to create-file 2022-03-25 06:25:11 +05:30
contrapunctus 319e13140a Create database connection for insert 2022-03-25 06:24:11 +05:30
contrapunctus c541224f73 Do nothing if inserting duplicates into intervals 2022-03-25 06:23:28 +05:30
contrapunctus f60526e42c Handle insertion of day-crossing intervals 2022-03-24 23:42:34 +05:30
contrapunctus 818d130066 Move `create-file` near old definition
Move `create-file` before `to-file`
2022-03-24 20:24:03 +05:30
contrapunctus a3aec17488 Reindent emacsql queries 2022-03-24 20:07:50 +05:30
contrapunctus 72c9a355b5 (WIP) Implement to-file and insert operations 2022-03-24 19:51:07 +05:30
contrapunctus 640c3e75ee Extend schema to support date properties and events 2022-03-23 21:19:24 +05:30
contrapunctus 4c06cc1ab6 Tag command; rename local variables 2022-03-23 21:17:46 +05:30
contrapunctus f9e68771c8 Remove tags column; use JSON column for user properties 2022-03-22 12:12:23 +05:30
contrapunctus b71c534693 Merge branch 'doc' into sqlite 2022-03-22 11:21:16 +05:30
contrapunctus a8a51accbf Add notes on SQLite backend 2022-03-21 09:27:25 +05:30
contrapunctus 239f733dd8 Merge branch 'dev' into sqlite 2022-03-21 09:19:17 +05:30
contrapunctus 23d94400cc Merge branch 'doc' into dev 2022-03-03 13:11:39 +05:30
contrapunctus e6c8ed785d Add task for better error reporting 2022-03-03 13:11:20 +05:30
contrapunctus 7cf2c86afd Convert user presets to strings 2022-02-25 15:20:21 +05:30
contrapunctus 3962c63f93 Mark task done; document chronometrist-goal bug 2022-02-25 00:27:50 +05:30
contrapunctus 9ef3337c5a Tweak default duration format 2022-02-24 15:47:06 +05:30
contrapunctus 7334be5743 Add key-value presets
Inspired by the Android application "My Expenses" by Michael
Totschnig.
2022-02-23 20:47:46 +05:30
contrapunctus 3d2bc8149e Format old-break duration like other durations 2022-02-23 14:52:53 +05:30
contrapunctus c51f4a4560 Remove MELPA Stable recommendation 2022-02-20 16:46:48 +05:30
contrapunctus e6df8138ee Merge branch 'doc' into dev 2022-02-20 16:01:23 +05:30
contrapunctus ba560d6857 Use manual.org as readme 2022-02-20 15:54:53 +05:30
contrapunctus 6556ee0bde Add screenshot 2022-02-20 15:54:53 +05:30
contrapunctus 08e53cfea3 Add predefined key-values idea 2022-02-17 23:44:08 +05:30
contrapunctus 6b56be318d Move tasks into main heading 2022-02-17 23:33:38 +05:30
contrapunctus dd8660ee73 Add tasks for implementing date properties 2022-02-17 23:33:07 +05:30
contrapunctus 2b2bfa2778 Merge branch 'doc' into dev 2022-02-16 00:34:00 +05:30
contrapunctus 2c12741474 Update versions for release 2022-02-15 21:03:25 +05:30
contrapunctus 12e11945a2 Rename "events" -> "ht" 2022-02-15 20:48:59 +05:30
contrapunctus 9cdc5e5cbb Tweak task 2022-02-15 18:17:14 +05:30
contrapunctus b222ecc35f Update section "customizable task list" and CHANGELOG 2022-02-15 12:49:02 +05:30
contrapunctus 4638b040ff Use headings for new backend items 2022-02-15 12:35:31 +05:30
contrapunctus 456fe451ab Add heading tags 2022-02-15 12:32:07 +05:30
contrapunctus 27ebc824fa Correct :nexport: tag 2022-02-15 01:26:15 +05:30
contrapunctus d88619b15f Update TODO for alert macro 2022-02-14 22:13:00 +05:30
contrapunctus 2eed71ad85 Add 'canned key-values' idea 2022-02-14 20:47:12 +05:30
contrapunctus a5c709253f Add CUSTOM_ID, macro idea 2022-02-14 13:05:02 +05:30
contrapunctus e7fd4df27e Split Third Time section into headings 2022-02-14 09:00:54 +05:30
contrapunctus fe9ed8c7f0 Add customizable alerts idea 2022-02-14 09:00:31 +05:30
contrapunctus cdcc059b26 Add notes on multiple intervals/time deduction 2022-02-13 17:16:56 +05:30
contrapunctus 21c442069e Merge branch 'doc' into dev 2022-02-13 16:04:54 +05:30
contrapunctus d69944b81c Correct tags, add docstring 2022-02-13 16:03:48 +05:30
contrapunctus 38f2b6239c Remove duplicate footnote 2022-02-13 02:59:12 +05:30
contrapunctus 005e54f79b Note branches; mark work on date properties as STARTED 2022-02-13 02:53:07 +05:30
contrapunctus 2c3b9eaa97 Add work hour implementation idea 2022-02-13 02:52:25 +05:30
contrapunctus 7f4e5e9c7c Remove leftover SO reference 2022-02-13 02:39:22 +05:30
contrapunctus 90d3f73cdf Remove :stop key for active split plists 2022-02-13 01:05:38 +05:30
contrapunctus a7eecc96ab Change latest-record to always return a unified record
This fixes -
* time displayed by activity indicator
* calculation of break time for chronometrist-third
2022-02-13 01:02:11 +05:30
contrapunctus 28a0f69132 Show different message when all break time is used up 2022-02-13 00:02:49 +05:30
contrapunctus bedbf6a8c5 Pass time as argument to callbacks 2022-02-12 23:50:34 +05:30
contrapunctus a78e1420d7 Merge branch 'doc' into dev 2022-02-12 23:42:32 +05:30
contrapunctus 29dee2792c Fix unintentionally exported heading 2022-02-12 23:41:39 +05:30
contrapunctus 1b6c21d529 Fix `alert` errors, blank duration in notification 2022-02-12 21:41:15 +05:30
contrapunctus 6ba6c0996c Update explanation 2022-02-12 07:39:01 +05:30
contrapunctus 66d747d27c Add autoload cookie 2022-02-11 22:01:24 +05:30
contrapunctus c13241b0a9 Update TODO 2022-02-11 12:36:24 +05:30
contrapunctus 09203bf5db Fix incorrect duration bug 2022-02-11 12:35:56 +05:30
contrapunctus 27e2b17214 Add docstrings 2022-02-11 12:29:49 +05:30
contrapunctus 16248f35b3 Add alternative date key-value form 2022-02-11 02:36:46 +05:30
contrapunctus 863cbcbf52 Update CHANGELOG 2022-02-11 00:22:22 +05:30
contrapunctus 3ad9c3b2aa Merge branch 'dev' into third-time 2022-02-11 00:19:28 +05:30
contrapunctus 2b57dc4807 Reword point, add deprecation notice 2022-02-11 00:19:18 +05:30
contrapunctus 64c14137af Add tasks for big break command, work hours, and date key-values 2022-02-10 23:19:18 +05:30
contrapunctus 811b79eb95 Add TODO items for Third Time 2022-02-10 22:14:25 +05:30
contrapunctus 0a1382abd7 Address lint issues 2022-02-10 22:05:31 +05:30
contrapunctus 276bf64cf7 Add library headers, define alerts 2022-02-10 21:57:17 +05:30
contrapunctus 8381dc36c5 Define minor mode 2022-02-10 19:17:04 +05:30
contrapunctus 1297f2faa1 Auto-load with literate-elisp 2022-02-10 18:53:06 +05:30
contrapunctus daf2d2895f Merge branch 'dev' into third-time 2022-02-10 18:51:20 +05:30
contrapunctus 75d0130df4 Implement clock-in and clock-out functions 2022-02-10 18:47:35 +05:30
contrapunctus bbb9986edf Rename function, update chronometrist-interval to add :stop times 2022-02-10 18:27:01 +05:30
contrapunctus a83173d0a9 Merge branch 'doc' into dev 2022-02-10 18:18:39 +05:30
contrapunctus bbe54ca3bf Fix undesired empty lines between plist groups 2022-02-10 17:27:19 +05:30
contrapunctus f94ab9643b Add scratch code 2022-02-10 17:03:08 +05:30
contrapunctus 409aa0337e Add newlines between headings 2022-02-10 16:47:28 +05:30
contrapunctus a04e9ebfba Add TODO entry for the Third Time system 2022-02-10 16:46:24 +05:30
contrapunctus cd7eb9c726 Merge branch 'dev' into sqlite 2022-01-31 22:52:58 +05:30
contrapunctus 406c23275c Correct example configuration for vertico-multiform 2022-01-24 19:59:33 +05:30
contrapunctus 5629b06f33 Tweak vertico-multiform example code 2022-01-24 02:27:09 +05:30
contrapunctus e603178655 Link to Vertico 2022-01-24 02:15:41 +05:30
contrapunctus 024bab2376 Add how-to for Vertico 2022-01-24 02:11:56 +05:30
contrapunctus 0880c3c966 Remove extra spaces 2022-01-24 01:49:37 +05:30
contrapunctus 28820db0d3 Update activity indicator code example 2022-01-24 01:48:42 +05:30
contrapunctus 54023fac6a Update auto-backup instructions 2022-01-24 01:45:02 +05:30
contrapunctus 88ee7710d8 Add history variable for unified key-value prompt 2022-01-24 01:36:15 +05:30
contrapunctus f0073b583b Fix relint action 2022-01-23 13:11:30 +05:30
contrapunctus 4a83282931 Address package-lint issues 2022-01-23 13:09:57 +05:30
contrapunctus ddb5729d21 Merge branch 'backend-tests' into dev 2022-01-23 13:08:52 +05:30
contrapunctus 7008da32fd Correct build actions 2022-01-23 13:05:17 +05:30
contrapunctus 66b92580d3 Fix code block export 2022-01-23 12:09:57 +05:30
contrapunctus 182c98ae7b Link to other repository 2022-01-23 11:23:48 +05:30
contrapunctus fd1dd3a5e8 Set up automatic literate-elisp-load 2022-01-23 11:22:30 +05:30
contrapunctus 0a09dc74e6 Add generated file warning 2022-01-23 11:06:02 +05:30
contrapunctus d22bd4f7e3 Tweak short descriptions 2022-01-23 11:02:55 +05:30
contrapunctus 4ddc06c084 Update README/manual 2022-01-23 10:56:08 +05:30
contrapunctus 07322c1052 Remove Cask file 2022-01-23 10:43:04 +05:30
contrapunctus 42cf241587 Make description consistent 2022-01-23 10:42:54 +05:30
contrapunctus bcff5690fc Add more test cases for insert 2022-01-14 21:48:32 +05:30
contrapunctus 86c7fa6d5b Use with-slots, new schema, and register-backend 2022-01-14 01:02:51 +05:30
contrapunctus fe9f2b494f Add links to tests 2022-01-13 22:48:42 +05:30
contrapunctus 84069f0b87 Remove old tests 2022-01-13 21:54:51 +05:30
contrapunctus 64dced972a Add debugging messages 2022-01-13 21:54:06 +05:30
contrapunctus b308fc06d3 Remove superfluous package prefix 2022-01-13 21:53:13 +05:30
contrapunctus 40a5eb60bd Clear hash table after each test 2022-01-13 21:50:28 +05:30
contrapunctus 240c0f5956 Change debug message formats 2022-01-13 17:09:26 +05:30
contrapunctus a909f323bd Fix insertion in debug buffer 2022-01-13 17:08:57 +05:30
contrapunctus c33eb1dac5 Merge branch 'dev' into backend-tests 2022-01-13 17:08:28 +05:30
contrapunctus 21285da0d9 Remove sqlite.org from Makefile 2022-01-13 17:07:48 +05:30
contrapunctus f558a3a469 Merge branch 'dev' into backend-tests 2022-01-13 17:06:28 +05:30
contrapunctus 5900a85491 Add Makefile 2022-01-13 14:44:51 +05:30
contrapunctus c2a223718e Correct dependencies 2022-01-13 14:40:51 +05:30
contrapunctus 77480b2e9f Create WIP sqlite backend
Mostly copied from the earlier attempt
2022-01-13 03:13:39 +05:30
contrapunctus e3590575d0 Check if file-watch is actually non-nil
A preemptive check against possible error situations
2022-01-13 01:40:44 +05:30
contrapunctus b9cc5b7a49 Add tests for create-file, latest-date-records, and insert 2022-01-13 01:37:25 +05:30
contrapunctus c06a5740fc Inhibit query when killing buffer in post-test cleanup 2022-01-12 20:05:05 +05:30
contrapunctus 4ef9890f67 Add type checks to replace-last and task-records-for-date 2022-01-12 14:56:42 +05:30
contrapunctus 69da87faf0 Move common testing definitions to Setup 2022-01-12 14:18:27 +05:30
contrapunctus 58ec3aa09a Update current-task test 2022-01-12 14:18:27 +05:30
contrapunctus 52a2aeae14 Extend plist type; add :before method for type-checking 2022-01-12 14:18:27 +05:30
contrapunctus 04bc6191c4 Change ert-deftest wrapper
Earlier, it was designed only for generating tests for all backend
operations. Now, with the addition of the NAME argument, it is made to
generate test groups (one per backend) for a specific operation.
2022-01-12 11:46:55 +05:30
contrapunctus 23d8831916 Merge branch 'dev' into backend-tests 2022-01-12 11:09:35 +05:30
contrapunctus 78b7a760a0 Add debug message to insert method 2022-01-12 11:06:11 +05:30
contrapunctus 766f22b6c5 Update changelog for changed debug mechanism 2022-01-12 11:05:40 +05:30
contrapunctus 4d1d0c53a9 Fix active record getting inserted in both old and new groups 2022-01-12 03:26:33 +05:30
contrapunctus 8e94af2f32 Add a basic verify method for plist-group backends 2022-01-11 05:04:54 +05:30
contrapunctus a4617f7066 Merge branch 'doc' into dev 2022-01-11 04:15:45 +05:30
contrapunctus 3aca6b2e70 Write own debug logging code to replace display-warning
The constant popping up of the warning buffer window was annoying
2022-01-11 00:42:49 +05:30
contrapunctus b05a1e5f13 Fix incorrect file change type logic and -let pattern
new-last-hash is supposed to be between rest-end and file-length,
rather than new-file-length
2022-01-10 22:09:43 +05:30
contrapunctus fcc34aa4ff Move cleanup code to macro 2022-01-10 15:56:15 +05:30
contrapunctus 278481b854 Use macro instead of looping inside deftest 2022-01-10 15:40:37 +05:30
contrapunctus bb5f2a9e41 Return non-nil on successful plist-backend insert method 2022-01-10 15:36:12 +05:30
contrapunctus 59c23aef13 Make backend test cleanup code generic; tweak class hierarchy 2022-01-10 14:47:28 +05:30
contrapunctus 94f538a6b0 Raise error if remove-last has nothing to remove 2022-01-10 14:45:58 +05:30
contrapunctus cbe62977b3 Refactor backend test cleanup forms 2022-01-10 10:54:32 +05:30
contrapunctus 79ca0242de Change create-file to return the file name on success 2022-01-10 10:53:32 +05:30
contrapunctus d144ccf62b Add newlines between headings 2022-01-09 20:54:24 +05:30
contrapunctus 09a045121b Add bug note, UX improvement 2022-01-09 18:46:20 +05:30
contrapunctus 52eefcd86f Prevent args out of bound errors 2022-01-09 18:32:19 +05:30
contrapunctus 6e00907e28 Change debug from a macro to a function 2022-01-09 18:31:44 +05:30
contrapunctus cadb28f030 Add menu entry for migrate command 2022-01-09 17:35:36 +05:30
contrapunctus aa40bbe291 Add debug messages for commands 2022-01-09 17:34:40 +05:30
contrapunctus 57ba75f376 Add debug logging and messages 2022-01-09 16:01:06 +05:30
contrapunctus 43b924ca03 Fix error stemming from incorrect pattern 2022-01-09 11:03:15 +05:30
contrapunctus 1525df5fc2 Fix insertion of day-spanning plists
Prevent insertion of undesired newlines
2022-01-09 03:35:16 +05:30
contrapunctus eb4fc565e0 Update TODO 2022-01-09 02:09:40 +05:30
contrapunctus 0938841b26 Update CHANGELOG 2022-01-08 23:31:17 +05:30
contrapunctus 0c4e3ef47c Rewrite file-change-type, update on-change 2022-01-08 23:26:03 +05:30
contrapunctus 41728d511c Convert file-state into discrete slots and update most references 2022-01-08 21:33:09 +05:30
contrapunctus 5b272da659 Make timer monomorphic, create backend-modified-p 2022-01-08 14:27:45 +05:30
contrapunctus cdcb031e31 Merge branch 'doc' into dev 2022-01-08 14:25:21 +05:30
contrapunctus ccc759d737 Fix error when buffer being edited 2022-01-08 13:43:06 +05:30
contrapunctus c92eaa924b Clarify optimization idea 2022-01-07 23:28:26 +05:30
contrapunctus b45cce3e34 Add implemented optimization idea 2022-01-07 23:26:59 +05:30
contrapunctus ea336afe09 Do not generate task list if it exists 2022-01-07 16:16:28 +05:30
contrapunctus e8137651d0 Update file-change-type tests 2022-01-06 12:00:31 +05:30
contrapunctus 0d00ac6fe2 Fix buffer not displayed on first start 2022-01-05 23:31:35 +05:30
contrapunctus 2721215ce7 Document backend protocol 2022-01-05 19:26:57 +05:30
contrapunctus 74cc00b1e5 Address byte compiler warnings 2022-01-05 15:20:26 +05:30
contrapunctus 8b0e3def68 Edit docstring, comment out debugging message 2022-01-05 14:32:46 +05:30
contrapunctus eaf09dea10 Add docstrings 2022-01-05 14:26:35 +05:30
contrapunctus 71eded0620 Update TODO 2022-01-05 14:26:25 +05:30
contrapunctus d07f99ff3a Change protocol signatures; implement plist-group methods 2022-01-05 13:55:19 +05:30
contrapunctus d5ce69ff92 Update plist backend docstrings and parameters 2022-01-05 13:21:31 +05:30
contrapunctus 2cefb73052 Add details to docstrings, remove fs-event parameter from GF 2022-01-05 13:13:04 +05:30
contrapunctus 4298af549d Refresh buffer after backend reset 2022-01-05 11:50:47 +05:30
contrapunctus 18f8c77025 Move task-list procedures 2022-01-05 11:07:19 +05:30
contrapunctus e02acdd3cc Refactor on-change, add 3 new generic functions
on-add/on-modify/on-remove are not completely implemented yet
2022-01-05 11:05:08 +05:30
contrapunctus 9a8b359cd9 Update active backend when chronometrist-file is changed
Users' custom chronometrist-file value was being ignored unless it was
set before the package was loaded.
2022-01-04 01:30:23 +05:30
contrapunctus b42a651d21 Address errors 2022-01-04 01:29:26 +05:30
contrapunctus eaa1692d3a Use backend-empty-p instead of file-empty-p 2022-01-04 00:56:56 +05:30
contrapunctus 36336367ef Use cl- for other list accessors 2022-01-04 00:30:47 +05:30
contrapunctus 46e28b022b Replace second with cl-second 2022-01-03 19:08:43 +05:30
contrapunctus 06f2ceb44a Address byte compiler warnings 2022-01-03 19:00:41 +05:30
contrapunctus 4206ffe615 Merge branch 'grouped-plist-format' into dev 2022-01-03 18:45:32 +05:30
contrapunctus b2cb16ff10 Address byte compiler warnings 2022-01-03 18:45:16 +05:30
contrapunctus 1aab30d399 Reorganize protocol 2022-01-03 18:02:06 +05:30
contrapunctus 0874b9697a Remove update-properties 2022-01-03 17:50:34 +05:30
contrapunctus 2756239e68 Move file-state to class slot 2022-01-03 17:47:15 +05:30
contrapunctus 0657777e8c Convert timestamps to properties 2022-01-03 16:18:07 +05:30
contrapunctus 3cf063b07b Add two optimization ideas 2022-01-03 16:14:47 +05:30
contrapunctus 1bd3c16929 Rename reset-internal -> reset-backend 2022-01-03 14:07:45 +05:30
contrapunctus df00031285 Remove references to files from protocol 2022-01-03 03:20:24 +05:30
contrapunctus 5d5b423c59 Implement multi-backend tests
Reorganize plist backend methods
Address some failing tests for plist backend methods
2022-01-03 02:11:49 +05:30
contrapunctus 909be83e3a Suppress load-time error about nonexistent file 2021-12-27 14:06:42 +05:30
contrapunctus c87b8a0a4d Add tests for case of file without records 2021-12-27 14:06:09 +05:30
contrapunctus 7ce9f8bf2d Move create-file earlier in the protocol 2021-12-27 14:03:14 +05:30
contrapunctus e031d43f24 Add error-checking and return values 2021-12-27 14:02:53 +05:30
contrapunctus 28f120f94b Reorder tests 2021-12-26 19:36:44 +05:30
contrapunctus 0fb0c6d0d6 Specify return value in protocol and update behavior 2021-12-26 19:36:44 +05:30
contrapunctus 283efd5a03 Reinstate test data as list of plists 2021-12-26 14:29:51 +05:30
contrapunctus f29c6c5157 Merge branch 'doc' into backend-tests 2021-12-24 21:55:25 +05:30
contrapunctus c58726ca7c doc(TODO): Add new task for verify command 2021-12-24 21:54:46 +05:30
contrapunctus e3acecfec9 doc(TODO): Cross off task 2021-12-24 21:54:12 +05:30
contrapunctus 5eee8f8df6 doc(TODO): Add Hamster as potential backend 2021-12-24 21:45:03 +05:30
contrapunctus bd4680fc04 Return non-nil if create-file succeeds 2021-12-23 20:33:52 +05:30
contrapunctus 01d11ba5d2 Add tests for backend protocol 2021-12-23 20:33:22 +05:30
contrapunctus 0dd4e775f5 Remove old test data file 2021-12-23 20:29:08 +05:30
contrapunctus 43e4d212f8 Merge branch 'doc' into backend-tests 2021-12-18 15:45:20 +05:30
contrapunctus 708b9fafa8 doc(TODO): Document bug 2021-12-18 15:44:35 +05:30
contrapunctus a41662236f Replace `cl-assert` with `error`
A failed `cl-assert` does not pass a `should-error` test.
2021-12-18 15:02:06 +05:30
contrapunctus c2f1590fe7 doc(TODO): Add ideas requiring changes to the format 2021-12-18 11:59:28 +05:30
contrapunctus cbcb10fdd4 Rename check-preconditions to run-assertions 2021-12-13 11:51:17 +05:30
contrapunctus a123e59611 Add type checks and assertions for plist group methods 2021-12-13 11:19:05 +05:30
contrapunctus 34ff8f9038 Segregate extended protocol for plist-group backend 2021-12-13 10:21:20 +05:30
contrapunctus 74414c313b Segregate extended protocol, add check-preconditions
Add details to protocol docstrings
2021-12-12 20:01:54 +05:30
contrapunctus be53039f00 doc(TODO): Add klog and SQLite 2021-12-07 16:24:41 +05:30
contrapunctus 7ca6413907 doc: explain plist-group specific concerns 2021-12-06 14:58:17 +05:30
contrapunctus 66853efd12 doc: clarify docstring for latest-record 2021-12-06 14:57:37 +05:30
contrapunctus 9d68ac8b53 Merge branch 'dev' into plist-group-handle-split-plists 2021-12-05 22:18:57 +05:30
contrapunctus 975b67af1f fix: order of records returned by plist-group to-list 2021-12-05 22:16:30 +05:30
contrapunctus 2ac54f418a Merge branch 'dev' into plist-group-handle-split-plists 2021-12-04 22:45:12 +05:30
contrapunctus 3fd8efea73 Create generic update-properties 2021-12-04 18:41:17 +05:30
contrapunctus 0e8e9f4373 Fix error messages to match Elisp guidelines 2021-12-04 18:39:46 +05:30
contrapunctus 7ddb9c653b Join line if only deleting a plist 2021-12-04 16:58:45 +05:30
contrapunctus c70a7791b0 code: use local variables for clarification 2021-12-04 15:15:33 +05:30
contrapunctus a947cca7c7 Throw error if remove-last is called on backend with no records 2021-12-04 15:13:40 +05:30
contrapunctus b469defcd4 Throw error if `insert` called with empty plist 2021-12-04 14:25:43 +05:30
contrapunctus 10b592a04b fix: make replace-last handle split plists 2021-12-04 14:19:31 +05:30
contrapunctus a669f2b765 fix: prevent leftover empty plist-groups 2021-12-04 12:58:54 +05:30
contrapunctus 07aaa0e78c fix: make remove-last take split plists into account
last-two-split-p - remove redundant conditions (we are checking for a
situation in which the newer-plist-group cannot, by definition, have
>1 plists), change return value.
2021-12-03 22:12:04 +05:30
contrapunctus 6ad88b19d3 Merge branch 'doc' into plist-group-handle-split-plists 2021-12-03 10:49:49 +05:30
contrapunctus 6aa4ef15f8 doc(TODO): move input frontends idea to its own heading 2021-12-03 10:49:28 +05:30
contrapunctus 9e8c3659d5 Create last-two-split-p 2021-12-03 01:01:27 +05:30
contrapunctus d108a4fdcb style: tweak plists-split-p 2021-12-03 00:16:53 +05:30
contrapunctus 83f19d70db Merge branch 'doc' into plist-group-handle-split-plists 2021-12-02 17:52:49 +05:30
contrapunctus 0fa2b270b4 doc(TODO): mention in-progress branch 2021-12-02 17:52:16 +05:30
contrapunctus 17b19d79f5 Create plists-split-p 2021-12-02 17:42:45 +05:30
contrapunctus 2725b742cd doc(TODO): auto-insert timestamp for new headings 2021-12-02 17:00:45 +05:30
contrapunctus f9dae2e2c8 doc(TODO): add new tasks 2021-12-01 18:59:50 +05:30
contrapunctus 311c3b0a1b Add test case 2021-12-01 17:09:08 +05:30
contrapunctus 5e5697b326 fix: dispatch on correct backend object
(This didn't have any incorrect behaviour that I noticed...I wonder why.)
2021-12-01 15:46:29 +05:30
contrapunctus 1234a65af5 doc(TODO): add implementation progress for plist-group backend 2021-12-01 15:44:44 +05:30
contrapunctus 6be3849136 doc: add TODO items and failing test case 2021-11-30 21:33:05 +05:30
contrapunctus 37bc29d923 fix: pretty printer behaviour for nested list values 2021-11-30 18:10:34 +05:30
contrapunctus e8eadc7048 fix: message user when discard-active is not applicable 2021-11-29 16:42:42 +05:30
contrapunctus 4eb4f30873 feat: new command chronometrist-discard-active 2021-11-28 17:26:12 +05:30
contrapunctus 14f02ff402 feat: implement backend operation remove-last 2021-11-28 12:36:31 +05:30
contrapunctus 8b82d68822 fix: timer running even when file buffer modified 2021-11-28 00:53:36 +05:30
contrapunctus 6768acd662 Reinstate chronometrist alias 2021-11-28 00:51:25 +05:30
contrapunctus 510a9400e1 Adjust nameless configuration
(To get aliases to work again, at the cost of nameless-insert-name
breaking. Opened an issue in the nameless repository -
https://github.com/Malabarba/Nameless/issues/32 )
2021-11-27 18:21:50 +05:30
contrapunctus 55f875ce1f Change "plist-pp" to "pp" 2021-11-27 18:08:50 +05:30
contrapunctus 40926e9d54 Merge branch 'dev' into doc 2021-11-27 17:32:49 +05:30
contrapunctus da45400b23 doc: add debugging suggestion 2021-11-25 19:12:36 +05:30
contrapunctus e29041e91d doc(TODO): add task 2021-11-25 17:37:04 +05:30
contrapunctus ac41b2ffde doc(TODO): update completed task 2021-11-25 09:55:59 +05:30
contrapunctus c9389d4404 doc(manual): add/update custom IDs, change manual.org link to chronometrist.org 2021-11-22 18:22:13 +05:30
contrapunctus 99f31e0381 doc(manual): make backup-per-save guide 2021-11-22 18:13:41 +05:30
contrapunctus d13362f6bf Move tests into chronometrist-tests.org 2021-11-22 17:01:57 +05:30
contrapunctus 87671c5a7f Merge branch 'doc' into dev 2021-11-22 16:43:59 +05:30
contrapunctus 2d28f896ff Remove unused local variable 2021-11-22 16:40:27 +05:30
contrapunctus a2a506f0f3 doc: clarify docstring 2021-11-22 16:39:43 +05:30
contrapunctus 702c1db887 Address byte compiler errors 2021-11-22 01:53:48 +05:30
contrapunctus 7e552e32fd Enable comment links to ease jumping from compilation errors to LP 2021-11-21 00:45:57 +05:30
contrapunctus bda42a5b90 doc(TODO): remove reset day operation, add sparkline task 2021-11-19 15:15:23 +05:30
contrapunctus 43a0645acd Add command to menu 2021-11-19 15:15:06 +05:30
contrapunctus e844e9f4b2 Create command to visit Org literate source 2021-11-19 15:10:17 +05:30
contrapunctus 0cc1a1174b Mention current name of macro 2021-11-19 15:05:54 +05:30
contrapunctus 4f91eb72e7 Merge branch 'dev' into doc 2021-11-18 18:05:13 +05:30
contrapunctus b0bbfb9ac5 Mirror changes in Org LP
(Forgot that this file was now an Org LP too.)
2021-11-18 12:00:28 +05:30
contrapunctus 0311640883 doc(TODO): add tasks for documentation discoverability 2021-11-18 09:50:13 +05:30
contrapunctus 133554d1b8 doc(changelog): note changes affecting user configuration 2021-11-18 08:51:14 +05:30
contrapunctus 6eb0549fb1 Update changelog 2021-11-18 02:36:58 +05:30
contrapunctus 1d278e4efc Merge branch 'grouped-plist-format' into dev 2021-11-18 02:28:56 +05:30
contrapunctus 7a3c5681ce Write currently-failing plist-pp-to-string test 2021-11-18 02:18:58 +05:30
contrapunctus b92f8b9c25 Write tests for plist-p, plist-group-p 2021-11-18 02:16:45 +05:30
contrapunctus 4477832673 doc: update TODO 2021-11-18 02:15:08 +05:30
contrapunctus 7b0df9a2d1 doc: mark hack 2021-11-18 02:14:11 +05:30
contrapunctus e15da603c7 [spark] write docstring 2021-11-09 23:13:14 +05:30
contrapunctus 866776d028 [plist-pp] handle plist groups 2021-11-08 22:44:01 +05:30
contrapunctus 552c64b967 [plist-pp] use shorter parameter name, add comments, use ?\s 2021-11-01 11:27:41 +05:30
contrapunctus 13c6cd184a [plist-group] temporary hack - reset data structures on file change
I intend to adapt the plist backend's change type detection code so it
can be used for any kind of s-expression file backends.
2021-11-01 11:04:47 +05:30
contrapunctus 38ba060de3 Fix incorrect backend being migrated 2021-11-01 11:03:54 +05:30
contrapunctus 8924e38401 Add :comments no in preperation for future top-level :comments link 2021-11-01 11:03:54 +05:30
contrapunctus a10e7f6e3a Fix chronometrist-migrate bugs 2021-11-01 11:03:54 +05:30
contrapunctus 62856f515d Move fs-watch to backend object slot 2021-11-01 11:03:54 +05:30
contrapunctus ae8c422d70 Run reset-internal instead of on-change after switching backends 2021-10-31 12:55:11 +05:30
contrapunctus 5892e9737f Write read-backend-name docstring; move to backend section 2021-10-31 12:53:37 +05:30
contrapunctus 186475d41e Create switch-backend command and register-backend function 2021-10-31 11:19:09 +05:30
contrapunctus 574ffd996c Split interval if necessary when clocking out 2021-10-31 09:55:58 +05:30
contrapunctus 7e19560d2a [plist-group] if required, create new tagged list before record insert 2021-10-30 09:51:21 +05:30
contrapunctus f583e5b3cc doc: update TODO 2021-10-30 08:36:21 +05:30
contrapunctus b70f304eb5 [plist-group] use hash table for task-records-for-date
* Move file-backend-mixin and related definitions to "common
  definitions for s-expression backends"
* Use loop-sexp-file and reinstate sexp-pre-read-check to reduce
  duplication
* Change some generics to dispatch on file-backend-mixin to make them
  common to the plist and plist-group backends
2021-10-30 01:17:23 +05:30
contrapunctus 7a17ed14a7 Rename functions; implement to-hash-table for plist-group
task-records -> task-records-for-date
active-time-one-day -> active-time-on

New generic - latest-date-records
Removed generic active-time
2021-10-30 00:00:35 +05:30
contrapunctus 06b29e3ce0 Update all references to chronometrist-events (except tests) 2021-10-29 09:54:28 +05:30
contrapunctus 45a2eeec06 Have details frontend use hash-table slot 2021-10-29 01:17:44 +05:30
contrapunctus 436aba7547 Remove list-tasks from backend protocol 2021-10-29 01:06:26 +05:30
contrapunctus d60d29c126 [key-values] remove key-value history limit 2021-10-29 00:40:54 +05:30
contrapunctus 4f0b0c0171 Use to-list + cl-loop instead of cl-loop wrapper macros 2021-10-29 00:40:54 +05:30
contrapunctus b0482699cd fix: use unwind-protect to always reset iterator state 2021-10-23 17:06:46 +05:30
contrapunctus 1d300249cc feat: customizable task list 2021-10-23 16:06:08 +05:30
contrapunctus 71ead83538 Rename test file 2021-10-21 14:57:33 +05:30
contrapunctus d10684f817 Add task-records test; restructure tests 2021-10-21 14:56:04 +05:30
contrapunctus 9eb9a05dd7 Make current-task a regular function 2021-10-21 14:33:50 +05:30
contrapunctus 8fd4d19c82 convert tests to Org LP; move hash table into backend object
* create generic functions reset-internal, on-change, backend-empty-p, memory-layer-empty-p
* move most of refresh-file to on-change method
2021-10-20 19:31:11 +05:30
contrapunctus 15b9bb832e fix(key-values): let -> let* 2021-10-17 17:34:05 +05:30
contrapunctus 1aa1a30291 Write tests for loop-records; move generic function 2021-10-15 07:39:59 +05:30
contrapunctus 31db47f46f Remove local variables from test file 2021-10-14 10:53:22 +05:30
contrapunctus 6424ef662c Remove docstrings from methods 2021-10-14 09:22:35 +05:30
contrapunctus 052bc23b17 Write backend tests 2021-10-14 09:21:28 +05:30
contrapunctus d6c980043b Implement insert method 2021-10-14 08:08:48 +05:30
contrapunctus 1f85435d56 Change slot name and values 2021-10-13 07:05:45 +05:30
contrapunctus 6bdb38f171 Implement replace-last 2021-10-13 06:59:34 +05:30
contrapunctus 786b6937ad Update task-records for changed loop-records interface 2021-10-13 06:56:45 +05:30
contrapunctus 177eb62d34 Update method for changed loop-records interface 2021-10-12 14:03:08 +05:30
contrapunctus fac18d2e08 optimize: speed up key/value history generation 2021-10-11 07:12:41 +05:30
contrapunctus 49d005be82 fix: remove :end from iterator states 2021-10-11 06:05:20 +05:30
contrapunctus b86825dded Use keywords for iterator-state; wrap cl-loop in save-excursion 2021-10-11 05:38:58 +05:30
contrapunctus 68d6b88bb0 doc: clarify plist time format 2021-10-10 22:02:13 +05:30
contrapunctus b76624c8e7 Convert plist-group-backend iterator to method;
Initialize iterator-state to t, so point is moved to (point-max) on
the first run of the iterator, too.

Our :after method for initialize-instance only applies to file-related
backends, and is thus changed to only dispatch on file-backend-mixin.
2021-10-10 21:58:27 +05:30
contrapunctus 5402130b27 Make iterator a generic function; store iterator-state as slot
Remove file-related details from `loop-records`
2021-10-10 17:52:52 +05:30
contrapunctus 1efc5ec7ac Move file-related slots into file-backend-mixin 2021-09-19 20:09:35 +05:30
contrapunctus a8cad8ccac fix file change detection 2021-09-13 19:34:20 +05:30
contrapunctus b310aa9298 migrate - fix insertion of file mode prop line 2021-09-12 19:19:41 +05:30
contrapunctus de2cabc4a6 update TODO 2021-09-12 17:22:08 +05:30
contrapunctus dfad505901 plist backend - fix test code 2021-09-12 17:22:08 +05:30
contrapunctus 4637f38408 optimize plist-group-read-record; extend backend documentation 2021-09-12 17:22:08 +05:30
contrapunctus 03b71132dc loop-records - fix infinite loop; list-tasks - fix test 2021-09-12 17:22:08 +05:30
contrapunctus e4c33d7097 create backend-generic `cl-loop` interface 2021-09-12 17:22:08 +05:30
contrapunctus 3b9e151bac migrate - prompt to overwrite if output file exists 2021-09-12 17:21:50 +05:30
contrapunctus 793491eb9b Remove list-records from protocol (there's already `to-hash-table`) 2021-09-10 19:41:55 +05:30
contrapunctus 69cda68f20 Convert references to chronometrist-file to chronometrist-backend-file 2021-09-09 21:25:40 +05:30
contrapunctus 7d7e801484 Update file watch setup for multiple backends 2021-09-09 18:30:37 +05:30
contrapunctus 34aeec3bb7 Revert changes to date format
Refactor spark-durations from spark-row-transformer for ease of
testing
2021-09-09 02:14:04 +05:30
contrapunctus 126b5710fb Merge branch 'doc' into grouped-plist-format 2021-09-07 07:29:39 +05:30
contrapunctus 4c7645bf55 TODO - add backend and code cleanup items 2021-09-07 07:29:16 +05:30
contrapunctus c14fea723c (WIP) implement task-records from task-events-in-day
Use ISO date instead of TS struct when dates required
2021-09-07 07:21:05 +05:30
contrapunctus b1192b1fdf Protocol - move list-records; remove task-time
task-time does not need to be polymorphic
2021-09-07 07:20:23 +05:30
contrapunctus 648253172a Implement task-records 2021-09-07 06:24:13 +05:30
contrapunctus 9e34fb8058 correct to-hash method, class definition 2021-09-06 22:31:27 +05:30
contrapunctus e0a6ff13a9 Clarify section name 2021-09-06 19:33:33 +05:30
contrapunctus 7f90191a40 rename loop-file; implement list-tasks and current-task methods 2021-09-06 19:32:21 +05:30
contrapunctus 63df54aa2e chronometrist-migrate - prompt for backends and files 2021-09-06 15:42:02 +05:30
contrapunctus 092f3e8f07 Hard wrap and copy hash table docstring 2021-09-06 01:18:26 +05:30
contrapunctus 8d73749c3f Associate create-file with both plist and plist-group backends
Move definitions common to sexp backends together
2021-09-06 01:18:26 +05:30
contrapunctus d673f00e5a Fix failing tests 2021-09-06 01:12:00 +05:30
contrapunctus 5a0d522e6f Describe plist group backend 2021-09-06 00:46:20 +05:30
contrapunctus dcf219aab4 Correct headings 2021-09-05 20:20:03 +05:30
contrapunctus b8b0967e18 Merge branch 'doc' into grouped-plist-format 2021-09-05 19:35:21 +05:30
contrapunctus a8eb47b184 TODO: note plist-group backend progress 2021-09-05 19:34:22 +05:30
contrapunctus 6893299427 Create methods stubs, create basic to-file method and migrate command 2021-09-05 19:10:37 +05:30
contrapunctus 43deef28af Use backend passed to method rather than active-backend 2021-09-05 17:39:43 +05:30
contrapunctus 3e7c41c9f7 Create on-file-change generic 2021-09-04 20:25:18 +05:30
contrapunctus 083a2f4d8e Adapt sexp-events-populate to to-hash-table 2021-09-04 20:20:59 +05:30
contrapunctus 78048824e5 Add package prefix to accessors 2021-09-04 19:45:20 +05:30
contrapunctus da1f2ed329 Fix more errors 2021-09-04 19:29:24 +05:30
contrapunctus 080e75a4c4 Move plist format description 2021-09-04 18:26:05 +05:30
contrapunctus a948fb4632 backend protocol - create count-record generic 2021-09-04 18:23:06 +05:30
contrapunctus 88d06da043 backend protocol - create migration generics 2021-09-04 18:08:06 +05:30
contrapunctus b218a835f7 plist backend - correct errors 2021-09-04 18:01:56 +05:30
contrapunctus 297f288130 Correct docstring 2021-09-04 17:29:34 +05:30
contrapunctus 9989cbf3d0 Update/clarify headings 2021-09-04 17:26:36 +05:30
contrapunctus 3f30eadbb0 Create method for list-tasks 2021-09-04 17:26:10 +05:30
contrapunctus af286f2d5d Create method for replace-last 2021-09-04 14:49:18 +05:30
contrapunctus dee921244b Create method for insert 2021-09-04 14:21:44 +05:30
contrapunctus 4e6243b3e3 Update call sites for create-file 2021-09-04 14:14:47 +05:30
contrapunctus 2b6ee13b96 Create method for create-file 2021-09-04 14:10:25 +05:30
contrapunctus 622914397e Create method for latest-record 2021-09-04 14:01:22 +05:30
contrapunctus 445614e9aa backend - add file slot 2021-09-04 13:55:26 +05:30
contrapunctus 260aafaebb Make edit-file method for plist backend 2021-09-04 08:45:56 +05:30
contrapunctus 0d612f28b5 Update current-task call sites 2021-09-04 08:43:32 +05:30
contrapunctus 4f50600e69 Correct generic function tags 2021-09-04 08:15:40 +05:30
contrapunctus 7bb023caf5 Rethink custom interface 2021-09-04 08:11:57 +05:30
contrapunctus 7c55a19e1f feat: create generic backend protocol 2021-09-04 00:28:33 +05:30
contrapunctus 2f218165b9 doc(TODO): file format ideas - list common pros and cons 2021-09-03 13:01:20 +05:30
contrapunctus 24a11688e2 doc(TODO): convert optimization ideas to headings 2021-09-03 01:26:52 +05:30
contrapunctus 6b00abca83 Merge branch 'dev' into doc 2021-09-03 01:22:10 +05:30
contrapunctus ba9e8dfa9c fix: correct docstring; indent code 2021-09-02 21:05:30 +05:30
contrapunctus 1b549a3c4c doc(TODO): note progress on format-seconds branch 2021-09-02 21:04:34 +05:30
contrapunctus a172e2cbb7 doc(TODO): add bug 2021-09-02 20:48:48 +05:30
contrapunctus 524ba9592f doc(README): add Liberapay widget, graph USP 2021-07-13 23:39:48 +05:30
contrapunctus 651aaa4ca1 doc(TODO): extend task-graph description 2021-07-09 20:19:40 +05:30
contrapunctus f2cd709242 doc(README): update installation instructions 2021-07-09 13:57:45 +05:30
contrapunctus 3ce8df0ba2 doc(TODO): add task for chronometrist-details 2021-07-09 08:20:31 +05:30
contrapunctus ecafd48a09 Merge branch 'dev' into doc 2021-07-09 07:52:54 +05:30
contrapunctus 73e6d98612 doc: bump versions 2021-07-08 03:17:55 +05:30
contrapunctus 58f0fdf0ba feat: add menu entry and keybinding for force-restart-timer 2021-07-08 03:15:47 +05:30
contrapunctus 103521be88 fix: remove auto-alignment of tags, tangle prompt
* I was almost always answering no to the prompt. Better to do it
  manually, then.
* The auto-alignment was resulting in garbage changes in each commit,
  probably because of an interaction with nameless-mode
2021-07-06 23:38:06 +05:30
contrapunctus 5620d87e31 doc(TODO): add note for interval editing UI 2021-07-06 23:38:00 +05:30
contrapunctus 5d42648c2a doc(TODO): add optimization idea - persist the hash table 2021-07-06 23:38:00 +05:30
contrapunctus 61eb311d0a feat(details): create new buffer for each range 2021-07-06 23:38:00 +05:30
contrapunctus d3ac1d4c18 doc(TODO): more thought on persisting hash table 2021-07-06 23:16:29 +05:30
contrapunctus 27b2c82635 doc(TODO): add note for interval editing UI 2021-07-06 03:46:18 +05:30
contrapunctus 7d51b46eac doc(TODO): add optimization idea - persist the hash table 2021-07-05 23:32:40 +05:30
contrapunctus 224a7fec3f cleanup: remove iso-date-to-ts, rename iso-timestamp-to-ts -> iso-to-ts 2021-07-03 10:59:22 +05:30
contrapunctus 26e3dd5c5d feat(details): implement timestamp ranges 2021-07-03 10:51:36 +05:30
contrapunctus 7976b53d91 fix(details): flatten key-values, remove keywords 2021-07-03 06:20:10 +05:30
contrapunctus 1931e88c85 feat(details): support begin/end in ranges 2021-07-03 00:23:56 +05:30
contrapunctus 883f813615 Merge branch 'doc' into details-view 2021-07-01 07:29:13 +05:30
contrapunctus ffa02ad3e9 doc(TODO): unify frontend ideas and add new ones 2021-07-01 07:28:06 +05:30
contrapunctus 0f68ab4f38 feat(details): add more menu items 2021-07-01 07:24:58 +05:30
contrapunctus f7f2349004 fix(details): set history variables 2021-07-01 07:24:26 +05:30
contrapunctus c5d4c33057 doc(details): update CHANGELOG 2021-06-28 07:51:10 +05:30
contrapunctus a04ca1fd28 fix(details): correct error 2021-06-28 07:50:19 +05:30
contrapunctus cc77b18797 fix(details): use read-from-minibuffer for filter 2021-06-28 06:53:35 +05:30
contrapunctus fd468a9ed5 feat(details): implement filter; refactor `rows` 2021-06-28 06:08:45 +05:30
contrapunctus 5adf3f3901 doc: update CHANGELOG 2021-06-27 22:28:11 +05:30
contrapunctus b774700450 feat(details): create prompt, menu 2021-06-27 22:24:37 +05:30
contrapunctus c4b94be941 feat(details): implement custom ranges 2021-06-27 22:24:28 +05:30
contrapunctus 9f8e455e01 feat(details): create state variables for custom ranges and filters 2021-06-27 20:57:38 +05:30
contrapunctus df8b7adf80 doc(TODO): add enhanced sparklines idea 2021-06-27 05:43:05 +05:30
contrapunctus 3f3567903b Merge branch 'dev' into doc 2021-06-27 03:38:47 +05:30
contrapunctus 3c9fc9b13a doc(TODO): add frontend macro idea 2021-06-27 03:38:04 +05:30
contrapunctus 2f8d136bf6 doc: shorten docstring 2021-06-26 09:09:11 +05:30
contrapunctus af9c463980 doc(TODO): add frontend idea 2021-06-22 13:18:07 +05:30
contrapunctus e7561cf0bb Merge branch 'doc' into dev 2021-06-22 11:50:48 +05:30
contrapunctus 37afe58cd9 fix: move menu after keymap 2021-06-22 06:14:27 +05:30
contrapunctus ece3c306a6 doc(TODO): align tags 2021-06-22 05:04:50 +05:30
contrapunctus bab42decf5 doc(TODO): add tasks, task tags and properties 2021-06-22 05:03:18 +05:30
contrapunctus 44b9094b29 Merge branch 'dev' into doc 2021-06-17 22:37:58 +05:30
contrapunctus 85d8a1c460 doc: align tags 2021-06-17 22:37:32 +05:30
contrapunctus e57a244519 feat: change menu text; add keybinding for details view 2021-06-17 22:32:50 +05:30
contrapunctus 8537fd214e feat: add key-values menu, commands to add key-values 2021-06-17 19:38:20 +05:30
contrapunctus a8e90d9661 doc(TODO): remove tasks noted elsewhere 2021-06-17 04:47:15 +05:30
contrapunctus 05c542ec32 doc(TODO): correct task heading 2021-06-17 00:45:34 +05:30
contrapunctus 0a313db966 doc(TODO): update status of UX task 2021-06-17 00:44:06 +05:30
contrapunctus 6f9e70e02d Merge branch 'dev' into doc 2021-06-17 00:42:47 +05:30
contrapunctus a0db18cb19 doc(TODO): update status of fixed bug 2021-06-17 00:29:11 +05:30
contrapunctus 15e1eb8580 feat(dir-locals): prompt before tangling 2021-06-17 00:25:58 +05:30
contrapunctus ad36dc0b2f doc(TODO): add feature idea 2021-06-17 00:10:13 +05:30
contrapunctus d47e5c8a80 doc(TODO): note bug 2021-06-16 18:43:23 +05:30
contrapunctus c32f70e405 doc: reset org-tags-column to default (-72) 2021-06-16 18:38:12 +05:30
contrapunctus c81c1029ca doc: org-align-all-tags before save 2021-06-16 18:37:45 +05:30
contrapunctus fd8aa22646 Merge branch 'print-keybinds-cleanup' into dev 2021-06-16 17:07:00 +05:30
contrapunctus d96ab209a6 doc: cleanup and correct; add link 2021-06-16 08:21:31 +05:30
contrapunctus 948abf6037 doc: add feature idea 2021-06-16 08:20:27 +05:30
contrapunctus f1225cb0fe doc: update changelog 2021-06-16 05:40:48 +05:30
contrapunctus e4b4dc277b feat(chronometrist-menu): add more entries; remove keybind printing 2021-06-16 05:35:55 +05:30
contrapunctus cf040a9c7d doc: describe goals and strategies 2021-06-15 22:20:45 +05:30
contrapunctus 7b024fe9ea Merge branch 'doc' into print-keybinds-cleanup 2021-06-15 22:05:53 +05:30
contrapunctus 50b2b03036 feat(chronometrist): create menu 2021-06-15 22:05:24 +05:30
contrapunctus d9dd465c5c doc: align Org tags 2021-06-15 20:21:50 +05:30
contrapunctus e5483c4b2a doc(TODO): add task 2021-06-15 16:27:47 +05:30
contrapunctus 8d7578e9f5 Merge branch 'dev' into doc 2021-06-15 16:26:59 +05:30
contrapunctus dc3692270d cleanup: create commands-to-print-alist 2021-06-15 10:14:19 +05:30
contrapunctus 2f26ef0a60 feat: add missing keybindings 2021-06-14 23:42:33 +05:30
contrapunctus 1ad4a92ebf cleanup: remove chronometrist-command-helper
It was neither shorter nor particularly easier to read... 🤔
2021-06-14 23:41:47 +05:30
contrapunctus 8141b6604c feat[restart/extend]: fail with message rather than silently 2021-06-14 15:01:50 +05:30
contrapunctus 71ddb725d8 fix(spark-range): write tests, fix error situations 2021-06-14 14:48:03 +05:30
contrapunctus 34fa77a173 refactor: use chronometrist-command-helper 2021-06-14 14:48:03 +05:30
contrapunctus 69f2b1d5c4 refactor: create chronometrist-command-helper 2021-06-14 14:45:16 +05:30
contrapunctus a3144050e4 doc(chronometrist-restart-task): clarify docstring 2021-06-12 21:12:55 +05:30
contrapunctus b0367023ef feat: new command chronometrist-extend-task 2021-06-12 21:12:37 +05:30
contrapunctus de41c158b5 feat: new command `chronometrist-restart-task` 2021-06-12 01:52:32 +05:30
contrapunctus 28e234fe61 doc: add backtrace for bug 2021-06-12 01:47:06 +05:30
contrapunctus 29c8939a7d rename: task-active? -> task-active-p 2021-06-12 01:25:01 +05:30
contrapunctus 9c0168a5e2 doc: move explanation closer to context 2021-06-12 01:16:02 +05:30
contrapunctus c8907f7dd7 doc(Explanation): add row and schema in terminology 2021-06-11 03:44:49 +05:30
contrapunctus 2280f9df73 doc(TODO): note new bug 2021-06-11 03:44:28 +05:30
contrapunctus 794c8382ce doc(TODO): correct errors and omissions; describe alternative syntax idea 2021-06-08 13:55:29 +05:30
contrapunctus b082b20dd3 Merge branch 'auto-tangle-test' into dev 2021-06-08 13:32:38 +05:30
contrapunctus ec42610dfa Merge branch 'doc' into dev 2021-06-08 13:32:06 +05:30
contrapunctus 6e0e6ccab3 doc(TODO): format-duration idea 2021-06-08 13:31:27 +05:30
contrapunctus 552972a55f doc: update changelog 2021-06-08 11:44:53 +05:30
contrapunctus 89afc959e9 Merge branch 'sparkline' into dev 2021-06-08 11:15:19 +05:30
contrapunctus aeaf5a0da4 feat: use .dir-locals.el for tangling 2021-06-08 03:48:33 +05:30
contrapunctus cfe1ab88a3 fix(spark): show only max duration for tasks with lone active day 2021-06-08 02:33:42 +05:30
contrapunctus 8a2c4c0b54 refactor: format-duration-long + create tests 2021-06-07 23:13:38 +05:30
contrapunctus 27fda20d2c feat(spark): show graph range 2021-06-07 23:12:50 +05:30
contrapunctus f42810ed06 doc: align headings 2021-06-07 21:59:57 +05:30
contrapunctus 72454a2bda doc(TODO): preserve line breaks 2021-06-07 19:49:54 +05:30
contrapunctus 4f65436677 feat(spark): display range 2021-06-07 19:48:16 +05:30
contrapunctus ec61367966 doc(TODO): extend extension macro idea 2021-06-07 19:47:29 +05:30
contrapunctus 19087b3f0f doc(TODO): edit and extend extension macro idea 2021-06-07 17:30:41 +05:30
contrapunctus ecf2b527b2 doc(TODO): add extension macro idea 2021-06-07 17:07:59 +05:30
contrapunctus 05525ab6ef doc: update notes 2021-06-06 16:55:09 +05:30
contrapunctus 343a08665f fix(details): do not display user keywords if using format string 2021-06-05 15:18:12 +05:30
contrapunctus 2dc8010056 cleanup(dir-locals): remove duplication, specify org-tags-column 2021-06-05 10:15:11 +05:30
contrapunctus 2f1ea5fd1f Merge branch 'dev' into doc 2021-06-05 10:06:17 +05:30
contrapunctus 0e22c725db doc(docstring): update argument name in docstring 2021-06-05 10:05:37 +05:30
contrapunctus 5c5b7fc5e7 fix: address linter complaints 2021-06-03 22:26:59 +05:30
contrapunctus ba8df3b78e fix: restore file-local compile commands
The .dir-local.el version documented stopped working...if it ever did work?
2021-06-03 20:23:29 +05:30
contrapunctus 2d26aa3fa6 doc(dev how-to): explain running after tangling end 2021-06-03 17:40:59 +05:30
contrapunctus 919c344003 doc: run org-align-all-tags 2021-06-03 17:25:06 +05:30
contrapunctus b404985f96 conf: move org-tags-column and org-html-head to .dir-locals.el 2021-06-03 17:23:10 +05:30
contrapunctus 0b605b4f98 doc: add setup approach thoughts, step for running makem.sh post-stage 2021-06-03 14:25:14 +05:30
contrapunctus aa142d798f doc: add author name 2021-06-03 14:02:22 +05:30
contrapunctus 85a14fe652 fix(dev): move file locals to dir-locals; remove adaptive-wrap suggestion 2021-06-03 11:09:11 +05:30
contrapunctus a09317e025 cleanup: remove tangled tests file 2021-06-03 04:19:49 +05:30
contrapunctus 3acd72686d fix(spark): fix package-lint issues 2021-06-03 04:19:27 +05:30
contrapunctus e1f6121230 fix: tangle tests to separate files 2021-06-03 02:12:04 +05:30
contrapunctus 753e8115cc Merge branch 'doc' into dev 2021-06-03 01:43:01 +05:30
contrapunctus 96699ceb3e doc: delete root headings, promote child headings 2021-06-03 01:28:17 +05:30
contrapunctus 529cd544d0 fix: address byte compilation warnings 2021-06-03 01:26:19 +05:30
contrapunctus 8541f58435 doc(dev/how-to): remove Git pre-commit hook instructions 2021-06-02 17:20:18 +05:30
contrapunctus fa171d7e00 doc(TODO): add documentation discoverability 2021-06-02 16:43:35 +05:30
contrapunctus 435cdadec3 doc(user manual): move section to how-to; add sections for literate-elisp 2021-06-02 13:42:38 +05:30
contrapunctus d54df33430 doc(user manual): correct custom_ids 2021-06-02 13:42:20 +05:30
contrapunctus 78ea66e614 doc(user manual): use new title/subtitle format 2021-06-02 13:41:30 +05:30
contrapunctus b1279e26f0 doc(user manual): remove root heading 2021-06-02 13:40:18 +05:30
contrapunctus 7e2d531bcf doc: remove root heading, demote all child headings 2021-06-02 03:56:27 +05:30
contrapunctus 026ce66f35 Merge branch 'dev' into doc 2021-06-02 03:56:05 +05:30
contrapunctus 93cb08c339 doc: use infojs 2021-06-02 03:07:06 +05:30
contrapunctus 3c8be625ce fix: on-save tangling setup; -let instead of -let* in spark 2021-06-01 18:37:29 +05:30
contrapunctus a6c02aaf80 fix(distribution): ship Elisp file; remove literate-elisp dependency 2021-06-01 17:35:01 +05:30
contrapunctus e9164ecca1 fix(distribution): ship Elisp file; remove literate-elisp dependency 2021-06-01 17:15:17 +05:30
contrapunctus 3a89ec926b tests: remove obsolete test files 2021-06-01 16:23:28 +05:30
contrapunctus 36d3d18769 doc: merge manual.org into chronometrist.org (again?) 2021-06-01 16:18:51 +05:30
contrapunctus ca5351cbf5 Merge branch 'doc' into dev 2021-06-01 14:13:39 +05:30
contrapunctus 3ad586ae70 doc: merge manual.org into chronometrist.org; remove root heading 2021-06-01 13:14:45 +05:30
contrapunctus bb2973405f cleanup: remove diary-view code (obsoleted by chronometrist-details) 2021-06-01 11:03:22 +05:30
contrapunctus 2b7fb133e7 docs: add TODO idea 2021-06-01 10:59:40 +05:30
contrapunctus f4317f027d docs: add "How-to guides for maintainers" 2021-05-31 19:25:19 +05:30
contrapunctus 0c4f7ac94c docs: update literate issues 2021-05-31 09:52:44 +05:30
contrapunctus 0f2946cd10 [CSS] use heading colors for links in headings 2021-05-31 09:22:07 +05:30
contrapunctus bdae4e0245 Merge branch 'org-manual' into dev 2021-05-31 01:26:25 +05:30
contrapunctus f6c0e9c858 Merge branch 'doc' into dev 2021-05-31 01:21:50 +05:30
contrapunctus 9aaac1bbde Update changelog 2021-05-31 01:19:43 +05:30
contrapunctus 0d9c7a2fd4 Run package-lint; update links 2021-05-31 01:07:33 +05:30
contrapunctus 0adac33f20 [spark] add header args and subtitle 2021-05-31 01:01:40 +05:30
contrapunctus 494cd25cda Merge branch 'dev' into doc 2021-05-31 00:32:06 +05:30
contrapunctus 476bc19cef Update subtitle 2021-05-31 00:31:26 +05:30
contrapunctus 30e0bf3be2 Merge branch 'doc' into dev 2021-05-31 00:00:46 +05:30
contrapunctus 99dba77ae6 Rename CSS file; remove shadow, fix overflow behavior 2021-05-30 23:59:27 +05:30
contrapunctus 199712bc11 Merge branch 'sparkline' into dev 2021-05-30 22:53:05 +05:30
contrapunctus 6a414194d9 Merge branch 'doc' into dev 2021-05-30 22:50:22 +05:30
contrapunctus 8b23bf244d Use our own CSS 2021-05-30 22:49:35 +05:30
contrapunctus 591f5dc9fa Add TEST keyword 2021-05-30 20:55:23 +05:30
contrapunctus 740bd239ab Define hooks using defcustom instead of defvar 2021-05-30 20:53:36 +05:30
contrapunctus 791ceaf1b3 Enable org-html-self-link-headlines 2021-05-30 20:41:04 +05:30
contrapunctus 3b111253c6 Update TODO 2021-05-30 20:40:43 +05:30
contrapunctus 09aee6e249 Restructure explanations to how-tos; start writing user reference 2021-05-30 17:00:06 +05:30
contrapunctus cd02dac5c7 Merge branch 'dev' into sparkline 2021-05-30 14:24:52 +05:30
contrapunctus 2d94898036 Moved TODO items to specific component 2021-05-30 14:24:32 +05:30
contrapunctus 8bf70bbf73 statistics-rows-internal - use loop instead of mapcar 2021-05-30 14:24:20 +05:30
contrapunctus f9fadd1595 TODO - update renamed definition; remove redundant explanation; ...
...add new use-case for unified task list idea.
2021-05-30 14:19:30 +05:30
contrapunctus 645877335c refresh-file - update docstring; clarify comments 2021-05-30 12:22:41 +05:30
contrapunctus d0d523db85 [literate] add footnote; fix footnote numbers 2021-05-30 12:20:48 +05:30
contrapunctus d47b06beb9 remove-from-task-list - add docstring, clarify local variable names 2021-05-30 12:18:04 +05:30
contrapunctus 218f4c6c0f [TODO] add task 2021-05-30 01:51:15 +05:30
contrapunctus 97a0699c65 [spark] remove extraneous copypasta code 2021-05-29 20:55:38 +05:30
contrapunctus 9ece3ae2ed [spark] add bootstrap .el 2021-05-29 17:44:51 +05:30
contrapunctus adc0261a7b [spark] write MVP 2021-05-29 17:15:05 +05:30
contrapunctus efe3042ad9 [spark] add skeleton Org file 2021-05-29 13:10:06 +05:30
contrapunctus f4a8006ac8 [chronometrist] make schema user customizable 2021-05-29 13:08:14 +05:30
contrapunctus 6272a642a4 [literate] add warnings about using newlines in tabulated-list-mode cells 2021-05-29 00:05:35 +05:30
contrapunctus 597cf30491 [TODO] clarify bug; update completed tasks; add wearable idea
add new ideas for sparkline extension
2021-05-28 19:18:37 +05:30
contrapunctus 7206299e51 Move plist-p and sexp-delete-list to Common; load Common first 2021-05-27 19:41:42 +05:30
contrapunctus 2f566b7778 [details] display key-values by default, increase column width
A possible misspelled variable (plist -> list) was fixed
2021-05-27 18:46:07 +05:30
contrapunctus b9c800232b [TODO] add sparkline idea 2021-05-27 05:20:21 +05:30
contrapunctus c859219d9d update tests 2021-05-27 05:15:23 +05:30
contrapunctus 18ffea1493 [statistics] fix error on start 2021-05-27 05:15:23 +05:30
contrapunctus 38b4c42430 Move roadmap items to TODO 2021-05-27 04:44:15 +05:30
contrapunctus 6124fa6f7b Correct link 2021-05-26 21:42:47 +05:30
contrapunctus 53a6315a20 Correct symlink 2021-05-26 14:53:43 +05:30
contrapunctus b4533235cf [manual] update tutorial and how-to guides
Remove v0.3 warning

Move mention of chronometrist-toggle-task-no-hooks from tutorial to
how-to; remove explanation

Merge duplicate text about buffer refresh

Change how-to headings to reflect how-to status
2021-05-26 13:51:39 +05:30
contrapunctus d2fdcfdd4e [manual] update project link 2021-05-26 13:28:41 +05:30
contrapunctus 3d996873ce Convert README.md to manual.org 2021-05-26 13:24:12 +05:30
contrapunctus 8c7c953ee2 [todo] clarify ideas 2021-05-26 13:12:00 +05:30
contrapunctus 5383493927 [todo] tick completed tasks 2021-05-26 13:11:22 +05:30
contrapunctus 422f389660 [key-values] clarify TODO 2021-05-26 13:09:43 +05:30
contrapunctus 9fa1e64fb3 [details] update nameless config 2021-05-26 13:07:27 +05:30
contrapunctus 4fdfe4d0c8 [details] implement key-value display
details-row-helper now expects a list of symbols or a full
plist (instead of just key-values). If the custom variables
controlling tag/key-value display are format strings,
details-row-helper will splice lists for display.
2021-05-26 11:10:30 +05:30
contrapunctus 8db801faeb details - implement key-value display 2021-05-25 22:45:24 +05:30
contrapunctus 2f02d6953a Move plist-p to section `common` 2021-05-25 22:44:40 +05:30
contrapunctus 9d7076a30b Update book text 2021-05-25 20:10:20 +05:30
contrapunctus a39c436378 details - clean up rows function 2021-05-25 18:53:15 +05:30
contrapunctus c0417dd255 details - fix error 2021-05-25 18:52:49 +05:30
contrapunctus 7f13f2c41b Finish leftover row/schema replacement 2021-05-25 16:23:52 +05:30
contrapunctus edf1fa8204 Use "row" instead of "entry" 2021-05-25 16:22:49 +05:30
contrapunctus 0cdb9fa4bb Use "row" instead of "entries" 2021-05-25 16:18:54 +05:30
contrapunctus d8c246eee0 details - create transformers 2021-05-25 16:10:32 +05:30
contrapunctus 5a8f8eb8bd Use "schema" instead of "table-format" 2021-05-25 16:06:13 +05:30
contrapunctus 7ee5961d55 format-duration - use make-string instead of string literals 2021-05-25 15:31:30 +05:30
contrapunctus 9d1017373d Rename format-time to format-duration 2021-05-25 15:26:56 +05:30
contrapunctus cefe297c08 Use make-string instead of string literal 2021-05-25 12:03:03 +05:30
contrapunctus e3c82a4ced Make function calls the first form in threaded macros for brevity 2021-05-25 12:03:03 +05:30
contrapunctus e6536bc9fd Use cl-loop instead of --map-indexed 2021-05-25 11:54:41 +05:30
contrapunctus 30b3d00f2c Replace chronometrist-common-clear-buffer with erase-buffer 2021-05-25 11:36:08 +05:30
contrapunctus 91b388acb3 chronometrist-details - make time format customizable 2021-05-25 09:58:24 +05:30
contrapunctus 29b6fd4c68 chronometrist-details - handle plists without :stop 2021-05-25 03:16:02 +05:30
contrapunctus f71ef8a995 chronometrist-details - fix "1 hour, 0 minutes" bug 2021-05-25 03:07:00 +05:30
contrapunctus 7b5f34000a chronometrist-details - fix duration-start/stop incongruence 2021-05-25 03:05:53 +05:30
contrapunctus 702dc3255d Implement MVP for chronometrist-details frontend 2021-05-25 01:14:48 +05:30
contrapunctus 3992cd953a Move plist-remove to chronometrist.org; create plist-key-values 2021-05-24 21:06:15 +05:30
contrapunctus d0d065a7d9 Bump versions 2021-05-18 15:21:19 +05:30
contrapunctus 2b62fb5606 Do not update buffer if file is being edited 2021-05-18 15:20:38 +05:30
contrapunctus 10326ba6e9 Rephrase "Why I wrote Chronometrist" 2021-05-18 15:19:07 +05:30
contrapunctus e7f2df1e55 Describe file format 2021-05-18 00:08:07 +05:30
contrapunctus ceb455288a [key-values] disable choice.el-based definitions 2021-05-14 13:57:32 +05:30
contrapunctus 35ddeaf5dd Drop choice.el; try to create unified completing-read prompt 2021-05-14 12:51:56 +05:30
contrapunctus 81b12053bc Bump versions 2021-05-07 14:53:16 +05:30
contrapunctus 397c4b4940 [literate] expand/reword explanations 2021-04-17 15:00:38 +05:30
contrapunctus b9f8ee7153 [literate] add example of midnight-spanning event 2021-04-14 17:14:24 +05:30
contrapunctus 961bc1cf49 [literate] use code markup 2021-04-14 17:14:07 +05:30
contrapunctus 9677a17a5c [literate] fix custom ID link syntax 2021-04-14 17:13:47 +05:30
contrapunctus 5619567d11 [literate] expand Explanations 2021-04-14 14:36:33 +05:30
contrapunctus 00759ec280 [literate] add Overview, remove Chronometrist heading 2021-04-12 20:15:36 +05:30
contrapunctus 3e12986086 [key-values] update links 2021-04-12 08:33:46 +05:30
contrapunctus accc32458f [literate] move tags and key-value section to key-values.org 2021-04-12 08:33:46 +05:30
contrapunctus e4bdeb41e3 [literate] add introduction; add more points to Explanation 2021-04-12 08:33:46 +05:30
contrapunctus 8885411c0e [literate] fix broken links, correct Org markup 2021-04-12 08:33:46 +05:30
contrapunctus 15b9d62906 [literate] remove outdated overview 2021-04-12 08:33:46 +05:30
contrapunctus 657e07e06e [literate] move time formats to chronometrist.org
* Move "Literate programming" to Explanation
* Make "The program" section for all code-related headings; remove
  "Code" section
2021-04-12 08:33:46 +05:30
contrapunctus d343119e41 [literate] move Explanation from manual to chronometrist.org 2021-04-12 08:33:46 +05:30
contrapunctus b72d83b0dd [literate] try to write some "why I wrote it"s 2021-04-12 08:33:46 +05:30
contrapunctus c33b3ae3fe [literate] add literate-elisp dependency; remove tangling setup 2021-04-12 08:33:46 +05:30
contrapunctus 129c7f7d78 test: make test.sexp before running tests
This does away with the whole issue of discovering the test.sexp file accurately.

It's also the point where I have increasing misgivings about shipping
the Org file itself. The tests, explanation docs, and now test
data...might be a "lot of" extra bytes to download, for something most
users have no need of.
2021-02-23 06:42:08 +05:30
contrapunctus f6082aeb1e [fix] void variable error in chronometrist-report-entries 2021-02-22 18:03:32 +05:30
contrapunctus 3ea6df7f20 [tests] moved tests to Org files
Upside - no more jumping between definitions and tests

Downside - if we ship the Org file, users will have to download some more bytes
2021-02-22 18:02:39 +05:30
contrapunctus 36e17fb4d7 [bugfix] incorrectly split plists in chronometrist-events 2021-02-22 17:59:16 +05:30
contrapunctus 8b09562aca [WIP] [bugfix] incorrectly split plists in chronometrist-events 2021-02-22 02:11:45 +05:30
contrapunctus 3fe5260972 Style and documentation changes 2021-02-21 22:09:00 +05:30
contrapunctus 83bbe0bd6a [data structures] add reset command 2021-02-18 15:52:26 +05:30
contrapunctus 620ea5433e [tests-ert] add file-hash test, make test.sexp path robust 2021-02-18 15:48:50 +05:30
contrapunctus 5854ec647d [chronometrist] add 2 TODO items 2021-02-17 10:56:42 +05:30
contrapunctus cc57d2d8ea [key-values] remove references to --skip-detail-prompts 2021-02-17 01:25:15 +05:30
contrapunctus f1632ef4b9 [key-values] add library headers 2021-02-16 15:35:32 +05:30
contrapunctus e4de5b2b2e [key-values] tag functions according to their purpose
Move TODO item, add commentary
2021-02-16 15:24:39 +05:30
contrapunctus bcaf3e4787 [doc] remove query skipping functions 2021-02-16 14:03:00 +05:30
contrapunctus 665705f1b5 [literate] tag functions according to their...function
Shorten file-empty-p

Merge `events->ts-pairs` and `ts-pairs->durations` into `events-to-durations` - they were only used in two places, both times with the results of the former being passed to the latter, and it didn't seem to me that they'd ever be used separately.

Rename all functions using the Scheme-like "foo->bar" naming scheme to the Elispy "foo-to-bar".

Move many docstrings out of source blocks into Org text and TODO items.

Remove unused chronometrist-report-date

Used `with` instead of `let` in chronometrist-report-entries

Use `loop` instead of `maphash` in `chronometrist-statistics-count-average-time-spent`
2021-02-16 13:11:30 +05:30
contrapunctus 4d4b9542f4 [doc] update README 2021-02-15 21:30:29 +05:30
contrapunctus f3e78c1fde [literate] Update other `require` calls 2021-02-15 13:40:35 +05:30
contrapunctus 7c81b3b937 [key-values] remove query-skipping definitions 2021-02-15 05:36:16 +05:30
contrapunctus 0f586a076b Update changelog 2021-02-15 05:31:42 +05:30
contrapunctus fbbfd1eeec [literate] checkdoc and package-lint changes 2021-02-15 05:31:34 +05:30
contrapunctus 3cbf9c1f10 [key-values] fix errors, implement unified-choice 2021-02-11 21:39:57 +05:30
contrapunctus dd17413f06 [literate migration] add missing function - plist-update 2021-02-11 17:29:14 +05:30
contrapunctus 3fb016cd75 [literate migration] add missing function 2021-02-11 15:48:26 +05:30
contrapunctus cd88cba389 [key-values] fix error, add missing dependency 2021-02-11 15:45:03 +05:30
contrapunctus 313c834e8b [key-values] Restructure, add TODO item 2021-02-11 15:30:21 +05:30
contrapunctus adbcf217a0 [key-values] Add single key prompts from hydra branch 2021-02-11 15:29:10 +05:30
contrapunctus 26ebe9243e [Literate migration] Convert key-values.el to literate Org program 2021-02-11 14:53:35 +05:30
contrapunctus 2b02967dc9 [Literate migration] Add missing function 2021-02-11 14:52:50 +05:30
contrapunctus d87997a0cd Remove dependencies on anaphora and s 2021-02-11 13:37:36 +05:30
contrapunctus 203838aa49 Merge branch 'dev' into literate 2021-02-11 12:08:50 +05:30
contrapunctus d1b42bbf0d Bump versions 2021-02-11 11:31:05 +05:30
contrapunctus fd376c7cf1 iso-date->ts - use pattern matching 2021-02-09 01:38:17 +05:30
contrapunctus aab2590e98 count-active-days - rewrite using cl-loop 2021-02-09 01:30:36 +05:30
contrapunctus 9f94250d94 Move task-list function to backend 2021-02-09 01:17:22 +05:30
contrapunctus 5cd9c9db47 "Hash table" -> "Data structures", move task list and related functions 2021-02-09 00:35:29 +05:30
contrapunctus 6a96baccdc Group frontends separately 2021-02-09 00:22:04 +05:30
contrapunctus 9ccaf1d41d Run tangle script only for this particular buffer 2021-02-09 00:21:27 +05:30
contrapunctus 9e1358dac9 Remove unnecessary variable whitespace-re 2021-02-09 00:20:59 +05:30
contrapunctus ae92a04164 Add namespace prefix to tangle command name and buffer 2021-02-09 00:05:42 +05:30
contrapunctus 4a7988e46d Remove blank lines 2021-02-09 00:04:48 +05:30
contrapunctus 333224498f Tag predicates 2021-02-09 00:04:27 +05:30
contrapunctus 39e66829e5 Update auto-tangle sed script 2021-02-08 14:40:28 +05:30
contrapunctus 0dd86c7c90 Clarify text 2021-02-08 14:36:59 +05:30
contrapunctus daaedc8d73 Move definitions from chronometrist-statistics.el to chronometrist.org 2021-02-08 12:46:29 +05:30
contrapunctus 075fb0323a Move definitions from plist-pp.el and report.el to chronometrist.org 2021-02-08 12:08:27 +05:30
contrapunctus 963e23e736 Restructure file to reflect conceptual data flow 2021-02-08 11:22:58 +05:30
contrapunctus 3bed912dcc Emit newlines in tangled source 2021-02-08 01:05:21 +05:30
contrapunctus 508998ebd1 Add commentary about file 2021-02-08 01:05:10 +05:30
contrapunctus 81fbf50100 Remove blank lines 2021-02-08 01:04:36 +05:30
contrapunctus 3a0b5fa045 Name headings more clearly 2021-02-08 01:03:56 +05:30
contrapunctus cb1293b6cd Move definitions from chronometrist-timer.el to chronometrist.org 2021-02-07 23:20:56 +05:30
contrapunctus f32f81844a Move definitions from chronometrist-time.el to chronometrist.org 2021-02-07 21:52:53 +05:30
contrapunctus d7ec8906f4 Move definitions from chronometrist-queries.el to chronometrist.org 2021-02-07 21:42:50 +05:30
contrapunctus 9bdf9fe47b Move definitions from chronometrist-migrate.el to chronometrist.org 2021-02-07 21:41:54 +05:30
contrapunctus 909ed00202 Move definitions from chronometrist-migrate.el to chronometrist.org 2021-02-07 21:39:24 +05:30
contrapunctus f597258d19 Move definitions from chronometrist-migrate.el to chronometrist.org 2021-02-07 21:25:04 +05:30
contrapunctus 4d523d821b TODO - add new ideas 2021-02-07 11:27:12 +05:30
contrapunctus 018fcdd858 Address some corner cases for task list updates 2021-02-05 17:11:28 +05:30
contrapunctus 9cfb4292ca Remove properties
Currently, headings represent components, and tags represent types and
concerns. If I need metadata which is "cross-cutting" (across
components) and doesn't suit tags, I'll use properties.
2021-02-05 16:25:02 +05:30
contrapunctus e11ac89359 Tangle chronometrist.org 2021-02-05 16:15:29 +05:30
contrapunctus bfa45eecf3 Enable nameless mode 2021-02-05 16:15:08 +05:30
contrapunctus f4a42df3cc Move definitions from chronometrist-sexp.el to chronometrist.org 2021-02-05 13:51:52 +05:30
contrapunctus 3c5a3c5556 Move definitions from chronometrist-events.el to chronometrist.org 2021-02-04 21:46:40 +05:30
contrapunctus edf63ee269 Move definitions from chronometrist-common.el to chronometrist.org 2021-02-04 21:31:25 +05:30
contrapunctus 5b308ea0a8 Add library headers and require calls 2021-02-04 16:13:19 +05:30
contrapunctus 9857c98127 Create subtree for definitions from main file 2021-02-04 16:03:00 +05:30
contrapunctus 863291c02c chronometrist.org - tag definitions 2021-02-04 08:58:52 +05:30
contrapunctus a7b70b5a3e chronometrist.el - move definitions to chronometrist.org 2021-02-04 08:58:21 +05:30
contrapunctus 8be5eb1ccb refresh-file - add some more checks 2021-02-03 18:02:40 +05:30
contrapunctus 1d4d5591a1 file-change-type - clean up comments and indentation 2021-02-03 17:55:05 +05:30
contrapunctus e054638175 loop-file - fix nil result on files without prop line 2021-02-03 17:54:16 +05:30
contrapunctus e259a04061 TODO - new optimization ideas 2021-02-03 17:06:41 +05:30
contrapunctus b8799ba260 TODO - add prop line query idea; update changelog 2021-02-03 17:06:38 +05:30
contrapunctus 55d4c6f2cb chronometrist-sexp-mode-hook - don't add auto-revert-mode by default 2021-02-03 17:03:28 +05:30
contrapunctus 5da1912f17 Refactor refresh-file; create chronometrist-sexp-mode for sexp file 2021-02-02 18:48:54 +05:30
contrapunctus 818d76c9e6 tests - remove duplication 2021-02-02 17:52:03 +05:30
contrapunctus b77a9bcc25 file-change-type - write tests 2021-02-02 17:41:54 +05:30
contrapunctus ffd3d01f6e README - update install instructions 2021-02-02 00:14:34 +05:30
contrapunctus 911cbcd1a9 Remove unused definitions 2021-01-31 19:42:20 +05:30
contrapunctus 09a5d127a1 (WIP) try to handle vanished/replaced file; unify partial update functions 2021-01-30 12:08:57 +05:30
contrapunctus 22ea8b4e99 Bump versions 2021-01-27 21:29:04 +05:30
contrapunctus 9a47f624bb Remove unnecessary refresh-file call
It is already called at the end of `chronometrist-in`
2021-01-27 21:26:38 +05:30
contrapunctus b8271bdd81 Update task list on file changes 2021-01-27 21:24:23 +05:30
contrapunctus c63ca7a552 Use pcase instead of cond 2021-01-27 13:46:43 +05:30
contrapunctus 3d898e6dd8 Remove unused variable 2021-01-27 13:46:02 +05:30
contrapunctus 0bcd711bdc Fix error when running emacs with `-q` 2021-01-27 12:41:01 +05:30
contrapunctus 81b1f4bb7f Fix incorrect order of tag/key/value history 2021-01-27 12:40:10 +05:30
contrapunctus f664297378 Update versions and changelog 2021-01-19 13:23:24 +05:30
contrapunctus 125d0ed289 Merge branch 'dev' 2021-01-19 13:21:25 +05:30
contrapunctus e36730b4ea Remove json.el dependency 2021-01-19 13:20:38 +05:30
contrapunctus cddeeb0d32 Make query function signatures more consistent 2021-01-09 12:28:28 +05:30
contrapunctus b3dbd21771 Remove chronometrist-tasks-from-table
It was unused, and benchmark.el reports it to be slower than the expression we are using in chronometrist-refresh-file
2021-01-09 10:19:57 +05:30
contrapunctus 5f46b51fef (WIP) chronometrist-loop-file - add edebug spec 2021-01-09 09:42:43 +05:30
contrapunctus 9535e11370 Remove excess whitespace 2021-01-09 09:41:31 +05:30
contrapunctus 3fa9cb40fb TODO - change "event" to "interval" 2021-01-09 09:41:09 +05:30
contrapunctus 95c46c25e1 TODO - clarify bug and feature descriptions 2021-01-09 09:39:25 +05:30
contrapunctus ad89106754 TODO - refine per-task prompt idea 2021-01-09 09:35:30 +05:30
contrapunctus cef185de5c Bump versions, update CHANGELOG 2021-01-07 03:17:30 +05:30
contrapunctus 0dc3db8f24 Merge branch 'dev' 2021-01-07 03:14:26 +05:30
contrapunctus 108cd37644 README - fix link 2021-01-06 04:04:39 +05:30
contrapunctus a7bb7dbcca Remove chronometrist-tags-history-replace-last
It was buggy, and also no longer necessary, since tag/key/value
history isn't expensive enough (especially after changing tag and key
history generation to be per-task) to require partial updates.
2021-01-06 00:20:01 +05:30
contrapunctus 1b13db776b Comment out debugging `message` call; bump versions 2021-01-04 10:56:36 +05:30
contrapunctus e95ad1d6e2 Bump versions, update changelog 2021-01-04 08:55:52 +05:30
contrapunctus ea6c379204 Remove unused function chronometrist-task-list-add 2021-01-04 08:53:11 +05:30
contrapunctus 0189c89a9b Remove dependency for transformers 2021-01-04 08:44:48 +05:30
contrapunctus 5b55002d5c Update manual, CHANGELOG, and TODO 2021-01-04 02:26:40 +05:30
contrapunctus 7204050f7f Create loop wrapper macro, remove mapcar-file/mapc-file 2021-01-04 00:43:48 +05:30
contrapunctus 682d801457 Update file-hash call sites, reinstate number/marker arguments 2021-01-03 22:08:29 +05:30
contrapunctus 4b451004a5 Remove unused hash of last sexp 2021-01-03 13:31:13 +05:30
contrapunctus f73169dcb7 Remove plist from hash table when :remove detected 2021-01-03 13:13:48 +05:30
contrapunctus d3095d4377 Detect removal of last s-expression 2021-01-03 13:13:35 +05:30
contrapunctus c7131e69e6 Correct tag generation behavior/max eval depth error 2021-01-03 08:51:07 +05:30
contrapunctus 440ff4ee59 Update CHANGELOG 2021-01-03 00:01:29 +05:30
contrapunctus ba27ee8a2e Clean up code 2021-01-02 23:34:50 +05:30
contrapunctus aacc109b88 Update file hashes unconditionally 2021-01-02 23:29:39 +05:30
contrapunctus 5fce2aae60 Fix undefined variable errors; parse full file if hashes not defined 2021-01-02 23:12:47 +05:30
contrapunctus 8ddcabb2c2 Use file change type for partial updates of memory layer
What was a few-second-long wait is now barely perceptible.
Optimization successful, I guess, barring any bugs.
2021-01-02 23:06:58 +05:30
contrapunctus 4288b10a2c Fix file change type code 2021-01-02 22:11:33 +05:30
contrapunctus a974c6a905 Merge branch 'dev' into optimize-by-hashing 2021-01-02 18:36:26 +05:30
contrapunctus 6c7f072371 manual - list other optimization strategies; update TODO 2021-01-02 18:26:45 +05:30
contrapunctus ebbed71fcb Generate value history from file, just before prompt 2021-01-02 12:41:54 +05:30
contrapunctus c606f0d118 Reverse file reading order, for future implementation of limits 2021-01-02 12:00:32 +05:30
contrapunctus 796626904e Fill key history per-task, before prompt 2021-01-01 00:58:07 +05:30
contrapunctus 3d9debf989 Fill tag history per-task, before prompt 2020-12-31 06:06:14 +05:30
contrapunctus 34779e9482 Document optimization idea 2020-12-31 00:21:19 +05:30
contrapunctus 59ba3346d4 Rewrite tasks-from-table to use cl-loop
This also seems to exhibit better performance.
2020-12-31 00:06:21 +05:30
contrapunctus 38204780cb Fix error, add (a little) documentation for hashing 2020-12-30 23:53:35 +05:30
contrapunctus e5fc25549e Fix timer error (decouple individual buffer timer functions) 2020-12-30 23:27:30 +05:30
contrapunctus 1644bdcff9 TODO - add implementation ideas for markup values 2020-12-23 11:36:06 +05:30
contrapunctus 81d42e40f2 TODO - add bug 2020-12-23 01:02:46 +05:30
contrapunctus 037ae967e2 manual - reindent content 2020-12-23 01:02:40 +05:30
contrapunctus eaf0d7feec Merge branch 'master' into dev 2020-12-22 20:45:41 +05:30
contrapunctus 551a750cae Bump versions and changelog, add idea for customizable field widths 2020-12-22 20:30:09 +05:30
contrapunctus f83b9a783f Use text properties to get the task at point 2020-12-22 20:29:22 +05:30
contrapunctus ff1136ab9a TODO - new idea for key-value input 2020-12-21 21:06:25 +05:30
contrapunctus ea16013110 changelog - I doubt users are interested in knowing about this 2020-12-21 20:03:56 +05:30
contrapunctus 2af6b1207d TODO - clarify need behind feature; add other implementation ideas 2020-11-26 02:00:07 +05:30
contrapunctus e5281ffe52 Remove `require` forms for deleted custom definition files 2020-11-26 01:59:49 +05:30
contrapunctus f95add3456 Move new features to their own section 2020-11-23 21:16:06 +05:30
contrapunctus 7c03b3028c TODO - add idea for input frontends, and :hide keyword for task list 2020-11-23 17:24:08 +05:30
contrapunctus 7e9341d6da Remove dedicated files for custom variables 2020-11-18 14:20:06 +05:30
contrapunctus 309d651dc9 Fix infinite loop in `chronometrist-report` triggered by non-English locale 2020-11-17 08:28:18 +05:30
contrapunctus 4413e93b76 TODO - clarify task list idea 2020-11-16 02:02:21 +05:30
contrapunctus 27368f992c sexp - use plist-pp as default 2020-11-11 01:49:55 +05:30
contrapunctus d1fd341b67 TODO - tick off plist-pp task 2020-11-08 18:10:16 +05:30
contrapunctus 6a9577d0dd Update CHANGELOG for plist-pp changes 2020-11-07 13:10:19 +05:30
contrapunctus 70a276d3c2 TODO - add sublist depth idea 2020-11-07 13:03:24 +05:30
contrapunctus 5ba00f8520 Remove old commentary 2020-11-07 13:02:50 +05:30
contrapunctus 010b828dbc Fix lone paren; add ERT tests 2020-11-07 10:53:44 +05:30
contrapunctus 8b16cfbe1f Fix indentation of plist keyword after a plist as value; update tests 2020-11-06 14:06:26 +05:30
contrapunctus 4b6e77eff5 Handle alists containing a mix of pairs and other values 2020-11-05 14:30:08 +05:30
contrapunctus 418abd55fe Handle cons pairs as plist values 2020-11-05 14:29:52 +05:30
contrapunctus e5cea8b3dd Remove unused arguments, change keyword argument to optional argument 2020-11-05 14:18:31 +05:30
contrapunctus 4952999299 Fix "end of buffer" error 2020-11-05 14:17:58 +05:30
contrapunctus aaa67d3559 Remove unnecessary `read`ing and newline insertion 2020-11-05 13:48:10 +05:30
contrapunctus 8fb1e0d7e7 Fix recursive indentation
Finally! It finally handles the test case perfectly.
2020-11-05 13:15:17 +05:30
contrapunctus 736eab7ae4 Remove old definition of chronometrist-plist-pp-buffer 2020-11-04 16:34:50 +05:30
contrapunctus 09523b010a (Almost) fix indenting of nested plists and alists 2020-11-04 16:33:06 +05:30
contrapunctus c56e4d0d35 Add super-function, and function to handle plists
alist-p is now robust against symbol values.
2020-11-03 22:14:51 +05:30
contrapunctus d5f44ebaf1 Rethink, and start working on a new approach 2020-11-03 18:17:14 +05:30
contrapunctus 0b583d6f4f Left-indent sublists in some more situations
...but it's still hosed.
2020-11-03 18:16:52 +05:30
contrapunctus c8dbc4300d Remove dash dependency 2020-11-03 10:17:17 +05:30
contrapunctus f1e33c950c Make left indentation work in some situations 2020-11-03 10:01:07 +05:30
contrapunctus bacf3218af Detect alists; prevent unbounded recursion
longest-keyword-length was rewritten to use cl-loop. Groundwork for
left-indenting sub-lists is done, but it's still broken at this point.
2020-11-03 00:38:02 +05:30
contrapunctus b76d118ee4 plist-pp - use `read` instead of regexps for detecting keywords
We also reinstate insertion of newlines - this code was meant to be
run on an expression in a single line, as output by (format "%S" ...)
2020-11-02 16:05:59 +05:30
contrapunctus 20d4fa4caa plist-pp - groundwork for handling of sub-plist(s) as values 2020-11-01 19:24:32 +05:30
contrapunctus 593b69556e plist-pp - clean up code 2020-11-01 19:23:57 +05:30
contrapunctus 334cb0b19c TODO - add delayed-history optimization idea 2020-11-01 18:37:30 +05:30
contrapunctus 31a51dd03b Discover bug 2020-11-01 17:25:24 +05:30
contrapunctus 551a9825b2 file-change-type - make code clearer 2020-11-01 16:58:49 +05:30
contrapunctus 1d3cfec71e TODO - add minute-precision idea 2020-10-20 12:12:53 +05:30
contrapunctus f4a5d0820d Use sexp-pretty-print-function in kv-add 2020-10-20 09:50:12 +05:30
contrapunctus 9d74ce744b TODO - add sub-plist completion idea 2020-10-20 09:48:37 +05:30
contrapunctus f41fa27d0a TODO - add task list idea 2020-10-10 17:30:37 +05:30
contrapunctus 04d7cce16b Update changelog 2020-09-24 13:41:25 +05:30
contrapunctus 46a7cc5e8d report - delete-other-windows is undesirable with multiple windows 2020-09-24 13:37:41 +05:30
contrapunctus 4eb0797a5a TODO - resolve to continue working on plist-pp 2020-09-13 15:22:30 +05:30
contrapunctus 8fa15f9aad Use ppp or pp for pretty-printing; add custom variable 2020-09-13 02:46:26 +05:30
contrapunctus 99c9e4001d sexp.el - make code denser 2020-08-31 14:38:20 +05:30
contrapunctus 18d5289072 file-change-type - use pattern matching 2020-08-31 11:42:53 +05:30
contrapunctus 2568380d19 file-change-type - handle shrinking file, correct :append logic 2020-08-31 11:36:38 +05:30
contrapunctus 7e292717e2 Add functions to detect type of changes made to chronometrist-file 2020-08-31 01:23:43 +05:30
52 changed files with 18209 additions and 4622 deletions

View File

@ -1,16 +1,26 @@
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")
((emacs-lisp-mode . ((nameless-aliases . (("cc" . "chronometrist-common")
("cd" . "chronometrist-diary")
("ce" . "chronometrist-events")
("ck" . "chronometrist-kv")
("cm" . "chronometrist-migrate")
("cp" . "chronometrist-plist-pp")
("cr" . "chronometrist-report")
("cs" . "chronometrist-statistics")
("cx" . "chronometrist-sexp")
("c" . "chronometrist")))
(outline-regexp . ";;;+ ")))
(dired-mode . ((dired-omit-mode . t)
(dired-omit-extensions . (".html" ".texi")))))
;; for some reason, setting `nameless-current-name' to "chronometrist"
;; makes all aliases not take effect - probably specific to Org
;; literate programs
((nil . ((nameless-aliases . (("cb" . "chronometrist-backend")
("cc" . "chronometrist-common")
("cd" . "chronometrist-details")
("ce" . "chronometrist-events")
("ck" . "chronometrist-key-values")
("cm" . "chronometrist-migrate")
("cp" . "chronometrist-plist-pp")
("cr" . "chronometrist-report")
("cs" . "chronometrist-statistics")
("cx" . "chronometrist-sexp")
("c" . "chronometrist")))
(sentence-end-double-space . t)))
(org-mode
. ((org-html-self-link-headlines . t)
(eval . (org-indent-mode))
(org-html-head
. (concat "<link rel=\"stylesheet\" "
"type=\"text/css\" "
"href=\"../org-doom-molokai.css\" />"))
(org-babel-tangle-use-relative-file-links . t))))

View File

@ -4,12 +4,119 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [unreleased]
## unreleased
### Added
* New hooks - `chronometrist-mode-hook`, `chronometrist-list-format-transformers`, `chronometrist-entry-transformers`. chronometrist no longer needs to know about extensions.
1. `chronometrist-third`, an extension to add support for the [Third Time](https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work) system.
2. New custom variable `chronometrist-key-value-preset-alist`, to define completion suggestions in advance.
3. New custom variable `chronometrist-key-value-use-database-history`, to control whether database history is used for key-value suggestions.
## [0.10.0] - 2022-02-15
### Changed
1. The value of `chronometrist-file` must now be a file path _without extension._ Please update your configurations.
2. The existing file format used by Chronometrist is now called the `plist` format.
3. The extension for files in the `plist` format is now `.plist`. Update the extension of your file to use it with the `plist` backend.
### Added
1. Multiple backend support - new custom variable `chronometrist-active-backend` to determine active backend, new command `chronometrist-switch-backend` to temporarily select a backend (with completion).
2. New `plist-group` backend, reducing time taken in startup and after changes to the file.
3. Unified migration interface with command `chronometrist-migrate`.
4. New custom variable `chronometrist-task-list`, to add/hide tasks without modifying the database. Setting it also disables generation of the task list from the database, speeding up many operations.
5. New command `chronometrist-discard-active`, to discard the active interval.
6. Debug logging messages - to view them, set `chronometrist-debug-enable`.
### Fixed
1. Code to detect the type of change made to the file has been rewritten, hopefully fixing some uncommon `read` errors and `args out of range` errors.
### Deprecated
1. The plist backend is deprecated and may be removed in a future release. The `plist-group` backend is more performant and extensible - please use `chronometrist-migrate` to convert your data to the `plist-group` backend.
## [0.9.0] - 2021-07-08
### Added
1. New commands `chronometrist-restart-task`, `chronometrist-extend-task`
2. Menus for `chronometrist`, `chronometrist-key-values`, and `chronometrist-details`
3. Custom ranges and filters for `chronometrist-details`. See command `chronometrist-details-set-range` and `chronometrist-details-set-filter`.
### Changed
4. Display graph ranges in `chronometrist-spark` column
5. `chronometrist-tags-add` and `chronometrist-key-values-unified-prompt` now also work interactively.
## [0.8.1] - 2021-06-01
### Changed
1. Distribute a tangled Elisp file as well as the literate program. Autoloads now work as usual.
## [0.8.0] - 2021-05-31
### Added
1. New frontend `chronometrist-details`, to display time intervals in a tabular format.
2. New extension `chronometrist-spark`, to display sparklines in the chronometrist buffer.
3. `chronometrist-schema`, custom variable used to define `tabulated-list-format`
### Changed
4. Renames -
* `chronometrist-list-format-transformers``chronometrist-schema-transformers`
* `chronometrist-entry-transformers``chronometrist-row-transformers`
5. Hooks now use `defcustom` instead of `defvar`.
### Fixed
6. error when launching `chronometrist-statistics`
## [0.7.2] - 2021-05-18
### Changed
* If `chronometrist-file` is being edited, `chronometrist-timer` will not refresh the buffer or run `chronometrist-timer-hook`, preventing errors resulting from `read`ing of incomplete data and easing editing of the file.
## [0.7.1] - 2021-05-14
### Added
* key-values - `chronometrist-key-values-unified-prompt`, which uses `completing-read`. A more streamlined way to enter key-values, for those comfortable with entering/editing s-expressions directly.
### Removed
* key-values - all `choice.el`-based prompts have been removed.
## [0.7.0] - 2021-05-07
### Added
* Single key prompts for key-values - `chronometrist-tag-choice`, and `chronometrist-key-values-unified-choice`, with more to come.
* `chronometrist-reset`, to clear all internal state
### Changed
* `chronometrist` is now a literate Org program.
### Removed
* `chronometrist-skip-query-prompt`, `chronometrist-skip-query-reset`, and `chronometrist--skip-detail-prompts` - these are covered by the new single key prompt functions
## [0.6.5] - 2021-02-11
### Added
* Major mode (syntax highlighting, hook) for Chronometrist s-expression files, derived from emacs-lisp-mode
### Fixed
* Bug in updating the task list in some cases of the latest interval being modified.
## [0.6.4] - 2021-01-27
### Fixed
* Incorrect order of tag/key/value history
* Error when running emacs with `-q`
* `chronometrist` buffer not being updated when a new task is added or the sole interval for a task is removed.
## [0.6.3] - 2021-01-19
### Fixed
* Error from missing `require` form
## [0.6.2] - 2021-01-07
### Fixed
* Incorrect tag history in some situations
## [0.6.1] - 2021-01-04
### Fixed
* Removed a debugging `message` call which was accidentally left in
## [0.6.0] - 2021-01-04
### Added
* New hooks - `chronometrist-mode-hook`, `chronometrist-list-format-transformers`, `chronometrist-entry-transformers`, and `chronometrist-timer-hook`.
* New custom variable `chronometrist-sexp-pretty-print-function`
### Changed
* `chronometrist-plist-pp` now indents recursively.
### Optimization
* Refresh time after changing `chronometrist-file` is now near-instant for the most common situations - an expression being added to the end of the file, or the last expression in the file being changed or removed. This works for changes made by the user as well as changes made by Chronometrist (or other) commands.
* Tag, key, and value histories are generated before the user is prompted, rather than each time the file is saved.
### Fixed
* Remove quotes from key-value prompt in quit keybindings
* Lisp objects being stored as un`read`able strings in `chronometrist-value-history`, resulting in value suggestions not matching user input.
* `chronometrist-report` no longer calls `delete-other-windows`; use `chronometrist-report-mode-hook` if it is desired.
* Fixed infinite loop in `chronometrist-report` triggered by non-English locales.
## [0.5.6] - 2020-12-22
### Fixed
* Bug triggered by clocking out of a task with a name longer than the field width, caused by hacky way of determining the task at point. (issue #2)
## [0.5.5] - 2020-09-02
### Added

25
Cask
View File

@ -1,25 +0,0 @@
(source gnu)
(source melpa)
(package
"Chronometrist"
"0.5.4"
"A time tracker for Emacs with a nice interface")
(depends-on "cl")
(depends-on "dash" "2.16.0")
(depends-on "seq" "2.20")
(depends-on "s" "1.12.0")
(depends-on "ts" "0.2")
(depends-on "anaphora" "1.0.4")
(depends-on "call-transformers" "0.0.1")
(files "elisp/*.el")
(development
(depends-on "f")
(depends-on "ecukes")
(depends-on "ert-runner")
(depends-on "el-mock")
(depends-on "elsa")
(depends-on "buttercup"))

82
Makefile Normal file
View File

@ -0,0 +1,82 @@
.phony: all setup tangle compile lint
all: clean-elc manual.md setup tangle compile lint
setup:
emacs --batch --eval="(package-initialize)" \
--eval="(mapcar #'package-install '(indent-lint package-lint relint))"
manual.md:
emacs -q -Q --batch --eval="(require 'ox-md)" \
"manual.org" -f 'org-md-export-to-markdown'
# No -q or -Q without ORG_PATH - if the user has a newer version of
# Org, we want to use it.
tangle:
cd elisp/ && \
emacs --batch \
--eval="(progn (package-initialize) (require 'ob-tangle))" \
--eval='(org-babel-tangle-file "chronometrist.org")' \
--eval='(org-babel-tangle-file "chronometrist-key-values.org")' \
--eval='(org-babel-tangle-file "chronometrist-spark.org")' \
--eval='(org-babel-tangle-file "chronometrist-third.org")' \
--eval='(org-babel-tangle-file "chronometrist-sqlite.org")' ; \
cd ..
compile: tangle
cd elisp/ && \
emacs --batch \
--eval="(progn (package-initialize) (require 'dash) (require 'ts))" \
--eval='(byte-compile-file "chronometrist.el")' \
--eval='(byte-compile-file "chronometrist-key-values.el")' \
--eval='(byte-compile-file "chronometrist-spark.el")' \
--eval='(byte-compile-file "chronometrist-third.el")' \
--eval='(byte-compile-file "chronometrist-sqlite.el")' ; \
cd ..
lint-check-declare: tangle
cd elisp/ && \
emacs -q --batch \
--eval='(check-declare-file "chronometrist.el")' \
--eval='(check-declare-file "chronometrist-key-values.el")' \
--eval='(check-declare-file "chronometrist-spark.el")' \
--eval='(check-declare-file "chronometrist-third.el")' \
--eval='(check-declare-file "chronometrist-sqlite.el")' ; \
cd ..
lint-checkdoc: tangle
cd elisp/ && \
emacs -q -Q --batch \
--eval='(checkdoc-file "chronometrist.el")' \
--eval='(checkdoc-file "chronometrist-key-values.el")' \
--eval='(checkdoc-file "chronometrist-spark.el")' \
--eval='(checkdoc-file "chronometrist-third.el")' \
--eval='(checkdoc-file "chronometrist-sqlite.el")' ; \
cd ..
lint-package-lint: setup tangle
cd elisp/ && \
emacs --batch \
--eval="(progn (package-initialize) (require 'dash) (require 'ts) (require 'package-lint))" \
-f 'package-lint-batch-and-exit' chronometrist.el \
-f 'package-lint-batch-and-exit' chronometrist-key-values.el \
-f 'package-lint-batch-and-exit' chronometrist-spark.el \
-f 'package-lint-batch-and-exit' chronometrist-third.el \
-f 'package-lint-batch-and-exit' chronometrist-sqlite.el ; \
cd ..
lint-relint: setup tangle
cd elisp/ && \
emacs --batch \
--eval="(progn (package-initialize) (require 'relint))" \
--eval='(relint-file "chronometrist.el")' \
--eval='(relint-file "chronometrist-key-values.el")' \
--eval='(relint-file "chronometrist-spark.el")' \
--eval='(relint-file "chronometrist-third.el")' \
--eval='(relint-file "chronometrist-sqlite.el")' ; \
cd ..
lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint
clean-elc:
rm elisp/*.elc

213
README.md
View File

@ -1,213 +0,0 @@
[![MELPA](https://melpa.org/packages/chronometrist-badge.svg)](https://melpa.org/#/chronometrist)
# chronometrist
A time tracker in Emacs with a nice interface
Largely modelled after the Android application, [A Time Tracker](https://github.com/netmackan/ATimeTracker)
* Benefits
1. Extremely simple and efficient to use
2. Displays useful information about your time usage
3. Support for both mouse and keyboard
4. Human errors in tracking are easily fixed by editing a plain text file
5. Hooks to let you perform arbitrary actions when starting/stopping tasks
* Limitations
1. No support (yet) for adding a task without clocking into it.
2. No support for concurrent tasks.
**IMPORTANT: with version v0.3, chronometrist no longer uses timeclock as a dependency and will use its own s-expression-based backend. A command to migrate the timeclock-file, `chronometrist-migrate-timelog-file->sexp-file`, is provided.**
## Comparisons
### timeclock.el
Compared to timeclock.el, Chronometrist
* stores data in an s-expression format rather than a line-based one
* supports attaching tags and arbitrary key-values to time intervals
* has commands to shows useful summaries
* has more hooks (see [Hooks](#Hooks))
### Org time tracking
Chronometrist and Org time tracking seem to be equivalent in terms of capabilities, approaching the same ends through different means.
* Chronometrist doesn't have a mode line indicator at the moment. (planned)
* Chronometrist doesn't have Org's sophisticated querying facilities. (an SQLite backend is planned)
* Org does so many things that keybindings seem to necessarily get longer. Chronometrist has far fewer commands than Org, so most of the keybindings are single keys, without modifiers.
* Chronometrist's UI makes keybindings discoverable - they are displayed in the buffers themselves.
* Chronometrist's UI is cleaner, since the storage is separate from the display. It doesn't show tasks as trees like Org, but it uses tags and key-values to achieve that. Additionally, navigating a flat list takes fewer user operations than navigating a tree.
* Chronometrist data is just s-expressions (plists), and may be easier to parse than a complex text format with numerous use-cases.
## Installation
You can get `chronometrist` from https://github.com/contrapunctus-1/chronometrist
`chronometrist` requires
* Emacs v26 or higher
* [dash.el](https://github.com/magnars/dash.el)
* [s.el](https://github.com/magnars/s.el)
* [ts.el](https://github.com/alphapapa/ts.el)
Add the "elisp/" subdirectory to your load-path, and `(require 'chronometrist)`.
## Usage
In the buffers created by the following three commands, you can press `l` (`chronometrist-open-log`) to view/edit your `chronometrist-file`, which by default is `~/.emacs.d/chronometrist.sexp`.
All of these commands will kill their buffer when run again with the buffer visible, so the keys you bind them to behave as a toggle.
### chronometrist
Run `M-x chronometrist` to see your projects, the time you spent on them today, which one is active, and the total time clocked today.
Hit `RET` (`chronometrist-toggle-task`) on a project to start tracking time for it. If it's already clocked in, it will be clocked out. This command runs some [hooks](#Hooks), which are useful for a wide range of functionality (see [Adding more information](#adding-more-information-experimental) below). In some cases, you may want to skip running the hooks - use `M-RET` (`chronometrist-toggle-task-no-hooks`) to do that.
You can also hit `<numeric prefix> RET` anywhere in the buffer to toggle the corresponding project, e.g. `C-1 RET` will toggle the project with index 1.
Press `r` to see a weekly report (see `chronometrist-report`)
`chronometrist` keeps itself updated via an idle timer - no need to frequently press `g` to update.
### chronometrist-report
Run `M-x chronometrist-report` (or `chronometrist` with a prefix argument of 1, or press `r` in the `chronometrist` buffer) to see a weekly report.
Press `b` to look at past weeks, and `f` for future weeks.
`chronometrist-report` keeps itself updated via an idle timer - no pressing `g` to update.
### chronometrist-statistics
Run `M-x chronometrist-statistics` (or `chronometrist` with a prefix argument of 2) to view statistics.
Press `b` to look at past time ranges, and `f` for future ones.
### Attaching tags and key values
Part of the reason Chronometrist stores time intervals as property lists is to allow you to add tags and arbitrary key-values to them.
#### Tags
To be prompted for tags, add `chronometrist-tags-add` to any hook except `chronometrist-before-in-functions`, based on your preference (see [Hooks](#Hooks)). 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`, or skip the prompt just this once by pressing `M-RET` (`chronometrist-toggle-task-no-hooks`).
#### Key-value pairs
Similarly, to be prompted for key-values, add `chronometrist-kv-add` to any hook except `chronometrist-before-in-functions`. 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.
#### Quick re-use of last-used tags and/or key-values
Add `chronometrist-skip-query-prompt` to the hook(s) containing `chronometrist-tags-add`/`chronometrist-kv-add`, _before_ these functions, and `chronometrist-skip-query-reset` _after_ them -
```elisp
(setq chronometrist-before-out-functions '(chronometrist-skip-query-prompt
chronometrist-tags-add
chronometrist-kv-add
chronometrist-skip-query-reset))
```
### Prompt when exiting Emacs
If you wish to be prompted when you exit Emacs while tracking time, you can use this -
`(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)`
### Time goals/targets
If you wish you could define time goals for some tasks, and have Chronometrist notify you when you're approaching the goal, completing it, or exceeding it, check out the extension [chronometrist-goal.el](https://github.com/contrapunctus-1/chronometrist-goal/).
## Customization
See the Customize groups `chronometrist` and `chronometrist-report` for variables intended to be user-customizable.
### Hooks
Chronometrist currently has four hooks -
1. `chronometrist-before-in-functions`
2. `chronometrist-after-in-functions`
3. `chronometrist-before-out-functions`
4. `chronometrist-after-out-functions`
As their names suggest, these are 'abnormal' hooks, i.e. the functions they contain must accept arguments. In this case, each function must accept exactly one argument - the name of the project which is being started or stopped, as a string.
`chronometrist-before-out-functions` is different from the others in that it runs until failure - the task will be clocked out only if all functions in this hook return `t`.
### Opening certain files when you start a task
An idea from the author's own init -
```elisp
(defun my-start-project (project)
(pcase project
("Guitar"
(find-file-other-window "~/repertoire.org"))
;; ...
))
(add-hook 'chronometrist-before-in-functions 'my-start-project)
```
### Reminding you to commit your changes
Another one, prompting the user if they have uncommitted changes in a git repository (assuming they use [Magit](https://magit.vc/)) -
```elisp
(autoload 'magit-anything-modified-p "magit")
(defun my-commit-prompt ()
"Prompt user if `default-directory' is a dirty Git repository.
Return t if the user answers yes, if the repository is clean, or
if there is no Git repository.
Return nil (and run `magit-status') if the user answers no."
(cond ((not (magit-anything-modified-p)) t)
((yes-or-no-p
(format "You have uncommitted changes in %S. Really clock out? "
default-directory)) t)
(t (magit-status) nil)))
(add-hook 'chronometrist-before-out-functions 'my-commit-prompt)
```
### Displaying the current time interval in the activity indicator
```elisp
(defun my-activity-indicator ()
(thread-last (plist-put (chronometrist-last)
:stop (chronometrist-format-time-iso8601))
list
chronometrist-events->ts-pairs
chronometrist-ts-pairs->durations
(-reduce #'+)
truncate
chronometrist-format-time))
(setq chronometrist-activity-indicator 'my-activity-indicator)
```
## Roadmap/Ideas
* Show details for time spent on a project when clicking on a non-zero "time spent" field (in both Chronometrist and Chronometrist-Report buffers).
### chronometrist
1. Use `make-thread` in v26 or the emacs-async library for `chronometrist-entries`/`chronometrist-report-entries`
2. Some way to update buffers every second without making Emacs unusable. (impossible?)
3. "Day summary" - for users who use the "reason" feature to note the specifics of their actual work. Combine the reasons together to create a descriptive overview of the work done in the day.
### chronometrist-statistics
1. Show range counter and max ranges; don't scroll past first/last time ranges
2. activity-specific - average time spent in $TIMEPERIOD, average days worked on in $TIMEPERIOD, current/longest/last streak, % of $TIMEPERIOD, % of active (tracked) time in $TIMEPERIOD, ...
3. general - most productive $TIMEPERIOD, GitHub-style work heatmap calendar, ...
4. press 1 for weekly stats, 2 for monthly, 3 for yearly
### Miscellaneous
1. README - add images
2. [-] Create test timelog file and UI behaviour tests
3. Use for `chronometrist-report-weekday-number-alist` whatever variables like `initial-frame-alist` use to get that fancy Custom UI for alists.
4. Multi-timelog-file support?
5. [inflatable raptor](https://github.com/MichaelMure/git-bug/#planned-features)
## Contributions and contact
Feedback and MRs are very welcome. 🙂
* [TODO.org][TODO.org] has a long list of tasks
* [doc/manual.org](doc/manual.org) contains an overview of the codebase, explains various mechanisms and decisions, and has a reference of definitions.
If you have tried using Chronometrist, I'd love to hear your experiences! Get in touch with the author and other Emacs users in the Emacs channel on the Jabber network - [xmpp:emacs@salas.suchat.org?join](https://conversations.im/j/emacs@salas.suchat.org) ([web chat](https://inverse.chat/#converse/room?jid=emacs@salas.suchat.org))
(For help in getting started with Jabber, [click here](https://xmpp.org/getting-started/))
## License
I dream of a world where all software is liberated - transparent, trustable, and accessible for anyone to use or improve. But I don't want to make demands or threats (e.g. via legal conditions) to get there.
I'd rather make a request - please do everything you can to help that dream come true. Please Unlicense as much software as you can.
Chronometrist is released under your choice of [Unlicense](https://unlicense.org/) or the [WTFPL](http://www.wtfpl.net/).
(See files [UNLICENSE](UNLICENSE) and [WTFPL](WTFPL)).
## Thanks
wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support
jwiegley for timeclock.el, which we used as a backend in earlier versions
blandest for helping me with the name
fiete for testing and bug reports

1
README.org Symbolic link
View File

@ -0,0 +1 @@
manual.org

1072
TODO.org

File diff suppressed because it is too large Load Diff

View File

@ -1,360 +0,0 @@
;;; chronometrist-tests.el --- Tests for Chronometrist -*- lexical-binding: t; -*-
;; 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:
;; These should be replaced by buttercup tests, which have a nicer
;; syntax.
(require 'ert)
(require 'chronometrist)
(require 'chronometrist-report)
;; TODO - add tests for chronometrist-task-time-one-day with custom day start
;; times.
;; #### CHRONOMETRIST-REPORT ####
;;; Code:
(defun interval-test (start target)
"Basic logic used to derive 'gap' in `chronometrist-previous-week-start'."
(cond ((= start target) 7)
((> start target) (- start target))
((< start target) (+ start (- 7 target)))))
(ert-deftest chronometrist-interval-test-0 ()
(should (= (interval-test 0 0) 7))
(should (= (interval-test 0 1) 6))
(should (= (interval-test 0 2) 5))
(should (= (interval-test 0 3) 4))
(should (= (interval-test 0 4) 3))
(should (= (interval-test 0 5) 2))
(should (= (interval-test 0 6) 1)))
(ert-deftest chronometrist-interval-test-1 ()
(should (= (interval-test 1 0) 1))
(should (= (interval-test 1 1) 7))
(should (= (interval-test 1 2) 6))
(should (= (interval-test 1 3) 5))
(should (= (interval-test 1 4) 4))
(should (= (interval-test 1 5) 3))
(should (= (interval-test 1 6) 2)))
(ert-deftest chronometrist-interval-test-2 ()
(should (= (interval-test 2 0) 2))
(should (= (interval-test 2 1) 1))
(should (= (interval-test 2 2) 7))
(should (= (interval-test 2 3) 6))
(should (= (interval-test 2 4) 5))
(should (= (interval-test 2 5) 4))
(should (= (interval-test 2 6) 3)))
(ert-deftest chronometrist-interval-test-3 ()
(should (= (interval-test 3 0) 3))
(should (= (interval-test 3 1) 2))
(should (= (interval-test 3 2) 1))
(should (= (interval-test 3 3) 7))
(should (= (interval-test 3 4) 6))
(should (= (interval-test 3 5) 5))
(should (= (interval-test 3 6) 4)))
(ert-deftest chronometrist-interval-test-4 ()
(should (= (interval-test 4 0) 4))
(should (= (interval-test 4 1) 3))
(should (= (interval-test 4 2) 2))
(should (= (interval-test 4 3) 1))
(should (= (interval-test 4 4) 7))
(should (= (interval-test 4 5) 6))
(should (= (interval-test 4 6) 5)))
(ert-deftest chronometrist-interval-test-5 ()
(should (= (interval-test 5 0) 5))
(should (= (interval-test 5 1) 4))
(should (= (interval-test 5 2) 3))
(should (= (interval-test 5 3) 2))
(should (= (interval-test 5 4) 1))
(should (= (interval-test 5 5) 7))
(should (= (interval-test 5 6) 6)))
(ert-deftest chronometrist-interval-test-6 ()
(should (= (interval-test 6 0) 6))
(should (= (interval-test 6 1) 5))
(should (= (interval-test 6 2) 4))
(should (= (interval-test 6 3) 3))
(should (= (interval-test 6 4) 2))
(should (= (interval-test 6 5) 1))
(should (= (interval-test 6 6) 7)))
(ert-deftest chronometrist-previous-week-start-sunday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Sunday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 1 9 2018 6 nil 19800))
'(0 0 0 26 8 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 2 9 2018 0 nil 19800)))))
(ert-deftest chronometrist-previous-week-start-monday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Monday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 1 9 2018 6 nil 19800))
'(0 0 0 27 8 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 27 8 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 3 9 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 3 9 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 3 9 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 3 9 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 3 9 2018 1 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 3 9 2018 1 nil 19800)))))
(ert-deftest chronometrist-previous-week-start-tuesday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Tuesday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 1 9 2018 6 nil 19800))
'(0 0 0 28 8 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 28 8 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 28 8 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 4 9 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 4 9 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 4 9 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 4 9 2018 2 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 4 9 2018 2 nil 19800)))))
(ert-deftest chronometrist-previous-week-start-wednesday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Wednesday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 1 9 2018 6 nil 19800))
'(0 0 0 29 8 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 29 8 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 29 8 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 29 8 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 5 9 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 5 9 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 5 9 2018 3 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 5 9 2018 3 nil 19800)))))
(ert-deftest chronometrist-previous-week-start-thursday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Thursday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 1 9 2018 6 nil 19800))
'(0 0 0 30 8 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 30 8 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 30 8 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 30 8 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 30 8 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 6 9 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 6 9 2018 4 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 6 9 2018 4 nil 19800)))))
(ert-deftest chronometrist-previous-week-start-friday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Friday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 1 9 2018 6 nil 19800))
'(0 0 0 31 8 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 31 8 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 31 8 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 31 8 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 31 8 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 31 8 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 7 9 2018 5 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 7 9 2018 5 nil 19800)))))
(ert-deftest chronometrist-previous-week-start-saturday ()
"Tests for `chronometrist-previous-week-start'."
(let ((chronometrist-report-week-start-day "Saturday"))
(should (equal (chronometrist-previous-week-start '(0 0 0 30 8 2018 4 nil 19800))
'(0 0 0 25 8 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 31 8 2018 5 nil 19800))
'(0 0 0 25 8 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 2 9 2018 0 nil 19800))
'(0 0 0 1 9 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 3 9 2018 1 nil 19800))
'(0 0 0 1 9 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 4 9 2018 2 nil 19800))
'(0 0 0 1 9 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 5 9 2018 3 nil 19800))
'(0 0 0 1 9 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 6 9 2018 4 nil 19800))
'(0 0 0 1 9 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 7 9 2018 5 nil 19800))
'(0 0 0 1 9 2018 6 nil 19800)))
(should (equal (chronometrist-previous-week-start '(0 0 0 8 9 2018 6 nil 19800))
'(0 0 0 8 9 2018 6 nil 19800)))))
;; #### CHRONOMETRIST-COMMON ####
(ert-deftest chronometrist-ptod-tests ()
"Tests for `chronometrist-task-time-one-day'."
(let ((timeclock-file "test.timelog"))
(timeclock-reread-log)
;; basic 1 hour test
(should (equal (chronometrist-task-time-one-day "Programming" '(0 0 0 1 1 2018))
[1 0 0]))
(should (equal (chronometrist-task-time-one-day "Swimming" '(0 0 0 1 1 2018))
[1 0 0]))
(should (equal (chronometrist-task-time-one-day "Cooking" '(0 0 0 1 1 2018))
[1 0 0]))
(should (equal (chronometrist-task-time-one-day "Guitar" '(0 0 0 1 1 2018))
[1 0 0]))
(should (equal (chronometrist-task-time-one-day "Cycling" '(0 0 0 1 1 2018))
[1 0 0]))
;; across midnight
(should (equal (chronometrist-task-time-one-day "Programming" '(0 0 0 2 1 2018))
[1 0 0]))
(should (equal (chronometrist-task-time-one-day "Programming" '(0 0 0 3 1 2018))
[1 0 0]))))
(ert-deftest chronometrist-ptod-midnight-clocked-in ()
"Tests for `chronometrist-task-time-one-day' behaviour
across midnight + when not clocked out."
:expected-result :failed
(let ((timeclock-file "test2.timelog"))
(timeclock-reread-log)
(should (equal (chronometrist-task-time-one-day "Test" '(0 0 0 1 1 2018))
[1 0 0]))
(should (equal (chronometrist-task-time-one-day "Test" '(0 0 0 2 1 2018))
[24 0 0]))))
;; #### CHRONOMETRIST ####
(ert-deftest chronometrist-seconds-to-hms-tests ()
"Tests for `chronometrist-seconds-to-hms'."
(should (equal (chronometrist-seconds-to-hms 1)
[0 0 1]))
(should (equal (chronometrist-seconds-to-hms 60)
[0 1 0]))
(should (equal (chronometrist-seconds-to-hms 61)
[0 1 1]))
(should (equal (chronometrist-seconds-to-hms 3600)
[1 0 0]))
(should (equal (chronometrist-seconds-to-hms 3660)
[1 1 0]))
(should (equal (chronometrist-seconds-to-hms 3661)
[1 1 1])))
(ert-deftest chronometrist-time-add-tests ()
"Tests for `chronometrist-time-add'."
(should (equal (chronometrist-time-add [0 0 0] [0 0 0])
[0 0 0]))
(should (equal (chronometrist-time-add [0 0 1] [0 0 0])
[0 0 1]))
(should (equal (chronometrist-time-add [0 0 1] [0 0 59])
[0 1 0]))
(should (equal (chronometrist-time-add [0 1 0] [0 0 1])
[0 1 1]))
(should (equal (chronometrist-time-add [0 1 1] [0 59 59])
[1 1 0])))
(ert-deftest chronometrist-ttod-tests ()
"Tests for `chronometrist-active-time-one-day'."
(let ((timeclock-file "test.timelog"))
(timeclock-reread-log)
;; 1 hour per activity test
(should (equal (chronometrist-active-time-one-day '(0 0 0 1 1 2018))
[5 0 0]))
;; pan-midnight tests
(should (equal (chronometrist-active-time-one-day '(0 0 0 2 1 2018))
[1 0 0]))
(should (equal (chronometrist-active-time-one-day '(0 0 0 3 1 2018))
[1 0 0]))
;; 1 second test
(should (equal (chronometrist-active-time-one-day '(0 0 0 4 1 2018))
[0 0 1]))))
(ert-deftest chronometrist-format-time-tests ()
"Tests for `chronometrist-format-time'."
(should (equal (chronometrist-format-time '( 0 0 0))
" -"))
(should (equal (chronometrist-format-time '( 0 0 1))
" 1"))
(should (equal (chronometrist-format-time '( 0 0 10))
" 10"))
(should (equal (chronometrist-format-time '( 0 1 10))
" 1:10"))
(should (equal (chronometrist-format-time '( 0 10 10))
" 10:10"))
(should (equal (chronometrist-format-time '( 1 10 10))
" 1:10:10"))
(should (equal (chronometrist-format-time '(10 10 10))
"10:10:10"))
(should (equal (chronometrist-format-time '[ 0 0 0])
" -"))
(should (equal (chronometrist-format-time '[ 0 0 1])
" 1"))
(should (equal (chronometrist-format-time '[ 0 0 10])
" 10"))
(should (equal (chronometrist-format-time '[ 0 1 10])
" 1:10"))
(should (equal (chronometrist-format-time '[ 0 10 10])
" 10:10"))
(should (equal (chronometrist-format-time '[ 1 10 10])
" 1:10:10"))
(should (equal (chronometrist-format-time '[10 10 10])
"10:10:10")))
(ert-deftest chronometrist-report-iodd-tests ()
(should (equal (chronometrist-date-op '(2020 2 28) '+)
'(2020 2 29))))
(provide 'chronometrist-tests)
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:
;;; chronometrist-tests.el ends here

9
cl/backend-sqlite.asd Normal file
View File

@ -0,0 +1,9 @@
(defsystem chronometrist-sqlite
:version "0.0.1"
:serial t
:license "Unlicense"
:author "contrapunctus <contrapunctus at disroot dot org>"
:description "SQLite backend for Chronometrist"
:defsystem-depends-on ("literate-lisp")
:depends-on (:trivia :clsql-sqlite3)
:components ((:org "chronometrist")))

3321
cl/chronometrist.org Normal file

File diff suppressed because it is too large Load Diff

1605
cl/clim.org Normal file

File diff suppressed because it is too large Load Diff

9
cl/lib.asd Normal file
View File

@ -0,0 +1,9 @@
(defsystem chronometrist
:version "0.0.1"
:serial t
:license "Unlicense"
:author "contrapunctus <contrapunctus at disroot dot org>"
:description "Friendly and extensible personal time tracker - common library"
:defsystem-depends-on ("literate-lisp")
:depends-on (:trivia)
:components ((:org "chronometrist")))

32
cl/manual.org Normal file
View File

@ -0,0 +1,32 @@
* Explanation
:PROPERTIES:
:CUSTOM_ID: explanation
:END:
This is a port of Chronometrist to Common Lisp.
Currently, it contains
1. a read-only plist-group backend
2. an incomplete SQLite backend
3. an incomplete CLIM frontend
** Source code overview
:PROPERTIES:
:CUSTOM_ID: source-code-overview
:END:
*** CLIM frontend
:PROPERTIES:
:CUSTOM_ID: clim-frontend
:END:
The CLIM frontend uses CLIM panes for each view of data Chronometrist provides. Each pane has a [[file:chronometrist.org::#display-pane][=display-pane=]] method to display its contents.
Currently, only one CLIM pane has been implemented, called the [[file:chronometrist.org::#task-duration-table-pane][task-duration table]]. By default, it displays a list of tasks, and each of their durations for today.
**** Tables
:PROPERTIES:
:CUSTOM_ID: tables
:END:
The tables in the CLIM frontend are designed with ease of extensibility in mind. Thus -
1. The columns displayed in this table are controlled by [[file:chronometrist.org::#*task-duration-table-spec*][=*task-duration-table-spec*=]], which contains a list of [[file:chronometrist.org::#column-specifier][=column-specifier=]] objects.
2. Each [[file:chronometrist.org::#column-specifier][=column-specifier=]] has two methods specializing on it, a [[file:chronometrist.org::#cell-data][=cell-data=]] method and a [[file:chronometrist.org::#cell-print][=cell-print=]] method. These determine the data contained by the cells of the column, and how that data is printed in the CLIM pane.
The function [[file:chronometrist.org::#task-duration-table-function][=task-duration-table-function=]] uses the [[file:chronometrist.org::#cell-data][=cell-data=]] methods to return the data of the table as a list of lists. This data is used by [[file:chronometrist.org::#display-pane][=display-pane=]] in conjunction with [[file:chronometrist.org::#cell-print][=cell-print=]] methods to display the data in the pane.

9
cl/ui-clim.asd Normal file
View File

@ -0,0 +1,9 @@
(defsystem chronometrist-clim
:version "0.0.1"
:serial t
:license "Unlicense"
:author "contrapunctus <contrapunctus at disroot dot org>"
:description "Friendly and extensible personal time tracker - CLIM GUI"
:defsystem-depends-on ("literate-lisp")
:depends-on (:trivia :mcclim)
:components ((:org "chronometrist")))

BIN
doc/2022-02-20 13-26-53.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,464 +0,0 @@
#+TITLE: The Chronometrist Manual
#+AUTHOR: contrapunctus
The structure of this manual was inspired by https://documentation.divio.com/
* How to...
:PROPERTIES:
:DESCRIPTION: Step-by-step guides to achieve specific tasks
:END:
** How to set up Emacs to contribute
All of these are optional, but recommended for the best experience.
1. Use [[https://github.com/Malabarba/Nameless][nameless-mode]] for easier reading of Emacs Lisp code, and
2. Use [[https://github.com/joostkremers/visual-fill-column][visual-fill-column-mode]] to soft-wrap lines in Org/Markdown files.
=org-indent-mode= (for Org files) and [[https://elpa.gnu.org/packages/adaptive-wrap.html][adaptive-prefix-mode]] (for Markdown and other files) will further enhance the experience.
3. Get the sources from https://github.com/contrapunctus-1/chronometrist and read this manual in the Org format (doc/manual.org), so links to identifiers can take you to their location in the source.
4. Install [[https://github.com/cask/cask][Cask]] to easily byte-compile and test the project.
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.
* Explanation
:PROPERTIES:
:DESCRIPTION: The design, the implementation, and a little history
:END:
** Design goals
:PROPERTIES:
:DESCRIPTION: Some vague objectives which guided the project
:END:
1. Don't make assumptions about the user's profession
- e.g. timeclock seems to assume you're using it for a 9-to-5/contractor job
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 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?
** Terminology
:PROPERTIES:
:DESCRIPTION: Explanation of some terms used later
:END:
For lack of a better term, events are how we refer to time intervals. They are stored as plists; each contains at least a =:name "<name>"=, a =:start "<iso-timestamp>"=, and (except in case of an ongoing task) a =:stop "<iso-timestamp>"=.
** Project overview
:PROPERTIES:
:DESCRIPTION: A broad overview of the code
:END:
Chronometrist has three components, and each has a file containing major mode definitions and user-facing commands.
1. [[file:../elisp/chronometrist.el][chronometrist.el]]
2. [[file:../elisp/chronometrist-report.el][chronometrist-report.el]]
3. [[file:../elisp/chronometrist-statistics.el][chronometrist-statistics.el]]
All three of these use =(info "(elisp)Tabulated List Mode")=. Each of them also contains a "-print-non-tabular" function, which prints the non-tabular parts of the buffer.
Each of them has a corresponding =-custom= file, which contain the Customize group and custom variable definitions for user-facing variables -
- [[file:../elisp/chronometrist-custom.el][chronometrist-custom.el]]
- [[file:../elisp/chronometrist-report-custom.el][chronometrist-report-custom.el]]
- [[file:../elisp/chronometrist-statistics-custom.el][chronometrist-statistics-custom.el]]
[[file:../elisp/chronometrist-common.el][chronometrist-common.el]] contains definitions common to all components.
All three components use timers to keep their buffers updated. [[file:../elisp/chronometrist-timer.el][chronometrist-timer.el]] contains all timer-related code.
Note - sometimes, when hacking or dealing with errors, timers may result in subtle bugs which are very hard to debug. Using =chronometrist-force-restart-timer= or restarting Emacs can fix them, so try that as a first sanity check.
** Chronometrist
:PROPERTIES:
:DESCRIPTION: The primary command and its associated buffer.
:END:
*** 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'.
+ Being an Emacs program, freezing the UI for any human-noticeable length of time is unacceptable - it prevents the user from working on anything in their environment.
Thus, I have considered various optimization strategies, and so far implemented two.
**** Prevent excess creation of file watchers
One of the earliest 'optimizations' of great importance turned out to simply be a bug - turns out, if you run an identical call to [[elisp:(describe-function 'file-notify-add-watch)][=file-notify-add-watch=]] twice, you create /two/ file watchers and your callback will be called /twice./ We were creating a file watcher /each time the chronometrist command was run./ 🤦 This was causing humongous slowdowns each time the file changed. 😅
+ 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.
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.
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=
* =chronometrist-task-list-add=, and
* =chronometrist-tags-history-add= / =chronometrist-tags-history-replace-last=, respectively.
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.
** Midnight-spanning events
:PROPERTIES:
:DESCRIPTION: Events starting on one day and ending on another
:END:
A unique problem in working with Chronometrist, one I had never foreseen, was tasks which start on one day and end on another. These mess up data consumption (especially interval calculations and acquiring data for a specific date) in all sorts of unforeseen ways.
There are a few different approaches of dealing with them. (Currently, Chronometrist uses #3.)
*** Check the code of the first event of the day (timeclock format)
:PROPERTIES:
:DESCRIPTION: When the code of the first event in the day is "o", it's a midnight-spanning event.
:END:
+ Advantage - very simple to detect
+ Disadvantage - "in" and "out" events must be represented separately
*** Split them at the file level
+ Advantage - operation is performed only once for each such event + simpler data-consuming code + reduced post-parsing load.
+ What happens when the user changes their day-start-time? The split-up events are now split wrongly, and the second event may get split /again./
Possible solutions -
1. Add function to check if, for two events A and B, the :stop of A is the same as the :start of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time.
2. Add a :split tag to split events. It can denote that the next event was originally a part of this one.
3. Re-check and update the file when the day-start-time changes.
- Possible with ~add-variable-watcher~ or ~:custom-set~ in Customize (thanks bpalmer)
*** Split them at the hash-table-level
Handled by ~chronometrist-sexp-events-populate~
+ Advantage - simpler data-consuming code.
*** Split them at the data-consumer level (e.g. when calculating time for one day/getting events for one day)
+ Advantage - reduced repetitive post-parsing load.
** Point restore behaviour
:PROPERTIES:
:DESCRIPTION: The desired behaviour of point in Chronometrist
:END:
After hacking, always test for and ensure the following -
1. Toggling the buffer via =chronometrist=/=chronometrist-report=/=chronometrist-statistics= should preserve point
2. The timer function should preserve point when the buffer is current
3. The timer function should preserve point when the buffer is not current, but is visible in another window
4. The next/previous week keys and buttons should preserve point.
** chronometrist-report date range logic
:PROPERTIES:
:DESCRIPTION: Deriving dates in the current week
:END:
A quick description, starting from the first time [[file:../elisp/chronometrist-report.el::defun chronometrist-report (][=chronometrist-report=]] is run in an Emacs session -
1. We get the current date as a ts struct =(chronometrist-date)=.
2. The variable =chronometrist-report-week-start-day= stores the day we consider the week to start with. The default is "Sunday".
We check if the date from #2 is on the week start day, else decrement it till we are, using =(chronometrist-report-previous-week-start)=.
3. We store the date from #3 in the global variable =chronometrist-report--ui-date=.
4. By counting up from =chronometrist-report--ui-date=, we get dates for the days in the next 7 days using =(chronometrist-report-date->dates-in-week)=. We store them in =chronometrist-report--ui-week-dates=.
The dates in =chronometrist-report--ui-week-dates= are what is finally used to query the data displayed in the buffer.
5. To get data for the previous/next weeks, we decrement/increment the date in =chronometrist-report--ui-date= by 7 days and repeat the above process (via =(chronometrist-report-previous-week)=/=(chronometrist-report-next-week)=).
** Tags and Key-Values
:PROPERTIES:
:DESCRIPTION: How tags and key-values are implemented
: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.
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.
*** 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.
Both have corresponding functions to create a prompt -
+ [[file:../elisp/chronometrist-key-values.el::defun chronometrist-key-prompt (][=chronometrist-key-prompt=]],
+ [[file:../elisp/chronometrist-key-values.el::defun chronometrist-value-prompt (][=chronometrist-value-prompt=]], and
+ [[file:../elisp/chronometrist-key-values.el::defun chronometrist-tags-prompt (][=chronometrist-tags-prompt=]].
[[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-add (][=chronometrist-kv-add=]]'s way of reading key-values from the user is somewhat different from most Emacs prompts - it creates a new buffer, and uses the minibuffer to alternatingly ask for keys and values in a loop. Key-values are inserted into the buffer as the user enters/selects them. The user can break out of this loop with an empty input (the keys to accept an empty input differ between completion systems, so we try to let the user know about them using [[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-completion-quit-key (][=chronometrist-kv-completion-quit-key=]]). After exiting the loop, they can edit the key-values in the buffer, and use the commands [[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-accept (][=chronometrist-kv-accept=]] to accept the key-values (which uses [[file:../elisp/chronometrist-key-values.el::defun chronometrist-append-to-last (][=chronometrist-append-to-last=]] to add them to the last plist in =chronometrist-file=) or [[file:../elisp/chronometrist-key-values.el::defun chronometrist-kv-reject (][=chronometrist-kv-reject=]] to discard them.
*** History
All prompts suggest past user inputs. These are queried from three history hash tables -
+ [[file:../elisp/chronometrist-key-values.el::defvar chronometrist-key-history (][=chronometrist-key-history=]],
+ [[file:../elisp/chronometrist-key-values.el::defvar chronometrist-value-history (][=chronometrist-value-history=]], and
+ [[file:../elisp/chronometrist-key-values.el::defvar chronometrist-tags-history (][=chronometrist-tags-history=]].
Each of these has a corresponding function to clear it and fill it with values -
+ [[file:../elisp/chronometrist-key-values.el::defun chronometrist-key-history-populate (][=chronometrist-key-history-populate=]]
+ [[file:../elisp/chronometrist-key-values.el::defun chronometrist-value-history-populate (][=chronometrist-value-history-populate=]], and
+ [[file:../elisp/chronometrist-key-values.el::defun chronometrist-tags-history-populate (][=chronometrist-tags-history-populate=]].
* Reference
:PROPERTIES:
:DESCRIPTION: A list of definitions, with some type information
:END:
** Legend of currently-used time formats
*** ts
ts.el struct
* Used by nearly all internal functions
*** iso-timestamp
"YYYY-MM-DDTHH:MM:SSZ"
* Used in the s-expression file format
* Read by chronometrist-sexp-events-populate
* Used in the plists in the chronometrist-events hash table values
*** iso-date
"YYYY-MM-DD"
* Used as hash table keys in chronometrist-events - can't use ts structs for keys, you'd have to make a hash table predicate which uses ts=
*** seconds
integer seconds as duration
* Used for most durations
* May be changed to floating point to allow larger durations. The minimum range of `most-positive-fixnum` is 536870911, which seems to be enough to represent durations of 17 years.
* Used for update intervals (chronometrist-update-interval, chronometrist-change-update-interval)
*** minutes
integer minutes as duration
* Used for goals (chronometrist-goals-list, chronometrist-get-goal) - minutes seems like the ideal unit for users to enter
*** list-duration
(hours minute seconds)
* Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
** chronometrist-common.el
1. Variable - chronometrist-empty-time-string
2. Variable - chronometrist-date-re
3. Variable - chronometrist-time-re-ui
4. Variable - chronometrist-task-list
5. Function - chronometrist-task-list-add (task)
6. Internal Variable - chronometrist--fs-watch
7. Function - chronometrist-current-task ()
8. Function - chronometrist-format-time (seconds &optional (blank " "))
* seconds -> "h:m:s"
9. Function - chronometrist-common-file-empty-p (file)
10. Function - chronometrist-common-clear-buffer (buffer)
11. Function - chronometrist-format-keybinds (command map &optional firstonly)
12. Function - chronometrist-events->ts-pairs (events)
* (plist ...) -> ((ts . ts) ...)
13. Function - chronometrist-ts-pairs->durations (ts-pairs)
* ((ts . ts) ...) -> seconds
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
3. Function - chronometrist-intervals-on (date)
4. Function - chronometrist-diary-tasks-reasons-on (date)
5. Function - chronometrist-diary-refresh (&optional ignore-auto noconfirm date)
6. Major Mode - chronometrist-diary-view-mode
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)
* 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)
** chronometrist-events.el
1. Variable - chronometrist-events
* keys - iso-date
2. Function - chronometrist-day-start (timestamp)
* iso-timestamp -> encode-time
3. Function - chronometrist-file-clean ()
* commented out, unused
4. Function - chronometrist-events-maybe-split (event)
5. Function - chronometrist-events-populate ()
6. Function - chronometrist-tasks-from-table ()
7. Function - chronometrist-events-add (plist)
8. Function - chronometrist-events-replace-last (plist)
9. Function - chronometrist-events-subset (start end)
* ts ts -> hash-table
** chronometrist-migrate.el
1. Variable - chronometrist-migrate-table
2. Function - chronometrist-migrate-populate (in-file)
3. Function - chronometrist-migrate-timelog-file->sexp-file (&optional in-file out-file)
4. Function - chronometrist-migrate-check ()
** chronometrist-plist-pp.el
1. Variable - chronometrist-plist-pp-keyword-re
2. Variable - chronometrist-plist-pp-whitespace-re
3. Function - chronometrist-plist-pp-longest-keyword-length ()
4. Function - chronometrist-plist-pp-buffer-keyword-helper ()
5. Function - chronometrist-plist-pp-buffer ()
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
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)
** chronometrist-key-values.el
1. Internal Variable - chronometrist--tag-suggestions
2. Internal Variable - chronometrist--value-suggestions
3. Function - chronometrist-plist-remove (plist &rest keys)
4. Function - chronometrist-maybe-string-to-symbol (list)
5. Function - chronometrist-maybe-symbol-to-string (list)
6. Function - chronometrist-append-to-last (tags plist)
7. Variable - chronometrist-tags-history
8. Function - chronometrist-tags-history-populate (events-table history-table)
9. Function - chronometrist-tags-history-add (plist)
10. Function - chronometrist-tags-history-replace-last (plist)
11. Function - chronometrist-tags-history-combination-strings (task)
12. Function - chronometrist-tags-history-individual-strings (task)
13. Function - chronometrist-tags-prompt (task &optional initial-input)
14. Function - chronometrist-tags-add (&rest args)
15. Custom Variable - chronometrist-kv-buffer-name
16. Variable - chronometrist-key-history
17. Variable - chronometrist-value-history
18. Function - chronometrist-ht-history-prep (table)
19. Function - chronometrist-key-history-populate (events-table history-table)
20. Function - chronometrist-value-history-populate (events-table history-table)
21. Keymap - chronometrist-kv-read-mode-map
22. Major Mode - chronometrist-kv-read-mode
23. Function - chronometrist-kv-completion-quit-key ()
24. Function - chronometrist-string-has-whitespace-p (string)
25. Function - chronometrist-key-prompt (used-keys)
26. Function - chronometrist-value-prompt (key)
27. Function - chronometrist-value-insert (value)
28. Function - chronometrist-kv-add (&rest args)
29. Command - chronometrist-kv-accept ()
30. Command - chronometrist-kv-reject ()
31. Internal Variable - chronometrist--skip-detail-prompts
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)
** chronometrist-time.el
1. Function - chronometrist-iso-timestamp->ts (timestamp)
* iso-timestamp -> ts
2. Function - chronometrist-iso-date->ts (date)
* iso-date -> ts
3. Function - chronometrist-date (&optional (ts (ts-now)))
* &optional ts -> ts (with time 00:00:00)
4. Function - chronometrist-format-time-iso8601 (&optional unix-time)
5. Function - chronometrist-midnight-spanning-p (start-time stop-time)
6. Function - chronometrist-seconds-to-hms (seconds)
* seconds -> list-duration
7. Function - chronometrist-interval (event)
* event -> duration
** chronometrist-timer.el
1. Internal Variable - chronometrist--timer-object
2. Function - chronometrist-timer ()
3. Command - chronometrist-stop-timer ()
4. Command - chronometrist-maybe-start-timer (&optional interactive-test)
5. Command - chronometrist-force-restart-timer ()
6. Command - chronometrist-change-update-interval (arg)
** chronometrist-goal
1. Internal Variable - chronometrist-goal--timers-list
2. Custom Variable - chronometrist-goal-list nil
3. Function - chronometrist-goal-run-at-time (time repeat function &rest args)
4. Function - chronometrist-goal-seconds->alert-string (seconds)
* seconds -> string
5. Function - chronometrist-goal-approach-alert (task goal spent)
* string minutes minutes
6. Function - chronometrist-goal-complete-alert (task goal spent)
* string minutes minutes
7. Function - chronometrist-goal-exceed-alert (task goal spent)
* string minutes minutes
8. Function - chronometrist-goal-no-goal-alert (task goal spent)
* string minutes minutes
9. Custom Variable - chronometrist-goal-alert-functions
* each function is passed - string minutes minutes
10. Function - chronometrist-goal-get (task &optional (goal-list chronometrist-goal-list))
* String &optional List -> minutes
11. Function - chronometrist-goal-run-alert-timers (task)
12. Function - chronometrist-goal-stop-alert-timers (&optional _task)
13. Function - chronometrist-goal-on-file-change ()
** chronometrist-sexp
1. Macro - chronometrist-sexp-in-file (file &rest body)
2. Function - chronometrist-sexp-open-log ()
3. Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
4. Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
5. Function - chronometrist-sexp-last ()
* -> plist
6. Function - chronometrist-sexp-current-task ()
7. Function - chronometrist-sexp-events-populate ()
8. Function - chronometrist-sexp-create-file ()
9. Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
10. Function - chronometrist-sexp-delete-list (&optional arg)
11. Function - chronometrist-sexp-replace-last (plist)
12. Command - chronometrist-sexp-reindent-buffer ()
# Local Variables:
# org-link-file-path-type: relative
# eval: (progn (make-local-variable (quote after-save-hook)) (add-hook (quote after-save-hook) (lambda () (org-export-to-file 'texinfo "manual.info"))))
# End:

View File

@ -1,154 +0,0 @@
;;; chronometrist-common.el --- Common definitions for Chronometrist -*- 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:
;;
(require 'dash)
(require 'cl-lib)
(require 'ts)
(require 'chronometrist-custom)
(require 'chronometrist-report-custom)
(require 'chronometrist-time)
(require 'chronometrist-sexp)
;; ## VARIABLES ##
;;; Code:
(defvar chronometrist-empty-time-string "-")
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
(defvar chronometrist-time-re-ui
(rx-to-string
`(or
(and (repeat 0 2
(optional (repeat 1 2 digit) ":"))
(repeat 1 2 digit))
,chronometrist-empty-time-string))
"Regular expression to represent a timestamp in `chronometrist'.
This is distinct from `chronometrist-time-re-file' (which see) -
`chronometrist-time-re-ui' is meant for the user interface, and
must correspond to the output from `chronometrist-format-time'.")
(defvar chronometrist-task-list nil
"List of tasks in `chronometrist-file', as returned by `chronometrist-tasks-from-table'.")
(defun chronometrist-task-list-add (task)
"Add TASK to `chronometrist-task-list', if it is not already present."
(unless (member task chronometrist-task-list)
(--> (list task)
(append chronometrist-task-list it)
(sort it #'string-lessp)
(setq chronometrist-task-list it))))
(defvar chronometrist--fs-watch nil
"Filesystem watch object.
Used to prevent more than one watch being added for the same
file.")
(defun chronometrist-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(chronometrist-sexp-current-task))
(cl-defun chronometrist-format-time (seconds &optional (blank " "))
"Format SECONDS as a string suitable for display in Chronometrist buffers.
SECONDS must be a positive integer.
BLANK is a string to display in place of blank values. If not
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))
blank)
((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)))))
(defun chronometrist-common-file-empty-p (file)
"Return t if FILE is empty."
(let ((size (elt (file-attributes file) 7)))
(if (zerop size) t nil)))
(defun chronometrist-common-clear-buffer (buffer)
"Clear the contents of BUFFER."
(with-current-buffer buffer
(goto-char (point-min))
(delete-region (point-min) (point-max))))
(defun chronometrist-format-keybinds (command map &optional firstonly)
"Return the keybindings for COMMAND in MAP as a string.
If FIRSTONLY is non-nil, return only the first keybinding found."
(if firstonly
(key-description
(where-is-internal command map firstonly))
(->> (where-is-internal command map)
(mapcar #'key-description)
(-take 2)
(-interpose ", ")
(apply #'concat))))
(defun chronometrist-events->ts-pairs (events)
"Convert EVENTS to a list of ts struct pairs (see `ts.el').
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))))
(defun chronometrist-ts-pairs->durations (ts-pairs)
"Return the durations represented by TS-PAIRS.
TS-PAIRS is a list of pairs, where each element is a ts struct (see `ts.el').
Return seconds as an integer, or 0 if TS-PAIRS is nil."
(if ts-pairs
(cl-loop for pair in ts-pairs collect
(ts-diff (cdr pair) (car pair)))
0))
(defun chronometrist-previous-week-start (ts)
"Find the previous `chronometrist-report-week-start-day' from TS.
Return a ts struct for said day's beginning.
If the day of TS is the same as the
`chronometrist-report-week-start-day', return TS.
TS must be a ts struct (see `ts.el')."
(cl-loop until (equal chronometrist-report-week-start-day
(ts-day-name ts))
do (ts-decf (ts-day ts))
finally return ts))
(provide 'chronometrist-common)
;;; chronometrist-common.el ends here

View File

@ -1,78 +0,0 @@
;;; chronometrist-custom.el --- Custom definitions for Chronometrist -*- 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:
(defgroup chronometrist nil
"A time tracker with a nice UI."
: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.\)"
:type 'file)
(defcustom chronometrist-buffer-name "*Chronometrist*"
"The name of the buffer created by `chronometrist'."
:type 'string)
(defcustom chronometrist-hide-cursor nil
"If non-nil, hide the cursor and only highlight the current line in the `chronometrist' buffer."
:type 'boolean)
(defcustom chronometrist-update-interval 5
"How often the `chronometrist' buffer should be updated, in seconds.
This is not guaranteed to be accurate - see (info \"(elisp)Timers\")."
:type 'integer)
(eval-when-compile (require 'subr-x))
(defcustom chronometrist-activity-indicator "*"
"How to indicate that a task is active.
Can be a string to be displayed, or a function which returns this string.
The default is \"*\""
:type '(choice string function))
(defcustom chronometrist-day-start-time "00:00:00"
"The time at which a day is considered to start, in \"HH:MM:SS\".
The default is midnight, i.e. \"00:00:00\"."
:type 'string)
(provide 'chronometrist-custom)
;;; chronometrist-custom.el ends here

View File

@ -1,103 +0,0 @@
;;; chronometrist-diary-view.el --- A diary-like view for Chronometrist -*- 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:
;; TODO - add forward/backward, current date display (and don't scroll
;; past the actual data)
;; TODO - when the reason is "-", add that interval to the next reason
;; TODO - permit switching between time formats
;; - hours and minutes - "X hours, Y minutes"
;; - minutes only - "80 minutes"
;; - relaxed - "(almost|slightly over) 1 hour"
;; - strict time periods (using `chronometrist-seconds-to-hms')
;; - period start/end ("HH:MM to HH:MM")
;; Add variable to store format functions as list (first one is
;; default), command to cycle between them
;;; Code:
(require 'chronometrist-events)
(require 'chronometrist-common)
(defvar chronometrist-diary-buffer-name "*Chronometrist-Diary*"
"Name for the buffer created by `chronometrist-diary'.")
(defvar chronometrist-diary--current-date nil
"Stores the date for the buffer.")
(defun chronometrist-intervals-on (date)
"Return a list of all time intervals on DATE.
DATE should be a list in the form \"YYYY-MM-DD\"
Each time interval is a string as returned by `chronometrist-seconds-to-hms'."
(->> (gethash date chronometrist-events)
(chronometrist-events->ts-pairs)
;; Why were we calling `-partition' here?
;; (-partition 2)
(--map (time-subtract (cadr it) (car it)))
(--map (chronometrist-seconds-to-hms (cadr it)))))
;; "X minutes on TASK (REASON)"
;; TODO - think of a better way to show details concisely, ideally
;; combining tags and key-values
(defun chronometrist-diary-tasks-reasons-on (date)
"Return a list of tasks and reasons on DATE."
(mapcar (lambda (plist)
(let ((task (plist-get plist :name))
(reason (or (plist-get plist :comment) "")))
(concat " on " task
(unless (equal reason "")
(concat " (" reason ")"))
"\n")))
(gethash date chronometrist-events)))
(defun chronometrist-diary-refresh (&optional _ignore-auto _noconfirm date)
"Refresh the `chronometrist-diary' buffer.
This does not re-read `chronometrist-file'.
Optional argument DATE should be a list in the form
\"YYYY-MM-DD\". If not supplied, today's date is used.
The optional arguments _IGNORE-AUTO and _NOCONFIRM are ignored,
and are present solely for the sake of using this function as a
value of `revert-buffer-function'."
(let* ((date (if date date (chronometrist-date)))
(intervals (->> (chronometrist-intervals-on date)
(mapcar #'chronometrist-format-time)))
(tasks-reasons (chronometrist-diary-tasks-reasons-on date))
(inhibit-read-only t))
(setq chronometrist-diary--current-date date)
(chronometrist-common-clear-buffer chronometrist-diary-buffer-name)
(seq-mapn #'insert intervals tasks-reasons)))
(define-derived-mode chronometrist-diary-view-mode special-mode "Chronometrist-Diary"
"A mode to view your activity today like a diary."
(setq revert-buffer-function #'chronometrist-diary-refresh))
(defun chronometrist-diary-view (&optional date)
"Display today's Chronometrist data in a diary-like view.
If DATE is supplied, show data for that date. DATE should be an
ISO-8601 date string (\"YYYY-MM-DD\")."
(interactive)
(switch-to-buffer
(get-buffer-create chronometrist-diary-buffer-name))
(chronometrist-diary-view-mode)
(chronometrist-diary-refresh nil nil date))
(provide 'chronometrist-diary-view)
;;; chronometrist-diary-view.el ends here

View File

@ -1,179 +0,0 @@
;;; chronometrist-events.el --- Event management and querying code for Chronometrist -*- 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>
;; (require 'chronometrist-plist-pp)
(require 'chronometrist-common)
(require 'chronometrist-sexp)
(require 'ts)
;; external -
;; chronometrist-day-start-time (-custom)
;; chronometrist-midnight-spanning-p (-time)
;; chronometrist-date-less-p (-time)
;;; Commentary:
;;
;;; Code:
(defvar chronometrist-events (make-hash-table :test #'equal)
"Each key is a date in the form (YEAR MONTH DAY).
Values are lists containing events, where each event is a list in
the form (:name \"NAME\" :tags (TAGS) <key value pairs> ...
:start TIME :stop TIME).")
(defun chronometrist-day-start (timestamp)
"Get start of day (according to `chronometrist-day-start-time') for TIMESTAMP.
TIMESTAMP must be a time string in the ISO-8601 format.
Return value is a time value (see `current-time')."
(let ((timestamp-date-list (->> timestamp
(parse-iso8601-time-string)
(decode-time)
(-drop 3)
(-take 3))))
(--> chronometrist-day-start-time
(split-string it ":")
(mapcar #'string-to-number it)
(reverse it)
(append it timestamp-date-list)
(apply #'encode-time it))))
;; (defun chronometrist-file-clean ()
;; "Clean `chronometrist-file' so that events can be processed accurately.
;; NOTE - currently unused.
;; This function splits midnight-spanning intervals into two. It
;; must be called before running `chronometrist-populate'.
;; It returns t if the table was modified, else nil."
;; (let ((buffer (find-file-noselect chronometrist-file))
;; modified
;; expr)
;; (with-current-buffer buffer
;; (save-excursion
;; (goto-char (point-min))
;; (while (setq expr (ignore-errors (read (current-buffer))))
;; (when (plist-get expr :stop)
;; (let ((split-time (chronometrist-midnight-spanning-p (plist-get expr :start)
;; (plist-get expr :stop))))
;; (when split-time
;; (let ((first-start (plist-get (cl-first split-time) :start))
;; (first-stop (plist-get (cl-first split-time) :stop))
;; (second-start (plist-get (cl-second split-time) :start))
;; (second-stop (plist-get (cl-second split-time) :stop)))
;; (backward-list 1)
;; (chronometrist-sexp-delete-list)
;; (-> expr
;; (plist-put :start first-start)
;; (plist-put :stop first-stop)
;; (chronometrist-plist-pp buffer))
;; (when (looking-at-p "\n\n")
;; (delete-char 2))
;; (-> expr
;; (plist-put :start second-start)
;; (plist-put :stop second-stop)
;; (chronometrist-plist-pp buffer))
;; (setq modified t))))))
;; (save-buffer)))
;; modified))
(defun chronometrist-events-maybe-split (event)
"Split EVENT if it spans midnight.
Return a list of two events if EVENT was split, else nil."
(when (plist-get event :stop)
(let ((split-time (chronometrist-midnight-spanning-p (plist-get event :start)
(plist-get event :stop))))
(when split-time
(let ((first-start (plist-get (cl-first split-time) :start))
(first-stop (plist-get (cl-first split-time) :stop))
(second-start (plist-get (cl-second split-time) :start))
(second-stop (plist-get (cl-second split-time) :stop))
;; plist-put modifies lists in-place. The resulting bugs
;; left me puzzled for a while.
(event-1 (cl-copy-list event))
(event-2 (cl-copy-list event)))
(list (-> event-1
(plist-put :start first-start)
(plist-put :stop first-stop))
(-> event-2
(plist-put :start second-start)
(plist-put :stop second-stop))))))))
;; TODO - Maybe strip dates from values, since they're part of the key
;; anyway. Consider using a state machine.
;; OPTIMIZE - It should not be necessary to call this unless the file
;; has changed. Any other refresh situations should not require this.
(defun chronometrist-events-populate ()
"Clear hash table `chronometrist-events' (which see) and populate it.
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))
(defun chronometrist-tasks-from-table ()
"Return a list of task names from `chronometrist-events'."
(let (acc)
(maphash (lambda (_key value)
(mapc (lambda (event)
(setq acc (append acc `(,(plist-get event :name)))))
value))
chronometrist-events)
(cl-remove-duplicates (sort acc #'string-lessp)
:test #'equal)))
(defun chronometrist-events-add (plist)
"Add new PLIST at the end of `chronometrist-events'."
(let* ((date-today (format-time-string "%Y-%m-%d"))
(events-today (gethash date-today chronometrist-events)))
(--> (list plist)
(append events-today it)
(puthash date-today it chronometrist-events))))
(defun chronometrist-events-replace-last (plist)
"Replace the last plist in `chronometrist-events' with PLIST."
(let* ((date-today (format-time-string "%Y-%m-%d"))
(events-today (gethash date-today chronometrist-events)))
(--> (reverse events-today)
(cdr it)
(append (list plist) it)
(reverse it)
(puthash date-today it chronometrist-events))))
;; to be replaced by plist-query
(defun chronometrist-events-subset (start end)
"Return a subset of `chronometrist-events'.
The subset will contain values between dates START and END (both
inclusive).
START and END must be ts structs (see `ts.el'). They will be
treated as though their time is 00:00:00."
(let ((subset (make-hash-table :test #'equal))
(start (chronometrist-date start))
(end (chronometrist-date end)))
(maphash (lambda (key value)
(when (ts-in start end (chronometrist-iso-date->ts key))
(puthash key value subset)))
chronometrist-events)
subset))
(provide 'chronometrist-events)
;;; chronometrist-events.el ends here

View File

@ -1,18 +1,11 @@
;;; 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")
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((chronometrist "0.7.0"))
;; Version: 0.1.0
;; This is free and unencumbered software released into the public domain.
;;
@ -25,35 +18,33 @@
;;; Commentary:
;;
;; This package lets users attach tags and key-values to their tracked time, similar to tags and properties in Org mode.
;;
;; To use, add one or more of these functions to any chronometrist hook except `chronometrist-before-in-functions'.
;; * `chronometrist-tags-add'
;; * `chronometrist-kv-add'
;; * `chronometrist-key-values-unified-prompt'
;;; Code:
(require 'chronometrist)
(require 'chronometrist-sexp)
(defun chronometrist-history-prep (key history-table)
"Prepare history of KEY in HISTORY-TABLE for use in prompts.
Each value in hash table TABLE must be a list. Each value will be reversed and will have duplicate elements removed."
(--> (gethash key history-table)
(cl-remove-duplicates it :test #'equal :from-end t)
(puthash key it history-table)))
(defvar chronometrist--tag-suggestions nil
"Suggestions for tags.
Used as history by `chronometrist-tags-prompt'.")
(defvar chronometrist--value-suggestions nil
"Suggestions for values.
Used as history by `chronometrist--value-suggestions'.")
(defun chronometrist-plist-remove (plist &rest keys)
"Return PLIST with KEYS and their associated values removed."
(let ((keys (--filter (plist-member plist it) keys)))
(mapc (lambda (key)
(let ((pos (seq-position plist key)))
(setq plist (append (seq-take plist pos)
(seq-drop plist (+ 2 pos))))))
keys)
plist))
(defun chronometrist-keyword-to-string (keyword)
"Return KEYWORD as a string, with the leading \":\" removed."
(replace-regexp-in-string "^:?" "" (symbol-name keyword)))
(defun chronometrist-maybe-string-to-symbol (list)
"For each string in LIST, if it has no spaces, convert it to a symbol."
(--map (if (chronometrist-string-has-whitespace-p it)
it
(intern it))
list))
(cl-loop for string in list
if (string-match-p "[[:space:]]" string)
collect string
else collect (intern string)))
(defun chronometrist-maybe-symbol-to-string (list)
"Convert each symbol in LIST to a string."
@ -62,105 +53,75 @@ Used as history by `chronometrist--value-suggestions'.")
it)
list))
(defun chronometrist-append-to-last (tags plist)
"Add TAGS and PLIST to the last entry in `chronometrist-file'.
TAGS should be a list of symbols and/or strings.
(defun chronometrist-plist-update (old-plist new-plist)
"Add tags and keyword-values from NEW-PLIST to OLD-PLIST.
OLD-PLIST and NEW-PLIST should be a property lists.
PLIST should be a property list. Properties reserved by
Chronometrist - currently :name, :tags, :start, and :stop - will
be removed."
(let* ((old-expr (chronometrist-last))
(old-name (plist-get old-expr :name))
(old-start (plist-get old-expr :start))
(old-stop (plist-get old-expr :stop))
(old-tags (plist-get old-expr :tags))
;; Anything that's left will be the user's key-values.
(old-kvs (chronometrist-plist-remove old-expr :name :tags :start :stop))
;; Prevent the user from adding reserved key-values.
(plist (chronometrist-plist-remove plist :name :tags :start :stop))
(new-tags (if old-tags
(-> (append old-tags tags)
(cl-remove-duplicates :test #'equal))
tags))
;; In case there is an overlap in key-values, we use
;; plist-put to replace old ones with new ones.
(new-kvs (cl-copy-list old-expr))
(new-kvs (if plist
(-> (cl-loop for (key val) on plist by #'cddr
do (plist-put new-kvs key val)
finally return new-kvs)
(chronometrist-plist-remove :name :tags :start :stop))
old-kvs))
(plist (append `(:name ,old-name)
(when new-tags `(:tags ,new-tags))
new-kvs
`(:start ,old-start)
(when old-stop `(:stop ,old-stop)))))
(chronometrist-sexp-replace-last plist)))
Keywords reserved by Chronometrist - :name, :start, and :stop -
will not be updated. Keywords in OLD-PLIST with new values in
NEW-PLIST will be updated. Tags in OLD-PLIST will be preserved
alongside new tags from NEW-PLIST."
(-let* (((&plist :name old-name :tags old-tags
:start old-start :stop old-stop) old-plist)
;; Anything that's left will be the user's key-values.
(old-kvs (chronometrist-plist-key-values old-plist))
;; Prevent the user from adding reserved key-values.
(plist (chronometrist-plist-key-values new-plist))
(new-tags (-> (append old-tags (plist-get new-plist :tags))
(cl-remove-duplicates :test #'equal)))
;; In case there is an overlap in key-values, we use
;; plist-put to replace old ones with new ones.
(new-kvs (cl-copy-list old-plist))
(new-kvs (if plist
(-> (cl-loop for (key val) on plist by #'cddr
do (plist-put new-kvs key val)
finally return new-kvs)
(chronometrist-plist-key-values))
old-kvs)))
(append `(:name ,old-name)
(when new-tags `(:tags ,new-tags))
new-kvs
`(:start ,old-start)
(when old-stop `(:stop ,old-stop)))))
;;;; TAGS ;;;;
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
"Hash table of tasks and past tag combinations.
Each value is a list of tag combinations, in reverse
chronological order. Each combination is a list containing tags
as symbol and/or strings.")
(defun chronometrist-tags-history-populate (events-table history-table)
"Clear HISTORY-TABLE and store tag history in it, using EVENTS-TABLE.
Return the new value of HISTORY-TABLE.
(defun chronometrist-tags-history-populate (task history-table backend)
"Store tag history for TASK in HISTORY-TABLE from FILE.
Return the new value inserted into HISTORY-TABLE.
HISTORY-TABLE and EVENTS-TABLE must be hash tables. (see
`chronometrist-tags-history' and `chronometrist-events')"
(clrhash history-table)
(cl-loop for events being the hash-values of events-table do
(cl-loop for event in events do
(let* ((name (plist-get event :name))
(tags (plist-get event :tags))
(existing-tags (gethash name history-table)))
(when tags
(puthash name
(if existing-tags
(append existing-tags `(,tags))
`(,tags))
history-table)))))
;; We can't use `chronometrist-ht-history-prep' to do this, because it uses
;; `-flatten'; the values of `chronometrist-tags-history' hold tag combinations
;; (as lists), not individual tags.
(cl-loop for task being the hash-keys of history-table
using (hash-values tag-lists) do
(puthash task
;; Because remove-duplicates keeps the _last_
;; occurrence, trying to avoid this `reverse' by
;; switching the args in the call to `append'
;; above will not get you the correct behavior!
(-> (cl-remove-duplicates tag-lists :test #'equal)
(reverse))
history-table)
finally return history-table))
HISTORY-TABLE must be a hash table. (see `chronometrist-tags-history')"
(puthash task nil history-table)
(cl-loop for plist in (chronometrist-to-list backend) do
(let ((new-tag-list (plist-get plist :tags))
(old-tag-lists (gethash task history-table)))
(and (equal task (plist-get plist :name))
new-tag-list
(puthash task
(if old-tag-lists
(append old-tag-lists (list new-tag-list))
(list new-tag-list))
history-table))))
(chronometrist-history-prep task history-table))
(defvar chronometrist--tag-suggestions nil
"Suggestions for tags.
Used as history by `chronometrist-tags-prompt'.")
(defun chronometrist-tags-history-add (plist)
"Add tags from PLIST to `chronometrist-tags-history'."
(let* ((table chronometrist-tags-history)
(name (plist-get plist :name))
(tags (awhen (plist-get plist :tags) (list it)))
(tags (plist-get plist :tags))
(old-tags (gethash name table)))
(when tags
(--> (append tags old-tags)
(--> (cons tags old-tags)
(puthash name it table)))))
(defun chronometrist-tags-history-replace-last (plist)
"Replace the latest tag combination for PLIST's task with tags from PLIST."
(let* ((table chronometrist-tags-history)
(name (plist-get plist :name))
(tags (awhen (plist-get plist :tags) (list it)))
(old-tags (gethash name table)))
(when tags
(if old-tags
(--> (cdr old-tags)
(append tags it)
(puthash name it table))
(puthash name tags table)))))
(defun chronometrist-tags-history-combination-strings (task)
"Return list of past tag combinations for TASK.
Each combination is a string, with tags separated by commas.
@ -182,12 +143,12 @@ This is used to provide history for `completing-read-multiple' in
This is used to provide completion for individual tags, in
`completing-read-multiple' in `chronometrist-tags-prompt'."
(--> (gethash task chronometrist-tags-history)
(-flatten it)
(cl-remove-duplicates it :test #'equal)
(cl-loop for elt in it
collect (if (stringp elt)
elt
(symbol-name elt)))))
(-flatten it)
(cl-remove-duplicates it :test #'equal)
(cl-loop for elt in it
collect (if (stringp elt)
elt
(symbol-name elt)))))
(defun chronometrist-tags-prompt (task &optional initial-input)
"Read one or more tags from the user and return them as a list of strings.
@ -205,29 +166,55 @@ INITIAL-INPUT is as used in `completing-read'."
"Read tags from the user; add them to the last entry in `chronometrist-file'.
_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))
(last-name (plist-get last-expr :name))
(last-tags (plist-get last-expr :tags))
(input (->> last-tags
(chronometrist-maybe-symbol-to-string)
(-interpose ",")
(apply #'concat)
(chronometrist-tags-prompt last-name)
(chronometrist-maybe-string-to-symbol))))
(when input
(--> (append last-tags input)
(reverse it)
(cl-remove-duplicates it :test #'equal)
(reverse it)
(chronometrist-append-to-last it nil)))))
t)
(interactive)
(let* ((backend (chronometrist-active-backend))
(last-expr (chronometrist-latest-record backend))
(last-name (plist-get last-expr :name))
(_history (chronometrist-tags-history-populate last-name chronometrist-tags-history backend))
(last-tags (plist-get last-expr :tags))
(input (->> (chronometrist-maybe-symbol-to-string last-tags)
(-interpose ",")
(apply #'concat)
(chronometrist-tags-prompt last-name)
(chronometrist-maybe-string-to-symbol))))
(when input
(--> (append last-tags input)
(reverse it)
(cl-remove-duplicates it :test #'equal)
(reverse it)
(list :tags it)
(chronometrist-plist-update
(chronometrist-latest-record backend) it)
(chronometrist-replace-last backend it)))
t))
;;;; KEY-VALUES ;;;;
(defgroup chronometrist-key-values nil
"Add key-values to Chronometrist time intervals."
:group 'chronometrist)
(defcustom chronometrist-key-value-use-database-history t
"If non-nil, use database to generate key-value suggestions.
If nil, only `chronometrist-key-value-preset-alist' is used."
:type 'boolean
:group 'chronometrist-key-value)
(defcustom chronometrist-key-value-preset-alist nil
"Alist of key-value suggestions for `chronometrist-key-value' prompts.
Each element must be in the form (\"TASK\" <KEYWORD> <VALUE> ...)"
:type
'(repeat
(cons
(string :tag "Task name")
(repeat :tag "Property preset"
(plist :tag "Property"
;; :key-type 'keyword :value-type 'sexp
))))
:group 'chronometrist-key-values)
(defun chronometrist-key-value-get-presets (task)
"Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists."
(alist-get task chronometrist-key-value-preset-alist nil nil #'equal))
(defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*"
"Name of buffer in which key-values are entered."
:group 'chronometrist-key-values
@ -241,83 +228,59 @@ containing keywords used with that task, in reverse chronological
order. The keywords are stored as strings and their leading \":\"
is removed.")
(defun chronometrist-key-history-populate (task history-table backend)
"Store key history for TASK in HISTORY-TABLE from FILE.
Return the new value inserted into HISTORY-TABLE.
HISTORY-TABLE must be a hash table (see `chronometrist-key-history')."
(puthash task nil history-table)
(cl-loop for plist in backend do
(catch 'quit
(let* ((name (plist-get plist :name))
(_check (unless (equal name task) (throw 'quit nil)))
(keys (--> (chronometrist-plist-key-values plist)
(seq-filter #'keywordp it)
(cl-loop for key in it collect
(chronometrist-keyword-to-string key))))
(_check (unless keys (throw 'quit nil)))
(old-keys (gethash name history-table)))
(puthash name
(if old-keys (append old-keys keys) keys)
history-table))))
(chronometrist-history-prep task history-table))
(defvar chronometrist-value-history
(make-hash-table :test #'equal)
"Hash table to store previously-used values for user-keys.
The hash table keys are user-key names (as strings), and the
values are lists containing values (as strings).")
(defun chronometrist-ht-history-prep (table)
"Prepare history hash tables for use in prompts.
Each value in hash table TABLE must be a list. Each value will be
reversed and will have duplicate elements removed."
(cl-loop for list being the hash-values of table
using (hash-keys key) do
(puthash key
;; placing `reverse' after `remove-duplicates'
;; to get a list in reverse chronological order
(-> (-flatten list)
(cl-remove-duplicates :test #'equal)
(reverse))
table)
finally return table))
(defun chronometrist-key-history-populate (events-table history-table)
"Clear HISTORY-TABLE and store key history in it, using EVENTS-TABLE.
Return the new value of HISTORY-TABLE.
EVENTS-TABLE and HISTORY-TABLE must be hash tables (see `chronometrist-events' and `chronometrist-key-history')."
(defun chronometrist-value-history-populate (history-table backend)
"Store value history in HISTORY-TABLE from FILE.
HISTORY-TABLE must be a hash table. (see `chronometrist-value-history')"
(clrhash history-table)
;; add each task as a key
(mapc (lambda (task)
(puthash task nil history-table))
;; ;; Not necessary, if the only place this is called is `chronometrist-refresh-file'
;; (setq chronometrist--task-list (chronometrist-tasks-from-table))
chronometrist-task-list)
(cl-loop for events being the hash-values of events-table do
(cl-loop for event in events do
(let ((name (plist-get event :name))
(keys (->> (chronometrist-plist-remove event :name :start :stop :tags)
(seq-filter #'keywordp))))
(cl-loop for key in keys do
(when key
(let ((old-keys (gethash name history-table))
(new-key (->> (symbol-name key)
(s-chop-prefix ":")
(list))))
(--> (if old-keys
(append old-keys new-key)
new-key)
(puthash name it history-table))))))))
(chronometrist-ht-history-prep history-table))
(defun chronometrist-value-history-populate (events-table history-table)
"Clear HISTORY-TABLE and store value history in it, using EVENTS-TABLE.
Return the new value of HISTORY-TABLE.
EVENTS-TABLE and HISTORY-TABLE must be hash tables. (see
`chronometrist-events' and `chronometrist-value-history')"
;; Note - while keys are Lisp keywords, values may be any Lisp
;; object, including lists
(clrhash history-table)
(cl-loop with user-key-values
for events-list being the hash-values of events-table do
(cl-loop for event in events-list do
;; We call them user-key-values because we filter out Chronometrist's
;; reserved key-values
(setq user-key-values (chronometrist-plist-remove event :name :tags :start :stop))
(cl-loop for plist in (chronometrist-to-list backend) do
;; We call them user-key-values because we filter out Chronometrist's
;; reserved key-values
(let ((user-key-values (chronometrist-plist-key-values plist)))
(cl-loop for (key value) on user-key-values by #'cddr do
(let* ((key-string (->> (symbol-name key) (s-chop-prefix ":")))
(let* ((key-string (chronometrist-keyword-to-string key))
(old-values (gethash key-string history-table))
(value (if (not (stringp value))
(value (if (not (stringp value)) ;; why?
(list (format "%S" value))
(list value))))
(puthash key-string
(if old-values
(append old-values value)
value)
(if old-values (append old-values value) value)
history-table)))))
(chronometrist-ht-history-prep history-table))
(maphash (lambda (key _values)
(chronometrist-history-prep key history-table))
history-table))
(defvar chronometrist--value-suggestions nil
"Suggestions for values.
Used as history by `chronometrist-value-prompt'.")
(defvar chronometrist-kv-read-mode-map
(let ((map (make-sparse-keymap)))
@ -345,24 +308,21 @@ It currently supports ido, ido-ubiquitous, ivy, and helm."
"\\<helm-comp-read-map>\\[helm-cr-empty-string]")
(t "leave blank"))))
(defun chronometrist-string-has-whitespace-p (string)
"Return non-nil if STRING contains whitespace."
(string-match-p "[[:space:]]" string))
(defun chronometrist-key-prompt (used-keys)
"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)
(plist-get it :name)
(gethash it chronometrist-key-history))))
(completing-read (format "Key (%s to quit): " (chronometrist-kv-completion-quit-key))
(let ((key-suggestions (--> (chronometrist-latest-record (chronometrist-active-backend))
(plist-get it :name)
(gethash it chronometrist-key-history))))
(completing-read (format "Key (%s to quit): "
(chronometrist-kv-completion-quit-key))
;; don't suggest keys which have already been used
(cl-loop for used-key in used-keys do
(->> key-suggestions
(seq-remove (lambda (key)
(equal key used-key)))
(setq key-suggestions))
(setq key-suggestions
(seq-remove (lambda (key)
(equal key used-key))
key-suggestions))
finally return key-suggestions)
nil nil nil 'key-suggestions)))
@ -370,8 +330,10 @@ of `chronometrist-kv-add'."
"Prompt the user to enter values.
KEY should be a string for the just-entered key."
(setq chronometrist--value-suggestions (gethash key chronometrist-value-history))
(completing-read (format "Value (%s to quit): " (chronometrist-kv-completion-quit-key))
chronometrist--value-suggestions nil nil nil 'chronometrist--value-suggestions))
(completing-read (format "Value (%s to quit): "
(chronometrist-kv-completion-quit-key))
chronometrist--value-suggestions nil nil nil
'chronometrist--value-suggestions))
(defun chronometrist-value-insert (value)
"Insert VALUE into the key-value entry buffer."
@ -394,56 +356,61 @@ to add them to the last s-expression in `chronometrist-file', or
_ARGS are ignored. This function always returns t, so it can be
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))
(used-keys (->> (seq-filter #'keywordp last-kvs)
(mapcar #'symbol-name)
(--map (s-chop-prefix ":" it)))))
(switch-to-buffer buffer)
(with-current-buffer buffer
(chronometrist-common-clear-buffer buffer)
(chronometrist-kv-read-mode)
(if (and (chronometrist-current-task) last-kvs)
(progn
(chronometrist-plist-pp last-kvs buffer)
(down-list -1)
(insert "\n "))
(insert "()")
(down-list -1))
(catch 'empty-input
(let (input key value)
(while t
(setq key (chronometrist-key-prompt used-keys)
input key
used-keys (append used-keys
(list key)))
(if (string-empty-p input)
(throw 'empty-input nil)
(unless first-key-p
(insert " "))
(insert ":" key)
(setq first-key-p nil))
(setq value (chronometrist-value-prompt key)
input value)
(if (string-empty-p input)
(throw 'empty-input nil)
(chronometrist-value-insert value)))))
(chronometrist-sexp-reindent-buffer))))
t)
(interactive)
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
(first-key-p t)
(backend (chronometrist-active-backend))
(last-sexp (chronometrist-latest-record backend))
(last-name (plist-get last-sexp :name))
(last-kvs (chronometrist-plist-key-values last-sexp))
(used-keys (--map (chronometrist-keyword-to-string it)
(seq-filter #'keywordp last-kvs))))
(chronometrist-key-history-populate last-name chronometrist-key-history backend)
(chronometrist-value-history-populate chronometrist-value-history backend)
(switch-to-buffer buffer)
(with-current-buffer buffer
(erase-buffer)
(chronometrist-kv-read-mode)
(if (and (chronometrist-current-task (chronometrist-active-backend)) last-kvs)
(progn
(funcall chronometrist-sexp-pretty-print-function last-kvs buffer)
(down-list -1)
(insert "\n "))
(insert "()")
(down-list -1))
(catch 'empty-input
(let (input key value)
(while t
(setq key (chronometrist-key-prompt used-keys)
input key
used-keys (append used-keys
(list key)))
(if (string-empty-p input)
(throw 'empty-input nil)
(unless first-key-p
(insert " "))
(insert ":" key)
(setq first-key-p nil))
(setq value (chronometrist-value-prompt key)
input value)
(if (string-empty-p input)
(throw 'empty-input nil)
(chronometrist-value-insert value)))))
(chronometrist-sexp-reindent-buffer))
t))
;;;; COMMANDS ;;;;
(defun chronometrist-kv-accept ()
"Accept the plist in `chronometrist-kv-buffer-name' and add it to `chronometrist-file'."
(interactive)
(let (user-kv-expr)
(let* ((backend (chronometrist-active-backend))
(latest (chronometrist-latest-record backend))
user-kv-expr)
(with-current-buffer (get-buffer chronometrist-kv-buffer-name)
(goto-char (point-min))
(setq user-kv-expr (ignore-errors (read (current-buffer))))
(kill-buffer chronometrist-kv-buffer-name))
(if user-kv-expr
(chronometrist-append-to-last nil user-kv-expr)
(chronometrist-replace-last backend (chronometrist-plist-update latest user-kv-expr))
(chronometrist-refresh))))
(defun chronometrist-kv-reject ()
@ -452,33 +419,44 @@ used in `chronometrist-before-out-functions'."
(kill-buffer chronometrist-kv-buffer-name)
(chronometrist-refresh))
;;;; SKIPPING QUERIES ;;;;
(defvar chronometrist--skip-detail-prompts nil)
(easy-menu-define chronometrist-key-value-menu chronometrist-mode-map
"Key value menu for Chronometrist mode."
'("Key-Values"
["Change tags for active/last interval" chronometrist-tags-add]
["Change key-values for active/last interval" chronometrist-kv-add]
["Change tags and key-values for active/last interval"
chronometrist-key-values-unified-prompt]))
(defun chronometrist-skip-query-prompt (task)
"Offer to skip tag/key-value prompts and reuse last-used details.
This function always returns t, so it can be used in `chronometrist-before-out-functions'."
;; find latest interval for TASK; if it has tags or key-values, prompt
(let (plist)
;; iterate over events in reverse
(cl-loop for key in (reverse (hash-table-keys chronometrist-events)) do
(cl-loop for event in (reverse (gethash key chronometrist-events))
when (and (equal task (plist-get event :name))
(setq plist (chronometrist-plist-remove event :name :start :stop)))
return nil)
when plist return nil)
(and plist
(yes-or-no-p
(format "Skip prompt and use last-used tags/key-values? %S " plist))
(setq chronometrist--skip-detail-prompts t)
(chronometrist-append-to-last (plist-get plist :tags) plist))
t))
(defun chronometrist-skip-query-reset (_task)
"Enable prompting for tags and key-values.
This function always returns t, so it can be used in `chronometrist-before-out-functions'."
(setq chronometrist--skip-detail-prompts nil) t)
(cl-defun chronometrist-key-values-unified-prompt
(&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name)))
"Query user for tags and key-values to be added for TASK.
Return t, to permit use in `chronometrist-before-out-functions'."
(interactive)
(let* ((backend (chronometrist-active-backend))
(presets (--map (format "%S" it)
(chronometrist-key-value-get-presets task)))
(key-values
(when chronometrist-key-value-use-database-history
(cl-loop for plist in (chronometrist-to-list backend)
when (equal (plist-get plist :name) task)
collect
(let ((plist (chronometrist-plist-remove plist :name :start :stop)))
(when plist (format "%S" plist)))
into key-value-plists
finally return
(--> (seq-filter #'identity key-value-plists)
(cl-remove-duplicates it :test #'equal :from-end t)))))
(latest (chronometrist-latest-record backend)))
(if (and (null presets) (null key-values))
(progn (chronometrist-tags-add) (chronometrist-kv-add))
(let* ((candidates (append presets key-values))
(input (completing-read
(format "Key-values for %s: " task)
candidates nil nil nil 'chronometrist-key-values-unified-prompt-history)))
(chronometrist-replace-last backend
(chronometrist-plist-update latest
(read input))))))
t)
(provide 'chronometrist-key-values)
;;; chronometrist-key-values.el ends here

View File

@ -0,0 +1,788 @@
#+TITLE: chronometrist-key-values
#+SUBTITLE: Key-value support for Chronometrist
#+AUTHOR: contrapunctus
#+TODO: TODO TEST WIP EXTEND CLEANUP FIXME REVIEW |
#+PROPERTY: header-args :tangle yes :load yes
* TODO [50%]
1. [X] Remove calls from =chronometrist.org= to make this an optional dependency.
2. [ ] key-values and tags should work regardless of what hook they're called from, including =chronometrist-before-in-functions=
3. [ ] investigate =rmc.el= (read multiple choice) as an alternative to =choice.el=
* About this file
** Definition metadata
Each definition has its own heading. The type of definition is stored in tags -
1. custom group
2. [custom|internal] variable
3. keymap (use variable instead?)
4. macro
5. function
* does not refer to external state
* primarily used for the return value
6. reader
* reads external state without modifying it
* primarily used for the return value
7. writer
* modifies external state, namely a data structure or file
* primarily used for side-effects
8. procedure
* any other impure function
* usually affects the display
* primarily used for side-effects
9. major/minor mode
10. command
A =:hook:variable:= is a variable which contains a list of functions; a =:hook:= tag with any of the function tags means a function meant to be added to a hook.
Further details are stored in properties -
1. :INPUT: (for functions)
2. :VALUE: list|hash table|...
* for functions, this is the return value
3. :STATE: <external file or data structure read or written to>
* Explanation
:PROPERTIES:
:DESCRIPTION: How tags and key-values are implemented
:END:
[[file:chronometrist-key-values.org][chronometrist-key-values.org]] 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:1] 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:1] 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 [[kv-add][=chronometrist-kv-add=]] and [[tags-add][=chronometrist-tags-add=]]. The user adds these to the desired hooks, and they prompt the user for tags/key-values.
Both have corresponding functions to create a prompt -
+ [[key-prompt][=chronometrist-key-prompt=]],
+ [[value-prompt][=chronometrist-value-prompt=]], and
+ [[tags-prompt][=chronometrist-tags-prompt=]].
[[kv-add][=chronometrist-kv-add=]]'s way of reading key-values from the user is somewhat different from most Emacs prompts - it creates a new buffer, and uses the minibuffer to alternatingly ask for keys and values in a loop. Key-values are inserted into the buffer as the user enters/selects them. The user can break out of this loop with an empty input (the keys to accept an empty input differ between completion systems, so we try to let the user know about them using [[kv-completion-quit-key][=chronometrist-kv-completion-quit-key=]]). After exiting the loop, they can edit the key-values in the buffer, and use the commands [[kv-accept][=chronometrist-kv-accept=]] to accept the key-values (which uses [[plist-update][=chronometrist-plist-update=]] to add them to the last plist in =chronometrist-file=) or [[kv-reject][=chronometrist-kv-reject=]] to discard them.
** History
All prompts suggest past user inputs. These are queried from three history hash tables -
+ [[key-history][=chronometrist-key-history=]],
+ [[value-history][=chronometrist-value-history=]], and
+ [[tags-history][=chronometrist-tags-history=]].
Each of these has a corresponding function to clear it and fill it with values -
+ [[key-history-populate][=chronometrist-key-history-populate=]]
+ [[value-history-populate][=chronometrist-value-history-populate=]], and
+ [[tags-history-populate][=chronometrist-tags-history-populate=]].
* Library headers and commentary
#+BEGIN_SRC emacs-lisp
;;; chronometrist-key-values.el --- add key-values to Chronometrist data -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((chronometrist "0.7.0"))
;; Version: 0.1.0
;; 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>
#+END_SRC
"Commentary" is displayed when the user clicks on the package's entry in =M-x list-packages=.
#+BEGIN_SRC emacs-lisp
;;; Commentary:
;;
;; This package lets users attach tags and key-values to their tracked time, similar to tags and properties in Org mode.
;;
;; To use, add one or more of these functions to any chronometrist hook except `chronometrist-before-in-functions'.
;; * `chronometrist-tags-add'
;; * `chronometrist-kv-add'
;; * `chronometrist-key-values-unified-prompt'
#+END_SRC
* Dependencies
#+BEGIN_SRC emacs-lisp
;;; Code:
(require 'chronometrist)
#+END_SRC
* Code
** Common
*** history-prep :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-history-prep (key history-table)
"Prepare history of KEY in HISTORY-TABLE for use in prompts.
Each value in hash table TABLE must be a list. Each value will be reversed and will have duplicate elements removed."
(--> (gethash key history-table)
(cl-remove-duplicates it :test #'equal :from-end t)
(puthash key it history-table)))
#+END_SRC
*** keyword-to-string :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-keyword-to-string (keyword)
"Return KEYWORD as a string, with the leading \":\" removed."
(replace-regexp-in-string "^:?" "" (symbol-name keyword)))
#+END_SRC
*** maybe-string-to-symbol :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-maybe-string-to-symbol (list)
"For each string in LIST, if it has no spaces, convert it to a symbol."
(cl-loop for string in list
if (string-match-p "[[:space:]]" string)
collect string
else collect (intern string)))
#+END_SRC
*** maybe-symbol-to-string :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-maybe-symbol-to-string (list)
"Convert each symbol in LIST to a string."
(--map (if (symbolp it)
(symbol-name it)
it)
list))
#+END_SRC
*** plist-update :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-plist-update (old-plist new-plist)
"Add tags and keyword-values from NEW-PLIST to OLD-PLIST.
OLD-PLIST and NEW-PLIST should be a property lists.
Keywords reserved by Chronometrist - :name, :start, and :stop -
will not be updated. Keywords in OLD-PLIST with new values in
NEW-PLIST will be updated. Tags in OLD-PLIST will be preserved
alongside new tags from NEW-PLIST."
(-let* (((&plist :name old-name :tags old-tags
:start old-start :stop old-stop) old-plist)
;; Anything that's left will be the user's key-values.
(old-kvs (chronometrist-plist-key-values old-plist))
;; Prevent the user from adding reserved key-values.
(plist (chronometrist-plist-key-values new-plist))
(new-tags (-> (append old-tags (plist-get new-plist :tags))
(cl-remove-duplicates :test #'equal)))
;; In case there is an overlap in key-values, we use
;; plist-put to replace old ones with new ones.
(new-kvs (cl-copy-list old-plist))
(new-kvs (if plist
(-> (cl-loop for (key val) on plist by #'cddr
do (plist-put new-kvs key val)
finally return new-kvs)
(chronometrist-plist-key-values))
old-kvs)))
(append `(:name ,old-name)
(when new-tags `(:tags ,new-tags))
new-kvs
`(:start ,old-start)
(when old-stop `(:stop ,old-stop)))))
#+END_SRC
** Tags
*** tags-history :variable:
:PROPERTIES:
:VALUE: hash table
:END:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
"Hash table of tasks and past tag combinations.
Each value is a list of tag combinations, in reverse
chronological order. Each combination is a list containing tags
as symbol and/or strings.")
#+END_SRC
*** tags-history-populate :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-tags-history-populate (task history-table backend)
"Store tag history for TASK in HISTORY-TABLE from FILE.
Return the new value inserted into HISTORY-TABLE.
HISTORY-TABLE must be a hash table. (see `chronometrist-tags-history')"
(puthash task nil history-table)
(cl-loop for plist in (chronometrist-to-list backend) do
(let ((new-tag-list (plist-get plist :tags))
(old-tag-lists (gethash task history-table)))
(and (equal task (plist-get plist :name))
new-tag-list
(puthash task
(if old-tag-lists
(append old-tag-lists (list new-tag-list))
(list new-tag-list))
history-table))))
(chronometrist-history-prep task history-table))
#+END_SRC
**** tests
#+BEGIN_SRC emacs-lisp :tangle chronometrist-key-values-tests.el :load test
(ert-deftest chronometrist-tags-history ()
(progn
(clrhash chronometrist-tags-history)
(cl-loop for task in '("Guitar" "Programming") do
(chronometrist-tags-history-populate task chronometrist-tags-history "test.sexp")))
(should
(= (hash-table-count chronometrist-tags-history) 2))
(should
(cl-loop for task being the hash-keys of chronometrist-tags-history
always (stringp task)))
(should
(equal (gethash "Guitar" chronometrist-tags-history)
'((classical solo)
(classical warm-up))))
(should
(equal (gethash "Programming" chronometrist-tags-history)
'((reading) (bug-hunting)))))
#+END_SRC
*** -tag-suggestions :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist--tag-suggestions nil
"Suggestions for tags.
Used as history by `chronometrist-tags-prompt'.")
#+END_SRC
*** tags-history-add :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-tags-history-add (plist)
"Add tags from PLIST to `chronometrist-tags-history'."
(let* ((table chronometrist-tags-history)
(name (plist-get plist :name))
(tags (plist-get plist :tags))
(old-tags (gethash name table)))
(when tags
(--> (cons tags old-tags)
(puthash name it table)))))
#+END_SRC
*** tags-history-combination-strings :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-tags-history-combination-strings (task)
"Return list of past tag combinations for TASK.
Each combination is a string, with tags separated by commas.
This is used to provide history for `completing-read-multiple' in
`chronometrist-tags-prompt'."
(->> (gethash task chronometrist-tags-history)
(mapcar (lambda (list)
(->> list
(mapcar (lambda (elt)
(if (stringp elt)
elt
(symbol-name elt))))
(-interpose ",")
(apply #'concat))))))
#+END_SRC
*** tags-history-individual-strings :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-tags-history-individual-strings (task)
"Return list of tags for TASK, with each tag being a single string.
This is used to provide completion for individual tags, in
`completing-read-multiple' in `chronometrist-tags-prompt'."
(--> (gethash task chronometrist-tags-history)
(-flatten it)
(cl-remove-duplicates it :test #'equal)
(cl-loop for elt in it
collect (if (stringp elt)
elt
(symbol-name elt)))))
#+END_SRC
*** tags-prompt :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-tags-prompt (task &optional initial-input)
"Read one or more tags from the user and return them as a list of strings.
TASK should be a string.
INITIAL-INPUT is as used in `completing-read'."
(setq chronometrist--tag-suggestions (chronometrist-tags-history-combination-strings task))
(completing-read-multiple (concat "Tags for " task " (optional): ")
(chronometrist-tags-history-individual-strings task)
nil
'confirm
initial-input
'chronometrist--tag-suggestions))
#+END_SRC
*** tags-add :hook:writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-tags-add (&rest _args)
"Read tags from the user; add them to the last entry in `chronometrist-file'.
_ARGS are ignored. This function always returns t, so it can be
used in `chronometrist-before-out-functions'."
(interactive)
(let* ((backend (chronometrist-active-backend))
(last-expr (chronometrist-latest-record backend))
(last-name (plist-get last-expr :name))
(_history (chronometrist-tags-history-populate last-name chronometrist-tags-history backend))
(last-tags (plist-get last-expr :tags))
(input (->> (chronometrist-maybe-symbol-to-string last-tags)
(-interpose ",")
(apply #'concat)
(chronometrist-tags-prompt last-name)
(chronometrist-maybe-string-to-symbol))))
(when input
(--> (append last-tags input)
(reverse it)
(cl-remove-duplicates it :test #'equal)
(reverse it)
(list :tags it)
(chronometrist-plist-update
(chronometrist-latest-record backend) it)
(chronometrist-replace-last backend it)))
t))
#+END_SRC
** Key-Values
*** key-values :custom:group:
#+BEGIN_SRC emacs-lisp
(defgroup chronometrist-key-values nil
"Add key-values to Chronometrist time intervals."
:group 'chronometrist)
#+END_SRC
*** use-database-history :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-key-value-use-database-history t
"If non-nil, use database to generate key-value suggestions.
If nil, only `chronometrist-key-value-preset-alist' is used."
:type 'boolean
:group 'chronometrist-key-value)
#+END_SRC
*** preset-alist :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-key-value-preset-alist nil
"Alist of key-value suggestions for `chronometrist-key-value' prompts.
Each element must be in the form (\"TASK\" <KEYWORD> <VALUE> ...)"
:type
'(repeat
(cons
(string :tag "Task name")
(repeat :tag "Property preset"
(plist :tag "Property"
;; :key-type 'keyword :value-type 'sexp
))))
:group 'chronometrist-key-values)
#+END_SRC
**** get-presets
#+BEGIN_SRC emacs-lisp
(defun chronometrist-key-value-get-presets (task)
"Return presets for TASK from `chronometrist-key-value-preset-alist' as a list of plists."
(alist-get task chronometrist-key-value-preset-alist nil nil #'equal))
#+END_SRC
*** kv-buffer-name :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-kv-buffer-name "*Chronometrist-Key-Values*"
"Name of buffer in which key-values are entered."
:group 'chronometrist-key-values
:type 'string)
#+END_SRC
*** key-history :variable:
:PROPERTIES:
:VALUE: hash table
:END:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-key-history
(make-hash-table :test #'equal)
"Hash table to store previously-used user-keys.
Each hash key is the name of a task. Each hash value is a list
containing keywords used with that task, in reverse chronological
order. The keywords are stored as strings and their leading \":\"
is removed.")
#+END_SRC
*** key-history-populate :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-key-history-populate (task history-table backend)
"Store key history for TASK in HISTORY-TABLE from FILE.
Return the new value inserted into HISTORY-TABLE.
HISTORY-TABLE must be a hash table (see `chronometrist-key-history')."
(puthash task nil history-table)
(cl-loop for plist in backend do
(catch 'quit
(let* ((name (plist-get plist :name))
(_check (unless (equal name task) (throw 'quit nil)))
(keys (--> (chronometrist-plist-key-values plist)
(seq-filter #'keywordp it)
(cl-loop for key in it collect
(chronometrist-keyword-to-string key))))
(_check (unless keys (throw 'quit nil)))
(old-keys (gethash name history-table)))
(puthash name
(if old-keys (append old-keys keys) keys)
history-table))))
(chronometrist-history-prep task history-table))
#+END_SRC
**** tests
#+BEGIN_SRC emacs-lisp :tangle chronometrist-key-values-tests.el :load test
(ert-deftest chronometrist-key-history ()
(progn
(clrhash chronometrist-key-history)
(cl-loop for task in '("Programming" "Arrangement/new edition") do
(chronometrist-key-history-populate task chronometrist-key-history "test.sexp")))
(should (= (hash-table-count chronometrist-key-history) 2))
(should (= (length (gethash "Programming" chronometrist-key-history)) 3))
(should (= (length (gethash "Arrangement/new edition" chronometrist-key-history)) 2)))
#+END_SRC
*** value-history :variable:
:PROPERTIES:
:VALUE: hash table
:END:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-value-history
(make-hash-table :test #'equal)
"Hash table to store previously-used values for user-keys.
The hash table keys are user-key names (as strings), and the
values are lists containing values (as strings).")
#+END_SRC
*** value-history-populate :writer:
We don't want values to be task-sensitive, so this does not have a KEY parameter similar to TASK for =chronometrist-tags-history-populate= or =chronometrist-key-history-populate=.
#+BEGIN_SRC emacs-lisp
(defun chronometrist-value-history-populate (history-table backend)
"Store value history in HISTORY-TABLE from FILE.
HISTORY-TABLE must be a hash table. (see `chronometrist-value-history')"
(clrhash history-table)
;; Note - while keys are Lisp keywords, values may be any Lisp
;; object, including lists
(cl-loop for plist in (chronometrist-to-list backend) do
;; We call them user-key-values because we filter out Chronometrist's
;; reserved key-values
(let ((user-key-values (chronometrist-plist-key-values plist)))
(cl-loop for (key value) on user-key-values by #'cddr do
(let* ((key-string (chronometrist-keyword-to-string key))
(old-values (gethash key-string history-table))
(value (if (not (stringp value)) ;; why?
(list (format "%S" value))
(list value))))
(puthash key-string
(if old-values (append old-values value) value)
history-table)))))
(maphash (lambda (key _values)
(chronometrist-history-prep key history-table))
history-table))
#+END_SRC
**** tests
#+BEGIN_SRC emacs-lisp :tangle chronometrist-key-values-tests.el :load test
(ert-deftest chronometrist-value-history ()
(progn
(clrhash chronometrist-value-history)
(chronometrist-value-history-populate chronometrist-value-history "test.sexp"))
(should (= (hash-table-count chronometrist-value-history) 5))
(should
(cl-loop for task being the hash-keys of chronometrist-value-history
always (stringp task))))
#+END_SRC
*** -value-suggestions :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist--value-suggestions nil
"Suggestions for values.
Used as history by `chronometrist-value-prompt'.")
#+END_SRC
*** kv-read-mode-map :keymap:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-kv-read-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'chronometrist-kv-accept)
(define-key map (kbd "C-c C-k") #'chronometrist-kv-reject)
map)
"Keymap used by `chronometrist-kv-read-mode'.")
#+END_SRC
*** kv-read-mode :major:mode:
#+BEGIN_SRC emacs-lisp
(define-derived-mode chronometrist-kv-read-mode emacs-lisp-mode "Key-Values"
"Mode used by `chronometrist' to read key values from the user."
(->> ";; Use \\[chronometrist-kv-accept] to accept, or \\[chronometrist-kv-reject] to cancel\n"
(substitute-command-keys)
(insert)))
#+END_SRC
*** kv-completion-quit-key :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-kv-completion-quit-key ()
"Return appropriate keybinding (as a string) to quit from `completing-read'.
It currently supports ido, ido-ubiquitous, ivy, and helm."
(substitute-command-keys
(cond ((or (bound-and-true-p ido-mode)
(bound-and-true-p ido-ubiquitous-mode))
"\\<ido-completion-map>\\[ido-select-text]")
((bound-and-true-p ivy-mode)
"\\<ivy-minibuffer-map>\\[ivy-immediate-done]")
((bound-and-true-p helm-mode)
"\\<helm-comp-read-map>\\[helm-cr-empty-string]")
(t "leave blank"))))
#+END_SRC
*** key-prompt :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-key-prompt (used-keys)
"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-latest-record (chronometrist-active-backend))
(plist-get it :name)
(gethash it chronometrist-key-history))))
(completing-read (format "Key (%s to quit): "
(chronometrist-kv-completion-quit-key))
;; don't suggest keys which have already been used
(cl-loop for used-key in used-keys do
(setq key-suggestions
(seq-remove (lambda (key)
(equal key used-key))
key-suggestions))
finally return key-suggestions)
nil nil nil 'key-suggestions)))
#+END_SRC
*** value-prompt :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-value-prompt (key)
"Prompt the user to enter values.
KEY should be a string for the just-entered key."
(setq chronometrist--value-suggestions (gethash key chronometrist-value-history))
(completing-read (format "Value (%s to quit): "
(chronometrist-kv-completion-quit-key))
chronometrist--value-suggestions nil nil nil
'chronometrist--value-suggestions))
#+END_SRC
*** value-insert :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-value-insert (value)
"Insert VALUE into the key-value entry buffer."
(insert " ")
(cond ((or
;; list or vector
(and (string-match-p (rx (and bos (or "(" "\"" "["))) value)
(string-match-p (rx (and (or ")" "\"" "]") eos)) value))
;; int or float
(string-match-p "^[0-9]*\\.?[0-9]*$" value))
(insert value))
(t (insert "\"" value "\"")))
(insert "\n"))
#+END_SRC
*** kv-add :hook:writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-kv-add (&rest _args)
"Read key-values from user, adding them to a temporary buffer for review.
In the resulting buffer, users can run `chronometrist-kv-accept'
to add them to the last s-expression in `chronometrist-file', or
`chronometrist-kv-reject' to cancel.
_ARGS are ignored. This function always returns t, so it can be
used in `chronometrist-before-out-functions'."
(interactive)
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
(first-key-p t)
(backend (chronometrist-active-backend))
(last-sexp (chronometrist-latest-record backend))
(last-name (plist-get last-sexp :name))
(last-kvs (chronometrist-plist-key-values last-sexp))
(used-keys (--map (chronometrist-keyword-to-string it)
(seq-filter #'keywordp last-kvs))))
(chronometrist-key-history-populate last-name chronometrist-key-history backend)
(chronometrist-value-history-populate chronometrist-value-history backend)
(switch-to-buffer buffer)
(with-current-buffer buffer
(erase-buffer)
(chronometrist-kv-read-mode)
(if (and (chronometrist-current-task (chronometrist-active-backend)) last-kvs)
(progn
(funcall chronometrist-sexp-pretty-print-function last-kvs buffer)
(down-list -1)
(insert "\n "))
(insert "()")
(down-list -1))
(catch 'empty-input
(let (input key value)
(while t
(setq key (chronometrist-key-prompt used-keys)
input key
used-keys (append used-keys
(list key)))
(if (string-empty-p input)
(throw 'empty-input nil)
(unless first-key-p
(insert " "))
(insert ":" key)
(setq first-key-p nil))
(setq value (chronometrist-value-prompt key)
input value)
(if (string-empty-p input)
(throw 'empty-input nil)
(chronometrist-value-insert value)))))
(chronometrist-sexp-reindent-buffer))
t))
#+END_SRC
*** kv-accept :command:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-kv-accept ()
"Accept the plist in `chronometrist-kv-buffer-name' and add it to `chronometrist-file'."
(interactive)
(let* ((backend (chronometrist-active-backend))
(latest (chronometrist-latest-record backend))
user-kv-expr)
(with-current-buffer (get-buffer chronometrist-kv-buffer-name)
(goto-char (point-min))
(setq user-kv-expr (ignore-errors (read (current-buffer))))
(kill-buffer chronometrist-kv-buffer-name))
(if user-kv-expr
(chronometrist-replace-last backend (chronometrist-plist-update latest user-kv-expr))
(chronometrist-refresh))))
#+END_SRC
*** kv-reject :command:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-kv-reject ()
"Reject the property list in `chronometrist-kv-buffer-name'."
(interactive)
(kill-buffer chronometrist-kv-buffer-name)
(chronometrist-refresh))
#+END_SRC
*** chronometrist-key-value-menu :menu:
#+BEGIN_SRC emacs-lisp
(easy-menu-define chronometrist-key-value-menu chronometrist-mode-map
"Key value menu for Chronometrist mode."
'("Key-Values"
["Change tags for active/last interval" chronometrist-tags-add]
["Change key-values for active/last interval" chronometrist-kv-add]
["Change tags and key-values for active/last interval"
chronometrist-key-values-unified-prompt]))
#+END_SRC
** WIP Single-key prompts [0%]
This was initially implemented using Hydra. But, at the moment of reckoning, it turned out that Hydra does not pause Emacs until the user provides an input, and is thus unsuited for use in a hook. Thus, we created a new library called =choice.el= which functions similarly to Hydra (associations of keys, Lisp forms, and hints are passed to a macro which emits a prompt function) and used that.
Then I discovered that there's =rmc.el= which does about the same thing.
1. [ ] Rewrite these using =rmc.el=
Types of prompts planned (#1 and #2 are meant to be mixed and matched)
1. [-] =(tag|key-value)-combination-choice= - select combinations of (tags|key-values)
* commands
+ 0-9 - use combination (and exit)
+ C-u 0-9 - edit combination (then exit)
+ s - skip (exit)
+ (b - back [to previous prompt])
* [X] tag-combination-prompt
* [ ] key-value-combination-prompt
2. [ ] =(tag|key|value)-multiselect-choice= - select individual (tags|keys|values)
* commands
+ 0-9 - select (toggles; save in var; doesn't exit)
+ u - use selection (and exit)
+ e - edit selection (then exit)
+ n - new tag/key/value
+ s - skip (exit)
+ (b - back [to previous prompt])
Great for values; makes it easy to add multiple values, too, especially for users who don't know Lisp.
3. [-] =unified-choice= - select tag-key-value combinations, all in one prompt
* commands
+ 0-9 - use combination (and exit)
+ C-u 0-9 - edit combination (then exit)
+ s - skip (exit)
* [X] basic implementation
* [ ] make it more aesthetically pleasing in case of long suggestion strings
*** defchoice :function:
#+BEGIN_SRC emacs-lisp :tangle no :load no
(defun chronometrist-defchoice (name type list)
"Construct and evaluate a `defchoice' form.
NAME should be a string - `defchoice' will be called with chronometrist-NAME.
TYPE should be a :key-values or :tags.
LIST should be a list, with all elements being either a plists,
or lists of symbols."
(cl-loop with backend = (chronometrist-active-backend)
with num = 0
with last = (chronometrist-latest-record backend)
for elt in (-take 7 list)
do (incf num)
if (= num 10) do (setq num 0)
collect
(list (format "%s" num)
`(chronometrist-replace-last
backend
(chronometrist-plist-update last
',(cl-case type
(:tags (list :tags elt))
(:key-values elt))))
(format "%s" elt)) into numeric-commands
finally do (eval
`(defchoice ,(intern (format "chronometrist-%s" name))
,@numeric-commands
("s" nil "skip")))))
#+END_SRC
*** tag-choice :function:
#+BEGIN_SRC emacs-lisp :tangle no :load no
(defun chronometrist-tag-choice (task)
"Query user for tags to be added to TASK.
Return t, to permit use in `chronometrist-before-out-functions'."
(let ((table chronometrist-tags-history))
(chronometrist-tags-history-populate task table (chronometrist-active-backend))
(if (hash-table-empty-p table)
(chronometrist-tags-add)
(chronometrist-defchoice "tag" :tag (gethash task table))
(chronometrist-tag-choice-prompt "Which tags?"))
t))
#+END_SRC
*** WIP chronometrist-key-choice :hook:writer:
#+BEGIN_SRC emacs-lisp :tangle no :load no
(defun chronometrist-key-choice (task)
"Query user for keys to be added to TASK.
Return t, to permit use in `chronometrist-before-out-functions'."
(let ((table chronometrist-key-history))
(chronometrist-key-history-populate task table (chronometrist-active-backend))
(if (hash-table-empty-p table)
(chronometrist-kv-add)
(chronometrist-defchoice :key task table)
(chronometrist-key-choice-prompt "Which keys?"))
t))
#+END_SRC
*** WIP chronometrist-kv-prompt-helper :function:
#+BEGIN_SRC emacs-lisp :tangle no :load no
(defun chronometrist-kv-prompt-helper (mode task)
(let ((table (case mode
(:tag chronometrist-tags-history)
(:key chronometrist-key-history)
(:value chronometrist-value-history)))
())))
#+END_SRC
*** WIP unified-prompt :hook:writer:
:PROPERTIES:
:CUSTOM_ID: unified-prompt
:END:
1. [ ] Improve appearance - is there an easy way to syntax highlight the plists?
#+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-key-values-unified-prompt
(&optional (task (plist-get (chronometrist-latest-record (chronometrist-active-backend)) :name)))
"Query user for tags and key-values to be added for TASK.
Return t, to permit use in `chronometrist-before-out-functions'."
(interactive)
(let* ((backend (chronometrist-active-backend))
(presets (--map (format "%S" it)
(chronometrist-key-value-get-presets task)))
(key-values
(when chronometrist-key-value-use-database-history
(cl-loop for plist in (chronometrist-to-list backend)
when (equal (plist-get plist :name) task)
collect
(let ((plist (chronometrist-plist-remove plist :name :start :stop)))
(when plist (format "%S" plist)))
into key-value-plists
finally return
(--> (seq-filter #'identity key-value-plists)
(cl-remove-duplicates it :test #'equal :from-end t)))))
(latest (chronometrist-latest-record backend)))
(if (and (null presets) (null key-values))
(progn (chronometrist-tags-add) (chronometrist-kv-add))
(let* ((candidates (append presets key-values))
(input (completing-read
(format "Key-values for %s: " task)
candidates nil nil nil 'chronometrist-key-values-unified-prompt-history)))
(chronometrist-replace-last backend
(chronometrist-plist-update latest
(read input))))))
t)
#+END_SRC
* Provide
#+BEGIN_SRC emacs-lisp
(provide 'chronometrist-key-values)
;;; chronometrist-key-values.el ends here
#+END_SRC
* Local variables :noexport:
# Local Variables:
# my-org-src-default-lang: "emacs-lisp"
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End:

View File

@ -1,127 +0,0 @@
;;; chronometrist-migrate.el --- Commands to aid in migrating from timeclock to chronometrist s-expr format -*- 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 'cl-lib)
(require 'dash)
(require 'seq)
(require 'chronometrist-common)
(require 'chronometrist-time)
(require 'chronometrist-plist-pp)
(defvar chronometrist-file)
(defvar chronometrist-migrate-table (make-hash-table))
;; TODO - support other timeclock codes (currently only "i" and "o"
;; are supported.)
(defun chronometrist-migrate-populate (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)
(with-current-buffer (find-file-noselect in-file)
(save-excursion
(goto-char (point-min))
(let ((key-counter 0))
(while (not (eobp))
(let* ((event-string (buffer-substring-no-properties (point-at-bol)
(point-at-eol)))
(event-list (split-string event-string "[ /:]"))
(code (cl-first event-list))
(date-time (--> event-list
(seq-drop it 1)
(seq-take it 6)
(mapcar #'string-to-number it)
(reverse it)
(apply #'encode-time it)
(chronometrist-format-time-iso8601 it)))
(project-or-comment
(replace-regexp-in-string
(rx (and (or "i" "o") " "
(and (= 4 digit) "/" (= 2 digit) "/" (= 2 digit) " ")
(and (= 2 digit) ":" (= 2 digit) ":" (= 2 digit))
(opt " ")))
""
event-string)))
(pcase code
("i"
(cl-incf key-counter)
(puthash key-counter
`(:name ,project-or-comment :start ,date-time)
chronometrist-migrate-table))
("o"
(--> (gethash key-counter chronometrist-migrate-table)
(append it
`(:stop ,date-time)
(when (and (stringp project-or-comment)
(not
(string= project-or-comment "")))
`(:comment ,project-or-comment)))
(puthash key-counter it chronometrist-migrate-table)))))
(forward-line)
(goto-char (point-at-bol))))
nil)))
(defvar timeclock-file)
(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
"): ")
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")
"): ")
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? "))
t)
(let ((output (find-file-noselect out-file)))
(with-current-buffer output
(chronometrist-common-clear-buffer output)
(chronometrist-migrate-populate in-file)
(maphash (lambda (_key value)
(chronometrist-plist-pp value output)
(insert "\n\n"))
chronometrist-migrate-table)
(save-buffer)))))
(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)))
(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)
(message "You can migrate later using `chronometrist-migrate-timelog-file->sexp-file'."))))
(provide 'chronometrist-migrate)
;;; chronometrist-migrate.el ends here

View File

@ -1,110 +0,0 @@
;;; chronometrist-plist-pp.el --- Functions to pretty print property lists -*- 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:
;;; potential improvements -
;;; * probably more robust code
;;; Code:
(require 'dash)
(defvar chronometrist-plist-pp-keyword-re ":[a-zA-Z0-9\-\?\+]+")
(defvar chronometrist-plist-pp-whitespace-re "[\t\s]+?")
(defun chronometrist-plist-pp-longest-keyword-length ()
"Find the length of the longest keyword.
This assumes there is a single plist in the current buffer."
(save-excursion
(let ((keyword-lengths-list)
(sexp))
(goto-char (point-min))
(ignore-errors (down-list 1))
(while (setq sexp (ignore-errors
(read (current-buffer))))
(when (symbolp sexp)
(setq keyword-lengths-list (append keyword-lengths-list
`(,(length (symbol-name sexp)))))))
(-> keyword-lengths-list
(sort #'>)
(car)))))
(defun chronometrist-plist-pp-buffer-keyword-helper (indent)
"Indent the keyword after point by INDENT spaces."
(looking-at chronometrist-plist-pp-keyword-re)
(let ((keyword (buffer-substring-no-properties (match-beginning 0)
(match-end 0))))
(delete-region (match-beginning 0)
(match-end 0))
(format (concat "% -" (number-to-string indent) "s")
keyword)))
(defun chronometrist-plist-pp-buffer ()
"Naive pretty-printer for plists."
(let ((indent (chronometrist-plist-pp-longest-keyword-length)))
(goto-char (point-min))
(while (not (eobp))
(cond
;; opening paren + first keyword
((looking-at-p (concat "(" chronometrist-plist-pp-keyword-re chronometrist-plist-pp-whitespace-re))
(ignore-errors (down-list 1))
(insert (chronometrist-plist-pp-buffer-keyword-helper indent))
(forward-sexp 1)
(insert "\n"))
((looking-at chronometrist-plist-pp-whitespace-re)
(delete-region (match-beginning 0)
(match-end 0)))
;; any other keyword
((looking-at chronometrist-plist-pp-keyword-re)
(insert " " (chronometrist-plist-pp-buffer-keyword-helper indent))
(forward-sexp)
(backward-sexp)
;; an alist as a value
(if (looking-at "((")
(progn
(ignore-errors (down-list 1))
(let (expr)
(while (setq expr (ignore-errors (read (current-buffer))))
;; end of alist
(unless (looking-at-p ")")
(insert "\n " (make-string (1+ indent) ?\s))))))
(forward-sexp 1))
(unless (looking-at-p ")")
(insert "\n")))
((and (looking-at ")") (bolp))
(delete-char -1)
(forward-char 1))
(t (forward-char 1))))))
(defun chronometrist-plist-pp-to-string (object)
"Convert OBJECT to a pretty-printed string."
(with-temp-buffer
(lisp-mode-variables nil)
(set-syntax-table emacs-lisp-mode-syntax-table)
(let ((print-quoted t))
(prin1 object (current-buffer)))
(when (> (length object) 2)
(chronometrist-plist-pp-buffer))
(buffer-string)))
(defun chronometrist-plist-pp (object &optional stream)
"Pretty-print OBJECT and output to STREAM (see `princ')."
(princ (chronometrist-plist-pp-to-string object)
(or stream standard-output)))
(provide 'chronometrist-plist-pp)
;;; chronometrist-plist-pp.el ends here

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

@ -1,44 +0,0 @@
;;; chronometrist-report-custom --- custom definitions for `chronometrist-report' -*- 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:
(defgroup chronometrist-report nil
"Weekly report for the `chronometrist' time tracker."
:group 'chronometrist)
(defcustom chronometrist-report-buffer-name "*Chronometrist-Report*"
"The name of the buffer created by `chronometrist-report'."
:type 'string)
(defcustom chronometrist-report-week-start-day "Sunday"
"The day used for start of week by `chronometrist-report'."
:type 'string)
(defcustom chronometrist-report-weekday-number-alist
'(("Sunday" . 0)
("Monday" . 1)
("Tuesday" . 2)
("Wednesday" . 3)
("Thursday" . 4)
("Friday" . 5)
("Saturday" . 6))
"Alist in the form (\"NAME\" . NUMBER), where \"NAME\" is the name of a weekday and NUMBER its associated number."
:type 'alist)
(provide 'chronometrist-report-custom)
;;; chronometrist-report-custom.el ends here

View File

@ -1,271 +0,0 @@
;;; chronometrist-report.el --- Report view for Chronometrist -*- 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:
;;
(require 'filenotify)
(require 'subr-x)
(require 'chronometrist-common)
(require 'chronometrist-queries)
(require 'chronometrist-timer)
(require 'chronometrist-report-custom)
(require 'chronometrist-migrate)
(declare-function chronometrist-refresh-file "chronometrist.el")
;; TODO - improve first-run (no file, or no data in file) behaviour
;; TODO - add support for custom week start day to
;; tabulated-list-format. Have it use chronometrist-report-weekday-number-alist for day
;; names to aid i10n
;; TODO - use variables instead of hardcoded numbers to determine spacing
;; ## VARIABLES ##
;;; Code:
(defvar chronometrist-report--ui-date nil
"The first date of the week displayed by `chronometrist-report'.
A value of nil means the current week. Otherwise, it must be a
date in the form \"YYYY-MM-DD\".")
(defvar chronometrist-report--ui-week-dates nil
"List of dates currently displayed by `chronometrist-report'.
Each date is a list containing calendrical information (see (info \"(elisp)Time Conversion\"))")
(defvar chronometrist-report--point nil)
;; ## FUNCTIONS ##
(defun chronometrist-report-date ()
"Return the date specified by `chronometrist-report--ui-date'.
If it is nil, return the current date as calendrical
information (see (info \"(elisp)Time Conversion\"))."
(if chronometrist-report--ui-date chronometrist-report--ui-date (chronometrist-date)))
(defun chronometrist-report-date->dates-in-week (first-date-in-week)
"Return a list of dates in a week, starting from FIRST-DATE-IN-WEEK.
Each date is a ts struct (see `ts.el').
FIRST-DATE-IN-WEEK must be a ts struct representing the first date."
(cl-loop for i from 0 to 6 collect
(ts-adjust 'day i first-date-in-week)))
(defun chronometrist-report-date->week-dates ()
"Return dates in week as a list.
Each element is a ts struct (see `ts.el').
The first date is the first occurrence of
`chronometrist-report-week-start-day' before the date specified in
`chronometrist-report--ui-date' (if non-nil) or the current date."
(->> (or chronometrist-report--ui-date (chronometrist-date))
(chronometrist-previous-week-start)
(chronometrist-report-date->dates-in-week)))
(defun chronometrist-report-entries ()
"Create entries to be displayed in the `chronometrist-report' buffer."
(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))))))
(defun chronometrist-report-print-keybind (command &optional description firstonly)
"Insert one or more keybindings for COMMAND into the current buffer.
DESCRIPTION is a description of the command.
If FIRSTONLY is non-nil, insert only the first keybinding found."
(insert "\n "
(chronometrist-format-keybinds command firstonly)
" - "
(if description description "")))
;; TODO - preserve point when clicking buttons
(defun chronometrist-report-print-non-tabular ()
"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))))
(goto-char (point-min))
(insert " ")
(insert (mapconcat (lambda (ts)
(ts-format "%F" ts))
(chronometrist-report-date->week-dates)
" "))
(insert "\n")
(goto-char (point-max))
(insert w (format "%- 21s" "Total"))
(->> total-time-daily
(mapcar #'chronometrist-format-time)
(--map (format "% 9s " it))
(apply #'insert))
(->> total-time-daily
(-reduce #'+)
(chronometrist-format-time)
(format "% 13s")
(insert))
(insert "\n" w)
(insert-text-button "<<" 'action #'chronometrist-report-previous-week 'follow-link t)
(insert (format "% 4s" " "))
(insert-text-button ">>" 'action #'chronometrist-report-next-week 'follow-link t)
(insert "\n")
(chronometrist-report-print-keybind 'chronometrist-report-previous-week)
(insert-text-button "previous week" 'action #'chronometrist-report-previous-week 'follow-link t)
(chronometrist-report-print-keybind 'chronometrist-report-next-week)
(insert-text-button "next week" 'action #'chronometrist-report-next-week 'follow-link t)
(chronometrist-report-print-keybind 'chronometrist-open-log)
(insert-text-button "open log file" 'action #'chronometrist-open-log 'follow-link t)))
(defun chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
"Refresh the `chronometrist-report' buffer, without re-reading `chronometrist-file'."
(let* ((w (get-buffer-window chronometrist-report-buffer-name t))
(p (point)))
(with-current-buffer chronometrist-report-buffer-name
(tabulated-list-print t nil)
(chronometrist-report-print-non-tabular)
(chronometrist-maybe-start-timer)
(set-window-point w p))))
;; REVIEW - merge this into `chronometrist-refresh-file', while moving the -refresh call to the call site?
(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-events-clean)
(chronometrist-report-refresh))
;; ## MAJOR MODE ##
(defvar chronometrist-report-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "l") #'chronometrist-open-log)
(define-key map (kbd "b") #'chronometrist-report-previous-week)
(define-key map (kbd "f") #'chronometrist-report-next-week)
;; Works when number of tasks < screen length; after that, you
;; probably expect mousewheel to scroll up/down, and
;; alt-mousewheel or something for next/previous week. For now,
;; I'm assuming most people won't have all that many tasks - I've
;; been using it for ~2 months and have 18 tasks, which are
;; still just half the screen on my 15" laptop. Let's see what
;; people say.
(define-key map [mouse-4] #'chronometrist-report-next-week)
(define-key map [mouse-5] #'chronometrist-report-previous-week)
map)
"Keymap used by `chronometrist-report-mode'.")
(define-derived-mode chronometrist-report-mode tabulated-list-mode "Chronometrist-Report"
"Major mode for `chronometrist-report'."
(make-local-variable 'tabulated-list-format)
(setq tabulated-list-format [("Task" 25 t)
("Sunday" 10 t)
("Monday" 10 t)
("Tuesday" 10 t)
("Wednesday" 10 t)
("Thursday" 10 t)
("Friday" 10 t)
("Saturday" 10 t :pad-right 5)
("Total" 12 t)])
(make-local-variable 'tabulated-list-entries)
(setq tabulated-list-entries 'chronometrist-report-entries)
(make-local-variable 'tabulated-list-sort-key)
(setq tabulated-list-sort-key '("Task" . nil))
(tabulated-list-init-header)
(chronometrist-maybe-start-timer)
(setq revert-buffer-function #'chronometrist-report-refresh)
(unless chronometrist--fs-watch
(setq chronometrist--fs-watch
(file-notify-add-watch chronometrist-file
'(change)
#'chronometrist-refresh-file))))
;; ## COMMANDS ##
;;;###autoload
(defun chronometrist-report (&optional keep-date)
"Display a weekly report of the data in `chronometrist-file'.
This is the 'listing command' for chronometrist-report-mode.
If a buffer called `chronometrist-report-buffer-name' already
exists and is visible, kill the buffer.
If KEEP-DATE is nil (the default when not supplied), set
`chronometrist-report--ui-date' to nil and display data from the
current week. Otherwise, display data from the week specified by
`chronometrist-report--ui-date'."
(interactive)
(chronometrist-migrate-check)
(let ((buffer (get-buffer-create chronometrist-report-buffer-name)))
(with-current-buffer buffer
(cond ((and (get-buffer-window chronometrist-report-buffer-name)
(not keep-date))
(setq chronometrist-report--point (point))
(kill-buffer buffer))
(t (delete-other-windows)
(unless keep-date
(setq chronometrist-report--ui-date nil))
(chronometrist-common-create-file)
(chronometrist-report-mode)
(switch-to-buffer buffer)
(chronometrist-report-refresh-file nil)
(goto-char (or chronometrist-report--point 1)))))))
(defun chronometrist-report-previous-week (arg)
"View the previous week's report.
With prefix argument ARG, move back ARG weeks."
(interactive "P")
(let ((arg (if (and arg (numberp arg))
(abs arg)
1)))
(setq chronometrist-report--ui-date
(ts-adjust 'day (- (* arg 7))
(if chronometrist-report--ui-date
chronometrist-report--ui-date
(ts-now)))))
(setq chronometrist-report--point (point))
(kill-buffer)
(chronometrist-report t))
(defun chronometrist-report-next-week (arg)
"View the next week's report.
With prefix argument ARG, move forward ARG weeks."
(interactive "P")
(let ((arg (if (and arg (numberp arg))
(abs arg)
1)))
(setq chronometrist-report--ui-date
(ts-adjust 'day (* arg 7)
(if chronometrist-report--ui-date
chronometrist-report--ui-date
(ts-now))))
(setq chronometrist-report--point (point))
(kill-buffer)
(chronometrist-report t)))
(provide 'chronometrist-report)
;;; chronometrist-report.el ends here

View File

@ -1,144 +0,0 @@
;;; chronometrist-sexp.el --- s-expression backend for Chronometrist -*- lexical-binding: t; -*-
;;; Commentary:
;;
(require 'chronometrist-custom)
(require 'chronometrist-plist-pp)
;;; Code:
;; chronometrist-file (-custom)
;; chronometrist-events, chronometrist-events-maybe-split (-events)
;; chronometrist-plist-pp (-plist-pp)
(defmacro chronometrist-sexp-in-file (file &rest body)
"Run BODY in a buffer visiting FILE, restoring point afterwards."
(declare (indent defun) (debug t))
`(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
(goto-char (point-min))
(let ((index 0) expr pending-expr)
(while (or pending-expr
(setq expr (ignore-errors (read (current-buffer)))))
;; find and split midnight-spanning events during deserialization itself
(let* ((split-expr (chronometrist-events-maybe-split expr))
(new-value (cond (pending-expr
(prog1 pending-expr
(setq pending-expr nil)))
(split-expr
(setq pending-expr (cl-second split-expr))
(cl-first split-expr))
(t expr)))
(new-value-date (->> (plist-get new-value :start)
(s-left 10)))
(existing-value (gethash new-value-date chronometrist-events)))
(unless pending-expr (cl-incf index))
(puthash new-value-date
(if existing-value
(append existing-value
(list new-value))
(list new-value))
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-defun chronometrist-sexp-new (plist)
"Add new PLIST at the end of `chronometrist-file'."
(chronometrist-sexp-in-file chronometrist-file
(goto-char (point-max))
;; If we're adding the first s-exp in the file, don't add a
;; newline before it
(unless (bobp) (insert "\n"))
(unless (bolp) (insert "\n"))
(chronometrist-plist-pp plist (current-buffer))
;; Update in-memory (`chronometrist-events', `chronometrist-task-list') too...
(chronometrist-events-add plist)
(chronometrist-task-list-add (plist-get plist :name))
(chronometrist-tags-history-add plist)
;; ...so we can skip some expensive operations.
(setq chronometrist--inhibit-read-p t)
(save-buffer)))
(defun chronometrist-sexp-delete-list (&optional arg)
"Delete ARG lists after point."
(let ((point-1 (point)))
(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
(goto-char (point-max))
(unless (and (bobp) (bolp))
(insert "\n"))
(backward-list 1)
(chronometrist-sexp-delete-list)
(chronometrist-plist-pp plist (current-buffer))
(chronometrist-events-replace-last plist)
;; We assume here that this function will always be used to
;; replace something with the same :name. At the time of writing,
;; this is indeed the case. The reason for this is that if the
;; replaced plist is the only one in `chronometrist-file' with that :name, the
;; :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')
(chronometrist-tags-history-replace-last plist)
(setq chronometrist--inhibit-read-p t)
(save-buffer)))
(defun chronometrist-sexp-reindent-buffer ()
"Reindent the current buffer.
This is meant to be run in `chronometrist-file' when using the s-expression backend."
(interactive)
(let (expr)
(goto-char (point-min))
(while (setq expr (ignore-errors (read (current-buffer))))
(backward-list)
(chronometrist-sexp-delete-list)
(when (looking-at "\n*")
(delete-region (match-beginning 0)
(match-end 0)))
(chronometrist-plist-pp expr (current-buffer))
(insert "\n")
(unless (eobp)
(insert "\n")))))
(provide 'chronometrist-sexp)
;;; chronometrist-sexp.el ends here

View File

@ -0,0 +1,104 @@
;;; chronometrist-spark.el --- Show sparklines in Chronometrist buffers -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "25.1") (chronometrist "0.7.0") (spark "0.1"))
;; Version: 0.1.0
;; 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:
;;
;; This package adds a column to Chronometrist displaying sparklines for each task.
;;; Code:
;; This file was automatically generated from chronometrist-spark.org.
(require 'chronometrist)
(require 'spark)
(defgroup chronometrist-spark nil
"Show sparklines in `chronometrist'."
:group 'applications)
(defcustom chronometrist-spark-length 7
"Length of each sparkline in number of days."
:type 'integer)
(defcustom chronometrist-spark-show-range t
"If non-nil, display range of each sparkline."
:type 'boolean)
(defun chronometrist-spark-range (durations)
"Return range for DURATIONS as a string.
DURATIONS must be a list of integer seconds."
(let* ((duration-minutes (--map (/ it 60) durations))
(durations-nonzero (seq-remove #'zerop duration-minutes))
(length (length durations-nonzero)))
(cond ((not durations-nonzero) "")
((> length 1)
(format "(%sm~%sm)" (apply #'min durations-nonzero)
(apply #'max duration-minutes)))
((= 1 length)
;; This task only had activity on one day in the given
;; range of days - these durations, then, cannot really
;; have a minimum and maximum range.
(format "(%sm)" (apply #'max duration-minutes))))))
(defun chronometrist-spark-durations (task length stop-ts)
"Return a list of durations for time tracked for TASK in the last LENGTH days before STOP-TS."
(cl-loop for day from (- (- length 1)) to 0
collect
(chronometrist-task-time-one-day task (ts-adjust 'day day stop-ts))))
(defun chronometrist-spark-row-transformer (row)
"Add a sparkline cell to ROW.
Used to add a sparkline column to `chronometrist-rows'.
ROW must be a valid element of the list specified by
`tabulated-list-entries'."
(-let* (((task vector) row)
(durations (chronometrist-spark-durations task chronometrist-spark-length (ts-now)))
(sparkline (if (and (not (seq-every-p #'zerop durations))
chronometrist-spark-show-range)
(format "%s %s" (spark durations) (chronometrist-spark-range durations))
(format "%s" (spark durations)))))
(list task (vconcat vector `[,sparkline]))))
(defun chronometrist-spark-schema-transformer (schema)
"Add a sparkline column to SCHEMA.
Used to add a sparkline column to `chronometrist-schema-transformers'.
SCHEMA should be a vector as specified by `tabulated-list-format'."
(vconcat schema `[("Graph"
,(if chronometrist-spark-show-range
(+ chronometrist-spark-length 12)
chronometrist-spark-length)
t)]))
(defun chronometrist-spark-setup ()
"Add `chronometrist-sparkline' functions to `chronometrist' hooks."
(add-to-list 'chronometrist-row-transformers #'chronometrist-spark-row-transformer)
(add-to-list 'chronometrist-schema-transformers #'chronometrist-spark-schema-transformer))
(defun chronometrist-spark-teardown ()
"Remove `chronometrist-sparkline' functions from `chronometrist' hooks."
(setq chronometrist-row-transformers
(remove #'chronometrist-spark-row-transformer chronometrist-row-transformers)
chronometrist-schema-transformers
(remove #'chronometrist-spark-schema-transformer chronometrist-schema-transformers)))
(define-minor-mode chronometrist-spark-minor-mode
nil nil nil nil
;; when being enabled/disabled, `chronometrist-spark-minor-mode' will already be t/nil here
(if chronometrist-spark-minor-mode (chronometrist-spark-setup) (chronometrist-spark-teardown)))
(provide 'chronometrist-spark)
;;; chronometrist-spark.el ends here

View File

@ -0,0 +1,177 @@
#+TITLE: chronometrist-spark
#+AUTHOR: contrapunctus
#+SUBTITLE: Show sparklines in Chronometrist
#+PROPERTY: header-args :tangle yes :load yes
* Library headers and commentary
#+BEGIN_SRC emacs-lisp
;;; chronometrist-spark.el --- Show sparklines in Chronometrist buffers -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "25.1") (chronometrist "0.7.0") (spark "0.1"))
;; Version: 0.1.0
;; 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>
#+END_SRC
"Commentary" is displayed when the user clicks on the package's entry in =M-x list-packages=.
#+BEGIN_SRC emacs-lisp
;;; Commentary:
;;
;; This package adds a column to Chronometrist displaying sparklines for each task.
#+END_SRC
* Dependencies
#+BEGIN_SRC emacs-lisp
;;; Code:
;; This file was automatically generated from chronometrist-spark.org.
(require 'chronometrist)
(require 'spark)
#+END_SRC
* Code
** custom group :custom:group:
#+BEGIN_SRC emacs-lisp
(defgroup chronometrist-spark nil
"Show sparklines in `chronometrist'."
:group 'applications)
#+END_SRC
** length :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-spark-length 7
"Length of each sparkline in number of days."
:type 'integer)
#+END_SRC
** show-range :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-spark-show-range t
"If non-nil, display range of each sparkline."
:type 'boolean)
#+END_SRC
** range :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-spark-range (durations)
"Return range for DURATIONS as a string.
DURATIONS must be a list of integer seconds."
(let* ((duration-minutes (--map (/ it 60) durations))
(durations-nonzero (seq-remove #'zerop duration-minutes))
(length (length durations-nonzero)))
(cond ((not durations-nonzero) "")
((> length 1)
(format "(%sm~%sm)" (apply #'min durations-nonzero)
(apply #'max duration-minutes)))
((= 1 length)
;; This task only had activity on one day in the given
;; range of days - these durations, then, cannot really
;; have a minimum and maximum range.
(format "(%sm)" (apply #'max duration-minutes))))))
#+END_SRC
*** tests
#+BEGIN_SRC emacs-lisp :tangle ../tests/chronometrist-spark-tests :load test
(ert-deftest chronometrist-spark-range ()
(should (equal (chronometrist-spark-range '(0 0 0)) ""))
(should (equal (chronometrist-spark-range '(0 1 2)) ""))
(should (equal (chronometrist-spark-range '(60 0 0)) "(1m)"))
(should (equal (chronometrist-spark-range '(60 0 120)) "(1m~2m)")))
#+END_SRC
** durations :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-spark-durations (task length stop-ts)
"Return a list of durations for time tracked for TASK in the last LENGTH days before STOP-TS."
(cl-loop for day from (- (- length 1)) to 0
collect
(chronometrist-task-time-one-day task (ts-adjust 'day day stop-ts))))
#+END_SRC
** TODO row-transformer :function:
if larger than 7
add space after (% length 7)th element
then add space after every 7 elements
+ if task has no time tracked for it - ""
+ don't display 0 as the minimum time tracked
+ [ ] if task has only one day of time tracked - "(40m)"
#+BEGIN_SRC emacs-lisp
(defun chronometrist-spark-row-transformer (row)
"Add a sparkline cell to ROW.
Used to add a sparkline column to `chronometrist-rows'.
ROW must be a valid element of the list specified by
`tabulated-list-entries'."
(-let* (((task vector) row)
(durations (chronometrist-spark-durations task chronometrist-spark-length (ts-now)))
(sparkline (if (and (not (seq-every-p #'zerop durations))
chronometrist-spark-show-range)
(format "%s %s" (spark durations) (chronometrist-spark-range durations))
(format "%s" (spark durations)))))
(list task (vconcat vector `[,sparkline]))))
#+END_SRC
** TODO schema-transformer :function:
calculate length while accounting for space
#+BEGIN_SRC emacs-lisp
(defun chronometrist-spark-schema-transformer (schema)
"Add a sparkline column to SCHEMA.
Used to add a sparkline column to `chronometrist-schema-transformers'.
SCHEMA should be a vector as specified by `tabulated-list-format'."
(vconcat schema `[("Graph"
,(if chronometrist-spark-show-range
(+ chronometrist-spark-length 12)
chronometrist-spark-length)
t)]))
#+END_SRC
** setup :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-spark-setup ()
"Add `chronometrist-sparkline' functions to `chronometrist' hooks."
(add-to-list 'chronometrist-row-transformers #'chronometrist-spark-row-transformer)
(add-to-list 'chronometrist-schema-transformers #'chronometrist-spark-schema-transformer))
#+END_SRC
** teardown :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-spark-teardown ()
"Remove `chronometrist-sparkline' functions from `chronometrist' hooks."
(setq chronometrist-row-transformers
(remove #'chronometrist-spark-row-transformer chronometrist-row-transformers)
chronometrist-schema-transformers
(remove #'chronometrist-spark-schema-transformer chronometrist-schema-transformers)))
#+END_SRC
** minor-mode :minor:mode:
#+BEGIN_SRC emacs-lisp
(define-minor-mode chronometrist-spark-minor-mode
nil nil nil nil
;; when being enabled/disabled, `chronometrist-spark-minor-mode' will already be t/nil here
(if chronometrist-spark-minor-mode (chronometrist-spark-setup) (chronometrist-spark-teardown)))
#+END_SRC
* Provide
#+BEGIN_SRC emacs-lisp
(provide 'chronometrist-spark)
;;; chronometrist-spark.el ends here
#+END_SRC
* Local variables :noexport:
# Local Variables:
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End:

View File

@ -0,0 +1,210 @@
;;; chronometrist-sqlite.el --- SQLite backend for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "24.3") (chronometrist "0.9.0") (emacsql-sqlite "1.0.0"))
;; Version: 0.1.0
;; 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:
;;
;; This package provides an SQLite 3 backend for Chronometrist.
;;; Code:
(require 'chronometrist)
(require 'emacsql-sqlite)
(defclass chronometrist-sqlite-backend (chronometrist-backend chronometrist-file-backend-mixin)
((extension :initform "sqlite"
:accessor chronometrist-backend-ext
:custom 'string)
(connection :initform nil
:initarg :connection
:accessor chronometrist-backend-connection)))
(chronometrist-register-backend
:sqlite "Store records in SQLite database."
(make-instance 'chronometrist-sqlite-backend :path chronometrist-file))
(cl-defmethod initialize-instance :after ((backend chronometrist-sqlite-backend)
&rest _initargs)
"Initialize connection for BACKEND based on its file."
(with-slots (file connection) backend
(when (and file (not connection))
(setf connection (emacsql-sqlite file)))))
(cl-defmethod chronometrist-create-file ((backend chronometrist-sqlite-backend) &optional file)
"Create file for BACKEND if it does not already exist.
Return the connection object from `emacsql-sqlite'."
(let* ((file (or file (chronometrist-backend-file backend)))
(db (or (chronometrist-backend-connection backend)
(setf (chronometrist-backend-connection backend)
(emacsql-sqlite file)))))
(cl-loop
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db)))
(defun chronometrist-iso-to-unix (timestamp)
(truncate (float-time (parse-iso8601-time-string timestamp))))
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(with-slots (connection) backend
(delete-file file)
(when connection (emacsql-close connection))
(setf connection nil)
(chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(emacsql connection [:insert-or-ignore-into dates [date] :values [$s1]]
(chronometrist-iso-to-unix date))
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)))))
(defun chronometrist-sqlite-insert-properties (backend plist)
"Insert properties from PLIST to (SQLite) BACKEND.
Properties are key-values excluding :name, :start, and :stop.
Insert nothing if the properties already exist. Return the
prop-id of the inserted or existing property."
(with-slots (connection) backend
(let* ((plist (chronometrist-plist-key-values plist))
(props (if (functionp chronometrist-sqlite-properties-function)
(funcall chronometrist-sqlite-properties-function plist)
plist)))
(emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]]
props)
(caar (emacsql connection [:select [prop-id]
:from properties
:where (= properties $s1)]
props)))))
(defun chronometrist-sqlite-properties-to-json (plist)
"Return PLIST as a JSON string."
(json-encode
;; `json-encode' throws an error when it thinks
;; it sees "alists" which have numbers as
;; "keys", so we convert any cons cells and any
;; lists starting with a number to vectors
(-tree-map (lambda (elt)
(cond ((chronometrist-pp-pair-p elt)
(vector (car elt) (cdr elt)))
((consp elt)
(vconcat elt))
(t elt)))
plist)))
(defcustom chronometrist-sqlite-properties-function nil
"Function used to control the encoding of user key-values.
The function must accept a single argument, the plist of key-values.
Any non-function value results in key-values being inserted as
s-expressions in a text column."
:type '(choice function (sexp :tag "Insert as s-expressions")))
(cl-defmethod chronometrist-insert ((backend chronometrist-sqlite-backend) plist)
(-let (((plist-1 plist-2) (chronometrist-split-plist plist))
(db (chronometrist-backend-connection backend)))
(cl-loop for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
do
(-let* (((&plist :name name :start start :stop stop) plist)
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))
(start-unix (chronometrist-iso-to-unix start))
(stop-unix (and stop (chronometrist-iso-to-unix stop)))
name-id interval-id prop-id)
;; insert name if it does not exist
(emacsql db [:insert-or-ignore-into interval-names [name]
:values [$s1]]
name)
;; insert interval properties if they do not exist
(setq prop-id (chronometrist-sqlite-insert-properties backend plist))
;; insert interval and associate it with the date
(setq name-id
(caar (emacsql db [:select [name-id]
:from interval-names
:where (= name $s1)]
name)))
(emacsql db [:insert-or-ignore-into intervals
[name-id start-time stop-time prop-id]
:values [$s1 $s2 $s3 $s4]]
name-id start-unix stop-unix prop-id)
(emacsql db [:insert-or-ignore-into dates [date]
:values [$s1]] date-unix)
(setq date-id
(caar (emacsql db [:select [date-id] :from dates
:where (= date $s1)]
date-unix))
interval-id
(caar (emacsql db [:select (funcall max interval-id) :from intervals])))
(emacsql db [:insert-into date-intervals [date-id interval-id]
:values [$s1 $s2]]
date-id interval-id)))))
(cl-defmethod chronometrist-edit-backend ((backend chronometrist-sqlite-backend))
(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-latest-record ((backend chronometrist-sqlite-backend) db)
(emacsql db [:select * :from events :order-by rowid :desc :limit 1]))
(cl-defmethod chronometrist-task-records-for-date ((backend chronometrist-sqlite-backend) task date-ts))
(cl-defmethod chronometrist-active-days ((backend chronometrist-sqlite-backend) task))
(cl-defmethod chronometrist-replace-last ((backend chronometrist-sqlite-backend) plist)
(emacsql db [:delete-from events :where ]))
(provide 'chronometrist-sqlite)
;;; chronometrist-sqlite.el ends here

View File

@ -0,0 +1,277 @@
#+TITLE: chronometrist-sqlite
#+AUTHOR: contrapunctus
#+SUBTITLE: SQLite backend for Chronometrist
#+PROPERTY: header-args :tangle yes :load yes
* Library headers and commentary
#+BEGIN_SRC emacs-lisp
;;; chronometrist-sqlite.el --- SQLite backend for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "24.3") (chronometrist "0.9.0") (emacsql-sqlite "1.0.0"))
;; Version: 0.1.0
;; 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>
#+END_SRC
"Commentary" is displayed when the user clicks on the package's entry in =M-x list-packages=.
#+BEGIN_SRC emacs-lisp
;;; Commentary:
;;
;; This package provides an SQLite 3 backend for Chronometrist.
#+END_SRC
* Dependencies
#+BEGIN_SRC emacs-lisp
;;; Code:
(require 'chronometrist)
(require 'emacsql-sqlite)
#+END_SRC
* Code
** class
#+BEGIN_SRC emacs-lisp
(defclass chronometrist-sqlite-backend (chronometrist-backend chronometrist-file-backend-mixin)
((extension :initform "sqlite"
:accessor chronometrist-backend-ext
:custom 'string)
(connection :initform nil
:initarg :connection
:accessor chronometrist-backend-connection)))
(chronometrist-register-backend
:sqlite "Store records in SQLite database."
(make-instance 'chronometrist-sqlite-backend :path chronometrist-file))
#+END_SRC
** initialize-instance :method:
#+BEGIN_SRC emacs-lisp
(cl-defmethod initialize-instance :after ((backend chronometrist-sqlite-backend)
&rest _initargs)
"Initialize connection for BACKEND based on its file."
(with-slots (file connection) backend
(when (and file (not connection))
(setf connection (emacsql-sqlite file)))))
#+END_SRC
** create-file
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-create-file ((backend chronometrist-sqlite-backend) &optional file)
"Create file for BACKEND if it does not already exist.
Return the connection object from `emacsql-sqlite'."
(let* ((file (or file (chronometrist-backend-file backend)))
(db (or (chronometrist-backend-connection backend)
(setf (chronometrist-backend-connection backend)
(emacsql-sqlite file)))))
(cl-loop
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db)))
#+END_SRC
** iso-to-unix :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-iso-to-unix (timestamp)
(truncate (float-time (parse-iso8601-time-string timestamp))))
#+END_SRC
** to-file :method:
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(with-slots (connection) backend
(delete-file file)
(when connection (emacsql-close connection))
(setf connection nil)
(chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(emacsql connection [:insert-or-ignore-into dates [date] :values [$s1]]
(chronometrist-iso-to-unix date))
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)))))
#+END_SRC
** insert-properties :writer:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-sqlite-insert-properties (backend plist)
"Insert properties from PLIST to (SQLite) BACKEND.
Properties are key-values excluding :name, :start, and :stop.
Insert nothing if the properties already exist. Return the
prop-id of the inserted or existing property."
(with-slots (connection) backend
(let* ((plist (chronometrist-plist-key-values plist))
(props (if (functionp chronometrist-sqlite-properties-function)
(funcall chronometrist-sqlite-properties-function plist)
plist)))
(emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]]
props)
(caar (emacsql connection [:select [prop-id]
:from properties
:where (= properties $s1)]
props)))))
#+END_SRC
*** properties-to-json :function:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-sqlite-properties-to-json (plist)
"Return PLIST as a JSON string."
(json-encode
;; `json-encode' throws an error when it thinks
;; it sees "alists" which have numbers as
;; "keys", so we convert any cons cells and any
;; lists starting with a number to vectors
(-tree-map (lambda (elt)
(cond ((chronometrist-pp-pair-p elt)
(vector (car elt) (cdr elt)))
((consp elt)
(vconcat elt))
(t elt)))
plist)))
#+END_SRC
*** properties-function :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-sqlite-properties-function nil
"Function used to control the encoding of user key-values.
The function must accept a single argument, the plist of key-values.
Any non-function value results in key-values being inserted as
s-expressions in a text column."
:type '(choice function (sexp :tag "Insert as s-expressions")))
#+END_SRC
** insert
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-insert ((backend chronometrist-sqlite-backend) plist)
(-let (((plist-1 plist-2) (chronometrist-split-plist plist))
(db (chronometrist-backend-connection backend)))
(cl-loop for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
do
(-let* (((&plist :name name :start start :stop stop) plist)
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))
(start-unix (chronometrist-iso-to-unix start))
(stop-unix (and stop (chronometrist-iso-to-unix stop)))
name-id interval-id prop-id)
;; insert name if it does not exist
(emacsql db [:insert-or-ignore-into interval-names [name]
:values [$s1]]
name)
;; insert interval properties if they do not exist
(setq prop-id (chronometrist-sqlite-insert-properties backend plist))
;; insert interval and associate it with the date
(setq name-id
(caar (emacsql db [:select [name-id]
:from interval-names
:where (= name $s1)]
name)))
(emacsql db [:insert-or-ignore-into intervals
[name-id start-time stop-time prop-id]
:values [$s1 $s2 $s3 $s4]]
name-id start-unix stop-unix prop-id)
(emacsql db [:insert-or-ignore-into dates [date]
:values [$s1]] date-unix)
(setq date-id
(caar (emacsql db [:select [date-id] :from dates
:where (= date $s1)]
date-unix))
interval-id
(caar (emacsql db [:select (funcall max interval-id) :from intervals])))
(emacsql db [:insert-into date-intervals [date-id interval-id]
:values [$s1 $s2]]
date-id interval-id)))))
#+END_SRC
** open-file
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-edit-backend ((backend chronometrist-sqlite-backend))
(require 'sql)
(switch-to-buffer
(sql-comint-sqlite 'sqlite (list file))))
#+END_SRC
** latest-record
#+BEGIN_SRC emacs-lisp
;; SELECT * FROM TABLE WHERE ID = (SELECT MAX(ID) FROM TABLE);
;; SELECT * FROM tablename ORDER BY column DESC LIMIT 1;
(cl-defmethod chronometrist-latest-record ((backend chronometrist-sqlite-backend) db)
(emacsql db [:select * :from events :order-by rowid :desc :limit 1]))
#+END_SRC
** task-records-for-date
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-task-records-for-date ((backend chronometrist-sqlite-backend) task date-ts))
#+END_SRC
** active-days
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-active-days ((backend chronometrist-sqlite-backend) task))
#+END_SRC
** replace-last
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-replace-last ((backend chronometrist-sqlite-backend) plist)
(emacsql db [:delete-from events :where ]))
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp
(provide 'chronometrist-sqlite)
;;; chronometrist-sqlite.el ends here
#+END_SRC
* Local variables :noexport:
# Local Variables:
# eval: (when (or (package-installed-p 'emacsql) (featurep 'emacsql)) (require 'emacsql) (emacsql-fix-vector-indentation))
# eval: (when (or (package-installed-p 'literate-elisp) (featurep 'literate-elisp)) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End:

View File

@ -1,29 +0,0 @@
;;; chronometrist-statistics-custom.el --- Custom definitions for chronometrist-statistics -*- 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:
(defgroup chronometrist-statistics nil
"Statistics buffer for the `chronometrist' time tracker."
:group 'chronometrist)
(defcustom chronometrist-statistics-buffer-name "*Chronometrist-Statistics*"
"The name of the buffer created by `chronometrist-statistics'."
:type 'string)
(provide 'chronometrist-statistics-custom)
;;; chronometrist-statistics-custom.el ends here

View File

@ -1,308 +0,0 @@
;;; chronometrist-statistics.el --- View statistics for 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:
;;
(require 'parse-time)
(require 'cl-lib)
(require 'filenotify)
(require 'chronometrist-common)
(require 'chronometrist-time)
(require 'chronometrist-timer)
(require 'chronometrist-events)
(require 'chronometrist-statistics-custom)
(require 'chronometrist-migrate)
(require 'chronometrist-queries)
(declare-function chronometrist-refresh-file "chronometrist.el")
;; details!
;; for each activity, spent most time on doing X (where X is a
;; comment, assuming you use comments to detail what you did)
;; Really might need emacs-async for this...buttloads of big
;; calculations which will only get bigger as the timelog file grows,
;; and the more the activities, the more the calculations! I'm
;; visualizing the table loading gradually, field by field, like an
;; image in a browser.
;; TODO -
;; 1. [x] show dash instead of zero
;; 2. [x] percent for active days
;; 3. buttons
;; 4. [x] display date ranges in a nicer way
;; 5. month and year ranges
;; 6. totals for each column
;; 7. (maybe) jump between chronometrist-report and chronometrist-statistics for viewing the same week's data
;; - in chronometrist-statistics, this only makes sense in week mode
;; 8. a 'counter' - if I have ten weeks of data and I'm on the latest,
;; show 10/10; update this as we scroll
;; - don't scroll past the end, as currently happens
;; - also applicable to chronometrist-report
;; TODO - convert all functions which take dates as arguments to use
;; the (YEAR MONTH DAY) format
;;; Code:
;; ## VARIABLES ##
(defvar chronometrist-statistics--ui-state nil
"Stores the display state for `chronometrist-statistics'.
This must be a plist in the form (:MODE :START :END).
:MODE is either 'week, 'month, 'year, 'full, or 'custom.
'week, 'month, and 'year mean display statistics
weekly/monthly/yearly respectively.
'full means display statistics from the beginning to the end of
the `chronometrist-file'.
'custom means display statistics from an arbitrary date range.
:START and :END are the start and end of the date range to be
displayed. They must be ts structs (see `ts.el').")
(defvar chronometrist-statistics--point nil)
(defvar chronometrist-statistics-mode-map)
;; ## FUNCTIONS ##
(cl-defun chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
"Return the average time the user has spent on TASK from TABLE.
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))))
;; (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))))
(when events-in-day
(setq days (1+ days))
(->> (chronometrist-events->ts-pairs events-in-day)
(chronometrist-ts-pairs->durations)
(-reduce #'+)
(list)
(append per-day-time-list)
(setq per-day-time-list)))))
table)
(if per-day-time-list
(--> (-reduce #'+ per-day-time-list)
(/ it days))
0)))
(defun chronometrist-statistics-entries-internal (table)
"Helper function for `chronometrist-statistics-entries'.
It simply operates on the entire hash table TABLE (see
`chronometrist-events' for table format), so ensure that TABLE is
reduced to the desired range using
`chronometrist-events-subset'."
(mapcar (lambda (task)
(let* ((active-days (chronometrist-statistics-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)
(format " % 6s" "-")
(format " %05.2f%%" active-percent)))
(active-days (format "% 5s"
(if (zerop active-days)
"-"
active-days)))
(average-time (->> (chronometrist-statistics-count-average-time-spent task table)
(chronometrist-format-time)
(format "% 5s")))
(content (vector task
active-days
active-percent
average-time)))
(list task content)))
chronometrist-task-list))
(defun chronometrist-statistics-entries ()
"Create entries to be displayed in the buffer created by `chronometrist-statistics'."
;; We assume that all fields in `chronometrist-statistics--ui-state' are set, so they must
;; be changed by the view-changing functions.
(cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
(end (plist-get chronometrist-statistics--ui-state :end))
(ht (chronometrist-events-subset start end)))
(chronometrist-statistics-entries-internal ht)))
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
(let* ((start (chronometrist-previous-week-start (chronometrist-date)))
(end (ts-adjust 'day 7 start))
(ht (chronometrist-events-subset start end)))
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
(chronometrist-statistics-entries-internal ht)))))
(defun chronometrist-statistics-print-keybind (command &optional description firstonly)
"Insert the keybindings for COMMAND.
If DESCRIPTION is non-nil, insert that too.
If FIRSTONLY is non-nil, return only the first keybinding found."
(insert "\n "
(chronometrist-format-keybinds command
chronometrist-statistics-mode-map
firstonly)
" - "
(if description description "")))
(defun chronometrist-statistics-print-non-tabular ()
"Print the non-tabular part of the buffer in `chronometrist-statistics'."
(let ((w "\n ")
(inhibit-read-only t))
(goto-char (point-max))
(insert w)
(insert-text-button (cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week "Weekly view"))
;; 'action #'chronometrist-report-previous-week ;; TODO - make interactive function to accept new mode from user
'follow-link t)
(insert ", from")
(insert
(format " %s to %s\n"
(ts-format "%F" (plist-get chronometrist-statistics--ui-state :start))
(ts-format "%F" (plist-get chronometrist-statistics--ui-state :end))))))
(defun chronometrist-statistics-refresh (&optional _ignore-auto _noconfirm)
"Refresh the `chronometrist-statistics' buffer.
This does not re-read `chronometrist-file'.
The optional arguments _IGNORE-AUTO and _NOCONFIRM are ignored,
and are present solely for the sake of using this function as a
value of `revert-buffer-function'."
(let* ((w (get-buffer-window chronometrist-statistics-buffer-name t))
(p (point)))
(with-current-buffer chronometrist-statistics-buffer-name
(tabulated-list-print t nil)
(chronometrist-statistics-print-non-tabular)
(chronometrist-maybe-start-timer)
(set-window-point w p))))
;; ## MAJOR MODE ##
(defvar chronometrist-statistics-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "l") #'chronometrist-open-log)
(define-key map (kbd "b") #'chronometrist-statistics-previous-range)
(define-key map (kbd "f") #'chronometrist-statistics-next-range)
map)
"Keymap used by `chronometrist-statistics-mode'.")
(define-derived-mode chronometrist-statistics-mode tabulated-list-mode "Chronometrist-Statistics"
"Major mode for `chronometrist-statistics'."
(make-local-variable 'tabulated-list-format)
(setq tabulated-list-format
[("Task" 25 t)
("Active days" 12 t)
("%% of days active" 17 t)
("Average time" 12 t)
;; ("Current streak" 10 t)
;; ("Last streak" 10 t)
;; ("Longest streak" 10 t)
])
(make-local-variable 'tabulated-list-entries)
(setq tabulated-list-entries 'chronometrist-statistics-entries)
(make-local-variable 'tabulated-list-sort-key)
(setq tabulated-list-sort-key '("Task" . nil))
(tabulated-list-init-header)
;; (chronometrist-maybe-start-timer)
(setq revert-buffer-function #'chronometrist-statistics-refresh)
(unless chronometrist--fs-watch
(setq chronometrist--fs-watch
(file-notify-add-watch chronometrist-file
'(change)
#'chronometrist-refresh-file))))
;; ## COMMANDS ##
;;;###autoload
(defun chronometrist-statistics (&optional preserve-state)
"Display statistics for data in `chronometrist-file'.
This is the 'listing command' for `chronometrist-statistics-mode'.
If a buffer called `chronometrist-statistics-buffer-name' already
exists and is visible, kill the buffer.
If PRESERVE-STATE is nil (the default when not supplied), display
data from the current week. Otherwise, display data from the week
specified by `chronometrist-statistics--ui-state'."
(interactive)
(chronometrist-migrate-check)
(let* ((buffer (get-buffer-create chronometrist-statistics-buffer-name))
(today (chronometrist-date))
(week-start (chronometrist-previous-week-start today))
(week-end (ts-adjust 'day 6 week-start)))
(with-current-buffer buffer
(cond ((get-buffer-window chronometrist-statistics-buffer-name)
(kill-buffer buffer))
(t ;; (delete-other-windows)
(unless preserve-state
(setq chronometrist-statistics--ui-state `(:mode week
:start ,week-start
:end ,week-end)))
(chronometrist-common-create-file)
(chronometrist-statistics-mode)
(switch-to-buffer buffer)
(chronometrist-statistics-refresh))))))
(defun chronometrist-statistics-previous-range (arg)
"View the statistics in the previous time range.
If ARG is a numeric argument, go back that many times."
(interactive "P")
(let* ((arg (if (and arg (numberp arg))
(abs arg)
1))
(start (plist-get chronometrist-statistics--ui-state :start)))
(cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week
(let* ((new-start (ts-adjust 'day (- (* arg 7)) start))
(new-end (ts-adjust 'day +6 new-start)))
(plist-put chronometrist-statistics--ui-state :start new-start)
(plist-put chronometrist-statistics--ui-state :end new-end))))
(setq chronometrist-statistics--point (point))
(kill-buffer)
(chronometrist-statistics t)))
(defun chronometrist-statistics-next-range (arg)
"View the statistics in the next time range.
If ARG is a numeric argument, go forward that many times."
(interactive "P")
(let* ((arg (if (and arg (numberp arg))
(abs arg)
1))
(start (plist-get chronometrist-statistics--ui-state :start)))
(cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week
(let* ((new-start (ts-adjust 'day (* arg 7) start))
(new-end (ts-adjust 'day 6 new-start)))
(plist-put chronometrist-statistics--ui-state :start new-start)
(plist-put chronometrist-statistics--ui-state :end new-end))))
(setq chronometrist-statistics--point (point))
(kill-buffer)
(chronometrist-statistics t)))
(provide 'chronometrist-statistics)
;;; chronometrist-statistics.el ends here

View File

@ -0,0 +1,178 @@
;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.0.1
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;; Add support for the Third Time system to Chronometrist. In Third
;; Time, you work for any length of time you like, and "earn" a third
;; of the work time as break time. For a more detailed explanation,
;; see
;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md
;;; Code:
(require 'chronometrist)
(require 'alert)
;; [[file:chronometrist-third.org::*group][group:1]]
(defgroup chronometrist-third nil
"Third Time support for Chronometrist."
:group 'chronometrist)
;; group:1 ends here
;; [[file:chronometrist-third.org::*divisor][divisor:1]]
(defcustom chronometrist-third-divisor 3
"Number to determine accumulation of break time relative to work time."
:type 'number)
;; divisor:1 ends here
;; [[file:chronometrist-third.org::*duration-format][duration-format:1]]
(defcustom chronometrist-third-duration-format "%H, %M and %S%z"
"Format string for durations, passed to `format-seconds'."
:type 'string)
;; duration-format:1 ends here
;; [[file:chronometrist-third.org::*break-time][break-time:1]]
(defvar chronometrist-third-break-time 0
"Accumulated break time in seconds.")
;; break-time:1 ends here
;; [[file:chronometrist-third.org::*alert-functions][alert-functions:1]]
(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert)
"List of timed alerts for the Third Time system.
Typically, each function in this list should call
`chronometrist-third-run-at-time' to run another function, which
in turn should call `alert' to notify the user.
All functions in this list are started when the user clocks out,
and stopped when they clock in."
:group 'chronometrist-third
:type 'hook)
;; alert-functions:1 ends here
;; [[file:chronometrist-third.org::*timer-list][timer-list:1]]
(defvar chronometrist-third-timer-list nil)
;; timer-list:1 ends here
;; [[file:chronometrist-third.org::*run-at-time][run-at-time:1]]
(defun chronometrist-third-run-at-time (time repeat function &rest args)
"Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'."
(cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list))
;; run-at-time:1 ends here
;; [[file:chronometrist-third.org::*half-alert][half-alert:1]]
(defun chronometrist-third-half-alert ()
"Display an alert when half the break time is consumed."
(let ((half-time (/ chronometrist-third-break-time 2.0)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
half-time nil
(lambda (half-time)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format half-time))))
half-time))))
;; half-alert:1 ends here
;; [[file:chronometrist-third.org::*quarter-alert][quarter-alert:1]]
(defun chronometrist-third-quarter-alert ()
"Display an alert when 3/4ths of the break time is consumed."
(let ((three-fourths (* chronometrist-third-break-time 7.5)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
three-fourths nil
(lambda (three-fourths)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format
(- chronometrist-third-break-time three-fourths)))))
three-fourths))))
;; quarter-alert:1 ends here
;; [[file:chronometrist-third.org::*break-over-alert][break-over-alert:1]]
(defun chronometrist-third-break-over-alert ()
"Display an alert when break time is over."
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
chronometrist-third-break-time nil
(lambda () (alert (format "Break time is over!"))))))
;; break-over-alert:1 ends here
;; [[file:chronometrist-third.org::*start-alert-timers][start-alert-timers:1]]
(defun chronometrist-third-start-alert-timers ()
"Run functions in `chronometrist-third-alert-functions'."
(mapc #'funcall chronometrist-third-alert-functions))
;; start-alert-timers:1 ends here
;; [[file:chronometrist-third.org::*stop-alert-timers][stop-alert-timers:1]]
(defun chronometrist-third-stop-alert-timers ()
"Stop timers in `chronometrist-third-timer-list'."
(mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list))
;; stop-alert-timers:1 ends here
;; [[file:chronometrist-third.org::*clock-in][clock-in:1]]
(defun chronometrist-third-clock-in (&optional _arg)
"Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time)
(-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend))))
(used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop)))
(used-break-string (format-seconds chronometrist-third-duration-format used-break))
(new-break (- chronometrist-third-break-time used-break))
(old-break chronometrist-third-break-time))
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0))
(alert
(if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break)
used-break-string)
(format "You have used %s of your break time (%s left)"
used-break-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
;; clock-in:1 ends here
;; [[file:chronometrist-third.org::*clock-out][clock-out:1]]
(defun chronometrist-third-clock-out (&optional _arg)
"Update break time based on the latest work interval.
Run `chronometrist-third-alert-functions' to alert user when
break time is up."
(let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))
(break-time-increment (/ latest-work-duration chronometrist-third-divisor)))
(cl-incf chronometrist-third-break-time break-time-increment)
(alert (format "You have gained %s of break time (%s total)"
(format-seconds chronometrist-third-duration-format break-time-increment)
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))
;; start alert timer(s)
(chronometrist-third-start-alert-timers)))
;; clock-out:1 ends here
;; [[file:chronometrist-third.org::*third-minor-mode][third-minor-mode:1]]
;;;###autoload
(define-minor-mode chronometrist-third-minor-mode
nil nil nil nil
(cond (chronometrist-third-minor-mode
(add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))
(t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))))
;; third-minor-mode:1 ends here
(provide 'chronometrist-third)
;;; chronometrist-third.el ends here

View File

@ -0,0 +1,213 @@
#+TITLE: chronometrist-third
#+SUBTITLE: Third Time System extension for Chronometrist
#+PROPERTY: header-args :tangle yes :load yes :comments link
* Program source
** Library headers and commentary
#+BEGIN_SRC emacs-lisp :comments no
;;; chronometrist-third.el --- Third Time support for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.0.1
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;; Add support for the Third Time system to Chronometrist. In Third
;; Time, you work for any length of time you like, and "earn" a third
;; of the work time as break time. For a more detailed explanation,
;; see
;; https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work
;; For information on usage and customization, see https://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/README.md
#+END_SRC
** Dependencies
#+BEGIN_SRC emacs-lisp :comments no
;;; Code:
(require 'chronometrist)
(require 'alert)
#+END_SRC
** group :custom:group:
#+BEGIN_SRC emacs-lisp
(defgroup chronometrist-third nil
"Third Time support for Chronometrist."
:group 'chronometrist)
#+END_SRC
** divisor :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-divisor 3
"Number to determine accumulation of break time relative to work time."
:type 'number)
#+END_SRC
** duration-format :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-duration-format "%H, %M and %S%z"
"Format string for durations, passed to `format-seconds'."
:type 'string)
#+END_SRC
** break-time :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-third-break-time 0
"Accumulated break time in seconds.")
#+END_SRC
** alert-functions :custom:variable:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-third-alert-functions '(chronometrist-third-half-alert chronometrist-third-quarter-alert chronometrist-third-break-over-alert)
"List of timed alerts for the Third Time system.
Typically, each function in this list should call
`chronometrist-third-run-at-time' to run another function, which
in turn should call `alert' to notify the user.
All functions in this list are started when the user clocks out,
and stopped when they clock in."
:group 'chronometrist-third
:type 'hook)
#+END_SRC
** timer-list :variable:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-third-timer-list nil)
#+END_SRC
** run-at-time :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-run-at-time (time repeat function &rest args)
"Like `run-at-time', but store timer objects in `chronometrist-third-timer-list'."
(cl-pushnew (apply #'run-at-time time repeat function args) chronometrist-third-timer-list))
#+END_SRC
** half-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-half-alert ()
"Display an alert when half the break time is consumed."
(let ((half-time (/ chronometrist-third-break-time 2.0)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
half-time nil
(lambda (half-time)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format half-time))))
half-time))))
#+END_SRC
** quarter-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-quarter-alert ()
"Display an alert when 3/4ths of the break time is consumed."
(let ((three-fourths (* chronometrist-third-break-time 7.5)))
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
three-fourths nil
(lambda (three-fourths)
(alert
(format "%s left on your break."
(format-seconds chronometrist-third-duration-format
(- chronometrist-third-break-time three-fourths)))))
three-fourths))))
#+END_SRC
** break-over-alert :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-break-over-alert ()
"Display an alert when break time is over."
(and (not (zerop chronometrist-third-break-time))
(chronometrist-third-run-at-time
chronometrist-third-break-time nil
(lambda () (alert (format "Break time is over!"))))))
#+END_SRC
** start-alert-timers :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-start-alert-timers ()
"Run functions in `chronometrist-third-alert-functions'."
(mapc #'funcall chronometrist-third-alert-functions))
#+END_SRC
** stop-alert-timers :procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-stop-alert-timers ()
"Stop timers in `chronometrist-third-timer-list'."
(mapc (lambda (timer) (cancel-timer timer)) chronometrist-third-timer-list))
#+END_SRC
** clock-in :hook:procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-clock-in (&optional _arg)
"Stop alert timers and update break time."
(chronometrist-third-stop-alert-timers)
(unless (zerop chronometrist-third-break-time)
(-let* (((&plist :stop stop) (cl-second (chronometrist-to-list (chronometrist-active-backend))))
(used-break (ts-diff (ts-now) (chronometrist-iso-to-ts stop)))
(used-break-string (format-seconds chronometrist-third-duration-format used-break))
(new-break (- chronometrist-third-break-time used-break))
(old-break chronometrist-third-break-time))
(setq chronometrist-third-break-time (if (> new-break 0) new-break 0))
(alert
(if (zerop chronometrist-third-break-time)
(format "You have used up all %s of your break time (%s break)"
(format-seconds chronometrist-third-duration-format old-break)
used-break-string)
(format "You have used %s of your break time (%s left)"
used-break-string
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))))))
#+END_SRC
** clock-out :hook:procedure:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-third-clock-out (&optional _arg)
"Update break time based on the latest work interval.
Run `chronometrist-third-alert-functions' to alert user when
break time is up."
(let* ((latest-work-duration (chronometrist-interval (chronometrist-latest-record (chronometrist-active-backend))))
(break-time-increment (/ latest-work-duration chronometrist-third-divisor)))
(cl-incf chronometrist-third-break-time break-time-increment)
(alert (format "You have gained %s of break time (%s total)"
(format-seconds chronometrist-third-duration-format break-time-increment)
(format-seconds chronometrist-third-duration-format chronometrist-third-break-time)))
;; start alert timer(s)
(chronometrist-third-start-alert-timers)))
#+END_SRC
** third-minor-mode :minor:mode:
#+BEGIN_SRC emacs-lisp
;;;###autoload
(define-minor-mode chronometrist-third-minor-mode
nil nil nil nil
(cond (chronometrist-third-minor-mode
(add-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(add-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))
(t (remove-hook 'chronometrist-after-in-functions #'chronometrist-third-clock-in)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-third-clock-out))))
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp :comments no
(provide 'chronometrist-third)
;;; chronometrist-third.el ends here
#+END_SRC
* Local variables :noexport:
# Local Variables:
# my-org-src-default-lang: "emacs-lisp"
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End:

View File

@ -1,111 +0,0 @@
;;; chronometrist-time.el --- Time and date functions for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
(require 'parse-time)
(require 'dash)
(require 's)
(require 'chronometrist-report-custom)
(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
;; 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:
;; Pretty sure quite a few of these are redundant. Hopefully putting
;; them together in the same file will make it easier to figure out
;; which ones those are.
;;; Code:
(defun chronometrist-iso-timestamp->ts (timestamp)
"Return new ts struct, parsing TIMESTAMP with `parse-iso8601-time-string'."
(-let [(second minute hour day month year dow _dst utcoff)
(decode-time
(parse-iso8601-time-string timestamp))]
(ts-update
(make-ts :hour hour :minute minute :second second
:day day :month month :year year
:dow dow :tz-offset utcoff))))
(defun chronometrist-iso-date->ts (date)
"Return a ts struct (see `ts.el') representing DATE.
DATE should be an ISO-8601 date string (\"YYYY-MM-DD\")."
(let* ((date-list (mapcar #'string-to-number
(split-string date "-")))
(day (caddr date-list))
(month (cadr date-list))
(year (car date-list)))
(ts-update
(make-ts :hour 0 :minute 0 :second 0
:day day :month month :year year))))
(cl-defun chronometrist-date (&optional (ts (ts-now)))
"Return a ts struct representing the time 00:00:00 on today's date.
If TS is supplied, use that date instead of today.
TS should be a ts struct (see `ts.el')."
(ts-apply :hour 0 :minute 0 :second 0 ts))
(defun chronometrist-format-time-iso8601 (&optional unix-time)
"Return current moment as an ISO-8601 format time string.
Optional argument UNIX-TIME should be a time value (see
`current-time') accepted by `format-time-string'."
(format-time-string "%FT%T%z" unix-time))
;; Note - this assumes that an event never crosses >1 day. This seems
;; sufficient for all conceivable cases.
(defun chronometrist-midnight-spanning-p (start-time stop-time)
"Return non-nil if START-TIME and STOP-TIME cross a midnight.
Return value is a list in the form
\((:start START-TIME
:stop <day-start time on initial day>)
(:start <day start time on second day>
:stop STOP-TIME))"
;; FIXME - time zones are ignored; may cause issues with
;; time-zone-spanning events
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
(let* ((first-day-start (chronometrist-day-start start-time))
;; HACK - won't work with custom day-start time
;; (first-day-end (parse-iso8601-time-string
;; (concat (chronometrist-date (parse-iso8601-time-string start-time))
;; "24:00:00")))
(next-day-start (time-add first-day-start
'(0 . 86400)))
(stop-time-unix (parse-iso8601-time-string stop-time)))
;; Does the event stop time exceed the next day start time?
(when (time-less-p next-day-start stop-time-unix)
(list `(:start ,start-time
:stop ,(chronometrist-format-time-iso8601 next-day-start))
`(:start ,(chronometrist-format-time-iso8601 next-day-start)
:stop ,stop-time)))))
(defun chronometrist-seconds-to-hms (seconds)
"Convert SECONDS to a vector in the form [HOURS MINUTES SECONDS].
SECONDS must be a positive integer."
(let* ((seconds (truncate seconds))
(s (% seconds 60))
(m (% (/ seconds 60) 60))
(h (/ seconds 3600)))
(list h m s)))
(defun chronometrist-interval (event)
"Return the period of time covered by EVENT as a time value.
EVENT should be a plist (see `chronometrist-file')."
(let ((start (plist-get event :start))
(stop (plist-get event :stop)))
(time-subtract (parse-iso8601-time-string stop)
(parse-iso8601-time-string start))))
(provide 'chronometrist-time)
;;; chronometrist-time.el ends here

View File

@ -1,84 +0,0 @@
;;; chronometrist-timer.el --- Timer-related functions for Chronometrist -*- 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 'chronometrist-custom)
(require 'chronometrist-statistics-custom)
(require 'chronometrist-report-custom)
(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)
(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
;; 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))))
(defun chronometrist-stop-timer ()
"Stop the timer for Chronometrist buffers."
(interactive)
(cancel-timer chronometrist--timer-object)
(setq chronometrist--timer-object nil))
(defun chronometrist-maybe-start-timer (&optional interactive-test)
"Start `chronometrist-timer' if `chronometrist--timer-object' is non-nil.
INTERACTIVE-TEST is used to determine if this has been called
interactively."
(interactive "p")
(unless chronometrist--timer-object
(setq chronometrist--timer-object
(run-at-time t chronometrist-update-interval #'chronometrist-timer))
(when interactive-test
(message "Timer started."))
t))
(defun chronometrist-force-restart-timer ()
"Restart the timer for Chronometrist buffers."
(interactive)
(when chronometrist--timer-object
(cancel-timer chronometrist--timer-object))
(setq chronometrist--timer-object
(run-at-time t chronometrist-update-interval #'chronometrist-timer)))
(defun chronometrist-change-update-interval (arg)
"Change the update interval for Chronometrist buffers.
ARG should be the new update interval, in seconds."
(interactive "NEnter new interval (in seconds): ")
(cancel-timer chronometrist--timer-object)
(setq chronometrist-update-interval arg
chronometrist--timer-object nil)
(chronometrist-maybe-start-timer))
(provide 'chronometrist-timer)
;;; chronometrist-timer.el ends here

File diff suppressed because it is too large Load Diff

4794
elisp/chronometrist.org Normal file

File diff suppressed because it is too large Load Diff

443
manual.org Normal file
View File

@ -0,0 +1,443 @@
#+TITLE: Chronometrist
#+SUBTITLE: Friendly and powerful personal time tracker/analyzer with Emacs and CLIM frontends
#+DESCRIPTION: User Manual
#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="style.css" />
#+BEGIN_EXPORT html
<a href="https://liberapay.com/contrapunctus/donate">
<img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay">
</a>
<a href="https://melpa.org/#/chronometrist">
<img src="https://melpa.org/packages/chronometrist-badge.svg">
</a>
#+END_EXPORT
* Explanation
:PROPERTIES:
:CUSTOM_ID: explanation
:END:
Chronometrist is a friendly and powerful personal time tracker and analyzer. It has frontends for Emacs and [[https://mcclim.common-lisp.dev/][CLIM]].
#+CAPTION: The main Chronometrist buffer, with the enabled extensions [[#time-goals][chronometrist-goal]] ("Targets" column + alerts) and chronometrist-spark ("Graph" column displaying the activity for the past 4 weeks).
[[file:doc/2022-02-20 13-26-53.png]]
** Benefits
:PROPERTIES:
:CUSTOM_ID: benefits
:END:
1. Extremely simple and efficient to use
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 can be easily fixed by editing a plain text file
5. Hooks to integrate time tracking into your workflow
** Limitations
:PROPERTIES:
:CUSTOM_ID: limitations
:END:
1. No support for concurrent tasks.
** Comparisons
:PROPERTIES:
:CUSTOM_ID: comparisons
:END:
*** timeclock.el (Emacs built-in)
:PROPERTIES:
:CUSTOM_ID: timeclock.el
:END:
Compared to timeclock.el, Chronometrist
+ stores data in an s-expression format rather than a line-based one
+ supports attaching tags and arbitrary key-values to time intervals
+ has commands to shows useful summaries
+ has more hooks
*** Org time tracking
:PROPERTIES:
:CUSTOM_ID: org-time-tracking
:END:
Chronometrist and Org time tracking seem to be equivalent in terms of capabilities, approaching the same ends through different means.
+ Chronometrist doesn't have a mode line indicator at the moment. (planned)
+ Chronometrist doesn't have Org's sophisticated querying facilities. (an SQLite backend is planned)
+ Org does so many things that keybindings seem to necessarily get longer. Chronometrist has far fewer commands than Org, so most of the keybindings are single keys, without modifiers.
+ Chronometrist's UI is cleaner, since the storage is separate from the display. It doesn't show tasks as trees like Org, but it uses tags and key-values to achieve that. Additionally, navigating a flat list takes fewer user operations than navigating a tree.
+ Chronometrist data is just s-expressions (plists), and may be easier to parse than a complex text format with numerous use-cases.
** Common Lisp port
:PROPERTIES:
:CUSTOM_ID: common-lisp-port
:END:
In March 2022, work began on the long-awaited Common Lisp port of Chronometrist, which aims to create -
1. a greater variety of backends (e.g. SQLite)
2. a common reusable library for frontends to use,
3. a greater variety of frontends, such as -
* a command line interface (CLI), for UNIX scripting;
* a terminal user inteface (TUI), for those so inclined;
* a CLIM (Common Lisp Interface Manager) GUI [fn:1],
* Qt and Android interfaces using [[https://gitlab.com/eql/lqml][LQML]],
* web frontends (possibly via [[https://common-lisp.net/project/parenscript/][Parenscript]] or [[https://github.com/rabbibotton/clog][CLOG]]),
* and perhaps even an interface for wearable devices!
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.
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")
#+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
:PROPERTIES:
:CUSTOM_ID: explanation-literate-program
:END:
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 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]
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
:PROPERTIES:
:CUSTOM_ID: source-code-overview
:END:
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:
:CUSTOM_ID: license
:END:
I'd /like/ for all software to be liberated - transparent, trustable, and accessible for anyone to use, study, or improve.
I'd /like/ anyone using my software to credit me for the work.
I'd /like/ to receive financial support for my efforts, so I can spend all my time doing what I find meaningful.
But I don't want to make demands or threats (e.g. via legal conditions) to accomplish all that, nor restrict my services to only those who can pay.
Thus, Chronometrist is released under your choice of [[https://unlicense.org/][Unlicense]] or the [[http://www.wtfpl.net/][WTFPL]].
(See files [[file:UNLICENSE][UNLICENSE]] and [[file:WTFPL][WTFPL]]).
** Thanks
:PROPERTIES:
:CUSTOM_ID: thanks
:END:
The main buffer and the report buffer are copied from the Android application, [[https://github.com/netmackan/ATimeTracker][A Time Tracker]]
wasamasa, bpalmer, aidalgol, pjb and the rest of #emacs for their tireless help and support
jwiegley for =timeclock.el=, which we used as a backend in earlier versions
blandest for helping me with the name
fiete and wu-lee for testing and bug reports
* Tutorials
:PROPERTIES:
:CUSTOM_ID: usage
:END:
** Installation
:PROPERTIES:
:CUSTOM_ID: installation
:END:
*** from MELPA
:PROPERTIES:
:CUSTOM_ID: install-from-melpa
:END:
1. Set up MELPA - https://melpa.org/#/getting-started
2. =M-x package-install RET chronometrist RET=
*** from Git
:PROPERTIES:
:CUSTOM_ID: install-from-git
:END:
You can get =chronometrist= from https://tildegit.org/contrapunctus/chronometrist or https://codeberg.org/contrapunctus/chronometrist
=chronometrist= requires
+ Emacs v25 or higher
+ [[https://github.com/magnars/dash.el][dash.el]]
+ [[https://github.com/alphapapa/ts.el][ts.el]]
Add the ="elisp/"= subdirectory to your load-path, and =(require 'chronometrist)=.
** chronometrist
:PROPERTIES:
:CUSTOM_ID: usage-chronometrist
:END:
Run =M-x chronometrist= to see your projects, the time you spent on them today, which one is active, and the total time clocked today.
Click or hit =RET= (=chronometrist-toggle-task=) on a project to start tracking time for it. If it's already clocked in, it will be clocked out.
You can also hit =<numeric prefix> RET= anywhere in the buffer to toggle the corresponding project, e.g. =C-1 RET= will toggle the project with index 1.
Press =r= to see a weekly report (see =chronometrist-report=)
** chronometrist-report
:PROPERTIES:
:CUSTOM_ID: usage-chronometrist-report
:END:
Run =M-x chronometrist-report= (or =chronometrist= with a prefix argument of 1, or press =r= in the =chronometrist= buffer) to see a weekly report.
Press =b= to look at past weeks, and =f= for future weeks.
** chronometrist-statistics
:PROPERTIES:
:CUSTOM_ID: usage-chronometrist-statistics
:END:
Run =M-x chronometrist-statistics= (or =chronometrist= with a prefix argument of 2) to view statistics.
Press =b= to look at past time ranges, and =f= for future ones.
** chronometrist-details
:PROPERTIES:
:CUSTOM_ID: chronometrist-details
:END:
** common commands
:PROPERTIES:
:CUSTOM_ID: usage-common-commands
:END:
In the buffers created by the previous three commands, you can press =l= (=chronometrist-open-log=) to view/edit your =chronometrist-file=, which by default is =~/.emacs.d/chronometrist.sexp=.
All of these commands will kill their buffer when run again with the buffer visible, so the keys you bind them to behave as a toggle.
All buffers keep themselves updated via an idle timer - no need to frequently press =g= to update.
** Time goals/targets
:PROPERTIES:
:CUSTOM_ID: time-goals
:END:
If you wish you could define time goals for some tasks, and have Chronometrist notify you when you're approaching the goal, completing it, or exceeding it, check out the extension [[https://github.com/contrapunctus-1/chronometrist-goal/][chronometrist-goal.el]].
* How-to Guides
:PROPERTIES:
:CUSTOM_ID: how-to
:END:
See the Customize groups =chronometrist= and =chronometrist-report= for variables intended to be user-customizable.
** How to display a prompt when exiting with an active task
:PROPERTIES:
:CUSTOM_ID: how-to-prompt-when-exiting-emacs
:END:
Evaluate or add to your init.el the following -
=(add-hook 'kill-emacs-query-functions 'chronometrist-query-stop)=
** How to load the program using literate-elisp
: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>")
(require 'literate-elisp) ;; or autoload, use-package, ...
(literate-elisp-load "chronometrist.org")
#+END_SRC
** How to attach tags to time intervals
:PROPERTIES:
:CUSTOM_ID: how-to-tags
:END:
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)
(add-to-list 'chronometrist-before-out-functions 'chronometrist-tags-add)
(add-to-list 'chronometrist-after-out-functions 'chronometrist-tags-add)
#+END_SRC
2. clock in/clock out to trigger the hook.
The prompt suggests past combinations you used for the current task, which you can browse with =M-p=/=M-n=. You can leave it blank by pressing =RET=.
[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: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)
#+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.
** How to skip running hooks/attaching tags and key values
:PROPERTIES:
:CUSTOM_ID: how-to-skip-running-hooks/attaching-tags-and-key-values
:END:
Use =M-RET= (=chronometrist-toggle-task-no-hooks=) to clock in/out.
** How to open certain files when you start a task
:PROPERTIES:
:CUSTOM_ID: how-to-open-files-on-task-start
:END:
An idea from the author's own init -
#+BEGIN_SRC emacs-lisp
(defun my-start-project (project)
(pcase project
("Guitar"
(find-file-other-window "~/repertoire.org"))
;; ...
))
(add-hook 'chronometrist-before-in-functions 'my-start-project)
#+END_SRC
** How to warn yourself about uncommitted changes
:PROPERTIES:
:CUSTOM_ID: how-to-warn-uncommitted-changes
:END:
Another one, prompting the user if they have uncommitted changes in a git repository (assuming they use [[https://magit.vc/][Magit]]) -
#+BEGIN_SRC emacs-lisp
(autoload 'magit-anything-modified-p "magit")
(defun my-commit-prompt ()
"Prompt user if `default-directory' is a dirty Git repository.
Return t if the user answers yes, if the repository is clean, or
if there is no Git repository.
Return nil (and run `magit-status') if the user answers no."
(cond ((not (magit-anything-modified-p)) t)
((yes-or-no-p
(format "You have uncommitted changes in %S. Really clock out? "
default-directory)) t)
(t (magit-status) nil)))
(add-hook 'chronometrist-before-out-functions 'my-commit-prompt)
#+END_SRC
** How to display the current time interval in the activity indicator
:PROPERTIES:
:CUSTOM_ID: how-to-activity-indicator
:END:
#+BEGIN_SRC emacs-lisp
(defun my-activity-indicator ()
(--> (chronometrist-latest-record (chronometrist-active-backend))
(plist-put it :stop (chronometrist-format-time-iso8601))
(list it)
(chronometrist-events-to-durations it)
(-reduce #'+ it)
(truncate it)
(chronometrist-format-duration it)))
(setq chronometrist-activity-indicator #'my-activity-indicator)
#+END_SRC
** How to back up your Chronometrist data
: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:4] Here's how you can do that.
1. Add the following to your init.
#+BEGIN_SRC emacs-lisp
(use-package async-backup)
#+END_SRC
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 =async-backup-location= to set a specific directory for the backups -
: (setq async-backup-location "/path/to/backup/dir/")
[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:
:CUSTOM_ID: howto-vertico
:END:
By default, [[https://github.com/minad/vertico][Vertico]] uses its own sorting function - for some commands (such as =chronometrist-key-values-unified-prompt=) this results in /worse/ suggestions, since Chronometrist sorts suggestions in most-recent-first order.
You can either disable Vertico's sorting entirely -
#+BEGIN_SRC emacs-lisp
(setq vertico-sort-function nil)
#+END_SRC
Or use =vertico-multiform= to disable sorting for only specific commands -
#+BEGIN_SRC emacs-lisp
(use-package vertico-multiform
:init (vertico-multiform-mode)
:config
(setq vertico-multiform-commands
'((chronometrist-toggle-task (vertico-sort-function . nil))
(chronometrist-toggle-task-no-hooks (vertico-sort-function . nil))
(chronometrist-key-values-unified-prompt (vertico-sort-function . nil)))))
#+END_SRC
* User's reference
:PROPERTIES:
:CUSTOM_ID: users-reference
:END:
All variables intended for user customization are listed here. They serve as the public API for this project for the purpose of semantic versioning. Any changes to these which require a user to modify their configuration are considered breaking changes.
1. =chronometrist-file=
2. =chronometrist-buffer-name=
3. =chronometrist-report-buffer-name=
4. =chronometrist-details-buffer-name=
5. =chronometrist-sexp-pretty-print-function=
6. =chronometrist-hide-cursor=
7. =chronometrist-update-interval=
8. =chronometrist-activity-indicator=
Buffer schemas
1. =chronometrist-schema=
2. =chronometrist-details-schema=
Hooks
1. =chronometrist-mode-hook=
2. =chronometrist-schema-transformers=
3. =chronometrist-row-transformers=
4. =chronometrist-before-in-functions=
5. =chronometrist-after-in-functions=
6. =chronometrist-before-out-functions=
7. =chronometrist-after-out-functions=
8. =chronometrist-file-change-hook=
9. =chronometrist-timer-hook=
* Local variables :noexport:
:PROPERTIES:
:CUSTOM_ID: local-variables
:END:
# Local Variables:
# my-org-src-default-lang: "emacs-lisp"
# End:

679
org-doom-molokai.css Normal file
View File

@ -0,0 +1,679 @@
/* An incomplete mix of */
/* 1. motherfuckingwebsite.com + bettermotherfuckingwebsite.com +
thebestmotherfucking.website*/
/* 2. Org default style*/
/* 3. doom-molokai, using htmlize-buffer */
body {
margin: 40px auto;
max-width: 1000px;
line-height: 1.6;
font-size: 18px;
padding: 0 10px;
font-family: sans-serif;
/* original colors */
/* color: #444; */
/* background-color: #eeeeee; */
/* colors resembling Firefox's reader mode in the dark setting */
color: #eeeeee !important;
background-color: #333333;
}
h1,h2,h3 {
line-height: 1.2;
}
img {
max-width: 80%;
height: auto;
width: auto\9; /* ie8 */
}
Org defaults
.title {
text-align: center;
margin-bottom: .2em;
}
.subtitle {
text-align: center;
font-size: medium;
font-weight: bold;
margin-top:0;
}
.todo {
font-family: monospace;
color: red;
}
.done {
font-family: monospace;
color: green;
}
.priority {
font-family: monospace;
color: orange;
}
.tag {
background-color: #111111;
font-family: monospace;
padding: 2px;
font-size: 80%;
font-weight: normal;
}
.timestamp {
color: #bebebe;
}
.timestamp-kwd {
color: #5f9ea0;
}
.org-right {
margin-left: auto;
margin-right: 0px;
text-align: right;
}
.org-left {
margin-left: 0px;
margin-right: auto;
text-align: left;
}
.org-center {
margin-left: auto;
margin-right: auto;
text-align: center;
}
.underline {
text-decoration: underline;
}
#postamble p, #preamble p {
font-size: 90%;
margin: .2em;
}
p.verse {
margin-left: 3%;
}
pre {
border: 1px solid #ccc;
/* box-shadow: 3px 3px 3px #eee; */
padding: 8pt;
font-family: monospace;
overflow: scroll;
margin: 1.2em;
}
pre.src {
position: relative;
overflow: scroll;
padding-top: 1.2em;
}
pre.src:before {
display: none;
position: absolute;
background-color: #111111;
/* top: -10px; */
/* right: 10px; */
/* padding: 3px; */
border: 1px solid black;
}
pre.src:hover:before {
display: inline;}
/* Languages per Org manual */
pre.src-asymptote:before {
content: 'Asymptote';
}
pre.src-awk:before {
content: 'Awk';
}
pre.src-C:before {
content: 'C';
}
/* pre.src-C++ doesn't work in CSS */
pre.src-clojure:before {
content: 'Clojure';
}
pre.src-css:before {
content: 'CSS';
}
pre.src-D:before {
content: 'D';
}
pre.src-ditaa:before {
content: 'ditaa';
}
pre.src-dot:before {
content: 'Graphviz';
}
pre.src-calc:before {
content: 'Emacs Calc';
}
pre.src-emacs-lisp:before {
content: 'Emacs Lisp';
}
pre.src-fortran:before {
content: 'Fortran';
}
pre.src-gnuplot:before {
content: 'gnuplot';
}
pre.src-haskell:before {
content: 'Haskell';
}
pre.src-hledger:before {
content: 'hledger';
}
pre.src-java:before {
content: 'Java';
}
pre.src-js:before {
content: 'Javascript';
}
pre.src-latex:before {
content: 'LaTeX';
}
pre.src-ledger:before {
content: 'Ledger';
}
pre.src-lisp:before {
content: 'Lisp';
}
pre.src-lilypond:before {
content: 'Lilypond';
}
pre.src-lua:before {
content: 'Lua';
}
pre.src-matlab:before {
content: 'MATLAB';
}
pre.src-mscgen:before {
content: 'Mscgen';
}
pre.src-ocaml:before {
content: 'Objective Caml';
}
pre.src-octave:before {
content: 'Octave';
}
pre.src-org:before {
content: 'Org mode';
}
pre.src-oz:before {
content: 'OZ';
}
pre.src-plantuml:before {
content: 'Plantuml';
}
pre.src-processing:before {
content: 'Processing.js';
}
pre.src-python:before {
content: 'Python';
}
pre.src-R:before {
content: 'R';
}
pre.src-ruby:before {
content: 'Ruby';
}
pre.src-sass:before {
content: 'Sass';
}
pre.src-scheme:before {
content: 'Scheme';
}
pre.src-screen:before {
content: 'Gnu Screen';
}
pre.src-sed:before {
content: 'Sed';
}
pre.src-sh:before {
content: 'shell';
}
pre.src-sql:before {
content: 'SQL';
}
pre.src-sqlite:before {
content: 'SQLite';
}
/* additional languages in org.el's org-babel-load-languages alist */
pre.src-forth:before {
content: 'Forth';
}
pre.src-io:before {
content: 'IO';
}
pre.src-J:before {
content: 'J';
}
pre.src-makefile:before {
content: 'Makefile';
}
pre.src-maxima:before {
content: 'Maxima';
}
pre.src-perl:before {
content: 'Perl';
}
pre.src-picolisp:before {
content: 'Pico Lisp';
}
pre.src-scala:before {
content: 'Scala';
}
pre.src-shell:before {
content: 'Shell Script';
}
pre.src-ebnf2ps:before {
content: 'ebfn2ps';
}
/* additional language identifiers per "defun org-babel-execute"
in ob-*.el */
pre.src-cpp:before {
content: 'C++';
}
pre.src-abc:before {
content: 'ABC';
}
pre.src-coq:before {
content: 'Coq';
}
pre.src-groovy:before {
content: 'Groovy';
}
/* additional language identifiers from org-babel-shell-names in
ob-shell.el: ob-shell is the only babel language using a lambda to put
the execution function name together. */
pre.src-bash:before {
content: 'bash';
}
pre.src-csh:before {
content: 'csh';
}
pre.src-ash:before {
content: 'ash';
}
pre.src-dash:before {
content: 'dash';
}
pre.src-ksh:before {
content: 'ksh';
}
pre.src-mksh:before {
content: 'mksh';
}
pre.src-posh:before {
content: 'posh';
}
/* Additional Emacs modes also supported by the LaTeX listings package */
pre.src-ada:before {
content: 'Ada';
}
pre.src-asm:before {
content: 'Assembler';
}
pre.src-caml:before {
content: 'Caml';
}
pre.src-delphi:before {
content: 'Delphi';
}
pre.src-html:before {
content: 'HTML';
}
pre.src-idl:before {
content: 'IDL';
}
pre.src-mercury:before {
content: 'Mercury';
}
pre.src-metapost:before {
content: 'MetaPost';
}
pre.src-modula-2:before {
content: 'Modula-2';
}
pre.src-pascal:before {
content: 'Pascal';
}
pre.src-ps:before {
content: 'PostScript';
}
pre.src-prolog:before {
content: 'Prolog';
}
pre.src-simula:before {
content: 'Simula';
}
pre.src-tcl:before {
content: 'tcl';
}
pre.src-tex:before {
content: 'TeX';
}
pre.src-plain-tex:before {
content: 'Plain TeX';
}
pre.src-verilog:before {
content: 'Verilog';
}
pre.src-vhdl:before {
content: 'VHDL';
}
pre.src-xml:before {
content: 'XML';
}
pre.src-nxml:before {
content: 'XML';
}
/* add a generic configuration mode;
LaTeX export needs an additional
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
pre.src-conf:before {
content: 'Configuration File';
}
/* table { */
/* border-collapse:collapse; */
/* } */
/* caption.t-above { */
/* caption-side: top; */
/* } */
/* caption.t-bottom { */
/* caption-side: bottom; */
/* } */
/* td, th { */
/* vertical-align:top; */
/* } */
/* th.org-right { */
/* text-align: center; */
/* } */
/* th.org-left { */
/* text-align: center; */
/* } */
/* th.org-center { */
/* text-align: center; */
/* } */
/* td.org-right { */
/* text-align: right; */
/* } */
/* td.org-left { */
/* text-align: left; */
/* } */
/* td.org-center { */
/* text-align: center; */
/* } */
/* dt { */
/* font-weight: bold; */
/* } */
/* .footpara { */
/* display: inline; */
/* } */
/* .footdef { */
/* margin-bottom: 1em; */
/* } */
/* .figure { */
/* padding: 1em; */
/* } */
/* .figure p { */
/* text-align: center; */
/* } */
/* .equation-container { */
/* display: table; */
/* text-align: center; */
/* width: 100%; */
/* } */
/* .equation { */
/* vertical-align: middle; */
/* } */
/* .equation-label { */
/* display: table-cell; */
/* text-align: right; */
/* vertical-align: middle; */
/* } */
/* .inlinetask { */
/* padding: 10px; */
/* border: 2px solid gray; */
/* margin: 10px; */
/* background: #ffffcc; */
/* } */
/* #org-div-home-and-up */
/* { */
/* text-align: right; */
/* font-size: 70%; */
/* white-space: nowrap; */
/* } */
/* textarea { */
/* overflow-x: auto; */
/* } */
/* .linenr { */
/* font-size: smaller */
/* } */
/* .code-highlighted { */
/* background-color: #ffff00; */
/* } */
/* .org-info-js_info-navigation { */
/* border-style: none; */
/* } */
/* #org-info-js_console-label */
/* { */
/* font-size: 10px; */
/* font-weight: bold; */
/* white-space: nowrap; */
/* } */
/* .org-info-js_search-highlight */
/* { */
/* background-color: #ffff00; */
/* color: #000000; */
/* font-weight: bold; */
/* } */
/* .org-svg { */
/* width: 90%; */
/* } */
/* htmlize-buffer output */
body {
color: #eff0f1;
background-color: #232629;
}
.builtin {
/* font-lock-builtin-face */
color: #fd971f;
}
.comment {
/* font-lock-comment-face */
color: #555556;
}
.comment-delimiter {
/* font-lock-comment-delimiter-face */
color: #555556;
}
.constant {
/* font-lock-constant-face */
color: #fd971f;
}
.custom {
/* (:background "#27da2b442eaf" :extend t) */
background-color: #27da2b442eaf;
}
.doc {
/* font-lock-doc-face */
color: #7f7f80;
}
.function-name {
/* font-lock-function-name-face */
color: #b6e63e;
}
.hl-line {
/* hl-line */
background-color: #222323;
}
.italic {
/* italic */
font-style: italic;
}
.keyword {
/* font-lock-keyword-face */
color: #fb2874;
}
.nameless {
/* nameless-face */
color: #66d9ef;
}
.org-block-begin-line {
/* org-block-begin-line */
color: #555556;
background-color: #2d2e2e;
}
.org-block-end-line {
/* org-block-end-line */
color: #555556;
background-color: #2d2e2e;
}
.org-checkbox {
/* org-checkbox */
color: #e2c770;
font-weight: bold;
}
.org-checkbox-statistics-todo {
/* org-checkbox-statistics-todo */
color: #e2c770;
font-weight: bold;
}
code {
/* org-code */
color: #fd971f;
}
.org-document-info-keyword {
/* org-document-info-keyword */
color: #555556;
}
.org-document-title {
/* org-document-title */
color: #fd971f;
font-weight: bold;
}
.org-done {
/* org-done */
color: #555556;
font-weight: bold;
}
.org-drawer {
/* org-drawer */
color: #87cefa;
}
.org-footnote {
/* org-footnote */
color: #fd971f;
}
.org-hide {
/* org-hide */
color: #232629;
}
/* I want URLs in headings to be colored using the heading colors */
a {
color: #fd971f;
font-weight: bold;
text-decoration: underline;
}
h1 {
color: #fb2874;
font-weight: bold;
}
h2 {
color: #fd971f;
font-weight: bold;
}
h3 {
color: #9c91e4;
font-weight: bold;
}
h4 {
color: #5ca8dd;
font-weight: bold;
}
h5 {
color: #fb5d96;
font-weight: bold;
}
h6 {
color: #92c4e8;
font-weight: bold;
}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a, h7 a { color: inherit; }
.org-meta-line {
/* org-meta-line */
color: #7f7f80;
}
.org-property-value {
/* org-property-value */
color: #7f7f80;
}
.org-special-keyword {
/* org-special-keyword */
color: #7f7f80;
}
.org-tag {
/* org-tag */
color: #e2c770;
}
.org-todo {
/* org-todo */
color: #e2c770;
font-weight: bold;
}
.org-verbatim {
/* org-verbatim */
color: #b6e63e;
}
.rainbow-delimiters-depth-1 {
/* rainbow-delimiters-depth-1-face */
color: #fb2874;
}
.rainbow-delimiters-depth-2 {
/* rainbow-delimiters-depth-2-face */
color: #fd971f;
}
.rainbow-delimiters-depth-3 {
/* rainbow-delimiters-depth-3-face */
color: #b6e63e;
}
.rainbow-delimiters-depth-4 {
/* rainbow-delimiters-depth-4-face */
color: #66d9ef;
}
.rainbow-delimiters-depth-5 {
/* rainbow-delimiters-depth-5-face */
color: #fb2874;
}
.rainbow-delimiters-depth-6 {
/* rainbow-delimiters-depth-6-face */
color: #fd971f;
}
.rainbow-delimiters-depth-7 {
/* rainbow-delimiters-depth-7-face */
color: #b6e63e;
}
.rainbow-delimiters-depth-8 {
/* rainbow-delimiters-depth-8-face */
color: #b6e63e;
}
.rainbow-delimiters-depth-9 {
/* rainbow-delimiters-depth-9-face */
color: #9c91e4;
}
.string {
/* font-lock-string-face */
color: #e2c770;
}
.type {
/* font-lock-type-face */
color: #66d9ef;
}
.variable-name {
/* font-lock-variable-name-face */
color: #fd971f;
}
.warning {
/* font-lock-warning-face */
color: #e2c770;
}
a:hover {
text-decoration: underline;
}

View File

@ -1,28 +0,0 @@
#!/bin/bash
if [[ $# == 0 ]] || [[ ! -d "$1" ]] || [[ ! -f "./chronometrist.el" ]]; then
printf "Usage: scratch-test DASH-PATH [TIMECLOCK-FILE]\n"
printf "Please run this script from the chronometrist directory.\n"
else
dashdir="$1"
if [[ $# == 2 ]]; then
if [[ -f "$2" ]]; then
emacs -q -L "$(pwd)" \
-L "$dashdir" \
--eval "(progn
(require 'chronometrist)
(setq timeclock-file \"$2\")
(chronometrist))";
else
printf "Invalid timeclock file - $2"
fi
else
mv -v ~/.emacs.d/timelog ~/.emacs.d/timelog.old &&
emacs -q -L "$(pwd)" \
-L "$dashdir" \
--eval "(progn
(require 'chronometrist)
(chronometrist))" ;
mv -v ~/.emacs.d/timelog.old ~/.emacs.d/timelog
fi
fi

View File

@ -1,14 +0,0 @@
i 2018/01/01 00:00:00 Programming
o 2018/01/01 01:00:00
i 2018/01/01 02:00:00 Swimming
o 2018/01/01 03:00:00
i 2018/01/01 04:00:00 Cooking
o 2018/01/01 05:00:00
i 2018/01/01 06:00:00 Guitar
o 2018/01/01 07:00:00
i 2018/01/01 08:00:00 Cycling
o 2018/01/01 09:00:00
i 2018/01/02 23:00:00 Programming
o 2018/01/03 01:00:00
i 2018/01/04 00:00:00 Programming
o 2018/01/04 00:00:01

View File

@ -1 +0,0 @@
i 2018/01/01 23:00:00 Test

View File

@ -1,6 +1,6 @@
;;; -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'chronometrist-common)
(require 'chronometrist)
(describe
"chronometrist-format-time"

View File

@ -1,14 +1,20 @@
;; -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'chronometrist-plist-pp)
(require 'chronometrist)
(describe "chronometrist-plist-pp-buffer"
:var ((buffer (find-file-noselect "tests/plist-pp-test.sexp")))
(it "indents plists nicely"
(expect
(with-current-buffer buffer
(chronometrist-plist-pp-buffer)
(buffer-substring (point-min) (point-max)))
(chronometrist-plist-pp-to-string
'(:name "Task"
:tags (foo bar)
:comment ((70 . "baz")
"zot"
(16 . "frob")
(20 20 "quux"))
:start "2020-06-25T19:27:57+0530"
:stop "2020-06-25T19:43:30+0530"))
:to-equal
(concat
"(:name \"Task\"\n"
@ -18,7 +24,81 @@
" (16 . \"frob\")\n"
" (20 20 \"quux\"))\n"
" :start \"2020-06-25T19:27:57+0530\"\n"
" :stop \"2020-06-25T19:43:30+0530\")\n"))))
" :stop \"2020-06-25T19:43:30+0530\")\n"))
(expect
(chronometrist-plist-pp-to-string
'(:name "Singing"
:tags (classical solo)
:piece ((:composer "Gioachino Rossini"
:name "Il barbiere di Siviglia"
:aria ("All'idea di quel metallo" "Dunque io son"))
(:composer "Ralph Vaughan Williams"
:name "Songs of Travel"
:movement ((4 . "Youth and Love")
(5 . "In Dreams")
(7 . "Wither Must I Wander?")))
(:composer "Ralph Vaughan Williams"
:name "Merciless Beauty"
:movement 1)
(:composer "Franz Schubert"
:name "Winterreise"
:movement ((1 . "Gute Nacht")
(2 . "Die Wetterfahne")
(4 . "Erstarrung"))))
:start "2020-11-01T12:01:20+0530"
:stop "2020-11-01T13:08:32+0530"))
:to-equal
(concat
"(:name \"Singing\"\n"
" :tags (classical solo)\n"
" :piece ((:composer \"Gioachino Rossini\"\n"
" :name \"Il barbiere di Siviglia\"\n"
" :aria (\"All'idea di quel metallo\" \"Dunque io son\"))\n"
" (:composer \"Ralph Vaughan Williams\"\n"
" :name \"Songs of Travel\"\n"
" :movement ((4 . \"Youth and Love\")\n"
" (5 . \"In Dreams\")\n"
" (7 . \"Wither Must I Wander?\")))\n"
" (:composer \"Ralph Vaughan Williams\"\n"
" :name \"Merciless Beauty\"\n"
" :movement 1)\n"
" (:composer \"Franz Schubert\"\n"
" :name \"Winterreise\"\n"
" :movement ((1 . \"Gute Nacht\")\n"
" (2 . \"Die Wetterfahne\")\n"
" (4 . \"Erstarrung\"))))\n"
" :start \"2020-11-01T12:01:20+0530\"\n"
" :stop \"2020-11-01T13:08:32+0530\")"))
(expect
(chronometrist-plist-pp-to-string
'(:name "Cooking"
:tags (lunch)
:recipe (:name "moong-masoor ki dal"
:url "https://www.mirchitales.com/moong-masoor-dal-red-and-yellow-lentil-curry/")
:start "2020-09-23T15:22:39+0530"
:stop "2020-09-23T16:29:49+0530"))
:to-equal
(concat
"(:name \"Cooking\""
" :tags (lunch)"
" :recipe (:name \"moong-masoor ki dal\""
" :url \"https://www.mirchitales.com/moong-masoor-dal-red-and-yellow-lentil-curry/\")"
" :start \"2020-09-23T15:22:39+0530\""
" :stop \"2020-09-23T16:29:49+0530\")"))
(expect
(chronometrist-plist-pp-to-string
'(:name "Exercise"
:tags (warm-up)
:start "2018-11-21T15:35:04+0530"
:stop "2018-11-21T15:38:41+0530"
:comment ("stretching" (25 10 "push-ups"))))
:to-equal
(concat
"(:name \"Exercise\"\n"
" :tags (warm-up)\n"
" :start \"2018-11-21T15:35:04+0530\"\n"
" :stop \"2018-11-21T15:38:41+0530\"\n"
" :comment (\"stretching\" (25 10 \"push-ups\")))"))))
;; Local Variables:
;; nameless-current-name: "chronometrist"

View File

@ -1,11 +1,7 @@
;; -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'ts)
(require 'chronometrist-sexp)
(require 'chronometrist-events)
(require 'chronometrist-queries)
(require 'chronometrist-time)
(require 'chronometrist)
(describe "chronometrist-task-time-one-day"
:var ((ts-1 (chronometrist-iso-date->ts "2018-01-01"))

View File

@ -0,0 +1,488 @@
#+PROPERTY: header-args :tangle yes :load yes :comments link
#+BEGIN_SRC emacs-lisp :load no :tangle no
(setq nameless-current-name "chronometrist")
#+END_SRC
* Setup
** test-file-path-stem
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-test-file-path-stem
(format "%stest" (file-name-directory (or load-file-name default-directory))))
#+END_SRC
** test-backends
=chronometrist-test-backend= is just here till I finish writing the tests for a single backend. Will tackle how to apply the same tests to different backends afterwards - possibly with a [[#chronometrist-ert-deftest][wrapper macro]] around =ert-deftest=.
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-test-backend
(make-instance 'chronometrist-plist-group-backend :path chronometrist-test-file-path-stem))
(defun chronometrist-make-test-backends ()
(cl-loop for backend in '(chronometrist-plist-backend chronometrist-plist-group-backend)
collect (make-instance backend :path chronometrist-test-file-path-stem)))
(defvar chronometrist-test-backends (chronometrist-make-test-backends))
#+END_SRC
** test-first-record
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-test-first-record
'(:name "Programming"
:start "2018-01-01T00:00:00+0530"
:stop "2018-01-01T01:00:00+0530"))
#+END_SRC
** test-latest-record
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-test-latest-record
'(:name "Programming"
:tags (reading)
:book "Smalltalk-80: The Language and Its Implementation"
:start "2020-05-10T16:33:17+0530"
:stop "2020-05-10T17:10:48+0530"))
#+END_SRC
** test-records
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-test-records
'((:name "Programming"
:start "2018-01-01T00:00:00+0530"
:stop "2018-01-01T01:00:00+0530")
(:name "Swimming"
:start "2018-01-01T02:00:00+0530"
:stop "2018-01-01T03:00:00+0530")
(:name "Cooking"
:start "2018-01-01T04:00:00+0530"
:stop "2018-01-01T05:00:00+0530")
(:name "Guitar"
:start "2018-01-01T06:00:00+0530"
:stop "2018-01-01T07:00:00+0530")
(:name "Cycling"
:start "2018-01-01T08:00:00+0530"
:stop "2018-01-01T09:00:00+0530")
(:name "Programming"
:start "2018-01-02T23:00:00+0530"
:stop "2018-01-03T01:00:00+0530")
(:name "Cooking"
:start "2018-01-03T23:00:00+0530"
:stop "2018-01-04T01:00:00+0530")
(:name "Programming"
:tags (bug-hunting)
:project "Chronometrist"
:component "goals"
:start "2020-05-09T20:03:25+0530"
:stop "2020-05-09T20:05:55+0530")
(:name "Arrangement/new edition"
:tags (new edition)
:song "Songs of Travel"
:composer "Vaughan Williams, Ralph"
:start "2020-05-10T00:04:14+0530"
:stop "2020-05-10T00:25:48+0530")
(:name "Guitar"
:tags (classical warm-up)
:start "2020-05-10T15:41:14+0530"
:stop "2020-05-10T15:55:42+0530")
(:name "Guitar"
:tags (classical solo)
:start "2020-05-10T16:00:00+0530"
:stop "2020-05-10T16:30:00+0530")
(:name "Programming"
:tags (reading)
:book "Smalltalk-80: The Language and Its Implementation"
:start "2020-05-10T16:33:17+0530"
:stop "2020-05-10T17:10:48+0530")))
#+END_SRC
** cleanup
#+BEGIN_SRC emacs-lisp
(cl-defgeneric chronometrist-backend-test-cleanup (backend)
"Delete any files created by BACKEND during testing.")
(cl-defmethod chronometrist-backend-test-cleanup ((backend chronometrist-elisp-sexp-backend))
(with-slots (file hash-table) backend
(when file
(when (file-exists-p file)
(delete-file file))
(with-current-buffer (get-file-buffer file)
;; (erase-buffer)
;; Unsuccessful attempt to inhibit "Delete excess backup versions of <file>?" prompts
(defvar delete-old-versions)
(let ((delete-old-versions t))
(set-buffer-modified-p nil)
(kill-buffer))))
(setf hash-table (clrhash hash-table))))
(cl-defmethod chronometrist-backend-test-cleanup :after ((backend t))
(setf chronometrist-test-backends (chronometrist-make-test-backends)))
#+END_SRC
** ert-deftest :macro:
:PROPERTIES:
:CUSTOM_ID: chronometrist-ert-deftest
:END:
#+BEGIN_SRC emacs-lisp
(defmacro chronometrist-ert-deftest (name backend-var &rest test-forms)
"Generate test groups containing TEST-FORMS for each backend.
BACKEND-VAR is bound to each backend in
`chronometrist-test-backends'. TEST-FORMS are passed to
`ert-deftest'."
(declare (indent defun) (debug t))
(cl-loop for backend in chronometrist-test-backends collect
(let* ((backend-name (string-remove-suffix
"-backend"
(string-remove-prefix "chronometrist"
(symbol-name
(eieio-object-class-name backend)))))
(test-name (concat "chronometrist-" (symbol-name name) backend-name)))
`(ert-deftest ,(intern test-name) ()
(let ((,backend-var ,backend))
(unwind-protect
(progn ,@test-forms)
;; cleanup - remove test backend file
(chronometrist-backend-test-cleanup ,backend)))))
into test-groups
finally return (cons 'progn test-groups)))
#+END_SRC
* Tests
** common
*** current-task
:PROPERTIES:
:CUSTOM_ID: tests-common-current-task
:END:
#+BEGIN_SRC emacs-lisp
(chronometrist-ert-deftest current-task b
;; (message "current-task test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
(chronometrist-create-file b)
(should (not (chronometrist-current-task b)))
(chronometrist-insert b (list :name "Test" :start (chronometrist-format-time-iso8601)))
(should (equal "Test" (chronometrist-current-task b)))
(chronometrist-remove-last b)
(should (not (chronometrist-current-task b))))
#+END_SRC
*** plist-p
:PROPERTIES:
:CUSTOM_ID: tests-common-plist-p
:END:
#+BEGIN_SRC emacs-lisp
(ert-deftest chronometrist-plist-p ()
(should (eq t (chronometrist-plist-p '(:a 1 :b 2))))
(should (eq nil (chronometrist-plist-p '(0 :a 1 :b 2))))
(should (eq nil (chronometrist-plist-p '(:a 1 :b 2 3))))
(should (not (chronometrist-plist-p nil))))
#+END_SRC
*** plists-split-p
:PROPERTIES:
:CUSTOM_ID: tests-common-plists-split-p
:END:
#+BEGIN_SRC emacs-lisp
(ert-deftest chronometrist-plists-split-p ()
(should
(chronometrist-plists-split-p
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-11-30T23:01:10+0530"
:stop "2021-12-01T00:00:00+0530")
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-12-01T00:00:00+0530"
:stop "2021-12-01T00:06:22+0530")))
;; without :stop
(should
(chronometrist-plists-split-p
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-11-30T23:01:10+0530"
:stop "2021-12-01T00:00:00+0530")
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-12-01T00:00:00+0530")))
;; difference in time
(should
(not (chronometrist-plists-split-p
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-11-30T23:01:10+0530"
:stop "2021-12-01T00:00:00+0530")
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-12-01T00:00:01+0530"
:stop "2021-12-01T00:06:22+0530"))))
;; difference in key-values
(should
(not (chronometrist-plists-split-p
'(:name "Cooking"
:recipe "whole wheat penne rigate in arrabbiata sauce"
:start "2021-11-30T23:01:10+0530"
:stop "2021-12-01T00:00:00+0530")
'(:name "Cooking"
:start "2021-12-01T00:00:00+0530"
:stop "2021-12-01T00:06:22+0530")))))
#+END_SRC
** data structures
*** list-tasks
#+BEGIN_SRC emacs-lisp
(chronometrist-ert-deftest list-tasks b
;; (message "list-tasks test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
(chronometrist-create-file b)
(let ((task-list (chronometrist-list-tasks b)))
(should (listp task-list))
(should (seq-every-p #'stringp task-list))))
#+END_SRC
** time functions
*** format-duration-long :pure:
#+BEGIN_SRC emacs-lisp
(ert-deftest chronometrist-format-duration-long ()
(should (equal (chronometrist-format-duration-long 5) ""))
(should (equal (chronometrist-format-duration-long 65) "1 minute"))
(should (equal (chronometrist-format-duration-long 125) "2 minutes"))
(should (equal (chronometrist-format-duration-long 3605) "1 hour"))
(should (equal (chronometrist-format-duration-long 3660) "1 hour, 1 minute"))
(should (equal (chronometrist-format-duration-long 3725) "1 hour, 2 minutes"))
(should (equal (chronometrist-format-duration-long 7200) "2 hours"))
(should (equal (chronometrist-format-duration-long 7260) "2 hours, 1 minute"))
(should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes")))
#+END_SRC
** plist pretty-printing
[[file:../elisp/chronometrist.org::#program-pretty-printer][source]]
*** plist-group-p
#+BEGIN_SRC emacs-lisp
(ert-deftest chronometrist-plist-group-p ()
(should (eq t (chronometrist-plist-group-p '(symbol (:a 1 :b 2)))))
(should (eq t (chronometrist-plist-group-p '("string" (:a 1 :b 2)))))
(should (not (chronometrist-plist-group-p nil)))
(should (not (chronometrist-plist-group-p '("string")))))
#+END_SRC
*** plist-pp-to-string
#+BEGIN_SRC emacs-lisp
(ert-deftest chronometrist-pp-to-string ()
(should
(equal
(chronometrist-pp-to-string
'(:name "Task"
:tags (foo bar)
:comment ((70 . "baz")
"zot"
(16 . "frob")
(20 20 "quux"))
:start "2020-06-25T19:27:57+0530"
:stop "2020-06-25T19:43:30+0530"))
(concat
"(:name \"Task\"\n"
" :tags (foo bar)\n"
" :comment ((70 . \"baz\")\n"
" \"zot\"\n"
" (16 . \"frob\")\n"
" (20 20 \"quux\"))\n"
" :start \"2020-06-25T19:27:57+0530\"\n"
" :stop \"2020-06-25T19:43:30+0530\")")))
(should
(equal
(chronometrist-pp-to-string
'(:name "Singing"
:tags (classical solo)
:piece ((:composer "Gioachino Rossini"
:name "Il barbiere di Siviglia"
:aria ("All'idea di quel metallo" "Dunque io son"))
(:composer "Ralph Vaughan Williams"
:name "Songs of Travel"
:movement ((4 . "Youth and Love")
(5 . "In Dreams")
(7 . "Wither Must I Wander?")))
(:composer "Ralph Vaughan Williams"
:name "Merciless Beauty"
:movement 1)
(:composer "Franz Schubert"
:name "Winterreise"
:movement ((1 . "Gute Nacht")
(2 . "Die Wetterfahne")
(4 . "Erstarrung"))))
:start "2020-11-01T12:01:20+0530"
:stop "2020-11-01T13:08:32+0530"))
(concat
"(:name \"Singing\"\n"
" :tags (classical solo)\n"
" :piece ((:composer \"Gioachino Rossini\"\n"
" :name \"Il barbiere di Siviglia\"\n"
" :aria (\"All'idea di quel metallo\" \"Dunque io son\"))\n"
" (:composer \"Ralph Vaughan Williams\"\n"
" :name \"Songs of Travel\"\n"
" :movement ((4 . \"Youth and Love\")\n"
" (5 . \"In Dreams\")\n"
" (7 . \"Wither Must I Wander?\")))\n"
" (:composer \"Ralph Vaughan Williams\"\n"
" :name \"Merciless Beauty\"\n"
" :movement 1)\n"
" (:composer \"Franz Schubert\"\n"
" :name \"Winterreise\"\n"
" :movement ((1 . \"Gute Nacht\")\n"
" (2 . \"Die Wetterfahne\")\n"
" (4 . \"Erstarrung\"))))\n"
" :start \"2020-11-01T12:01:20+0530\"\n"
" :stop \"2020-11-01T13:08:32+0530\")")))
(should (equal
(chronometrist-pp-to-string
'(:name "Cooking"
:tags (lunch)
:recipe (:name "moong-masoor ki dal"
:url "https://www.mirchitales.com/moong-masoor-dal-red-and-yellow-lentil-curry/")
:start "2020-09-23T15:22:39+0530"
:stop "2020-09-23T16:29:49+0530"))
(concat
"(:name \"Cooking\"\n"
" :tags (lunch)\n"
" :recipe (:name \"moong-masoor ki dal\"\n"
" :url \"https://www.mirchitales.com/moong-masoor-dal-red-and-yellow-lentil-curry/\")\n"
" :start \"2020-09-23T15:22:39+0530\"\n"
" :stop \"2020-09-23T16:29:49+0530\")")))
(should (equal
(chronometrist-pp-to-string
'(:name "Exercise"
:tags (warm-up)
:start "2018-11-21T15:35:04+0530"
:stop "2018-11-21T15:38:41+0530"
:comment ("stretching" (25 10 "push-ups"))))
(concat
"(:name \"Exercise\"\n"
" :tags (warm-up)\n"
" :start \"2018-11-21T15:35:04+0530\"\n"
" :stop \"2018-11-21T15:38:41+0530\"\n"
" :comment (\"stretching\" (25 10 \"push-ups\")))")))
(should (equal
(chronometrist-pp-to-string
'(:name "Guitar"
:tags (classical)
:warm-up ((right-hand-patterns "pima" "piam" "pmia" "pmai" "pami" "paim"))
:start "2021-09-28T17:49:18+0530"
:stop "2021-09-28T17:53:49+0530"))
(concat
"(:name \"Guitar\"\n"
" :tags (classical)\n"
" :warm-up ((right-hand-patterns \"pima\" \"piam\" \"pmia\" \"pmai\" \"pami\" \"paim\"))\n"
" :start \"2021-09-28T17:49:18+0530\"\n"
" :stop \"2021-09-28T17:53:49+0530\")")))
(should (equal
(chronometrist-pp-to-string
'(:name "Cooking"
:tags (lunch)
:recipe ("urad dhuli"
(:name "brown rice"
:brand "Dawat quick-cooking"
:quantity "40% of steel measuring glass"
:water "2× dry rice"))
:start "2021-11-07T14:40:45+0530"
:stop "2021-11-07T15:28:13+0530"))
(concat
"(:name \"Cooking\"\n"
" :tags (lunch)\n"
" :recipe (\"urad dhuli\"\n"
" (:name \"brown rice\"\n"
" :brand \"Dawat quick-cooking\"\n"
" :quantity \"40% of steel measuring glass\"\n"
" :water \"2× dry rice\"))\n"
" :start \"2021-11-07T14:40:45+0530\"\n"
" :stop \"2021-11-07T15:28:13+0530\")"))))
#+END_SRC
** backend
Situations
1. no file
2. empty file
3. non-empty file with no records
4. single record
* active
* inactive
* active, day-crossing
* inactive, day-crossing
5. multiple records
6. +[plist-group] latest plist is split+ (covered in #4)
Tests to be added -
1. to-hash-table
2. to-file
The order of these tests is important - the last test for each case is one which moves into the next case.
*** create-file
:PROPERTIES:
:CUSTOM_ID: tests-backend-create-file
:END:
#+BEGIN_SRC emacs-lisp
(chronometrist-ert-deftest create-file b
;; (message "create-file test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
;; * file does not exist *
(should (chronometrist-create-file b))
;; * file exists but has no records *
(should (not (chronometrist-create-file b))))
#+END_SRC
*** latest-date-records
#+BEGIN_SRC emacs-lisp
(chronometrist-ert-deftest latest-date-records b
;; (message "latest-date-records test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
(let ((plist-1 (cl-first chronometrist-test-records))
(plist-2 (cl-second chronometrist-test-records))
(today-ts (chronometrist-date-ts)))
;; * file does not exist *
(should-error (chronometrist-latest-date-records b))
(should (chronometrist-create-file b))
;; (message "latest-date-records: %S" (chronometrist-latest-date-records b))
;; * file exists but has no records *
(should (not (chronometrist-latest-date-records b)))
(should (chronometrist-insert b plist-1))
;; * backend has a single active record *
;; * backend has a single inactive record *
;; * backend has a single active day-crossing record *
;; * backend has a single inactive day-crossing record *
;; * backend has two records and one is active *
;; * backend has two records and both are inactive *
;; * backend has two day-crossing records and one is active *
;; * backend has two day-crossing records and both are inactive *
))
#+END_SRC
*** insert
#+BEGIN_SRC emacs-lisp
(chronometrist-ert-deftest insert b
;; (message "insert test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
(let* ((plist1 (list :name "Test" :start (chronometrist-format-time-iso8601)))
(plist2 (append plist1 (list :stop (chronometrist-format-time-iso8601)))))
;; * file does not exist *
(should-error (chronometrist-insert b plist1))
(should (chronometrist-create-file b))
;; * file exists but has no records *
(should (chronometrist-insert b plist1))
(should (equal (progn (chronometrist-reset-backend b)
(chronometrist-latest-date-records b))
(list (chronometrist-date-iso) plist1)))
;; * backend has a single active record *
(should (chronometrist-replace-last b plist2))
(should (equal (progn (chronometrist-reset-backend b)
(chronometrist-latest-date-records b))
(list (chronometrist-date-iso) plist2)))
;; * backend has a single inactive record *
;; * backend has a single active day-crossing record *
;; * backend has a single inactive day-crossing record *
;; * backend has two records and one is active *
;; * backend has two records and both are inactive *
;; * backend has two day-crossing records and one is active *
;; * backend has two day-crossing records and both are inactive *
))
#+END_SRC
* Local Variables
# Local Variables:
# delete-old-versions: t
# End:

View File

@ -1 +0,0 @@
(:name "Task" :tags (foo bar) :comment ((70 . "baz") "zot" (16 . "frob") (20 20 "quux")) :start "2020-06-25T19:27:57+0530" :stop "2020-06-25T19:43:30+0530")