Compare commits

...

4 Commits

Author SHA1 Message Date
contrapunctus a1e305fb8c Add design description 2022-02-10 15:31:23 +05:30
contrapunctus 5068bca3d6 Create alerts section 2022-02-10 15:06:20 +05:30
contrapunctus ab38cf29d2 Create literate source 2022-02-10 14:48:05 +05:30
contrapunctus c0597098fe Correct docstring position 2022-02-10 14:47:44 +05:30
2 changed files with 396 additions and 1 deletions

View File

@ -222,10 +222,10 @@ ROW must be a valid element of the list specified by
(list task (vconcat vector `[,goal-string]))))
(defun chronometrist-goal-schema-transformer (schema)
(vconcat schema `[("Target" 3 t)]))
"Add a goal column to SCHEMA.
Used to add a goal column to `chronometrist-schema-transformers'.
SCHEMA should be a vector as specified by `tabulated-list-format'."
(vconcat schema `[("Target" 5 t :pad-right 3)]))
(defun chronometrist-goal-setup ()
"Add `chronometrist-goal' functions to `chronometrist' hooks."

395
chronometrist-goal.org Normal file
View File

@ -0,0 +1,395 @@
#+TITLE: chronometrist-goal
#+SUBTITLE: Third Time System extension for Chronometrist
* Explanation
** Structure
The [[#goal-list][goal-list]] associates each Chronometrist task with a goal.
Enabling the [[#goal-minor-mode][minor-mode]] sets up hooks to
1. display a goal column in the Chronometrist buffer, and
2. start and stop alert timers.
Alerts are functions which start timers - at the end of the timer, a notification is displayed. Alerts are stored in [[#alert-functions][alert-functions]] (used by [[#run-alert-timers][run-alert-timers]] to start the timers). The timers created are stored in [[#timers-list][timers-list]] (used by [[#stop-alert-timers][stop-alert-timers]] to stop the timers).
* Program source
** Library headers and commentary
#+BEGIN_SRC emacs-lisp
;;; chronometrist-goal.el --- Adds support for time goals to Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; Keywords: calendar
;; Homepage: https://github.com/contrapunctus-1/chronometrist
;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.2.1
;; 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:
;; Add support for time goals to Chronometrist
;; For information on usage and customization, see https://github.com/contrapunctus-1/chronometrist-goal/blob/master/README.md
#+END_SRC
** Dependencies
#+BEGIN_SRC emacs-lisp
;;; Code:
(require 'chronometrist)
(require 'alert)
(declare-function chronometrist-last "chronometrist")
#+END_SRC
** goal-list
:PROPERTIES:
:CUSTOM_ID: goal-list
:END:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-goal-list nil
"List to specify daily time goal for each task.
Each element must be in the form (GOAL TASK *).
GOAL is an integer specifying number of minutes.
TASK is the task on which you would like spend GOAL time.
There can be more than one TASK, to specify that you would
like to spend GOAL time on any one of those tasks."
:group 'chronometrist
:type '(repeat
(list (integer :value 15)
(set :inline t (repeat string)))))
#+END_SRC
** TODO goal-get
:PROPERTIES:
:CUSTOM_ID: goal-get
:END:
TODO - if there are multiple tasks associated with a single time goal (i.e. =(int "task1" "task2" ...)=), and the user has reached the goal for one of those tasks, don't display the goal for the other associated tasks.
#+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-goal-get (task &optional (goal-list chronometrist-goal-list))
"Return time goal for TASK from GOAL-LIST.
Return value is minutes as an integer, or nil.
If GOAL-LIST is not supplied, `chronometrist-goal-list' is used."
(cl-loop for list in goal-list
when (member task list)
return (car list)))
#+END_SRC
** Alerts
:PROPERTIES:
:CUSTOM_ID: Alerts
:END:
*** timers-list
:PROPERTIES:
:CUSTOM_ID: timers-list
:END:
#+BEGIN_SRC emacs-lisp
(defvar chronometrist-goal--timers-list nil)
#+END_SRC
*** run-at-time
:PROPERTIES:
:CUSTOM_ID: run-at-time
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-run-at-time (time repeat function &rest args)
"Like `run-at-time', but append timers to `chronometrist-goal--timers-list'.
TIME, REPEAT, FUNCTION, and ARGS are as used in `run-at-time'."
(->> (apply #'run-at-time time repeat function args)
(list)
(append chronometrist-goal--timers-list)
(setq chronometrist-goal--timers-list)))
#+END_SRC
*** seconds->alert-string
:PROPERTIES:
:CUSTOM_ID: seconds->alert-string
:END:
#+BEGIN_SRC emacs-lisp
;; (mapcar #'chronometrist-goal-seconds->alert-string '(0 1 2 59 60 61 62 120 121 122))
(defun chronometrist-goal-seconds->alert-string (seconds)
"Convert SECONDS to a string suitable for displaying in alerts.
SECONDS should be a positive integer."
(-let [(h m _) (chronometrist-seconds-to-hms seconds)]
(let* ((h-str (unless (zerop h)
(number-to-string h)))
(m-str (unless (zerop m)
(number-to-string m)))
(h-unit (cl-case h
(0 nil)
(1 " hour")
(t " hours")))
(m-unit (cl-case m
(0 nil)
(1 " minute")
(t " minutes")))
(and (if (and h-unit m-unit)
" and "
"")))
(concat h-str h-unit
and
m-str m-unit))))
#+END_SRC
*** approach-alert
:PROPERTIES:
:CUSTOM_ID: approach-alert
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-approach-alert (task goal spent)
"Alert the user when they are 5 minutes away from reaching GOAL for TASK.
TASK is the name of the current task (as a string).
GOAL is the goal time for that task (minutes as an integer).
SPENT is the time spent on that task (minutes as an integer)."
(and goal
(< spent goal)
(chronometrist-goal-run-at-time (* 60 (- goal 5 spent)) ;; negative seconds = run now
nil
(lambda (task)
(alert (format "5 minutes remain for %s" task)))
task)))
#+END_SRC
*** complete-alert
:PROPERTIES:
:CUSTOM_ID: complete-alert
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-complete-alert (task goal spent)
"Alert the user when they have reached the GOAL for TASK.
TASK is the name of the current task (as a string).
GOAL is the goal time for that task (minutes as an integer).
SPENT is the time spent on that task (minutes as an integer)."
(and goal
;; In case the user reaches GOAL but starts tracking again -
;; CURRENT is slightly over GOAL, but we notify the user of
;; reaching the GOAL anyway.
(< spent (+ goal 5))
(chronometrist-goal-run-at-time (* 60 (- goal spent)) ;; negative seconds = run now
nil
(lambda (task)
(alert (format "Goal for %s reached" task)))
task)))
#+END_SRC
*** exceed-alert
:PROPERTIES:
:CUSTOM_ID: exceed-alert
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-exceed-alert (task goal spent)
"Alert the user when they have exceeded the GOAL for TASK.
TASK is the name of the current task (as a string).
GOAL is the goal time for that task (minutes as an integer).
SPENT is the time spent on that task (minutes as an integer)."
(and goal
(chronometrist-goal-run-at-time (* 60 (- (+ goal 5) spent)) ;; negative seconds = run now
nil
(lambda (task)
(alert (format "You are exceeding the goal for %s!" task)
:severity 'medium))
task)))
#+END_SRC
*** no-goal-alert
:PROPERTIES:
:CUSTOM_ID: no-goal-alert
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-no-goal-alert (task goal _spent)
"If TASK has no GOAL, regularly remind the user of the time spent on it.
TASK is the name of the current task (as a string).
GOAL is the goal time for that task (minutes as an integer).
SPENT is the time spent on that task (minutes as an integer)."
(unless goal
(chronometrist-goal-run-at-time (* 15 60) ;; first run after 15 minutes from now
(* 15 60) ;; repeat every 15 minutes
(lambda (task)
;; We cannot use SPENT here, because that will
;; remain the value it had when we clocked in
;; (when `chronometrist-goal-run-alert-timers'
;; is run), and we need show the time spent at
;; the time of notification.
(alert (format "You have spent %s on %s"
(chronometrist-goal-seconds->alert-string
(chronometrist-task-time-one-day task))
task)))
task)))
#+END_SRC
*** alert-functions
:PROPERTIES:
:CUSTOM_ID: alert-functions
:END:
#+BEGIN_SRC emacs-lisp
(defcustom chronometrist-goal-alert-functions
'(chronometrist-goal-approach-alert
chronometrist-goal-complete-alert
chronometrist-goal-exceed-alert
chronometrist-goal-no-goal-alert)
"List to describe timed alerts.
Each element should be a function, which will be called with
three arguments - the name of the current task (as a string) and
the goal time for that task (minutes as an integer), and the time
spent on that task (minutes as an integer).
Typically, each function in this list should call `run-at-time'
to run another function, which in turn should call `alert' to
notify the user.
The timer returned by `run-at-time' should also be appended to
`chronometrist-goal--timers-list', so that it can later be stopped by
`chronometrist-goal-stop-alert-timers'. `chronometrist-goal-run-at-time'
will do that for you.
Note - the time spent passed to these functions is calculated
when `chronometrist-goal-run-alert-timers' is run, i.e. when the
user clocks in. To obtain the time spent at the time of
notification, use `chronometrist-task-time-one-day' within the
function passed to `run-at-time'."
:group 'chronometrist
:type 'hook)
#+END_SRC
*** run-alert-timers
:PROPERTIES:
:CUSTOM_ID: run-alert-timers
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-run-alert-timers (task)
"Run timers to alert the user of the time spent on TASK.
To use, add this to `chronometrist-after-in-functions', and
`chronometrist-goal-stop-alert-timers' to
`chronometrist-after-out-functions'."
(let ((goal (chronometrist-goal-get task))
(spent (/ (chronometrist-task-time-one-day task) 60)))
(add-hook 'chronometrist-file-change-hook #'chronometrist-goal-on-file-change)
(mapc (lambda (f)
(funcall f task goal spent))
chronometrist-goal-alert-functions)))
#+END_SRC
*** stop-alert-timers
:PROPERTIES:
:CUSTOM_ID: stop-alert-timers
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-stop-alert-timers (&optional _task)
"Stop timers to alert the user of the time spent on TASK.
To use, add this to `chronometrist-after-out-functions', and
`chronometrist-goal-run-alert-timers' to
`chronometrist-after-in-functions'."
(and chronometrist-goal--timers-list ;; in case of start task -> exit Emacs without stopping -> start Emacs -> stop task
(mapc #'cancel-timer chronometrist-goal--timers-list)
(setq chronometrist-goal--timers-list nil)))
#+END_SRC
** Minor mode
*** on-file-change
:PROPERTIES:
:CUSTOM_ID: on-file-change
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-on-file-change ()
"Manage timed alerts when `chronometrist-file' changes."
(let ((last (chronometrist-latest-record (chronometrist-active-backend))))
(chronometrist-goal-stop-alert-timers)
;; if there's a task running, start timed alerts for it
(unless (plist-get last :stop)
(chronometrist-goal-run-alert-timers (plist-get last :name)))))
#+END_SRC
*** row-transformers
:PROPERTIES:
:CUSTOM_ID: row-transformers
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-row-transformer (row)
"Add a goal cell to ROW.
Used to add a goal column to `chronometrist-rows'.
ROW must be a valid element of the list specified by
`tabulated-list-entries'."
(-let* (((task vector) row)
(goal-minutes (chronometrist-goal-get task))
(goal-string (if goal-minutes (format "% 4d" goal-minutes) "")))
(list task (vconcat vector `[,goal-string]))))
#+END_SRC
*** schema-transformer
:PROPERTIES:
:CUSTOM_ID: schema-transformer
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-schema-transformer (schema)
"Add a goal column to SCHEMA.
Used to add a goal column to `chronometrist-schema-transformers'.
SCHEMA should be a vector as specified by `tabulated-list-format'."
(vconcat schema `[("Target" 5 t :pad-right 3)]))
#+END_SRC
*** setup
:PROPERTIES:
:CUSTOM_ID: setup
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-setup ()
"Add `chronometrist-goal' functions to `chronometrist' hooks."
(add-to-list 'chronometrist-row-transformers #'chronometrist-goal-row-transformer)
(add-to-list 'chronometrist-schema-transformers #'chronometrist-goal-schema-transformer)
(add-hook 'chronometrist-after-in-functions #'chronometrist-goal-run-alert-timers)
(add-hook 'chronometrist-after-out-functions #'chronometrist-goal-stop-alert-timers))
#+END_SRC
*** teardown
:PROPERTIES:
:CUSTOM_ID: teardown
:END:
#+BEGIN_SRC emacs-lisp
(defun chronometrist-goal-teardown ()
"Remove `chronometrist-goal' functions from `chronometrist' hooks."
(setq chronometrist-row-transformers
(remove #'chronometrist-goal-row-transformer chronometrist-row-transformers)
chronometrist-schema-transformers
(remove #'chronometrist-goal-schema-transformer chronometrist-schema-transformers))
(remove-hook 'chronometrist-after-in-functions #'chronometrist-goal-run-alert-timers)
(remove-hook 'chronometrist-after-out-functions #'chronometrist-goal-stop-alert-timers))
#+END_SRC
*** goal-minor-mode
:PROPERTIES:
:CUSTOM_ID: goal-minor-mode
:END:
#+BEGIN_SRC emacs-lisp
(define-minor-mode chronometrist-goal-minor-mode
nil nil nil nil
;; when being enabled/disabled, `chronometrist-goal-minor-mode' will already be t/nil here
(if chronometrist-goal-minor-mode (chronometrist-goal-setup) (chronometrist-goal-teardown)))
#+END_SRC
** Provide
#+BEGIN_SRC emacs-lisp
(provide 'chronometrist-goal)
#+END_SRC
* Local Variables :NOEXPORT:
#+BEGIN_SRC emacs-lisp
;; Local Variables:
;; nameless-current-name: "chronometrist-goal"
;; End:
;;; chronometrist-goal.el ends here
#+END_SRC
# Local Variables:
# my-org-src-default-lang: "emacs-lisp"
# End: