diff --git a/README.org b/README.org index 68ca013..49aa2c3 100644 --- a/README.org +++ b/README.org @@ -1,7 +1,37 @@ +#+TITLE: pomm.el + +Yet another implementation of a [[https://en.wikipedia.org/wiki/Pomodoro_Technique][pomodoro timer]] for Emacs. + +This particular package features: +- Managing the timer with the excellent [[https://github.com/magit/transient/blob/master/lisp/transient.el][transient.el]]. +- A persistent state between Emacs sessions. + The timer state isn't reset if you close Emacs. Also, the state file can be syncronized between machines. + +None of the available [[*Alternatives][alternatives]] were doing quite what I wanted, and the idea of the timer is quite simple, so I figured I'd implement one myself. + +* Setup +While the package is available only this this repository, one way to install is to clone the repository, add the package to the =load-path= and load it with =require=: +#+begin_src emacs-lisp +(require 'pomm) +#+end_src + +My preferred way is =use-package= with =straight.el=: +#+begin_src emacs-lisp +(use-package pomm + :straight (:host github :repo "SqrtMinusOne/pomm.el") + :commands (pomm)) +#+end_src + +The package requires Emacs 27.1, because the time API of the previous versions is kinda crazy and 27.1 has =time-convert=. + +** Alerts +The package sends alerts via =alert.el=. The default style of alert is a plain =message=, but if you want an actual notification, set =alert-default-style= accordingly: #+begin_src emacs-lisp (setq alert-default-style 'libnotify) #+end_src +** Modeline +If you want the timer to display in the modeline, add the following code to your config: #+begin_src emacs-lisp (add-to-list 'mode-line-misc-info '(:eval pomm-current-mode-line-string)) (add-hook 'pomm-on-tick-hook 'pomm-update-mode-line-string) @@ -9,3 +39,56 @@ (add-hook 'pomm-on-status-changed-hook 'pomm-update-mode-line-string) (add-hook 'pomm-on-status-changed-hook 'force-mode-line-update) #+end_src + +This is quite verbose, but as I don't use this feature, I want to avoid adding an unnecesary load to my Emacs. + +** Polybar module +If you want to display the pomodoro status in something like polybar, you can add the following lines to your config: +#+begin_src emacs-lisp +(add-hook 'pomm-on-tick-hook 'pomm-update-mode-line-string) +(add-hook 'pomm-on-status-changed-hook 'pomm-update-mode-line-string) +#+end_src + +Create a script like this: +#+begin_src bash +if ps -e | grep emacs >> /dev/null; then + emacsclient --eval "pomm-current-mode-line-string" | xargs echo -e +fi +#+end_src + +And add a polybar module definition to your polybar config: +#+begin_src conf-windows +[module/pomm] +type = custom/script +exec = /home/pavel/bin/polybar/pomm.sh +interval = 1 +#+end_src + +* Usage +Run =M-x pomm= to open the transient buffer. + +The listed commands are rather self-descriptive and match the pomodoro ideology. + +The timer can have 3 states: +- *Stopped*. + Can be started with "s" or =M-x pomm-start=. A new iteration of the timer will be started. +- *Paused*. Can be continuted with "s" / =M-x pomm-start= or stopped competely with "S" / =M-x pomm-stop=. +- *Running*. Can be paused with "p" / =M-x pomm-pause= or stopped with "S" / =M-x pomm-stop=. + +The state of the timer can be reset with "R" or =M-x pomm-reset=. + +"U" updates the transient buffer. The update is manual because I didn't figure out how to automate this, and I think this is not /really/ necessary. + +Some settings are available in the transient buffer, but you can customize the relevant variables to make them permanent. Check =M-x customize-group= =pomm= for more information. + +* Alternatives +There is a number of packages with a similar purpose, here are some: +- [[https://github.com/marcinkoziej/org-pomodoro/tree/master][org-pomodoro]] +- [[https://github.com/TatriX/pomidor/][pomidor]] +- [[https://github.com/baudtack/pomodoro.el/][pomodoro.el]] +- [[https://github.com/konr/tomatinho/][tomatinho]] +- [[https://github.com/ferfebles/redtick][redtick]] +Be sure to check those out if this one doesn't quite fit your workflow! + +* P.S. +The package name is not an abbreviation. I just hope it doesn't mean something horrible in some language I don't know. diff --git a/pomm.el b/pomm.el index 9331ee5..0ec9336 100644 --- a/pomm.el +++ b/pomm.el @@ -91,7 +91,7 @@ The format is the same as in `format-seconds'" :type 'string) (defcustom pomm-on-tick-hook nil - "A hook to run on a tick when the timer is running." + "A hook to run on every tick when the timer is running." :group 'pomm :type 'hook) @@ -113,19 +113,29 @@ Current period is also an alist with the following keys: - kind: either 'short-break, 'long-break or 'work - start-time: start timestamp - effective-start-time: start timestamp, corrected for pauses -- iteration: number the current pomodoro iteration") +- iteration: number the current pomodoro iteration + +History is a list of alists with the following keys: +- kind: same as in current +- iteration +- start-time: start timestamp +- end-time: end timestamp +- paused-time: time spent it a paused state") (defvar pomm--timer nil "A variable for the pomm timer.") (defvar pomm-current-mode-line-string nil - "Current mode-line of the pomodoro timer.") + "Current mode-line string of the pomodoro timer. + +Updated by `pomm-update-mode-line-string'.") (defun pomm--do-reset () "Reset the pomodoro timer state." (when pomm--timer (cancel-timer pomm--timer) (setq pomm--timer nil)) + ;; This is necessary to make the reset work with setf on the variable (setq pomm--state `((status . ,'stopped) (current . ,nil) @@ -156,7 +166,7 @@ KIND is the same as in `pomm--state'" :title "Pomodoro")) (defun pomm--new-iteration () - "Initialize state as a new iteration of pomodoro." + "Start a new iteration of the pomodoro timer." (setf (alist-get 'current pomm--state) `((kind . work) (start-time . ,(time-convert nil 'integer)) @@ -181,7 +191,9 @@ KIND is the same as in `pomm--state'" (_ 0)))) (defun pomm--need-switch-p () - "Check if it is necessary to switch a period." + "Check if it is necessary to switch a period. + +The condition is: (effective-start-time + length) < now." (< (+ (alist-get 'effective-start-time (alist-get 'current pomm--state)) (pomm--get-kind-length (alist-get 'kind (alist-get 'current pomm--state)))) @@ -207,6 +219,7 @@ KIND is the same as in `pomm--state'" "Switch to the next period." (let* ((current-kind (alist-get 'kind (alist-get 'current pomm--state))) (current-iteration (alist-get 'iteration (alist-get 'current pomm--state))) + ;; Number of work periods in the current iteration (work-periods (+ (seq-count (lambda (item) (and (= (alist-get 'iteration item) current-iteration) @@ -240,7 +253,6 @@ KIND is the same as in `pomm--state'" (defun pomm--on-tick () "A function to be ran on a timer tick." - (pcase (alist-get 'status pomm--state) ('stopped (when pomm--timer (cancel-timer pomm--timer) @@ -253,7 +265,12 @@ KIND is the same as in `pomm--state'" (run-hooks 'pomm-on-tick-hook))))) (defun pomm--get-time-remaning () - "Get time remaining in the current pomodoro period." + "Get time remaining in the current pomodoro period. + +The formula is: +\(effective-start-time + length\) - now + paused-time, +where paused-time is 0 if status is not 'paused, otherwise: +paused-time := now - last-changed-time" (+ (+ (or (alist-get 'effective-start-time (alist-get 'current pomm--state)) 0) (pomm--get-kind-length @@ -281,12 +298,27 @@ KIND is the same as in `pomm--state'" (format-seconds pomm-remaining-time-format time-remaining)))))) (defun pomm-update-mode-line-string () - "Update the modeline string." + "Update the modeline string for the pomodoro timer. + +This sets the variable `pomm-current-mode-line-string' with a value +from `pomm-format-mode-line'. This is made so to minimize the load on +the modeline, because otherwise updates may be quite frequent. + +To add this to the modeline, add the following code to your config: +\(add-to-list 'mode-line-misc-info '\(:eval pomm-current-mode-line-string\)') +\(add-hook 'pomm-on-tick-hook 'pomm-update-mode-line-string\) +\(add-hook 'pomm-on-tick-hook 'force-mode-line-update\) +\(add-hook 'pomm-on-status-changed-hook 'pomm-update-mode-line-string\) +\(add-hook 'pomm-on-status-changed-hook 'force-mode-line-update)" (setq pomm-current-mode-line-string (pomm-format-mode-line))) ;;;###autoload (defun pomm-start () - "Start or continue the pomodoro timer." + "Start or continue the pomodoro timer. + +- If the timer is not initialized, initialize the state. +- If the timer is stopped, start a new iteration. +- If the timer is paused, unpause the timer." (interactive) (unless pomm--state (pomm--init-state)) @@ -297,7 +329,8 @@ KIND is the same as in `pomm--state'" (alist-get 'effective-start-time (alist-get 'current pomm--state)) (+ (alist-get 'effective-start-time (alist-get 'current pomm--state)) (- (time-convert nil 'integer) (alist-get 'last-changed-time pomm--state))) - (alist-get 'last-changed-time pomm--state) (time-convert nil 'integer)))) + (alist-get 'last-changed-time pomm--state) (time-convert nil 'integer))) + ((eq (alist-get 'status pomm--state) 'running) (message "The timer is running!"))) (run-hooks 'pomm-on-status-changed-hook) (unless pomm--timer (setq pomm--timer (run-with-timer 0 1 'pomm--on-tick)))) @@ -305,21 +338,25 @@ KIND is the same as in `pomm--state'" (defun pomm-stop () "Stop the current iteration of the pomodoro timer." (interactive) - (pomm--store-current-to-history) - (setf (alist-get 'status pomm--state) 'stopped - (alist-get 'current pomm--state) nil - (alist-get 'last-changed-time pomm--state) (time-convert nil 'integer)) - (run-hooks 'pomm-on-status-changed-hook)) + (if (eq (alist-get 'status pomm--state) 'stopped) + (message "The timer is already stopped!") + (pomm--store-current-to-history) + (setf (alist-get 'status pomm--state) 'stopped + (alist-get 'current pomm--state) nil + (alist-get 'last-changed-time pomm--state) (time-convert nil 'integer)) + (run-hooks 'pomm-on-status-changed-hook))) (defun pomm-pause () "Pause the pomodoro timer." (interactive) - (setf (alist-get 'status pomm--state) 'paused - (alist-get 'last-changed-time pomm--state) (time-convert nil 'integer)) - (run-hooks 'pomm-on-status-changed-hook)) + (if (eq (alist-get 'status pomm--state) 'running) + (progn + (setf (alist-get 'status pomm--state) 'paused + (alist-get 'last-changed-time pomm--state) (time-convert nil 'integer)) + (run-hooks 'pomm-on-status-changed-hook)) + (message "The timer is not running!"))) ;;;; Transient - (transient-define-infix pomm--set-short-break-period () :class 'transient-lisp-variable :variable 'pomm-short-break-period @@ -353,23 +390,30 @@ KIND is the same as in `pomm--state'" :key "-p" :description "Number of work periods before long break: " :reader (lambda (&rest _) - (read-number "Number of work periods before long break:" + (read-number "Number of work periods before a long break:" pomm-number-of-periods))) (defclass pomm--transient-current (transient-suffix) - (transient :initform t)) + (transient :initform t) + "A transient class to display the current state of the timer.") -(cl-defmethod transient-init-value ((obj pomm--transient-current)) +(cl-defmethod transient-init-value ((_ pomm--transient-current)) + "A dummy method for `pomm--transient-current'. + +The class doesn't actually have any value, but this is necessary for transient." nil) (defun pomm--get-kind-face (kind) + "Get a face for a KIND of period. + +KIND is the same as in `pomm--state'" (pcase kind ('work 'success) ('short-break 'warning) ('long-break 'error))) -(cl-defmethod transient-format ((obj pomm--transient-current)) - "hello!" +(cl-defmethod transient-format ((_ pomm--transient-current)) + "Format the state of the pomodoro timer." (let ((status (alist-get 'status pomm--state))) (if (or (eq 'stopped status) (not (alist-get 'current pomm--state))) "The timer is not running" @@ -399,13 +443,17 @@ KIND is the same as in `pomm--state'" 'face 'success)))))) (defclass pomm--transient-history (transient-suffix) - (transient :initform t)) + (transient :initform t) + "A transient class to display the history of the pomodoro timer.") -(cl-defmethod transient-init-value ((obj pomm--transient-history)) +(cl-defmethod transient-init-value ((_ pomm--transient-history)) + "A dummy method for `pomm--transient-history'. + +The class doesn't actually have any value, but this is necessary for transient." nil) -(cl-defmethod transient-format ((obj pomm--transient-history)) - "hello!" +(cl-defmethod transient-format ((_ pomm--transient-history)) + "Format the history list for the transient buffer." (if (not (alist-get 'history pomm--state)) "No history yet" (let ((previous-iteration -1)) @@ -431,14 +479,15 @@ KIND is the same as in `pomm--state'" (transient-define-infix pomm--transient-history-suffix () :class 'pomm--transient-history + ;; A dummy key. Seems to be necessary for transient. + ;; Just don't press ~ while in buffer. :key "~~1") (transient-define-infix pomm--transient-current-suffix () :class 'pomm--transient-current :key "~~2") -;;;###autoload (autoload 'pomm "Pomodoro timer" nil t) -(transient-define-prefix pomm () +(transient-define-prefix pomm-transient () ["Settings" (pomm--set-short-break-period) (pomm--set-long-break-period) @@ -457,5 +506,15 @@ KIND is the same as in `pomm--state'" ["History" (pomm--transient-history-suffix)]) +;;;###autoload +(defun pomm () + "A Pomodoro timer. + +This command initialized the state of timer and triggers the transient buffer." + (interactive) + (unless pomm--state + (pomm--init-state)) + (call-interactively #'pomm-transient)) + (provide 'pomm) ;;; pomm.el ends here