From 125e44c033e52261e89f9103ac1e1e1c7ceec082 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Mon, 25 May 2026 03:06:38 -0400 Subject: [PATCH] feat(gui): auto-copy the host ticket on start, with a copied indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI/interactive host auto-copies the ticket to the clipboard and says so; the GUI host only offered a manual Copy button. Users conditioned by the CLI assumed the GUI auto-copied too, didn't click Copy, and pasted whatever stale ticket was already in the clipboard — then dialed a dead host and saw an unexplained "can't connect". (Compounded by flaky Wayland clipboard / KDE Connect sync.) Now the ticket is copied the moment it arrives (same arboard path as the manual button), with a green "✓ Copied to clipboard" confirmation. Auto-copy failure is non-fatal: the code stays visible, is now selectable for manual copy, and a hint tells the user to click Copy. Verified: clicking only Start lands a fresh ticket in the clipboard (wl-paste). Co-Authored-By: Claude Opus 4.7 --- src/gui/mod.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index fdfc7dd..ba63074 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -34,6 +34,15 @@ pub fn run() -> anyhow::Result<()> { .map_err(|e| anyhow::anyhow!("GUI failed to start: {e}")) } +/// Best-effort clipboard write. Returns whether it succeeded so callers can +/// show an honest "✓ Copied" / fallback hint (the clipboard can be flaky on +/// Wayland, and a silent miss is what left users pasting stale tickets). +fn set_clipboard(text: &str) -> bool { + arboard::Clipboard::new() + .and_then(|mut cb| cb.set_text(text.to_owned())) + .is_ok() +} + /// Which screen the single window is currently showing. #[derive(Default, PartialEq)] enum Screen { @@ -117,6 +126,11 @@ struct HostState { active: u32, max: u32, capturing: bool, + /// Whether the current ticket made it onto the clipboard (auto-copy on + /// arrival, or a manual Copy click). Drives the "✓ Copied" hint so the + /// user isn't left guessing whether to click Copy — the trap that had + /// people pasting a stale clipboard ticket. + copied: bool, last_refusal: Option, error: Option, } @@ -134,6 +148,7 @@ impl Default for HostState { active: 0, max: 0, capturing: false, + copied: false, last_refusal: None, error: None, } @@ -329,11 +344,29 @@ impl PixelPassApp { ui.label("Share this code with your viewer(s):"); ui.add_space(4.0); egui::Frame::group(ui.style()).show(ui, |ui| { - ui.add(egui::Label::new(egui::RichText::new(&ticket).monospace().small()).wrap()); + ui.add( + egui::Label::new(egui::RichText::new(&ticket).monospace().small()) + .wrap() + .selectable(true), + ); }); ui.add_space(4.0); - if ui.button("📋 Copy code").clicked() { - self.copy_to_clipboard(&ticket); + ui.horizontal(|ui| { + if ui.button("📋 Copy code").clicked() { + self.copy_to_clipboard(&ticket); + } + if self.host.copied { + ui.colored_label(egui::Color32::LIGHT_GREEN, "✓ Copied to clipboard"); + } + }); + if !self.host.copied { + ui.label( + egui::RichText::new( + "Couldn't auto-copy — click Copy, or select the code above.", + ) + .small() + .weak(), + ); } } @@ -362,6 +395,7 @@ impl PixelPassApp { self.host.active = 0; self.host.max = 0; self.host.capturing = false; + self.host.copied = false; let mut args = vec![ "--host".to_string(), @@ -392,6 +426,7 @@ impl PixelPassApp { self.host.proc = None; self.host.capturing = false; self.host.ticket = None; + self.host.copied = false; } /// Drain the host child's event channel into state, and detect an @@ -424,7 +459,15 @@ impl PixelPassApp { fn apply_host_event(&mut self, ev: ChildEvent) { match ev { - ChildEvent::Ticket { value } => self.host.ticket = Some(value), + ChildEvent::Ticket { value } => { + // Auto-copy on arrival, mirroring the CLI/interactive host + // (which copies the ticket and prints "copied to your + // clipboard"). A failure here is non-fatal: the ticket stays + // visible for manual copy, and `copied` stays false so the UI + // doesn't falsely claim success. + self.host.copied = set_clipboard(&value); + self.host.ticket = Some(value); + } ChildEvent::HostInfo { display_server, capture, @@ -456,9 +499,18 @@ impl PixelPassApp { } } + /// Manual Copy-button handler: copy and reflect success in the UI, or + /// surface a clear error if the clipboard rejected it. fn copy_to_clipboard(&mut self, text: &str) { - if let Err(e) = arboard::Clipboard::new().and_then(|mut cb| cb.set_text(text.to_owned())) { - self.host.error = Some(format!("Clipboard copy failed: {e}")); + if set_clipboard(text) { + self.host.copied = true; + self.host.error = None; + } else { + self.host.copied = false; + self.host.error = Some( + "Couldn't write to the clipboard. Select the code above and copy it manually." + .to_string(), + ); } }