use std::env;
use std::ffi::OsString;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use which::which;
use crate::{error, Result};
#[derive(Clone, Debug)]
pub struct Clipboard {
content: String,
}
#[derive(Clone, Debug)]
pub struct Selected {
display: DisplayKind,
content: String,
}
#[derive(Clone, Debug)]
pub struct Binary {
bin: OsString,
selected: Selected,
program: ClipboardBinProgram,
}
#[derive(Clone, Debug)]
pub struct EscapeSequence {
selected: Selected,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
enum DisplayKind {
X11,
Wayland,
Wsl,
SshTty,
Unknown,
#[cfg(target_is = "macos")]
MacOs,
#[cfg(target_os = "windows")]
Windows,
}
fn select_display() -> DisplayKind {
#[cfg(target_os = "windows")]
return DisplayKind::Windows;
#[cfg(target_is = "macos")]
return DisplayKind::MacOs;
if is_wsl() {
DisplayKind::Wsl
} else if is_wayland() {
DisplayKind::Wayland
} else if is_x11() {
DisplayKind::X11
} else if is_ssh_tty() {
DisplayKind::SshTty
} else {
DisplayKind::Unknown
}
}
#[cfg(target_family = "unix")]
fn is_wsl() -> bool {
env::var("WSL_DISTRO_NAME").is_ok()
|| env::var("WT_SESSION").is_ok()
|| env::var("WSL_INTEROP").is_ok()
}
#[cfg(all(
target_family = "unix",
not(all(target_os = "macos", target_os = "ios", target_os = "android"))
))]
fn is_wayland() -> bool {
let mut score = 0;
match env::var("XDG_SESSION_TYPE").ok().as_deref() {
Some("wayland") => score += 1,
Some(_) | None => (),
}
if env::var("WAYLAND_DISPLAY").is_ok() {
score += 1;
}
score > 0
}
#[cfg(all(
target_family = "unix",
not(all(target_os = "macos", target_os = "ios", target_os = "android"))
))]
fn is_x11() -> bool {
let mut score = 0;
match env::var("XDG_SESSION_TYPE").ok().as_deref() {
Some("x11") => score += 1,
Some(_) | None => (),
}
if env::var("DISPLAY").is_ok() {
score += 1;
}
score > 0
}
#[cfg(all(
target_family = "unix",
not(all(target_os = "macos", target_os = "ios", target_os = "android"))
))]
fn is_ssh_tty() -> bool {
let tty = env::var("XDG_SESSION_TYPE").as_deref() == Ok("tty");
let ssh = env::var("SSH_CLIENT").is_ok();
tty && ssh
}
impl Clipboard {
#[must_use]
pub fn new(content: &str) -> Self {
Self {
content: content.to_owned(),
}
}
pub fn try_into_selected(self) -> Result<Selected> {
match select_display() {
DisplayKind::Unknown => Err(error::Clipboard::UnsupportedPlatform.into()),
valid => Ok(Selected {
display: valid,
content: self.content,
}),
}
}
}
pub trait Provider {
fn set_contents(&self) -> Result<()>;
}
impl Provider for Binary {
fn set_contents(&self) -> Result<()> {
let mut command = Command::new(&self.bin);
match self.program {
ClipboardBinProgram::Xclip => {
command.arg("-sel").arg("clip");
}
ClipboardBinProgram::Xsel => {
command.arg("--clipboard");
}
ClipboardBinProgram::WlCopy | ClipboardBinProgram::ClipExe => (),
};
let mut process = command
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
process
.stdin
.as_mut()
.expect("to access stdin")
.write_all(self.selected.content.as_bytes())?;
let _status = process.wait()?;
Ok(())
}
}
impl Provider for EscapeSequence {
fn set_contents(&self) -> Result<()> {
print!("\x1B]52;c;{}\x07", base64::encode(&self.selected.content));
Ok(())
}
}
impl Selected {
#[must_use]
pub fn into_provider(self) -> Box<dyn Provider> {
match self.try_into_bin() {
Ok(bin_clipboard) => {
return Box::new(bin_clipboard);
}
Err(err) => {
println!("{:?}", err);
}
}
Box::new(EscapeSequence { selected: self })
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
enum ClipboardBinProgram {
Xclip,
Xsel,
ClipExe,
WlCopy,
#[cfg(all(target_os = "macos", target_os = "ios"))]
PbCopy,
}
#[cfg(all(
target_family = "unix",
not(all(target_os = "macos", target_os = "ios", target_os = "android"))
))]
impl Selected {
fn try_into_bin(&self) -> Result<Binary> {
let (bin, program) = match self.display {
DisplayKind::X11 => {
let mut binaries = [
(which("xclip"), ClipboardBinProgram::Xclip),
(which("xsel"), ClipboardBinProgram::Xsel),
]
.into_iter();
let (bin, program) = binaries
.find(|(bin, _)| bin.is_ok())
.ok_or(error::Clipboard::MissingBinary)?;
(bin.unwrap(), program)
}
DisplayKind::Wayland => {
let bin = which("wl-copy")?;
let program = ClipboardBinProgram::WlCopy;
(bin, program)
}
DisplayKind::SshTty => {
let _xauth = which("xauth")?;
env::var("DISPLAY").map_err(|_| error::Clipboard::DisplayNotSet)?;
let mut binaries = [
(which("xclip"), ClipboardBinProgram::Xclip),
(which("xsel"), ClipboardBinProgram::Xsel),
]
.into_iter();
let (bin, program) = binaries
.find(|(bin, _)| bin.is_ok())
.ok_or(error::Clipboard::MissingBinary)?;
(bin.unwrap(), program)
}
DisplayKind::Wsl => {
let bin = PathBuf::from("clip.exe");
let program = ClipboardBinProgram::ClipExe;
(bin, program)
}
DisplayKind::Unknown => panic!("clipboard feature not supported"),
};
Ok(Binary {
bin: bin.as_os_str().to_owned(),
selected: self.clone(),
program,
})
}
}
#[cfg(all(target_os = "macos", target_os = "ios"))]
impl Selected {
fn try_into_bin(&self) -> Result<Binary> {
let bin = match self.display {
DisplayKind::MacOs => which("pbcopy")
.ok()
.map(|t| t.as_os_str().to_owned())
.ok_or(ErrorKind::MissingClipboardBinary)?,
DisplayKind::Unknown => panic!("clipboard feature not supported"),
};
let program = ClipboardBinProgram::PbCopy;
Ok(Binary {
bin,
program,
selected: self.clone(),
})
}
}
#[cfg(target_os = "windows")]
impl Selected {
fn try_into_bin(&self) -> Result<Binary> {
Err(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
pub fn clipboard_test_selection_order() {
env::remove_var("DISPLAY");
env::remove_var("WSL_DISTRO_NAME");
env::remove_var("WAYLAND_DISPLAY");
env::remove_var("SSH_CLIENT");
env::remove_var("WT_SESSION");
env::remove_var("WSL_INTEROP");
env::set_var("DISPLAY", "localhost");
let clip1 = Clipboard::new("foo").try_into_selected().unwrap();
assert_eq!(clip1.display, DisplayKind::X11);
env::set_var("WAYLAND_DISPLAY", "wayland");
let clip2 = Clipboard::new("bar").try_into_selected().unwrap();
assert_eq!(clip2.display, DisplayKind::Wayland);
env::set_var("WSL_DISTRO_NAME", "hanna_montana_linux");
let clip3 = Clipboard::new("baz").try_into_selected().unwrap();
assert_eq!(clip3.display, DisplayKind::Wsl);
}
}