a simple clipboard for complicated times
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.toml | 12 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | src/lib.rs | 23 | ||||
| -rw-r--r-- | src/providers.rs | 231 |
6 files changed, 297 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..84776c6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clipp" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/bend-n/clipp" +description = "clipboard, simple." +keywords = ["clipboard"] +categories = ["os"] +license = "MIT" + +[target.'cfg(target_family = "windows")'.dependencies] +clipboard-win = "4.5.0" @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 bendn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b21b742 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# clipp + +a simple clipboard for complicated times + +```rust +clipp::copy("hello world"); +assert_eq!(clipp::paste(), "hello world"); +```
\ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0c2c8d2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,23 @@ +//! simple possibly cross platform clipboard crate +//! +//! ``` +//! clipp::copy("wow such clipboard"); +//! assert_eq!(clipp::paste(), "wow such clipboard"); +//! ``` +#![warn(clippy::pedantic)] +#![forbid(unsafe_code)] +mod providers; + +use std::{fmt::Display, sync::OnceLock}; + +static CLIP: OnceLock<providers::Board> = OnceLock::new(); + +/// Copy text to the clipboard. +pub fn copy(text: impl Display) { + CLIP.get_or_init(providers::provide).0(&format!("{text}")); +} + +/// Paste text from the clipboard. +pub fn paste() -> String { + CLIP.get_or_init(providers::provide).1() +} diff --git a/src/providers.rs b/src/providers.rs new file mode 100644 index 0000000..5c9c586 --- /dev/null +++ b/src/providers.rs @@ -0,0 +1,231 @@ +//! 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" "-p" "--clear").status().unwrap().success(), + "wl-copy fail" + ), + s => c!("wl-copy" "-p").put(s), + } + } + + fn paste() -> String { + c!("wl-paste" "-n" "-p").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<T: Clipboard>() -> Board { + (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::<Windows>(); + #[cfg(target_os = "macos")] + return get::<PbCopy>(); + + if wsl() { + return get::<Wsl>(); + } + assert!(std::env::var("DISPLAY").is_ok(), "no clipboard available"); + if std::env::var("WAYLAND_DISPLAY").is_ok() && has("wl-copy") { + get::<Wayland>() + } else if has("xsel") { + get::<XSel>() + } else if has("xclip") { + get::<XClip>() + } else if has("klipper") && has("qdbus") { + get::<Klipper>() + } 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); + } +} |