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}