//! implements different clipboard types use std::{ io::{Read, Write}, process::{Command, Stdio}, }; pub trait Clipboard { fn copy(text: &str); fn paste() -> String; } macro_rules! c { ($p:ident $($args:ident)+) => { Command::new(stringify!($p)).args([$(stringify!($args),)+]) }; ($p:literal) => { Command::new($p) }; ($p:literal $($args:literal)+) => { Command::new($p).args([$($args,)+]) } } trait Eat { fn eat(&mut self) -> String; } impl Eat for Command { fn eat(&mut self) -> String { let mut s = String::new(); self.stdout(Stdio::piped()) .spawn() .expect("spawn ok") .stdout .take() .unwrap() .read_to_string(&mut s) .expect("read ok"); s } } trait Put { fn put(&mut self, s: impl AsRef<[u8]>); } impl Put for Command { fn put(&mut self, s: impl AsRef<[u8]>) { let mut ch = self.stdin(Stdio::piped()).spawn().expect("spawn ok"); ch.stdin .take() .unwrap() .write_all(s.as_ref()) .expect("write ok"); ch.wait().expect("proc ok"); } } #[cfg(target_os = "macos")] pub struct PbCopy {} #[cfg(target_os = "macos")] impl Clipboard for PbCopy { fn copy(text: &str) { c!(pbcopy w).put(text) } fn paste() -> String { c!(pbcopy r).eat() } } pub struct XClip {} impl Clipboard for XClip { fn copy(text: &str) { c!("xclip" "-selection" "c").put(text); } fn paste() -> String { c!("xclip" "-selection" "c" "-o") // xcclip is complainy .stderr(Stdio::null()) .stdout(Stdio::null()) .eat() } } pub struct XSel {} impl Clipboard for XSel { fn copy(text: &str) { c!("xsel" "-b" "-i").put(text); } fn paste() -> String { c!("xsel" "-b" "-o").eat() } } struct Wayland {} impl Clipboard for Wayland { fn copy(text: &str) { match text { "" => assert!( c!("wl-copy" "--clear").status().unwrap().success(), "wl-copy fail" ), s => c!("wl-copy").put(s), } } fn paste() -> String { c!("wl-paste" "-n").eat() } } struct Klipper {} impl Clipboard for Klipper { fn copy(text: &str) { c!("qdbus" "org.kde.klipper" "/klipper" "setClipboardContents").arg(text); } fn paste() -> String { let mut s = c!("qdbus" "org.kde.klipper" "/klipper" "getClipboardContents").eat(); assert!(s.ends_with('\n')); s.truncate(s.len() - 1); s } } #[cfg(target_family = "windows")] struct Windows {} #[cfg(target_family = "windows")] impl Clipboard for Windows { fn copy(text: &str) { clipboard_win::set_clipboard_string(text).expect("set clip ok") } fn paste() -> String { clipboard_win::get_clipboard_string().expect("get clip ok") } } struct Wsl {} impl Clipboard for Wsl { fn copy(text: &str) { c!("clip.exe").put(text); } fn paste() -> String { let mut s = c!("powershell.exe" "-noprofile" "-command" "Get-Clipboard").eat(); s.truncate(s.len() - 2); // \r\n s } } pub type Board = (for<'a> fn(&'a str), fn() -> String); fn get() -> Board { println!("{}", std::any::type_name::()); (T::copy, T::paste) } fn has(c: &str) -> bool { c!("which") .arg(c) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .expect("ok") .success() } fn wsl() -> bool { if let Ok(s) = std::fs::read_to_string("/proc/version") { if s.to_lowercase().contains("microsoft") { return true; } } false } pub fn provide() -> Board { #[cfg(target_family = "windows")] return get::(); #[cfg(target_os = "macos")] return get::(); if wsl() { return get::(); } assert!(std::env::var("DISPLAY").is_ok(), "no clipboard available"); if std::env::var("WAYLAND_DISPLAY").is_ok() && has("wl-copy") { get::() } else if has("xsel") { get::() } else if has("xclip") { get::() } else if has("klipper") && has("qdbus") { get::() } else { panic!("no clipboard available"); } } #[test] fn test() { macro_rules! test { ($clipboard:ty) => { <$clipboard>::copy("text"); assert_eq!(<$clipboard>::paste(), "text"); <$clipboard>::copy(""); }; } #[cfg(target_os = "macos")] test!(PbCopy); #[cfg(target_os = "linux")] test!(XClip); #[cfg(target_os = "linux")] test!(XSel); #[cfg(target_os = "linux")] if std::env::var("WAYLAND_DISPLAY").is_ok() { test!(Wayland); } #[cfg(target_os = "linux")] test!(Klipper); #[cfg(target_family = "windows")] test!(Windows); if wsl() { #[cfg(target_os = "linux")] test!(Wsl); } }