feat(gui): auto-copy the host ticket on start, with a copied indicator

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:06:38 -04:00
parent 57328f740c
commit 125e44c033
+58 -6
View File
@@ -34,6 +34,15 @@ pub fn run() -> anyhow::Result<()> {
.map_err(|e| anyhow::anyhow!("GUI failed to start: {e}")) .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. /// Which screen the single window is currently showing.
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
enum Screen { enum Screen {
@@ -117,6 +126,11 @@ struct HostState {
active: u32, active: u32,
max: u32, max: u32,
capturing: bool, 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<String>, last_refusal: Option<String>,
error: Option<String>, error: Option<String>,
} }
@@ -134,6 +148,7 @@ impl Default for HostState {
active: 0, active: 0,
max: 0, max: 0,
capturing: false, capturing: false,
copied: false,
last_refusal: None, last_refusal: None,
error: None, error: None,
} }
@@ -329,11 +344,29 @@ impl PixelPassApp {
ui.label("Share this code with your viewer(s):"); ui.label("Share this code with your viewer(s):");
ui.add_space(4.0); ui.add_space(4.0);
egui::Frame::group(ui.style()).show(ui, |ui| { 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); ui.add_space(4.0);
if ui.button("📋 Copy code").clicked() { ui.horizontal(|ui| {
self.copy_to_clipboard(&ticket); 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.active = 0;
self.host.max = 0; self.host.max = 0;
self.host.capturing = false; self.host.capturing = false;
self.host.copied = false;
let mut args = vec![ let mut args = vec![
"--host".to_string(), "--host".to_string(),
@@ -392,6 +426,7 @@ impl PixelPassApp {
self.host.proc = None; self.host.proc = None;
self.host.capturing = false; self.host.capturing = false;
self.host.ticket = None; self.host.ticket = None;
self.host.copied = false;
} }
/// Drain the host child's event channel into state, and detect an /// 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) { fn apply_host_event(&mut self, ev: ChildEvent) {
match ev { 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 { ChildEvent::HostInfo {
display_server, display_server,
capture, 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) { 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())) { if set_clipboard(text) {
self.host.error = Some(format!("Clipboard copy failed: {e}")); 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(),
);
} }
} }