Skip to content

feat: 017 — configurable display spanning - #14

Merged
archae0pteryx merged 2 commits into
mainfrom
feat/017-display-spanning
May 2, 2026
Merged

feat: 017 — configurable display spanning#14
archae0pteryx merged 2 commits into
mainfrom
feat/017-display-spanning

Conversation

@archae0pteryx

@archae0pteryx archae0pteryx commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds Displays submenu to tray listing all connected monitors; primary enabled by default with ✓ checkmark
  • Each toggled monitor gets its own transparent overlay WebviewWindow (overlay-0, overlay-1, …) with independent PigHitTester + polling thread
  • Selection persists in settings.yaml under displays.enabled and survives restart
  • "main" window renamed to "overlay-0" throughout; capabilities use overlay-* glob

Test plan

  • App launches without crash (runtime gate per feedback memory)
  • Tray Displays submenu lists connected monitors with ✓ on primary
  • Toggling a secondary display creates its overlay within 1s
  • Toggling it off hides the overlay within 1s
  • ~/.adhd-ranch/settings.yaml updates with correct displays.enabled value on toggle
  • Restarting app restores display selection from settings
  • Click-through hit-testing works independently on each enabled display
  • task check green

Closes #11

Summary by CodeRabbit

  • New Features

    • Multi-monitor transparent overlay windows with per-monitor hit-testing and automatic monitor detection.
    • Tray menu now exposes per-display toggles; display selections persist to settings.
    • New interoperable rectangle/hit-test API for reporting/updating on-screen target regions.
    • App menu and window targets updated to use overlay window labels.
  • Bug Fixes

    • Settings access no longer consumes state; settings are preserved across calls.

Add Displays submenu to tray listing all connected monitors with checkmarks.
Toggling a monitor shows/hides a per-display overlay window within 1s.
Selection persists in settings.yaml and survives restart.

- domain: DisplayConfig { enabled_indices: Vec<usize> } added to Settings;
  to_yaml/parse_yaml updated (comma-separated format); Settings no longer Copy
- overlay_manager: OverlayManager manages per-display WebviewWindow lifecycle
  and per-window PigHitTester + polling thread; window ops dispatched to main thread
- tray: Displays submenu above focus list; toggle handler persists + applies config
- ui_bridge: update_pig_rects injects WebviewWindow to route rects per-window
- tauri.conf.json: "main" window renamed to "overlay-0"
- capabilities: "main" → "overlay-*" glob
@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds multi-display overlay support: a DisplayConfig in Settings, per-monitor overlay windows managed by a new OverlayManager, tray "Displays" submenu to toggle monitors (persisted to settings.yaml), and plumbing so per-window pig hit-testing and rect updates route to the appropriate overlay.

Changes

Multi-Display Overlay Architecture

Layer / File(s) Summary
Data Shape
crates/domain/src/settings.rs, crates/domain/src/pig_rect.rs, crates/domain/src/lib.rs
Adds DisplayConfig { enabled_indices: Vec<usize> } (default [0]) and new PigRect + RectUpdater trait; re-exports DisplayConfig, PigRect, and RectUpdater. Settings gains a displays: DisplayConfig field.
Settings Persistence
crates/domain/src/settings.rs, crates/storage/src/settings_writer.rs
Settings::to_yaml() now emits displays.enabled as comma-separated indices; Settings::parse_yaml() parses it (skips invalid entries). Tests and storage writer tests updated to include displays.
Core Overlay Manager
src-tauri/src/app/overlay_manager.rs
New OverlayManager that ensures one transparent webview per enabled monitor (overlay-{idx}), manages PigHitTester per overlay, spawns per-window cursor-polling hit-test threads, and exposes update_rects(label, rects) via RectUpdater impl. Defines MonitorInfo and OverlayManagerState.
Application Wiring
src-tauri/src/app/mod.rs, src-tauri/tauri.conf.json
Enumerates available_monitors() into MonitorsState; stores DisplayConfig in DisplayConfigState (Arc<Mutex<_>>); instantiates OverlayManager, registers OverlayManagerState, calls overlay_manager.apply(...) at setup; removes legacy single-window hit-test setup; changes default window label to overlay-0.
Tray & Menu Integration
src-tauri/src/app/tray.rs, src-tauri/src/app/menu.rs
Tray setup signature now takes settings_path; tray menu builds a “Displays” submenu listing monitors with tray-display-<idx> IDs. Display-toggle handler updates DisplayConfigState, persists settings to disk, calls OverlayManager.apply on main thread, and rebuilds the tray menu. MAIN_WINDOW constant updated to overlay-0 so menu actions target overlay window.
UI Bridge & Commands
src-tauri/src/ui_bridge/mod.rs, crates/commands/src/lib.rs, crates/commands/src/caps.rs
PigHitState now holds Arc<dyn RectUpdater>; update_pig_rects command receives tauri::WebviewWindow and routes rects to update_rects(window.label(), rects). Commands::settings() now returns self.settings.clone(). Test helper populates Settings.displays.
Configuration & Permissions
src-tauri/capabilities/default.json, src-tauri/tauri.conf.json
Capability windows allowed pattern changed to overlay-*; first window label in tauri.conf.json changed to overlay-0.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Tray as Tray Menu
    participant Handler as Toggle Handler
    participant State as DisplayConfigState
    participant Writer as Settings Writer
    participant AppMain as App Main Thread
    participant OverlayMgr as OverlayManager

    User->>Tray: click monitor checkbox
    Tray->>Handler: dispatch DISPLAY_PREFIX event
    Handler->>State: lock & toggle index
    State->>State: update enabled_indices
    Handler->>Writer: persist DisplayConfig to settings.yaml
    Writer->>Writer: read/parse settings.yaml -> write updated settings
    Handler->>AppMain: run_on_main_thread(apply overlays)
    AppMain->>OverlayMgr: apply(monitors, config)
    OverlayMgr->>OverlayMgr: for each monitor index
    alt enabled
        OverlayMgr->>OverlayMgr: ensure_shown -> create/show overlay-{idx}
        OverlayMgr->>OverlayMgr: spawn hit-test polling thread
    else disabled
        OverlayMgr->>OverlayMgr: destroy/hide overlay-{idx}
    end
    AppMain->>Tray: rebuild_tray_menu()
    Tray->>User: updated menu checkmarks
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped from monitor one to two,

