Merge branch 'dev'
This commit is contained in:
commit
125d0ed289
10
Cask
10
Cask
|
@ -7,11 +7,11 @@
|
|||
"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 "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")
|
||||
|
||||
(files "elisp/*.el")
|
||||
|
||||
|
|
25
TODO.org
25
TODO.org
|
@ -114,7 +114,7 @@ Some options and ideas -
|
|||
* Per-task history generation will create problems - e.g. values for a given key for one task won't be suggested for values for the same key in another 🤦
|
||||
+ Tags and keys are already task-sensitive; just don't make values task-sensitive.
|
||||
2. [X] Compare partial hashes of file to know what has changed - only update memory when necessary.
|
||||
3. [ ] In-memory cache - don't store entire file into memory; instead, split midnight-spanning events just for the requested data.
|
||||
3. [ ] In-memory cache - don't store entire file into memory; instead, split midnight-spanning intervals just for the requested data.
|
||||
* Will increase load time for the first time =chronometrist=, =chronometrist-report=, or =chronometrist-statistics= are run (including forward/backward commands in the latter two)
|
||||
+ Can try pre-emptively loading data for the latter two
|
||||
* Will reduce memory used by =chronometrist-events=.
|
||||
|
@ -128,21 +128,21 @@ Some options and ideas -
|
|||
* Will greatly impede human editing of the file, too. 🤔
|
||||
+ An editing UI could help - pretty sure every timestamp edit I've ever made has been for the last interval, or at most an interval in today's data.
|
||||
** Cache
|
||||
+ Lessons from the parsimonious-reading branch - iterating =read= over the whole file is fast; splitting the events is not.
|
||||
+ Lessons from the parsimonious-reading branch - iterating =read= over the whole file is fast; splitting the intervals is not.
|
||||
+ Things we need to read the whole file for - task list, tag/key/value history.
|
||||
+ Fill =chronometrist-events= only as much as the buffer needing split events requires. e.g. for =chronometrist=, just a day; for =chronometrist-report=, a week; etc.
|
||||
+ Anything requiring split events will first look in =chronometrist-events=, and if not found, will read from the file and update =chronometrist-events=.
|
||||
+ Fill =chronometrist-events= only as much as the buffer needing split intervals requires. e.g. for =chronometrist=, just a day; for =chronometrist-report=, a week; etc.
|
||||
+ Anything requiring split intervals will first look in =chronometrist-events=, and if not found, will read from the file and update =chronometrist-events=.
|
||||
+ When the file changes, use the file byte length and hash strategy described below to know whether to keep the cache.
|
||||
+ Save cache to a file, so that event splitting is avoided by reading from that.
|
||||
** Ideas to make -refresh-file faster
|
||||
1. Support multiple files, so we read and process lesser data when one of them changes.
|
||||
2. Make file writing async
|
||||
3. Don't refresh from file when clocking in.
|
||||
4. Only write to the file when Emacs is idle or being killed, and store data in memory (in the events hash table) in the meantime
|
||||
4. Only write to the file when Emacs is idle or being killed, and store data in memory (in the intervals hash table) in the meantime
|
||||
5. What if commands both write to the file /and/ add to the hash table, so we don't have to re-read the file and re-populate the table for commands? The expensive reading+parsing could be avoided for commands, and only take place for the user changing the file.
|
||||
* [X] jonasw - store length and hash of previous file, see if the new file has the same hash until old-length bytes.
|
||||
* Rather than storing and hashing the full length, we could do it until (before) the last s-expression (or last N s-expressions?). That way, we know if the last expression (or last N expressions) have changed.
|
||||
* Or even the first expression of the current date. That way, we just re-read the events for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
|
||||
* Or even the first expression of the current date. That way, we just re-read the intervals for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
|
||||
6. [ ] Don't generate tag/keyword/value history from the entire log, just from the last N days (where N is user-customizable).
|
||||
7. [ ] Just why are we reading the whole file? ~chronometrist~ should not read more than a day; ~chronometrist-report~ should not read more than a week at a time, and so on. Make a branch which works on this logic, see if it is faster.
|
||||
|
||||
|
@ -193,7 +193,7 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
|||
:start "2020-12-22T23:01:00+0530"
|
||||
:stop "2020-12-23T00:54:52+0530")
|
||||
#+END_SRC
|
||||
...is displayed as 1:53:52 after clocking out. :\
|
||||
...is displayed as 1:53:52 (rather than 00:54:52) after clocking out. :\
|
||||
|
||||
** chronometrist [8%]
|
||||
1. [ ] Add =:stop= time when we call =chronometrist-kv-accept=, not when we quit the key-value prompt.
|
||||
|
@ -219,7 +219,7 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
|||
=chronometrist-kv-add= could quit nonlocally when the user enters a blank input (or hits C-g? Maybe by using =unwind-protect=?), cancelling the clock in/out, and thereby letting =chronometrist-kv-accept= resume clock in/out. (It can determine whether to clock in or out using =chronometrist-current-task=)
|
||||
2. [X] Enhanced tag/key-value prompt - before asking for tags/key-values, if the last occurence of task had tags/key-values, ask if they should be reused. y - yes, n - no (continue to usual prompts).
|
||||
* [X] Show what those tags/key-values are, so the user knows what will be added.
|
||||
3. [ ] Expandable items - show intervals for task today
|
||||
3. [ ] "Explain" command - show intervals for task today
|
||||
* [ ] Switch between intervals and tag-combination breakdown
|
||||
4. [ ] Magit/other VCS integration
|
||||
* [ ] Add support for using key-values to point to a commit (commit hash + repo path?)
|
||||
|
@ -236,7 +236,7 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
|||
2. [ ] Highlight column of current day
|
||||
3. [ ] Command to narrow report to specific project(s)
|
||||
4. [ ] Jump to beginning/end of data (keys B/F)
|
||||
5. [ ] Expandable items - show tag-combination-based breakdown
|
||||
5. [ ] "Explain" command - show tag-combination-based breakdown
|
||||
|
||||
** Code quality [25%]
|
||||
1. [ ] Remove duplication between =chronometrist-toggle-task= and =chronometrist-toggle-task-button=
|
||||
|
@ -274,7 +274,7 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
|||
: ‘file-notify-add-watch’ ((describe-function 'file-notify-add-watch))
|
||||
...yuck :\
|
||||
* Currently using file: links with text search - =[file:../elisp/file.el::defun identifier (]=, =[file:../elisp/file.el::defvar identifier (]=, etc.
|
||||
4. [ ] Fix heading link to "midnight-spanning events" - jumps to the correct heading in HTML export, but jumps to its own self in Org mode.
|
||||
4. [ ] Fix heading link to "midnight-spanning intervals" - jumps to the correct heading in HTML export, but jumps to its own self in Org mode.
|
||||
5. [ ] Figure out some way to hide package prefixes in identifiers in Org mode (without actually affecting the contents, a la nameless-mode)
|
||||
|
||||
** UX [30%]
|
||||
|
@ -317,7 +317,10 @@ ppp.el doesn't align plist values along the same column. It's also GPL, and I'm
|
|||
2. a list, with the first element being the task name as a string, followed by keyword-value pairs
|
||||
|
||||
Keywords can be
|
||||
1. =:tag-prompt=, =:key-prompt= - values can be nil, t (the default), or a function. If nil, don't ask for tags/keys for this task. If t, ask for tags/keys for this task using =chronometrist-tags-add=/=chronometrist-key-add=. If it's a function, use that as the prompt.
|
||||
1. +=:tag-prompt=, =:key-prompt= - values can be nil, t (the default), or a function. If nil, don't ask for tags/keys for this task. If t, ask for tags/keys for this task using =chronometrist-tags-add=/=chronometrist-key-add=. If it's a function, use that as the prompt.+
|
||||
* Tags and key-values are optional extensions; we don't want Chronometrist to know about them.
|
||||
+ Well, even with this style of configuration, Chronometrist doesn't necessarily have to...it could use the fields it knows about, ignoring the rest; the extensions could check for the fields they know about.
|
||||
* Instead of setting the prompt function, set hooks (=chronometrist-before-in-functions=/=chronometrist-after-in-functions=/=chronometrist-before-out-functions=/=chronometrist-after-out-functions=) per-task. This is preferable, because if you define a custom prompt function, you probably also want to remove certain functions coming earlier in the hook, such as =chronometrist-skip-query-prompt=, for that task.
|
||||
2. =:hide= - values can be nil (the default) or t - if t, hide this task from being displayed in =chronometrist=/=chronometrist-report=/=chronometrist-statistics= buffers. (effectively a non-destructive deletion of all intervals of the task)
|
||||
...
|
||||
|
||||
|
|
|
@ -727,8 +727,6 @@ Function - chronometrist-events-maybe-split (event)
|
|||
@item
|
||||
Function - chronometrist-events-populate ()
|
||||
@item
|
||||
Function - chronometrist-tasks-from-table ()
|
||||
@item
|
||||
Function - chronometrist-events-add (plist)
|
||||
@item
|
||||
Function - chronometrist-events-replace-last (plist)
|
||||
|
@ -791,7 +789,7 @@ Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
|
|||
String &optional ts -> seconds
|
||||
@end itemize
|
||||
@item
|
||||
Function - chronometrist-active-time-one-day (&optional ts)
|
||||
Function - chronometrist-active-time-one-day (&optional (ts (ts-now)))
|
||||
@itemize
|
||||
@item
|
||||
&optional ts -> seconds
|
||||
|
@ -799,7 +797,7 @@ Function - chronometrist-active-time-one-day (&optional ts)
|
|||
@item
|
||||
Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
||||
@item
|
||||
Function - chronometrist-task-events-in-day (task ts)
|
||||
Function - chronometrist-task-events-in-day (task &optional (ts (ts-now)))
|
||||
@end enumerate
|
||||
|
||||
@node chronometrist-report-customel
|
||||
|
|
|
@ -293,19 +293,18 @@ Each of these has a corresponding function to clear it and fill it with values -
|
|||
38. 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
|
||||
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-events-add (plist)
|
||||
7. Function - chronometrist-events-replace-last (plist)
|
||||
8. Function - chronometrist-events-subset (start end)
|
||||
* ts ts -> hash-table
|
||||
|
||||
** chronometrist-migrate.el
|
||||
1. Variable - chronometrist-migrate-table
|
||||
|
@ -327,10 +326,10 @@ Each of these has a corresponding function to clear it and fill it with values -
|
|||
* -> 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)
|
||||
3. Function - chronometrist-active-time-one-day (&optional (ts (ts-now)))
|
||||
* &optional ts -> seconds
|
||||
4. Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
|
||||
5. Function - chronometrist-task-events-in-day (task ts)
|
||||
5. Function - chronometrist-task-events-in-day (task &optional (ts (ts-now)))
|
||||
|
||||
** chronometrist-report-custom.el
|
||||
1. Custom variable - chronometrist-report-buffer-name
|
||||
|
|
|
@ -41,7 +41,7 @@ This is distinct from `chronometrist-time-re-file' (which see) -
|
|||
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'.")
|
||||
"List of tasks in `chronometrist-file'.")
|
||||
|
||||
(defvar chronometrist--fs-watch nil
|
||||
"Filesystem watch object.
|
||||
|
|
|
@ -127,14 +127,6 @@ were none."
|
|||
(clrhash chronometrist-events)
|
||||
(chronometrist-sexp-events-populate))
|
||||
|
||||
(defun chronometrist-tasks-from-table ()
|
||||
"Return a list of task names from `chronometrist-events'."
|
||||
(cl-loop for plists being the hash-values of chronometrist-events append
|
||||
(cl-loop for plist in plists collect (plist-get plist :name)) into list
|
||||
finally
|
||||
(cl-return
|
||||
(cl-remove-duplicates (sort list #'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"))
|
||||
|
|
|
@ -40,6 +40,14 @@ considers it an alist."
|
|||
(when (listp list)
|
||||
(cl-loop for elt in list thereis (chronometrist-plist-pp-pair-p elt))))
|
||||
|
||||
(defun chronometrist-plist-pp-plist-p (list)
|
||||
(while (consp list)
|
||||
(setq list (if (and (keywordp (car list))
|
||||
(consp (cdr list)))
|
||||
(cddr list)
|
||||
'not-plist)))
|
||||
(null list))
|
||||
|
||||
(defun chronometrist-plist-pp-longest-keyword-length ()
|
||||
"Find the length of the longest keyword in a plist.
|
||||
This assumes there is a single plist in the current buffer, and
|
||||
|
@ -62,7 +70,7 @@ The list must be on a single line, as emitted by `prin1'."
|
|||
(progn
|
||||
(setq sexp (save-excursion (read (current-buffer))))
|
||||
(cond
|
||||
((json-plist-p sexp)
|
||||
((chronometrist-plist-pp-plist-p sexp)
|
||||
(chronometrist-plist-pp-buffer-plist inside-sublist-p)
|
||||
(chronometrist-plist-pp-buffer inside-sublist-p))
|
||||
((chronometrist-plist-pp-alist-p sexp)
|
||||
|
@ -104,7 +112,7 @@ The list must be on a single line, as emitted by `prin1'."
|
|||
(make-string left-indent ?\ ))
|
||||
(chronometrist-plist-pp-indent-sexp sexp right-indent)))
|
||||
;; not a keyword = a value
|
||||
((json-plist-p sexp)
|
||||
((chronometrist-plist-pp-plist-p sexp)
|
||||
(chronometrist-plist-pp-buffer-plist))
|
||||
((and (listp sexp)
|
||||
(not (chronometrist-plist-pp-pair-p sexp)))
|
||||
|
|
|
@ -40,7 +40,7 @@ The return value is seconds, as an integer."
|
|||
;; no events for this task on TS, i.e. no time spent
|
||||
0)))
|
||||
|
||||
(defun chronometrist-active-time-one-day (&optional ts)
|
||||
(cl-defun chronometrist-active-time-one-day (&optional (ts (ts-now)))
|
||||
"Return the total active time on TS (if non-nil) or today.
|
||||
TS must be a ts struct (see `ts.el')
|
||||
|
||||
|
@ -65,7 +65,7 @@ which span midnights. (see `chronometrist-events-clean')"
|
|||
table)
|
||||
count))
|
||||
|
||||
(defun chronometrist-task-events-in-day (task ts)
|
||||
(cl-defun chronometrist-task-events-in-day (task &optional (ts (ts-now)))
|
||||
"Get events for TASK on TS.
|
||||
TS should be a ts struct (see `ts.el').
|
||||
|
||||
|
|
|
@ -23,7 +23,9 @@ STREAM (which is the value of `current-buffer')."
|
|||
(defmacro chronometrist-loop-file (for expr in file &rest loop-clauses)
|
||||
"`cl-loop' LOOP-CLAUSES over s-expressions in FILE.
|
||||
VAR is bound to each s-expression."
|
||||
(declare (indent defun))
|
||||
(declare (indent defun)
|
||||
;; FIXME
|
||||
(debug ("for" form "in" form &rest &or sexp form)))
|
||||
`(chronometrist-sexp-in-file ,file
|
||||
(goto-char (point-max))
|
||||
(cl-loop with ,expr
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
;; -*- lexical-binding: t; -*-
|
||||
(require 'chronometrist-plist-pp)
|
||||
|
||||
(ert-deftest plist-p ()
|
||||
(should (eq t (chronometrist-plist-pp-plist-p '(:a 1 :b 2))))
|
||||
(should (eq nil (chronometrist-plist-pp-plist-p '(0 :a 1 :b 2))))
|
||||
(should (eq nil (chronometrist-plist-pp-plist-p '(:a 1 :b 2 3)))))
|
||||
|
||||
(ert-deftest plist-pp-buffer ()
|
||||
(should
|
||||
(equal
|
||||
|
@ -96,5 +101,5 @@
|
|||
" :comment (\"stretching\" (25 10 \"push-ups\")))"))))
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist"
|
||||
;; nameless-current-name: "chronometrist-plist-pp"
|
||||
;; End:
|
||||
|
|
Reference in New Issue