limit_cli/
clipboard_text.rs1use base64::Engine;
9use std::io::Write;
10
11#[cfg(not(target_os = "android"))]
18pub fn copy_text_to_clipboard(text: &str) -> Result<(), String> {
19 if std::env::var_os("SSH_CONNECTION").is_some() || std::env::var_os("SSH_TTY").is_some() {
21 return copy_via_osc52(text);
22 }
23
24 let error = match arboard::Clipboard::new() {
26 Ok(mut clipboard) => match clipboard.set_text(text.to_string()) {
27 Ok(()) => return Ok(()),
28 Err(err) => format!("clipboard unavailable: {err}"),
29 },
30 Err(err) => format!("clipboard unavailable: {err}"),
31 };
32
33 #[cfg(target_os = "linux")]
35 let error = if is_probably_wsl() {
36 match copy_via_wsl_clipboard(text) {
37 Ok(()) => return Ok(()),
38 Err(wsl_err) => format!("{error}; WSL fallback failed: {wsl_err}"),
39 }
40 } else {
41 error
42 };
43
44 Err(error)
45}
46
47#[cfg(not(target_os = "android"))]
49fn copy_via_osc52(text: &str) -> Result<(), String> {
50 let sequence = osc52_sequence(text, std::env::var_os("TMUX").is_some());
51
52 #[cfg(unix)]
54 {
55 use std::fs::OpenOptions;
56
57 let mut tty = OpenOptions::new()
58 .write(true)
59 .open("/dev/tty")
60 .map_err(|e| format!("failed to open /dev/tty: {e}"))?;
61 tty.write_all(sequence.as_bytes())
62 .map_err(|e| format!("failed to write OSC 52: {e}"))?;
63 tty.flush()
64 .map_err(|e| format!("failed to flush OSC 52: {e}"))?;
65 }
66
67 #[cfg(windows)]
69 {
70 use std::io::stdout;
71 stdout()
72 .write_all(sequence.as_bytes())
73 .map_err(|e| format!("failed to write OSC 52: {e}"))?;
74 stdout()
75 .flush()
76 .map_err(|e| format!("failed to flush OSC 52: {e}"))?;
77 }
78
79 Ok(())
80}
81
82fn osc52_sequence(text: &str, tmux: bool) -> String {
84 let payload = base64::engine::general_purpose::STANDARD.encode(text);
85 if tmux {
86 format!("\x1bPtmux;\x1b\x1b]52;c;{payload}\x07\x1b\\")
88 } else {
89 format!("\x1b]52;c;{payload}\x07")
91 }
92}
93
94#[cfg(all(not(target_os = "android"), target_os = "linux"))]
96fn copy_via_wsl_clipboard(text: &str) -> Result<(), String> {
97 use std::process::{Command, Stdio};
98
99 let mut child = Command::new("powershell.exe")
100 .stdin(Stdio::piped())
101 .stdout(Stdio::null())
102 .stderr(Stdio::piped())
103 .args([
104 "-NoProfile",
105 "-Command",
106 "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; \
107 $ErrorActionPreference = 'Stop'; \
108 $text = [Console]::In.ReadToEnd(); \
109 Set-Clipboard -Value $text",
110 ])
111 .spawn()
112 .map_err(|e| format!("failed to spawn powershell.exe: {e}"))?;
113
114 let Some(mut stdin) = child.stdin.take() else {
115 let _ = child.kill();
116 return Err("failed to open powershell.exe stdin".to_string());
117 };
118
119 stdin
120 .write_all(text.as_bytes())
121 .map_err(|e| format!("failed to write to powershell.exe: {e}"))?;
122
123 drop(stdin);
124
125 let output = child
126 .wait_with_output()
127 .map_err(|e| format!("failed to wait for powershell.exe: {e}"))?;
128
129 if output.status.success() {
130 Ok(())
131 } else {
132 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
133 Err(if stderr.is_empty() {
134 format!("powershell.exe exited with status {}", output.status)
135 } else {
136 format!("powershell.exe failed: {stderr}")
137 })
138 }
139}
140
141#[cfg(target_os = "linux")]
143pub(crate) fn is_probably_wsl() -> bool {
144 if let Ok(version) = std::fs::read_to_string("/proc/version") {
146 let version_lower = version.to_lowercase();
147 if version_lower.contains("microsoft") || version_lower.contains("wsl") {
148 return true;
149 }
150 }
151
152 std::env::var_os("WSL_DISTRO_NAME").is_some() || std::env::var_os("WSL_INTEROP").is_some()
154}
155
156#[cfg(all(test, not(target_os = "android")))]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn osc52_sequence_encodes_text_for_terminal_clipboard() {
162 assert_eq!(osc52_sequence("hello", false), "\u{1b}]52;c;aGVsbG8=\u{7}");
163 }
164
165 #[test]
166 fn osc52_sequence_wraps_tmux_passthrough() {
167 assert_eq!(
168 osc52_sequence("hello", true),
169 "\u{1b}Ptmux;\u{1b}\u{1b}]52;c;aGVsbG8=\u{7}\u{1b}\\"
170 );
171 }
172}