Merge pull request #9 from SqrtMinusOne/feature/third-time

Add Third Time technique
This commit is contained in:
Korytov Pavel 2022-08-14 18:18:51 +05:00 committed by GitHub
commit 85200761b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 781 additions and 70 deletions

View file

@ -2,18 +2,16 @@
[[https://melpa.org/#/pomm][file:https://melpa.org/packages/pomm-badge.svg]] [[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]] [[./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]]. - Managing the timer with the excellent [[https://github.com/magit/transient/blob/master/lisp/transient.el][transient.el]].
- Persistent state between Emacs sessions. - 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. - 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. 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.
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.
* Installation * Installation
The package is available on MELPA. Install it however you usually install Emacs packages, e.g. 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 #+begin_src emacs-lisp
(use-package pomm (use-package pomm
:straight t :straight t
:commands (pomm)) :commands (pomm pomm-thrid-time))
#+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))
#+end_src #+end_src
Or you can clone the repository, add the package to the =load-path= and load it with =require=: 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=. The package requires Emacs 27.1 because the time API of the previous versions is kinda crazy and 27.1 has =time-convert=.
* Usage * Usage
** Pomodoro
Run =M-x pomm= to open the transient buffer. Run =M-x pomm= to open the transient buffer.
The listed commands are rather self-descriptive and match the Pomodoro ideology. 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. "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. 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 * 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 ** 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: 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 #+end_src
** Sounds ** 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: =<executable> /path/to/sound.wav=. This functionality needs =pomm-audio-player-executable= to be set so that the program could be invoked like: =<executable> /path/to/sound.wav=.
@ -98,17 +108,23 @@ interval = 1
#+end_src #+end_src
** State file location ** 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. To implement pesistence between Emacs sessions, the package stores its state in the following files:
** History - =pomm-state-file-location=, =.emacs.d/pomm= by default
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. - =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= - =timestamp=
- =status= (=stopped=, =paused= or =running=, according to the [[*Usage][usage]] section) - =status= (=stopped=, =paused= or =running=, according to the [[*Usage][usage]] section)
- =kind= (=work=, =short-break=, =long-break= or =nil=) - =kind= (=work=, =short-break=, =long-break= or =nil=)
- =iteration= - =iteration=
- =context= - =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. 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=: 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: 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 | | 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/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/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 | | [[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 (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. * 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. The package name is not an abbreviation. I just hope it doesn't mean something horrible in some language I don't know.

BIN
img/screenshot-tt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

673
pomm-third-time.el Normal file
View file

@ -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 <thexcloud@gmail.com>
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; 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 <https://www.gnu.org/licenses/>.
;;; 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

118
pomm.el
View file

@ -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 <thexcloud@gmail.com> ;; Author: Korytov Pavel <thexcloud@gmail.com>
;; Maintainer: Korytov Pavel <thexcloud@gmail.com> ;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; Version: 0.1.4 ;; Version: 0.2.0
;; Package-Requires: ((emacs "27.1") (alert "1.2") (seq "2.22") (transient "0.3.0")) ;; Package-Requires: ((emacs "27.1") (alert "1.2") (seq "2.22") (transient "0.3.0"))
;; Homepage: https://github.com/SqrtMinusOne/pomm.el ;; Homepage: https://github.com/SqrtMinusOne/pomm.el
@ -25,14 +25,16 @@
;;; Commentary: ;;; Commentary:
;; An implementation of a Pomodoro timer for Emacs. Distintive features ;; Implementation of two time management methods in Emacs: Pomodoro
;; of this particular implementation: ;; and Third Time.
;; - Managing the timer with transient.el (`pomm' command) ;; This implementation features:
;; - Managing the timer with transient.el
;; - Persistent state between Emacs sessions. ;; - Persistent state between Emacs sessions.
;; So one could close & reopen Emacs without interruption the timer. ;; So one could close & reopen Emacs without interruption the timer.
;; ;;
;; Take a look at `pomm-update-mode-line-string' on how to setup this ;; Main entrypoints are: `pomm' for Pomodoro and `pomm-third-time' for
;; package with a modeline. ;; Third Time.
;;
;; Also take a look at README at ;; Also take a look at README at
;; <https://github.com/SqrtMinusOne/pomm.el> for more information. ;; <https://github.com/SqrtMinusOne/pomm.el> for more information.
@ -43,7 +45,7 @@
(require 'transient) (require 'transient)
(defgroup pomm nil (defgroup pomm nil
"Yet another Pomodoro timer implementation." "Pomodoro and Third Time timers."
:group 'tools) :group 'tools)
(defcustom pomm-work-period 25 (defcustom pomm-work-period 25
@ -77,12 +79,12 @@
:type 'string) :type 'string)
(defcustom pomm-ask-before-long-break t (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 :group 'pomm
:type 'boolean) :type 'boolean)
(defcustom pomm-ask-before-work nil (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 :group 'pomm
:type 'boolean) :type 'boolean)
@ -120,9 +122,9 @@ The format is the same as in `format-seconds'"
:type 'string) :type 'string)
(defcustom pomm-csv-history-file nil (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 A new entry is written whenever the timer changes status or kind
of period. The format is as follows: 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 location of this file, which means that resources folder should
be in the same directory. be in the same directory.
If the file is evaluated interactively (for development If the file is evaluated interactively (for development purposes), the
purposes), the `default-directory' is most likely the project `default-directory' variable is most likely the project root."
root."
(or (and load-file-name (concat (file-name-directory load-file-name) name)) (or (and load-file-name (concat (file-name-directory load-file-name) name))
(concat default-directory name))) (concat default-directory name)))
@ -183,6 +184,7 @@ root."
`((work . ,(pomm--get-sound-file-path "resources/bell.wav")) `((work . ,(pomm--get-sound-file-path "resources/bell.wav"))
(tick . ,(pomm--get-sound-file-path "resources/tick.wav")) (tick . ,(pomm--get-sound-file-path "resources/tick.wav"))
(short-break . ,(pomm--get-sound-file-path "resources/bell.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")) (long-break . ,(pomm--get-sound-file-path "resources/bell.wav"))
(stop . ,(pomm--get-sound-file-path "resources/air_horn.wav"))) (stop . ,(pomm--get-sound-file-path "resources/air_horn.wav")))
"Paths to the sounds to play on various events. "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 - key is an event type
- value is either a path to the sound file or nil." - value is either a path to the sound file or nil."
:group 'pomm :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") :type '(alist :key-type (symbol :tag "Event")
:value-type (choice (string :tag "Path") :value-type (choice (string :tag "Path")
(const nil :tag "No sound")))) (const nil :tag "No sound"))))
@ -206,18 +208,13 @@ Each element of the list is a cons cell, where:
:group 'pomm :group 'pomm
:type 'hook) :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 (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 - status: either 'stopped, 'paused or 'running
- current: an alist with a current period - 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 - last-changed-time: a timestamp of the last change in status
- context: a string that describes the current task - 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 - kind: either 'short-break, 'long-break or 'work
- start-time: start timestamp - start-time: start timestamp
- effective-start-time: start timestamp, corrected for pauses - 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: History is a list of alists with the following keys:
- kind: same as in current - kind: same as in current
- iteration - iteration
- start-time: start timestamp - start-time: start timestamp
- end-time: end 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 (defvar pomm--timer nil
"A variable for the pomm timer.") "A variable for the pomm timer.")
@ -260,11 +258,10 @@ Updated by `pomm-update-mode-line-string'.")
(defun pomm--init-state () (defun pomm--init-state ()
"Initialize the Pomodoro timer 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--save-state)
(add-hook 'pomm-on-status-changed-hook #'pomm--maybe-save-csv) (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) (add-hook 'pomm-on-status-changed-hook #'pomm--dispatch-current-sound)
(if (or (not (file-exists-p pomm-state-file-location)) (if (or (not (file-exists-p pomm-state-file-location))
(not 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)) (if (not (string-empty-p data))
(setq pomm--state (car (read-from-string data))) (setq pomm--state (car (read-from-string data)))
(pomm--do-reset))))) (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 () (defun pomm--save-state ()
"Save the current Pomodoro timer state." "Save the current Pomodoro timer state."
@ -315,11 +314,10 @@ variable doesn't exist, function does nothing."
(or (alist-get 'context pomm--state) "")) (or (alist-get 'context pomm--state) ""))
nil pomm-csv-history-file 'append 1))) nil pomm-csv-history-file 'append 1)))
(defun pomm-reset () (transient-define-prefix pomm-reset ()
"Reset the Pomodoro timer." ["Are you sure you want to reset the Pomodoro timer?"
(interactive) ("y" "Yes" (lambda () (interactive) (pomm--do-reset)))
(when (y-or-n-p "Are you sure you want to reset the Pomodoro timer? ") ("n" "No" transient-quit-one)])
(pomm--do-reset)))
(defun pomm--maybe-play-sound (kind) (defun pomm--maybe-play-sound (kind)
"Play a sound of 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'" KIND is the same as in `pomm--state'"
(alert (alert
(pcase kind (pcase kind
('short-break pomm-short-break-message) ((or 'break 'short-break) pomm-short-break-message)
('long-break pomm-long-break-message) ('long-break pomm-long-break-message)
('work pomm-work-message)) ('work pomm-work-message))
:title "Pomodoro")) :title "Pomodoro"))
@ -458,7 +456,7 @@ The condition is: (effective-start-time + length) < now."
(setf (alist-get 'context pomm--state) nil)))) (setf (alist-get 'context pomm--state) nil))))
(defun pomm--on-tick () (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) (pcase (alist-get 'status pomm--state)
('stopped (when pomm--timer ('stopped (when pomm--timer
(cancel-timer pomm--timer) (cancel-timer pomm--timer)
@ -565,6 +563,7 @@ minor mode."
(setf (alist-get 'context pomm--state) (setf (alist-get 'context pomm--state)
(prin1-to-string (read-minibuffer "Context: " (current-word))))) (prin1-to-string (read-minibuffer "Context: " (current-word)))))
;;;###autoload
(defun pomm-start-with-context () (defun pomm-start-with-context ()
"Prompt for context and call `pomm-start'." "Prompt for context and call `pomm-start'."
(interactive) (interactive)
@ -631,21 +630,30 @@ minor mode."
(read-number "Number of work periods before a long break:" (read-number "Number of work periods before a long break:"
pomm-number-of-periods))) pomm-number-of-periods)))
(defclass pomm--set-context-on-iteration-end-infix (transient-switch) (defclass pomm--transient-lisp-variable-switch (transient-switch)
((transient :initform t)) ((transient :initform t)
"A transient class to toggle `pomm-reset-context-on-iteration-end'.") (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 (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)) (cl-defmethod transient-infix-read ((obj pomm--transient-lisp-variable-switch))
"Toggle the switch on or off." "Toggle the value of the `pomm--transient-lisp-variable-switch'.
(setq pomm-reset-context-on-iteration-end
(not pomm-reset-context-on-iteration-end))) 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 () (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" :argument "--context-reset"
:key "-r" :key "-r"
:description "Reset the context on the interation end") :description "Reset the context on the interation end")
@ -655,15 +663,19 @@ minor mode."
(always-read :initform t))) (always-read :initform t)))
(cl-defmethod transient-init-value ((_ pomm--set-context-infix)) (cl-defmethod transient-init-value ((_ pomm--set-context-infix))
"Initialize the value of the context infix from `pomm-state'."
(alist-get 'context pomm--state)) (alist-get 'context pomm--state))
(cl-defmethod transient-infix-set ((_ pomm--set-context-infix) value) (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)) (setf (alist-get 'context pomm--state) value))
(cl-defmethod transient-prompt ((_ pomm--set-context-infix)) (cl-defmethod transient-prompt ((_ pomm--set-context-infix))
"Return the prompt text for the context infix."
"Set context: ") "Set context: ")
(cl-defmethod transient-format-value ((_ pomm--set-context-infix)) (cl-defmethod transient-format-value ((_ pomm--set-context-infix))
"Format value for the context infix."
(propertize (if-let (val (alist-get 'context pomm--state)) (propertize (if-let (val (alist-get 'context pomm--state))
(prin1-to-string val) (prin1-to-string val)
"unset") "unset")
@ -821,7 +833,15 @@ The timer can have 3 states:
'S' / `pomm-stop'. 'S' / `pomm-stop'.
- Running. - Running.
Can be paused with 'p' / `pomm-pause' or stopped with 'S' / 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) (interactive)
(unless pomm--state (unless pomm--state
(pomm--init-state)) (pomm--init-state))