pypomodo 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pypomodo-0.3.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Davidson (WulfAlpha)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,377 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypomodo
3
+ Version: 0.3.1
4
+ Summary: A terminal-first Pomodoro timer in the Unix tradition, with named timers, routines, and event hooks
5
+ Keywords: pomodoro,timer,countdown,cli,terminal
6
+ Author: Andrew Davidson
7
+ Author-email: Andrew Davidson <drace.davidson@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.12
21
+ Project-URL: Homepage, https://github.com/wulfalpha/pypomo
22
+ Project-URL: Issues, https://github.com/wulfalpha/pypomo/issues
23
+ Project-URL: Changelog, https://github.com/wulfalpha/pypomo/blob/main/CHANGELOG.md
24
+ Description-Content-Type: text/markdown
25
+
26
+ # pypomo
27
+
28
+ A terminal-first Pomodoro timer in the Unix tradition. pypomo keeps time well:
29
+ durations, named timers, finite Pomodoro routines, pause and resume, and clean
30
+ output for terminals and pipes. Notifications, sounds, and logging are left to
31
+ the tools you already use, through [event hooks](#hooks) and
32
+ [JSON lines output](#json-lines-output).
33
+ Requires Python 3.12 or newer; no runtime dependencies.
34
+
35
+ Licensed under the [MIT License](LICENSE). See the [changelog](CHANGELOG.md)
36
+ for what changed in each release.
37
+
38
+ Install from PyPI with uv or pipx. The package is named `pypomodo`; the command
39
+ is `pypomo`:
40
+
41
+ ```bash
42
+ uv tool install pypomodo
43
+ pipx install pypomodo
44
+ ```
45
+
46
+ Or install from a checkout with `pip install .`, or run with `uv run pypomo`.
47
+
48
+ ```bash
49
+ pypomo 15m
50
+ pypomo 30s
51
+ pypomo 2h
52
+ pypomo 1h30m15s
53
+ pypomo 1h 30m
54
+ pypomo pomo
55
+ pypomo --list
56
+ ```
57
+
58
+ Durations use positive whole-number totals with `d`, `h`, `m`, and `s`
59
+ (days, hours, minutes, seconds). Compound units must appear in that order
60
+ without repeats. Values such as `90m` and `36h` are valid. Countdowns always
61
+ display as `HH:MM:SS`, so `2d` shows `48:00:00`. Each duration is limited to
62
+ 365 days (`365d`), including durations in config files.
63
+ Spaces may separate units, with or without quotes: `pypomo 1h 30m` and
64
+ `pypomo "1h 30m"` both equal `1h30m`, and config durations such as
65
+ `"1h 30m"` work too. Spaces are allowed only between units (`5 m` is rejected),
66
+ and tabs or other whitespace are not accepted.
67
+
68
+ In a terminal, the timer name and `HH:MM:SS` countdown update on a single line:
69
+ `pomo · 00:24:59 remaining`. Completion or cancellation replaces that line,
70
+ then ends it with a newline so your shell prompt starts cleanly. Completion
71
+ also rings the terminal bell.
72
+ Use `--silent` to suppress the bell while keeping the countdown visible.
73
+
74
+ Redirected output contains separate start and completion (or cancellation)
75
+ lines without terminal control characters. Use `--live` to force in-place
76
+ updates through a pipe, for example `pypomo 5m --live | lolcat`. The receiving
77
+ program must support terminal control sequences and flush output promptly.
78
+ Ctrl+C cancels the timer with exit status 130. Invalid input exits with status 2.
79
+ On POSIX systems, SIGTERM and SIGHUP also cancel the timer, with exit statuses
80
+ 143 and 129 respectively. On Windows, Ctrl+Break cancels the same way, with
81
+ exit status 149. A broken output pipe exits quietly with status 141.
82
+ If stdout is unavailable, the timer runs silently to its deadline.
83
+
84
+ ## Pausing
85
+
86
+ While a timer runs in a terminal, press `p` (or Space) to pause and `p` again
87
+ to resume; the paused time is added back to the timer. Press `q` to cancel,
88
+ exactly like Ctrl+C. Keys work whenever standard input is the terminal and
89
+ pypomo is the foreground job, including with redirected output, where the log
90
+ gains `paused at …` and `resumed at …` lines. Keys are read immediately and are
91
+ not echoed; the terminal's normal input mode is restored on exit.
92
+
93
+ The timer uses a monotonic clock, so changing the system clock does not change
94
+ the countdown. Ctrl+Z does not pause it: stopped process time still counts.
95
+ Use `p` to pause.
96
+ Laptop-suspend behavior depends on the platform's monotonic clock; pypomo does
97
+ not currently normalize it or guarantee that suspended time counts. Ordinary
98
+ sleep overshoot does not accumulate into display drift, though long scheduling
99
+ stalls can still skip displayed seconds.
100
+
101
+ ## Configuration
102
+
103
+ Create `~/.config/pypomo/config.toml` (or `$XDG_CONFIG_HOME/pypomo/config.toml`
104
+ when set).
105
+ Relative `XDG_CONFIG_HOME` values are ignored. On Windows the default remains
106
+ `%USERPROFILE%\.config\pypomo`. UTF-8 files with or without a BOM are accepted.
107
+ TOML supports comments and sections:
108
+
109
+ ```toml
110
+ default_units = "minutes"
111
+
112
+ [timers]
113
+ break = "15m"
114
+ lunch = "30m"
115
+ deep-work = "1h30m"
116
+
117
+ [pretty]
118
+ enabled = false # Use --pretty to enable for a single run
119
+
120
+ [pretty.bar]
121
+ enabled = true
122
+ width = 20
123
+ filled = "█"
124
+ empty = "░"
125
+
126
+ [pretty.colors]
127
+ enabled = true
128
+ label = "cyan"
129
+ remaining = "white"
130
+ bar = "green"
131
+ complete = "green"
132
+ cancelled = "yellow"
133
+ paused = "yellow" # paused and ready-to-start lines
134
+ ```
135
+
136
+ Existing `config.json` files remain supported with the same settings as nested
137
+ JSON objects. If both files exist, `config.toml` takes precedence; files are not
138
+ merged. Use `--config PATH` to explicitly select a `.toml` or `.json` file.
139
+ An invalid TOML config reports an error rather than silently falling back to JSON.
140
+
141
+ With this config, `pypomo 15` means 15 minutes, `pypomo break` means 15 minutes,
142
+ and `pypomo pomo` runs the default Pomodoro routine. Explicit units always take precedence.
143
+ `default_units` accepts `seconds`, `minutes`, `hours`, `days`, or `s`, `m`, `h`, `d`.
144
+ Without it, bare numbers are rejected. Configured durations are strings and
145
+ use the same syntax and defaults as CLI durations.
146
+
147
+ Names are case-sensitive, start with a letter, and contain only letters,
148
+ digits, underscores, or hyphens. Missing settings retain built-in defaults.
149
+ No config is required; an explicitly selected config must exist.
150
+ `pomo` is reserved for the default Pomodoro routine. For a single focus timer,
151
+ use `pypomo 25m` or define a timer named `focus`.
152
+
153
+ ## Pomodoro routines
154
+
155
+ `pypomo pomo` runs four 25-minute focus sessions, with 5-minute short breaks
156
+ between them and a 15-minute long break after the last focus session. Phases
157
+ advance automatically by default; the routine finishes after the long break
158
+ rather than repeating indefinitely. Ctrl+C or `q` cancels the entire routine.
159
+
160
+ To wait for you between phases, set `auto_start = false` or pass
161
+ `--no-auto-start` (`--auto-start` overrides a config that disables it). After
162
+ each phase ends, the display shows `sprint.pomo · short break 1/3 · ready ·
163
+ press Enter to start`, and the next phase begins when you press Enter. Waiting
164
+ time does not count against the next phase; `q` or Ctrl+C cancels instead.
165
+ Without a terminal keyboard, pypomo reads a line from standard input instead,
166
+ and starts immediately if standard input is closed or exhausted, so unattended
167
+ runs never block. The flags have no effect on single timers.
168
+
169
+ Customize the default or add named routines in your config:
170
+
171
+ ```toml
172
+ [pomo]
173
+ focus = "25m"
174
+ short_break = "5m"
175
+ long_break = "15m"
176
+ rounds = 4
177
+ auto_start = true # false: wait for Enter before each phase after the first
178
+
179
+ [pomodoros.sprint]
180
+ focus = "15m"
181
+ short_break = "3m"
182
+ long_break = "10m"
183
+ rounds = 3
184
+ ```
185
+
186
+ ```bash
187
+ pypomo pomo --pretty
188
+ pypomo sprint.pomo --pretty
189
+ pypomo --list
190
+ ```
191
+
192
+ `rounds` is a positive integer counting focus sessions. `auto_start` is a boolean. A one-round routine
193
+ has one focus session followed by a long break, with no short break.
194
+ The maximum is 1,000 focus rounds per routine.
195
+ Durations must be positive strings, using the same duration syntax and
196
+ `default_units` as ordinary timers. Omitted default settings use built-in
197
+ values; custom routines inherit omitted settings from the configured `[pomo]`.
198
+ Routine names follow the same naming rules as timers. A timer `sprint` and a
199
+ routine `sprint.pomo` can coexist.
200
+
201
+ The display includes the routine name, phase and round, such as
202
+ `sprint.pomo · focus 1/3`. Live output stays on one line across phase changes;
203
+ the progress bar measures the current phase and resets at each transition.
204
+ The terminal bell rings when each phase ends. Redirected output records the
205
+ start and completion of each phase on separate lines.
206
+
207
+ Preview a routine's phases and total duration without starting it:
208
+
209
+ ```bash
210
+ pypomo pomo --preview
211
+ pypomo sprint.pomo --preview
212
+ pypomo 1h30m --preview
213
+ ```
214
+
215
+ The total includes all focus sessions and breaks, including the final long
216
+ break. The built-in routine totals `02:10:00`. A final line estimates the local
217
+ end time if the timer started now, such as `Ends: 14:35 if started now`, dated
218
+ when it falls on a later day. It does not account for pauses, and routines that
219
+ wait between phases say so. `--auto-start` and `--no-auto-start` apply to the
220
+ preview. `--preview` also supports named
221
+ timers and explicit durations. It cannot be combined with `--list`.
222
+ `--silent` suppresses bells for every phase of a running routine.
223
+
224
+ For compatibility, a legacy `[timers] pomo = "25m"` setting (or its JSON
225
+ equivalent) supplies the default routine's focus duration. An explicit
226
+ `[pomo] focus` takes precedence. **The `pomo` command now runs a complete
227
+ routine, even with a legacy config**, rather than a single focus timer.
228
+ JSON supports the same structure using `pomo` and `pomodoros` objects.
229
+
230
+ ## Pretty output
231
+
232
+ ```bash
233
+ pypomo pomo --pretty
234
+ pypomo pomo --no-pretty
235
+ pypomo 5m --pretty --live
236
+ ```
237
+
238
+ `--pretty` enables the configured bar and theme; `--no-pretty` overrides
239
+ `pretty.enabled = true`. Without a flag, the config decides (default: false).
240
+ The progress bar fills as time elapses and remains visible on completion or
241
+ cancellation. Disable `pretty.bar.enabled` for colors only, or disable
242
+ `pretty.colors.enabled` for a bar without colors. Disable both to retain plain
243
+ text. Each styled segment resets its color so it does not affect the shell prompt.
244
+
245
+ Bar width is 1–80 characters. Filled and empty glyphs must each be a single
246
+ narrow printable character (for example `#` and `-`, or `█` and `░`); spaces
247
+ are allowed, but emoji, wide characters, combining marks and control characters
248
+ are rejected. The bar shrinks or disappears when the terminal is narrow;
249
+ long labels are truncated to preserve the status, and extremely narrow displays
250
+ truncate the status as well. Available colors are
251
+ `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, and `default`
252
+ (the terminal's default foreground). Actual shades follow your terminal palette.
253
+
254
+ Styling applies to terminal output or output forced live with `--live`.
255
+ Redirected output remains plain without `--live`, even with `--pretty`.
256
+ For coloring with an external program, use `pypomo 5m --live | lolcat`;
257
+ add `--no-pretty` if styling is enabled in your config.
258
+
259
+ A nonempty `NO_COLOR` disables colors while preserving the progress bar.
260
+ `TERM=dumb` uses plain start/end lines, including when `--live` is supplied.
261
+ Output uses readable ASCII separators and bar glyphs when the stream's encoding
262
+ cannot represent the configured characters. Other unencodable text is escaped.
263
+ Live rendering requires a terminal that understands ANSI control sequences.
264
+
265
+ ## Hooks
266
+
267
+ pypomo does the timing and leaves notifications, sounds, and logging to other
268
+ programs. A hook is a command run when a timer event happens:
269
+
270
+ ```toml
271
+ [hooks]
272
+ end = ["notify-send", "pypomo", "Time is up"]
273
+ pause = "playerctl pause"
274
+ resume = "playerctl play"
275
+ any = 'echo "$(date -Is) $PYPOMO_EVENT $PYPOMO_LABEL" >> ~/.local/state/pomodoro.log'
276
+ ```
277
+
278
+ Events are `start`, `end`, `pause`, `resume`, `cancel`, and `wait`; an `any`
279
+ hook runs for every event, after the event's own hook. Each phase of a routine
280
+ has its own `start` and `end`. `wait` happens when a routine with
281
+ `auto_start = false` begins waiting for Enter before a phase, so a hook can say
282
+ "break ready". Quitting while waiting sends `cancel` for that phase.
283
+ A list runs the program directly; a string runs through the system shell
284
+ (`sh` on POSIX, `cmd` on Windows), so shell variables, pipes, and redirection work.
285
+ Add hooks for a single run with `--hook CMD` (repeatable, shell syntax,
286
+ equivalent to an extra `any` hook), or ignore configured hooks with `--no-hooks`.
287
+
288
+ | Variable | Value |
289
+ | --- | --- |
290
+ | `PYPOMO_EVENT` | `start`, `end`, `pause`, `resume`, `cancel`, or `wait` |
291
+ | `PYPOMO_TIMER` | the timer as given: `25m`, `tea`, `sprint.pomo` |
292
+ | `PYPOMO_LABEL` | the displayed label: `sprint.pomo · focus 1/3` |
293
+ | `PYPOMO_PHASE` | `timer`, `focus`, `short_break`, or `long_break` |
294
+ | `PYPOMO_ROUND`, `PYPOMO_ROUNDS` | round and total rounds; empty for single timers |
295
+ | `PYPOMO_DURATION` | the timer or phase length in seconds |
296
+ | `PYPOMO_REMAINING` | whole seconds left (`0` at `end`) |
297
+ | `PYPOMO_LAST` | `1` for a single timer or a routine's final phase, else `0` |
298
+
299
+ Hooks never affect the timer. pypomo starts them in the background without
300
+ waiting, so a slow hook does not delay the countdown, and hooks for consecutive
301
+ events may overlap. A hook's exit status is ignored. A command that cannot be
302
+ started produces one warning on stderr and the timer continues. Hooks receive no
303
+ standard input and their standard output is discarded, keeping pypomo's output
304
+ clean for pipes; standard error stays connected so failures are visible. Each
305
+ hook runs in its own session or process group, so Ctrl+C or closing the
306
+ terminal does not interrupt a hook that is already running.
307
+ `--preview` and `--list` never run hooks.
308
+
309
+ Example: a desktop notification only when a whole routine finishes:
310
+
311
+ ```bash
312
+ pypomo pomo --hook '[ "$PYPOMO_EVENT" = end ] && [ "$PYPOMO_LAST" = 1 ] && notify-send "Routine done"'
313
+ ```
314
+
315
+ ## JSON lines output
316
+
317
+ `--format jsonl` replaces the countdown display with one JSON object per event
318
+ on standard output, for `jq`, scripts, and logs:
319
+
320
+ ```bash
321
+ pypomo pomo --format jsonl | jq -r '"\(.time) \(.event) \(.label)"'
322
+ pypomo pomo --format jsonl >> ~/.local/state/pomodoro.jsonl
323
+ ```
324
+
325
+ ```json
326
+ {"event": "end", "timer": "pomo", "label": "pomo \u00b7 focus 1/4", "phase": "focus", "round": 1, "rounds": 4, "duration": 1500, "remaining": 0, "last": false, "time": "2026-11-07T14:25:00-05:00"}
327
+ ```
328
+
329
+ Events and fields match the [hook variables](#hooks), plus `time`, the local
330
+ time with its UTC offset. `round` and `rounds` are `null` for single timers.
331
+ Each line is written and flushed as the event happens, and the output is ASCII
332
+ (other characters use JSON escapes), so it is valid in any locale.
333
+ Nothing else is written to standard output: no countdown, prompts, or bell.
334
+ Pause keys still work, and their events appear in the stream; a routine that
335
+ waits between phases emits `wait` and starts on Enter (or a line of input).
336
+ Hooks run alongside JSON output. The text display remains the default
337
+ (`--format text`). `--format jsonl` cannot be combined with `--list`,
338
+ `--preview`, or `--live`. Exit statuses are unchanged, so a stream that ends
339
+ in `cancel` is followed by exit status 130.
340
+
341
+ ## Development
342
+
343
+ ```bash
344
+ uv sync # create .venv with pypomo (editable) and ruff
345
+ uv run python -m unittest discover -s tests -v
346
+ uv run pypomo 1s
347
+ uv run ruff check src tests
348
+ ```
349
+
350
+ Without uv, the tests need no dependencies:
351
+ `PYTHONPATH=src python -m unittest discover -s tests -v`.
352
+
353
+ ## Releasing
354
+
355
+ The package is named `pypomodo` on PyPI (`pypomo` was too similar to an
356
+ existing project); the command and the Python package remain `pypomo`.
357
+ To release, bump `version` in `pyproject.toml`, date the changelog section,
358
+ merge, then tag the merge commit and push the tag:
359
+
360
+ ```bash
361
+ git tag -a v0.4.0 -m "pypomo 0.4.0" && git push origin v0.4.0
362
+ ```
363
+
364
+ The `Publish` workflow checks that the tag matches the package version, builds
365
+ and tests the wheel, and uploads it to PyPI with trusted publishing from the
366
+ `pypi` environment. No API token is stored in the repository.
367
+
368
+ ## Continuous integration
369
+
370
+ GitHub Actions runs on pushes to `main` and on pull requests, with a manual run
371
+ option. It tests Python 3.12 and 3.15 on Linux, macOS, and Windows, and 3.13 and
372
+ 3.14 on Linux. Each job lints the code, builds the source distribution and
373
+ wheel, installs the wheel, runs the test suite against the installed package,
374
+ and checks `pypomo --help`, `pypomo --list`, and a one-second timer.
375
+ Linux PTY tests cover live output, Ctrl+C, pause/resume keys, and waiting between phases; POSIX signal tests run on Linux and
376
+ macOS. Encoding tests run on every platform. Terminal appearance and interactive
377
+ Windows console behavior still benefit from manual testing.