now overlays span the whole view.
Tray toggles dance, the pigs roam free,
each screen a stage for joy and glee. 🐷✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% 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 'feat: 017 — configurable display spanning' directly and clearly describes the main change: adding configurable display spanning functionality.
Linked Issues check ✅ Passed All primary coding requirements from issue #11 are met: DisplayConfig added to Settings with default [0]; Displays submenu implemented with monitor enumeration; per-monitor overlay windows created with correct labels and positioning; independent PigHitTester per overlay; settings persisted to YAML and restored on restart.
Out of Scope Changes check ✅ Passed All changes align with issue #11 requirements: new DisplayConfig/PigRect/RectUpdater abstractions; OverlayManager for window management; tray menu display toggles; PigHitState refactoring to support per-window hit-testing; window label changes and capability updates. No extraneous changes detected.

✏️ 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 feat/017-display-spanning

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/storage/src/settings_writer.rs (1)

45-71: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

preserves_all_keys test should explicitly verify displays.

This test currently uses DisplayConfig::default() and never asserts displays, so a regression could slip through undetected.

Proposed test hardening
         let original = Settings {
@@
-            displays: DisplayConfig::default(),
+            displays: DisplayConfig {
+                enabled_indices: vec![0, 2],
+            },
         };
@@
         assert!(final_settings.alerts.system_notifications);
         assert!(final_settings.widget.always_on_top);
