1use std::io::{self, ErrorKind, Write as _};
8use std::path::{Path, PathBuf};
9use std::process::Stdio;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use rig_core::message::ImageMediaType;
13
14use crate::process::Launcher;
15
16#[derive(Clone, Copy, PartialEq, Eq)]
18enum Use {
19 ReadText,
21 ReadImage,
23 WriteText,
25}
26
27fn programs(what: Use) -> Vec<(&'static str, &'static [&'static str])> {
29 let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some();
30 let x11 = std::env::var_os("DISPLAY").is_some();
31 let mac = cfg!(target_os = "macos");
32 let all: [(bool, Use, &'static str, &'static [&'static str]); 9] = [
33 (wayland, Use::ReadText, "wl-paste", &["--no-newline"]),
34 (
35 wayland,
36 Use::ReadImage,
37 "wl-paste",
38 &["--no-newline", "--type", "image/png"],
39 ),
40 (wayland, Use::WriteText, "wl-copy", &[]),
41 (
42 x11,
43 Use::ReadText,
44 "xclip",
45 &["-selection", "clipboard", "-out"],
46 ),
47 (
48 x11,
49 Use::ReadImage,
50 "xclip",
51 &["-selection", "clipboard", "-target", "image/png", "-out"],
52 ),
53 (
54 x11,
55 Use::WriteText,
56 "xclip",
57 &["-selection", "clipboard", "-in"],
58 ),
59 (mac, Use::ReadText, "pbpaste", &[]),
60 (mac, Use::ReadImage, "pngpaste", &["-"]),
61 (mac, Use::WriteText, "pbcopy", &[]),
62 ];
63 all.into_iter()
64 .filter(|(here, kind, _, _)| *here && *kind == what)
65 .map(|(_, _, program, args)| (program, args))
66 .collect()
67}
68
69fn none_of(programs: &[(&str, &[&str])], what: &str) -> io::Error {
71 if programs.is_empty() {
72 return io::Error::new(
73 ErrorKind::Unsupported,
74 "no clipboard here: it needs wl-paste and wl-copy, xclip, or pbpaste and pbcopy",
75 );
76 }
77 let names: Vec<&str> = programs.iter().map(|(program, _)| *program).collect();
78 io::Error::new(
79 ErrorKind::NotFound,
80 format!(
81 "no {what} in the clipboard (read with {})",
82 names.join(", ")
83 ),
84 )
85}
86
87fn output_of(
89 launcher: &Launcher,
90 what: Use,
91) -> (
92 Vec<(&'static str, &'static [&'static str])>,
93 Option<Vec<u8>>,
94) {
95 let programs = programs(what);
96 let printed = programs.iter().find_map(|(program, args)| {
97 let output = launcher
98 .command(program)
99 .args(*args)
100 .stdin(Stdio::null())
101 .stderr(Stdio::null())
102 .output()
103 .ok()?;
104 output.status.success().then_some(output.stdout)
105 });
106 (programs, printed)
107}
108
109pub fn read_text(launcher: &Launcher) -> io::Result<String> {
117 match output_of(launcher, Use::ReadText) {
118 (_, Some(bytes)) => Ok(String::from_utf8_lossy(&bytes).into_owned()),
119 (programs, None) => Err(none_of(&programs, "text")),
120 }
121}
122
123pub fn read_image(launcher: &Launcher, into: &Path) -> io::Result<Option<PathBuf>> {
132 let (programs, printed) = output_of(launcher, Use::ReadImage);
133 if programs.is_empty() {
134 return Err(none_of(&programs, "image"));
135 }
136 let Some((bytes, kind)) =
137 printed.and_then(|bytes| ImageMediaType::sniff(&bytes).map(|kind| (bytes, kind)))
138 else {
139 return Ok(None);
140 };
141 std::fs::create_dir_all(into)?;
142 let stamp = SystemTime::now()
143 .duration_since(UNIX_EPOCH)
144 .map(|elapsed| elapsed.as_millis())
145 .unwrap_or_default();
146 let path = into.join(format!("pasted-{stamp}.{}", kind.extension()));
147 std::fs::write(&path, bytes)?;
148 Ok(Some(path))
149}
150
151pub fn write_text(launcher: &Launcher, text: &str) -> io::Result<()> {
158 let programs = programs(Use::WriteText);
159 let mut failed = none_of(&[], "");
160 for (program, args) in programs {
161 let written = launcher
162 .command(program)
163 .args(args)
164 .stdin(Stdio::piped())
165 .stdout(Stdio::null())
166 .stderr(Stdio::null())
167 .spawn()
168 .and_then(|mut child| {
169 if let Some(mut stdin) = child.stdin.take() {
170 stdin.write_all(text.as_bytes())?;
171 }
172 let status = child.wait()?;
173 match status.success() {
174 true => Ok(()),
175 false => Err(io::Error::other(format!("{program} failed: {status}"))),
176 }
177 });
178 match written {
179 Ok(()) => return Ok(()),
180 Err(error) => failed = error,
181 }
182 }
183 Err(failed)
184}