Skip to content

[032] Preferences window#34

Merged
archae0pteryx merged 2 commits intomainfrom
032-preferences-window
May 4, 2026
Merged

[032] Preferences window#34
archae0pteryx merged 2 commits intomainfrom
032-preferences-window

Conversation

@archae0pteryx
Copy link
Copy Markdown
Contributor

@archae0pteryx archae0pteryx commented May 4, 2026

Summary

  • Preferences window opened via Cmd-, or tray → Settings… — created on demand, destroyed on close, reopens cleanly
  • Tray simplified: Settings submenu replaced with a single Settings… item; all settings live in the window
  • Sections: General (max focuses / tasks), Widget (always on top, confirm delete), Displays (monitor toggles), Alerts (system notifications), Debug (debug overlay, devtools)
  • Debug overlay off by default; toggled from Settings → Debug → Debug Overlay; uses app.emit from Rust for reliable cross-window delivery
  • Dynamic window sizing via ResizeObserver + getBoundingClientRect().height (includes padding); window fits content exactly

New Tauri commands

get_settings, update_settings, get_monitors, get_debug_overlay, set_debug_overlay, toggle_devtools, get_devtools_open

Test plan

  • Cmd-, opens Preferences window
  • Tray → Settings… opens Preferences window
  • Close window, reopen — works without restart
  • Toggle Debug Overlay on/off — red bar appears/disappears on overlay
  • Toggle Always on Top — takes effect immediately
  • Change max focuses — persists to settings.yaml
  • Display toggles update active overlay windows
  • task check green

Summary by CodeRabbit

  • New Features

    • Preferences window (Cmd/Ctrl+, or tray "Settings…") with grouped sections: General, Displays, Widget, Alerts, Debug.
    • Display/monitor management and per-display enablement.
    • Debug overlay (ephemeral) toggle and DevTools control.
    • Settings persist immediately and apply runtime changes (e.g., always-on-top) without restart.
    • Window created/destroyed on demand and auto-resizes to content.
  • Documentation

    • Added a PRD/spec describing behaviors and acceptance criteria.

…rlay control

- Add on-demand Preferences window (Cmd-,  / tray → Settings…)
  - Sections: General (caps), Widget, Displays, Alerts, Debug
  - Auto-sizes to content via ResizeObserver + Tauri setSize
  - Reopens cleanly after close: destroys stale label, creates fresh window
- Simplify tray: replace Settings submenu with single Settings… item
- Add Tauri commands: get/update settings, get monitors, get/set debug overlay,
  toggle/query devtools
- Debug overlay off by default; toggled via Settings > Debug > Debug Overlay
  using backend app.emit for reliable cross-window event delivery
- DebugOverlayState managed in app; devtools toggle uses is_devtools_open
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

This PR adds a Tauri "Preferences" (Settings) window opened from Cmd-, or the tray "Settings…" item. It defines backend state and commands for getting/updating settings and monitors, persists settings to disk, applies runtime changes (e.g., always_on_top to overlays) and rebuilds the tray, and implements a React frontend (types, API wrappers, hook, component, entry HTML/entrypoint, styles) plus Vite wiring.

Changes

Preferences Window Implementation

Layer / File(s) Summary
Specification
issues/032-preferences-window.md
PRD for Preferences window: entry points, sections (General, Displays, Widget, Alerts, Debug), backend/frontend responsibilities, wiring, and acceptance criteria.
Type Definitions
src/types/settings.ts, src/types/monitor.ts
Adds TS interfaces: Settings (+ Caps, Widget, Alerts, DisplayConfig) and MonitorInfo.
Backend State & Commands
src-tauri/src/app/mod.rs, src-tauri/src/ui_bridge/mod.rs
Registers SettingsPathState and DebugOverlayState; exposes Tauri commands: get_settings, update_settings (persists settings.yaml, applies overlay always_on_top, updates display config and rebuilds tray), get_monitors, get_debug_overlay, set_debug_overlay, toggle_devtools, get_devtools_open.
Menu & Tray Integration
src-tauri/src/app/menu.rs, src-tauri/src/app/tray.rs
Adds "Preferences…" menu item (CmdOrCtrl+,) wired to open settings; tray simplified to single "Settings…" item and a TRAY_OPEN_PREFS_ID handler; tray setup signature changed (no settings_path arg) and rebuild_tray_menu made public.
Window Management
src-tauri/src/app/mod.rs
Adds open_settings_window to focus/create/destroy the "settings" WebView with fixed sizing.
Frontend API Layer
src/api/settings.ts, src/api/debug.ts, src/api/monitors.ts
Adds Tauri invoke wrappers: getSettings, updateSettings, getMonitors, getDebugOverlay, setDebugOverlay, toggleDevtools, getDevtoolsOpen.
Frontend State Management
src/hooks/useSettingsWindow.ts, src/hooks/useDebugOverlay.ts
useSettingsWindow loads settings/monitors/debug/devtools, exposes update, setDebugOverlayEnabled, toggleDevtoolsOpen; useDebugOverlay initial visible state now false.
Frontend UI Components
src/components/SettingsWindow.tsx, settings.html
SettingsWindow React component with ToggleRow and NumberRow, conditional Displays section per monitors; settings.html entrypoint with #root.
Frontend App Entrypoint
src/settings.tsx
Mounts SettingsWindow, wires useSettingsWindow, installs ResizeObserver to resize Tauri window height (width fixed at 380).
Styling
src/styles.css
Adds styles for settings window layout, sections, rows, toggles, and numeric inputs.
Build Configuration
vite.config.ts
Adds settings: resolve(__dirname, "settings.html") to Rollup input entrypoints.