+        assert_eq!(final_settings.displays.enabled_indices, vec![0, 2]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/storage/src/settings_writer.rs` around lines 45 - 71, The
preserves_all_keys test doesn't assert the displays field so regressions can
slip through; update the test (preserves_all_keys) to initialize
original.displays with a non-default DisplayConfig (or at least inspect the
default) and add assertions after reloading that final_settings.displays equals
the original displays (compare relevant DisplayConfig fields or implement
PartialEq check) to ensure write_settings/Settings::parse_yaml preserve the
displays value; reference Settings, DisplayConfig, write_settings, and
Settings::parse_yaml when locating the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/domain/src/settings.rs`:
- Around line 127-134: The match arm handling ("displays", "enabled") currently
skips assigning settings.displays.enabled_indices when the parsed indices vector
is empty, preventing an intentionally empty selection from being persisted;
change the logic so parsed Vec<usize> (built by the split/trim/parse/filter_map
sequence) is always assigned to settings.displays.enabled_indices regardless of
emptiness (i.e., remove the if !indices.is_empty() guard) so that an explicit
empty "enabled:" round-trips correctly.

In `@src-tauri/src/app/mod.rs`:
- Around line 99-108: available_monitors() is being silently ignored via
unwrap_or_default(), losing the error; change to capture its Result, log any Err
with context (e.g., using tracing::error or eprintln!) and then proceed to map
the Ok iterator into MonitorInfo (the mapping code that constructs MonitorInfo {
name: ..., size: ..., position: ... } can remain). In short: replace the
unwrap_or_default() call with a match or map_err that logs the error, then use
the Ok value (or an empty iterator) to build the Vec<MonitorInfo>.

In `@src-tauri/src/app/overlay_manager.rs`:
- Around line 53-62: Currently hidden overlays are never torn down so their
poller loops keep running; change the teardown logic so that when an overlay is
not enabled or its monitor no longer exists you fully close/destroy the webview
rather than just hiding it. In the loop in overlay_manager.rs that builds label
= format!("overlay-{idx}") (and the similar cleanup block later), replace the
hide-only branch that calls app.get_webview_window(&label).hide() with logic
that obtains the window and calls the API to close/destroy the window (so the
poller exits), and additionally add a cleanup pass that enumerates existing
overlay windows (matching "overlay-{n}") and closes any whose index is >=
monitors.len() or not present in config.enabled_indices; ensure this uses the
same ensure_shown/teardown patterns so associated resources/pollers are cleaned
up.

In `@src-tauri/src/app/tray.rs`:
- Around line 213-218: The toggle logic for display indices should validate that
idx is within the current MonitorsState bounds before mutating
config.enabled_indices: check that idx < monitors_state.0.len() and only then
push/sort or retain/remove; if idx is out of bounds simply ignore the add (or
remove if present) to avoid persisting stale indices—update the block around
enabled_indices handling (the idx variable and MonitorsState usage) to perform
this guard and keep OverlayManager::apply() semantics unchanged.

In `@src-tauri/src/ui_bridge/mod.rs`:
- Around line 12-17: The ui_bridge currently depends directly on
app::overlay_manager by wrapping OverlayManager in PigHitState and referencing
PigRect; extract a trait in the domain layer (e.g., trait RectUpdater with fn
update_rects(&self, label: &str, rects: Vec<PigRect>)) and move PigRect into the
domain if it isn't already, then change PigHitState to hold a boxed trait object
(e.g., Box<dyn RectUpdater + Send + Sync>) or a generic parameter instead of
OverlayManager, implement the trait for app::overlay_manager::OverlayManager,
and update any code that constructs PigHitState to pass the OverlayManager via
the trait so ui_bridge no longer imports OverlayManager directly.

---

Outside diff comments:
In `@crates/storage/src/settings_writer.rs`:
- Around line 45-71: The preserves_all_keys test doesn't assert the displays
field so regressions can slip through; update the test (preserves_all_keys) to
initialize original.displays with a non-default DisplayConfig (or at least
inspect the default) and add assertions after reloading that
final_settings.displays equals the original displays (compare relevant
DisplayConfig fields or implement PartialEq check) to ensure
write_settings/Settings::parse_yaml preserve the displays value; reference
Settings, DisplayConfig, write_settings, and Settings::parse_yaml when locating
the changes.
🪄 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: f9e59439-fb61-4875-80cf-a63e7388fff9

📥 Commits

Reviewing files that changed from the base of the PR and between 18cd9d5 and bae5b99.

📒 Files selected for processing (12)
  • crates/commands/src/caps.rs
  • crates/commands/src/lib.rs
  • crates/domain/src/lib.rs
  • crates/domain/src/settings.rs
  • crates/storage/src/settings_writer.rs
  • src-tauri/capabilities/default.json
  • src-tauri/src/app/menu.rs
  • src-tauri/src/app/mod.rs
  • src-tauri/src/app/overlay_manager.rs
  • src-tauri/src/app/tray.rs
  • src-tauri/src/ui_bridge/mod.rs
  • src-tauri/tauri.conf.json

Comment thread crates/domain/src/settings.rs Outdated
Comment thread src-tauri/src/app/mod.rs Outdated
Comment thread src-tauri/src/app/overlay_manager.rs
Comment thread src-tauri/src/app/tray.rs
Comment thread src-tauri/src/ui_bridge/mod.rs Outdated
- domain: add PigRect + RectUpdater trait; move PigRect out of pig_hittest
  so ui_bridge depends on domain, not on app internals
- overlay_manager: implement RectUpdater; destroy (window.close) disabled
  overlays instead of hide so poller threads exit cleanly; add stale-window
  cleanup for indices beyond current monitor count; expose OverlayManagerState
- tray: use OverlayManagerState for apply; guard idx < monitors.len() before
  toggling to prevent stale config entries from panicking
- mod.rs: log available_monitors() error instead of swallowing with
  unwrap_or_default; wire Arc<dyn RectUpdater> into PigHitState
- settings.rs: remove !is_empty() guard so explicit empty enabled: round-trips
- settings_writer test: assert displays field survives write/read cycle
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.

017 — Configurable display spanning

1 participant