|
|
|
@ -95,7 +95,7 @@ One of the earliest 'optimizations' of great importance turned out to simply be
|
|
|
|
|
|
|
|
|
|
*** Preserve hash table state for some commands
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: preserve-hash-table-state-for-some-commands
|
|
|
|
|
:CUSTOM_ID: preserve-hash-table-state-some-commands
|
|
|
|
|
:END:
|
|
|
|
|
NOTE - this has been replaced with a more general optimization - see next section.
|
|
|
|
|
|
|
|
|
@ -153,14 +153,14 @@ There are a few different approaches of dealing with them. (Currently, Chronomet
|
|
|
|
|
*** Check the code of the first event of the day (timeclock format)
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:DESCRIPTION: When the code of the first event in the day is "o", it's a midnight-spanning event.
|
|
|
|
|
:CUSTOM_ID: check-the-code-of-the-first-event-of-the-day-(timeclock-format)
|
|
|
|
|
:CUSTOM_ID: check-code-of-first-event-of-day
|
|
|
|
|
:END:
|
|
|
|
|
+ Advantage - very simple to detect
|
|
|
|
|
+ Disadvantage - "in" and "out" events must be represented separately
|
|
|
|
|
|
|
|
|
|
*** Split them at the file level
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: split-them-at-the-file-level
|
|
|
|
|
:CUSTOM_ID: split-in-file
|
|
|
|
|
:END:
|
|
|
|
|
+ Advantage - operation is performed only once for each such event + simpler data-consuming code + reduced post-parsing load.
|
|
|
|
|
+ What happens when the user changes their day-start-time? The split-up events are now split wrongly, and the second event may get split /again./
|
|
|
|
@ -176,14 +176,14 @@ This strategy is implemented in the [[#program-backend-plist-group][plist-group]
|
|
|
|
|
|
|
|
|
|
*** Split them at the hash-table-level
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: split-them-at-the-hash-table-level
|
|
|
|
|
:CUSTOM_ID: split-in-hash-table
|
|
|
|
|
:END:
|
|
|
|
|
Handled by ~sexp-events-populate~
|
|
|
|
|
+ Advantage - simpler data-consuming code.
|
|
|
|
|
|
|
|
|
|
*** Split them at the data-consumer level (e.g. when calculating time for one day/getting events for one day)
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: split-them-at-the-data-consumer-level-(e.g.-when-calculating-time-for-one-day-getting-events-for-one-day)
|
|
|
|
|
:CUSTOM_ID: split-at-data-consumer-level
|
|
|
|
|
:END:
|
|
|
|
|
+ Advantage - reduced repetitive post-parsing load.
|
|
|
|
|
|
|
|
|
@ -380,7 +380,7 @@ There are many operations which are file-oriented, whereas I have tried to treat
|
|
|
|
|
|
|
|
|
|
** generic loop interface for iterating over records
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: generic-loop-interface-for-iterating-over-records
|
|
|
|
|
:CUSTOM_ID: generic-loop-interface-iterating-over-records
|
|
|
|
|
:END:
|
|
|
|
|
Of all the ways to work with Chronometrist data, both as part of the program and as part of my occasional "queries", my favorite was to use =cl-loop=.
|
|
|
|
|
|
|
|
|
@ -403,7 +403,7 @@ The macro still exists in its non-generic form as =loop-sexp-file=, providing a
|
|
|
|
|
|
|
|
|
|
* How-to guides for maintainers
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: how-to-guides-for-maintainers
|
|
|
|
|
:CUSTOM_ID: how-to-guides-maintainers
|
|
|
|
|
:END:
|
|
|
|
|
** How to set up Emacs to contribute
|
|
|
|
|
:PROPERTIES:
|
|
|
|
@ -474,7 +474,7 @@ Use =org-babel= (=org-babel-tangle= / =org-babel-tangle-file=), /not/ =literate-
|
|
|
|
|
|
|
|
|
|
* The Program
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: the-program
|
|
|
|
|
:CUSTOM_ID: program
|
|
|
|
|
:END:
|
|
|
|
|
** chronometrist :package:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
@ -546,7 +546,7 @@ Use =org-babel= (=org-babel-tangle= / =org-babel-tangle-file=), /not/ =literate-
|
|
|
|
|
|
|
|
|
|
*** *user-configuration-file* :custom:variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: *user-configuration-file*
|
|
|
|
|
:CUSTOM_ID: user-configuration-file
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *user-configuration-file*
|
|
|
|
@ -555,7 +555,7 @@ Use =org-babel= (=org-babel-tangle= / =org-babel-tangle-file=), /not/ =literate-
|
|
|
|
|
|
|
|
|
|
*** *user-data-file* :custom:variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: *user-data-file*
|
|
|
|
|
:CUSTOM_ID: user-data-file
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *user-data-file* (merge-pathnames "chronometrist" *xdg-data-dir*)
|
|
|
|
@ -574,7 +574,7 @@ Use =org-babel= (=org-babel-tangle= / =org-babel-tangle-file=), /not/ =literate-
|
|
|
|
|
|
|
|
|
|
*** *day-start-time* :custom:variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: *day-start-time*
|
|
|
|
|
:CUSTOM_ID: day-start-time
|
|
|
|
|
:END:
|
|
|
|
|
=chronometrist-events-maybe-split= refers to this, but I'm not sure this has the desired effect at the moment—haven't even tried using it.
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
@ -822,8 +822,9 @@ treated as though their time is 00:00:00."
|
|
|
|
|
:CUSTOM_ID: task-duration-one-day
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defun task-duration-one-day (task &optional (date (timestamp-to-unix (today)))
|
|
|
|
|
(backend (active-backend)))
|
|
|
|
|
(defun task-duration-one-day (task &optional
|
|
|
|
|
(date (timestamp-to-unix (today)))
|
|
|
|
|
(backend (active-backend)))
|
|
|
|
|
"Return total time spent on TASK today or on DATE.
|
|
|
|
|
The return value is seconds, as an integer."
|
|
|
|
|
(let ((task-intervals (task-records-for-date backend task date)))
|
|
|
|
@ -867,7 +868,7 @@ which span midnights."
|
|
|
|
|
*** *task-list* :custom:variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:VALUE: list
|
|
|
|
|
:CUSTOM_ID: *task-list*
|
|
|
|
|
:CUSTOM_ID: task-list
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *task-list* nil
|
|
|
|
@ -1088,7 +1089,7 @@ The backend may use no files, a single file, or multiple files. Thus, =backend=
|
|
|
|
|
|
|
|
|
|
**** *backends-alist* :variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: *backends-alist*
|
|
|
|
|
:CUSTOM_ID: backends-alist
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *backends-alist* nil
|
|
|
|
@ -1100,7 +1101,7 @@ EIEIO object such as one returned by `make-instance'.")
|
|
|
|
|
|
|
|
|
|
**** *active-backend* :custom:variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: *active-backend*
|
|
|
|
|
:CUSTOM_ID: active-backend-variable
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *active-backend* :sqlite
|
|
|
|
@ -1111,7 +1112,7 @@ Value must be a keyword corresponding to a key in
|
|
|
|
|
|
|
|
|
|
**** active-backend :reader:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: active-backend
|
|
|
|
|
:CUSTOM_ID: active-backend-function
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defun active-backend ()
|
|
|
|
@ -1136,7 +1137,7 @@ be replaced."
|
|
|
|
|
|
|
|
|
|
**** task-list :function:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: task-list
|
|
|
|
|
:CUSTOM_ID: task-list-1
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defun task-list ()
|
|
|
|
@ -1360,7 +1361,7 @@ entire (unsplit) record must be returned."))
|
|
|
|
|
|
|
|
|
|
**** task-records-for-date :generic:function:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: task-records-for-date
|
|
|
|
|
:CUSTOM_ID: task-records-date
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(declaim (ftype (function (chronometrist:backend
|
|
|
|
@ -1382,7 +1383,7 @@ Return nil if BACKEND contains no records.")
|
|
|
|
|
|
|
|
|
|
***** task-records-for-date :before:method:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: task-records-for-date-1
|
|
|
|
|
:CUSTOM_ID: before-task-records-for-date
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
#+(or)
|
|
|
|
@ -1570,7 +1571,7 @@ These can be implemented in terms of the minimal protocol above.
|
|
|
|
|
|
|
|
|
|
*** Common definitions for s-expression backends
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: common-definitions-for-s-expression-backends
|
|
|
|
|
:CUSTOM_ID: common-definitions-sexp-backends
|
|
|
|
|
:END:
|
|
|
|
|
**** file-backend-mixin :mixin:class:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
@ -1806,7 +1807,7 @@ expression first)."
|
|
|
|
|
|
|
|
|
|
**** indices and hashes
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: indices-and-hashes
|
|
|
|
|
:CUSTOM_ID: indices-hashes
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defun rest-start (file)
|
|
|
|
@ -2533,7 +2534,7 @@ Return value is either a list in the form
|
|
|
|
|
|
|
|
|
|
***** task-records-for-date :reader:method:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: task-records-for-date-2
|
|
|
|
|
:CUSTOM_ID: plist-group-task-records-for-date
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defmethod chronometrist:task-records-for-date
|
|
|
|
@ -3019,26 +3020,27 @@ s-expressions in a text column.")
|
|
|
|
|
|
|
|
|
|
**** task-records-for-date
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: task-records-for-date-3
|
|
|
|
|
:CUSTOM_ID: sqlite-task-records-for-date
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defmethod chronometrist:task-records-for-date
|
|
|
|
|
((backend sqlite-backend) task date &key &allow-other-keys)
|
|
|
|
|
(let ((list (execute-sxql #'execute-to-list
|
|
|
|
|
(select (:name :start_time :stop_time :properties)
|
|
|
|
|
(from :intervals)
|
|
|
|
|
(left-join :interval_names :using (:name_id))
|
|
|
|
|
(left-join :properties :using (:prop_id))
|
|
|
|
|
(where (:and
|
|
|
|
|
(:in :interval_id
|
|
|
|
|
(select (:interval_id)
|
|
|
|
|
(from :date_intervals)
|
|
|
|
|
(where (:= :date_id
|
|
|
|
|
(select (:date_id)
|
|
|
|
|
(from :dates)
|
|
|
|
|
(where (:= :date date)))))))
|
|
|
|
|
(:= :name task))))
|
|
|
|
|
(backend-connection backend))))
|
|
|
|
|
(let ((list (execute-sxql
|
|
|
|
|
#'execute-to-list
|
|
|
|
|
(select (:name :start_time :stop_time :properties)
|
|
|
|
|
(from :intervals)
|
|
|
|
|
(left-join :interval_names :using (:name_id))
|
|
|
|
|
(left-join :properties :using (:prop_id))
|
|
|
|
|
(where (:and
|
|
|
|
|
(:in :interval_id
|
|
|
|
|
(select (:interval_id)
|
|
|
|
|
(from :date_intervals)
|
|
|
|
|
(where (:= :date_id
|
|
|
|
|
(select (:date_id)
|
|
|
|
|
(from :dates)
|
|
|
|
|
(where (:= :date date)))))))
|
|
|
|
|
(:= :name task))))
|
|
|
|
|
(backend-connection backend))))
|
|
|
|
|
(loop for (name start stop prop-string) in list
|
|
|
|
|
collect (make-interval name start stop prop-string))))
|
|
|
|
|
#+END_SRC
|
|
|
|
@ -3091,11 +3093,21 @@ s-expressions in a text column.")
|
|
|
|
|
(defpackage :chronometrist.clim
|
|
|
|
|
(:use :clim :clim-lisp)
|
|
|
|
|
(:import-from :local-time :now :today :timestamp-to-unix)
|
|
|
|
|
(:import-from :format-seconds :format-seconds)
|
|
|
|
|
(:import-from :chronometrist :task-list :task-duration-one-day)
|
|
|
|
|
(:export :run-chronometrist))
|
|
|
|
|
(in-package :chronometrist.clim)
|
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
**** *duration-format-string* :variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: duration-format-string
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *duration-format-string* "~2h:~2,'0m:~2,'0s"
|
|
|
|
|
"Format string used for durations, acceptable to `format-seconds'.")
|
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
**** chronometrist :application:frame:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: chronometrist-1
|
|
|
|
@ -3195,12 +3207,14 @@ FRAME and PANE are the CLIM frame and pane as passed to the display function."))
|
|
|
|
|
duration
|
|
|
|
|
frame pane)
|
|
|
|
|
(with-output-as-presentation (pane duration 'number)
|
|
|
|
|
(format t "~A" duration)))
|
|
|
|
|
(if (zerop duration)
|
|
|
|
|
(format t "~10@A" "-")
|
|
|
|
|
(format-seconds t *duration-format-string* duration))))
|
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
***** *task-duration-table-spec* :custom:variable:
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:CUSTOM_ID: *task-duration-table-spec*
|
|
|
|
|
:CUSTOM_ID: task-duration-table-spec
|
|
|
|
|
:END:
|
|
|
|
|
#+BEGIN_SRC lisp
|
|
|
|
|
(defvar *task-duration-table-spec*
|
|
|
|
|