Compare commits

...
This repository has been archived on 2022-05-13. You can view files and clone it, but cannot push or open issues or pull requests.

16 Commits

Author SHA1 Message Date
contrapunctus 01db31e44d Auto-load with literate-elisp 2022-02-10 18:56:28 +05:30
contrapunctus 65d36aa83a Update website 2022-02-10 16:21:43 +05:30
contrapunctus 5301d0327a Tweak explanation 2022-02-10 16:21:18 +05:30
contrapunctus 3e9244eb09 Add Makefile 2022-02-10 15:47:13 +05:30
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
contrapunctus 1474c7fa5a fix: defcustom :type syntax 2021-10-22 05:22:56 +05:30
contrapunctus 0e6e57cdee Update for EIEIO migration 2021-09-04 23:22:36 +05:30
contrapunctus 858da9341b Update docstrings 2021-09-04 23:12:42 +05:30
contrapunctus 79c73cbb7f Remove docstring to use generated docstring 2021-09-04 22:53:05 +05:30
contrapunctus b9d910f4d6 Update for chronometrist.org literate program migration 2021-09-04 22:42:51 +05:30
contrapunctus b115c8644d Update names (list-format -> schema; entries -> rows) 2021-05-25 16:28:48 +05:30
contrapunctus 53ed966671 Remove anaphoric macro usage 2021-05-14 12:47:50 +05:30
contrapunctus b52ddb3e03 [Literate] Depend on chronometrist, not chronometrist-queries 2021-02-11 12:43:16 +05:30
3 changed files with 467 additions and 30 deletions

42
Makefile Normal file
View File

@ -0,0 +1,42 @@
.phony: all setup tangle compile lint-check-declare lint-checkdoc lint-package-lint lint-relint lint clean-elc
all: clean-elc setup tangle compile lint
setup:
emacs --batch --eval="(package-initialize)" \
--eval="(mapcar #'package-install '(indent-lint package-lint relint))"
# No -q or -Q without ORG_PATH - if the user has a newer version of
# Org, we want to use it.
tangle:
emacs --batch \
--eval="(package-initialize)" --eval="(require 'ob-tangle)" \
--eval='(org-babel-tangle-file "chronometrist-goal.org")'
compile: tangle
emacs --batch \
--eval="(progn (package-initialize) (require 'chronometrist) (require 'alert))" \
--eval='(byte-compile-file "chronometrist-goal.el")'
lint-check-declare: tangle
emacs -q -Q --batch --eval='(check-declare-file "chronometrist-goal.el")'
lint-checkdoc: tangle
emacs -q -Q --batch --eval='(checkdoc-file "chronometrist-goal.el")'
lint-package-lint: setup tangle
emacs --batch \
--eval="(progn (package-initialize) (require 'chronometrist) (require 'alert))" \
--eval="(require 'package-lint)" \
-f 'package-lint-batch-and-exit' chronometrist-goal.el \
lint-relint: setup tangle
emacs -q -Q --batch \
--eval="(package-initialize)" \
--eval="(require 'relint)" \
--eval='(relint-file "chronometrist-goal.el")'
lint: lint-check-declare lint-checkdoc lint-package-lint lint-relint
clean-elc:
-rm *.elc

View File

@ -7,7 +7,7 @@
;; Package-Requires: ((emacs "25.1") (alert "1.2") (chronometrist "0.6.0"))
;; Version: 0.2.1
(require 'chronometrist-queries)
(require 'chronometrist)
(require 'alert)
;; This is free and unencumbered software released into the public domain.
@ -26,7 +26,7 @@
;;; Code:
(declare-function chronometrist-last "chronometrist-queries")
(declare-function chronometrist-last "chronometrist")
(defvar chronometrist-goal--timers-list nil)
@ -42,8 +42,8 @@ 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
(repeat :inline t string))))
(list (integer :value 15)
(set :inline t (repeat string)))))
(defun chronometrist-goal-run-at-time (time repeat function &rest args)
"Like `run-at-time', but append timers to `chronometrist-goal--timers-list'.
@ -203,49 +203,48 @@ To use, add this to `chronometrist-after-out-functions', and
(defun chronometrist-goal-on-file-change ()
"Manage timed alerts when `chronometrist-file' changes."
(let ((last (chronometrist-last)))
(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)))))
;;;; minor mode
(defun chronometrist-goal-entry-transformer (entry)
"Add goal information to the return value of `chronometrist-entries'.
ENTRY must be a valid element in the list specified by
`tabulated-list-entries'."
(-let* (((task vector) entry)
(goal (aif (chronometrist-goal-get task) (format "% 4d" it) "")))
(list task (vconcat vector `[,goal]))))
(defun chronometrist-goal-row-transformer (row)
"Add a goal cell to ROW.
Used to add a goal column to `chronometrist-rows'.
(defun chronometrist-goal-list-format-transformer (format)
"Add a goal column to the return value of `tabulated-list-format'.
FORMAT should be a vector (see `tabulated-list-format')."
(vconcat format `[("Target" 3 t)]))
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]))))
(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)]))
(defun chronometrist-goal-setup ()
"Add `chronometrist-goal' functions to `chronometrist' hooks."
(add-to-list 'chronometrist-entry-transformers #'chronometrist-goal-entry-transformer)
(add-to-list 'chronometrist-list-format-transformers #'chronometrist-goal-list-format-transformer)
(add-hook 'chronometrist-after-in-functions #'chronometrist-goal-run-alert-timers)
(add-hook 'chronometrist-after-out-functions #'chronometrist-goal-stop-alert-timers))
(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))
(defun chronometrist-goal-teardown ()
"Remove `chronometrist-goal' functions from `chronometrist' hooks."
(setq chronometrist-entry-transformers
(remove #'chronometrist-goal-entry-transformer chronometrist-entry-transformers)
chronometrist-list-format-transformers
(remove #'chronometrist-goal-list-format-transformer chronometrist-list-format-transformers))
(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))
(define-minor-mode chronometrist-goal-minor-mode
"Toggle `chronometrist-goal-minor-mode'.
With a prefix argument ARG, enable
`chronometrist-goal-minor-mode' if ARG is positive, and disable
it otherwise. If called from Lisp, enable the mode if ARG is
omitted or nil."
nil nil nil
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)))

396
chronometrist-goal.org Normal file
View File

@ -0,0 +1,396 @@
#+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. display alerts about the time spent relative to the defined time goal.
Alerts are functions which start timers - at the end of the timer, a notification is displayed. Alerts are stored in the custom variable [[#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://tildegit.org/contrapunctus/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://tildegit.org/contrapunctus/chronometrist-goal/src/branch/production/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"
# eval: (when (package-installed-p 'literate-elisp) (require 'literate-elisp) (literate-elisp-load (buffer-file-name)))
# End: