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:
+58
-6
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user