Sequence Diagram

sequenceDiagram
    actor User
    participant Menu as Menu/Tray
    participant Backend as Tauri Backend
    participant Frontend as Settings React App
    participant Storage as Settings File

    User->>Menu: Click "Preferences…" or "Settings…"
    Menu->>Backend: open_settings_window
    Backend->>Backend: create/focus settings webview
    Backend->>Frontend: load settings window assets

    Frontend->>Backend: get_settings()
    Backend->>Storage: read settings.yaml
    Backend->>Frontend: return Settings

    Frontend->>Backend: get_monitors()
    Backend->>Frontend: return MonitorInfo[]

    Frontend->>Backend: get_debug_overlay()
    Backend->>Frontend: return bool

    User->>Frontend: Edit a setting (e.g., always_on_top)
    Frontend->>Backend: update_settings(modified_settings)
    Backend->>Storage: persist settings.yaml
    Backend->>Backend: apply always_on_top to overlay-N windows
    Backend->>Backend: rebuild_tray_menu
    Backend->>Frontend: confirm update

    User->>Frontend: Toggle Debug Overlay
    Frontend->>Backend: set_debug_overlay(enabled)
    Backend->>Frontend: emit debug-overlay-toggle event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰
A window pops up, tidy and bright,
Toggles and numbers all just right.
I hop through monitors, settings in tow,
Persisting the changes so the overlays glow. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[032] Preferences window' clearly and directly summarizes the main change: implementation of a preferences/settings window feature. It is concise, specific, and accurately reflects the primary objective of the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 032-preferences-window

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@issues/032-preferences-window.md`:
- Line 20: Update the spec so the tray behavior and acceptance criteria match
the implemented flow: replace the old "Settings submenu / Open Preferences…"
wording and any mention that "Displays stay in the tray (complex, per-monitor)"
with the new single tray entry labeled "Settings…" and move the Displays section
into the Preferences window; update the Acceptance criteria and any checklist
items (headings or sections named "Displays", "Settings submenu", and
"Acceptance criteria") to reflect that Displays are configured inside
Preferences rather than via the tray, and ensure all references and examples
throughout the document consistently use the new "Settings…" tray entry and
Preferences-based Displays workflow.

In `@src-tauri/src/ui_bridge/mod.rs`:
- Around line 157-173: The update_settings command currently calls the concrete
write_settings function directly, coupling ui_bridge to the storage
implementation; refactor update_settings to accept an injected storage trait
object (e.g., a SettingsPersister) via State (in addition to or replacing
SettingsPathState) and call its persist method instead of write_settings. Locate
the update_settings function and replace the direct
write_settings(&path_state.0, &settings) call with something like
persister.persist_settings(&path, &settings) (use the trait name
SettingsPersister and method name persist_settings or save_settings
consistently), and ensure the injected State type (e.g., State<'_, dyn
SettingsPersister> or a wrapper SettingsStorageState) is used to get the
concrete persister; keep the existing lock/update of SettingsState and error
mapping logic but surface storage errors via map_err as before.
- Around line 237-243: The command set_debug_overlay currently emits
"debug-overlay-toggle" regardless of whether state.0.lock() succeeded; change it
so the event is emitted only after successfully acquiring the lock and updating
the value (move the app.emit call inside the Ok branch that sets *v = enabled),
and in the Err/poisoned-lock case emit the actual stored value instead of the
requested enabled (or skip emit and log the error) so frontend cannot desync
from the backend state; locate the lock call state.0.lock(), the mutation *v =
enabled, and the app.emit("debug-overlay-toggle", ...) to implement this.
- Around line 186-190: The DisplayConfigState lock is currently being silently
ignored on poison; update the block that uses
app.try_state::<crate::app::DisplayConfigState>() / display_state.0.lock() to
handle a poisoned mutex the same way as the SettingsState path—either propagate
a Tauri error (return Err(...)) or at minimum log the failure with context—so
that a poisoned lock doesn't leave DisplayConfigState stale while SettingsState
was updated; refer to the existing SettingsState lock handling for the exact
error propagation/logging pattern and apply it to the DisplayConfigState branch.

In `@src/components/SettingsWindow.tsx`:
- Around line 167-169: The prop type mismatch: ToggleRow calls onChange(v:
boolean) but the handler onToggleDevtools is declared as () => void; either
update onToggleDevtools to accept a boolean parameter (e.g., onToggleDevtools(v:
boolean)) and use it to set state explicitly, or wrap the existing no-arg
handler in a short adapter when passing to ToggleRow (e.g., onChange={(v) =>
onToggleDevtools()}), making the intent explicit; locate the usage around
ToggleRow, the devtoolsOpen state, and the onToggleDevtools handler to apply the
change so the prop signature matches ToggleRow's onChange(v: boolean).

In `@src/hooks/useSettingsWindow.ts`:
- Around line 31-44: The callbacks update, setDebugOverlayEnabled, and
toggleDevtoolsOpen currently perform optimistic local state updates then call
updateSettings, setDebugOverlay, and toggleDevtools without handling failures;
change each to perform the state update optimistically but catch rejection to
either rollback the local state (restore previous value from a captured prev
variable) or surface a user-visible error (e.g., trigger a toast/notification)
so the UI does not remain inconsistent — specifically modify update to capture
prevSettings before setSettings and restore it on updateSettings failure, modify
setDebugOverlayEnabled to capture prev debug state from setDebugOverlayState and
restore on setDebugOverlay failure (or show toast), and modify
toggleDevtoolsOpen to capture previous devtools open state from setDevtoolsOpen
and restore on toggleDevtools failure (or show toast).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cc0a8ef8-9df8-435a-9ac7-d85e527d7aba

📥 Commits

Reviewing files that changed from the base of the PR and between fcab392 and b609cf7.

📒 Files selected for processing (17)
  • issues/032-preferences-window.md
  • settings.html
  • src-tauri/src/app/menu.rs
  • src-tauri/src/app/mod.rs
  • src-tauri/src/app/tray.rs
  • src-tauri/src/ui_bridge/mod.rs
  • src/api/debug.ts
  • src/api/monitors.ts
  • src/api/settings.ts
  • src/components/SettingsWindow.tsx
  • src/hooks/useDebugOverlay.ts
  • src/hooks/useSettingsWindow.ts
  • src/settings.tsx
  • src/styles.css
  • src/types/monitor.ts
  • src/types/settings.ts
  • vite.config.ts

Comment thread issues/032-preferences-window.md Outdated
Comment on lines +157 to +173
#[tauri::command]
pub fn update_settings(
settings: Settings,
app: AppHandle<Wry>,
state: State<'_, SettingsState>,
path_state: State<'_, SettingsPathState>,
) -> Result<(), String> {
let old_displays = {
let Ok(mut s) = state.0.lock() else {
return Err("settings lock poisoned".to_string());
};
let old = s.displays.clone();
*s = settings.clone();
old
};

write_settings(&path_state.0, &settings).map_err(|e| format!("persist settings: {e}"))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Direct storage call bypasses trait interface.

write_settings is called directly rather than through an injected trait, which couples this command to the concrete storage implementation. As per coding guidelines, ui_bridge/ should depend on storage via traits.

Consider injecting a storage trait (e.g., SettingsPersister) as state to decouple this layer from the concrete implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/ui_bridge/mod.rs` around lines 157 - 173, The update_settings
command currently calls the concrete write_settings function directly, coupling
ui_bridge to the storage implementation; refactor update_settings to accept an
injected storage trait object (e.g., a SettingsPersister) via State (in addition
to or replacing SettingsPathState) and call its persist method instead of
write_settings. Locate the update_settings function and replace the direct
write_settings(&path_state.0, &settings) call with something like
persister.persist_settings(&path, &settings) (use the trait name
SettingsPersister and method name persist_settings or save_settings
consistently), and ensure the injected State type (e.g., State<'_, dyn
SettingsPersister> or a wrapper SettingsStorageState) is used to get the
concrete persister; keep the existing lock/update of SettingsState and error
mapping logic but surface storage errors via map_err as before.

