Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
* **Settings and Statistics windows left orphaned when main window is closed** — closing the main window while Settings or Statistics was open left those windows running with no way to reopen the main window. Both child windows are now closed automatically whenever the main window is truly closed (not when hiding to tray).
* **macOS auto-update failing with "platform not found"** — the updater manifest (`latest.json`) used `darwin-universal` as the macOS platform key, which Tauri 2's updater does not recognise; it checks only for `darwin-aarch64` (Apple Silicon) and `darwin-x86_64` (Intel). Additionally, the release workflow pointed the updater at the `.dmg` distribution file rather than the `.app.tar.gz` bundle that the Tauri updater downloads and applies in-place. Both are fixed: the manifest now lists `darwin-aarch64` and `darwin-x86_64` entries, and the workflow uploads and references the `.app.tar.gz` artifact.

### Timer

* **Optional breaks** — short breaks and long breaks can now be independently disabled in Settings → Timer. Disabling short breaks chains work rounds back-to-back; disabling long breaks substitutes a short break at the cycle boundary so the round counter resets cleanly. When both are disabled the timer runs in a pure work loop. Duration sliders for disabled break types are dimmed to indicate they have no effect. When long breaks are disabled the round indicator switches from the `X / Y` cycle counter to a rolling "round N" session counter that increments continuously and resets only when the timer is reset.

[v1.2.0] - 2026-03-16
-----------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-14
74 changes: 74 additions & 0 deletions openspec/changes/archive/2026-03-16-optional-breaks/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## 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.)

**"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.
→ 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.
33 changes: 33 additions & 0 deletions openspec/changes/archive/2026-03-16-optional-breaks/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## 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 "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

### 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 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`.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## 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`

### 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`
Loading
Loading