Merge branch 'dev' into cl-port

This commit is contained in:
contrapunctus 2022-04-01 22:42:10 +05:30
commit 04ede947b4
5 changed files with 175 additions and 123 deletions

View File

@ -936,8 +936,20 @@ The plist backend could theoretically be extended to support it, but I'd rather
:PROPERTIES: :PROPERTIES:
:CREATED: 2022-01-08T23:32:37+0530 :CREATED: 2022-01-08T23:32:37+0530
:END: :END:
1. [ ] default suggested input backend should be the active backend 1. [ ] On first launch, offer list of supported backends to migrate from
2. [ ] conserve file local variables prop line for text backends + Some backends may not be installed - "see MELPA for more formats"
2. [ ] Use active backend as the suggested input backend
3. [ ] Conserve file local variables prop line for text backends
** make migrate command async [33%]
:PROPERTIES:
:CREATED: 2022-04-01T18:02:03+0530
:END:
=chronometrist-migrate= / =chronometrist-to-file= can take a long time to run, freezing Emacs until completion if run synchronously.
1. [X] basic async export
2. [ ] display message on start and completion
3. [ ] test the INPUT-FILE argument
* STARTED Support for the Third Time System [57%] :extension: * STARTED Support for the Third Time System [57%] :extension:
:PROPERTIES: :PROPERTIES:
@ -1042,3 +1054,9 @@ Similar to [[#verify-command][verify command]], perhaps, but this is really abou
:PROPERTIES: :PROPERTIES:
:CREATED: 2022-02-28T11:35:47+0530 :CREATED: 2022-02-28T11:35:47+0530
:END: :END:
* User-defined categories
:PROPERTIES:
:CREATED: 2022-03-31T22:44:34+0530
:END:
A task can be part of any number of categories. A category may also contain other categories. The categories, and the total time spent on each one, could be displayed in the main view, separate from the tasks.

View File

@ -96,7 +96,7 @@ Return the connection object from `emacsql-sqlite'."
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file) (cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(with-slots (connection) backend (with-slots (connection) backend
(delete-file file) (delete-file file)
(emacsql-close connection) (when connection (emacsql-close connection))
(setf connection nil) (setf connection nil)
(chronometrist-create-file backend file) (chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do (cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
@ -120,7 +120,10 @@ prop-id of the inserted or existing property."
(emacsql connection (emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]] [:insert-or-ignore-into properties [properties] :values [$s1]]
props) props)
(caar (emacsql connection [:select (funcall max prop-id) :from properties]))))) (caar (emacsql connection [:select [prop-id]
:from properties
:where (= properties $s1)]
props)))))
(defun chronometrist-sqlite-properties-to-json (plist) (defun chronometrist-sqlite-properties-to-json (plist)
"Return PLIST as a JSON string." "Return PLIST as a JSON string."
@ -156,22 +159,23 @@ s-expressions in a text column."
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start))) (date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))
(start-unix (chronometrist-iso-to-unix start)) (start-unix (chronometrist-iso-to-unix start))
(stop-unix (and stop (chronometrist-iso-to-unix stop))) (stop-unix (and stop (chronometrist-iso-to-unix stop)))
name-id interval-id) name-id interval-id prop-id)
;; insert name if it does not exist ;; insert name if it does not exist
(emacsql db [:insert-or-ignore-into interval-names [name] (emacsql db [:insert-or-ignore-into interval-names [name]
:values [$s1]] :values [$s1]]
name) name)
;; insert interval properties if they do not exist ;; insert interval properties if they do not exist
(chronometrist-sqlite-insert-properties backend plist) (setq prop-id (chronometrist-sqlite-insert-properties backend plist))
;; insert interval and associate it with the date ;; insert interval and associate it with the date
(setq name-id (setq name-id
(caar (emacsql db [:select [name-id] (caar (emacsql db [:select [name-id]
:from interval-names :from interval-names
:where (= name $s1)] :where (= name $s1)]
name))) name)))
(emacsql db [:insert-or-ignore-into intervals [name-id start-time stop-time] (emacsql db [:insert-or-ignore-into intervals
:values [$s1 $s2 $s3]] [name-id start-time stop-time prop-id]
name-id start-unix stop-unix) :values [$s1 $s2 $s3 $s4]]
name-id start-unix stop-unix prop-id)
(emacsql db [:insert-or-ignore-into dates [date] (emacsql db [:insert-or-ignore-into dates [date]
:values [$s1]] date-unix) :values [$s1]] date-unix)
(setq date-id (setq date-id
@ -201,6 +205,6 @@ s-expressions in a text column."
(cl-defmethod chronometrist-replace-last ((backend chronometrist-sqlite-backend) plist) (cl-defmethod chronometrist-replace-last ((backend chronometrist-sqlite-backend) plist)
(emacsql db [:delete-from events :where ])) (emacsql db [:delete-from events :where ]))
(provide 'chronometrist-sqlite3) (provide 'chronometrist-sqlite)
;;; chronometrist-sqlite3.el ends here ;;; chronometrist-sqlite.el ends here

View File

@ -115,29 +115,18 @@ Return the connection object from `emacsql-sqlite'."
finally return db))) finally return db)))
#+END_SRC #+END_SRC
** to-file :method: ** iso-to-unix :function:
#+BEGIN_SRC emacs-lisp :load no :tangle no
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(cl-loop with db = (emacsql-sqlite file)
with count = 0
for records being the hash-values of hash-table do
(cl-loop for record in records do
(chronometrist-insert record db)
(cl-incf count)
(when (zerop (% count 5))
(message "chronometrist-migrate - %s records converted" count)))
finally return count do
(message "chronometrist-migrate - finished converting %s events." count)))
#+END_SRC
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-iso-to-unix (timestamp) (defun chronometrist-iso-to-unix (timestamp)
(truncate (float-time (parse-iso8601-time-string timestamp)))) (truncate (float-time (parse-iso8601-time-string timestamp))))
#+END_SRC
** to-file :method:
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file) (cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(with-slots (connection) backend (with-slots (connection) backend
(delete-file file) (delete-file file)
(emacsql-close connection) (when connection (emacsql-close connection))
(setf connection nil) (setf connection nil)
(chronometrist-create-file backend file) (chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do (cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
@ -164,7 +153,10 @@ prop-id of the inserted or existing property."
(emacsql connection (emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]] [:insert-or-ignore-into properties [properties] :values [$s1]]
props) props)
(caar (emacsql connection [:select (funcall max prop-id) :from properties]))))) (caar (emacsql connection [:select [prop-id]
:from properties
:where (= properties $s1)]
props)))))
#+END_SRC #+END_SRC
*** properties-to-json :function: *** properties-to-json :function:
@ -209,22 +201,23 @@ s-expressions in a text column."
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start))) (date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))
(start-unix (chronometrist-iso-to-unix start)) (start-unix (chronometrist-iso-to-unix start))
(stop-unix (and stop (chronometrist-iso-to-unix stop))) (stop-unix (and stop (chronometrist-iso-to-unix stop)))
name-id interval-id) name-id interval-id prop-id)
;; insert name if it does not exist ;; insert name if it does not exist
(emacsql db [:insert-or-ignore-into interval-names [name] (emacsql db [:insert-or-ignore-into interval-names [name]
:values [$s1]] :values [$s1]]
name) name)
;; insert interval properties if they do not exist ;; insert interval properties if they do not exist
(chronometrist-sqlite-insert-properties backend plist) (setq prop-id (chronometrist-sqlite-insert-properties backend plist))
;; insert interval and associate it with the date ;; insert interval and associate it with the date
(setq name-id (setq name-id
(caar (emacsql db [:select [name-id] (caar (emacsql db [:select [name-id]
:from interval-names :from interval-names
:where (= name $s1)] :where (= name $s1)]
name))) name)))
(emacsql db [:insert-or-ignore-into intervals [name-id start-time stop-time] (emacsql db [:insert-or-ignore-into intervals
:values [$s1 $s2 $s3]] [name-id start-time stop-time prop-id]
name-id start-unix stop-unix) :values [$s1 $s2 $s3 $s4]]
name-id start-unix stop-unix prop-id)
(emacsql db [:insert-or-ignore-into dates [date] (emacsql db [:insert-or-ignore-into dates [date]
:values [$s1]] date-unix) :values [$s1]] date-unix)
(setq date-id (setq date-id
@ -272,9 +265,9 @@ s-expressions in a text column."
** Provide ** Provide
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(provide 'chronometrist-sqlite3) (provide 'chronometrist-sqlite)
;;; chronometrist-sqlite3.el ends here ;;; chronometrist-sqlite.el ends here
#+END_SRC #+END_SRC
* Local variables :noexport: * Local variables :noexport:

View File

@ -755,6 +755,12 @@ Value must be a keyword corresponding to a key in
(cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist))) (cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist)))
;; active-backend:1 ends here ;; active-backend:1 ends here
;; [[file:chronometrist.org::*get-backend][get-backend:1]]
(defun chronometrist-get-backend (keyword)
"Return the backend object associated with KEYWORD."
(cl-second (alist-get keyword chronometrist-backends-alist)))
;; get-backend:1 ends here
;; [[file:chronometrist.org::*switch-backend][switch-backend:1]] ;; [[file:chronometrist.org::*switch-backend][switch-backend:1]]
(defun chronometrist-switch-backend () (defun chronometrist-switch-backend ()
(interactive) (interactive)
@ -788,8 +794,7 @@ be replaced."
;; register-backend:1 ends here ;; register-backend:1 ends here
;; [[file:chronometrist.org::*read-backend-name][read-backend-name:1]] ;; [[file:chronometrist.org::*read-backend-name][read-backend-name:1]]
(defun chronometrist-read-backend-name (prompt backend-alist (defun chronometrist-read-backend-name (prompt backend-alist &optional predicate return-keyword)
&optional predicate return-keyword)
"Prompt user for a Chronometrist backend name. "Prompt user for a Chronometrist backend name.
BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'. BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'.
@ -1876,50 +1881,63 @@ Return value is either a list in the form
;; remove-prefix:1 ends here ;; remove-prefix:1 ends here
;; [[file:chronometrist.org::*migrate][migrate:1]] ;; [[file:chronometrist.org::*migrate][migrate:1]]
(defun chronometrist-migrate () (defun chronometrist-migrate (&optional input-backend-keyword input-file
"Convert from one Chronometrist backend to another." output-backend-keyword output-file
&rest load-files)
"Convert from one Chronometrist backend to another.
INPUT-BACKEND-KEYWORD and OUTPUT-BACKEND-KEYWORD must be keywords
corresponding to entries in `chronometrist-backends-alist'.
INPUT-FILE and OUTPUT-FILE must be file names. INPUT-FILE must
already exist.
LOAD-FILES must be paths to Elisp files. These files are `load'ed
before conversion. They may be used to point to Chronometrist
source files to be used."
(interactive) (interactive)
(let* ((input-backend (let* ((input-backend-keyword (or input-backend-keyword
(chronometrist-read-backend-name "Backend to convert: " (chronometrist-read-backend-name "Backend to convert: " chronometrist-backends-alist nil t)))
chronometrist-backends-alist)) (input-backend (chronometrist-get-backend input-backend-keyword))
(input-file-suggestion (chronometrist-backend-file input-backend)) (suggested-input-file (chronometrist-backend-file input-backend))
(input-file (read-file-name "File to convert: " nil (input-file (or input-file
input-file-suggestion t (read-file-name "File to convert: "
input-file-suggestion)) nil suggested-input-file t suggested-input-file)))
(output-backend (chronometrist-read-backend-name (output-backend-keyword (or output-backend-keyword
"Backend to write: " (chronometrist-read-backend-name "Backend to write: "
chronometrist-backends-alist chronometrist-backends-alist
(lambda (keyword) (lambda (keyword)
(not (equal (cl-second (not (equal (cl-second
(alist-get keyword chronometrist-backends-alist)) (alist-get keyword chronometrist-backends-alist))
input-backend))))) input-backend)))
(output-file-suggestion (chronometrist-backend-file output-backend)) t)))
(output-file (read-file-name "File to write: " nil nil nil (output-backend (chronometrist-get-backend output-backend-keyword))
output-file-suggestion)) (suggested-output-file (chronometrist-backend-file output-backend))
(input-backend-name (chronometrist-remove-prefix (output-file (or output-file
(symbol-name (read-file-name "File to write: " nil nil nil suggested-output-file)))
(eieio-object-class-name input-backend)))) (input-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name input-backend))))
(output-backend-name (chronometrist-remove-prefix (output-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name output-backend))))
(symbol-name (confirm (yes-or-no-p (format "Convert %s (%s) to %s (%s)? "
(eieio-object-class-name output-backend)))) input-file input-backend-name
(confirm (yes-or-no-p output-file output-backend-name)))
(format "Convert %s (%s) to %s (%s)? " (confirm-overwrite (if (and confirm
input-file (file-exists-p output-file)
input-backend-name (not (chronometrist-file-empty-p output-file)))
output-file (yes-or-no-p
output-backend-name))) (format "Overwrite existing non-empty file %s ?" output-file))
(confirm-overwrite t)))
(if (and confirm
(file-exists-p output-file)
(not (chronometrist-file-empty-p output-file)))
(yes-or-no-p
(format "Overwrite existing non-empty file %s ?"
output-file))
t)))
(if (and confirm confirm-overwrite) (if (and confirm confirm-overwrite)
(chronometrist-to-file (chronometrist-backend-hash-table input-backend) (start-process
output-backend "chronometrist-migrate" (generate-new-buffer-name "chronometrist-migrate")
output-file) "emacs" "--batch"
"--eval=(package-initialize)"
"--eval=(cl-loop for (pkg . desc) in package-alist
when (string-match-p \"^chronometrist\" (symbol-name pkg))
do (require pkg))"
(format "--eval=(cl-loop for file in (quote %S) do (load file))" load-files)
;; the hash table slot may be empty, so we generate the hash table from scratch
(format "--eval=(chronometrist-to-file (chronometrist-to-hash-table (chronometrist-get-backend %s)) (chronometrist-get-backend %s) %S)"
input-backend-keyword output-backend-keyword output-file))
(message "Conversion aborted.")))) (message "Conversion aborted."))))
;; migrate:1 ends here ;; migrate:1 ends here

