Skip to main content

git_cli/
clipboard.rs

1use anyhow::Result;
2use nils_common::clipboard::{ClipboardOutcome, ClipboardPolicy, ClipboardTool, copy_best_effort};
3use std::env;
4
5const CLIPBOARD_TOOL_ORDER: [ClipboardTool; 4] = [
6    ClipboardTool::Pbcopy,
7    ClipboardTool::WlCopy,
8    ClipboardTool::Xclip,
9    ClipboardTool::Xsel,
10];
11
12pub fn set_clipboard_best_effort(text: &str) -> Result<()> {
13    if env::var("GIT_CLI_FIXTURE_CLIPBOARD_MODE").ok().as_deref() == Some("missing") {
14        eprintln!("⚠️  No clipboard tool found (requires pbcopy, xclip, or xsel)");
15        return Ok(());
16    }
17
18    let policy = ClipboardPolicy::new(&CLIPBOARD_TOOL_ORDER);
19    if matches!(
20        copy_best_effort(text, &policy),
21        ClipboardOutcome::SkippedNoTool | ClipboardOutcome::SkippedFailure
22    ) {
23        eprintln!("⚠️  No clipboard tool found (requires pbcopy, xclip, or xsel)");
24    }
25    Ok(())
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31    use nils_test_support::{EnvGuard, GlobalStateLock, StubBinDir, prepend_path};
32    use pretty_assertions::assert_eq;
33    use std::fs;
34    use tempfile::TempDir;
35
36    fn write_clipboard_stub(stubs: &StubBinDir, name: &str) {
37        stubs.write_exe(
38            name,
39            &format!(
40                r#"#!/bin/bash
41set -euo pipefail
42chosen="${{CLIPBOARD_TOOL_CHOSEN:?CLIPBOARD_TOOL_CHOSEN is required}}"
43payload="${{CLIPBOARD_PAYLOAD_OUT:?CLIPBOARD_PAYLOAD_OUT is required}}"
44printf "%s" "{name}" > "$chosen"
45/bin/cat > "$payload"
46"#
47            ),
48        );
49    }
50
51    #[test]
52    fn set_clipboard_best_effort_prefers_pbcopy_when_present() {
53        let lock = GlobalStateLock::new();
54
55        let stubs = StubBinDir::new();
56        let out_dir = TempDir::new().expect("tempdir");
57        let out_path = out_dir.path().join("pbcopy.out");
58
59        stubs.write_exe(
60            "pbcopy",
61            r#"#!/bin/bash
62set -euo pipefail
63out="${PB_COPY_OUT:?PB_COPY_OUT is required}"
64/bin/cat > "$out"
65"#,
66        );
67
68        let _path_guard: EnvGuard = prepend_path(&lock, stubs.path());
69        let out_path_str = out_path.to_string_lossy();
70        let _out_guard = EnvGuard::set(&lock, "PB_COPY_OUT", out_path_str.as_ref());
71
72        set_clipboard_best_effort("hello").expect("copy");
73        let out = fs::read_to_string(out_path).expect("read stub output");
74        assert_eq!(out, "hello");
75    }
76
77    #[test]
78    fn set_clipboard_best_effort_prefers_pbcopy_over_other_tools() {
79        let lock = GlobalStateLock::new();
80        let stubs = StubBinDir::new();
81        write_clipboard_stub(&stubs, "pbcopy");
82        write_clipboard_stub(&stubs, "wl-copy");
83        write_clipboard_stub(&stubs, "xclip");
84        write_clipboard_stub(&stubs, "xsel");
85
86        let out_dir = TempDir::new().expect("tempdir");
87        let chosen_path = out_dir.path().join("chosen.txt");
88        let payload_path = out_dir.path().join("payload.txt");
89
90        let _path_guard = EnvGuard::set(&lock, "PATH", &stubs.path_str());
91        let chosen_path_str = chosen_path.to_string_lossy();
92        let payload_path_str = payload_path.to_string_lossy();
93        let _chosen_guard = EnvGuard::set(&lock, "CLIPBOARD_TOOL_CHOSEN", chosen_path_str.as_ref());
94        let _payload_guard =
95            EnvGuard::set(&lock, "CLIPBOARD_PAYLOAD_OUT", payload_path_str.as_ref());
96
97        set_clipboard_best_effort("hello").expect("copy");
98        assert_eq!(
99            fs::read_to_string(chosen_path).expect("chosen"),
100            "pbcopy".to_string()
101        );
102        assert_eq!(fs::read_to_string(payload_path).expect("payload"), "hello");
103    }
104
105    #[test]
106    fn set_clipboard_best_effort_prefers_wl_copy_over_xclip_and_xsel() {
107        let lock = GlobalStateLock::new();
108        let stubs = StubBinDir::new();
109        write_clipboard_stub(&stubs, "wl-copy");
110        write_clipboard_stub(&stubs, "xclip");
111        write_clipboard_stub(&stubs, "xsel");
112
113        let out_dir = TempDir::new().expect("tempdir");
114        let chosen_path = out_dir.path().join("chosen.txt");
115        let payload_path = out_dir.path().join("payload.txt");
116
117        let _path_guard = EnvGuard::set(&lock, "PATH", &stubs.path_str());
118        let chosen_path_str = chosen_path.to_string_lossy();
119        let payload_path_str = payload_path.to_string_lossy();
120        let _chosen_guard = EnvGuard::set(&lock, "CLIPBOARD_TOOL_CHOSEN", chosen_path_str.as_ref());
121        let _payload_guard =
122            EnvGuard::set(&lock, "CLIPBOARD_PAYLOAD_OUT", payload_path_str.as_ref());
123
124        set_clipboard_best_effort("hello").expect("copy");
125        assert_eq!(
126            fs::read_to_string(chosen_path).expect("chosen"),
127            "wl-copy".to_string()
128        );
129        assert_eq!(fs::read_to_string(payload_path).expect("payload"), "hello");
130    }
131
132    #[test]
133    fn set_clipboard_best_effort_prefers_xclip_over_xsel() {
134        let lock = GlobalStateLock::new();
135        let stubs = StubBinDir::new();
136        write_clipboard_stub(&stubs, "xclip");
137        write_clipboard_stub(&stubs, "xsel");
138
139        let out_dir = TempDir::new().expect("tempdir");
140        let chosen_path = out_dir.path().join("chosen.txt");
141        let payload_path = out_dir.path().join("payload.txt");
142
143        let _path_guard = EnvGuard::set(&lock, "PATH", &stubs.path_str());
144        let chosen_path_str = chosen_path.to_string_lossy();
145        let payload_path_str = payload_path.to_string_lossy();
146        let _chosen_guard = EnvGuard::set(&lock, "CLIPBOARD_TOOL_CHOSEN", chosen_path_str.as_ref());
147        let _payload_guard =
148            EnvGuard::set(&lock, "CLIPBOARD_PAYLOAD_OUT", payload_path_str.as_ref());
149
150        set_clipboard_best_effort("hello").expect("copy");
151        assert_eq!(
152            fs::read_to_string(chosen_path).expect("chosen"),
153            "xclip".to_string()
154        );
155        assert_eq!(fs::read_to_string(payload_path).expect("payload"), "hello");
156    }
157
158    #[test]
159    fn set_clipboard_best_effort_uses_xsel_when_present() {
160        let lock = GlobalStateLock::new();
161
162        let stubs = StubBinDir::new();
163        let out_dir = TempDir::new().expect("tempdir");
164        let out_path = out_dir.path().join("xsel.out");
165
166        stubs.write_exe(
167            "xsel",
168            r#"#!/bin/bash
169set -euo pipefail
170out="${XSEL_OUT:?XSEL_OUT is required}"
171/bin/cat > "$out"
172"#,
173        );
174
175        let _path_guard = EnvGuard::set(&lock, "PATH", &stubs.path_str());
176        let out_path_str = out_path.to_string_lossy();
177        let _out_guard = EnvGuard::set(&lock, "XSEL_OUT", out_path_str.as_ref());
178
179        set_clipboard_best_effort("hello").expect("copy");
180        let out = fs::read_to_string(out_path).expect("read stub output");
181        assert_eq!(out, "hello");
182    }
183
184    #[test]
185    fn set_clipboard_best_effort_falls_back_when_pbcopy_fails() {
186        let lock = GlobalStateLock::new();
187
188        let stubs = StubBinDir::new();
189        let out_dir = TempDir::new().expect("tempdir");
190        let out_path = out_dir.path().join("wl-copy.out");
191
192        stubs.write_exe(
193            "pbcopy",
194            r#"#!/bin/bash
195exit 1
196"#,
197        );
198        stubs.write_exe(
199            "wl-copy",
200            r#"#!/bin/bash
201set -euo pipefail
202out="${WL_COPY_OUT:?WL_COPY_OUT is required}"
203/bin/cat > "$out"
204"#,
205        );
206
207        let _path_guard = EnvGuard::set(&lock, "PATH", &stubs.path_str());
208        let out_path_str = out_path.to_string_lossy();
209        let _out_guard = EnvGuard::set(&lock, "WL_COPY_OUT", out_path_str.as_ref());
210
211        set_clipboard_best_effort("hello").expect("copy");
212        let out = fs::read_to_string(out_path).expect("read stub output");
213        assert_eq!(out, "hello");
214    }
215
216    #[test]
217    fn set_clipboard_best_effort_missing_mode_short_circuits_with_ok() {
218        let lock = GlobalStateLock::new();
219        let _mode = EnvGuard::set(&lock, "GIT_CLI_FIXTURE_CLIPBOARD_MODE", "missing");
220        set_clipboard_best_effort("hello").expect("missing mode should still succeed");
221    }
222}