Skip to main content

lean_ctx/core/
share.rs

1//! Frictionless share helpers: copy text to the OS clipboard and open files/URLs.
2//!
3//! Pure process orchestration — no third-party crates. Every function degrades
4//! gracefully (returns `false`) when no suitable tool exists, so callers can fall
5//! back to printing the text. Used by the Wrapped share flow (`gain --copy` / `--open`).
6
7use std::io::Write;
8use std::process::{Command, Stdio};
9
10/// Copies `text` to the system clipboard. Returns `true` on success.
11///
12/// Tries the platform-native tools in order; the first that accepts the text on
13/// stdin and exits 0 wins. Never panics — returns `false` when none are available
14/// so the caller can fall back to printing.
15pub fn copy_to_clipboard(text: &str) -> bool {
16    clipboard_commands()
17        .into_iter()
18        .any(|(bin, args)| pipe_to(bin, &args, text))
19}
20
21/// Opens `target` (a file path or URL) in the default handler. Returns `true` if
22/// the launcher process spawned successfully (not whether the GUI actually opened).
23pub fn open_in_browser(target: &str) -> bool {
24    let (bin, mut args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
25        ("open", vec![])
26    } else if cfg!(target_os = "windows") {
27        // `start` is a cmd builtin; the empty "" is the window-title placeholder.
28        ("cmd", vec!["/C", "start", ""])
29    } else {
30        ("xdg-open", vec![])
31    };
32    args.push(target);
33    Command::new(bin)
34        .args(&args)
35        .stdout(Stdio::null())
36        .stderr(Stdio::null())
37        .stdin(Stdio::null())
38        .spawn()
39        .is_ok()
40}
41
42/// Platform-ordered list of `(binary, args)` candidates that read clipboard text on stdin.
43fn clipboard_commands() -> Vec<(&'static str, Vec<&'static str>)> {
44    #[cfg(target_os = "macos")]
45    {
46        vec![("pbcopy", vec![])]
47    }
48    #[cfg(target_os = "windows")]
49    {
50        vec![("clip", vec![])]
51    }
52    #[cfg(all(unix, not(target_os = "macos")))]
53    {
54        vec![
55            ("wl-copy", vec![]),
56            ("xclip", vec!["-selection", "clipboard"]),
57            ("xsel", vec!["--clipboard", "--input"]),
58            ("clip.exe", vec![]), // WSL fallback
59        ]
60    }
61}
62
63/// Spawns `bin args`, writes `text` to its stdin, and reports whether it exited 0.
64fn pipe_to(bin: &str, args: &[&str], text: &str) -> bool {
65    let Ok(mut child) = Command::new(bin)
66        .args(args)
67        .stdin(Stdio::piped())
68        .stdout(Stdio::null())
69        .stderr(Stdio::null())
70        .spawn()
71    else {
72        return false;
73    };
74    // Scope the stdin handle so it is flushed and closed before we wait, otherwise
75    // tools that read to EOF (pbcopy, xclip) would block forever.
76    {
77        let Some(mut stdin) = child.stdin.take() else {
78            return false;
79        };
80        if stdin.write_all(text.as_bytes()).is_err() {
81            return false;
82        }
83    }
84    matches!(child.wait(), Ok(status) if status.success())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::clipboard_commands;
90
91    #[test]
92    fn clipboard_candidates_are_present_for_this_platform() {
93        // Every supported platform must offer at least one clipboard tool to try.
94        assert!(!clipboard_commands().is_empty());
95    }
96
97    #[test]
98    fn clipboard_candidate_binaries_are_non_empty() {
99        for (bin, _) in clipboard_commands() {
100            assert!(!bin.is_empty(), "clipboard binary name must not be empty");
101        }
102    }
103}