From d23848decc557022534434e222e8958083c21080 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Mon, 25 May 2026 03:37:53 -0400 Subject: [PATCH] feat(gui): paste button, Enter-to-connect, version, and clipboard prefill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick viewer-flow polish: - a "📋 Paste" button pinned to the right of the code field — the read-side mirror of the host's Copy button; - Enter in the code field connects (same decode gate as the button); - the View screen prefills the field from the clipboard on open when it holds a decodable ticket and the field is empty, so the freshly-shared code is usually already there (live decode still shows the id to verify); - the menu shows the binary version under the heading. Tightens the common "host clicks Copy → viewer clicks Paste → Connect" loop; the prefill only ever drops in a *valid* ticket, so it can't reintroduce the stale/garbage paste it guards against. Co-Authored-By: Claude Opus 4.7 --- src/gui/mod.rs | 64 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 03f4083..c25c5a9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -43,6 +43,15 @@ fn set_clipboard(text: &str) -> bool { .is_ok() } +/// Best-effort clipboard read, backing the viewer Paste button and the +/// View-screen prefill. `None` on a flaky/empty clipboard — callers just leave +/// the field untouched. +fn get_clipboard() -> Option { + arboard::Clipboard::new() + .and_then(|mut cb| cb.get_text()) + .ok() +} + /// Decode the host endpoint id from a ticket string, using the exact same /// parse the viewer does (`EndpointTicket::from_str`), so the GUI agrees with /// the child about what's a valid code. `None` means it isn't a pixelpass @@ -233,6 +242,11 @@ impl PixelPassApp { ui.add_space(24.0); ui.heading("PixelPass"); ui.label("P2P screen sharing"); + ui.label( + egui::RichText::new(concat!("v", env!("CARGO_PKG_VERSION"))) + .small() + .weak(), + ); ui.add_space(32.0); if ui .add_sized([260.0, 40.0], egui::Button::new("Host — share my screen")) @@ -249,6 +263,7 @@ impl PixelPassApp { .clicked() { self.screen = Screen::Viewer; + self.prefill_viewer_ticket(); } }); } @@ -580,11 +595,26 @@ impl PixelPassApp { ui.label("Paste the share code you received:"); ui.add_space(4.0); - ui.add( - egui::TextEdit::singleline(&mut self.viewer.ticket_input) - .desired_width(f32::INFINITY) - .hint_text("endpoint…"), - ); + // Field fills the row, with a Paste button pinned to its right — the + // read-side mirror of the host's Copy button (one click grabs the code + // the host just put on the clipboard). + let ticket_resp = ui + .with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("📋 Paste").clicked() + && let Some(text) = get_clipboard() + { + self.viewer.ticket_input = text.trim().to_string(); + } + ui.add( + egui::TextEdit::singleline(&mut self.viewer.ticket_input) + .desired_width(f32::INFINITY) + .hint_text("endpoint…"), + ) + }) + .inner; + // Enter in the field connects (gated on a decodable code below). + let enter_pressed = + ticket_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); // Decode the pasted code live: confirms it's a real ticket and shows // which host it points at, so a stale clipboard paste is caught here @@ -621,14 +651,14 @@ impl PixelPassApp { ui.add_space(16.0); // Only dial a code that actually decodes — no point spawning a child to - // spend 15s timing out against garbage. - if ui + // spend 15s timing out against garbage. Enter takes the same path. + let connect_clicked = ui .add_enabled( decoded_id.is_some(), egui::Button::new("Connect").min_size(egui::vec2(140.0, 36.0)), ) - .clicked() - { + .clicked(); + if decoded_id.is_some() && (connect_clicked || enter_pressed) { self.start_viewer(ui.ctx().clone()); } } @@ -656,6 +686,22 @@ impl PixelPassApp { } } + /// On entering the View screen, drop a clipboard ticket straight into the + /// field — the host's Copy button (and our auto-copy) usually leaves the + /// freshly-shared code right there. Guarded so it only fires when the field + /// is empty and the clipboard holds a *decodable* ticket, so stray + /// clipboard text never lands in the box; the live decode still shows the + /// id for the user to verify. + fn prefill_viewer_ticket(&mut self) { + if self.viewer.ticket_input.trim().is_empty() + && self.viewer.proc.is_none() + && let Some(text) = get_clipboard() + && ticket_endpoint_id(&text).is_some() + { + self.viewer.ticket_input = text.trim().to_string(); + } + } + fn start_viewer(&mut self, ctx: egui::Context) { self.viewer.error = None; self.viewer.url = None;