diff --git a/apps/nesdoctor/README.md b/apps/nesdoctor/README.md index c098e72c..37431e40 100644 --- a/apps/nesdoctor/README.md +++ b/apps/nesdoctor/README.md @@ -21,8 +21,16 @@ rather than a promise about our intentions: there is nothing to switch on later. The shareable line carries **no hostname, no IP, no username, no game titles and no file paths** — a size band rather than a size, and an hour histogram rather -than timestamps. The long version does include titles and paths, and it stays in -`nesdoctor.json` on your disk. +than timestamps. It is put on your clipboard at the end so pasting it is one +keystroke. + +The long version lands in `nesdoctor.json`: every check with its reason, the +full latency series, and your installed titles with sizes and launch times if +you said yes to Steam. **That file is considerably more useful to us than the +line** — it is what lets us size a game library and see which requirement +actually stops people — so do have a read through it and send it along if +nothing in there bothers you. Plain JSON, entirely optional, and the one-line +version is already plenty. Reading your Steam library needs an explicit yes, and the question is asked last, after you have seen what this program does. diff --git a/apps/nesdoctor/src/main.rs b/apps/nesdoctor/src/main.rs index 6f7b4b77..d47754f4 100644 --- a/apps/nesdoctor/src/main.rs +++ b/apps/nesdoctor/src/main.rs @@ -206,27 +206,62 @@ fn main() { return; } + let clip = report::to_clipboard(&line); + println!(); - println!("\x1b[1mThe line to share, if you want to\x1b[0m"); - println!( - "\x1b[2m No hostname, no IP, no username, no game titles, no paths. A size band\x1b[0m" - ); - println!("\x1b[2m rather than a size, and hours rather than dates. Read it yourself:\x1b[0m"); + println!("\x1b[1m─── Copy this ───────────────────────────────────────────────────\x1b[0m"); println!(); - println!(" {line}"); + println!("\x1b[1;97;44m {line} \x1b[0m"); println!(); - if wrote { - println!( - "\x1b[2mThe long version — which does include titles and paths — is in {} and\x1b[0m", - args.json.display() - ); - println!("\x1b[2mgoes nowhere unless you send it.\x1b[0m"); + match clip { + Some(tool) => println!( + "\x1b[32m ✓ Already on your clipboard\x1b[0m \x1b[2m(via {tool}) — just paste it.\x1b[0m" + ), + None => println!( + "\x1b[2m Select the line above to copy it. (Install wl-clipboard or xclip and\x1b[0m\n\x1b[2m this happens by itself next time.)\x1b[0m" + ), } println!(); println!( - "\x1b[2mIf you are willing: paste that line into the thread you got this from.\x1b[0m" + "\x1b[2m Every field is above: no hostname, no IP, no username, no game titles,\x1b[0m" ); - println!("\x1b[2mIt is the only way we learn what the machines on the other end are.\x1b[0m"); + println!("\x1b[2m no paths. A size band rather than a size, hours rather than dates.\x1b[0m"); + println!(); + println!("\x1b[1m → Paste it into the thread you got this from.\x1b[0m"); + println!( + "\x1b[2m It is the only way we find out what the machines on the other end are,\x1b[0m" + ); + println!("\x1b[2m and right now we genuinely have no idea.\x1b[0m"); + + if wrote { + println!(); + println!("\x1b[1mAnd if you feel like being properly helpful\x1b[0m"); + println!( + "\x1b[2m {} has the long version: every check with its reason, the full\x1b[0m", + args.json.display() + ); + println!( + "\x1b[2m latency series, and — if you said yes to Steam — your installed titles\x1b[0m" + ); + println!("\x1b[2m with their sizes and launch times.\x1b[0m"); + println!(); + println!( + "\x1b[2m That file is more useful to us than the line by a long way: it is what\x1b[0m" + ); + println!( + "\x1b[2m lets us size a game library properly and see which requirement actually\x1b[0m" + ); + println!( + "\x1b[2m stops people. Have a look through it — it is plain JSON — and send it\x1b[0m" + ); + println!( + "\x1b[2m along if nothing in there bothers you. Entirely optional, and the\x1b[0m" + ); + println!("\x1b[2m line above is already plenty.\x1b[0m"); + } + println!(); + println!("\x1b[2mThanks. Genuinely — this is the part we cannot do on our own.\x1b[0m"); + println!(); } fn banner() { diff --git a/apps/nesdoctor/src/report.rs b/apps/nesdoctor/src/report.rs index e53b39bc..0f754278 100644 --- a/apps/nesdoctor/src/report.rs +++ b/apps/nesdoctor/src/report.rs @@ -310,3 +310,44 @@ fn wrap(s: &str, width: usize) -> Vec { out.retain(|l| !l.is_empty()); out } + +/// Put the summary line on the clipboard, and say which tool did it. +/// +/// Selecting a long line out of a terminal is fiddly and it is the last step +/// before we learn anything, so it should not be work. Every one of these ships +/// with the desktop it belongs to; where none is present we simply say so and +/// the line is still on screen. +pub fn to_clipboard(line: &str) -> Option<&'static str> { + use std::io::Write; + use std::process::{Command, Stdio}; + + const TOOLS: [(&str, &[&str]); 5] = [ + ("wl-copy", &[]), // Wayland + ("xclip", &["-selection", "clipboard"]), // X11 + ("xsel", &["--clipboard", "--input"]), // X11, the other one + ("pbcopy", &[]), // macOS + ("clip", &[]), // Windows + ]; + + for (tool, args) in TOOLS { + let Ok(mut child) = Command::new(tool) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + else { + continue; + }; + let wrote = child + .stdin + .as_mut() + .is_some_and(|s| s.write_all(line.as_bytes()).is_ok()); + // Wait either way, so a failed tool is not left running. + let ok = child.wait().map(|s| s.success()).unwrap_or(false); + if wrote && ok { + return Some(tool); + } + } + None +}