use std::io::{self, ErrorKind, Write as _};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{SystemTime, UNIX_EPOCH};
use rig_core::message::ImageMediaType;
use crate::process::Launcher;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Use {
ReadText,
ReadImage,
WriteText,
}
fn programs(what: Use) -> Vec<(&'static str, &'static [&'static str])> {
let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some();
let x11 = std::env::var_os("DISPLAY").is_some();
let mac = cfg!(target_os = "macos");
let all: [(bool, Use, &'static str, &'static [&'static str]); 9] = [
(wayland, Use::ReadText, "wl-paste", &["--no-newline"]),
(
wayland,
Use::ReadImage,
"wl-paste",
&["--no-newline", "--type", "image/png"],
),
(wayland, Use::WriteText, "wl-copy", &[]),
(
x11,
Use::ReadText,
"xclip",
&["-selection", "clipboard", "-out"],
),
(
x11,
Use::ReadImage,
"xclip",
&["-selection", "clipboard", "-target", "image/png", "-out"],
),
(
x11,
Use::WriteText,
"xclip",
&["-selection", "clipboard", "-in"],
),
(mac, Use::ReadText, "pbpaste", &[]),
(mac, Use::ReadImage, "pngpaste", &["-"]),
(mac, Use::WriteText, "pbcopy", &[]),
];
all.into_iter()
.filter(|(here, kind, _, _)| *here && *kind == what)
.map(|(_, _, program, args)| (program, args))
.collect()
}
fn none_of(programs: &[(&str, &[&str])], what: &str) -> io::Error {
if programs.is_empty() {
return io::Error::new(
ErrorKind::Unsupported,
"no clipboard here: it needs wl-paste and wl-copy, xclip, or pbpaste and pbcopy",
);
}
let names: Vec<&str> = programs.iter().map(|(program, _)| *program).collect();
io::Error::new(
ErrorKind::NotFound,
format!(
"no {what} in the clipboard (read with {})",
names.join(", ")
),
)
}
fn output_of(
launcher: &Launcher,
what: Use,
) -> (
Vec<(&'static str, &'static [&'static str])>,
Option<Vec<u8>>,
) {
let programs = programs(what);
let printed = programs.iter().find_map(|(program, args)| {
let output = launcher
.command(program)
.args(*args)
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
output.status.success().then_some(output.stdout)
});
(programs, printed)
}
pub fn read_text(launcher: &Launcher) -> io::Result<String> {
match output_of(launcher, Use::ReadText) {
(_, Some(bytes)) => Ok(String::from_utf8_lossy(&bytes).into_owned()),
(programs, None) => Err(none_of(&programs, "text")),
}
}
pub fn read_image(launcher: &Launcher, into: &Path) -> io::Result<Option<PathBuf>> {
let (programs, printed) = output_of(launcher, Use::ReadImage);
if programs.is_empty() {
return Err(none_of(&programs, "image"));
}
let Some((bytes, kind)) =
printed.and_then(|bytes| ImageMediaType::sniff(&bytes).map(|kind| (bytes, kind)))
else {
return Ok(None);
};
std::fs::create_dir_all(into)?;
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_millis())
.unwrap_or_default();
let path = into.join(format!("pasted-{stamp}.{}", kind.extension()));
std::fs::write(&path, bytes)?;
Ok(Some(path))
}
pub fn write_text(launcher: &Launcher, text: &str) -> io::Result<()> {
let programs = programs(Use::WriteText);
let mut failed = none_of(&[], "");
for (program, args) in programs {
let written = launcher
.command(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes())?;
}
let status = child.wait()?;
match status.success() {
true => Ok(()),
false => Err(io::Error::other(format!("{program} failed: {status}"))),
}
});
match written {
Ok(()) => return Ok(()),
Err(error) => failed = error,
}
}
Err(failed)
}