Merge branch 'dev'
This commit is contained in:
commit
faeb0a2040
|
@ -1,9 +1,11 @@
|
|||
;;; Directory Local Variables
|
||||
;;; For more information see (info "(emacs) Directory Variables")
|
||||
|
||||
((emacs-lisp-mode
|
||||
(nameless-aliases
|
||||
("c" . "chronometrist")
|
||||
("cr" . "chronometrist-report")
|
||||
("cs" . "chronometrist-statistics")
|
||||
("cd" . "chronometrist-diary"))))
|
||||
((emacs-lisp-mode . ((nameless-aliases . (("c" . "chronometrist")
|
||||
("cr" . "chronometrist-report")
|
||||
("cs" . "chronometrist-statistics")
|
||||
("cd" . "chronometrist-diary")
|
||||
("cx" . "chronometrist-sexp")))
|
||||
(outline-regexp . ";;;+ ")))
|
||||
(dired-mode . ((dired-omit-mode . t)
|
||||
(dired-omit-extensions . (".html" ".texi")))))
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -4,6 +4,17 @@ 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).
|
||||
|
||||
## [0.5.0] 2020-06-30
|
||||
### Added
|
||||
* Support for time goals via optional package `chronometrist-targets`.
|
||||
* New hook - `chronometrist-file-change-hook`
|
||||
### Changed
|
||||
* Use [ts.el](https://github.com/alphapapa/ts.el) structs to represent date-time, wherever possible. (`chronometrist-events` and `chronometrist-file` being notable exceptions)
|
||||
### Fixed
|
||||
* Prefix arguments now work with the point on a button, too.
|
||||
* Bug with missing entries in `chronometrist-key-history`
|
||||
* Operations for adding a new s-expression and replacing the last s-expression have been optimized. Notably, commands for clocking in/out are now significantly faster.
|
||||
|
||||
## [0.4.4]
|
||||
### Fixed
|
||||
* Error when adding a task for the first time.
|
||||
|
|
4
Cask
4
Cask
|
@ -8,6 +8,10 @@
|
|||
|
||||
(depends-on "dash" "2.16.0")
|
||||
(depends-on "cl")
|
||||
(depends-on "s" "1.12.0")
|
||||
(depends-on "ts" "0.2")
|
||||
|
||||
(files "elisp/*.el")
|
||||
|
||||
(development
|
||||
(depends-on "f")
|
||||
|
|
|
@ -181,7 +181,7 @@ Chronometrist is released under your choice of [Unlicense](https://unlicense.org
|
|||
(See files [LICENSE](LICENSE) and [LICENSE.1](LICENSE.1)).
|
||||
|
||||
## Thanks
|
||||
wasamasa, bpalmer and #emacs for all their help and support
|
||||
wasamasa, bpalmer, aidalgol, and the rest of #emacs for their tireless help and support
|
||||
|
||||
jwiegley for timeclock.el, which we used as a backend in earlier versions
|
||||
|
||||
|
|
77
TODO.org
77
TODO.org
|
@ -37,10 +37,6 @@
|
|||
5. [ ] Expandable items - show intervals for task today
|
||||
* [ ] Switch between intervals and tag-combination breakdown
|
||||
6. Revisit 'no reason' commands - maybe we should ask for tags and key-values with the regular commands, and skip them with the 'no reason' variants?
|
||||
7. Time targets/goals
|
||||
* User defines targets for tasks
|
||||
* Chronometrist displays targets column
|
||||
* Remind user when about to approach target (e.g. -5 minutes)
|
||||
*** Key-values [66%]
|
||||
1. [X] bug - value-history appears in chronological rather than reverse chronological order
|
||||
2. [X] generate history hash table from chronometrist-file.
|
||||
|
@ -81,12 +77,14 @@
|
|||
Observations
|
||||
* This means you can't enter symbols via prompt. Can be added if the demand is there...
|
||||
* This also means you can't have multiple atom values for a keyword...but that's irrelevant, because plists can't have multiple values anyway. :)
|
||||
|
||||
*** Tags [100%]
|
||||
1. [X] generate history from chronometrist-file
|
||||
* [X] narrow it down to the :name
|
||||
2. [X] write tags to last expression
|
||||
3. [X] show task name in prompt
|
||||
4. [X] bug - tags being added twice
|
||||
|
||||
** UX
|
||||
1. don't suggest nil when asking for first project on first run
|
||||
2. when starting a project with time of "-" (i.e. not worked on today until now), immediately set time to 0 instead of waiting for the first timer refresh
|
||||
|
@ -95,34 +93,51 @@
|
|||
5. mouse-3 should clock-out without asking for reason.
|
||||
6. Some way to ask for the reason just before starting a project. Even when clocking out, the reason is asked /before/ clocking out, which adds time to the project.
|
||||
7. Allow calling chronometrist-in/out from anywhere-within-Emacs (a la timeclock) as well as from the chronometrist buffer.
|
||||
*** Optimization
|
||||
**** 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
|
||||
5. What if commands both write to the file /and/ add to the hash table, so we don't have to re-read the file and re-populate the table for commands? The expensive reading+parsing could be avoided for commands, and only take place for the user changing the file.
|
||||
* jonasw - store length and hash of previous file, see if the new file has the same hash until old-length bytes.
|
||||
* Rather than storing and hashing the full length, we could do it until (before) the last s-expression (or last N s-expressions?). That way, we know if the last expression (or last N expressions) have changed.
|
||||
* Or even the first expression of the current date. That way, we just re-read the events for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
|
||||
|
||||
** Code [25%]
|
||||
1. refactor repetitive calls to (format "%04d-%02d-%02d" (elt seq a) (elt seq b) (elt seq c))
|
||||
2. [X] Use buttercup instead of ert
|
||||
3. See if it is possible to store buttons in a variable, so *-print-non-tabular functions can be made shorter and less imperative. (see ~make-text-button~)
|
||||
4. Merge all event-querying functions so that they always operate on an entire hash table (so no 'day' variants),
|
||||
5. [ ] Use ~substitute-command-keys~ instead of ~chronometrist-format-keybinds~
|
||||
6. [ ] See if using iteration constructs (especially ~loop~) can lead to nicer code than nested maps
|
||||
** Code [28%]
|
||||
1. [ ] Write tests (in buttercup)
|
||||
2. [ ] Rewrite using cl-loop
|
||||
1. [ ] chronometrist-statistics-count-active-days
|
||||
2. [ ] chronometrist-statistics-count-average-time-spent
|
||||
3. refactor repetitive calls to (format "%04d-%02d-%02d" (elt seq a) (elt seq b) (elt seq c))
|
||||
4. See if it is possible to store buttons in a variable, so *-print-non-tabular functions can be made shorter and less imperative. (see ~make-text-button~)
|
||||
5. Merge all event-querying functions so that they always operate on an entire hash table (so no 'day' variants),
|
||||
6. [ ] Use ~substitute-command-keys~ instead of ~chronometrist-format-keybinds~
|
||||
7. [ ] recreate -events-clean, remove splitting code from -events-populate
|
||||
* How should we deal with the active event?
|
||||
* Earlier, we would add a closing entry and update that on a timer.
|
||||
8. [ ] Make docstrings consistent - describe inputs and then the return value, in that order.
|
||||
9. [ ] ~chronometrist-seconds->alert-string~ can probably be replaced by ~org-duration-from-minutes~ - read the format for FMT
|
||||
10. [X] Decouple storage-related code from rest of the program.
|
||||
11. [X] See if using iteration constructs (especially ~loop~) can lead to nicer code than nested maps
|
||||
+1. use variables instead of hardcoded numbers to determine spacing+
|
||||
* Don't see the benefit
|
||||
+6. Timeclock already _has_ hooks! :| Why do we re-implement them?+
|
||||
- I don't know of a way to know the project being clocked into using timeclock hooks.
|
||||
- With v0.2.0 Chronometrist also has a before-project-stop-functions, which runs before the project is stopped, and can control whether the project actually is stopped.
|
||||
|
||||
* Optimization
|
||||
** Cache
|
||||
+ Lessons from the parsimonious-reading branch - iterating =read= over the whole file is fast; splitting the events 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=.
|
||||
+ 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.
|
||||
*** Thoughts
|
||||
+ =chronometrist-key-value-cache= would basically be the entire file, if =chronometrist-history-suggestion-limit= is nil.
|
||||
+ history generation for tags/keys/values - which involve the most parsing - doesn't actually need the events to be split at midnights. Why not make that a keyword argument to =chronometrist-sexp-read=, so it's faster for 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
|
||||
5. What if commands both write to the file /and/ add to the hash table, so we don't have to re-read the file and re-populate the table for commands? The expensive reading+parsing could be avoided for commands, and only take place for the user changing the file.
|
||||
* [ ] jonasw - store length and hash of previous file, see if the new file has the same hash until old-length bytes.
|
||||
* Rather than storing and hashing the full length, we could do it until (before) the last s-expression (or last N s-expressions?). That way, we know if the last expression (or last N expressions) have changed.
|
||||
* Or even the first expression of the current date. That way, we just re-read the events 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.
|
||||
* chronometrist-report
|
||||
** Features
|
||||
1. [ ] Expandable items - show tag-combination-based breakdown
|
||||
|
@ -136,3 +151,23 @@
|
|||
|
||||
* Documentation [0%]
|
||||
1. [ ] Make Texinfo documentation
|
||||
* Time targets/goals [50%]
|
||||
1. [X] User defines targets for tasks
|
||||
2. [X] Chronometrist displays targets column
|
||||
3. [-] Notify user when
|
||||
+ approaching target (e.g. -5 minutes)
|
||||
+ target completed
|
||||
+ exceeding target (target+5)
|
||||
+ [X] More flexible way to define alerts.
|
||||
+ [ ] If time goals are defined and there is a task without a goal, just remind the user every 15 minutes of the time they've spent on it
|
||||
- [ ] Needs an even more flexible way to define alerts...
|
||||
+ [-] Handle manual file changes; on file change -
|
||||
- [ ] clearing existing notifications
|
||||
- [X] if last expression has a :stop value, stop alert timers
|
||||
+ [ ] Also take time spent so far into account (e.g. don't start approach or complete alerts again if time has been exceeded and we're starting the task again despite that)
|
||||
4. [ ] Colorize times in Chronometrist buffer
|
||||
- untouched project with target defined - red
|
||||
- target ±5 minutes - green
|
||||
- target*2 and above - red
|
||||
* Backend abstraction [0%]
|
||||
1. [ ] Add checks to ~chronometrist-sexp-reindent-buffer~ to ensure it only runs on an s-expression buffer
|
||||
|
|
|
@ -1,232 +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 'chronometrist-custom)
|
||||
(require 'chronometrist-report-custom)
|
||||
(require 'chronometrist-time)
|
||||
|
||||
(declare-function chronometrist-last-expr "chronometrist-sexp.el")
|
||||
|
||||
;; ## 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'.")
|
||||
|
||||
(defvar chronometrist--fs-watch nil
|
||||
"Filesystem watch object.
|
||||
|
||||
Used to prevent more than one watch being added for the same
|
||||
file.")
|
||||
|
||||
(defun chronometrist-buffer-exists? (buffer-name)
|
||||
"Return non-nil if BUFFER-NAME exists."
|
||||
(--> (buffer-list)
|
||||
(mapcar #'buffer-name it)
|
||||
(member buffer-name it)))
|
||||
|
||||
(defun chronometrist-buffer-visible? (buffer-or-buffer-name)
|
||||
"Return t if BUFFER-OR-BUFFER-NAME is visible to user."
|
||||
;; It'd be simpler to use only the windows of the current frame (-->
|
||||
;; (selected-frame) (window-list it) ...) - but it wouldn't be
|
||||
;; robust, because it is possible that a frame partially covers
|
||||
;; another and the buffer is visible to the user from the latter.
|
||||
(let ((result (-->
|
||||
(visible-frame-list)
|
||||
(mapcar #'window-list it)
|
||||
(mapcar (lambda (list)
|
||||
(mapcar #'window-buffer list))
|
||||
it)
|
||||
(mapcar (lambda (list)
|
||||
(mapcar (lambda (buffer)
|
||||
(if (bufferp buffer-or-buffer-name)
|
||||
(equal buffer-or-buffer-name buffer)
|
||||
(equal (buffer-name buffer)
|
||||
buffer-or-buffer-name)))
|
||||
list))
|
||||
it)
|
||||
(mapcar (lambda (list)
|
||||
(-filter #'identity list))
|
||||
it)
|
||||
(mapcar #'car it)
|
||||
(car it))))
|
||||
(if result t nil)))
|
||||
|
||||
(defun chronometrist-format-time (duration &optional blank)
|
||||
"Format DURATION as a string suitable for display in Chronometrist buffers.
|
||||
DURATION must be a vector or a list of the form [HOURS MINUTES
|
||||
SECONDS] or (HOURS MINUTES SECONDS).
|
||||
|
||||
BLANK is a string to display in place of blank values. If not
|
||||
supplied, 3 spaces are used."
|
||||
(let ((h (elt duration 0))
|
||||
(m (elt duration 1))
|
||||
(s (elt duration 2))
|
||||
(blank (if blank blank " ")))
|
||||
(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-open-file (&optional _button)
|
||||
"Open `chronometrist-file' in another window.
|
||||
|
||||
Argument _BUTTON is for the purpose of using this command as a
|
||||
button action."
|
||||
(interactive)
|
||||
(find-file-other-window chronometrist-file)
|
||||
(goto-char (point-max)))
|
||||
|
||||
(defun chronometrist-common-create-chronometrist-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))))
|
||||
|
||||
(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->time-list (events)
|
||||
"Convert EVENTS to a list of time values.
|
||||
|
||||
EVENTS must be a list of valid Chronometrist property lists (see
|
||||
`chronometrist-file').
|
||||
|
||||
For each event, a list of two time values is returned.
|
||||
|
||||
For time value format, see (info \"(elisp)Time of Day\")."
|
||||
(let ((index 0)
|
||||
(length (length events))
|
||||
result)
|
||||
(while (not (= index length))
|
||||
(let* ((elt (elt events index))
|
||||
(start-iso (parse-iso8601-time-string (plist-get elt :start)))
|
||||
(stop (plist-get elt :stop))
|
||||
(stop-iso (if stop
|
||||
(parse-iso8601-time-string stop)
|
||||
(current-time))))
|
||||
(cl-incf index)
|
||||
(setq result (append result `((,start-iso ,stop-iso))))))
|
||||
result))
|
||||
|
||||
(defun chronometrist-time-list->sum-of-intervals (time-value-lists)
|
||||
"From a list of start/end timestamps TIME-VALUES, get the total time interval.
|
||||
|
||||
TIME-VALUE-LISTS is a list in the form
|
||||
\((START STOP) ...)
|
||||
where START and STOP are time values (see (info \"(elisp)Time of Day\")).
|
||||
|
||||
This function obtains the intervals between them, and adds the
|
||||
intervals to return a single time value.
|
||||
|
||||
If TIME-VALUES is nil, return '(0 0)."
|
||||
(if time-value-lists
|
||||
(->> time-value-lists
|
||||
(--map (time-subtract (cadr it) (car it)))
|
||||
(-reduce #'time-add))
|
||||
'(0 0)))
|
||||
|
||||
(defun chronometrist-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-previous-week-start (date-string)
|
||||
"Find the previous `chronometrist-report-week-start-day' from DATE-STRING.
|
||||
|
||||
Return the time value of said day's beginning.
|
||||
|
||||
If the day of DATE is the same as the
|
||||
`chronometrist-report-week-start-day', return DATE.
|
||||
|
||||
DATE-STRING must be in the form \"YYYY-MM-DD\"."
|
||||
(let* ((date-time (chronometrist-iso-date->timestamp date-string))
|
||||
(date-unix (parse-iso8601-time-string date-time))
|
||||
(date-list (decode-time date-unix))
|
||||
(day (elt date-list 6)) ;; 0-6, where 0 = Sunday
|
||||
(week-start (chronometrist-day-of-week->number chronometrist-report-week-start-day))
|
||||
(gap (cond ((> day week-start) (- day week-start))
|
||||
((< day week-start) (+ day (- 7 week-start))))))
|
||||
(if gap
|
||||
(time-subtract date-unix `(0 ,(* gap 86400)))
|
||||
date-unix)))
|
||||
|
||||
(defun chronometrist-current-task ()
|
||||
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
||||
(let ((last-event (chronometrist-last-expr)))
|
||||
(if (plist-member last-event :stop)
|
||||
nil
|
||||
(plist-get last-event :name))))
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist-common"
|
||||
;; End:
|
||||
|
||||
(provide 'chronometrist-common)
|
||||
|
||||
;;; chronometrist-common.el ends here
|
|
@ -1,95 +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-task-time-one-day (task &optional date-string)
|
||||
"Return total time spent on TASK today or (if supplied) on DATE-STRING.
|
||||
The data is obtained from `chronometrist-file', via `chronometrist-events'.
|
||||
|
||||
DATE-STRING must be in the form \"YYYY-MM-DD\".
|
||||
|
||||
The return value is a vector in the form [HOURS MINUTES SECONDS]"
|
||||
(let* ((date-string (if date-string date-string (chronometrist-date)))
|
||||
(task-events (chronometrist-task-events-in-day task date-string))
|
||||
(last-event (cl-copy-list (car (last task-events))))
|
||||
(reversed-events-tail (-> task-events
|
||||
(reverse)
|
||||
(cdr))))
|
||||
(if task-events
|
||||
(->> (if (plist-member last-event :stop)
|
||||
task-events
|
||||
;; last-event is a currently ongoing task
|
||||
(-> (plist-put last-event :stop (chronometrist-format-time-iso8601))
|
||||
(list)
|
||||
(append reversed-events-tail)
|
||||
(reverse)))
|
||||
(chronometrist-events->time-list)
|
||||
(chronometrist-time-list->sum-of-intervals)
|
||||
(cadr)
|
||||
(chronometrist-seconds-to-hms))
|
||||
;; no events for this task on DATE-STRING i.e. no time spent
|
||||
[0 0 0])))
|
||||
|
||||
(defun chronometrist-active-time-one-day (&optional date-string)
|
||||
"Return the total active time on DATE (if non-nil) or today.
|
||||
DATE-STRING must be in the form \"YYYY-MM-DD\".
|
||||
|
||||
Return value is a vector in the form [HOURS MINUTES SECONDS]."
|
||||
(->> chronometrist-task-list
|
||||
(--map (chronometrist-task-time-one-day it date-string))
|
||||
(-reduce #'chronometrist-time-add)))
|
||||
|
||||
(defun chronometrist-statistics-count-active-days (task &optional table)
|
||||
"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)
|
||||
(table (if table table chronometrist-events)))
|
||||
(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 date-string)
|
||||
"Get events for TASK on DATE-STRING.
|
||||
DATE-STRING must be in the form \"YYYY-MM-DD\".
|
||||
|
||||
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 date-string chronometrist-events)
|
||||
(mapcar (lambda (event)
|
||||
(when (equal task (plist-get event :name))
|
||||
event)))
|
||||
(seq-filter #'identity)))
|
||||
|
||||
|
||||
(provide 'chronometrist-queries)
|
||||
|
||||
;;; chronometrist-queries.el ends here
|
|
@ -1,180 +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:
|
||||
|
||||
(defconst chronometrist-seconds-in-day (* 60 60 24)
|
||||
"Number of seconds in a day.")
|
||||
|
||||
(defun chronometrist-date (&optional time)
|
||||
"Return date for TIME or today, in the form \"YYYY-MM-DD\"."
|
||||
(s-left 10 (chronometrist-format-time-iso8601 time)))
|
||||
|
||||
;; (defun chronometrist-time (&optional time))
|
||||
|
||||
(defun chronometrist-day-of-week->number (day-of-week)
|
||||
"Return an integer (0-6) representing DAY-OF-WEEK.
|
||||
|
||||
DAY-OF-WEEK should be a string, e.g. \"Sunday\" - see
|
||||
`chronometrist-report-weekday-number-alist'."
|
||||
(cdr
|
||||
(assoc-string day-of-week chronometrist-report-weekday-number-alist)))
|
||||
|
||||
(defun chronometrist-number->day-of-week (number)
|
||||
"Return the day of the week (as a string), corresponding to NUMBER.
|
||||
|
||||
NUMBER should be an integer (0-6) - see
|
||||
`chronometrist-report-weekday-number-alist'."
|
||||
(car
|
||||
(rassoc number chronometrist-report-weekday-number-alist)))
|
||||
|
||||
(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))
|
||||
|
||||
(defun chronometrist-time-interval-span-midnight? (t1 t2)
|
||||
"Return t if time range T1 to T2 extends across midnight.
|
||||
|
||||
T1 and T2 must be lists in the form (YEAR MONTH DAY HOURS MINUTES
|
||||
SECONDS), as returned by `timestamp->list'. T2 must be
|
||||
chronologically more recent than T1."
|
||||
(let* ((day-1 (elt t1 2))
|
||||
(day-2 (elt t2 2))
|
||||
(month-1 (elt t1 1))
|
||||
(month-2 (elt t2 1)))
|
||||
;; not Absolutely Perfect™, but should do for most situations
|
||||
(or (= day-2 (1+ day-1))
|
||||
(= month-2 (1+ month-1)))))
|
||||
|
||||
;; 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 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-time->seconds (duration)
|
||||
"Convert DURATION to seconds.
|
||||
DURATION must be a vector in the form [HOURS MINUTES SECONDS]."
|
||||
(-let [[h m s] duration]
|
||||
(+ (* h 60 60)
|
||||
(* m 60)
|
||||
s)))
|
||||
|
||||
(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)))
|
||||
(vector h m s)))
|
||||
|
||||
(defun chronometrist-time-add (a b)
|
||||
"Add durations A and B and return a vector in the same form.
|
||||
A and B should be vectors in the form [HOURS MINUTES SECONDS]."
|
||||
(let ((h1 (elt a 0))
|
||||
(m1 (elt a 1))
|
||||
(s1 (elt a 2))
|
||||
(h2 (elt b 0))
|
||||
(m2 (elt b 1))
|
||||
(s2 (elt b 2)))
|
||||
(chronometrist-seconds-to-hms (+ (* h1 3600) (* h2 3600)
|
||||
(* m1 60) (* m2 60)
|
||||
s1 s2))))
|
||||
|
||||
(defun chronometrist-iso-date->timestamp (date)
|
||||
"Convert DATE to a complete timestamp by adding a time part (T00:00:00)."
|
||||
;; potential problem - time zones are ignored
|
||||
(concat date "T00:00:00"))
|
||||
|
||||
(defun chronometrist-date->time (date)
|
||||
"Convert DATE to a time value (see (info \"(elisp)Time of Day\")).
|
||||
DATE must be a list in the form (YEAR MONTH DAY)."
|
||||
(->> date (reverse) (apply #'encode-time 0 0 0)))
|
||||
|
||||
(defun chronometrist-date-less-p (date1 date2)
|
||||
"Like `time-less-p' but for dates. Return t if DATE1 is less than DATE2.
|
||||
Both must be dates in the ISO-8601 format."
|
||||
(time-less-p (-> date1
|
||||
(chronometrist-iso-date->timestamp)
|
||||
(parse-iso8601-time-string))
|
||||
(-> date2
|
||||
(chronometrist-iso-date->timestamp)
|
||||
(parse-iso8601-time-string))))
|
||||
|
||||
(defun chronometrist-time-less-or-equal-p (t1 t2)
|
||||
"Return t if T1 is less than or equal to T2.
|
||||
|
||||
T1 and T2 should be time values (see `current-time')."
|
||||
(or (equal t1 t2)
|
||||
(time-less-p t1 t2)))
|
||||
|
||||
(defun chronometrist-calendrical->date (date)
|
||||
"Convert calendrical information DATE to a date in the form (YEAR MONTH DAY).
|
||||
|
||||
For input format, see (info \"(elisp)Time of Day\")."
|
||||
(-> date (-slice 3 6) (reverse)))
|
||||
|
||||
(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)
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist"
|
||||
;; End:
|
||||
|
||||
;;; chronometrist-time.el ends here
|
|
@ -14,7 +14,7 @@ Each of them has a corresponding `-custom` file, which contain the Customize gro
|
|||
|
||||
[chronometrist-common.el](chronometrist-common.el) contains definitions common to all components.
|
||||
|
||||
All three components use timers to keep their buffers updated. [chronometrist-timer.el](chronometrist-timer.el) contains all timer-related code. Note - sometimes, when hacking, timers may cause subtle bugs which are very hard to debug. Restarting Emacs can fix them, so try that as a first sanity check.
|
||||
All three components use timers to keep their buffers updated. [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.
|
||||
|
||||
## Browsing the code
|
||||
I recommend using
|
||||
|
|
331
doc/manual.org
331
doc/manual.org
|
@ -37,53 +37,55 @@
|
|||
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)
|
||||
**** 3. Split them at the hash-table-level
|
||||
Handled by ~chronometrist-events-clean~
|
||||
Handled by ~chronometrist-sexp-events-populate~
|
||||
* Advantage - simpler data-consuming code.
|
||||
**** 4. 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.
|
||||
|
||||
** Reference
|
||||
* (?) - of dubious utility, a candidate for deprecation
|
||||
* DEPRECATED - deprecated, slated to be removed in the future
|
||||
|
||||
*** Legend of currently-used time formats
|
||||
| 1. | decode-time | (seconds minutes hours day month year dow dst utcoff) |
|
||||
| 2. | list-timestamp | (year month day hours minutes seconds) |
|
||||
| 3. | list-time/list-duration | (hours minutes seconds) |
|
||||
| 4. | list-date | (year month day) |
|
||||
| 5. | vector-date | [year month day] |
|
||||
| 6. | vector-time/vector-duration | [hours minutes seconds] |
|
||||
| 7. | encode-time (UNIX epoch) | (sec-high sec-low microsec picosec) |
|
||||
| 8. | seconds | seconds as an integer |
|
||||
| 9. | iso-timestamp | "YYYY-MM-DDTHH:MM:SSZ" |
|
||||
| 10. | iso-date | "YYYY-MM-DD" |
|
||||
| 11. | (DEPRECATED) timeclock-timestamp | "year/month/day hours:minutes:seconds" |
|
||||
| 12. | (DEPRECATED) timeclock-date | "year/month/day" |
|
||||
**** 1. ts
|
||||
ts.el struct
|
||||
* Used by nearly all internal functions
|
||||
**** 2. 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
|
||||
**** 3. 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=
|
||||
**** 4. 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)
|
||||
**** 5. minutes
|
||||
integer minutes as duration
|
||||
* Used for goals (chronometrist-goals-list, chronometrist-get-goal) - minutes seems like the ideal unit for users to enter
|
||||
**** 6. 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. Variable - chronometrist--fs-watch
|
||||
6. Function - chronometrist-buffer-exists? (buffer-name)
|
||||
* String -> List?
|
||||
7. Function - chronometrist-buffer-visible? (buffer-or-buffer-name)
|
||||
* Buffer | String -> Boolean
|
||||
8. Function - chronometrist-format-time (duration &optional blank)
|
||||
* vector-duration | list-duration -> "h:m:s"
|
||||
9. Command - chronometrist-open-file (&optional button)
|
||||
10. Function - chronometrist-common-create-chronometrist-file ()
|
||||
11. Function - chronometrist-common-file-empty-p (file)
|
||||
12. Function - chronometrist-common-clear-buffer (buffer)
|
||||
13. Function - chronometrist-format-keybinds (command map &optional firstonly)
|
||||
14. Function - chronometrist-events->time-list (events)
|
||||
* (event ...) -> ((encode-time encode-time) ...)
|
||||
15. Function - chronometrist-time-list->sum-of-intervals (time-value-lists)
|
||||
* ((encode-time encode-time) ...) -> encode-time
|
||||
16. Function - chronometrist-delete-list (&optional arg)
|
||||
17. Function - chronometrist-previous-week-start (date-string)
|
||||
18. Function - chronometrist-current-task ()
|
||||
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
|
||||
|
@ -93,52 +95,64 @@
|
|||
6. Custom variable - chronometrist-day-start-time
|
||||
*** chronometrist-diary-view.el
|
||||
1. Variable - chronometrist-diary-buffer-name
|
||||
2. Variable - chronometrist-diary--current-date
|
||||
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. Variable - chronometrist--task-history
|
||||
2. Variable - chronometrist--point
|
||||
3. Variable - chronometrist-mode-map
|
||||
4. Function - chronometrist-task-active? (task)
|
||||
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
|
||||
5. Function - chronometrist-activity-indicator ()
|
||||
6. Function - chronometrist-entries ()
|
||||
7. Function - chronometrist-task-at-point ()
|
||||
8. Function - chronometrist-goto-last-task ()
|
||||
9. Function - chronometrist-print-keybind (command &optional description firstonly)
|
||||
10. Function - chronometrist-print-non-tabular ()
|
||||
11. Function - chronometrist-goto-nth-task (n)
|
||||
12. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
|
||||
13. Function - chronometrist-refresh-file (fs-event)
|
||||
14. Command - chronometrist-query-stop ()
|
||||
15. Variable - chronometrist-before-in-functions
|
||||
16. Variable - chronometrist-after-in-functions
|
||||
17. Variable - chronometrist-before-out-functions
|
||||
18. Variable - chronometrist-after-out-functions
|
||||
19. Function - chronometrist-run-functions-and-clock-in (task)
|
||||
20. Function - chronometrist-run-functions-and-clock-out (task)
|
||||
21. Variable - chronometrist-mode-map
|
||||
22. Major Mode - chronometrist-mode
|
||||
23. Function - chronometrist-toggle-task-button (button)
|
||||
24. Function - chronometrist-add-new-task-button (button)
|
||||
25. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
|
||||
26. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
|
||||
27. Command - chronometrist-add-new-task ()
|
||||
28. Command - chronometrist (&optional arg)
|
||||
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
|
||||
2. Function - chronometrist-list-midnight-spanning-events ()
|
||||
3. Function - chronometrist-day-start (timestamp)
|
||||
4. Function - chronometrist-file-clean ()
|
||||
5. Function - chronometrist-events-maybe-split (event)
|
||||
6. Function - chronometrist-events-populate ()
|
||||
7. Function - chronometrist-tasks-from-table ()
|
||||
8. Function - chronometrist-events-subset (start-date end-date)
|
||||
9. Function - chronometrist-events-query-spec-match-p (plist specifiers)
|
||||
* 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
|
||||
10. Function - chronometrist-events-query-spec-match-p (plist specifiers)
|
||||
11. Function - chronometrist-events-query (table &key get specifiers except)
|
||||
*** chronometrist-migrate.el
|
||||
1. Variable - chronometrist-migrate-table
|
||||
2. Function - chronometrist-migrate-populate (in-file)
|
||||
|
@ -153,43 +167,47 @@
|
|||
6. Function - chronometrist-plist-pp-to-string (object)
|
||||
7. Function - chronometrist-plist-pp (object &optional stream)
|
||||
*** chronometrist-queries.el
|
||||
1. Function - chronometrist-task-time-one-day (task &optional date-string)
|
||||
2. Function - chronometrist-active-time-one-day (&optional date-string)
|
||||
3. Function - chronometrist-statistics-count-active-days (task &optional table)
|
||||
4. Function - chronometrist-task-events-in-day (task date-string)
|
||||
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. Variable - chronometrist-report--ui-date
|
||||
2. Variable - chronometrist-report--ui-week-dates
|
||||
3. Variable - chronometrist-report--point
|
||||
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-format-date (format-string time-date)
|
||||
9. Function - chronometrist-report-print-keybind (command &optional description firstonly)
|
||||
10. Function - chronometrist-report-print-non-tabular ()
|
||||
11. Function - chronometrist-report-refresh (&optional ignore-auto noconfirm)
|
||||
12. Function - chronometrist-report-refresh-file (fs-event)
|
||||
13. Variable - chronometrist-report-mode-map
|
||||
14. Major Mode - chronometrist-report-mode
|
||||
15. Function - chronometrist-report (&optional keep-date)
|
||||
16. Function - chronometrist-report-previous-week (arg)
|
||||
17. Function - chronometrist-report-next-week (arg)
|
||||
*** chronometrist-sexp.el
|
||||
1. Variable - chronometrist--tag-suggestions
|
||||
2. Variable - chronometrist--value-suggestions
|
||||
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. Command - chronometrist-reindent-buffer ()
|
||||
7. Function - chronometrist-last-expr ()
|
||||
8. Function - chronometrist-append-to-last-expr (tags plist)
|
||||
9. Variable - chronometrist-tags-history
|
||||
10. Function - chronometrist-tags-history-populate ()
|
||||
6. Function - chronometrist-append-to-last (tags plist)
|
||||
7. Variable - chronometrist-tags-history
|
||||
8. Function - chronometrist-tags-history-populate ()
|
||||
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)
|
||||
|
@ -200,62 +218,85 @@
|
|||
18. Function - chronometrist-ht-history-prep (table)
|
||||
19. Function - chronometrist-key-history-populate ()
|
||||
20. Function - chronometrist-value-history-populate ()
|
||||
21. Command - chronometrist-kv-accept ()
|
||||
22. Command - chronometrist-kv-reject ()
|
||||
23. Variable - chronometrist-kv-read-mode-map
|
||||
24. Major Mode - chronometrist-kv-read-mode
|
||||
25. Function - chronometrist-kv-completion-quit-key ()
|
||||
26. Function - chronometrist-string-has-whitespace-p (string)
|
||||
27. Function - chronometrist-key-prompt (used-keys)
|
||||
28. Function - chronometrist-value-prompt (key)
|
||||
29. Function - chronometrist-value-insert (value)
|
||||
30. Function - chronometrist-kv-add (&rest args)
|
||||
31. Command - chronometrist-in (task &optional prefix)
|
||||
32. Command - chronometrist-out (&optional prefix)
|
||||
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 ()
|
||||
*** chronometrist-statistics-custom.el
|
||||
1. Custom variable - chronometrist-statistics-buffer-name
|
||||
*** chronometrist-statistics.el
|
||||
1. Variable - chronometrist-statistics--ui-state
|
||||
2. Variable - chronometrist-statistics--point
|
||||
3. Variable - chronometrist-statistics-mode-map
|
||||
4. Function - chronometrist-statistics-count-average-time-spent (task &optional table)
|
||||
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
|
||||
5. Function - chronometrist-statistics-entries-internal (table)
|
||||
6. Function - chronometrist-statistics-entries ()
|
||||
7. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
|
||||
8. Function - chronometrist-statistics-format-date (date)
|
||||
9. Function - chronometrist-statistics-print-non-tabular ()
|
||||
10. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
|
||||
11. Major Mode - chronometrist-statistics-mode
|
||||
12. Command - chronometrist-statistics (&optional preserve-state)
|
||||
13. Command - chronometrist-statistics-previous-range (arg)
|
||||
14. Command - chronometrist-statistics-next-range (arg)
|
||||
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. Constant - chronometrist-seconds-in-day
|
||||
2. Function - chronometrist-date (&optional time)
|
||||
3. Function - chronometrist-day-of-week->number (day-of-week)
|
||||
4. Function - chronometrist-number->day-of-week (number)
|
||||
5. Function - chronometrist-format-time-iso8601 (&optional unix-time)
|
||||
6. Function - chronometrist-time-interval-span-midnight? (t1 t2)
|
||||
* list-timestamp list-timestamp -> Boolean
|
||||
7. Function - chronometrist-midnight-spanning-p (start-time stop-time)
|
||||
8. Function - chronometrist-time->seconds (time)
|
||||
* vector-duration -> seconds
|
||||
9. Function - chronometrist-seconds-to-hms (seconds)
|
||||
* seconds -> vector-duration
|
||||
10. Function - chronometrist-time-add (a b)
|
||||
* time-vector time-vector -> time-vector
|
||||
11. Function - chronometrist-iso-date->timestamp (date)
|
||||
12. Function - chronometrist-date->time (date)
|
||||
13. Function - chronometrist-date-less-p (date1 date2)
|
||||
14. Function - chronometrist-time-less-or-equal-p (t1 t2)
|
||||
15. Function - chronometrist-calendrical->date (date)
|
||||
16. Function - chronometrist-interval (event)
|
||||
* event -> encode-time
|
||||
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. Variable - chronometrist--timer-object
|
||||
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-goals
|
||||
1. Internal Variable - chronometrist--timers-list
|
||||
2. Custom Variable - chronometrist-goals-list nil
|
||||
3. Function - chronometrist-run-at-time (time repeat function &rest args)
|
||||
4. Function - chronometrist-seconds->alert-string (seconds)
|
||||
* seconds -> string
|
||||
5. Function - chronometrist-approach-alert (task goal spent)
|
||||
* string minutes minutes
|
||||
6. Function - chronometrist-complete-alert (task goal spent)
|
||||
* string minutes minutes
|
||||
7. Function - chronometrist-exceed-alert (task goal spent)
|
||||
* string minutes minutes
|
||||
8. Function - chronometrist-no-goal-alert (task goal spent)
|
||||
* string minutes minutes
|
||||
9. Custom Variable - chronometrist-goals-alert-functions
|
||||
* each function is passed - string minutes minutes
|
||||
10. Function - chronometrist-get-goal (task &optional (goals-list chronometrist-goals-list))
|
||||
* String &optional List -> minutes
|
||||
11. Function - chronometrist-goals-run-alert-timers (task)
|
||||
12. Function - chronometrist-goals-stop-alert-timers (&optional _task)
|
||||
13. Function - chronometrist-goals-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 ()
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
;;; 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)
|
||||
(--> (append chronometrist-task-list task)
|
||||
(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))
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist-common"
|
||||
;; End:
|
||||
|
||||
(provide 'chronometrist-common)
|
||||
|
||||
;;; chronometrist-common.el ends here
|
|
@ -59,8 +59,6 @@ vectors.\)"
|
|||
This is not guaranteed to be accurate - see (info \"(elisp)Timers\")."
|
||||
:type 'integer)
|
||||
|
||||
(declare-function chronometrist-format-time "chronometrist-common")
|
||||
(declare-function chronometrist-last-expr "chronometrist-sexp")
|
||||
(eval-when-compile (require 'subr-x))
|
||||
|
||||
(defcustom chronometrist-activity-indicator "*"
|
|
@ -43,7 +43,7 @@ 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->time-list)
|
||||
(chronometrist-events->ts-pairs)
|
||||
;; Why were we calling `-partition' here?
|
||||
;; (-partition 2)
|
||||
(--map (time-subtract (cadr it) (car it)))
|
|
@ -11,15 +11,27 @@
|
|||
;;
|
||||
;; For more information, please refer to <https://unlicense.org>
|
||||
|
||||
(require 'chronometrist-plist-pp)
|
||||
;; (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))
|
||||
(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.
|
||||
|
@ -39,43 +51,44 @@ Return value is a time value (see `current-time')."
|
|||
(append it timestamp-date-list)
|
||||
(apply #'encode-time it))))
|
||||
|
||||
(defun chronometrist-file-clean ()
|
||||
"Clean `chronometrist-file' so that events can be processed accurately.
|
||||
;; (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'.
|
||||
;; 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-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))
|
||||
;; 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.
|
||||
|
@ -106,47 +119,13 @@ Return a list of two events if EVENT was split, else nil."
|
|||
;; 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' and populate it.
|
||||
|
||||
"Clear hash table `chronometrist-events' (which see) and populate it.
|
||||
The data is acquired from `chronometrist-file'.
|
||||
|
||||
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).
|
||||
|
||||
Return final number of events read from file, or nil if there
|
||||
were none."
|
||||
(clrhash chronometrist-events)
|
||||
(with-current-buffer (find-file-noselect chronometrist-file)
|
||||
(save-excursion
|
||||
(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)))))
|
||||
(chronometrist-sexp-events-populate))
|
||||
|
||||
(defun chronometrist-tasks-from-table ()
|
||||
"Return a list of task names from `chronometrist-events'."
|
||||
|
@ -159,18 +138,38 @@ were none."
|
|||
(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-date end-date)
|
||||
(defun chronometrist-events-subset (start end)
|
||||
"Return a subset of `chronometrist-events'.
|
||||
|
||||
The subset will contain values between START-DATE and
|
||||
END-DATE (both inclusive).
|
||||
The subset will contain values between dates START and END (both
|
||||
inclusive).
|
||||
|
||||
START-DATE and END-DATE must be dates in the form '(YEAR MONTH DAY)."
|
||||
(let ((subset (make-hash-table :test #'equal)))
|
||||
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 (and (not (chronometrist-date-less-p key start-date))
|
||||
(not (chronometrist-date-less-p end-date key)))
|
||||
(when (ts-in start end (chronometrist-iso-date->ts key))
|
||||
(puthash key value subset)))
|
||||
chronometrist-events)
|
||||
subset))
|
|
@ -1,4 +1,4 @@
|
|||
;;; chronometrist-sexp.el --- S-expression backend for Chronometrist -*- lexical-binding: t; -*-
|
||||
;;; chronometrist-key-values.el --- add key-values to Chronometrist data -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
|
@ -26,6 +26,8 @@
|
|||
|
||||
;;; Code:
|
||||
|
||||
(require 'chronometrist-sexp)
|
||||
|
||||
(defvar chronometrist--tag-suggestions nil
|
||||
"Suggestions for tags.
|
||||
Used as history by `chronometrist-tags-prompt'.")
|
||||
|
@ -58,71 +60,42 @@ Used as history by `chronometrist--value-suggestions'.")
|
|||
it)
|
||||
list))
|
||||
|
||||
(defun chronometrist-reindent-buffer ()
|
||||
"Reindent the current buffer.
|
||||
This is meant to be run in `chronometrist-file'."
|
||||
(interactive)
|
||||
(let (expr)
|
||||
(goto-char (point-min))
|
||||
(while (setq expr (ignore-errors (read (current-buffer))))
|
||||
(backward-list)
|
||||
(chronometrist-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")))))
|
||||
|
||||
(defun chronometrist-last-expr ()
|
||||
"Return last s-expression from `chronometrist-file'."
|
||||
(let ((buffer (find-file-noselect chronometrist-file)))
|
||||
(with-current-buffer buffer
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(backward-list)
|
||||
(ignore-errors
|
||||
(read buffer))))))
|
||||
|
||||
(defun chronometrist-append-to-last-expr (tags plist)
|
||||
"Add TAGS and PLIST to last s-expression in `chronometrist-file'.
|
||||
(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.
|
||||
|
||||
PLIST should be a property list. Properties reserved by
|
||||
Chronometrist - currently :name, :tags, :start, and :stop - will
|
||||
be removed."
|
||||
(let* ((old-expr (chronometrist-last-expr))
|
||||
(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))
|
||||
(old-kvs (chronometrist-plist-remove old-expr :name :tags :start :stop))
|
||||
(plist (chronometrist-plist-remove plist :name :tags :start :stop))
|
||||
(new-tags (if old-tags
|
||||
(-> (append old-tags tags)
|
||||
(cl-remove-duplicates :test #'equal))
|
||||
tags))
|
||||
(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))
|
||||
(buffer (find-file-noselect chronometrist-file)))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(backward-list)
|
||||
(chronometrist-delete-list)
|
||||
(-> (append `(:name ,old-name)
|
||||
(when new-tags `(:tags ,new-tags))
|
||||
new-kvs
|
||||
`(:start ,old-start)
|
||||
(when old-stop `(:stop ,old-stop)))
|
||||
(chronometrist-plist-pp buffer))
|
||||
(save-buffer))))
|
||||
(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)))
|
||||
|
||||
;;;; TAGS ;;;;
|
||||
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
|
||||
|
@ -145,20 +118,41 @@ as symbol and/or strings.")
|
|||
(append existing-tags `(,tags))
|
||||
`(,tags))
|
||||
table))))
|
||||
;; We can't use `chronometrist-ht-history-prep' here, because it uses
|
||||
;; `-flatten'; `chronometrist-tags-history' holds tag combinations (as lists),
|
||||
;; not individual tags.
|
||||
;; 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 table
|
||||
using (hash-values list)
|
||||
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 list :test #'equal)
|
||||
(-> (cl-remove-duplicates tag-lists :test #'equal)
|
||||
(reverse))
|
||||
table))))
|
||||
|
||||
(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
|
||||
(puthash name (append tags old-tags) 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 (plist-get plist :tags))
|
||||
(old-tags (gethash name table)))
|
||||
(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.
|
||||
|
@ -200,9 +194,10 @@ INITIAL-INPUT is as used in `completing-read'."
|
|||
'chronometrist--tag-suggestions))
|
||||
|
||||
(defun chronometrist-tags-add (&rest _args)
|
||||
"Read tags from the user, add them to the last s-expr in `chronometrist-file'.
|
||||
_ARGS are ignored. This function always returns t."
|
||||
(let* ((last-expr (chronometrist-last-expr))
|
||||
"Read tags from the user and 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'."
|
||||
(let* ((last-expr (chronometrist-last))
|
||||
(last-name (plist-get last-expr :name))
|
||||
(last-tags (plist-get last-expr :tags))
|
||||
(input (->> last-tags
|
||||
|
@ -212,11 +207,11 @@ _ARGS are ignored. This function always returns t."
|
|||
(chronometrist-tags-prompt last-name)
|
||||
(chronometrist-maybe-string-to-symbol))))
|
||||
(when input
|
||||
(-> (append last-tags input)
|
||||
(reverse)
|
||||
(cl-remove-duplicates :test #'equal)
|
||||
(reverse)
|
||||
(chronometrist-append-to-last-expr nil)))
|
||||
(--> (append last-tags input)
|
||||
(reverse it)
|
||||
(cl-remove-duplicates it :test #'equal)
|
||||
(reverse it)
|
||||
(chronometrist-append-to-last it nil)))
|
||||
t))
|
||||
|
||||
;;;; KEY-VALUES ;;;;
|
||||
|
@ -255,44 +250,36 @@ reversed and will have duplicate elements removed."
|
|||
table))
|
||||
table))
|
||||
|
||||
;; Since we have discarded sorting-by-frequency, we can now consider
|
||||
;; implementing this by querying `chronometrist-events' instead of reading the file
|
||||
(defun chronometrist-key-history-populate ()
|
||||
"Populate `chronometrist-key-history' from from `chronometrist-file'.
|
||||
"Populate `chronometrist-key-history' from `chronometrist-file'.
|
||||
Each hash table key is the name of a task. Each hash table 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."
|
||||
(clrhash chronometrist-key-history)
|
||||
;; add each task as a key
|
||||
(mapc (lambda (task)
|
||||
(puthash task nil chronometrist-key-history))
|
||||
;; ;; Not necessary, if the only placed this is called is `chronometrist-refresh-file'
|
||||
;; (setq chronometrist--task-list (chronometrist-tasks-from-table))
|
||||
chronometrist-task-list)
|
||||
(with-current-buffer (find-file-noselect chronometrist-file)
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(let (expr)
|
||||
(while (setq expr (ignore-errors (read (current-buffer))))
|
||||
(let* ((name (plist-get expr :name))
|
||||
(name-ht-value (gethash name chronometrist-key-history))
|
||||
(keys (->> (chronometrist-plist-remove expr :name :start :stop :tags)
|
||||
(seq-filter #'keywordp))))
|
||||
(cl-loop for key in keys
|
||||
do (when key
|
||||
(let ((key-string (->> (symbol-name key)
|
||||
(s-chop-prefix ":")
|
||||
(list))))
|
||||
(puthash name
|
||||
(if name-ht-value
|
||||
(append name-ht-value key-string)
|
||||
key-string)
|
||||
chronometrist-key-history))))))
|
||||
(chronometrist-ht-history-prep chronometrist-key-history)))))
|
||||
;; ;; 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 chronometrist-events do
|
||||
(cl-loop for plist in events do
|
||||
(let ((name (plist-get plist :name))
|
||||
(keys (->> (chronometrist-plist-remove plist :name :start :stop :tags)
|
||||
(seq-filter #'keywordp))))
|
||||
(cl-loop for key in keys do
|
||||
(when key
|
||||
(let ((old-keys (gethash name chronometrist-key-history))
|
||||
(new-key (->> (symbol-name key)
|
||||
(s-chop-prefix ":")
|
||||
(list))))
|
||||
(--> (if old-keys
|
||||
(append old-keys new-key)
|
||||
new-key)
|
||||
(puthash name it chronometrist-key-history))))))))
|
||||
(chronometrist-ht-history-prep chronometrist-key-history))
|
||||
|
||||
;; FIXME - seems to be a little buggy. The latest value for e.g. :song
|
||||
;; is different from the one that ends up as the last in
|
||||
;; `chronometrist-value-history' (before being reversed by `chronometrist-ht-history-prep')
|
||||
(defun chronometrist-value-history-populate ()
|
||||
"Read values for user-keys from `chronometrist-events'.
|
||||
The values are stored in `chronometrist-value-history'."
|
||||
|
@ -301,73 +288,37 @@ The values are stored in `chronometrist-value-history'."
|
|||
(let ((table chronometrist-value-history)
|
||||
user-kvs)
|
||||
(clrhash table)
|
||||
(maphash (lambda (_date plist-list)
|
||||
(cl-loop for plist in plist-list
|
||||
do (setq user-kvs (chronometrist-plist-remove plist
|
||||
:name :tags
|
||||
:start :stop))
|
||||
(cl-loop for (key1 val1) on user-kvs by #'cddr
|
||||
do (let* ((key1-string (->> (symbol-name key1)
|
||||
(s-chop-prefix ":")))
|
||||
(key1-ht (gethash key1-string table))
|
||||
(val1 (if (not (stringp val1))
|
||||
(list
|
||||
(format "%s" val1))
|
||||
(list val1))))
|
||||
(puthash key1-string
|
||||
(if key1-ht
|
||||
(append key1-ht val1)
|
||||
val1)
|
||||
table)))))
|
||||
chronometrist-events)
|
||||
(cl-loop
|
||||
for plist-list being the hash-values of chronometrist-events do
|
||||
(cl-loop
|
||||
for plist in plist-list do
|
||||
;; We call them user-kvs because we filter out Chronometrist's
|
||||
;; reserved key-values
|
||||
(setq user-kvs (chronometrist-plist-remove plist
|
||||
:name :tags
|
||||
:start :stop))
|
||||
(cl-loop
|
||||
for (key1 val1) on user-kvs by #'cddr do
|
||||
(let* ((key1-string (->> (symbol-name key1)
|
||||
(s-chop-prefix ":")))
|
||||
(key1-ht (gethash key1-string table))
|
||||
(val1 (if (not (stringp val1))
|
||||
(list
|
||||
(format "%s" val1))
|
||||
(list val1))))
|
||||
(puthash key1-string
|
||||
(if key1-ht
|
||||
(append key1-ht val1)
|
||||
val1)
|
||||
table)))))
|
||||
(chronometrist-ht-history-prep table)))
|
||||
|
||||
;; TODO - refactor this to use `chronometrist-append-to-last-expr'
|
||||
(defun chronometrist-kv-accept ()
|
||||
"Accept the plist in `chronometrist-kv-buffer-name' and add it to `chronometrist-file'."
|
||||
(interactive)
|
||||
(let ((backend-buffer (find-file-noselect chronometrist-file))
|
||||
user-kv-expr
|
||||
last-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
|
||||
(with-current-buffer backend-buffer
|
||||
(goto-char (point-max))
|
||||
(backward-list)
|
||||
(setq last-expr (ignore-errors (read backend-buffer)))
|
||||
(backward-list)
|
||||
(chronometrist-delete-list)
|
||||
(let ((name (plist-get last-expr :name))
|
||||
(tags (plist-get last-expr :tags))
|
||||
(start (plist-get last-expr :start))
|
||||
(stop (plist-get last-expr :stop))
|
||||
(old-kvs (chronometrist-plist-remove last-expr :name :tags :start :stop)))
|
||||
(chronometrist-plist-pp (append (when name `(:name ,name))
|
||||
(when tags `(:tags ,tags))
|
||||
old-kvs
|
||||
user-kv-expr
|
||||
(when start `(:start ,start))
|
||||
(when stop `(:stop ,stop)))
|
||||
backend-buffer))
|
||||
(save-buffer))
|
||||
(switch-to-buffer chronometrist-buffer-name)
|
||||
(chronometrist-refresh))))
|
||||
|
||||
(defun chronometrist-kv-reject ()
|
||||
"Reject the property list in `chronometrist-kv-buffer-name'."
|
||||
(interactive)
|
||||
(kill-buffer chronometrist-kv-buffer-name)
|
||||
(chronometrist-refresh))
|
||||
|
||||
(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-mode'.")
|
||||
"Keymap used by `chronometrist-kv-read-mode'.")
|
||||
|
||||
(define-derived-mode chronometrist-kv-read-mode emacs-lisp-mode "Key-Values"
|
||||
"Mode used by `chronometrist' to read key values from the user."
|
||||
|
@ -395,8 +346,9 @@ It currently supports ido, ido-ubiquitous, ivy, and helm."
|
|||
|
||||
(defun chronometrist-key-prompt (used-keys)
|
||||
"Prompt the user to enter keys.
|
||||
USED-KEYS are keys they have already used in this session."
|
||||
(let ((key-suggestions (--> (chronometrist-last-expr)
|
||||
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 (concat "Key ("
|
||||
|
@ -443,10 +395,11 @@ 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."
|
||||
_ARGS are ignored. This function always returns t, so it can be
|
||||
used in `chronometrist-before-out-functions'."
|
||||
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
|
||||
(first-key-p t)
|
||||
(last-kvs (chronometrist-plist-remove (chronometrist-last-expr)
|
||||
(last-kvs (chronometrist-plist-remove (chronometrist-last)
|
||||
:name :tags :start :stop))
|
||||
(used-keys (->> (seq-filter #'keywordp last-kvs)
|
||||
(mapcar #'symbol-name)
|
||||
|
@ -480,51 +433,32 @@ _ARGS are ignored. This function always returns t."
|
|||
(if (string-empty-p input)
|
||||
(throw 'empty-input nil)
|
||||
(chronometrist-value-insert value)))))
|
||||
(chronometrist-reindent-buffer)))
|
||||
(chronometrist-sexp-reindent-buffer)))
|
||||
t)
|
||||
|
||||
|
||||
;;;; COMMANDS ;;;;
|
||||
(defun chronometrist-in (task &optional _prefix)
|
||||
"Clock in to TASK; record current time in `chronometrist-file'.
|
||||
TASK is the name of the task, a string.
|
||||
(defun chronometrist-kv-accept ()
|
||||
"Accept the plist in `chronometrist-kv-buffer-name' and add it to `chronometrist-file'."
|
||||
(interactive)
|
||||
(let (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-refresh))))
|
||||
|
||||
PREFIX is ignored."
|
||||
(interactive "P")
|
||||
(let ((buffer (find-file-noselect chronometrist-file)))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(unless (bobp) (insert "\n"))
|
||||
(unless (bolp) (insert "\n"))
|
||||
(chronometrist-plist-pp `(:name ,task
|
||||
:start ,(format-time-string "%FT%T%z"))
|
||||
buffer)
|
||||
(save-buffer))))
|
||||
(defun chronometrist-kv-reject ()
|
||||
"Reject the property list in `chronometrist-kv-buffer-name'."
|
||||
(interactive)
|
||||
(kill-buffer chronometrist-kv-buffer-name)
|
||||
(chronometrist-refresh))
|
||||
|
||||
(defun chronometrist-out (&optional _prefix)
|
||||
"Record current moment as stop time to last s-exp in `chronometrist-file'.
|
||||
PLIST is a property list containing any other information about
|
||||
this time interval that should be recorded.
|
||||
|
||||
PREFIX is ignored."
|
||||
(interactive "P")
|
||||
(let ((buffer (find-file-noselect chronometrist-file)))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(unless (bobp) (insert "\n"))
|
||||
(backward-list 1)
|
||||
(--> (read buffer)
|
||||
(plist-put it :stop (chronometrist-format-time-iso8601))
|
||||
(progn
|
||||
(backward-list 1)
|
||||
(chronometrist-delete-list)
|
||||
(chronometrist-plist-pp it buffer)))
|
||||
(save-buffer))))
|
||||
|
||||
(provide 'chronometrist-sexp)
|
||||
(provide 'chronometrist-key-values)
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist"
|
||||
;; End:
|
||||
|
||||
;;; chronometrist-sexp.el ends here
|
||||
;;; chronometrist-key-values.el ends here
|
|
@ -0,0 +1,87 @@
|
|||
;;; 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
|
|
@ -57,20 +57,15 @@ information (see (info \"(elisp)Time Conversion\"))."
|
|||
|
||||
(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 day is a time value (see (info \"(elisp)Time of Day\")).
|
||||
Each date is a ts struct (see `ts.el').
|
||||
|
||||
FIRST-DATE-IN-WEEK must be a time value representing the first date."
|
||||
(--> '(0 1 2 3 4 5 6)
|
||||
;; 1 day = 86400 seconds
|
||||
(--map (* 86400 it) it)
|
||||
(--map (list
|
||||
(car first-date-in-week)
|
||||
(+ (cadr first-date-in-week) it))
|
||||
it)))
|
||||
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 calendrical information (see (info \"(elisp)Time Conversion\")).
|
||||
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
|
||||
|
@ -81,32 +76,21 @@ The first date is the first occurrence of
|
|||
|
||||
(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
|
||||
(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)
|
||||
(mapcar (lambda (task)
|
||||
(let ((task-daily-time-list
|
||||
(--map (chronometrist-task-time-one-day task
|
||||
(chronometrist-date it))
|
||||
week-dates)))
|
||||
(list task
|
||||
(vconcat
|
||||
(vector task)
|
||||
(->> task-daily-time-list
|
||||
(mapcar #'chronometrist-format-time)
|
||||
(apply #'vector))
|
||||
(->> task-daily-time-list
|
||||
(-reduce #'chronometrist-time-add)
|
||||
(chronometrist-format-time)
|
||||
(vector))))))
|
||||
chronometrist-task-list)))
|
||||
|
||||
(defun chronometrist-report-format-date (format-string time-date)
|
||||
"Extract date from TIME-DATE and format it according to FORMAT-STRING."
|
||||
(->> time-date
|
||||
(-take 6)
|
||||
(-drop 3)
|
||||
(reverse)
|
||||
(apply #'format format-string)))
|
||||
(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.
|
||||
|
@ -122,50 +106,39 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
|
|||
(defun chronometrist-report-print-non-tabular ()
|
||||
"Print the non-tabular part of the buffer in `chronometrist-report'."
|
||||
(let ((inhibit-read-only t)
|
||||
(w "\n "))
|
||||
(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 #'chronometrist-date
|
||||
(insert (mapconcat (lambda (ts)
|
||||
(ts-format "%F" ts))
|
||||
(chronometrist-report-date->week-dates)
|
||||
" "))
|
||||
(insert "\n")
|
||||
(goto-char (point-max))
|
||||
(insert w (format "%- 21s" "Total"))
|
||||
(let ((total-time-daily (->> chronometrist-report--ui-week-dates
|
||||
(mapcar #'chronometrist-date)
|
||||
(mapcar #'chronometrist-active-time-one-day))))
|
||||
(->> total-time-daily
|
||||
(mapcar #'chronometrist-format-time)
|
||||
(--map (format "% 9s " it))
|
||||
(apply #'insert))
|
||||
(->> total-time-daily
|
||||
(-reduce #'chronometrist-time-add)
|
||||
(chronometrist-format-time)
|
||||
(format "% 13s")
|
||||
(insert)))
|
||||
|
||||
(->> 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-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-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)
|
||||
(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-file)
|
||||
(insert-text-button "open log file"
|
||||
'action #'chronometrist-open-file
|
||||
'follow-link t)))
|
||||
(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'."
|
||||
|
@ -189,7 +162,7 @@ Argument _FS-EVENT is ignored."
|
|||
|
||||
(defvar chronometrist-report-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "l") #'chronometrist-open-file)
|
||||
(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
|
||||
|
@ -255,7 +228,7 @@ current week. Otherwise, display data from the week specified by
|
|||
(t (delete-other-windows)
|
||||
(unless keep-date
|
||||
(setq chronometrist-report--ui-date nil))
|
||||
(chronometrist-common-create-chronometrist-file)
|
||||
(chronometrist-common-create-file)
|
||||
(chronometrist-report-mode)
|
||||
(switch-to-buffer buffer)
|
||||
(chronometrist-report-refresh-file nil)
|
||||
|
@ -269,12 +242,10 @@ With prefix argument ARG, move back ARG weeks."
|
|||
(abs arg)
|
||||
1)))
|
||||
(setq chronometrist-report--ui-date
|
||||
(thread-first (if chronometrist-report--ui-date
|
||||
(parse-iso8601-time-string
|
||||
(chronometrist-iso-date->timestamp chronometrist-report--ui-date))
|
||||
(current-time))
|
||||
(time-subtract `(0 ,(* 7 arg 86400)))
|
||||
(chronometrist-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))
|
||||
|
@ -287,12 +258,10 @@ With prefix argument ARG, move forward ARG weeks."
|
|||
(abs arg)
|
||||
1)))
|
||||
(setq chronometrist-report--ui-date
|
||||
(thread-first (if chronometrist-report--ui-date
|
||||
(parse-iso8601-time-string
|
||||
(chronometrist-iso-date->timestamp chronometrist-report--ui-date))
|
||||
(current-time))
|
||||
(time-add `(0 ,(* 7 arg 86400)))
|
||||
(chronometrist-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)))
|
|
@ -0,0 +1,139 @@
|
|||
;;; 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))
|
||||
`(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
|
|
@ -64,9 +64,10 @@
|
|||
|
||||
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.
|
||||
: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'.
|
||||
|
@ -74,7 +75,7 @@ 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 dates in the form (YEAR MONTH DAY).")
|
||||
displayed. They must be ts structs (see `ts.el').")
|
||||
|
||||
(defvar chronometrist-statistics--point nil)
|
||||
|
||||
|
@ -82,32 +83,30 @@ displayed. They must be dates in the form (YEAR MONTH DAY).")
|
|||
|
||||
;; ## FUNCTIONS ##
|
||||
|
||||
(defun chronometrist-statistics-count-average-time-spent (task &optional table)
|
||||
(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 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 ((table (if table table chronometrist-events))
|
||||
(days 0)
|
||||
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 key)))
|
||||
(let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
|
||||
(when events-in-day
|
||||
(setq days (1+ days))
|
||||
(->> events-in-day
|
||||
(chronometrist-events->time-list)
|
||||
(chronometrist-time-list->sum-of-intervals)
|
||||
(->> (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
|
||||
(--> per-day-time-list
|
||||
(-reduce #'time-add it)
|
||||
(cadr it)
|
||||
(--> (-reduce #'+ per-day-time-list)
|
||||
(/ it days))
|
||||
0)))
|
||||
|
||||
|
@ -130,7 +129,6 @@ reduced to the desired range using
|
|||
"-"
|
||||
active-days)))
|
||||
(average-time (->> (chronometrist-statistics-count-average-time-spent task table)
|
||||
(chronometrist-seconds-to-hms)
|
||||
(chronometrist-format-time)
|
||||
(format "% 5s")))
|
||||
(content (vector task
|
||||
|
@ -148,17 +146,14 @@ reduced to the desired range using
|
|||
('week
|
||||
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
|
||||
(end (plist-get chronometrist-statistics--ui-state :end))
|
||||
(table (chronometrist-events-subset start end)))
|
||||
(chronometrist-statistics-entries-internal table)))
|
||||
(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-long (chronometrist-previous-week-start (chronometrist-date)))
|
||||
(end-long (time-add start-long
|
||||
(* chronometrist-seconds-in-day 7)))
|
||||
(start (chronometrist-calendrical->date start-long))
|
||||
(end (chronometrist-calendrical->date end-long))
|
||||
(table (chronometrist-events-subset start end)))
|
||||
(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 table)))))
|
||||
(chronometrist-statistics-entries-internal ht)))))
|
||||
|
||||
(defun chronometrist-statistics-print-keybind (command &optional description firstonly)
|
||||
"Insert the keybindings for COMMAND.
|
||||
|
@ -171,11 +166,6 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
" - "
|
||||
(if description description "")))
|
||||
|
||||
(defun chronometrist-statistics-format-date (date)
|
||||
"Return DATE (YEAR MONTH DAY) as a string in the form \"YYYY-MM-DD\"."
|
||||
(-let [(year month day) date]
|
||||
(format "%04d-%02d-%02d" year month day)))
|
||||
|
||||
(defun chronometrist-statistics-print-non-tabular ()
|
||||
"Print the non-tabular part of the buffer in `chronometrist-statistics'."
|
||||
(let ((w "\n ")
|
||||
|
@ -189,8 +179,8 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
(insert ", from")
|
||||
(insert
|
||||
(format " %s to %s\n"
|
||||
(plist-get chronometrist-statistics--ui-state :start)
|
||||
(plist-get chronometrist-statistics--ui-state :end)))))
|
||||
(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.
|
||||
|
@ -211,7 +201,7 @@ value of `revert-buffer-function'."
|
|||
|
||||
(defvar chronometrist-statistics-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "l") #'chronometrist-open-file)
|
||||
(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)
|
||||
|
@ -258,22 +248,19 @@ 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 (time-add week-start
|
||||
`(0 ,(* 6 chronometrist-seconds-in-day))))
|
||||
(week-start-iso (chronometrist-date week-start))
|
||||
(week-end-iso (chronometrist-date week-end)))
|
||||
(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-iso
|
||||
:end ,week-end-iso)))
|
||||
(chronometrist-common-create-chronometrist-file)
|
||||
(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))))))
|
||||
|
@ -283,24 +270,16 @@ specified by `chronometrist-statistics--ui-state'."
|
|||
|
||||
If ARG is a numeric argument, go back that many times."
|
||||
(interactive "P")
|
||||
(let* ((arg (if (and arg (numberp arg))
|
||||
(abs arg)
|
||||
1))
|
||||
(start-unix (->> (plist-get chronometrist-statistics--ui-state :start)
|
||||
(chronometrist-iso-date->timestamp)
|
||||
(parse-iso8601-time-string))))
|
||||
(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 (time-subtract start-unix
|
||||
(* 7 chronometrist-seconds-in-day arg)))
|
||||
(new-end (time-add new-start
|
||||
(* 6 chronometrist-seconds-in-day))))
|
||||
(plist-put chronometrist-statistics--ui-state
|
||||
:start
|
||||
(chronometrist-date new-start))
|
||||
(plist-put chronometrist-statistics--ui-state
|
||||
:end
|
||||
(chronometrist-date new-end)))))
|
||||
(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)))
|
||||
|
@ -313,21 +292,13 @@ If ARG is a numeric argument, go forward that many times."
|
|||
(let* ((arg (if (and arg (numberp arg))
|
||||
(abs arg)
|
||||
1))
|
||||
(start-unix (->> (plist-get chronometrist-statistics--ui-state :start)
|
||||
(chronometrist-iso-date->timestamp)
|
||||
(parse-iso8601-time-string))))
|
||||
(start (plist-get chronometrist-statistics--ui-state :start)))
|
||||
(cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
||||
('week
|
||||
(let* ((new-start (time-add start-unix
|
||||
(* 7 chronometrist-seconds-in-day arg)))
|
||||
(new-end (time-add new-start
|
||||
(* 6 chronometrist-seconds-in-day))))
|
||||
(plist-put chronometrist-statistics--ui-state
|
||||
:start
|
||||
(chronometrist-date new-start))
|
||||
(plist-put chronometrist-statistics--ui-state
|
||||
:end
|
||||
(chronometrist-date new-end)))))
|
||||
(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)))
|
|
@ -0,0 +1,115 @@
|
|||
;;; 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)
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist"
|
||||
;; End:
|
||||
|
||||
;;; chronometrist-time.el ends here
|
|
@ -18,7 +18,7 @@
|
|||
(require 'chronometrist-custom)
|
||||
(require 'chronometrist-statistics-custom)
|
||||
(require 'chronometrist-report-custom)
|
||||
(require 'chronometrist-common)
|
||||
(require 'chronometrist)
|
||||
|
||||
(declare-function chronometrist-refresh "chronometrist.el")
|
||||
(declare-function chronometrist-report-refresh "chronometrist-report.el")
|
|
@ -12,13 +12,11 @@
|
|||
(require 'subr-x)
|
||||
|
||||
(require 'chronometrist-common)
|
||||
(require 'chronometrist-timer)
|
||||
(require 'chronometrist-custom)
|
||||
(require 'chronometrist-report)
|
||||
(require 'chronometrist-statistics)
|
||||
(require 'chronometrist-sexp)
|
||||
(require 'chronometrist-key-values)
|
||||
(require 'chronometrist-queries)
|
||||
(require 'chronometrist-migrate)
|
||||
(require 'chronometrist-sexp)
|
||||
|
||||
;; This is free and unencumbered software released into the public domain.
|
||||
;;
|
||||
|
@ -62,15 +60,43 @@
|
|||
;; ## VARIABLES ##
|
||||
;;; Code:
|
||||
|
||||
;; `chronometrist-goals' is an optional extension. But even these don't make the
|
||||
;; warnings go away :\
|
||||
(defvar chronometrist-goals-list)
|
||||
(declare-function 'chronometrist-get-goal "chronometrist-goals")
|
||||
|
||||
(autoload 'chronometrist-maybe-start-timer "chronometrist-timer" nil t)
|
||||
(autoload 'chronometrist-report "chronometrist-report" nil t)
|
||||
(autoload 'chronometrist-statistics "chronometrist-statistics" nil t)
|
||||
|
||||
(defvar chronometrist--task-history nil)
|
||||
(defvar chronometrist--point nil)
|
||||
(defvar chronometrist--inhibit-read-p nil)
|
||||
(defvar chronometrist-mode-map)
|
||||
|
||||
;; ## FUNCTIONS ##
|
||||
(defun chronometrist-open-log (&optional _button)
|
||||
"Open `chronometrist-file' in another window.
|
||||
|
||||
Argument _BUTTON is for the purpose of using this command as a
|
||||
button action."
|
||||
(interactive)
|
||||
(chronometrist-sexp-open-log))
|
||||
|
||||
(defun chronometrist-common-create-file ()
|
||||
"Create `chronometrist-file' if it doesn't already exist."
|
||||
(chronometrist-sexp-create-file))
|
||||
|
||||
(defun chronometrist-task-active? (task)
|
||||
"Return t if TASK is currently clocked in, else nil."
|
||||
(equal (chronometrist-current-task) task))
|
||||
|
||||
(defun chronometrist-use-goals? ()
|
||||
"Return t if `chronometrist-goals' is available and
|
||||
`chronometrist-goals-list' is bound."
|
||||
(and (featurep 'chronometrist-goals)
|
||||
(bound-and-true-p chronometrist-goals-list)))
|
||||
|
||||
(defun chronometrist-activity-indicator ()
|
||||
"Return a string to indicate that a task is active.
|
||||
See custom variable `chronometrist-activity-indicator'."
|
||||
|
@ -88,16 +114,26 @@ See custom variable `chronometrist-activity-indicator'."
|
|||
(->> chronometrist-task-list
|
||||
(-sort #'string-lessp)
|
||||
(--map-indexed
|
||||
(list it
|
||||
(vector (number-to-string (1+ it-index))
|
||||
(list it
|
||||
'action 'chronometrist-toggle-task-button
|
||||
'follow-link t)
|
||||
(-> (chronometrist-task-time-one-day it)
|
||||
(chronometrist-format-time))
|
||||
(if (chronometrist-task-active? it)
|
||||
(chronometrist-activity-indicator)
|
||||
""))))))
|
||||
(let* ((task it)
|
||||
(index (number-to-string (1+ it-index)))
|
||||
(task-button (list task
|
||||
'action 'chronometrist-toggle-task-button
|
||||
'follow-link t))
|
||||
(task-time (chronometrist-format-time (chronometrist-task-time-one-day task)))
|
||||
(indicator (if (chronometrist-task-active? task)
|
||||
(chronometrist-activity-indicator)
|
||||
""))
|
||||
(use-goals (chronometrist-use-goals?))
|
||||
(target (when use-goals
|
||||
;; this can return nil if there is no goal for a task
|
||||
(chronometrist-get-goal task)))
|
||||
(target-str (if target
|
||||
(format "% 4d" target)
|
||||
"")))
|
||||
(list task
|
||||
(vconcat (vector index task-button task-time indicator)
|
||||
(when use-goals
|
||||
(vector target-str))))))))
|
||||
|
||||
(defun chronometrist-task-at-point ()
|
||||
"Return the task at point in the `chronometrist' buffer, or nil if there is no task at point."
|
||||
|
@ -115,8 +151,7 @@ See custom variable `chronometrist-activity-indicator'."
|
|||
(defun chronometrist-goto-last-task ()
|
||||
"In the `chronometrist' buffer, move point to the line containing the last active task."
|
||||
(goto-char (point-min))
|
||||
;; FIXME
|
||||
;; (re-search-forward timeclock-last-project nil t)
|
||||
(re-search-forward (plist-get (chronometrist-last) :name) nil t)
|
||||
(beginning-of-line))
|
||||
|
||||
(defun chronometrist-print-keybind (command &optional description firstonly)
|
||||
|
@ -138,45 +173,24 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
|||
(w "\n ")
|
||||
;; (keybind-start-new (chronometrist-format-keybinds 'chronometrist-add-new-task
|
||||
;; chronometrist-mode-map))
|
||||
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task
|
||||
chronometrist-mode-map
|
||||
t)))
|
||||
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
|
||||
(goto-char (point-max))
|
||||
(-->
|
||||
(chronometrist-active-time-one-day)
|
||||
(chronometrist-format-time it)
|
||||
(format "%s%- 26s%s" w "Total" it)
|
||||
(insert it))
|
||||
|
||||
(insert "\n")
|
||||
(insert w (format "% 17s" "Keys")
|
||||
w (format "% 17s" "----"))
|
||||
|
||||
(insert w (format "% 17s" "Keys") w (format "% 17s" "----"))
|
||||
(chronometrist-print-keybind 'chronometrist-add-new-task)
|
||||
(insert-text-button "start a new task"
|
||||
'action #'chronometrist-add-new-task-button
|
||||
'follow-link t)
|
||||
|
||||
(chronometrist-print-keybind 'chronometrist-toggle-task
|
||||
"toggle task at point")
|
||||
|
||||
(chronometrist-print-keybind 'chronometrist-toggle-task-no-hooks
|
||||
"toggle without running hooks")
|
||||
|
||||
(insert "\n " (format "%s %s - %s"
|
||||
"<numeric argument N>"
|
||||
keybind-toggle
|
||||
"toggle <N>th task"))
|
||||
|
||||
(insert-text-button "start a new task" 'action #'chronometrist-add-new-task-button 'follow-link t)
|
||||
(chronometrist-print-keybind 'chronometrist-toggle-task "toggle task at point")
|
||||
(chronometrist-print-keybind 'chronometrist-toggle-task-no-hooks "toggle without running hooks")
|
||||
(insert "\n " (format "%s %s - %s" "<numeric argument N>" keybind-toggle "toggle <N>th task"))
|
||||
(chronometrist-print-keybind 'chronometrist-report)
|
||||
(insert-text-button "see weekly report"
|
||||
'action #'chronometrist-report
|
||||
'follow-link t)
|
||||
|
||||
(chronometrist-print-keybind 'chronometrist-open-file)
|
||||
(insert-text-button "open log file"
|
||||
'action #'chronometrist-open-file
|
||||
'follow-link t)
|
||||
(insert-text-button "see weekly report" 'action #'chronometrist-report 'follow-link t)
|
||||
(chronometrist-print-keybind 'chronometrist-open-log)
|
||||
(insert-text-button "view/edit log file" 'action #'chronometrist-open-log 'follow-link t)
|
||||
(insert "\n"))))
|
||||
|
||||
(defun chronometrist-goto-nth-task (n)
|
||||
|
@ -207,22 +221,43 @@ value of `revert-buffer-function'."
|
|||
"Re-read `chronometrist-file' and refresh the `chronometrist' buffer.
|
||||
Argument _FS-EVENT is ignored."
|
||||
;; (chronometrist-file-clean)
|
||||
(chronometrist-events-populate)
|
||||
(setq chronometrist-task-list (chronometrist-tasks-from-table))
|
||||
(chronometrist-tags-history-populate)
|
||||
(run-hooks 'chronometrist-file-change-hook)
|
||||
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
|
||||
(if chronometrist--inhibit-read-p
|
||||
(setq chronometrist--inhibit-read-p nil)
|
||||
(chronometrist-events-populate)
|
||||
(setq chronometrist-task-list (chronometrist-tasks-from-table))
|
||||
(chronometrist-tags-history-populate))
|
||||
(chronometrist-key-history-populate)
|
||||
(chronometrist-value-history-populate)
|
||||
(chronometrist-refresh))
|
||||
|
||||
(defun chronometrist-query-stop ()
|
||||
"Ask the user if they would like to clock out."
|
||||
(interactive)
|
||||
(let ((task (chronometrist-current-task)))
|
||||
(and task
|
||||
(yes-or-no-p (concat "Stop tracking time for " task "? "))
|
||||
(chronometrist-out))
|
||||
t))
|
||||
|
||||
(defun chronometrist-in (task &optional _prefix)
|
||||
"Clock in to TASK; record current time in `chronometrist-file'.
|
||||
TASK is the name of the task, a string.
|
||||
|
||||
PREFIX is ignored."
|
||||
(interactive "P")
|
||||
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
|
||||
(chronometrist-sexp-new plist)
|
||||
(chronometrist-refresh)))
|
||||
|
||||
(defun chronometrist-out (&optional _prefix)
|
||||
"Record current moment as stop time to last s-exp in `chronometrist-file'.
|
||||
PREFIX is ignored."
|
||||
(interactive "P")
|
||||
(let ((plist (plist-put (chronometrist-last) :stop
|
||||
(chronometrist-format-time-iso8601))))
|
||||
(chronometrist-sexp-replace-last plist)))
|
||||
|
||||
;; ## HOOKS ##
|
||||
|
||||
(defvar chronometrist-before-in-functions nil
|
||||
|
@ -256,6 +291,9 @@ return a non-nil value.")
|
|||
Each function in this hook must accept a single argument, which
|
||||
is the name of the task to be clocked out of.")
|
||||
|
||||
(defvar chronometrist-file-change-hook nil
|
||||
"Functions to be run after `chronometrist-file' is changed on disk.")
|
||||
|
||||
(defun chronometrist-run-functions-and-clock-in (task)
|
||||
"Run hooks and clock in to TASK."
|
||||
(run-hook-with-args 'chronometrist-before-in-functions task)
|
||||
|
@ -273,7 +311,7 @@ is the name of the task to be clocked out of.")
|
|||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "RET") #'chronometrist-toggle-task)
|
||||
(define-key map (kbd "M-RET") #'chronometrist-toggle-task-no-hooks)
|
||||
(define-key map (kbd "l") #'chronometrist-open-file)
|
||||
(define-key map (kbd "l") #'chronometrist-open-log)
|
||||
(define-key map (kbd "r") #'chronometrist-report)
|
||||
(define-key map [mouse-1] #'chronometrist-toggle-task)
|
||||
(define-key map [mouse-3] #'chronometrist-toggle-task-no-hooks)
|
||||
|
@ -284,10 +322,13 @@ is the name of the task to be clocked out of.")
|
|||
(define-derived-mode chronometrist-mode tabulated-list-mode "Chronometrist"
|
||||
"Major mode for `chronometrist'."
|
||||
(make-local-variable 'tabulated-list-format)
|
||||
(setq tabulated-list-format [("#" 3 t)
|
||||
("Task" 25 t)
|
||||
("Time" 10 t)
|
||||
("Active" 3 t)])
|
||||
(setq tabulated-list-format
|
||||
(vconcat [("#" 3 t)
|
||||
("Task" 25 t)
|
||||
("Time" 10 t)
|
||||
("Active" 10 t)]
|
||||
(when (chronometrist-use-goals?)
|
||||
[("Target" 3 t)])))
|
||||
(make-local-variable 'tabulated-list-entries)
|
||||
(setq tabulated-list-entries 'chronometrist-entries)
|
||||
(make-local-variable 'tabulated-list-sort-key)
|
||||
|
@ -303,6 +344,8 @@ is the name of the task to be clocked out of.")
|
|||
|
||||
Argument _BUTTON is for the purpose of using this as a button
|
||||
action, and is ignored."
|
||||
(when current-prefix-arg
|
||||
(chronometrist-goto-nth-task (prefix-numeric-value current-prefix-arg)))
|
||||
(let ((current (chronometrist-current-task))
|
||||
(at-point (chronometrist-task-at-point)))
|
||||
;; clocked in + point on current = clock out
|
||||
|
@ -391,7 +434,8 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
|||
(interactive "P")
|
||||
(chronometrist-migrate-check)
|
||||
(let ((buffer (get-buffer-create chronometrist-buffer-name))
|
||||
(w (get-buffer-window chronometrist-buffer-name t)))
|
||||
(w (save-excursion
|
||||
(get-buffer-window chronometrist-buffer-name t))))
|
||||
(cond
|
||||
(arg (cl-case arg
|
||||
(1 (chronometrist-report))
|
||||
|
@ -403,7 +447,7 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
|
|||
(cond ((or (not (file-exists-p chronometrist-file))
|
||||
(chronometrist-common-file-empty-p chronometrist-file))
|
||||
;; first run
|
||||
(chronometrist-common-create-chronometrist-file)
|
||||
(chronometrist-common-create-file)
|
||||
(let ((inhibit-read-only t))
|
||||
(chronometrist-common-clear-buffer buffer)
|
||||
(insert "Welcome to Chronometrist! Hit RET to ")
|
|
@ -3,29 +3,29 @@ Feature: Point restore in Chronometrist
|
|||
# What about when we want point to be on the last project instead? In which situations should that happen?
|
||||
|
||||
Background:
|
||||
Given I open Chronometrist
|
||||
And I go to a random point in the buffer
|
||||
Given I open Chronometrist
|
||||
And I go to a random point in the buffer
|
||||
|
||||
Scenario: Simple re-open 1
|
||||
When I kill its buffer
|
||||
And I open it again
|
||||
Then the position of point should be preserved
|
||||
|
||||
Scenario: Simple re-open 2
|
||||
When I toggle its buffer
|
||||
And I open it again
|
||||
Then the position of point should be preserved
|
||||
Scenario: Simple re-open 2
|
||||
When I toggle its buffer
|
||||
And I open it again
|
||||
Then the position of point should be preserved
|
||||
|
||||
Scenario: Timer with buffer current
|
||||
When buffer is current
|
||||
Then the timer should preserve the position of point
|
||||
Scenario: Timer with buffer current
|
||||
When buffer is current
|
||||
Then the timer should preserve the position of point
|
||||
|
||||
Scenario: Timer with buffer not current, but visible
|
||||
When buffer is not current
|
||||
But it is visible in another window
|
||||
Then the timer should preserve the position of point
|
||||
Scenario: Timer with buffer not current, but visible
|
||||
When buffer is not current
|
||||
But it is visible in another window
|
||||
Then the timer should preserve the position of point
|
||||
|
||||
Scenario: Previous/next week keys
|
||||
When I open chronometrist-report
|
||||
And I view the previous/next week
|
||||
Then the position of point should be preserved
|
||||
Scenario: Previous/next week keys
|
||||
When I open chronometrist-report
|
||||
And I view the previous/next week
|
||||
Then the position of point should be preserved
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
;;; -*- lexical-binding: t; -*-
|
||||
(require 'buttercup)
|
||||
(require 'chronometrist-common)
|
||||
|
||||
(describe
|
||||
"chronometrist-format-time"
|
||||
(it "works with lists"
|
||||
(expect (chronometrist-format-time 0)
|
||||
:to-equal " -")
|
||||
(expect (chronometrist-format-time 1)
|
||||
:to-equal " 1")
|
||||
(expect (chronometrist-format-time 10)
|
||||
:to-equal " 10")
|
||||
(expect (chronometrist-format-time 70)
|
||||
:to-equal " 1:10")
|
||||
(expect (chronometrist-format-time (+ (* 10 60) ;; 10 minutes
|
||||
10)) ;; 10 seconds
|
||||
:to-equal " 10:10")
|
||||
(expect (chronometrist-format-time (+ (* 1 60 60) ;; 1 hour
|
||||
(* 10 60) ;; 10 minutes
|
||||
10)) ;; 10 seconds
|
||||
:to-equal " 1:10:10")
|
||||
(expect (chronometrist-format-time (+ (* 10 60 60) ;; 10 hours
|
||||
(* 10 60) ;; 10 minutes
|
||||
10)) ;; 10 seconds
|
||||
:to-equal "10:10:10")))
|
||||
|
||||
(describe
|
||||
"chronometrist-previous-week-start"
|
||||
:var ((chronometrist-report-week-start-day "Sunday")
|
||||
(ts (chronometrist-iso-date->ts "2018-09-02")))
|
||||
(it "should work with Sundays"
|
||||
(should (ts= (chronometrist-previous-week-start
|
||||
(chronometrist-iso-date->ts "2018-09-01"))
|
||||
(chronometrist-iso-date->ts "2018-08-26")))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-02"))))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-03"))))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-04"))))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-05"))))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-06"))))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-07"))))
|
||||
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-08"))))))
|
|
@ -1,5 +1,5 @@
|
|||
(require 'buttercup)
|
||||
(require 'chronometrist-sexp)
|
||||
(require 'chronometrist-key-values)
|
||||
|
||||
(describe
|
||||
"chronometrist-plist-remove"
|
||||
|
@ -34,6 +34,22 @@
|
|||
:d :a)
|
||||
:to-equal '(:b 2 :c 3))))
|
||||
|
||||
(describe
|
||||
"chronometrist-key-history"
|
||||
(before-all
|
||||
(setq chronometrist-file "tests/test.sexp")
|
||||
(chronometrist-events-populate)
|
||||
(setq chronometrist-task-list (chronometrist-tasks-from-table))
|
||||
(chronometrist-key-history-populate))
|
||||
(it "should have 6 keys"
|
||||
(expect (hash-table-count chronometrist-key-history)
|
||||
:to-be 6))
|
||||
(it "should store multiple values"
|
||||
(expect (length (gethash "Programming" chronometrist-key-history))
|
||||
:to-be 3)
|
||||
(expect (length (gethash "Arrangement/new edition" chronometrist-key-history))
|
||||
:to-be 2)))
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist"
|
||||
;; End:
|
|
@ -0,0 +1,41 @@
|
|||
;; -*- lexical-binding: t; -*-
|
||||
(require 'buttercup)
|
||||
(require 'ts)
|
||||
(require 'chronometrist-sexp)
|
||||
(require 'chronometrist-events)
|
||||
(require 'chronometrist-queries)
|
||||
(require 'chronometrist-time)
|
||||
|
||||
|
||||
(describe "chronometrist-task-time-one-day"
|
||||
:var ((ts-1 (chronometrist-iso-date->ts "2018-01-01"))
|
||||
(ts-2 (chronometrist-iso-date->ts "2018-01-02"))
|
||||
(ts-3 (chronometrist-iso-date->ts "2018-01-03"))
|
||||
(1-hour 3600))
|
||||
(before-all
|
||||
(setq chronometrist-file-old chronometrist-file
|
||||
chronometrist-file "tests/test.sexp")
|
||||
(chronometrist-events-populate))
|
||||
(after-all
|
||||
(setq chronometrist-file chronometrist-file-old))
|
||||
(it "returns the time spent in one day, in seconds"
|
||||
(expect (chronometrist-task-time-one-day "Programming" ts-1)
|
||||
:to-equal 1-hour)
|
||||
(expect (chronometrist-task-time-one-day "Swimming" ts-1)
|
||||
:to-equal 1-hour)
|
||||
(expect (chronometrist-task-time-one-day "Cooking" ts-1)
|
||||
:to-equal 1-hour)
|
||||
(expect (chronometrist-task-time-one-day "Guitar" ts-1)
|
||||
:to-equal 1-hour)
|
||||
(expect (chronometrist-task-time-one-day "Cycling" ts-1)
|
||||
:to-equal 1-hour))
|
||||
|
||||
(it "works with midnight-crossing events"
|
||||
(expect (chronometrist-task-time-one-day "Programming" ts-2)
|
||||
:to-equal 1-hour)
|
||||
(expect (chronometrist-task-time-one-day "Programming" ts-3)
|
||||
:to-equal 1-hour)))
|
||||
|
||||
;; Local Variables:
|
||||
;; nameless-current-name: "chronometrist"
|
||||
;; End:
|
|
@ -0,0 +1,57 @@
|
|||
(: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")
|
Reference in New Issue