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}"))
}
/// 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<String>,
error: Option<String>,
}
@@ -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(),
);
}
}