From ac9f707b7266ae8ab702964b6b501fb516cd4685 Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 16 Mar 2026 08:18:22 -0600 Subject: [PATCH 1/5] feat: Add support for disabling breaks. - Add options for disabling short and long breaks. - Add translations. - Add spec docs. --- .../changes/optional-breaks/.openspec.yaml | 2 + openspec/changes/optional-breaks/design.md | 70 +++++++ openspec/changes/optional-breaks/proposal.md | 30 +++ .../optional-breaks/specs/settings/spec.md | 33 ++++ .../specs/timer-sequence/spec.md | 48 +++++ openspec/changes/optional-breaks/tasks.md | 60 ++++++ src-tauri/src/db/migrations.rs | 17 +- src-tauri/src/settings/defaults.rs | 2 + src-tauri/src/settings/mod.rs | 6 + src-tauri/src/timer/sequence.rs | 137 ++++++++++++- .../settings/sections/TimerSection.svelte | 181 ++++++++++-------- src/lib/stores/settings.ts | 2 + src/lib/types.ts | 2 + src/messages/de.json | 4 + src/messages/en.json | 4 + src/messages/es.json | 4 + src/messages/fr.json | 4 + src/messages/ja.json | 4 + src/messages/pt.json | 4 + src/messages/tr.json | 4 + src/messages/zh.json | 4 + 21 files changed, 538 insertions(+), 84 deletions(-) create mode 100644 openspec/changes/optional-breaks/.openspec.yaml create mode 100644 openspec/changes/optional-breaks/design.md create mode 100644 openspec/changes/optional-breaks/proposal.md create mode 100644 openspec/changes/optional-breaks/specs/settings/spec.md create mode 100644 openspec/changes/optional-breaks/specs/timer-sequence/spec.md create mode 100644 openspec/changes/optional-breaks/tasks.md diff --git a/openspec/changes/optional-breaks/.openspec.yaml b/openspec/changes/optional-breaks/.openspec.yaml new file mode 100644 index 00000000..49ccc670 --- /dev/null +++ b/openspec/changes/optional-breaks/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-14 diff --git a/openspec/changes/optional-breaks/design.md b/openspec/changes/optional-breaks/design.md new file mode 100644 index 00000000..ea7c029f --- /dev/null +++ b/openspec/changes/optional-breaks/design.md @@ -0,0 +1,70 @@ +## Context + +The timer sequence is owned entirely by `SequenceState::advance()` in `src-tauri/src/timer/sequence.rs`. This function already receives `&Settings` and determines the next `RoundType` and duration. All four combinations of break enable/disable flags are handled purely inside this function — no other layer of the timer stack needs to change. + +The current branching logic in `advance()`: +``` +Work → if work_round_number >= work_rounds_total → LongBreak, else → ShortBreak +ShortBreak → work_round_number += 1; → Work +LongBreak → work_round_number = 1; → Work +``` + +The new logic introduces two guard checks: + +``` +Work → + if at_long_break_point (work_round_number >= work_rounds_total): + if long_breaks_enabled → LongBreak + elif short_breaks_enabled → ShortBreak, work_round_number = 0 ← resets to 1 after Short→Work + else → Work, work_round_number = 1 ← immediate reset, pure work loop + else: + if short_breaks_enabled → ShortBreak + else → Work, work_round_number += 1 ← skip short, keep counting + +ShortBreak → work_round_number += 1; → Work (unchanged) +LongBreak → work_round_number = 1; → Work (unchanged) +``` + +Setting `work_round_number = 0` before a substituted ShortBreak is the key detail: the existing `ShortBreak → Work` arm increments unconditionally, so setting it to 0 means it arrives at Work as 1 — preserving the invariant that `work_round_number` is always 1 at the start of a fresh cycle. + +## Goals / Non-Goals + +**Goals:** +- Independent enable/disable for short and long breaks. +- All four combinations produce correct, well-defined cycle behaviour. +- `work_round_number` always resets cleanly at the natural cycle boundary (the long-break point), regardless of whether a long break actually fires. +- UI dims (not hides) dependent controls when a break type is disabled. + +**Non-Goals:** +- Per-round skip (transient — the existing Skip Round button covers this). +- Different behaviour per cycle (e.g., disable only the first short break). +- Changes to auto-start logic (it simply has nothing to do when a break is skipped). + +## Decisions + +**Substitute rather than skip at the long-break point when long breaks are disabled.** If long breaks are disabled and short breaks are enabled, taking a short break at the long-break boundary is more natural than jumping straight back to Work(1). It also reuses existing Short→Work transition logic for the counter reset. + +**`work_round_number = 0` trick rather than a new reset path.** The existing `ShortBreak → Work` arm increments `work_round_number` unconditionally. Zeroing it before entering ShortBreak as a long-break substitute means no new arm is needed — the existing arm does the right thing. + +**Both settings default `true`.** Existing behaviour is fully preserved for all current users. No breaking change; no migration needed to maintain the existing sequence. (Migration 5 still seeds the rows via `INSERT OR IGNORE` for correctness, but the default value matches prior implicit behaviour.) + +**Dim dependent controls, don't hide them.** Consistent with the global shortcuts pattern: controls are always in the DOM, just `opacity: 0.4; pointer-events: none` when their parent toggle is off. When long breaks are disabled, both the Long Break duration slider and the Rounds until Long Break slider are wrapped together in a single `disabled` container. + +## Risks / Trade-offs + +**Risk: `work_round_number = 0` is a surprising intermediate state.** If a snapshot is taken between the Work→ShortBreak transition and the ShortBreak→Work transition, the round number will read as 0. +→ Mitigation: Snapshots are only emitted at round boundaries after `advance()` completes; the 0 state is never observable externally. + +**Risk: Both breaks disabled with `work_rounds_total = 1` produces an immediate tight loop: Work(1) → Work(1) → ...** This is correct by the spec, but a user who accidentally ends up here may wonder why nothing seems to happen between rounds. +→ Mitigation: No action needed — the auto-start settings control whether transitions are immediate or require a manual start, so the experience is no worse than the current single-round long-break loop. + +## Migration Plan + +Migration 5 in `db/migrations.rs`: +```sql +INSERT OR IGNORE INTO settings (key, value) VALUES ('short_breaks_enabled', 'true'); +INSERT OR IGNORE INTO settings (key, value) VALUES ('long_breaks_enabled', 'true'); +INSERT INTO schema_version VALUES (5); +``` + +`INSERT OR IGNORE` means existing installs are unaffected. Fresh installs get both keys from `seed_defaults` instead. diff --git a/openspec/changes/optional-breaks/proposal.md b/openspec/changes/optional-breaks/proposal.md new file mode 100644 index 00000000..0c73a038 --- /dev/null +++ b/openspec/changes/optional-breaks/proposal.md @@ -0,0 +1,30 @@ +## Why + +The Pomodoro technique prescribes fixed work/break cycles, but real workflows vary. Some users prefer to chain work rounds without interruption and take breaks manually; others want short breaks but find long breaks disruptive to their flow. Currently every break type fires unconditionally — there is no way to disable short or long breaks without deleting the app entirely or using an obscure workaround. This is a persistent workflow customization, not a one-time skip. + +## What Changes + +- Add two boolean settings: `short_breaks_enabled` (default `true`) and `long_breaks_enabled` (default `true`). +- When short breaks are disabled, the sequence skips ShortBreak entirely; work rounds advance directly to the next work round (counter increments normally). +- When long breaks are disabled, the sequence substitutes a ShortBreak at the long-break point (if short breaks are enabled) or loops directly back to Work(1) (if both are disabled). The round counter resets as it would have at the long-break point either way. +- Settings → Timer gains two enable/disable toggles — one above the Short Break duration slider, one above the Long Break duration slider. When a break type is disabled, its duration slider (and for long breaks, the Rounds until Long Break slider) is visually dimmed and non-interactive. + +## Capabilities + +### New Capabilities +- none + +### Modified Capabilities +- `timer-sequence`: Add requirement that short breaks and long breaks can each be independently disabled; document the resulting cycle behaviour for all four combinations. +- `settings`: Add `short_breaks_enabled` and `long_breaks_enabled` setting keys, both defaulting to `true`. + +## Impact + +- **Rust**: `src-tauri/src/timer/sequence.rs` — `advance()` receives `&Settings` already; add two flag checks to skip or substitute break types. +- **Rust**: `src-tauri/src/settings/mod.rs` — add `short_breaks_enabled: bool` and `long_breaks_enabled: bool` fields and defaults. +- **Rust**: `src-tauri/src/settings/defaults.rs` — add both keys with value `"true"`. +- **Rust**: `src-tauri/src/db/migrations.rs` — migration 5 seeds both new keys via `INSERT OR IGNORE`. +- **Frontend**: `src/lib/types.ts` — add both fields to `Settings` interface. +- **Frontend**: `src/lib/stores/settings.ts` — add both fields to the defaults object. +- **Frontend**: `src/lib/components/settings/sections/TimerSection.svelte` — add two `SettingsToggle` rows and wrap dependent sliders in a `disabled` container. +- **Frontend**: `src/messages/*.json` — add i18n keys for both toggles across all 8 locales. diff --git a/openspec/changes/optional-breaks/specs/settings/spec.md b/openspec/changes/optional-breaks/specs/settings/spec.md new file mode 100644 index 00000000..26436512 --- /dev/null +++ b/openspec/changes/optional-breaks/specs/settings/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: short_breaks_enabled setting +The system SHALL store a `short_breaks_enabled` boolean setting (DB key: `short_breaks_enabled`, default `'true'`) in SQLite. The `Settings` struct SHALL expose a `short_breaks_enabled: bool` field, and `types.ts` SHALL mirror this as `short_breaks_enabled: boolean`. + +#### Scenario: Default value is true +- **WHEN** the application runs for the first time with no existing settings +- **THEN** `short_breaks_enabled` SHALL be `true` + +#### Scenario: Setting persists across restarts +- **WHEN** the user disables short breaks and restarts the application +- **THEN** `short_breaks_enabled` SHALL be `false` after restart + +#### Scenario: Migration seeds setting for existing users +- **WHEN** an existing user upgrades from a version without `short_breaks_enabled` +- **THEN** the setting SHALL be inserted with value `'true'` via `INSERT OR IGNORE` +- **AND** the user's other settings SHALL be unchanged + +### Requirement: long_breaks_enabled setting +The system SHALL store a `long_breaks_enabled` boolean setting (DB key: `long_breaks_enabled`, default `'true'`) in SQLite. The `Settings` struct SHALL expose a `long_breaks_enabled: bool` field, and `types.ts` SHALL mirror this as `long_breaks_enabled: boolean`. + +#### Scenario: Default value is true +- **WHEN** the application runs for the first time with no existing settings +- **THEN** `long_breaks_enabled` SHALL be `true` + +#### Scenario: Setting persists across restarts +- **WHEN** the user disables long breaks and restarts the application +- **THEN** `long_breaks_enabled` SHALL be `false` after restart + +#### Scenario: Migration seeds setting for existing users +- **WHEN** an existing user upgrades from a version without `long_breaks_enabled` +- **THEN** the setting SHALL be inserted with value `'true'` via `INSERT OR IGNORE` +- **AND** the user's other settings SHALL be unchanged diff --git a/openspec/changes/optional-breaks/specs/timer-sequence/spec.md b/openspec/changes/optional-breaks/specs/timer-sequence/spec.md new file mode 100644 index 00000000..9302e14a --- /dev/null +++ b/openspec/changes/optional-breaks/specs/timer-sequence/spec.md @@ -0,0 +1,48 @@ +## ADDED Requirements + +### Requirement: Short breaks can be independently disabled +The system SHALL support a `short_breaks_enabled` setting (default `true`). When `false`, the sequence SHALL skip ShortBreak rounds entirely: a Work round that would normally advance to ShortBreak SHALL instead advance directly to the next Work round, incrementing `work_round_number` by one. All other sequence behaviour (long breaks, counter reset at the long-break point) SHALL be unaffected. + +#### Scenario: Short breaks disabled — work rounds chain directly +- **WHEN** `short_breaks_enabled` is `false` +- **AND** a Work round completes before the long-break point +- **THEN** the next round SHALL be Work with `work_round_number` incremented by one +- **AND** no ShortBreak round SHALL occur + +#### Scenario: Short breaks disabled — long breaks still fire +- **WHEN** `short_breaks_enabled` is `false` +- **AND** a Work round completes at the long-break point (`work_round_number >= work_rounds_total`) +- **AND** `long_breaks_enabled` is `true` +- **THEN** the next round SHALL be LongBreak + +#### Scenario: Short breaks re-enabled — cycle resumes normally +- **WHEN** `short_breaks_enabled` is changed to `true` +- **THEN** the next Work-to-break transition SHALL produce a ShortBreak or LongBreak as determined by the current round position + +### Requirement: Long breaks can be independently disabled +The system SHALL support a `long_breaks_enabled` setting (default `true`). When `false`, the sequence SHALL never advance to a LongBreak round. At the long-break point, the sequence SHALL advance to ShortBreak instead (if `short_breaks_enabled` is `true`) or directly to Work(1) (if `short_breaks_enabled` is also `false`). In both cases `work_round_number` SHALL reset to 1 at that boundary, preserving the cycle structure. + +#### Scenario: Long breaks disabled — short break substituted at long-break point +- **WHEN** `long_breaks_enabled` is `false` +- **AND** `short_breaks_enabled` is `true` +- **AND** a Work round completes at the long-break point +- **THEN** the next round SHALL be ShortBreak +- **AND** `work_round_number` SHALL reset to 1 after that ShortBreak completes + +#### Scenario: Long breaks disabled — cycle resets when both breaks disabled +- **WHEN** `long_breaks_enabled` is `false` +- **AND** `short_breaks_enabled` is `false` +- **AND** a Work round completes at the long-break point +- **THEN** the next round SHALL be Work with `work_round_number` reset to 1 + +#### Scenario: Long breaks disabled — short breaks still fire before the long-break point +- **WHEN** `long_breaks_enabled` is `false` +- **AND** `short_breaks_enabled` is `true` +- **AND** a Work round completes before the long-break point +- **THEN** the next round SHALL be ShortBreak as normal + +#### Scenario: Both breaks disabled — pure work loop +- **WHEN** `short_breaks_enabled` is `false` +- **AND** `long_breaks_enabled` is `false` +- **THEN** the sequence SHALL consist entirely of Work rounds +- **AND** `work_round_number` SHALL increment each round and reset to 1 at `work_rounds_total` diff --git a/openspec/changes/optional-breaks/tasks.md b/openspec/changes/optional-breaks/tasks.md new file mode 100644 index 00000000..2bacfdc9 --- /dev/null +++ b/openspec/changes/optional-breaks/tasks.md @@ -0,0 +1,60 @@ +## 1. Rust — Settings + +- [x] 1.1 In `src-tauri/src/settings/mod.rs`, add `pub short_breaks_enabled: bool` and `pub long_breaks_enabled: bool` fields to the `Settings` struct (after `long_break_interval`) +- [x] 1.2 In `src-tauri/src/settings/mod.rs`, add `short_breaks_enabled: true` and `long_breaks_enabled: true` to `Settings::default()` +- [x] 1.3 In `src-tauri/src/settings/mod.rs`, add both fields to the `load()` DB→struct mapping using `parse_bool` +- [x] 1.4 In `src-tauri/src/settings/defaults.rs`, add `("short_breaks_enabled", "true")` and `("long_breaks_enabled", "true")` to `DEFAULTS` + +## 2. Rust — Database Migration + +- [x] 2.1 In `src-tauri/src/db/migrations.rs`, add `MIGRATION_5` constant that seeds both new keys with `INSERT OR IGNORE` and increments schema version to 5 +- [x] 2.2 In the `run()` function, add `if version < 5 { ... }` block applying `MIGRATION_5` with logging consistent with existing blocks +- [x] 2.3 Update the migration idempotency test assertion from `v == 4` to `v == 5` + +## 3. Rust — Timer Sequence + +- [x] 3.1 In `src-tauri/src/timer/sequence.rs`, update `advance()` to implement the four-combination break logic: + - Work, before long-break point: if `short_breaks_enabled` → ShortBreak, else → Work with `work_round_number += 1` + - Work, at long-break point: if `long_breaks_enabled` → LongBreak; elif `short_breaks_enabled` → ShortBreak with `work_round_number = 0` (resets to 1 after Short→Work); else → Work with `work_round_number = 1` +- [x] 3.2 Add unit tests covering all four flag combinations: + - `short_breaks_disabled_chains_work_rounds` — verify Work→Work→...→LongBreak with `short_breaks_enabled=false` + - `long_breaks_disabled_substitutes_short_break` — verify Work(N=total)→ShortBreak and counter reset with `long_breaks_enabled=false` + - `both_breaks_disabled_pure_work_loop` — verify Work(1)→Work(2)→...→Work(total)→Work(1) with both false + - `long_breaks_disabled_short_breaks_fire_normally` — verify ShortBreak still fires before the long-break point when only `long_breaks_enabled=false` + +## 4. Frontend — Types and Store + +- [x] 4.1 In `src/lib/types.ts`, add `short_breaks_enabled: boolean` and `long_breaks_enabled: boolean` to the `Settings` interface (after `long_break_interval`) +- [x] 4.2 In `src/lib/stores/settings.ts`, add `short_breaks_enabled: true` and `long_breaks_enabled: true` to the defaults object + +## 5. Frontend — TimerSection UI + +- [x] 5.1 In `src/lib/components/settings/sections/TimerSection.svelte`, add a `toggle` helper that calls `setSetting(dbKey, current ? 'false' : 'true')` and updates the settings store +- [x] 5.2 Add a `SettingsToggle` for `short_breaks_enabled` immediately above the Short Break duration slider row, using i18n keys `timer_toggle_short_breaks` and `timer_toggle_short_breaks_desc` +- [x] 5.3 Wrap the Short Break duration slider row in a `
` that applies `opacity: 0.4; pointer-events: none` when disabled +- [x] 5.4 Add a `SettingsToggle` for `long_breaks_enabled` immediately above the Long Break duration slider row +- [x] 5.5 Wrap both the Long Break duration slider row and the Rounds until Long Break slider row together in a `
` that applies `opacity: 0.4; pointer-events: none` when disabled +- [x] 5.6 Add `.break-body` and `.break-body.disabled` CSS rules to the component's ` diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index fa951c78..766fbcfd 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -9,6 +9,8 @@ const defaults: Settings = { time_short_break_secs: 300, time_long_break_secs: 900, long_break_interval: 4, + short_breaks_enabled: true, + long_breaks_enabled: true, auto_start_work: false, auto_start_break: false, tray_icon_enabled: false, diff --git a/src/lib/types.ts b/src/lib/types.ts index ee77a908..5efc1c74 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -21,6 +21,8 @@ export interface Settings { time_short_break_secs: number; time_long_break_secs: number; long_break_interval: number; + short_breaks_enabled: boolean; + long_breaks_enabled: boolean; auto_start_work: boolean; auto_start_break: boolean; tray_icon_enabled: boolean; diff --git a/src/messages/de.json b/src/messages/de.json index da3e231c..55f5b5a3 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "Die nächste Arbeitssitzung automatisch starten, wenn eine Pause endet.", "timer_toggle_auto_start_break": "Pausen automatisch starten", "timer_toggle_auto_start_break_desc": "Automatisch eine Pause starten, wenn eine Arbeitssitzung endet.", + "timer_toggle_short_breaks": "Kurze Pausen deaktivieren", + "timer_toggle_short_breaks_desc": "Kurze Pause zwischen den Arbeitsrunden überspringen.", + "timer_toggle_long_breaks": "Lange Pausen deaktivieren", + "timer_toggle_long_breaks_desc": "Lange Pause am Ende jedes Zyklus überspringen.", "timer_toggle_countdown": "Countdown-Zifferblatt", "timer_toggle_countdown_desc": "Der Bogen beginnt voll und nimmt im Laufe der Zeit ab.", "timer_reset": "Zurücksetzen", diff --git a/src/messages/en.json b/src/messages/en.json index adc5ebce..9f1deb3b 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "Automatically begin the next work session after a break ends.", "timer_toggle_auto_start_break": "Auto-start Breaks", "timer_toggle_auto_start_break_desc": "Automatically begin a break when a work session ends.", + "timer_toggle_short_breaks": "Disable Short Breaks", + "timer_toggle_short_breaks_desc": "Skip the short break between work rounds.", + "timer_toggle_long_breaks": "Disable Long Breaks", + "timer_toggle_long_breaks_desc": "Skip the long break at the end of each cycle.", "timer_toggle_countdown": "Countdown Dial", "timer_toggle_countdown_desc": "Arc starts full and subtracts as time passes.", "timer_reset": "Reset", diff --git a/src/messages/es.json b/src/messages/es.json index 8c73e63c..4cfc6b7d 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "Comenzar automáticamente la siguiente sesión de trabajo cuando termine un descanso.", "timer_toggle_auto_start_break": "Iniciar descansos automáticamente", "timer_toggle_auto_start_break_desc": "Comenzar automáticamente un descanso cuando termine una sesión de trabajo.", + "timer_toggle_short_breaks": "Desactivar descansos cortos", + "timer_toggle_short_breaks_desc": "Omitir el descanso corto entre rondas de trabajo.", + "timer_toggle_long_breaks": "Desactivar descansos largos", + "timer_toggle_long_breaks_desc": "Omitir el descanso largo al final de cada ciclo.", "timer_toggle_countdown": "Dial de cuenta regresiva", "timer_toggle_countdown_desc": "El arco comienza lleno y se reduce a medida que pasa el tiempo.", "timer_reset": "Restablecer", diff --git a/src/messages/fr.json b/src/messages/fr.json index 1eab4b89..d216ea7c 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "Commencer automatiquement la prochaine session de travail après une pause.", "timer_toggle_auto_start_break": "Démarrage automatique des pauses", "timer_toggle_auto_start_break_desc": "Commencer automatiquement une pause lorsqu'une session de travail se termine.", + "timer_toggle_short_breaks": "Désactiver les pauses courtes", + "timer_toggle_short_breaks_desc": "Ignorer la pause courte entre les sessions de travail.", + "timer_toggle_long_breaks": "Désactiver les pauses longues", + "timer_toggle_long_breaks_desc": "Ignorer la pause longue à la fin de chaque cycle.", "timer_toggle_countdown": "Cadran à rebours", "timer_toggle_countdown_desc": "L'arc commence plein et se réduit au fil du temps.", "timer_reset": "Réinitialiser", diff --git a/src/messages/ja.json b/src/messages/ja.json index 3e5ea16d..d18e3988 100644 --- a/src/messages/ja.json +++ b/src/messages/ja.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "休憩が終わったら次の作業セッションを自動的に開始します。", "timer_toggle_auto_start_break": "休憩を自動開始", "timer_toggle_auto_start_break_desc": "作業セッションが終わったら休憩を自動的に開始します。", + "timer_toggle_short_breaks": "短い休憩を無効にする", + "timer_toggle_short_breaks_desc": "作業ラウンド間の短い休憩をスキップします。", + "timer_toggle_long_breaks": "長い休憩を無効にする", + "timer_toggle_long_breaks_desc": "各サイクルの終わりに長い休憩をスキップします。", "timer_toggle_countdown": "カウントダウンダイヤル", "timer_toggle_countdown_desc": "アークが満タンから始まり、時間の経過とともに減少します。", "timer_reset": "リセット", diff --git a/src/messages/pt.json b/src/messages/pt.json index 1a123f61..dca4a480 100644 --- a/src/messages/pt.json +++ b/src/messages/pt.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "Iniciar automaticamente a próxima sessão de trabalho após o fim de uma pausa.", "timer_toggle_auto_start_break": "Iniciar Pausas Automaticamente", "timer_toggle_auto_start_break_desc": "Iniciar automaticamente uma pausa quando uma sessão de trabalho terminar.", + "timer_toggle_short_breaks": "Desativar Pausas Curtas", + "timer_toggle_short_breaks_desc": "Pular a pausa curta entre rodadas de trabalho.", + "timer_toggle_long_breaks": "Desativar Pausas Longas", + "timer_toggle_long_breaks_desc": "Pular a pausa longa ao final de cada ciclo.", "timer_toggle_countdown": "Mostrador de Contagem Regressiva", "timer_toggle_countdown_desc": "O arco começa cheio e diminui conforme o tempo passa.", "timer_reset": "Redefinir", diff --git a/src/messages/tr.json b/src/messages/tr.json index 22080d5c..5138287f 100644 --- a/src/messages/tr.json +++ b/src/messages/tr.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "Mola bittikten sonra bir sonraki çalışma oturumunu otomatik olarak başlatır.", "timer_toggle_auto_start_break": "Molaları Otomatik Başlat", "timer_toggle_auto_start_break_desc": "Çalışma oturumu bittiğinde molayı otomatik olarak başlatır.", + "timer_toggle_short_breaks": "Kısa Molaları Devre Dışı Bırak", + "timer_toggle_short_breaks_desc": "Çalışma turları arasındaki kısa molayı atla.", + "timer_toggle_long_breaks": "Uzun Molaları Devre Dışı Bırak", + "timer_toggle_long_breaks_desc": "Her döngünün sonundaki uzun molayı atla.", "timer_toggle_countdown": "Geri Sayım Kadranı", "timer_toggle_countdown_desc": "Yay dolu başlar ve zaman geçtikçe azalır.", "timer_reset": "Sıfırla", diff --git a/src/messages/zh.json b/src/messages/zh.json index 10831518..455a8b0e 100644 --- a/src/messages/zh.json +++ b/src/messages/zh.json @@ -17,6 +17,10 @@ "timer_toggle_auto_start_work_desc": "休息结束后自动开始下一个工作时段。", "timer_toggle_auto_start_break": "自动开始休息", "timer_toggle_auto_start_break_desc": "工作时段结束后自动开始休息。", + "timer_toggle_short_breaks": "禁用短休息", + "timer_toggle_short_breaks_desc": "跳过工作轮次之间的短暂休息。", + "timer_toggle_long_breaks": "禁用长休息", + "timer_toggle_long_breaks_desc": "跳过每个周期结束时的长休息。", "timer_toggle_countdown": "倒计时表盘", "timer_toggle_countdown_desc": "表盘从满格开始,随时间推移减少。", "timer_reset": "重置", From 35fb65e97a2f282a825e450889012d510eb68d0e Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 16 Mar 2026 18:06:24 -0600 Subject: [PATCH 2/5] feat: Show round accumulation when long breaks are disabled. - Update spec docs. --- openspec/changes/optional-breaks/design.md | 4 ++++ openspec/changes/optional-breaks/proposal.md | 7 +++++-- .../specs/timer-sequence/spec.md | 18 ++++++++++++++++++ src-tauri/src/timer/mod.rs | 5 +++++ src-tauri/src/timer/sequence.rs | 11 +++++++++++ src-tauri/src/websocket/mod.rs | 1 + src/lib/components/TimerFooter.svelte | 8 ++++++-- src/lib/stores/timer.ts | 1 + src/lib/types.ts | 1 + src/messages/de.json | 1 + src/messages/en.json | 1 + src/messages/es.json | 1 + src/messages/fr.json | 1 + src/messages/ja.json | 1 + src/messages/pt.json | 1 + src/messages/tr.json | 1 + src/messages/zh.json | 1 + 17 files changed, 60 insertions(+), 4 deletions(-) diff --git a/openspec/changes/optional-breaks/design.md b/openspec/changes/optional-breaks/design.md index ea7c029f..2ffe6125 100644 --- a/openspec/changes/optional-breaks/design.md +++ b/openspec/changes/optional-breaks/design.md @@ -48,8 +48,12 @@ Setting `work_round_number = 0` before a substituted ShortBreak is the key detai **Both settings default `true`.** Existing behaviour is fully preserved for all current users. No breaking change; no migration needed to maintain the existing sequence. (Migration 5 still seeds the rows via `INSERT OR IGNORE` for correctness, but the default value matches prior implicit behaviour.) +**"Disable" toggle framing, not "Enable".** Since both break types are on by default, the toggles are labelled "Disable Short Breaks" / "Disable Long Breaks". The knob is OFF (unchecked) in the normal state and turns ON when the user opts out. This means the `checked` prop is bound to `!breaks_enabled` — the inverse of the stored setting — so the visual state matches the label. + **Dim dependent controls, don't hide them.** Consistent with the global shortcuts pattern: controls are always in the DOM, just `opacity: 0.4; pointer-events: none` when their parent toggle is off. When long breaks are disabled, both the Long Break duration slider and the Rounds until Long Break slider are wrapped together in a single `disabled` container. +**Session counter replaces cycle counter when long breaks are disabled.** The `X / Y` round counter (current work round / total before long break) has no meaning when long breaks are off — there is no natural cycle boundary for Y. Instead, `SequenceState` carries a `session_work_count` field: a monotonically-increasing count of Work rounds entered since the last reset, which never resets at cycle boundaries. `TimerFooter` switches between `X / Y` (long breaks enabled) and a localised `"round N"` label (long breaks disabled). The `timer_session_round` i18n key carries an `{n}` parameter so each locale can position the number naturally. + ## Risks / Trade-offs **Risk: `work_round_number = 0` is a surprising intermediate state.** If a snapshot is taken between the Work→ShortBreak transition and the ShortBreak→Work transition, the round number will read as 0. diff --git a/openspec/changes/optional-breaks/proposal.md b/openspec/changes/optional-breaks/proposal.md index 0c73a038..aab9bd09 100644 --- a/openspec/changes/optional-breaks/proposal.md +++ b/openspec/changes/optional-breaks/proposal.md @@ -7,7 +7,8 @@ The Pomodoro technique prescribes fixed work/break cycles, but real workflows va - Add two boolean settings: `short_breaks_enabled` (default `true`) and `long_breaks_enabled` (default `true`). - When short breaks are disabled, the sequence skips ShortBreak entirely; work rounds advance directly to the next work round (counter increments normally). - When long breaks are disabled, the sequence substitutes a ShortBreak at the long-break point (if short breaks are enabled) or loops directly back to Work(1) (if both are disabled). The round counter resets as it would have at the long-break point either way. -- Settings → Timer gains two enable/disable toggles — one above the Short Break duration slider, one above the Long Break duration slider. When a break type is disabled, its duration slider (and for long breaks, the Rounds until Long Break slider) is visually dimmed and non-interactive. +- Settings → Timer gains two "Disable" toggles — one above the Short Break duration slider, one above the Long Break duration slider. Toggles are off by default (breaks are enabled); turning one on opts out of that break type. When a break type is disabled, its duration slider (and for long breaks, the Rounds until Long Break slider) is visually dimmed and non-interactive. +- The round counter in the timer footer (`X / Y`) only makes sense when long breaks are active. When long breaks are disabled, it switches to a session counter that reads "round N" — a rolling count of focus rounds completed since the last reset. ## Capabilities @@ -27,4 +28,6 @@ The Pomodoro technique prescribes fixed work/break cycles, but real workflows va - **Frontend**: `src/lib/types.ts` — add both fields to `Settings` interface. - **Frontend**: `src/lib/stores/settings.ts` — add both fields to the defaults object. - **Frontend**: `src/lib/components/settings/sections/TimerSection.svelte` — add two `SettingsToggle` rows and wrap dependent sliders in a `disabled` container. -- **Frontend**: `src/messages/*.json` — add i18n keys for both toggles across all 8 locales. +- **Frontend**: `src/messages/*.json` — add i18n keys for both toggles and the session round label (`timer_session_round`) across all 8 locales. +- **Rust**: `src-tauri/src/timer/sequence.rs` — add `session_work_count` field to `SequenceState`; exposed via `TimerSnapshot` for the frontend. +- **Frontend**: `src/lib/components/TimerFooter.svelte` — switch round counter display based on `long_breaks_enabled`. diff --git a/openspec/changes/optional-breaks/specs/timer-sequence/spec.md b/openspec/changes/optional-breaks/specs/timer-sequence/spec.md index 9302e14a..c205bf78 100644 --- a/openspec/changes/optional-breaks/specs/timer-sequence/spec.md +++ b/openspec/changes/optional-breaks/specs/timer-sequence/spec.md @@ -46,3 +46,21 @@ The system SHALL support a `long_breaks_enabled` setting (default `true`). When - **AND** `long_breaks_enabled` is `false` - **THEN** the sequence SHALL consist entirely of Work rounds - **AND** `work_round_number` SHALL increment each round and reset to 1 at `work_rounds_total` + +### Requirement: Session work count +`SequenceState` SHALL expose a `session_work_count: u32` field that starts at 1 and increments by 1 each time `advance()` enters a Work round. Unlike `work_round_number`, it SHALL never reset at cycle boundaries — only a call to `reset()` returns it to 1. It is included in `TimerSnapshot` and surfaced to the frontend as a session counter. + +#### Scenario: session_work_count increments across cycle boundaries +- **WHEN** `long_breaks_enabled` is `false` +- **AND** multiple work rounds complete across what would have been a long-break boundary +- **THEN** `session_work_count` SHALL continue incrementing without resetting + +#### Scenario: session_work_count resets on timer reset +- **WHEN** the user triggers a timer reset +- **THEN** `session_work_count` SHALL be reset to 1 + +#### Scenario: Round counter display adapts to long_breaks_enabled +- **WHEN** `long_breaks_enabled` is `true` +- **THEN** the round counter SHALL display `work_round_number / work_rounds_total` +- **WHEN** `long_breaks_enabled` is `false` +- **THEN** the round counter SHALL display a localised "round N" label using `session_work_count` diff --git a/src-tauri/src/timer/mod.rs b/src-tauri/src/timer/mod.rs index 7cb2150e..f6bd134a 100644 --- a/src-tauri/src/timer/mod.rs +++ b/src-tauri/src/timer/mod.rs @@ -33,6 +33,9 @@ pub struct TimerSnapshot { pub is_paused: bool, pub work_round_number: u32, pub work_rounds_total: u32, + /// Monotonically-increasing focus round count since last reset. Used as a + /// session counter when long breaks are disabled. + pub session_work_count: u32, } // --------------------------------------------------------------------------- @@ -187,6 +190,7 @@ impl TimerController { is_paused: !shared.is_running && shared.elapsed_secs > 0, work_round_number: seq.work_round_number, work_rounds_total: seq.work_rounds_total, + session_work_count: seq.session_work_count, } } @@ -502,5 +506,6 @@ fn build_snapshot( is_paused: !sh.is_running && sh.elapsed_secs > 0, work_round_number: seq.work_round_number, work_rounds_total: seq.work_rounds_total, + session_work_count: seq.session_work_count, } } diff --git a/src-tauri/src/timer/sequence.rs b/src-tauri/src/timer/sequence.rs index 98fb88d4..b1fa26c1 100644 --- a/src-tauri/src/timer/sequence.rs +++ b/src-tauri/src/timer/sequence.rs @@ -42,6 +42,10 @@ pub struct SequenceState { pub work_round_number: u32, /// Total work rounds before a long break (from settings). pub work_rounds_total: u32, + /// Monotonically-increasing count of work rounds since the last reset. + /// Unlike `work_round_number` this never resets at cycle boundaries, + /// so it can be used as a session counter when long breaks are disabled. + pub session_work_count: u32, } impl SequenceState { @@ -50,6 +54,7 @@ impl SequenceState { current_round: RoundType::Work, work_round_number: 1, work_rounds_total, + session_work_count: 1, } } @@ -100,6 +105,11 @@ impl SequenceState { } }; + // Increment the session counter every time we enter a new Work round. + if self.current_round == RoundType::Work { + self.session_work_count += 1; + } + let duration = self.current_duration_secs(settings); (self.current_round, duration) } @@ -108,6 +118,7 @@ impl SequenceState { pub fn reset(&mut self) { self.current_round = RoundType::Work; self.work_round_number = 1; + self.session_work_count = 1; } } diff --git a/src-tauri/src/websocket/mod.rs b/src-tauri/src/websocket/mod.rs index aaa68d35..84287c1c 100644 --- a/src-tauri/src/websocket/mod.rs +++ b/src-tauri/src/websocket/mod.rs @@ -270,6 +270,7 @@ mod tests { is_paused: false, work_round_number: 1, work_rounds_total: 4, + session_work_count: 1, }; let event = WsEvent::RoundChange { payload: snap }; let json = serde_json::to_string(&event).unwrap(); diff --git a/src/lib/components/TimerFooter.svelte b/src/lib/components/TimerFooter.svelte index ad8ef059..eadc9abb 100644 --- a/src/lib/components/TimerFooter.svelte +++ b/src/lib/components/TimerFooter.svelte @@ -42,9 +42,13 @@