Compare commits

...

37 commits

Author SHA1 Message Date
ee7b60c65e
Merge pull request #16 from maikol-solis/master
fix: context writing in CSV file for third-time implementation
2025-02-02 15:02:50 +03:00
Maikol Solís
feaca3836b
fix: context writing in CSV file for third-time implementation 2025-01-30 03:48:00 -06:00
ba0b308886 pomm: fix byte-compile (x2) 2024-07-10 00:56:26 +03:00
1b06edc33a pomm: fix byte-compile for Emacs 29 2024-07-10 00:52:33 +03:00
4fc8cfc8a7 readme: add link to comment 2024-07-10 00:43:14 +03:00
2acf51043c pomm: add org-clock integration 2024-07-10 00:36:35 +03:00
14c7b42e15 readme: fix typos 2024-07-10 00:09:10 +03:00
a95343f643
Merge pull request #12 from juergenhoetzel/file-defcustom
Use the more specific 'file type for file-based custom variables
2024-01-31 00:26:09 +03:00
Juergen Hoetzel
121163d149 Use the more specific 'file type for file-based custom variables 2024-01-30 21:58:29 +01:00
a717c3a053 pomm: add Published-At & update copyright 2023-12-26 02:08:28 +03:00
12ce6a68c7 ci: fix ci 2023-06-18 16:24:32 +03:00
bc42fdfa77 ci: update python 2023-06-02 23:46:23 +03:00
d05d9cb333
Merge pull request #10 from vale981/total_time
display total time worked, braked and tracked
2023-06-02 23:45:40 +03:00
Valentin Boettcher
39fd164b1d
display total time worked, braked and tracked 2023-06-02 16:02:11 -04:00
42f03d6ff2 fix: run the right hook on reset 2022-08-15 13:24:40 +05:00
0942131ac3 fix: remove unnecessary function 2022-08-14 18:39:58 +05:00
2dad0ced49 fix: removed lambdas from transient 2022-08-14 18:36:57 +05:00
3315304dd1 fix: typo 2022-08-14 18:24:54 +05:00
85200761b0
Merge pull request #9 from SqrtMinusOne/feature/third-time
Add Third Time technique
2022-08-14 18:18:51 +05:00
3367784a0f docs: update docs 2022-08-14 18:14:21 +05:00
5cdd5ca82d fix: persistence 2022-08-14 17:47:48 +05:00
90e9abcdf5 fix: lint 2022-08-14 17:44:06 +05:00
98e9904a98 feat: use transient instead of y-or-n-p 2022-08-14 17:21:39 +05:00
633540c7b5 fix: CSV 2022-08-14 15:36:03 +05:00
7ac74966d2 refactor: checkdoc 2022-08-13 21:05:44 +05:00
23ebf485d8 refactor: lint 2022-08-13 21:01:42 +05:00
f66b7cade1 feat: remaining third time features 2022-08-13 20:47:26 +05:00
1691d36473 feat: first version of third time transient 2022-08-13 16:57:31 +05:00
2f95a2a1c5 refactor: extract transient class 2022-08-13 15:53:05 +05:00
adfb483168 refactor: new break sound & some typos 2022-08-13 15:52:50 +05:00
f8dcbdb5ce feat: modeline for third time 2022-08-13 15:06:43 +05:00
bce36584cc feat: basic third time logic 2022-08-12 22:46:08 +05:00
2a2673bdc8
Merge pull request #7 from teeann/tick-audio
add option to disable ticking sound
2022-03-15 23:38:55 +03:00
Anh T Nguyen
1202197e5b add option to disable ticking sound 2022-03-14 21:06:17 +07:00
6dc3b5f913 ci: add melpazoid 2022-02-08 20:01:21 +03:00
2a47dc4c19
Merge pull request #6 from teeann/ask-start-pomodoro
add option pomm-ask-before-work
2022-02-08 19:48:09 +03:00
Anh T Nguyen
cefbeebf96 add option pomm-ask-before-work 2022-02-08 22:19:36 +07:00
5 changed files with 956 additions and 101 deletions

31
.github/workflows/melpazoid.yml vendored Normal file
View file

@ -0,0 +1,31 @@
# melpazoid <https://github.com/riscy/melpazoid> build checks.
# If your package is on GitHub, enable melpazoid's checks by copying this file
# to .github/workflows/melpazoid.yml and modifying RECIPE and EXIST_OK below.
name: melpazoid
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install
run: |
python -m pip install --upgrade pip
sudo apt-get install emacs && emacs --version
git clone https://github.com/riscy/melpazoid.git ~/melpazoid
pip install ~/melpazoid
- name: Run
env:
LOCAL_REPO: ${{ github.workspace }}
# RECIPE is your recipe as written for MELPA:
RECIPE: (pomm :repo "SqrtMinusOne/pomm.el" :fetcher github)
# set this to false (or remove it) if the package isn't on MELPA:
EXIST_OK: false
run: echo $GITHUB_REF && make -C ~/melpazoid

