interactive: Host/View entry menu, clipboard copy, player picker

Bare `pixelpass` now opens a dialoguer-driven Host/View menu instead of
going straight to host mode. Host path copies the ticket to the system
clipboard via arboard with silent print-only fallback. View path
prompts for the ticket, then after the local listener binds prompts
mpv-vs-VLC and spawns it detached (setsid + null stdio) so the player
survives pixelpass exiting.

Headless invocations (`pixelpass <ticket>`, `pixelpass --repair`)
unchanged. Per spec at ~/Documents/pixelpass-interactive-mode-spec.md.
This commit is contained in:
2026-05-18 15:46:57 -04:00
parent 4cab9f6b20
commit bb437a73cf
9 changed files with 410 additions and 15 deletions
+25 -3
View File
@@ -30,7 +30,8 @@ pub async fn run(opts: HostOpts) -> Result<()> {
let addr = endpoint.addr();
let ticket = EndpointTicket::new(addr);
print_host_banner(&ticket, display, &opts);
let clipboard_ok = opts.interactive && copy_to_clipboard(&ticket.to_string());
print_host_banner(&ticket, display, &opts, clipboard_ok);
let result = accept_loop(&endpoint, display, &opts, cancel.clone()).await;
@@ -93,7 +94,12 @@ async fn handle_peer(
Ok(())
}
fn print_host_banner(ticket: &EndpointTicket, display: DisplayServer, opts: &HostOpts) {
fn print_host_banner(
ticket: &EndpointTicket,
display: DisplayServer,
opts: &HostOpts,
clipboard_ok: bool,
) {
eprintln!();
eprintln!("┌─ PixelPass · host ─────────────────────────────────────────");
eprintln!("│ display server : {display:?}");
@@ -101,7 +107,13 @@ fn print_host_banner(ticket: &EndpointTicket, display: DisplayServer, opts: &Hos
eprintln!("│ bitrate / fps : {} kbps @ {} fps", opts.bitrate, opts.framerate);
eprintln!("│ hw encode : {}", if opts.no_hwencode { "off" } else { "auto (VAAPI if available)" });
eprintln!("");
eprintln!("│ Share this ticket with your viewer:");
if clipboard_ok {
eprintln!("│ Your share code has been copied to your clipboard.");
eprintln!("│ Send it to your viewer. (If clipboard didn't work, the");
eprintln!("│ code is also shown below for manual copy.)");
} else {
eprintln!("│ Share this ticket with your viewer:");
}
eprintln!("");
eprintln!("│ pixelpass {ticket}");
eprintln!("");
@@ -111,6 +123,16 @@ fn print_host_banner(ticket: &EndpointTicket, display: DisplayServer, opts: &Hos
eprintln!();
}
fn copy_to_clipboard(text: &str) -> bool {
match arboard::Clipboard::new().and_then(|mut cb| cb.set_text(text.to_owned())) {
Ok(()) => true,
Err(e) => {
tracing::warn!("clipboard copy failed: {e}");
false
}
}
}
fn capture_summary(opts: &HostOpts) -> String {
let mut bits = vec![if opts.window { "window" } else { "fullscreen" }.to_string()];
if let Some(app) = &opts.app {