feat(overlay): in-game voice overlay window, Windows (#25) #50

Merged
jared merged 1 commits from feat/voice-overlay into main 2026-11-09 21:31:59 -05:00
Owner

Phase 1 (MVP) of #25, built to the plan in #25 (comment) with the decisions made there: everyone in the call, speakers lit; shown while Lotus isn't focused; off by default (beta); voice only.

Web side: LotusGuild/cinny feat/voice-overlay (setting, useTauriOverlay). Either can merge first. Until both are in, nothing calls the command and nothing shows.

What it does

While you're in a call with the setting on and Lotus isn't the focused window, a small window sits in a corner of the monitor your foreground app is on. It shows:

  • a header with your own Muted / Deafened / PTT state;
  • everyone in the call (you first), with a green ring on whoever is speaking and a "Muted" pill on muted people;
  • "+N more" past 10 people.

How (src-tauri/src/native/overlay.rs)

  • Separate window, no injection:

    • transparent, no decorations, always on top, no taskbar button;
    • focusable(false) plus WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW, shown with SW_SHOWNOACTIVATE and SetWindowPos(…, SWP_NOACTIVATE);
    • set_ignore_cursor_events(true), so clicks pass through.

    It must never take focus from a game.

  • Page: three static files (src-tauri/overlay/) served by the binary over the lotusoverlay custom protocol.

    • CSP default-src 'none'; script-src 'self'; style-src 'self', so no network.
    • No IPC. No capability names the overlay window, and the new command also rejects any caller that isn't main.
    • Rust pushes state with eval of a serde JSON literal. The page builds the DOM with textContent only, so display names can't inject markup.
  • set_overlay_state (async, because creating a window from a sync command can deadlock on Windows):

    • sanitises its input: at most 50 people, names cut to 40 characters, a known corner;
    • stores the snapshot and shows or hides the window;
    • tracks the main window's Focused events.
  • Placement: the work area of the foreground window's monitor (GetForegroundWindow → monitor_from_point), DPI-scaled, 16 px margin, chosen corner. It's recomputed on each update.

  • Elsewhere: Windows only; the command is a no-op on other platforms.

  • Wiring: the command is declared in build.rs and granted in capabilities/migrated.json, like every other command.

Tests

  • cargo test --lib: 37/37, 9 of them new:
    • show/hide rule;
    • corner parsing;
    • input sanitising;
    • window size;
    • placement in each corner and on tiny or offset monitors;
    • script escaping (a name containing ");alert(1);//</script> and U+2028 round-trips as data);
    • the protocol serving only the page files;
    • the page using no innerHTML and no external URLs.
  • cargo check and cargo clippy for x86_64-pc-windows-gnu are clean, so the Windows-only code type-checks. The MSVC build hasn't run yet; I'm starting the Windows smoke workflow on this branch.
  • Not included: the regenerated src-tauri/gen/schemas/*. The build rewrites them, and they only add the new allow-set-overlay-state permission.

Needs a real Windows check before it leaves beta

  1. Settings → Calls → In-game overlay on, join a call, alt-tab into a borderless game: the overlay appears in the chosen corner, the game keeps focus, and clicks pass through.
  2. The speaking ring follows whoever talks, and mute/deafen/PTT show in the header.
  3. Focusing Lotus hides it, and hanging up hides it.
  4. A transparent background (no white or black box) on your setup.
  5. An exclusive-fullscreen game: expected not to show it (documented in the setting's help text).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA

Phase 1 (MVP) of #25, built to the plan in https://code.lotusguild.org/LotusGuild/cinny-desktop/issues/25#issuecomment-6206 with the decisions made there: **everyone in the call, speakers lit; shown while Lotus isn't focused; off by default (beta); voice only.** Web side: LotusGuild/cinny `feat/voice-overlay` (setting, `useTauriOverlay`). Either can merge first. Until both are in, nothing calls the command and nothing shows. ## What it does While you're in a call with the setting on and Lotus isn't the focused window, a small window sits in a corner of the monitor your foreground app is on. It shows: - a header with your own **Muted / Deafened / PTT** state; - **everyone in the call** (you first), with a green ring on whoever is speaking and a "Muted" pill on muted people; - "+N more" past 10 people. ## How (`src-tauri/src/native/overlay.rs`) - **Separate window, no injection:** - transparent, no decorations, always on top, no taskbar button; - `focusable(false)` plus `WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW`, shown with `SW_SHOWNOACTIVATE` and `SetWindowPos(…, SWP_NOACTIVATE)`; - `set_ignore_cursor_events(true)`, so clicks pass through. It must never take focus from a game. - **Page:** three static files (`src-tauri/overlay/`) served by the binary over the `lotusoverlay` custom protocol. - CSP `default-src 'none'; script-src 'self'; style-src 'self'`, so no network. - **No IPC.** No capability names the `overlay` window, and the new command also rejects any caller that isn't `main`. - Rust pushes state with `eval` of a serde JSON literal. The page builds the DOM with `textContent` only, so display names can't inject markup. - **`set_overlay_state`** (async, because creating a window from a sync command can deadlock on Windows): - sanitises its input: at most 50 people, names cut to 40 characters, a known corner; - stores the snapshot and shows or hides the window; - tracks the main window's `Focused` events. - **Placement:** the work area of the foreground window's monitor (`GetForegroundWindow` → `monitor_from_point`), DPI-scaled, 16 px margin, chosen corner. It's recomputed on each update. - **Elsewhere:** Windows only; the command is a no-op on other platforms. - **Wiring:** the command is declared in `build.rs` and granted in `capabilities/migrated.json`, like every other command. ## Tests - `cargo test --lib`: **37/37**, 9 of them new: - show/hide rule; - corner parsing; - input sanitising; - window size; - placement in each corner and on tiny or offset monitors; - script escaping (a name containing `");alert(1);//</script>` and U+2028 round-trips as data); - the protocol serving only the page files; - the page using no `innerHTML` and no external URLs. - `cargo check` and `cargo clippy` for **`x86_64-pc-windows-gnu`** are clean, so the Windows-only code type-checks. The MSVC build hasn't run yet; I'm starting the Windows smoke workflow on this branch. - Not included: the regenerated `src-tauri/gen/schemas/*`. The build rewrites them, and they only add the new `allow-set-overlay-state` permission. ## Needs a real Windows check before it leaves beta 1. Settings → Calls → In-game overlay on, join a call, alt-tab into a **borderless** game: the overlay appears in the chosen corner, the game **keeps focus**, and clicks pass through. 2. The speaking ring follows whoever talks, and mute/deafen/PTT show in the header. 3. Focusing Lotus hides it, and hanging up hides it. 4. A transparent background (no white or black box) on your setup. 5. An exclusive-fullscreen game: expected **not** to show it (documented in the setting's help text). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
jared added 1 commit 2026-11-09 17:26:25 -05:00
While you're in a call and Lotus isn't the focused window, a small
always-on-top, click-through window lists everyone in the call, lights
up whoever is speaking, and shows your own mute / deafen / PTT state.

- A separate window, nothing injected into the game: no anti-cheat
  surface. Never takes focus (focusable(false), WS_EX_NOACTIVATE,
  SW_SHOWNOACTIVATE) and ignores the mouse.
- Its page is three static files served by the binary over the
  `lotusoverlay` custom protocol, with a strict CSP (no network) and no
  IPC (no capability names the window). Rust pushes state in as a JSON
  literal; the page renders with textContent only.
- New `set_overlay_state` command (main window only) takes the call
  snapshot from cinny's `useTauriOverlay`. Sanitised: bounded list and
  names, known corner. Placed in the chosen corner of the foreground
  window's monitor, DPI-scaled.
- Windows only; a no-op elsewhere (Linux has no in-app calls yet, #21).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
jared merged commit 3f1af92520 into main 2026-11-09 21:31:59 -05:00
jared deleted branch feat/voice-overlay 2026-11-09 21:32:29 -05:00
Sign in to join this conversation.