feat: keep Clipbin recording in the tray

Add native Linux SNI and Windows notification-area trays, single-instance reopening, background startup and login entries. Preserve recording and discovery after close, with clean Quit and reachable no-tray fallbacks.

Track capture-source audio delivery independently of silent output tracks and warn without stopping video. Give clip metadata its own row and shape labels at final bounds to avoid GPUI 0.2.2 truncation cache and text-run bugs.

Validated with strict workspace checks, 57 tests, Windows core/OBS/probe cross-clippy, all ten capture scenarios, tray/session/clips/playback rigs and narrow/wide screenshots. Document runtime validation gaps.
This commit is contained in:
2026-10-13 18:29:37 +02:00
parent 178c400eeb
commit f1c31d6c90
21 changed files with 1413 additions and 133 deletions
+166 -14
View File
@@ -8,9 +8,9 @@ pass/fail results.
| Where | Script | Status |
| --- | --- | --- |
| Linux, headless rig (Wayland portal + Xwayland) | `scripts/validate/linux-rig.sh` | All 9 scenarios pass on devbox (Intel UHD 770, VA-API) |
| Linux, headless rig (Wayland portal + Xwayland) | `scripts/validate/linux-rig.sh` | All 10 scenarios pass on devbox (Intel UHD 770, VA-API), 2026-10-13 |
| Linux, real session (CachyOS, Hyprland, RX 9070 XT, 1440p60) | `scripts/validate/linux-hardware.sh` | Passed 2026-10-08 (run 4, dc9cbd6) |
| Linux desktop automatic sessions (headless Wayland) | `scripts/validate/linux-session.sh` | Passed 2026-10-08: Recording sidebar, game exit, library reload, window close and active Quit |
| Linux desktop automatic sessions (headless Wayland) | `scripts/validate/linux-session.sh` | Passed 2026-10-13: recording sidebar, close-to-tray, game exit, second-launch reopen and active Quit |
| Windows (Game Capture, NVENC/AMF/QSV) | `scripts/validate/windows.ps1` | Not run yet; script untested |
## Probe commands
@@ -174,8 +174,11 @@ Windows status:
monitor's own size, set when capture first has a picture (portrait monitors
record portrait). If the window is resized mid-session the size stays and
the picture is scaled to fit.
- **Audio capture failures are not detected yet.** When the PulseAudio server
was unreachable, recording continued with a silent track.
- **Audio delivery health.** The configured system/microphone capture sources
are monitored directly through OBS audio capture callbacks. After five seconds
without delivery, recording is degraded with “audio capture lost”; video
continues. The silent muxer track cannot mask absent source callbacks. See
Background operation below for validation and limits.
- **Linux runtime dependencies.** The Linux OBS build links the system's
FFmpeg, PipeWire and other libraries, so it is tied to the distribution it
was built on. Packaging (for example Flatpak, or bundling FFmpeg) is an open
@@ -251,17 +254,19 @@ Real Steam validated 2026-10-08 on CachyOS/Hyprland (native Steam): Escape
from Duckov (AppId 3167020, Proton Experimental under SteamLinuxRuntime_4)
runs under `reaper SteamLaunch AppId=3167020 -- …/_v2-entry-point … proton
waitforexitandrun …/Duckov.exe`. Clipbin detected it, named it from its
manifest ("Escape from Duckov") and recorded the session. Audio-source
failure detection remains limited as described above.
manifest ("Escape from Duckov") and recorded the session. Audio delivery loss
is now detected as described below; the new warning still needs real hardware
and Windows validation.
GPUI 0.2.2 ends its Linux event loop when the last window is removed, even
without an application close callback. Main keeps the recorder worker alive
after that return. Closing the window continues the current game session;
when that game exits, it finalises and the process exits. If no session is
active, it exits immediately. It stops discovering new games once closed,
so there is no indefinitely running invisible app. Reopening the window and
persistent background discovery need the future tray integration. Explicit
Quit/Ctrl+Q stops and finalises the current recording immediately.
GPUI 0.2.2 ends its Linux and Windows event loops when the last window is
removed. Clipbin keeps the recorder, tray and single-instance endpoint alive
outside each GPUI run, and constructs a fresh Application on the main thread
when reopened. The previous route is restored and the library/settings are
reloaded; unavailable sessions fall back to the library. This also releases
playback resources while the interface is closed. No fake or hidden GPUI window
is kept alive: GPUI's Linux backends ignore WindowOptions::show. Window bounds,
scroll offsets and unsaved editor selection are not restored. Explicit Quit
waits for the existing worker's clean session finish before joining/exiting.
## Clips, bookmarks and storage cleanup
@@ -497,3 +502,150 @@ Logs are `/tmp/clipbin-clips-quit-fixed.log` and
output directories. `bash -n`, `git diff --check` and `cargo fmt --check` also
passed. This follow-up changes only the Linux rig scripts and documentation;
Windows runtime and real Hyprland validation limits above remain unchanged.
## Background operation (2026-10-13)
Linux uses [ksni 0.3.6](https://github.com/iovxw/ksni) with the blocking,
async-io API. It implements SNI and com.canonical.dbusmenu on its own executor,
works on Wayland and X11, and requires no GTK main loop. Using tray-icon on
Linux would require GTK alongside GPUI's main-thread loop, so it is only a
Windows dependency. Windows uses [tray-icon 0.26.1](https://github.com/tauri-apps/tray-icon)
with its menu and notification-area icon on a dedicated Win32 message thread;
its documented Windows event loop does not need the main thread. GPUI owns the
main thread on both platforms. Platform services live in clipbin-core and are
included in Windows cross-clippy. The new raw Win32 calls are confined to
windows_background.rs, with a module unsafe allowance, SAFETY comments and
crate-level unsafe_code=deny; desktop retains the workspace forbid policy.
The tray has purple idle, red recording and amber failed/degraded variants,
with status, game and error detail in its tooltip. Open Clipbin, Save clip,
Add bookmark and Quit use the recorder's existing actions. Linux requires an
SNI watcher with a registered host: startup failure opens the interface even
with --background, logs the reason and makes close finish/quit. A watcher that
disappears later requests Open and disables close-to-tray until it returns.
This avoids leaving an unreachable process after a bar/watcher disappears.
Linux owns dev.clipbin.Clipbin on the session bus and exposes Open at
/dev/clipbin/Clipbin. A second process requests the same name without queueing,
calls Open on the owner and exits before starting OBS or reconciling autostart.
A session bus is required; endpoint acquisition errors are logged and startup
exits rather than starting a duplicate recorder. Windows holds a per-user
Local named mutex and posts a private message to the first instance's hidden
endpoint window, with a bounded five-second startup race wait. The hidden
endpoint owns its message thread independently of the GPUI windows.
Start on login reconciles on startup and each settings save. Linux writes or
removes $XDG_CONFIG_HOME/autostart/clipbin.desktop (default ~/.config/autostart),
with a quoted executable path and --background; Windows writes/removes the
Clipbin value in HKCU\Software\Microsoft\Windows\CurrentVersion\Run. Changing
login settings does not interrupt capture. The executable must stay at its
installed path; development builds point the entry at the build executable.
Actual desktop login dispatch remains unvalidated.
Audio health uses [OBS 32.0.4's source capture callback API](https://github.com/obsproject/obs-studio/blob/32.0.4/libobs/obs-source.c)
on every configured system/microphone source, independently of the encoder,
mixed audio counters and silent output track. A boxed callback context stores
an atomic monotonic delivery timestamp; it performs no allocation or locking.
The context is removed before source release: OBS's audio_cb_mutex serialises
callback removal with callbacks. Five seconds without delivery (including a
source that never starts) is a warning-level RecordingStatus::Degraded,
with “audio capture lost” in the sidebar and tray. A new callback clears the
warning; video/source/output/disk failures still take precedence and stop
recording. Intentionally disabled audio has no callback and no warning. This
checks delivery, not amplitude: valid silent samples are healthy. It cannot
detect a driver delivering endless synthetic silence, and WASAPI silence or
idle-device callback behaviour needs a Windows runtime check. No automatic
source restart is added; video continues and audio can recover if OBS resumes
its source. Source identity is not displayed separately for system vs mic loss.
The rig's audio failure injection kills its private pipewire-pulse daemon,
leaving PipeWire video and the portal alive. Unloading the null sink and
requesting kill-source-output did not reliably stop delivery in PipeWire 1.6.8;
killing that private audio server deterministically stops source callbacks.
The original nine scenarios are retained alongside this tenth warning case.
The rig now uses an explicit private SWAYSOCK and waits for it; discovery by
the previous sway-ipc glob did not locate the installed Sway 1.12 endpoint.
CLIPBIN_RIG_KEEP_LOGS also preserves startup-failure logs.
Windows limitations for this step: core/OBS/probe cross-clippy covers the native
tray, message pump, named mutex/hidden-window endpoint, HKCU autostart and OBS
callback registration/removal. Nothing new has been linked or run on Windows.
GPUI desktop and clipbin-media remain excluded from that lint set because the
MSVC C toolchain/FFmpeg development import libraries are unavailable here.
Unvalidated: tray rendering/menu focus/Explorer restart, icon/tooltips, singleton
startup races and multi-user/desktop behaviour, foreground activation, repeated
GPUI Application creation after close, clean Quit/file closure, --background,
registry writes and real login launch, WASAPI delivery-loss/recovery/idle
semantics, and the Clips layout on Windows. Real Linux desktop trays (Hyprland
Waybar, KDE, GNOME extensions), login launch, SNI host replacement/re-registration
and native X11 tray lifecycle still need runtime validation.
Clips cards give title/subtitle their own row and keep fixed-size Play/Delete
controls below. The pinned GPUI text element caches nowrap widths independently
of truncation width and mutates its text runs during repeated measurements;
ordinary truncate produced hard clipping, ellipsis-only text, or fallback fonts
in the grid. A small private canvas label uses GPUI's line-wrapper and shaping
APIs once at the final bounds, retaining Geist Mono, colour, weight and proper
ellipsis without patching GPUI. Newlines in game names become spaces. Sample
screenshots at 1100x760 and 1600x1000 were inspected: all sample metadata is
readable and controls cannot steal its width. The amber audio warning was also
inspected in lifecycle-final/audio-lost.png. Screenshot validation exercises
the actual rendering path; no unit test can reproduce the GPU/grid text
measurement chain without the rig.
Validation commands, from the background-tray worktree (all passed):
```sh
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo fmt --all -- --check
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo clippy --locked --workspace --all-targets -- -D warnings
env PATH=/usr/bin:/bin:$HOME/.cargo/bin cargo test --locked --workspace
RUSTUP_HOME=$HOME/.cache/clipbin/rustup CARGO_HOME=$HOME/.cache/clipbin/cargo-home PATH=$HOME/.cache/clipbin/cargo-home/bin:$PATH CARGO_TARGET_DIR=target/windows-check cargo clippy --locked --target x86_64-pc-windows-msvc -p clipbin-core -p clipbin-obs -p clipbin-probe --all-targets -- -D warnings
```
57 workspace tests passed (generated-media tests keep their existing opt-in
fixture behaviour). The added status test covers audio degradation, recovery
and video-loss precedence. The dependency's existing proc-macro-error2 future
incompatibility notice is still printed; strict current clippy passes.
/tmp is a user-quota tmpfs here: an earlier recording probe failed with
Disk quota exceeded despite df reporting free bytes. Successful serial rig
runs used repository output storage, with a short absolute TMPDIR so private
Unix socket paths fit their limits:
```sh
export PATH=/usr/bin:/bin:$HOME/.cargo/bin
export TMPDIR=/home/harry/Dev/clipbin-desktop/target/rig-tmp
out=/home/harry/Dev/clipbin-desktop/target/background-validation
mkdir -p "$TMPDIR"
scripts/validate/linux-rig.sh "$out/capture"
scripts/validate/linux-background.sh "$out/lifecycle-final"
scripts/validate/linux-session.sh "$out/sessions"
scripts/validate/linux-clips.sh "$out/clips"
scripts/validate/linux-playback.sh "$out/playback"
CLIPBIN_RIG_WIDTH=1100 CLIPBIN_RIG_HEIGHT=760 scripts/rig/screenshot-ui.sh "$out/screens-narrow-labels" clips
CLIPBIN_RIG_WIDTH=1600 CLIPBIN_RIG_HEIGHT=1000 scripts/rig/screenshot-ui.sh "$out/screens-wide-labels" clips
```
Capture: all original nine scenarios plus audio loss passed. Lifecycle: actual
watcher registration and D-Bus menu Open/Quit, recording tooltip, no-window
background start, autostart toggle removal/write and startup reconciliation,
segments growing after close, second launch reopening the same process,
preserved Settings route, degraded UI/tooltip, clean active-session Quit,
watcher-loss reveal and no-host close=Quit fallback passed. Sessions: game exit,
exact cumulative segment placement, continued background discovery/library
reload, second-launch Open and active Ctrl+Q passed. Clips: H.264 MP4 retained,
360 CFR frames, continuous audio, zero decode errors, original press range,
bookmarks, tiny-budget cleanup and bookmark re-export passed. Playback: moving
preview (6.1% changed pixels), device audio RMS 0.08027, thumbnails, continued
recording, gaps, cross-boundary seek, volume/mute and held-key controls passed.
Decode median/p95 was 6.335/10.472 ms; GPUI upload median/p95 was 0.208/0.404 ms.
Both old UI suites still use explicit Quit and completed successfully.
The simplify quick self-review covered reuse, clarity and efficiency before
commit: removed the obsolete window-close shutdown mode, reused existing clip,
bookmark and clean-finish paths, avoided copying the full library for tray
polling, limited icon updates to state changes, kept callbacks allocation/lock
free, and made native tray/thread cleanup explicit. The text helper is limited
to the demonstrated GPUI 0.2.2 rendering bug; no dependency fork or general UI
framework was added. Native Windows runtime and real desktop limits above
remain unchanged.