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