Tai/fable bump/dev - #69
Draft
tai271828 wants to merge 28 commits into
Draft
Conversation
- switch the revamp guidance and implementation plan to uv-managed Python (env, lockfile, CI, build/publish, uv tool install) - append correctness audit (guidance §9): Debian startup crash via macOS-only say, duplicate/junk session rows, start-time reset on resume, tick drift and dialog freeze, stats over-counting - append fix specification (plan Appendix A + Phase 0 task T0.4): hotfixes now, corrected engine/store/stats semantics and legacy-DB cleaning rules for Phase 1+ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete setup.py/setup.cfg/MANIFEST.in/tox.ini/requirements*.txt and the generated egg-info, build/ and dist/ trees. Packaging is now a hatchling pyproject.toml named pomodoro-ctimer at 3.0.0a1, with a PEP 735 dev group (pytest, ruff) that uv installs on `uv sync`. - .python-version pins 3.12; uv.lock is committed. - ctimer.__version__ reads the installed metadata, and `ctimer --version` reports it instead of the hardcoded 1.0.0. - bokeh and the git-URL calmap dependency are dropped now; ctimer/visual is imported lazily so --stats/--overall print a "being revamped" message rather than crashing at import time. pandas/matplotlib stay until Phase 4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Configure ruff in pyproject.toml (default rule set plus `I` for import sorting, target py311), then apply `ruff check --fix` and `ruff format` across the tree. No hand edits in this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
- Ignore the DTZ ruleset: ctimer is a local-time desktop app, so a clock's date is deliberately the user's naive local date. - Per-file-ignore B008/C408/F841 for ctimer/visual, which is deleted in Phase 4 and whose deps are no longer installed. - cli.py: log through a module logger instead of the root logger. - ctimer_db.py: drop the unused clock_duration, and narrow the blanket `except Exception` in get_clock_count to (sqlite3.Error, IndexError). - model.py: `int(round(x))` -> `round(x)` (round already returns int). - docs/conf.py: `pomodoro - ctimer.__version__` was a NameError waiting to happen; use ctimer.__version__. - tests/test_model.py: delete the dead mock_midnight helper (never called, referenced an undefined db_file). utils.py and view.py findings are left for T0.4b/T0.4a, which rewrite them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
minimal.yaml (ubuntu, py3.7, pip + `python setup.py install` + a calmap-from-git clone + `inv test`) is replaced by ci.yaml: os: ubuntu-latest, macos-latest x python: 3.11, 3.12, 3.13 checkout -> astral-sh/setup-uv (cached) -> uv sync -> ruff -> pytest Xvfb stays on the Linux legs only, purely for the legacy Tk tests; it goes away with the Tk UI in Phase 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
playback_voice_message shelled out to `say` unconditionally, so on Debian
the very first call from CtimerClockView.__init__ ("Welcome") raised
FileNotFoundError and the app could not start without --silence.
Look the binary up with shutil.which first, skip the cue when it is absent,
and wrap the subprocess.run in try/except OSError so a broken `say` can
never take the app down. Also pass argv as a list instead of shlex-splitting
the whole command line.
Covered by tests/test_voice_cue.py, which drives the method off a stub self
and needs no display.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
get_cache_filepath referenced an undefined `root_path` (NameError on every --db path), swallowed it in a blanket `except Exception` that then re-ran the first-time-user branch, and had a fall-through returning None when --db matched the stored path. Rewritten as small pure functions over pathlib: cache_filename / read_stored_db_dir / remember_db_dir / get_cache_filepath Every branch now returns a Path, only FileNotFoundError (plus an empty-file IndexError) is caught, and the four cases are explicit: nothing remembered, remembered + no --db, --db equal to stored, --db different (prompt). dir_path is pathlib too and keeps its behavior: relative paths resolve under $HOME, absolute paths are used as-is. tests/test_utils_paths.py covers all of it with Path.home monkeypatched to tmp_path and the y/n prompt stubbed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
The module built a Tk root and called mainloop() at import time, so merely importing it hung the process (or crashed without a display). Nothing imported it — it was a copy-pasted forum snippet kept as a reference — so it just goes. The one reference to it, a comment in view.flash_window, is retargeted, and its stale .gitignore entry is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
view.terminate wrote a clock_details row unconditionally, so pressing Stop on a freshly launched (or freshly reset) app inserted a junk row with start_clock=0 and a 1970 timestamp, which every stats view then counted. Return early when tm.fresh_new: no dialog, no DB write, no state churn — the app is already in the reset state. This is a stopgap; the Phase 1 engine makes it structural by never emitting a record for a session that never started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
First module of the UI-free ctimer.core package: a frozen TimerSettings dataclass (focus/break/long-break seconds, clocks per long break, daily aim) plus load()/save() against a JSON file, destined for ~/.ctimer/settings.json. clocks_per_long_break is clamped to >= 2 and all fields are rounded to int, preserving legacy Meta's normalisation. TimerSettings.debug() reproduces the current --debug clock lengths (5s/5s/7s, long break every 2, aim 8). load() degrades to defaults for a missing, corrupt, or partially stale file rather than raising, and unknown keys from a future version are ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
ctimer.core.records defines the single SessionRecord the whole app revolves around; ctimer.core.engine defines Phase, the eight events, and TimerEngine with start/pause/resume/stop/tick and read-only phase, remaining_seconds, completed_today and current_goal. The engine has no wall-clock reads to drive time, no sleeps and no I/O: time moves only through tick(), and the injectable `now` is used solely to stamp records. Appendix A.2 semantics are built in rather than transcribed from the legacy code: - started_at is stamped once per session; resume never moves it (B1). - pause()/resume() accumulate pause_seconds and emit no record at all (E1); a paused focus clock is not "complete". - exactly one record per session, carried on the terminal event FocusCompleted / BreakCompleted / Stopped (A1). - breaks are their own sessions with their own timestamps, kind and no goal fields (A2). - stop() while idle is a no-op: no record, no event (A3). - a single tick may span several phases, so a slow event loop cannot lose time (B2), and completed_today is re-read from an injected provider on every start() instead of being cached across midnight (B3, E2). tests/core/test_engine.py covers the cycle, long-break cadence, pausing, early stops, tick carry-over and one test per Appendix A.5 bullet — all off a fake clock, with no sleeps and no display. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
ctimer.core.store replaces the all-text `clock_details` table with a properly typed `sessions` table at PRAGMA user_version = 2: REAL timestamps, an INTEGER is_complete, a CHECKed `kind` of focus/break/long_break, a nullable typed `reason` (fixes E3), a `pause_seconds` column that finally persists paused time (fixes E1), and a `uuid` column for a possible future sync. The file is a single ~/.ctimer/ctimer.db with no year in the name (ctimer_debug.db in debug mode); `--db DIR` still chooses the directory. Store is a context manager exposing add / completed_count / sessions_between / all_sessions / last_completed_at / close. completed_count is a COUNT(*) over kind='focus' — never the "read the last row and trust its counter" heuristic (fixes E2) and never counting breaks (fixes D1). clock_index is derived on insert: a focus row takes the next index of its day, a break carries the index of the clock it follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
…3, part 2)
migrate_legacy(legacy_path, store) imports the old all-text clock_details
table into `sessions`. The legacy file is opened read-only (`mode=ro`) and is
never modified; the migration only runs when the destination store is still
empty, and logs a one-line summary:
migrated N focus, M breaks; dropped J junk, K duplicates
The legacy rows are not trustworthy, so Appendix A.3's rules are applied in
order: drop rows whose start_clock is 0/""/None (A3); coerce the text
booleans and text floats, turning a numeric-zero `reason` — the old
written-on-pause marker — into NULL (E3); collapse each (date, clock_count)
group of focus rows to the one with the greatest end_clock, so the rows the
old app wrote on every pause stop counting as separate clocks (A1); and
re-anchor break rows to the end of the focus clock they follow instead of the
stale focus start they inherited, dropping the goal fields they also
inherited (A2).
find_legacy_db() locates ctimer_<year>.db / ctimer_debug_<year>.db in the db
directory. The code lives in ctimer/core/migration.py rather than inside
store.py as the plan sketched — the cleaning rules are substantial enough to
want their own module, and store.py stays about the schema.
tests/core/test_migration.py migrates a fixture containing exactly the
defects A.3 describes and asserts each rule, plus that the legacy bytes are
unchanged afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
ctimer.core.stats provides daily_counts / weekly_summary / yearly_counts / total_focus_minutes as pure functions over Store — no pandas, no matplotlib, no rendering. Per Appendix A.4 every aggregate consumes only kind='focus' sessions, one count per session, so break rows and the old pause partials can no longer inflate the numbers (fixes D1); focus minutes are computed per session from its own started_at/ended_at minus pause_seconds (fixes D2, B1). tests/core/test_stats.py uses the fixture A.4 prescribes — 2 completed focus sessions, 2 breaks, 1 early-stopped focus — and asserts the hand-computed counts and minutes, plus that a migrated legacy fixture reports 2 clocks rather than its 5 rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Imports every module of ctimer.core in a fresh interpreter and fails if flet, tkinter or another toolkit ends up in sys.modules. Verified by temporarily adding `import tkinter` to a core module: the test fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
The Tkinter UI now runs on the core engine and store. It is a shim, not a rewrite — the widgets are untouched; what changed is who owns the state. - controller.py is the shim: its after() loop calls engine.tick(1) once a second and dispatches engine events to the existing view methods. It is also the only place that writes to the database — on FocusCompleted / Stopped it asks the view for reached/reason and calls store.add() exactly once, and on BreakCompleted it stores the break as its own row. - model.py keeps only a view-model: clock_ticking / fresh_new / remaining_time / set_time are now read-only views onto the engine's phase and countdown, so nothing can desynchronise them. - view.toggle_start_pause and view.terminate just drive the engine (start / pause / resume / stop). The view no longer imports the database, no longer writes a row on pause, and no longer resets timestamps. - ctimer.py wires store -> engine -> model -> controller and persists an in-flight session on window close without opening a dialog. - cli.py opens a Store at ~/.ctimer/ctimer.db, migrates a legacy ctimer_<year>.db into it on first run, and loads/saves TimerSettings at ~/.ctimer/settings.json — which is what --cus now writes. - ctimer_db.py is deleted; core.store replaces all of it. Meta is deleted; TimerSettings replaces it, so tests/test_model.py is deleted in favour of tests/core/test_settings.py, which covers the same clamping and rounding. - --stats/--overall print the "being revamped" notice: the legacy plotting reads the old clock_details schema and its deps are already gone. Phase 4 brings stats back in-app. The legacy Tk tests are rewired to the new wiring and now assert the fixed behaviour (one row per session, pause writes nothing, a break follows the clock automatically, every 2nd break is long in debug mode) against a tmp_path store instead of the user's real ~/.ctimer. Known gap, fixed in Phase 2: Tk's modal dialogs still block the after() loop, so the break countdown does not advance while the "did you reach your goal?" dialog is open (finding B2). Flet's non-blocking dialogs settle this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
…T2.2) `ctimer` now opens a Flet desktop window; `ctimer --legacy-tk` still opens the Tk one until T2.5 deletes it. Every other flag (--debug, --silence, --hide, --db, --cus, --stats, --overall, --version) behaves as before. ctimer/ui/presenter.py is the whole translation layer and imports no Flet: it consumes engine events and produces a frozen ViewModel of strings and flags. ctimer/ui/app.py does nothing but copy those onto controls, which is why the UI is testable without a display. The window is ~320x420 and resizable, showing top to bottom: the date, a gear icon, "Done: N / Aim: M", the big MM:SS countdown (green during breaks), Start/Pause + Stop, the current goal, and "Completed at: HH:MM:SS" of the last finished clock. --hide keeps it out of always-on-top and brings it to the front when a clock completes. Two behaviours the Tk app could not manage: - The countdown is one async loop that derives elapsed *whole seconds* from time.monotonic() instead of counting callbacks, so event-loop latency can no longer make the clock drift (Appendix A.2.1). - The goal and "did you reach your goal?" dialogs are awaited coroutines, so the break countdown runs while they are open — the legacy modal froze it (finding B2). flet[desktop] is added as a runtime dependency; the desktop extra matters, because plain `flet` otherwise pip-installs flet-desktop behind uv's back on first launch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
ctimer/ui/cues.py replaces the macOS-only `say` call with something that works everywhere and never crashes: - Three short cue sounds are bundled in ctimer/ui/assets/sounds and played through flet-audio. They are synthesised sine tones generated by tools/make_sounds.py, so nothing third-party is vendored; regenerate them with `uv run python tools/make_sounds.py`. - Desktop notifications on focus-complete and break-over via notify-send on Linux and osascript on macOS. A missing tool, a missing dbus, a failing subprocess: all logged at debug and swallowed. - macOS `say` stays as an optional nicety, guarded by shutil.which. - --silence mutes sound and speech and blinks the countdown instead, which is what the legacy flash_window never got around to doing. Also fixes a bug from the previous commit: BreakCompleted had no handler in the Flet app, so break sessions were never written to the store. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
The gear icon opens a dialog editing the five TimerSettings values (clock, break and long-break length in minutes, clocks per long break, daily aim). Saving applies them to the running engine — mid-clock changes take effect on the next countdown — and writes ~/.ctimer/settings.json, the same file the --cus prompts write. The form logic is pure and lives in presenter.py: settings_form_values() and parse_settings_form(), which reports per-field errors and keeps the current value for any field that failed, so a typo cannot wipe a setting. Both are unit-tested; the dialog itself is only wiring. In --debug mode no settings path is passed, so the 5-second debug clocks can never overwrite the real settings file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Removes view.py, controller.py, model.py, ctimer.py (maintk), the --legacy-tk flag, the Tk test modules, and the Xvfb step from CI. The Flet window is now the only UI, and the test suite runs headless on both runners. `grep -rn tkinter ctimer tests` returns only the comment in ctimer/core/__init__.py that explains why the core may not import one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
tests/ui/test_app_wiring.py drives CtimerApp through a stub page that records what the app asked the page to do. It pins down the persistence contract that the Tk app kept getting wrong: - a finished clock stores nothing until the outcome question is answered, and then stores exactly one row; - the break has already started while that question is still open (B2); - finished and stopped breaks are stored without asking anything (A2); - Stop while idle and pause/resume store nothing at all (A3, A1); - closing the window persists an in-flight clock, unanswered; - the day count and last completion time are read back from the store. Together with tests/ui/test_presenter.py and test_cues.py, the whole UI seam is covered without a display; test_app_import.py additionally imports ctimer.ui.app with DISPLAY unset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Point the launcher at the Flet app: add StartupWMClass so the window matches its launcher, better Categories/Keywords/GenericName, and two launcher actions (--hide, --silence) in the current Actions= form rather than the Unity-only X-Ayatana shortcut groups. The --stats/--overall shortcuts are dropped; those views become a page inside the window in Phase 4, not separate commands. xdg/README.md documents installing the .desktop file and the icon, for a plain install and for the Linux bundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Pushing a v* tag now runs four jobs: - dist: `uv build` -> wheel + sdist artifact; - bundles: `flet build linux` on ubuntu and `flet build macos` on macOS, each packed into a tarball (the Linux one also carries the .desktop file and icon). The Flutter SDK is a build-time-only requirement, fetched and cached inside the job by subosito/flutter-action; - pypi: `uv publish --trusted-publishing always` from a `pypi` environment with id-token: write, so no API token is stored anywhere; - github-release: `gh release create` with generated notes and every artifact attached. main.py at the project root is the module `flet build` runs inside a bundle; it only calls ctimer.cli:main, which is the same entry point the wheel installs. [tool.flet] carries the bundle metadata (product, org, copyright). Verified locally: `uv build` produces both artifacts, `uv tool install .` installs a working `ctimer` executable and launching it opens the Flet window. The bundle jobs are not verifiable here — they need the Flutter SDK and a macOS runner — so they are written from the flet build CLI contract and want a first real tag to shake out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
The README described `python setup.py develop`, a Tkinter window and `--overall` calmap plots, none of which exist any more. It now covers: install via `uv tool install` / pipx / the release bundles, what the window does, every flag, where the data lives (including that a 1.x/2.x database is imported automatically and the old file left untouched), the dev loop (`uv sync`, `uv run pytest`, `uv run ruff check .`) and the package layout. CONTRIBUTING.rst and docs/installation.rst lose their setup.py/tox/flake8 instructions for the same uv commands, and the supported versions become 3.11-3.13. Also deletes the Makefile and the invoke `tasks/` package: both drove setup.py and `inv test`, and CI has run `uv run pytest` since Phase 0. No screenshot yet — one wants a real desktop session to capture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
A bar-chart icon in the window opens a stats page and a close icon goes back. It shows this week's clocks per day as a flet-charts BarChart with the daily aim drawn behind each bar (today highlighted), a one-line summary of clocks and focus time, and a calmap-style year heatmap built from plain coloured Containers — no matplotlib, no browser, no files in /tmp. The numbers live in ctimer/ui/stats_presenter.py: pure functions over ctimer.core.stats producing WeekView and YearView, including the weekday padding of the first heatmap column, the month labels, the heat levels scaled against the daily aim, and the best day of the year. All of it is unit-tested, including a leap year and the year boundary; stats_page.py is rendering only. Per Appendix A.4 the aggregates come from focus sessions only and focus minutes exclude paused time, so breaks and pause partials cannot inflate the chart the way they inflated the old calmap plot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Both flags are kept, both now open the window on the stats page, and both print a one-line note saying the views moved in-app instead of writing HTML into /tmp and opening a browser. Their --help text is updated to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
Deletes ctimer/visual (the bokeh/calmap/matplotlib plotting that wrote HTML into /tmp) now that the stats live in the window, and removes pandas and matplotlib from the runtime dependencies along with the ruff per-file-ignore that existed only for that package. `uv tree --no-dev` no longer mentions pandas, matplotlib, bokeh or calmap: the runtime tree is flet[desktop], flet-audio and flet-charts. data/ is left alone: it is gitignored, and the ctimer.db in it may be somebody's real history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vcqggYJjg15WUpQBQUBqj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Revamp the code stack with modern stack for:
Also, validate the algorithm by GenAI.
Smoke test ok Debian looks good. Still need to careful validation for:
After comprehensive validation, I will convert this pull request from a draft to an official PR.