Skip to content

Fix snooze budget tracking and enhance break management - #2

Merged
ANSHAY merged 5 commits into
mainfrom
fix/snooze-budget-per-kind
Jul 18, 2026
Merged

Fix snooze budget tracking and enhance break management#2
ANSHAY merged 5 commits into
mainfrom
fix/snooze-budget-per-kind

Conversation

@ANSHAY

@ANSHAY ANSHAY commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Track snooze budgets separately for different break types, ensuring accurate management during interleaved cycles. Introduce a pause feature for breaks during media playback, preventing interruptions. Update the tray icon to reflect user engagement and mood, and expand exercise options with intensity tiers. Include regression tests and documentation updates for new features.

ANSHAY and others added 5 commits July 18, 2026 17:33
A 3-snooze budget could be stretched to 5 on a long break.

`_snoozesLeft` was a single global counter, but micro and long breaks run
independent, interleavable cycles. Snoozing a long break pushes it past
`_microDue + _mergeWindow`, so `_nextKind()` flips to the already-due micro
break, which fires in the gap. Completing that micro cycle refilled the
shared counter, handing the still-pending long break a fresh budget on top
of what it had already spent.

Two changes:

- `_snoozesLeft` becomes `Map<BreakKind, int>`, decremented for the active
  kind and refilled only by that kind's own `_finishCycle`. A long break
  still refills the micro budget, because it genuinely restarts that cycle.
- `_resetDue` no longer refills the budget as a side effect of rescheduling
  a timer. Budget changes now happen only at real cycle boundaries, so the
  three resume paths (pause, work hours, config change) refill explicitly.

Adds a regression test reproducing the exact interleave, plus a guard that
each kind carries its own budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A break overlay in the middle of a film is the one interruption users will
uninstall over. Breaks now hold while something on screen must not be
interrupted, and resume when it ends.

Detection is built on **idle inhibitors**, not audio: video players,
browsers in fullscreen video, presentation tools and games all take one so
the screen never blanks on them, while music players do not. Keying off raw
audio would let a background playlist suppress breaks all day. Two sources
are unioned — gnome-session (where ScreenSaver.Inhibit lands on GNOME, and
which also names the responsible app) and logind (desktop-independent, so
KDE and wlroots sessions degrade to something useful). Both verified live
against a real inhibitor.

This is a *pause*, not a deferral. The busy/DND path caps at deferCap; a
film runs far longer, and a cap would guarantee the interruption the feature
exists to prevent.

Two deliberate semantics:
- Timers are not reset on resume. Two hours of fullscreen video is still two
  hours of screen time, so the interval keeps running down during playback —
  a film must not buy the user a fresh 20 minutes. An overdue break arrives
  through the existing short re-warn rather than seizing the screen the
  instant the credits roll.
- A break already on screen is left to finish. Abandoning it would log a
  BreakEscaped the user never earned.

Also generalizes the TTL/in-flight caching out of ContextSampler into
PolledValue<T> so both samplers share it, and gives Paused a reason
(user / workHours / media) so the dashboard can say what it is waiting on.
Behind a Settings toggle, default on. 8 new engine tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tray icon now carries an expression: colour AND face change with how
well breaks are being taken, so the app can praise, nudge, or warn without
ever interrupting.

Colour is never the only signal. Every mood changes the eyes or the mouth
too, so the indicator still reads under a colour vision deficiency and in
the monochrome tray themes some desktops apply, and the SNI ToolTip says the
mood in words — a red icon with no explanation is just anxiety.

The mark's two pause bars were already readable as eyes, which is what makes
this possible without abandoning the brand. Faces are drawn at runtime with
PictureRecorder rather than shipped as PNGs (seven moods x two sizes would
be fourteen assets to keep in sync), which also lets the pulse render
in-between frames. The two bundled tray PNGs and their loader are gone.

Structure:
- core/mood/ — pure rules over a *rolling window* of recent break responses,
  not day totals: judging on totals means one bad morning colours the icon
  red until midnight and a good afternoon can never earn its way back.
- MoodTracker adds asymmetric hysteresis — praise quickly, scold slowly.
  Improving is adopted at once; worsening must persist across three samples,
  so one skipped break never turns the icon red on its own. Without this the
  icon flickers as breaks roll through the window, and flicker in the corner
  of the eye is exactly what this app exists not to be.
- MoodService decides what the app feels; TrayMoodPresenter decides how it
  looks. Only the second half needs a rendering pipeline.
- A snooze is not treated as a miss until it becomes a habit — it is a
  deferral the app itself offers, and punishing users for using a feature as
  intended would be perverse.

The window is persisted so a restart is not a way to clear a warning.
Animation is a four-frame acknowledgement on change, not an idle loop: every
frame is a D-Bus signal plus a host re-read. Behind a Settings toggle.
17 mood tests plus a rendering guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds twelve exercises (12 -> 24) spanning soft, medium and heavy, including
the movement-based ones: forward folds, calf raises, arm circles, stairs,
squats, lunges, marching and jumping jacks.

Every exercise now carries an ExerciseIntensity, with a user-set ceiling in
Settings (default medium). This is a physical constraint, not a preference:
"stand up and do ten squats" is good advice at home and impossible in an
open-plan office, and an overlay that suggests the impossible just teaches
people to dismiss overlays. When opt-outs and the ceiling together leave
nothing pickable the opt-outs yield first — showing an exercise someone
muted is a smaller broken promise than showing one they cannot do where
they are sitting.

Ten new illustrations, built by extending the shared _figure primitive with
fold/twist/legBend/kneeLift/stance/armsUp parameters rather than writing new
painters, so a squat bends visible knees instead of merely shrinking. Two
bugs were caught by rendering the frames and looking at them: jumping-jack
arms swept past vertical and drew straight through the head, and rooting the
arms at the spine rather than the shoulder width grazed it even after that.

Also collapses the art->painter switch from twelve near-identical blocks to
one line per art while keeping compile-time exhaustiveness; it would have
been twenty-two blocks otherwise.

Exercise ids are append-only (the exercise log references them), so existing
history is untouched. 4 new picker tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ANSHAY ANSHAY self-assigned this Jul 18, 2026
@ANSHAY
ANSHAY merged commit dbb3c74 into main Jul 18, 2026
1 check passed
@ANSHAY
ANSHAY deleted the fix/snooze-budget-per-kind branch July 18, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant