docs: correct Wayland monitor protocol availability and post-0.3.0 drift

The monitor.rs comments claimed Mutter implements ext-data-control-v1 on
GNOME 47+, contradicting ARCHITECTURE.md. Mutter exposes neither
ext-data-control-v1 nor zwlr-data-control-v1, so the monitor never binds
on GNOME and ingest comes from the extension via Meta.Selection +
SubmitItem; the monitor only runs on wlroots. Correct the comments
(module doc, Ext probe, bail! message) and the main.rs spawn comment.

Also reconcile ARCHITECTURE.md with the code: supervisor backoff
(1/2/4/8/16 s, 5 s reset, log-only give-up), SIGTERM at 1.5 s, prune as a
separate post-insert call, multi-thread tokio default, the 200 px
load-more threshold, and the SQL-side preview truncation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Eduard Tolosa 2026-05-26 13:08:50 -05:00
parent a4a6f1a3f9
commit 8a7091755b
4 changed files with 35 additions and 19 deletions

View file

@ -97,3 +97,5 @@ Signals: `ItemAdded(id s, mime_type s, preview s)`, `ItemDeleted(id s)`, `Histor
- `Ordering::Relaxed` is intentional on `Arc<AtomicUsize>` limits (advisory, not critical path). - `Ordering::Relaxed` is intentional on `Arc<AtomicUsize>` limits (advisory, not critical path).
- Never execute clipboard content. Writes go through `wl-clipboard-rs` (`copy_multi`) in the daemon, never via shell subprocess or `eval`. - Never execute clipboard content. Writes go through `wl-clipboard-rs` (`copy_multi`) in the daemon, never via shell subprocess or `eval`.
- Excluded apps list is checked before storing any clipboard item. - Excluded apps list is checked before storing any clipboard item.
- Ingest paths are mutually exclusive by environment: on GNOME the monitor cannot bind (Mutter exposes neither data-control protocol) so ingest is GJS `SubmitItem`; on wlroots the built-in monitor is the path.
- List queries (`get_history_page`, `search_history`) return `content_text` truncated to `PREVIEW_CHARS` via `substr`; full content is served only by `GetItemContent` for paste-back.

View file

@ -57,10 +57,13 @@ hot ingest path (it fires `SubmitItem` and returns immediately).
`bin/strata-daemon` and registers a watchdog: `bin/strata-daemon` and registers a watchdog:
- If the child exits, schedule a respawn with exponential backoff - If the child exits, schedule a respawn with exponential backoff
(1 s, 2 s, 4 s, capped at 30 s). (1 s, 2 s, 4 s, 8 s, 16 s). A run that lasts at least 5 s resets the
- After 5 rapid restarts within 60 s, stop and surface a notification. counter, so only a rapid crash loop escalates.
- After 5 rapid restarts, stop retrying and log an error. (A missing
`strata-daemon` binary is reported up front with a desktop notification;
the crash-loop give-up is log-only.)
- On disable, send `Shutdown` over D-Bus first, then `SIGTERM` if it - On disable, send `Shutdown` over D-Bus first, then `SIGTERM` if it
doesn't exit within 2 s. doesn't exit within 1.5 s.
### Startup ### Startup
@ -182,15 +185,16 @@ search query, not the SQL parser.
### Pruning ### Pruning
`upsert_item` checks `COUNT(*) > max_history` after each insert and After a genuinely new item is inserted, the ingest task calls `prune`,
deletes the oldest excess rows by `created_at ASC`, returning their ids. which deletes every row outside the newest `max_history` (by `created_at`)
The D-Bus layer emits `ItemDeleted` for each so the extension can unlink and returns their ids. The D-Bus layer emits `ItemDeleted` for each so the
the matching `~/.cache/strata/thumbnails/<id>.png`. extension can unlink the matching `~/.cache/strata/thumbnails/<id>.png`.
## Concurrency ## Concurrency
The Rust daemon runs `tokio::main(flavor = "multi_thread")`. zbus The Rust daemon runs on a multi-threaded tokio runtime (`#[tokio::main]`,
dispatches each incoming method call on the executor. rusqlite is sync, which defaults to the multi-thread flavor). zbus dispatches each incoming
method call on the executor. rusqlite is sync,
so every DB call is wrapped: so every DB call is wrapped:
```rust ```rust
@ -227,10 +231,13 @@ history size.
### Paginated history ### Paginated history
`GetHistory(offset, limit)` returns metadata only (id, mime, short text `GetHistory(offset, limit)` returns metadata only (id, mime, short text
preview, timestamp). The panel loads `page-size` rows on open, then preview, timestamp). The preview is truncated in SQL (`substr`, first
another page each time the scroll position passes ~80 % of the viewport. ~200 chars), so a page of large text items costs a few KB of JSON rather
The Rust side serves these from the `idx_created_at DESC` index with than megabytes; the full payload is only fetched on paste-back via
`LIMIT/OFFSET`, which stays O(log n) for any history size. `GetItemContent`. The panel loads `page-size` rows on open, then another
page each time the scroll position comes within a fixed threshold (200 px)
of the bottom. The Rust side serves these from the `idx_created_at DESC`
index with `LIMIT/OFFSET`, which stays O(log n) for any history size.
Search shortcuts this path: when the search box is non-empty the panel Search shortcuts this path: when the search box is non-empty the panel
calls `SearchHistory(query, limit)` instead and disables scroll-driven calls `SearchHistory(query, limit)` instead and disables scroll-driven

