Release 0.3.3: Linux terminal clipboard, selection sync, Gdk log filter#104
Release 0.3.3: Linux terminal clipboard, selection sync, Gdk log filter#104
Conversation
- Read/write terminal clipboard via wl-paste/wl-copy and xclip (subprocess + timeout) - Sanitize paste; Ctrl+Shift+C/V and Shift+Insert; selection syncs to primary (debounced) - Suppress noisy Gdk broken-pipe selection warning on Wayland - Bump app version and document in CHANGELOG, releases.md, Flatpak metainfo
There was a problem hiding this comment.
Pull request overview
Prepares the desktop app for v0.3.3, focusing on more reliable Linux terminal clipboard/selection behavior (Wayland/X11) and reducing noisy GTK/Gdk logging, plus the usual version/documentation bumps for a release.
Changes:
- Reworks terminal copy/paste UX: sanitizes pasted text, adds Linux-friendly shortcuts, and syncs selection to clipboard/primary (debounced).
- Moves Linux clipboard I/O to subprocess calls (
wl-paste/wl-copy,xclip) with timeouts and adds a targeted Gdk warning filter. - Bumps version references to 0.3.3 and updates release docs/changelog/Flatpak metadata.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| flatpak/dev.nosuckshell.desktop.metainfo.xml | Adds 0.3.3 release entry for Flatpak metadata. |
| docs/releases.md | Updates “current release” pointers to 0.3.3. |
| docs/CHANGELOG.md | Adds 0.3.3 changelog entry and release link. |
| apps/desktop/src/tauri-api.ts | Exposes new Tauri command for writing terminal selection to clipboard. |
| apps/desktop/src/features/terminal-paste-sanitize.ts | Introduces paste sanitization helper for terminal input. |
| apps/desktop/src/features/terminal-paste-sanitize.test.ts | Adds Vitest coverage for paste sanitization behavior. |
| apps/desktop/src/e2e/tauri-core-shim.ts | Stubs the new Tauri invoke command for e2e. |
| apps/desktop/src/components/TerminalPane.tsx | Implements copy/paste shortcuts, selection→clipboard sync, and paste sanitization. |
| apps/desktop/src/components/HelpPanel.tsx | Documents the new terminal copy/paste behavior and shortcuts. |
| apps/desktop/src-tauri/tauri.conf.json | Bumps Tauri app version to 0.3.3. |
| apps/desktop/src-tauri/src/main.rs | Implements Linux clipboard subprocess read/write and registers new command + Gdk filter. |
| apps/desktop/src-tauri/src/gdk_log_suppress.rs | Adds GLib log handler to suppress the specific noisy Gdk warning line. |
| apps/desktop/src-tauri/Cargo.toml | Bumps Rust crate version to 0.3.3. |
| apps/desktop/package.json | Bumps desktop package version to 0.3.3. |
| apps/desktop/package-lock.json | Bumps lockfile package versions to 0.3.3. |
Files not reviewed (1)
- apps/desktop/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline", "--type", "text/plain"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("wl-paste", &["--primary", "--no-newline"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-t", "UTF8_STRING", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-t", "UTF8_STRING", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); |
There was a problem hiding this comment.
These branches return sanitize_terminal_clipboard_text(t); immediately after a command succeeds. If sanitization returns None (e.g., replacement chars from invalid UTF-8 or only stripped control bytes), the function returns None and skips trying later fallbacks. Consider chaining clipboard_from_command(...).and_then(sanitize_terminal_clipboard_text) and only returning when the sanitized result is Some, otherwise continue to the next source.
| ) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline", "--type", "text/plain"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--primary", "--no-newline"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-t", "UTF8_STRING", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-t", "UTF8_STRING", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| ) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline", "--type", "text/plain"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--primary", "--no-newline"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-t", "UTF8_STRING", "-o"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-o"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-t", "UTF8_STRING", "-o"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-o"]) | |
| .and_then(sanitize_terminal_clipboard_text) | |
| { | |
| return Some(t); |
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline", "--type", "text/plain"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("wl-paste", &["--primary", "--no-newline"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-t", "UTF8_STRING", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-t", "UTF8_STRING", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); | ||
| } | ||
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-o"]) { | ||
| return sanitize_terminal_clipboard_text(t); |
There was a problem hiding this comment.
Same issue as above for the xclip fallbacks: return sanitize_terminal_clipboard_text(t); can short-circuit to None after a successful command output, preventing later fallbacks (clipboard selection / arboard) from being attempted. Only return when sanitization yields Some, otherwise keep falling through.
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline", "--type", "text/plain"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--primary", "--no-newline"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-t", "UTF8_STRING", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-t", "UTF8_STRING", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-o"]) { | |
| return sanitize_terminal_clipboard_text(t); | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline", "--type", "text/plain"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--primary", "--no-newline"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("wl-paste", &["--no-newline"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-t", "UTF8_STRING", "-o"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "primary", "-o"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-t", "UTF8_STRING", "-o"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } | |
| } | |
| if let Some(t) = clipboard_from_command("xclip", &["-selection", "clipboard", "-o"]) { | |
| if let Some(t) = sanitize_terminal_clipboard_text(t) { | |
| return Some(t); | |
| } |
| let out = ""; | ||
| for (const ch of input) { | ||
| if (ch === "\0") continue; | ||
| if (ch === "\n" || ch === "\r" || ch === "\t") { | ||
| out += ch; | ||
| continue; | ||
| } | ||
| const cp = ch.codePointAt(0)!; | ||
| if (cp < 0x20 || cp === 0x7f || (cp >= 0x80 && cp <= 0x9f)) continue; | ||
| out += ch; | ||
| } | ||
| return out; |
There was a problem hiding this comment.
Building out via out += ch in a per-character loop can be unnecessarily slow for large pastes (creates many intermediate strings). Consider accumulating characters into an array and join(""), or otherwise batching writes, to keep sanitization linear-time with less allocation churn.
| let out = ""; | |
| for (const ch of input) { | |
| if (ch === "\0") continue; | |
| if (ch === "\n" || ch === "\r" || ch === "\t") { | |
| out += ch; | |
| continue; | |
| } | |
| const cp = ch.codePointAt(0)!; | |
| if (cp < 0x20 || cp === 0x7f || (cp >= 0x80 && cp <= 0x9f)) continue; | |
| out += ch; | |
| } | |
| return out; | |
| const out: string[] = []; | |
| for (const ch of input) { | |
| if (ch === "\0") continue; | |
| if (ch === "\n" || ch === "\r" || ch === "\t") { | |
| out.push(ch); | |
| continue; | |
| } | |
| const cp = ch.codePointAt(0)!; | |
| if (cp < 0x20 || cp === 0x7f || (cp >= 0x80 && cp <= 0x9f)) continue; | |
| out.push(ch); | |
| } | |
| return out.join(""); |
Summary
Prepares v0.3.3 with Linux/Wayland terminal clipboard hardening and UX aligned with typical Linux terminals.
Changes
wl-paste/wl-copyandxclipvia subprocess withtimeout; arboard as guarded fallback. Avoids WebKit/GTK Wayland deadlocks from in-process clipboard crates.Version / docs
package.json,tauri.conf.json,Cargo.toml.docs/CHANGELOG.md,docs/releases.md, Flatpak metainfo updated.After merge
v0.3.3to runrelease.yml(full matrix + Arch job if configured there).0.3.3→ enable Linux (and optionally other targets). The Linux job builds bundles and runs the Archmakepkgstep in Docker; download artifactmanual-bundle-linux-arch-<run_id>for.pkg.tar.zst.Validation
bash .agents/skills/nosuckshell_ops/scripts/validate_project.shpassed locally (tsc, Vitest,cargo test).