Comment thread src-tauri/src/ui_bridge/mod.rs
Comment thread src-tauri/src/ui_bridge/mod.rs
Comment thread src/components/SettingsWindow.tsx
Comment on lines +31 to +44
const update = useCallback((next: Settings) => {
setSettings(next);
updateSettings(next).catch(console.error);
}, []);

const setDebugOverlayEnabled = useCallback((enabled: boolean) => {
setDebugOverlayState(enabled);
setDebugOverlay(enabled).catch(console.error);
}, []);

const toggleDevtoolsOpen = useCallback(() => {
setDevtoolsOpen((prev) => !prev);
toggleDevtools().catch(console.error);
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Optimistic updates without rollback on failure.

The callbacks update local state immediately, then persist asynchronously. If the backend call fails, the UI will show stale/incorrect state until the next mount. This is especially notable for toggleDevtoolsOpen where the actual devtools state may differ from displayed state.

For a settings window, this is typically acceptable UX (user can retry), but consider whether a toast/notification on failure would be helpful for user feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useSettingsWindow.ts` around lines 31 - 44, The callbacks update,
setDebugOverlayEnabled, and toggleDevtoolsOpen currently perform optimistic
local state updates then call updateSettings, setDebugOverlay, and
toggleDevtools without handling failures; change each to perform the state
update optimistically but catch rejection to either rollback the local state
(restore previous value from a captured prev variable) or surface a user-visible
error (e.g., trigger a toast/notification) so the UI does not remain
inconsistent — specifically modify update to capture prevSettings before
setSettings and restore it on updateSettings failure, modify
setDebugOverlayEnabled to capture prev debug state from setDebugOverlayState and
restore on setDebugOverlay failure (or show toast), and modify
toggleDevtoolsOpen to capture previous devtools open state from setDevtoolsOpen
and restore on toggleDevtools failure (or show toast).

- Log DisplayConfigState lock failure instead of silently ignoring (#3)
- set_debug_overlay: only emit event when lock succeeds, not on failure (#4)
- SettingsWindow: wrap onToggleDevtools in arrow to explicitly drop the
  boolean arg from ToggleRow onChange (#5)
- Update 032 issue spec to match implemented tray/display behavior (#1)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/ui_bridge/mod.rs`:
- Around line 245-256: toggle_devtools (and similarly get_devtools_open) only
target the single window "overlay-0"; either make the behavior explicit by
adding a short comment above toggle_devtools stating this is intentionally
limited to the first overlay for debugging, or change the implementation to
allow multi-overlay control by iterating over all overlay windows (reuse the
iteration pattern used around lines 179-183) or by accepting a window identifier
parameter and calling app.get_webview_window(window_id) so the caller can target
a specific overlay; update toggle_devtools and get_devtools_open accordingly and
keep the logic for opening/closing devtools
(win.is_devtools_open()/open_devtools()/close_devtools()) intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c2fd6a9e-a2ad-48c0-af2e-cbe04d8a1b4e

📥 Commits

Reviewing files that changed from the base of the PR and between b609cf7 and 2d8fe50.

📒 Files selected for processing (3)
  • issues/032-preferences-window.md
  • src-tauri/src/ui_bridge/mod.rs
  • src/components/SettingsWindow.tsx

Comment on lines +245 to +256
#[tauri::command]
pub fn toggle_devtools(app: AppHandle<Wry>) {
if let Some(win) = app.get_webview_window("overlay-0") {
#[cfg(debug_assertions)]
if win.is_devtools_open() {
win.close_devtools();
} else {
win.open_devtools();
}
let _ = win;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

DevTools toggle targets only overlay-0.

toggle_devtools and get_devtools_open only operate on the first overlay window. If this is intentional for debugging purposes, consider adding a brief comment. If multi-monitor devtools control is needed later, this would require iteration similar to lines 179-183.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/ui_bridge/mod.rs` around lines 245 - 256, toggle_devtools (and
similarly get_devtools_open) only target the single window "overlay-0"; either
make the behavior explicit by adding a short comment above toggle_devtools
stating this is intentionally limited to the first overlay for debugging, or
change the implementation to allow multi-overlay control by iterating over all
overlay windows (reuse the iteration pattern used around lines 179-183) or by
accepting a window identifier parameter and calling
app.get_webview_window(window_id) so the caller can target a specific overlay;
update toggle_devtools and get_devtools_open accordingly and keep the logic for
opening/closing devtools
(win.is_devtools_open()/open_devtools()/close_devtools()) intact.

@archae0pteryx archae0pteryx merged commit fd9c4cc into main May 4, 2026
2 checks passed
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