#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ClipboardKinds {
pub image: bool,
pub text: bool,
}
impl ClipboardKinds {
pub(crate) fn is_image_only(self) -> bool {
self.image && !self.text
}
}
pub(crate) fn clipboard_info_fingerprint(info: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
info.split_whitespace()
.collect::<Vec<_>>()
.hash(&mut hasher);
hasher.finish()
}
const IMAGE_CLASS_MARKERS: &[&str] = &[
"PNGf", "picture", "TIFF", "JPEG", "GIF", "BMP", "jp2", "AVIF", "8BPS", "TPIC", "PICT",
];
const TEXT_CLASS_MARKERS: &[&str] = &["string", "utf8", "ut16", "utxt", "Unicode text"];
pub(crate) fn clipboard_info_has_image(info: &str) -> bool {
IMAGE_CLASS_MARKERS.iter().any(|m| info.contains(m))
}
pub(crate) fn clipboard_info_has_text(info: &str) -> bool {
TEXT_CLASS_MARKERS.iter().any(|m| info.contains(m))
}
#[cfg_attr(any(target_os = "macos", windows), allow(dead_code))]
pub(crate) fn kinds_from_mime_list(list: &str) -> ClipboardKinds {
let mut kinds = ClipboardKinds {
image: false,
text: false,
};
for line in list.lines() {
let line = line.trim();
if line.starts_with("image/") {
kinds.image = true;
} else if line.starts_with("text/") || line == "UTF8_STRING" || line == "STRING" {
kinds.text = true;
}
}
kinds
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) fn kinds_from_clipboard_info(info: &str) -> ClipboardKinds {
ClipboardKinds {
image: clipboard_info_has_image(info),
text: clipboard_info_has_text(info),
}
}
pub(crate) fn read_clipboard_kinds() -> Option<(ClipboardKinds, String)> {
#[cfg(target_os = "macos")]
{
let out = run_probe("osascript", &["-e", "clipboard info"])?;
let kinds = kinds_from_clipboard_info(&out);
Some((kinds, out))
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let out = run_probe("wl-paste", &["--list-types"])
.or_else(|| run_probe("xclip", &["-selection", "clipboard", "-t", "TARGETS", "-o"]))?;
let kinds = kinds_from_mime_list(&out);
Some((kinds, out))
}
#[cfg(windows)]
{
let out = run_probe(
"powershell.exe",
&[
"-NoProfile",
"-NonInteractive",
"-Command",
"Add-Type -AssemblyName System.Windows.Forms; \
$i = [System.Windows.Forms.Clipboard]::ContainsImage(); \
$t = [System.Windows.Forms.Clipboard]::ContainsText(); \
Write-Output \"image=$i text=$t\"",
],
)?;
let kinds = kinds_from_windows_probe(&out);
Some((kinds, out))
}
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn kinds_from_windows_probe(out: &str) -> ClipboardKinds {
let flag = |key: &str| {
out.split_whitespace()
.any(|tok| tok.eq_ignore_ascii_case(&format!("{key}=True")))
};
ClipboardKinds {
image: flag("image"),
text: flag("text"),
}
}
fn run_probe(program: &str, args: &[&str]) -> Option<String> {
use std::process::{Command, Stdio};
let mut cmd = Command::new(program);
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000);
}
let out = cmd.output().ok()?;
if !out.status.success() {
tracing::debug!(program, "clipboard probe failed");
return None;
}
let text = String::from_utf8_lossy(&out.stdout).to_string();
(!text.trim().is_empty()).then_some(text)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClipboardRoute<'a> {
Text(&'a str),
Image,
}
pub(crate) fn route_for_clipboard_text(text: Option<&str>) -> ClipboardRoute<'_> {
match text {
Some(t) if !t.trim().is_empty() => ClipboardRoute::Text(t),
_ => ClipboardRoute::Image,
}
}