use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
fn paste_dir() -> PathBuf {
crate::config::CACHE_DIR.join("pastes")
}
const PNG_MAGIC: &[u8] = b"\x89PNG\r\n\x1a\n";
#[derive(Debug, PartialEq, Eq)]
pub enum NoImage {
Clipboard,
NoTool,
}
impl NoImage {
pub fn message(&self) -> String {
match self {
NoImage::Clipboard => "No image on the clipboard".to_string(),
NoImage::NoTool if over_ssh() => {
"The clipboard is on the machine you sshed from — F1 says how to get one here"
.to_string()
}
NoImage::NoTool => format!("No tool here can read an image clipboard — {}", HOW),
}
}
}
fn over_ssh() -> bool {
["SSH_CONNECTION", "SSH_TTY", "SSH_CLIENT"]
.iter()
.any(|var| std::env::var_os(var).is_some_and(|v| !v.is_empty()))
}
#[cfg(target_os = "macos")]
const HOW: &str = "install pngpaste, or use a build of macOS with osascript";
#[cfg(target_os = "windows")]
const HOW: &str = "powershell.exe was not found on PATH";
#[cfg(all(unix, not(target_os = "macos")))]
const HOW: &str = "install wl-clipboard or xclip (WSL uses powershell.exe)";
pub fn image_to_file() -> Result<PathBuf, NoImage> {
let dir = paste_dir();
if std::fs::create_dir_all(&dir).is_err() {
return Err(NoImage::NoTool);
}
let Ok(dest) = reserve(&dir) else {
return Err(NoImage::NoTool);
};
let mut ran_something = false;
for helper in HELPERS {
match helper.run(&dest) {
Attempt::Wrote => return Ok(dest),
Attempt::Empty => ran_something = true,
Attempt::Missing => {}
}
}
let _ = std::fs::remove_file(&dest);
match ran_something {
true => Err(NoImage::Clipboard),
false => Err(NoImage::NoTool),
}
}
pub fn png_from_paste(text: &str) -> Option<Vec<u8>> {
let trimmed = text.trim();
let body = match trimmed.strip_prefix("data:image/png;base64,") {
Some(rest) => rest,
None if trimmed.starts_with("iVBORw0KGgo") => trimmed,
None => return None,
};
let bytes = crate::util::b64_decode(body)?;
bytes.starts_with(PNG_MAGIC).then_some(bytes)
}
pub fn write_png(png: &[u8]) -> std::io::Result<PathBuf> {
let dir = paste_dir();
std::fs::create_dir_all(&dir)?;
let dest = reserve(&dir)?;
std::fs::write(&dest, png)?;
Ok(dest)
}
fn reserve(dir: &Path) -> std::io::Result<PathBuf> {
let stamp = chrono::Local::now().format("%Y%m%d-%H%M%S").to_string();
for n in 1..=99u32 {
let name = match n {
1 => format!("paste-{stamp}.png"),
n => format!("paste-{stamp}-{n}.png"),
};
let dest = dir.join(name);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&dest)
{
Ok(_) => return Ok(dest),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"too many images pasted in the same second",
))
}
enum Attempt {
Wrote,
Empty,
Missing,
}
struct Helper {
command: &'static str,
args: &'static [&'static str],
output: Output,
}
enum Output {
Stdout,
File,
}
const HELPERS: &[Helper] = &[
Helper {
command: "wl-paste",
args: &["--no-newline", "--type", "image/png"],
output: Output::Stdout,
},
Helper {
command: "xclip",
args: &["-selection", "clipboard", "-t", "image/png", "-o"],
output: Output::Stdout,
},
Helper {
command: "pngpaste",
args: &["-"],
output: Output::Stdout,
},
Helper {
command: "osascript",
args: &[
"-e",
"set f to open for access POSIX file \"{}\" with write permission",
"-e",
"try",
"-e",
"write (the clipboard as «class PNGf») to f",
"-e",
"end try",
"-e",
"close access f",
],
output: Output::File,
},
Helper {
command: "powershell.exe",
args: &[
"-NoProfile",
"-STA",
"-Command",
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; \
$i=[System.Windows.Forms.Clipboard]::GetImage(); \
if ($null -eq $i) { exit 1 }; \
$i.Save('{}',[System.Drawing.Imaging.ImageFormat]::Png)",
],
output: Output::File,
},
];
const WSL_POWERSHELL: &[&str] = &[
"/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe",
"/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe",
];
impl Helper {
fn programs(&self) -> Vec<&'static str> {
let mut out = vec![self.command];
if self.command == "powershell.exe" {
out.extend(
WSL_POWERSHELL
.iter()
.filter(|p| Path::new(p).exists())
.copied(),
);
}
out
}
fn run(&self, dest: &Path) -> Attempt {
let Some(path) = self.dest_for(dest) else {
return Attempt::Missing;
};
let args: Vec<String> = self
.args
.iter()
.map(|arg| arg.replace("{}", &path))
.collect();
let mut out = None;
for program in self.programs() {
let attempt = Command::new(program)
.args(&args)
.stdin(Stdio::null())
.stderr(Stdio::null())
.output();
if let Ok(done) = attempt {
out = Some(done);
break;
}
}
let Some(out) = out else {
return Attempt::Missing;
};
match self.output {
Output::Stdout => match out.stdout.starts_with(PNG_MAGIC) {
true => match std::fs::write(dest, &out.stdout) {
Ok(()) => Attempt::Wrote,
Err(_) => Attempt::Empty,
},
false => Attempt::Empty,
},
Output::File => match is_png(dest) {
true => Attempt::Wrote,
false => Attempt::Empty,
},
}
}
fn dest_for(&self, dest: &Path) -> Option<String> {
if self.command != "powershell.exe" || cfg!(windows) {
return Some(dest.display().to_string());
}
let out = Command::new("wslpath")
.arg("-w")
.arg(dest)
.stderr(Stdio::null())
.output()
.ok()?;
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!path.is_empty()).then_some(path)
}
}
fn is_png(path: &Path) -> bool {
let Ok(bytes) = std::fs::read(path) else {
return false;
};
bytes.starts_with(PNG_MAGIC)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_png_counts_as_an_image() {
let dir = tempfile::tempdir().expect("tempdir");
let good = dir.path().join("good.png");
std::fs::write(&good, PNG_MAGIC).expect("write");
assert!(is_png(&good));
let text = dir.path().join("text.png");
std::fs::write(&text, "<html>not an image</html>").expect("write");
assert!(!is_png(&text));
assert!(!is_png(&dir.path().join("missing.png")));
}
#[test]
#[ignore = "reads the machine's real clipboard"]
fn the_clipboard_image_becomes_a_png_on_disk() {
match image_to_file() {
Ok(path) => {
assert!(is_png(&path), "{} is not a PNG", path.display());
eprintln!(
"pasted {} bytes to {}",
std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0),
path.display()
);
}
Err(why) => panic!("nothing was pasted: {}", why.message()),
}
}
#[test]
fn a_second_paste_in_the_same_second_gets_its_own_name() {
let dir = tempfile::tempdir().expect("tempdir");
let first = reserve(dir.path()).expect("first");
let second = reserve(dir.path()).expect("second");
let third = reserve(dir.path()).expect("third");
assert_ne!(first, second);
assert_ne!(second, third);
for path in [&first, &second, &third] {
assert!(path.exists(), "{} was not claimed", path.display());
}
assert!(
second
.file_name()
.unwrap()
.to_string_lossy()
.contains("-2."),
"the second name does not say which it is: {}",
second.display()
);
}
#[test]
fn the_destination_is_substituted_into_every_argument() {
let script = HELPERS
.iter()
.find(|h| h.command == "osascript")
.expect("the AppleScript helper");
let dest = Path::new("/tmp/paste-1.png");
let path = script.dest_for(dest).expect("a path for a local helper");
assert_eq!(path, "/tmp/paste-1.png");
let filled: Vec<String> = script
.args
.iter()
.map(|arg| arg.replace("{}", &path))
.collect();
assert!(
filled.iter().any(|a| a.contains("/tmp/paste-1.png")),
"the path never reached the script: {filled:?}"
);
assert!(
!filled.iter().any(|a| a.contains("{}")),
"an argument kept its placeholder: {filled:?}"
);
}
}