View File

@ -1410,6 +1410,13 @@ Value must be a keyword corresponding to a key in
(cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist))) (cl-second (alist-get chronometrist-active-backend chronometrist-backends-alist)))
#+END_SRC #+END_SRC
**** get-backend :reader:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-get-backend (keyword)
"Return the backend object associated with KEYWORD."
(cl-second (alist-get keyword chronometrist-backends-alist)))
#+END_SRC
**** switch-backend :command: **** switch-backend :command:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-switch-backend () (defun chronometrist-switch-backend ()
@ -1446,8 +1453,7 @@ be replaced."
**** read-backend-name :procedure: **** read-backend-name :procedure:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-read-backend-name (prompt backend-alist (defun chronometrist-read-backend-name (prompt backend-alist &optional predicate return-keyword)
&optional predicate return-keyword)
"Prompt user for a Chronometrist backend name. "Prompt user for a Chronometrist backend name.
BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'. BACKEND-ALIST should be an alist similar to `chronometrist-backends-alist'.
@ -2903,50 +2909,63 @@ We apply the same hack as in the [[hack-note-plist-group-insert][insert]] method
*** migrate :command: *** migrate :command:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-migrate () (defun chronometrist-migrate (&optional input-backend-keyword input-file
"Convert from one Chronometrist backend to another." output-backend-keyword output-file
&rest load-files)
"Convert from one Chronometrist backend to another.
INPUT-BACKEND-KEYWORD and OUTPUT-BACKEND-KEYWORD must be keywords
corresponding to entries in `chronometrist-backends-alist'.
INPUT-FILE and OUTPUT-FILE must be file names. INPUT-FILE must
already exist.
LOAD-FILES must be paths to Elisp files. These files are `load'ed
before conversion. They may be used to point to Chronometrist
source files to be used."
(interactive) (interactive)
(let* ((input-backend (let* ((input-backend-keyword (or input-backend-keyword
(chronometrist-read-backend-name "Backend to convert: " (chronometrist-read-backend-name "Backend to convert: " chronometrist-backends-alist nil t)))
chronometrist-backends-alist)) (input-backend (chronometrist-get-backend input-backend-keyword))
(input-file-suggestion (chronometrist-backend-file input-backend)) (suggested-input-file (chronometrist-backend-file input-backend))
(input-file (read-file-name "File to convert: " nil (input-file (or input-file
input-file-suggestion t (read-file-name "File to convert: "
input-file-suggestion)) nil suggested-input-file t suggested-input-file)))
(output-backend (chronometrist-read-backend-name (output-backend-keyword (or output-backend-keyword
"Backend to write: " (chronometrist-read-backend-name "Backend to write: "
chronometrist-backends-alist chronometrist-backends-alist
(lambda (keyword) (lambda (keyword)
(not (equal (cl-second (not (equal (cl-second
(alist-get keyword chronometrist-backends-alist)) (alist-get keyword chronometrist-backends-alist))
input-backend))))) input-backend)))
(output-file-suggestion (chronometrist-backend-file output-backend)) t)))
(output-file (read-file-name "File to write: " nil nil nil (output-backend (chronometrist-get-backend output-backend-keyword))
output-file-suggestion)) (suggested-output-file (chronometrist-backend-file output-backend))
(input-backend-name (chronometrist-remove-prefix (output-file (or output-file
(symbol-name (read-file-name "File to write: " nil nil nil suggested-output-file)))
(eieio-object-class-name input-backend)))) (input-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name input-backend))))
(output-backend-name (chronometrist-remove-prefix (output-backend-name (chronometrist-remove-prefix (symbol-name (eieio-object-class-name output-backend))))
(symbol-name (confirm (yes-or-no-p (format "Convert %s (%s) to %s (%s)? "
(eieio-object-class-name output-backend)))) input-file input-backend-name
(confirm (yes-or-no-p output-file output-backend-name)))
(format "Convert %s (%s) to %s (%s)? " (confirm-overwrite (if (and confirm
input-file (file-exists-p output-file)
input-backend-name (not (chronometrist-file-empty-p output-file)))
output-file (yes-or-no-p
output-backend-name))) (format "Overwrite existing non-empty file %s ?" output-file))
(confirm-overwrite t)))
(if (and confirm
(file-exists-p output-file)
(not (chronometrist-file-empty-p output-file)))
(yes-or-no-p
(format "Overwrite existing non-empty file %s ?"
output-file))
t)))
(if (and confirm confirm-overwrite) (if (and confirm confirm-overwrite)
(chronometrist-to-file (chronometrist-backend-hash-table input-backend) (start-process
output-backend "chronometrist-migrate" (generate-new-buffer-name "chronometrist-migrate")
output-file) "emacs" "--batch"
"--eval=(package-initialize)"
"--eval=(cl-loop for (pkg . desc) in package-alist
when (string-match-p \"^chronometrist\" (symbol-name pkg))
do (require pkg))"
(format "--eval=(cl-loop for file in (quote %S) do (load file))" load-files)
;; the hash table slot may be empty, so we generate the hash table from scratch
(format "--eval=(chronometrist-to-file (chronometrist-to-hash-table (chronometrist-get-backend %s)) (chronometrist-get-backend %s) %S)"
input-backend-keyword output-backend-keyword output-file))
(message "Conversion aborted.")))) (message "Conversion aborted."))))
#+END_SRC #+END_SRC