a simple clipboard for complicated times
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml12
-rw-r--r--LICENSE21
-rw-r--r--README.md8
-rw-r--r--src/lib.rs23
-rw-r--r--src/providers.rs231
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"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1fafc15
--- /dev/null
+++ b/LICENSE
@@ -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);
+ }
+}