diff --git a/README.org b/README.org index 35202cf..15a821a 100644 --- a/README.org +++ b/README.org @@ -2,18 +2,16 @@ [[https://melpa.org/#/pomm][file:https://melpa.org/packages/pomm-badge.svg]] -Yet another implementation of a [[https://en.wikipedia.org/wiki/Pomodoro_Technique][pomodoro timer]] for Emacs. +Implementation of [[https://en.wikipedia.org/wiki/Pomodoro_Technique][Pomodoro]] and [[https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work][Third Time]] techniques for Emacs. [[./img/screenshot.png]] -This particular package features: +Features: - Managing the timer with the excellent [[https://github.com/magit/transient/blob/master/lisp/transient.el][transient.el]]. - Persistent state between Emacs sessions. - The timer state isn't reset if you close Emacs. Also, the state file can be synchronized between machines. + The timer state isn't reset if you close Emacs. If necessary, the state file can be synchronized between machines. - History. - I've implemented an option to store the timer history in a CSV file. Eventually, I want to join this with [[https://activitywatch.net/][other activity data]] to see if the state of the timer changes how I use the computer. - -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. + History of the timer can be stored in a CSV file. Eventually, I want to join this with [[https://activitywatch.net/][other activity data]] to see if the state of the timer changes how I use the computer. * Installation The package is available on MELPA. Install it however you usually install Emacs packages, e.g. @@ -25,14 +23,7 @@ My preferred way is =use-package= with =straight.el=: #+begin_src emacs-lisp (use-package pomm :straight t - :commands (pomm)) -#+end_src - -If you want sounds before the MELPA recipe got updated to include resources, use: -#+begin_src emacs-lisp -(use-package pomm - :straight (:host github :repo "SqrtMinusOne/pomm.el" :files (:defaults "resources")) - :commands (pomm)) + :commands (pomm pomm-thrid-time)) #+end_src Or you can clone the repository, add the package to the =load-path= and load it with =require=: @@ -42,6 +33,7 @@ Or you can clone the repository, add the package to the =load-path= and load it The package requires Emacs 27.1 because the time API of the previous versions is kinda crazy and 27.1 has =time-convert=. * Usage +** Pomodoro Run =M-x pomm= to open the transient buffer. The listed commands are rather self-descriptive and match the Pomodoro ideology. @@ -56,9 +48,27 @@ 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. With "r" or =M-x pomm-set-context= you can set the current "context", that is some description of the task you are currently working on. This description will show up in history and in the csv file. Also, =M-x pomm-start-with-context= will prompt for the context and then start the timer. +** Third Time +Run =M-x pomm-third-time= to open the transient buffer for the Third Time technique. + +[[./img/screenshot-tt.png]] + +Essentially, the techique is designed aroud the formula: + +#+begin_example +Time of break = 1/3 x Time of work. +#+end_example + +I.e. you work as long as you want or need, and then take a break with the maximum duration =1/3= of the time worked. If you take a shorter break, the remaining break time is saved and added to the next break within the same session. [[https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work][Here is a more detailed explanation]]. + +The Third Time timer can have 2 states: +- *Stopped*. Can be started with "s" or =M-x pomm-third-time-start=. +- *Running*. Can be stopped with "S" or =M-x pomm-third-time-stop=. This resets the accumulated break time. + +Use "b" or =M-x pomm-third-time-switch= to switch the current period type (work or break). If the break time runs out, the timer automatically switches to work. * Customization -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. +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= and =M-x customize-group pomm-third-time= for more information. ** 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: @@ -67,7 +77,7 @@ The package sends alerts via =alert.el=. The default style of alert is a plain = #+end_src ** Sounds -By default sounds are disabled. Set =pomm-audio-enabled= to =t= to toggle them. +By default sounds are disabled. Set =pomm-audio-enabled= to =t= to toggle them. Set =pomm-audio-tick-enabled= to =t= if you want the ticking sound. This functionality needs =pomm-audio-player-executable= to be set so that the program could be invoked like: = /path/to/sound.wav=. @@ -98,17 +108,23 @@ interval = 1 #+end_src ** State file location -The package stores the current state to a file by the path =pomm-state-file-location=, which is =emacs.d/pomm= by default. Set it to wherever you like. -** History -If you set the =pomm-csv-history-file= variable, the package will write CSV with the usage history there. Just keep in mind that the parent directory has to exist. +To implement pesistence between Emacs sessions, the package stores its state in the following files: +- =pomm-state-file-location=, =.emacs.d/pomm= by default +- =pomm-third-time-state-file-location=, =/.emacs.d/pomm-third-time= by default -The file has the following columns: +Set these paths however like. +** History +If you set the =pomm-csv-history-file= (and/or =pomm-third-time-csv-history-file=) variable, the package will log its history in CSV format. Just keep in mind that the parent directory has to exist. + +The file for the Pomodoro technique has the following columns: - =timestamp= - =status= (=stopped=, =paused= or =running=, according to the [[*Usage][usage]] section) - =kind= (=work=, =short-break=, =long-break= or =nil=) - =iteration= - =context= +One for the Third Time technique has an extra column called =break-time-remaining=. + A new entry is written after a particular state of the timer comes into being. To customize timestamp, set the =pomm-csv-history-file-timestamp-format= variable. For example, for traditional =YYYY-MM-DD HH:mm:ss=: @@ -122,7 +138,7 @@ The format is the same as in =format-time-string=. There is a number of packages with a similar purpose, here is a rough comparison of features: | Package | 3rd party integrations | Control method (1) | Persistent history | Persistent state | Notifications | |------------------------+------------------------+--------------------------------+--------------------------+----------------------------------------------+---------------------------| -| [[https://github.com/SqrtMinusOne/pomm.el][pomm.el]] | - | transient.el | CSV | + | alert.el | +| [[https://github.com/SqrtMinusOne/pomm.el][pomm.el]] | - | transient.el | CSV | + | alert.el + sounds | | [[https://github.com/marcinkoziej/org-pomodoro/tree/master][org-pomodoro]] | Org Mode! | via Org commands | via Org mode | - | alert.el + sounds | | [[https://github.com/TatriX/pomidor/][pomidor]] | - | self-cooked interactive buffer | custom delimited format? | +, but saving on-demand | alert.el + sounds | | [[https://github.com/baudtack/pomodoro.el/][pomodoro.el]] | - | - | - | - | notifications.el + sounds | @@ -133,6 +149,8 @@ Be sure to check those out if this one doesn't quite fit your workflow! (1) Means of timer control with exception of Emacs interactive commands +Also take a look at [[https://github.com/telotortium/org-pomodoro-third-time][org-pomodoro-third-time]], which adapts =org-pomodoro= for the Third Time technique. + * 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/img/screenshot-tt.png b/img/screenshot-tt.png new file mode 100644 index 0000000..443badd Binary files /dev/null and b/img/screenshot-tt.png differ diff --git a/pomm-third-time.el b/pomm-third-time.el new file mode 100644 index 0000000..e8e4361 --- /dev/null +++ b/pomm-third-time.el @@ -0,0 +1,673 @@ +;;; pomm-third-time.el --- Implementation of the third time technique in Emacs -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Korytov Pavel + +;; Author: Korytov Pavel +;; Maintainer: Korytov Pavel +;; Homepage: https://github.com/SqrtMinusOne/pomm.el + +;; This file is NOT part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This is an implementation of the third time technique. Take a look +;; at the `pomm-third-time' function for the general idea. +;; +;; This file reuses some parts from the main `pomm' file, but a lot of +;; functionality is duplicated. + +;;; Code: +(require 'pomm) +(require 'alert) +(require 'transient) +(require 'calc) + +(defgroup pomm-third-time nil + "Third Time timer implementation." + :group 'pomm) + +(defcustom pomm-third-time-fraction "1/3" + "Fraction of break time to work time. + +Can be string or number, a string is interpreted with +`calc-eval'." + :group 'pomm-third-time + :type '(choice (string :tag "String") + (number :tag "Number"))) + +(defcustom pomm-third-time-on-status-changed-hook nil + "A hook to run on a status change." + :group 'pomm-third-time + :type 'hook) + +(defcustom pomm-third-time-on-tick-hook nil + "A hook to run on every tick when the timer is running." + :group 'pomm-third-time + :type 'hook) + +(defcustom pomm-third-time-state-file-location + (locate-user-emacs-file "pomm-third-time") + "Location of the `pomm-third-time' state file." + :group 'pomm-third-time + :type 'string) + +(defcustom pomm-third-time-break-message "Take a break!" + "Message for a start of a short break period." + :group 'pomm + :type 'string) + +(defcustom pomm-third-time-csv-history-file nil + "If non-nil, save timer history in a CSV format. + +The parent directory has to exist! + +A new entry is written whenever the timer changes status or kind +of period. The format is as follows: +- timestamp +- status +- kind +- iteration +- break-time-remaining +- context" + :group 'pomm-third-time + :type '(choice (string :tag "Path") + (const nil :tag "Do not save"))) + +(defvar pomm-third-time--state nil + "The current state of the Third Time timer. + +This is an alist with the following keys: +- status: either 'stopped or 'running + (having a pause state seems to make little sense here) +- current: an alist with a current period +- history: a list with history for today +- last-changed-time: a timestamp of the last change in status +- context: a string that describes the current task + +'current is an alist with the following keys: +- kind: either 'work or 'break +- start-time: start timestamp +- break-time-bank: break time, postpone from previous periods +- iteration: number of the current 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 +- context: context") + +(defvar pomm-third-time--timer nil + "A variable for the Third Time timer.") + +(defun pomm-third-time--do-reset () + "Reset the Third Time timer state." + (when pomm-third-time--timer + (cancel-timer pomm-third-time--timer) + (setq pomm-third-time--timer nil)) + (setq pomm-third-time--state + `((status . , 'stopped) + (current . ,nil) + (history . ,nil) + (last-changed-time ,(time-convert nil 'integer))) + pomm-current-mode-line-string "") + (setf (alist-get 'status pomm-third-time--state) 'stopped) + (run-hooks 'pomm-on-status-changed-hook)) + +(defun pomm-third-time--init-state () + "Initialize the Third Time timer state." + (add-hook 'pomm-third-time-on-status-changed-hook #'pomm-third-time--save-state) + (add-hook 'pomm-third-time-on-status-changed-hook #'pomm-third-time--maybe-save-csv) + (add-hook 'pomm-third-time-on-status-changed-hook + #'pomm-third-time--dispatch-current-sound) + (add-hook 'pomm-mode-line-mode-hook + #'pomm-third-time--setup-mode-line) + (pomm-third-time--setup-mode-line) + (if (or (not (file-exists-p pomm-third-time-state-file-location)) + (not pomm-third-time-state-file-location)) + (pomm-third-time--do-reset) + (with-temp-buffer + (insert-file-contents pomm-third-time-state-file-location) + (let ((data (buffer-substring (point-min) (point-max)))) + (if (not (string-empty-p data)) + (setq pomm-third-time--state (car (read-from-string data))) + (pomm-third-time--do-reset))))) + (pomm-third-time--cleanup-old-history) + (when (eq (alist-get 'status pomm-third-time--state) 'running) + (setq pomm--timer (run-with-timer 0 1 #'pomm-third-time--on-tick)))) + +(defun pomm-third-time--save-state () + "Save the current Third Time timer state." + (when pomm-third-time-state-file-location + (with-temp-file pomm-third-time-state-file-location + (insert (prin1-to-string pomm-third-time--state))))) + +(defun pomm-third-time--cleanup-old-history () + "Clear history of previous days from the Third Time timer." + (let ((cleanup-time (decode-time))) + (setf (decoded-time-second cleanup-time) 0 + (decoded-time-minute cleanup-time) 0 + (decoded-time-hour cleanup-time) pomm-history-reset-hour) + + (let ((cleanup-timestamp (time-convert (encode-time cleanup-time) 'integer))) + (setf (alist-get 'history pomm-third-time--state) + (seq-filter + (lambda (item) + (> (alist-get 'start-time item) cleanup-timestamp)) + (alist-get 'history pomm-third-time--state)))))) + +(defun pomm-third-time--maybe-save-csv () + "Log the current state of the timer to a CSV history file. + +Set `pomm-third-time-csv-history-file' to customize the file location. +If the variable is nil, the function does nothing." + (when pomm-third-time-csv-history-file + (unless (file-exists-p pomm-third-time-csv-history-file) + (with-temp-file pomm-third-time-csv-history-file + (insert "timestamp,status,period,iteration,break-time-remaining,context\n"))) + (write-region + (format "%s,%s,%s,%d,%d,%s\n" + (format-time-string pomm-csv-history-file-timestamp-format) + (symbol-name (alist-get 'status pomm-third-time--state)) + (symbol-name (alist-get 'kind (alist-get 'current pomm-third-time--state))) + (or (alist-get 'iteration (alist-get 'current pomm-third-time--state)) 0) + (pomm-third-time--break-time) + (alist-get 'context pomm--state)) + nil pomm-third-time-csv-history-file 'append 1))) + +(transient-define-prefix pomm-third-time-reset () + ["Are you sure you want to reset the Third Time timer?" + ("y" "Yes" (lambda () (interactive) (pomm-third-time--do-reset))) + ("n" "No" transient-quit-one)]) + +(defun pomm-third-time--dispatch-current-sound () + "Dispatch an appropriate sound for the current state of the timer." + (cond + ((eq (alist-get 'status pomm-third-time--state) 'stopped) + (pomm--maybe-play-sound 'stop)) + ((eq (alist-get 'status pomm-third-time--state) 'running) + (pomm--maybe-play-sound + (alist-get 'kind (alist-get 'current pomm-third-time--state)))))) + +(defun pomm-third-time--calc-eval (value) + "Evaluate VALUE and return number. + +If VALUE is not a string, return it. + +Otherwise, try to evaluate with `calc-eval'. If unsuccessful, return +the calc error. If the result is numeric, convert it to number and +return it, otherwise, return a value like a calc error." + (if (stringp value) + (let ((res (calc-eval value))) + (if (listp res) + res + (if (string-match-p (rx (+ num) (? (: "." (* num)))) value) + (string-to-number res) + (list nil (format "Can't parse number: %s" res))))) + value)) + +(defun pomm-third-time--fraction () + "Get fraction of break time to work time." + (let ((parsed (pomm-third-time--calc-eval + pomm-third-time-fraction))) + (if (listp parsed) + (user-error "Error in `pomm-third-time-fraction': %s" (nth 1 parsed)) + parsed))) + +(defun pomm-third-time--current-period-time () + "Get the time spent in the current period." + (if-let ((time (alist-get 'start-time (alist-get 'current pomm-third-time--state)))) + (- (time-convert nil 'integer) time) + 0)) + +(defun pomm-third-time--break-time () + "Get the available break time." + (max + (+ (float + (or (alist-get 'break-time-bank + (alist-get 'current pomm-third-time--state)) + 0)) + (pcase (alist-get 'kind (alist-get 'current pomm-third-time--state)) + ('work (* (pomm-third-time--fraction) + (pomm-third-time--current-period-time))) + ('break (- (pomm-third-time--current-period-time))) + ('nil 0))) + 0)) + +(defun pomm-third-time--worked-time () + "Get total time worked in the current iteration." + (let ((iteration (alist-get 'iteration + (alist-get 'current pomm-third-time--state))) + (current-kind (alist-get 'kind (alist-get 'current pomm-third-time--state)))) + (apply + #'+ + (if (eq current-kind 'work) + (pomm-third-time--current-period-time) + 0) + (mapcar + (lambda (item) + (- (alist-get 'end-time item) + (alist-get 'start-time item))) + (seq-filter + (lambda (item) + (and (= (alist-get 'iteration item) iteration) + (eq (alist-get 'kind item) 'work))) + (alist-get 'history pomm-third-time--state)))))) + +(defun pomm-third-time--need-switch-p () + "Check if the break period has to end." + (and + (eq (alist-get 'kind (alist-get 'current pomm-third-time--state)) 'break) + (<= (pomm-third-time--break-time) 0))) + +(defun pomm-third-time--store-current-to-history () + "Store the timer state to history." + (let ((current-kind (alist-get 'kind (alist-get 'current pomm-third-time--state))) + (current-start-time (alist-get 'start-time + (alist-get 'current pomm-third-time--state))) + (current-iteration (alist-get 'iteration + (alist-get 'current pomm-third-time--state))) + (current-context (alist-get 'context pomm-third-time--state))) + (when current-kind + (push `((kind . ,current-kind) + (start-time . ,current-start-time) + (end-time . ,(time-convert nil 'integer)) + (iteration . ,current-iteration) + (context . ,current-context)) + (alist-get 'history pomm-third-time--state))))) + +(defun pomm-third-time--format-period (seconds) + "Format SECONDS into string." + (if (>= seconds (* 60 60)) + (format-seconds "%.2h:%.2m:%.2s" seconds) + (format-seconds "%.2m:%.2s" seconds))) + +(defun pomm-third-time--dispatch-notification (kind) + "Dispatch a notification about a start of a period. + +KIND is the same as in `pomm-third-time--state'" + (alert + (pcase kind + ('break (concat pomm-third-time-break-message + (format "\nTime available: %s" + (pomm-third-time--format-period + (pomm-third-time--break-time))))) + ('work (concat pomm-work-message + (when (> (pomm-third-time--break-time) 0) + (format "\nBreak time remaining: %s" + (pomm-third-time--format-period + (pomm-third-time--break-time))))))) + :title "Pomodoro")) + +(defun pomm-third-time--switch () + "Switch between periods." + (let* ((current-kind (alist-get 'kind (alist-get 'current pomm-third-time--state))) + (break-time (pomm-third-time--break-time)) + (iteration (alist-get 'iteration + (alist-get 'current pomm-third-time--state))) + (next-kind (pcase current-kind + ('work 'break) + ('break 'work)))) + (pomm-third-time--store-current-to-history) + (setf (alist-get 'current pomm-third-time--state) + `((kind . ,next-kind) + (start-time . ,(time-convert nil 'integer)) + (break-time-bank . ,break-time) + (iteration . ,iteration))) + (pomm-third-time--dispatch-notification next-kind) + (pomm-third-time--save-state) + (run-hooks 'pomm-third-time-on-status-changed-hook))) + +(defun pomm-third-time--on-tick () + "Function to execute on each timer tick." + (pcase (alist-get 'status pomm-third-time--state) + ('stopped (when pomm-third-time--timer + (cancel-timer pomm-third-time--timer) + (setq pomm-third-time--timer nil))) + ('running + (when (pomm-third-time--need-switch-p) + (pomm-third-time--switch)) + (run-hooks 'pomm-third-time-on-tick-hook) + (when (eq (alist-get 'kind (alist-get 'current pomm-third-time--state)) 'work) + (pomm--maybe-play-sound 'tick))))) + +(defun pomm-third-time--new-iteration () + "Start a new iteration of the Third Time timer." + (setf (alist-get 'current pomm-third-time--state) + `((kind . work) + (start-time . ,(time-convert nil 'integer)) + (break-time-bank . 0) + (iteration . ,(1+ (seq-max + (cons 0 + (mapcar + (lambda (h) (alist-get 'iteration h)) + (alist-get 'history pomm-third-time--state))))))) + (alist-get 'status pomm-third-time--state) 'running + (alist-get 'last-changed-time pomm-third-time--state) (time-convert nil 'integer)) + (pomm-third-time--dispatch-notification 'work)) + +;;;###autoload +(defun pomm-third-time-start () + "Start the Third Time timer. + +Take a look at the `pomm-third-time' function for more details." + (interactive) + (unless pomm-third-time--state + (pomm-third-time--init-state)) + (pcase (alist-get 'status pomm-third-time--state) + ('stopped (pomm-third-time--new-iteration) + (run-hooks 'pomm-third-time-on-status-changed-hook)) + ('running (message "The timer is running!"))) + (unless pomm-third-time--timer + (setq pomm-third-time--timer (run-with-timer 0 1 'pomm-third-time--on-tick)))) + +(defun pomm-third-time--running-p () + "Check if the timer is running." + (eq (alist-get 'status pomm-third-time--state) 'running)) + +(transient-define-prefix pomm-third-time-stop () + ["This will reset the accumulated break time. Continue?" + ("y" "Yes" (lambda () (interactive) + (unless (pomm-third-time--can-stop-p) + (user-error "The timer is not running!")) + (pomm-third-time--store-current-to-history) + (setf (alist-get 'status pomm-third-time--state) 'stopped + (alist-get 'current pomm-third-time--state) nil + (alist-get 'last-changed-time pomm-third-time--state) + (time-convert nil 'integer)) + (run-hooks 'pomm-third-time-on-status-changed-hook) + (when pomm-reset-context-on-iteration-end + (setf (alist-get 'context pomm-third-time--state) nil)))) + ("n" "No" transient-quit-one)]) + +(defun pomm-third-time-switch () + "Toggle work/break in the Third Time timer." + (interactive) + (pomm-third-time--switch)) + +(defun pomm-third-time-format-mode-line () + "Format the modeline string for the Third Time timer." + (let ((current-status (alist-get 'status pomm-third-time--state))) + (if (or (eq current-status 'stopped) + (not (alist-get 'current pomm-third-time--state))) + "" + (let ((current-kind (alist-get 'kind (alist-get 'current pomm-third-time--state)))) + (format "[%s] %s (%s) " + current-kind + (pomm-third-time--format-period + (pomm-third-time--current-period-time)) + (pomm-third-time--format-period + (pomm-third-time--break-time))))))) + +(defun pomm-third-time-update-mode-string () + "Update modeline for the Third Time timer." + (setq pomm-current-mode-line-string (pomm-third-time-format-mode-line))) + +(defun pomm-third-time--setup-mode-line () + "Setup `pomm-mode-line-mode' to work with `pomm-third-time'." + (if pomm-mode-line-mode + (progn + (add-hook 'pomm-third-time-on-tick-hook #'pomm-third-time-update-mode-string) + (add-hook 'pomm-third-time-on-tick-hook #'force-mode-line-update) + (add-hook 'pomm-third-time-on-status-changed-hook #'pomm-third-time-update-mode-string) + (add-hook 'pomm-third-time-on-status-changed-hook #'force-mode-line-update)) + (remove-hook 'pomm-third-time-on-tick-hook #'pomm-third-time-update-mode-string) + (remove-hook 'pomm-third-time-on-tick-hook #'force-mode-line-update) + (remove-hook 'pomm-third-time-on-status-changed-hook #'pomm-third-time-update-mode-string) + (remove-hook 'pomm-third-time-on-status-changed-hook #'force-mode-line-update))) + +(defun pomm-third-time-set-context () + "Set the current context for the Third Time timer." + (interactive) + (setf (alist-get 'context pomm-third-time--state) + (prin1-to-string (read-minibuffer "Context: " (current-word))))) + +;;;###autoload +(defun pomm-third-time-start-with-context () + "Prompt for context call call `pomm-third-time-start'." + (interactive) + (pomm-third-time-set-context) + (pomm-third-time-start)) + +;;;; Transient +(defun pomm-third-time--completing-read-calc () + "Do `completing-read' with `calc-eval'." + (let ((res (completing-read + "Time: " + (lambda (string _ flag) + (when (eq flag 'metadata) + (let ((res (pomm-third-time--calc-eval string))) + (if (listp res) + (message (nth 1 res)) + (message "%f" res)))))))) + (let ((eval-res (pomm-third-time--calc-eval res))) + (if (listp eval-res) + (user-error "Bad value: %s" (nth 1 eval-res)) + res)))) + +(transient-define-infix pomm-third-time--set-fraction () + :class 'transient-lisp-variable + :variable 'pomm-third-time-fraction + :key "-f" + :description "Fraction of break time to work time:" + :reader (lambda (&rest _) + (let ((current-value pomm-third-time-fraction)) + (condition-case error + (pomm-third-time--completing-read-calc) + (error (progn + (message "%s" error) + current-value)))))) + +(transient-define-infix pomm-third-time-set-reset-context-on-iteration-end () + :class 'pomm--transient-lisp-variable-switch + :variable 'pomm-reset-context-on-iteration-end + :argument "--context-reset" + :key "-r" + :description "Reset the context on the interation end") + +(defclass pomm-third-time--set-context-infix (transient-variable) + ((transient :initform 'transient--do-call) + (always-read :initform t))) + +(cl-defmethod transient-init-value ((_ pomm-third-time--set-context-infix)) + "Initialize the value of context infix from `pomm-third-time-state'." + (alist-get 'context pomm-third-time--state)) + +(cl-defmethod transient-infix-set ((_ pomm-third-time--set-context-infix) value) + "Update `pomm-third-time-start' with VALUE from the context infix." + (setf (alist-get 'context pomm-third-time--state) value)) + +(cl-defmethod transient-prompt ((_ pomm-third-time--set-context-infix)) + "Return the prompt text for the context infix." + "Set context: ") + +(cl-defmethod transient-format-value ((_ pomm-third-time--set-context-infix)) + "Format value for the context infix." + (propertize (if-let (val (alist-get 'context pomm-third-time--state)) + (prin1-to-string val) + "unset") + 'face 'transient-value)) + +(transient-define-infix pomm-third-time--set-context () + :class 'pomm-third-time--set-context-infix + :key "-c" + :description "Context:") + +(defclass pomm-third-time--transient-current (transient-suffix) + ((transient :initform t)) + "A transient class to display the current state of the timer.") + +(cl-defmethod transient-init-value ((_ pomm-third-time--transient-current)) + "A dummy method for `pomm-third-time--transient-current'. + +The class doesn't actually have any value, but this is necessary for transient." + nil) + +(defun pomm-third-time--get-kind-face (kind) + "Get a face for a KIND of period. + +KIND is the same as in `pomm-third-time--state'" + (pcase kind + ('work 'success) + ('break 'error))) + +(cl-defmethod transient-format ((_ pomm-third-time--transient-current)) + "Format the state of the Third Time timer." + (let ((status (alist-get 'status pomm-third-time--state))) + (if (or (eq 'stopped status) (not (alist-get 'current pomm-third-time--state))) + "The timer is not running" + (let ((kind (alist-get 'kind (alist-get 'current pomm-third-time--state))) + (start-time (alist-get 'start-time + (alist-get 'current pomm-third-time--state))) + (iteration (alist-get 'iteration + (alist-get 'current pomm-third-time--state))) + (break-time (pomm-third-time--break-time)) + (period-time (pomm-third-time--current-period-time))) + (concat + (format "Iteration #%d. " iteration) + "State: " + (propertize + (upcase (symbol-name kind)) + 'face + (pomm-third-time--get-kind-face kind)) + ". Time: " + (propertize + (pomm-third-time--format-period period-time) + 'face 'success) + " (started at " + (propertize + (format-time-string "%H:%M:%S" (seconds-to-time start-time)) + 'face 'success) + ")\nAvailable break time: " + (propertize + (pomm-third-time--format-period break-time) + 'face 'success) + ". Total time worked: " + (propertize + (pomm-third-time--format-period (pomm-third-time--worked-time)) + 'face 'success)))))) + +(defclass pomm-third-time--transient-history (transient-suffix) + ((transient :initform t)) + "A transient class to display the history of the pomodoro timer.") + +(cl-defmethod transient-init-value ((_ pomm-third-time--transient-history)) + "A dummy method for `pomm-third-time--transient-history'. + +The class doesn't actually have any value, but this is necessary for transient." + nil) + +(cl-defmethod transient-format ((_ pomm-third-time--transient-history)) + "Format the history list for the transient buffer." + (if (not (alist-get 'history pomm-third-time--state)) + "No history yet" + (let ((previous-iteration 1000)) + (mapconcat + (lambda (item) + (let ((kind (alist-get 'kind item)) + (iteration (alist-get 'iteration item)) + (start-time (alist-get 'start-time item)) + (end-time (alist-get 'end-time item)) + (context (alist-get 'context item))) + (concat + (if (< iteration previous-iteration) + (let ((is-first (= previous-iteration 1000))) + (setq previous-iteration iteration) + (if is-first + "" + "\n")) + "") + (format "[%02d] " iteration) + (propertize + (format "%12s " (upcase (symbol-name kind))) + 'face (pomm-third-time--get-kind-face kind)) + (format-time-string "%H:%M" (seconds-to-time start-time)) + "-" + (format-time-string "%H:%M" (seconds-to-time end-time)) + (if context + (format " : %s" (propertize context 'face 'transient-value)) + "")))) + (alist-get 'history pomm-third-time--state) + "\n")))) + +(transient-define-infix pomm-third-time--transient-current-suffix () + :class 'pomm-third-time--transient-current + :key "~~2") + +(transient-define-infix pomm-third-time--transient-history-suffix () + :class 'pomm-third-time--transient-history + :key "~~1") + +(transient-define-prefix pomm-third-time-transient () + ["Timer settings" + (pomm-third-time--set-fraction)] + ["Context settings" + (pomm-third-time--set-context) + (pomm-third-time-set-reset-context-on-iteration-end)] + ["Commands" + :class transient-row + ("s" "Start the timer" pomm-third-time-start :transient t) + ;; XXX I tried to use the `:if' predicate here, but unfortunately + ;; visibilty doesn't refresh with `:transient t' + ("S" "Stop the timer" pomm-third-time-stop :transient t) + ("b" "Switch work/break" pomm-third-time-switch :transient t) + ("R" "Reset" pomm-third-time-reset :transient t) + ("u" "Update" pomm--transient-update :transient t) + ("q" "Quit" transient-quit-one)] + ["Status" + (pomm-third-time--transient-current-suffix)] + ["History" + (pomm-third-time--transient-history-suffix)]) + +;;;###autoload +(defun pomm-third-time () + "Implementation of the Third Time timer in Emacs. + +The idea of the technique is as follows: +- Work as long as you need, take a break as 1/3 of the work time (the + fraction of work time to break time is set in + `pomm-third-time-fraction') +- If you've ended a break early, unused break time is saved and added + to the next break within the same session. +- If you've finished the session, either to take a longer break or to + end working, remaining break time is discarded. Each session starts + from a clean slate. + +The timer can have two states: +- Stopped. + Can be started with 's' or `pomm-third-time-start'. +- Running. + Can be stopped with 'S' or `pomm-third-time-stop'. + +If the timer is running, the current period type (work or break) can +be switched by 'b' or `pomm-third-time-switch'. If the break time +runs out, the timer automatically switches to work. + +The timer supports setting \"context\", for example, a task on which +you're working on. It can be set with '-c' or +`pomm-third-time-set-context'. This is useful together with CSV +logging, which is enabled if `pomm-third-time-csv-history-file' is +non-nil. + +Enable `pomm-mode-line-mode' to display the timer state in the +modeline." + (interactive) + (unless pomm-third-time--state + (pomm-third-time--init-state)) + (call-interactively #'pomm-third-time-transient)) + +(provide 'pomm-third-time) +;;; pomm-third-time.el ends here diff --git a/pomm.el b/pomm.el index cda9c71..8e15542 100644 --- a/pomm.el +++ b/pomm.el @@ -1,10 +1,10 @@ -;;; pomm.el --- Yet another Pomodoro timer implementation -*- lexical-binding: t -*- +;;; pomm.el --- Pomodoro and Third Time timers -*- lexical-binding: t -*- -;; Copyright (C) 2021 Korytov Pavel +;; Copyright (C) 2022 Korytov Pavel ;; Author: Korytov Pavel ;; Maintainer: Korytov Pavel -;; Version: 0.1.4 +;; Version: 0.2.0 ;; Package-Requires: ((emacs "27.1") (alert "1.2") (seq "2.22") (transient "0.3.0")) ;; Homepage: https://github.com/SqrtMinusOne/pomm.el @@ -25,14 +25,16 @@ ;;; Commentary: -;; An implementation of a Pomodoro timer for Emacs. Distintive features -;; of this particular implementation: -;; - Managing the timer with transient.el (`pomm' command) +;; Implementation of two time management methods in Emacs: Pomodoro +;; and Third Time. +;; This implementation features: +;; - Managing the timer with transient.el ;; - Persistent state between Emacs sessions. ;; So one could close & reopen Emacs without interruption the timer. ;; -;; Take a look at `pomm-update-mode-line-string' on how to setup this -;; package with a modeline. +;; Main entrypoints are: `pomm' for Pomodoro and `pomm-third-time' for +;; Third Time. +;; ;; Also take a look at README at ;; for more information. @@ -43,7 +45,7 @@ (require 'transient) (defgroup pomm nil - "Yet another Pomodoro timer implementation." + "Pomodoro and Third Time timers." :group 'tools) (defcustom pomm-work-period 25 @@ -77,12 +79,12 @@ :type 'string) (defcustom pomm-ask-before-long-break t - "Ask a user whether to do a long break or stop the pomodoros." + "Ask the user whether to do a long break or stop the pomodoros." :group 'pomm :type 'boolean) (defcustom pomm-ask-before-work nil - "Ask a user whether to start a new pomodoro period." + "Ask the user whether to start a new pomodoro period." :group 'pomm :type 'boolean) @@ -120,9 +122,9 @@ The format is the same as in `format-seconds'" :type 'string) (defcustom pomm-csv-history-file nil - "The csv history file location. + "If non-nil, save timer history in a CSV format. -The parent directory has to exists! +The parent directory has to exist! A new entry is written whenever the timer changes status or kind of period. The format is as follows: @@ -173,9 +175,8 @@ When loading the package, `load-file-name' should point to the location of this file, which means that resources folder should be in the same directory. -If the file is evaluated interactively (for development -purposes), the `default-directory' is most likely the project -root." +If the file is evaluated interactively (for development purposes), the +`default-directory' variable is most likely the project root." (or (and load-file-name (concat (file-name-directory load-file-name) name)) (concat default-directory name))) @@ -183,6 +184,7 @@ root." `((work . ,(pomm--get-sound-file-path "resources/bell.wav")) (tick . ,(pomm--get-sound-file-path "resources/tick.wav")) (short-break . ,(pomm--get-sound-file-path "resources/bell.wav")) + (break . ,(pomm--get-sound-file-path "resources/bell.wav")) (long-break . ,(pomm--get-sound-file-path "resources/bell.wav")) (stop . ,(pomm--get-sound-file-path "resources/air_horn.wav"))) "Paths to the sounds to play on various events. @@ -191,7 +193,7 @@ Each element of the list is a cons cell, where: - key is an event type - value is either a path to the sound file or nil." :group 'pomm - :options '(work tick short-break long-break stop) + :options '(work tick break short-break long-break stop) :type '(alist :key-type (symbol :tag "Event") :value-type (choice (string :tag "Path") (const nil :tag "No sound")))) @@ -206,18 +208,13 @@ Each element of the list is a cons cell, where: :group 'pomm :type 'hook) -(defcustom pomm-on-period-changed-hook nil - "A hook to run on a period status change." - :group 'pomm - :type 'hook) - (defvar pomm--state nil - "The current state of pomm.el. + "The current state of the Pomodoro timer. -This is an alist of with the following keys: +This is an alist with the following keys: - status: either 'stopped, 'paused or 'running - current: an alist with a current period -- history: a list with today's history +- history: a list with history for today - last-changed-time: a timestamp of the last change in status - context: a string that describes the current task @@ -225,14 +222,15 @@ This is an alist of 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 of 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 in a paused state") +- paused-time: time spent in a paused state +- context: current context.") (defvar pomm--timer nil "A variable for the pomm timer.") @@ -260,11 +258,10 @@ Updated by `pomm-update-mode-line-string'.") (defun pomm--init-state () "Initialize the Pomodoro timer state. -This function is meant to be ran only once, at the first start of the timer." +This function is meant to be executed only once, at the first +start of the timer." (add-hook 'pomm-on-status-changed-hook #'pomm--save-state) (add-hook 'pomm-on-status-changed-hook #'pomm--maybe-save-csv) - (add-hook 'pomm-on-period-changed-hook #'pomm--maybe-save-csv) - (add-hook 'pomm-on-period-changed-hook #'pomm--dispatch-current-sound) (add-hook 'pomm-on-status-changed-hook #'pomm--dispatch-current-sound) (if (or (not (file-exists-p pomm-state-file-location)) (not pomm-state-file-location)) @@ -275,7 +272,9 @@ This function is meant to be ran only once, at the first start of the timer." (if (not (string-empty-p data)) (setq pomm--state (car (read-from-string data))) (pomm--do-reset))))) - (pomm--cleanup-old-history)) + (pomm--cleanup-old-history) + (when (eq (alist-get 'status pomm--state) 'running) + (setq pomm--timer (run-with-timer 0 1 #'pomm--on-tick)))) (defun pomm--save-state () "Save the current Pomodoro timer state." @@ -315,11 +314,10 @@ variable doesn't exist, function does nothing." (or (alist-get 'context pomm--state) "")) nil pomm-csv-history-file 'append 1))) -(defun pomm-reset () - "Reset the Pomodoro timer." - (interactive) - (when (y-or-n-p "Are you sure you want to reset the Pomodoro timer? ") - (pomm--do-reset))) +(transient-define-prefix pomm-reset () + ["Are you sure you want to reset the Pomodoro timer?" + ("y" "Yes" (lambda () (interactive) (pomm--do-reset))) + ("n" "No" transient-quit-one)]) (defun pomm--maybe-play-sound (kind) "Play a sound of KIND. @@ -355,7 +353,7 @@ which can be played by `pomm-audio-player-executable'." KIND is the same as in `pomm--state'" (alert (pcase kind - ('short-break pomm-short-break-message) + ((or 'break 'short-break) pomm-short-break-message) ('long-break pomm-long-break-message) ('work pomm-work-message)) :title "Pomodoro")) @@ -458,7 +456,7 @@ The condition is: (effective-start-time + length) < now." (setf (alist-get 'context pomm--state) nil)))) (defun pomm--on-tick () - "A function to be ran on a timer tick." + "A function to execute on each timer tick." (pcase (alist-get 'status pomm--state) ('stopped (when pomm--timer (cancel-timer pomm--timer) @@ -565,6 +563,7 @@ minor mode." (setf (alist-get 'context pomm--state) (prin1-to-string (read-minibuffer "Context: " (current-word))))) +;;;###autoload (defun pomm-start-with-context () "Prompt for context and call `pomm-start'." (interactive) @@ -631,21 +630,30 @@ minor mode." (read-number "Number of work periods before a long break:" pomm-number-of-periods))) -(defclass pomm--set-context-on-iteration-end-infix (transient-switch) - ((transient :initform t)) - "A transient class to toggle `pomm-reset-context-on-iteration-end'.") +(defclass pomm--transient-lisp-variable-switch (transient-switch) + ((transient :initform t) + (variable :initarg :variable))) -(cl-defmethod transient-init-value ((obj pomm--set-context-on-iteration-end-infix)) +(cl-defmethod transient-init-value ((obj pomm--transient-lisp-variable-switch)) + "Initialize the value for the `pomm--transient-lisp-variable-switch'. + +OBJ is an instance of the class." (oset obj value - pomm-reset-context-on-iteration-end)) + (symbol-value (oref obj variable)))) -(cl-defmethod transient-infix-read ((_ pomm--set-context-on-iteration-end-infix)) - "Toggle the switch on or off." - (setq pomm-reset-context-on-iteration-end - (not pomm-reset-context-on-iteration-end))) +(cl-defmethod transient-infix-read ((obj pomm--transient-lisp-variable-switch)) + "Toggle the value of the `pomm--transient-lisp-variable-switch'. + +This changes both the value of the variable and the value of the class. + +OBJ is an instance of the class." + (oset obj value + (set (oref obj variable) + (not (symbol-value (oref obj variable)))))) (transient-define-infix pomm--set-reset-context-on-iteration-end () - :class 'pomm--set-context-on-iteration-end-infix + :class 'pomm--transient-lisp-variable-switch + :variable 'pomm-reset-context-on-iteration-end :argument "--context-reset" :key "-r" :description "Reset the context on the interation end") @@ -655,15 +663,19 @@ minor mode." (always-read :initform t))) (cl-defmethod transient-init-value ((_ pomm--set-context-infix)) + "Initialize the value of the context infix from `pomm-state'." (alist-get 'context pomm--state)) (cl-defmethod transient-infix-set ((_ pomm--set-context-infix) value) + "Update `pomm-state' with VALUE from the context infix." (setf (alist-get 'context pomm--state) value)) (cl-defmethod transient-prompt ((_ pomm--set-context-infix)) + "Return the prompt text for the context infix." "Set context: ") (cl-defmethod transient-format-value ((_ pomm--set-context-infix)) + "Format value for the context infix." (propertize (if-let (val (alist-get 'context pomm--state)) (prin1-to-string val) "unset") @@ -821,7 +833,15 @@ The timer can have 3 states: 'S' / `pomm-stop'. - Running. Can be paused with 'p' / `pomm-pause' or stopped with 'S' / - `pomm-stop'." + `pomm-stop'. + +The timer supports setting \"context\", for example, a task on which +you're working on. It can be set with '-c' or `pomm-set-context'. +This is useful together with CSV logging, which is enabled if +`pomm-csv-history-file' is non-nil. + +Enable `pomm-mode-line-mode' to display the timer state in the +modeline." (interactive) (unless pomm--state (pomm--init-state))