View file

@ -1,5 +1,7 @@
/// Wayland clipboard monitor using the ext-data-control-v1 protocol (GNOME 47+) /// Wayland clipboard monitor using the ext-data-control-v1 protocol with a
/// with a zwlr-data-control-v1 fallback for wlroots-based compositors. /// zwlr-data-control-v1 fallback for wlroots-based compositors. GNOME's Mutter
/// exposes neither, so on GNOME this monitor does not bind and clipboard
/// content arrives from the extension via Meta.Selection + SubmitItem.
/// ///
/// Runs on a dedicated OS thread (NOT on the tokio runtime) to avoid blocking /// Runs on a dedicated OS thread (NOT on the tokio runtime) to avoid blocking
/// the async executor. Communicates clipboard change events to the tokio world /// the async executor. Communicates clipboard change events to the tokio world
@ -245,7 +247,9 @@ fn probe_protocol(conn: &Connection) -> Result<Protocol> {
// A quick roundtrip to get the global list; no persistent state needed here. // A quick roundtrip to get the global list; no persistent state needed here.
let (globals, _) = registry_queue_init::<MonitorState>(conn)?; let (globals, _) = registry_queue_init::<MonitorState>(conn)?;
// ext-data-control-v1 preferred (GNOME 47+, KDE Plasma 6) // ext-data-control-v1 preferred (KDE Plasma 6 and other compositors that
// expose it). GNOME's Mutter does NOT expose it, so this probe fails on
// GNOME and ingest falls back to the extension's SubmitItem path.
if globals.contents().with_list(|list| { if globals.contents().with_list(|list| {
list.iter() list.iter()
.any(|g| g.interface == "ext_data_control_manager_v1") .any(|g| g.interface == "ext_data_control_manager_v1")
@ -263,7 +267,8 @@ fn probe_protocol(conn: &Connection) -> Result<Protocol> {
bail!( bail!(
"No supported clipboard control protocol found.\n\ "No supported clipboard control protocol found.\n\
- GNOME: requires GNOME 47+ (Mutter implements ext-data-control-v1)\n\ - GNOME: Mutter exposes neither protocol; ingest comes from the\n\
extension via Meta.Selection + SubmitItem (this is expected).\n\
- wlroots: requires zwlr-data-control-v1 (Sway, Hyprland, etc.)" - wlroots: requires zwlr-data-control-v1 (Sway, Hyprland, etc.)"
); );
} }

View file

@ -67,8 +67,10 @@ async fn main() -> Result<()> {
tracing::info!("D-Bus service registered as org.gnome.Strata"); tracing::info!("D-Bus service registered as org.gnome.Strata");
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Clipboard monitor (Wayland protocols - optional, soft-fail on GNOME) // Clipboard monitor (Wayland protocols - optional, soft-fail on GNOME).
// GJS provides clipboard content via SubmitItem when this is unavailable. // GNOME's Mutter does not expose ext-data-control-v1 or zwlr-data-control-v1,
// so this fails to bind there and the extension feeds content via SubmitItem
// instead. On wlroots compositors (Sway, Hyprland) the monitor is the path.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
match clipboard::monitor::spawn(clip_tx) { match clipboard::monitor::spawn(clip_tx) {
Ok(()) => tracing::info!("Wayland clipboard monitor started"), Ok(()) => tracing::info!("Wayland clipboard monitor started"),