View file

@ -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-third-time))
#+end_src
Or you can clone the repository, add the package to the =load-path= and load it with =require=:
@ -42,23 +33,43 @@ 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.
The listed commands are self-descriptive and match the Pomodoro ideology.
The timer can have 3 states:
- *Stopped*. Can be started with "s" or =M-x pomm-start=. A new iteration of the timer will be started.
- *Paused*. Can be continuted with "s" / =M-x pomm-start= or stopped competely with "S" / =M-x pomm-stop=.
- *Paused*. Can be continued with "s" / =M-x pomm-start= or stopped completely with "S" / =M-x pomm-stop=.
- *Running*. Can be paused with "p" / =M-x pomm-pause= or stopped with "S" / =M-x pomm-stop=.
The state of the timer can be reset with "R" or =M-x pomm-reset=.
"u" updates the transient buffer. The update is manual because I didn't figure out how to automate this, and I think this is not /really/ necessary.
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 technique is designed around 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 of =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 +78,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: =<executable> /path/to/sound.wav=.
@ -98,17 +109,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 persistence 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=
The 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=:
@ -117,21 +134,37 @@ To customize timestamp, set the =pomm-csv-history-file-timestamp-format= variabl
#+end_src
The format is the same as in =format-time-string=.
** Usage with =org-clock=
The package can be used with [[https://orgmode.org/manual/Clocking-commands.html][org-clock]] in the following way. Set up these two hooks:
#+begin_src emacs-lisp
(add-hook 'pomm-on-status-changed-hook #'pomm--sync-org-clock)
(add-hook 'pomm-third-time-on-status-changed-hook
#'pomm-third-time--sync-org-clock
#+end_src
Then, start the timer (either =pomm= or =pomm-third-time=) and =org-clock-in=, in whichever order. The package will call =org-clock-out= when a break starts and =org-clock-in-last= when it ends.
Setting =pomm-org-clock-in-immediately= to =nil= "defers" calling =org-clock-in-last= until after any command from the user (via =post-command-hook=). I've added this because I occasionally return to my PC a few minutes after the break ends, so I don't want these minutes to show up in =org-clock=.
Also see [[https://github.com/SqrtMinusOne/pomm.el/issues/13#issuecomment-2216868331][this comment]] ([[https://github.com/SqrtMinusOne/pomm.el/issues/13][#13]]) for an alternative approach.
* Alternatives
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/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 |
| [[https://github.com/konr/tomatinho/][tomatinho]] | - | self-cooked interactive buffer | - | - | message + sounds |
| [[https://github.com/ferfebles/redtick][redtick]] | - | mode-line icon | + | - | sounds |
| [[https://github.com/abo-abo/gtk-pomodoro-indicator][gtk-pomodoro-indicator]] | GTK panel | CLI | - | -, but the program is independent from Emacs | GTK 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 + 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 |
| [[https://github.com/konr/tomatinho/][tomatinho]] | - | self-cooked interactive buffer | - | - | message + sounds |
| [[https://github.com/ferfebles/redtick][redtick]] | - | mode-line icon | + | - | sounds |
| [[https://github.com/abo-abo/gtk-pomodoro-indicator][gtk-pomodoro-indicator]] | GTK panel | CLI | - | -, but the program is independent of Emacs | GTK notifications |
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 to 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.

BIN
img/screenshot-tt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

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

@ -0,0 +1,710 @@
;;; pomm-third-time.el --- Implementation of the third time technique in Emacs -*- lexical-binding: t -*-
;; Copyright (C) 2023 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)
;; XXX optional dependency on org-clock
(declare-function org-clock-in-last "org-clock")
(declare-function org-clock-out "org-clock")
(defvar org-clock-current-task)
(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."
(interactive)
(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-third-time-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-third-time--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" 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--total-time (&optional kind)
"Get total time spent in `state` in the current iteration.
KIND is the same as in `pomm-third-time--state'."
(let* ((kind (if kind kind 'work))
(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 kind)
(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) kind)))
(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--stop ()
"Stop the running Third Time timer."
(interactive)
(unless (eq (alist-get 'status pomm-third-time--state) 'running)
(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)))
(transient-define-prefix pomm-third-time-stop ()
["This will reset the accumulated break time. Continue?"
("y" "Yes" pomm-third-time--stop)
("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)))))
(defun pomm-third-time--sync-org-clock ()
"Sync org-clock with the pomodoro timer."
(let* ((status (alist-get 'status pomm-third-time--state))
(kind (alist-get 'kind (alist-get 'current pomm-third-time--state)))
(active-p (and (eq kind 'work)
(eq status 'running)))
(resume-next-time-p (not (eq status 'stopped))))
(cond
((and active-p (not org-clock-current-task)
pomm--sync-org-clock-was-stopped)
(if pomm-org-clock-in-immediately
(org-clock-in-last)
(pomm--org-clock-in-last-after-action)))
((and (not active-p) org-clock-current-task)
(org-clock-out)
(setq pomm--sync-org-clock-was-stopped resume-next-time-p)))))
;;;###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))
(total-work (pomm-third-time--total-time 'work))
(total-break (pomm-third-time--total-time 'break)))
(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)
".\nTotal time worked: "
(propertize
(pomm-third-time--format-period total-work)
'face 'success)
". Total time braked: "
(propertize
(pomm-third-time--format-period total-break)
'face 'success)
". Total time tracked: "
(propertize
(pomm-third-time--format-period (+ total-work total-break))
'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

217
pomm.el
View file

@ -1,12 +1,13 @@
;;; 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) 2023 Korytov Pavel
;; Author: 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"))
;; Homepage: https://github.com/SqrtMinusOne/pomm.el
;; Published-At: 2021-11-05
;; This file is NOT part of GNU Emacs.
@ -25,14 +26,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
;; <https://github.com/SqrtMinusOne/pomm.el> for more information.
@ -42,8 +45,13 @@
(require 'eieio)
(require 'transient)
;; XXX optional dependency on org-clock
(declare-function org-clock-in-last "org-clock")
(declare-function org-clock-out "org-clock")
(defvar org-clock-current-task)
(defgroup pomm nil
"Yet another Pomodoro timer implementation."
"Pomodoro and Third Time timers."
:group 'tools)
(defcustom pomm-work-period 25
@ -77,7 +85,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 the user whether to start a new pomodoro period."
:group 'pomm
:type 'boolean)
@ -95,7 +108,7 @@
(locate-user-emacs-file "pomm")
"Location of the pomm state file."
:group 'pomm
:type 'string)
:type 'file)
(defcustom pomm-history-reset-hour 0
"An hour on which the history will be reset.
@ -115,9 +128,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:
@ -127,7 +140,7 @@ of period. The format is as follows:
- iteration
- context"
:group 'pomm
:type 'string)
:type '(choice file (const nil)))
(defcustom pomm-csv-history-file-timestamp-format "%s"
"Timestamp format in the csv file.
@ -156,6 +169,11 @@ a particular event."
:group 'pomm
:type 'boolean)
(defcustom pomm-audio-tick-enabled nil
"Whether to play ticking sound."
:group 'pomm
:type 'boolean)
(defun pomm--get-sound-file-path (name)
"Get path to the sound resource NAME.
@ -163,9 +181,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)))
@ -173,6 +190,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.
@ -181,7 +199,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"))))
@ -196,33 +214,36 @@ 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."
(defcustom pomm-org-clock-in-immediately t
"Run `org-clock-in-last' immediately after the break ends.
If nil, the clock in happens after the first command."
:group 'pomm
:type 'hook)
:type 'boolean)
(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:
- status: either 'stopped, 'paused or 'running
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
'current is also an alist with the following keys:
- kind: either 'short-break, 'long-break or 'work
\\='current is also an alist with the following keys:
- kind: either \\='short-break, \\='long-break or \\='work
- start-time: start timestamp
- effective-start-time: start timestamp, corrected for pauses
- iteration: number the current Pomodoro iteration
- iteration: number 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.")
@ -234,6 +255,7 @@ Updated by `pomm-update-mode-line-string'.")
(defun pomm--do-reset ()
"Reset the pomodoro timer state."
(interactive)
(when pomm--timer
(cancel-timer pomm--timer)
(setq pomm--timer nil))
@ -250,11 +272,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))
@ -265,7 +286,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."
@ -305,11 +328,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" pomm--do-reset)
("n" "No" transient-quit-one)])
(defun pomm--maybe-play-sound (kind)
"Play a sound of KIND.
@ -322,7 +344,8 @@ which can be played by `pomm-audio-player-executable'."
(unless pomm-audio-player-executable
(error "No audio player executable! Set 'pomm-audio-player-executable'")
(setq pomm-audio-enabled nil))
(when-let (sound (alist-get kind pomm-audio-files))
(when-let ((play-sound (or (not (eq 'tick kind)) pomm-audio-tick-enabled))
(sound (alist-get kind pomm-audio-files)))
(start-process
"pomm-audio-player"
nil
@ -344,7 +367,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"))
@ -424,25 +447,30 @@ The condition is: (effective-start-time + length) < now."
(+ current-iteration 1)
current-iteration)))
(pomm--store-current-to-history)
(if (or (not (eq next-kind 'long-break))
(not pomm-ask-before-long-break)
(y-or-n-p "Start a long break (y) or end the pomodors (n)? "))
(if (or
(and (eq next-kind 'long-break)
pomm-ask-before-long-break
(not (y-or-n-p "Start a long break (y) or end the pomodoros (n)? ")))
(and (eq next-kind 'work)
pomm-ask-before-work
(not (y-or-n-p "Start a pomodoro (y) or end the pomodoros (n)? "))))
(progn
(setf (alist-get 'current pomm--state)
`((kind . ,next-kind)
(start-time . ,(time-convert nil 'integer))
(effective-start-time . ,(time-convert nil 'integer))
(iteration . ,next-iteration)))
(pomm--dispatch-notification next-kind))
(setf (alist-get 'current pomm--state) nil)
(setf (alist-get 'status pomm--state) 'stopped))
(setf (alist-get 'current pomm--state) nil)
(setf (alist-get 'status pomm--state) 'stopped))
(progn
(setf (alist-get 'current pomm--state)
`((kind . ,next-kind)
(start-time . ,(time-convert nil 'integer))
(effective-start-time . ,(time-convert nil 'integer))
(iteration . ,next-iteration)))
(pomm--dispatch-notification next-kind)))
(pomm--save-state)
(run-hooks 'pomm-on-status-changed-hook)
(when (and (eq next-kind 'long-break) pomm-reset-context-on-iteration-end)
(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)
@ -461,7 +489,7 @@ The condition is: (effective-start-time + length) < now."
The formula is:
\(effective-start-time + length\) - now + paused-time,
where paused-time is 0 if status is not 'paused, otherwise:
where paused-time is 0 if status is not \\='paused, otherwise:
paused-time := now - last-changed-time"
(+
(+ (or (alist-get 'effective-start-time (alist-get 'current pomm--state)) 0)
@ -520,6 +548,37 @@ minor mode."
(remove-hook 'pomm-on-status-changed-hook #'pomm-update-mode-line-string)
(remove-hook 'pomm-on-status-changed-hook #'force-mode-line-update))))
(defvar pomm--sync-org-clock-was-stopped nil
"If t, `pomm--sync-org-clock' had stopped `org-clock.")
(defun pomm--org-clock-in-last-after-action ()
"Run `org-clock-in-last' after some action by the user.
This exists because I sometimes return to PC after a the break ends."
(add-hook 'post-command-hook #'pomm--org-clock-in-last-and-remove-from-hook))
(defun pomm--org-clock-in-last-and-remove-from-hook ()
"Run `org-clock-in-last' and remove self from the `post-command-hook'."
(org-clock-in-last)
(remove-hook 'post-command-hook #'pomm--org-clock-in-last-and-remove-from-hook))
(defun pomm--sync-org-clock ()
"Sync org-clock with the pomodoro timer."
(let* ((status (alist-get 'status pomm--state))
(kind (alist-get 'kind (alist-get 'current pomm--state)))
(active-p (and (eq kind 'work)
(eq status 'running)))
(resume-next-time-p (not (eq status 'stopped))))
(cond
((and active-p (not org-clock-current-task)
pomm--sync-org-clock-was-stopped)
(if pomm-org-clock-in-immediately
(org-clock-in-last)
(pomm--org-clock-in-last-after-action)))
((and (not active-p) org-clock-current-task)
(org-clock-out)
(setq pomm--sync-org-clock-was-stopped resume-next-time-p)))))
;;;###autoload
(defun pomm-start ()
"Start or continue the pomodoro timer.
@ -549,6 +608,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)
@ -615,21 +675,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")
@ -639,15 +708,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")
@ -798,14 +871,22 @@ This command initializes the timer and triggers the transient buffer.
The timer can have 3 states:
- Stopped.
Can be started with 's' or `pomm-start'. A new iteration of the
Can be started with \\='s' or `pomm-start'. A new iteration of the
timer will be started.
- Paused.
Can be continuted with 's' / `pomm-start' or stopped competely with
'S' / `pomm-stop'.
Can be continuted with \\='s' / `pomm-start' or stopped competely with
\\='S' / `pomm-stop'.
- Running.
Can be paused with 'p' / `pomm-pause' or stopped with 'S' /
`pomm-stop'."
Can be paused with \\='p' / `pomm-pause' or stopped with \\='S' /
`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))