This commit is contained in:
2026-03-07 21:59:56 +01:00
parent ff3f8f94ef
commit 4d0a381f12
5 changed files with 389 additions and 2050 deletions

1785
rklipd/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,15 +4,3 @@ version = "0.1.0"
edition = "2024"
[dependencies]
arboard = "3.6.1"
image = "0.25.9"
serde = {version = "1.0.228", features = ["derive"]}
serde_json = "1.0.149"
serde_millis = "0.1.1"
base64 = "0.22.1"
clipboard-master = "4.0.0"
rand = "0.10.0"
[features]
x11 = []
wayland = []

View File

@ -1,202 +0,0 @@
use arboard::{Clipboard, ImageData};
use image::codecs::png::PngEncoder;
use image::{EncodableLayout, ExtendedColorType, ImageEncoder, ImageReader, math};
use rand::{RngExt, distr::Alphanumeric};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::Cursor;
use std::path::Path;
use std::result::Result;
use std::sync::mpsc::Sender;
use std::{error::Error, time::SystemTime};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ClipboardEntry {
pub content: ClipboardData,
#[serde(with = "serde_millis")]
pub timestamp: SystemTime,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ClipboardData {
Text(String),
// #[serde(with = "base64_vec")]
Image(Vec<u8>),
}
// X11
#[cfg(feature = "x11")]
use clipboard_master::{CallbackResult, ClipboardHandler};
#[cfg(feature = "x11")]
pub struct Handler {
pub clipboard_tx: Sender<()>,
}
#[cfg(feature = "x11")]
impl ClipboardHandler for Handler {
fn on_clipboard_change(&mut self) -> CallbackResult {
if let Err(e) = self.clipboard_tx.send(()) {
eprintln!("{}", e);
}
CallbackResult::Next
}
}
// X11 end
// Wayland
// Wayland end
// mod base64_vec {
// use base64::{Engine as _, engine::general_purpose::STANDARD};
// use serde::{Deserialize, Deserializer, Serializer};
// pub fn serialize<S: Serializer>(v: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error> {
// let base64_str = STANDARD.encode(v);
// serializer.serialize_str(&base64_str)
// }
// pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
// let base64_str = String::deserialize(deserializer)?;
// match STANDARD.decode(base64_str) {
// Ok(bytes) => Ok(bytes),
// Err(error_base64) => {
// let error_serde = serde::de::Error::custom(error_base64);
// Err(error_serde)
// }
// }
// }
// }
pub trait ImageDataExt {
fn to_png(&self) -> Result<Vec<u8>, Box<dyn Error>>;
fn save_img(bytes: Vec<u8>, dir_path: &str) -> Result<(), Box<dyn Error>>;
}
impl ImageDataExt for ImageData<'_> {
fn to_png(&self) -> Result<Vec<u8>, Box<dyn Error>> {
let mut buffer = Vec::new();
let encoder = PngEncoder::new(&mut buffer);
encoder.write_image(
&self.bytes,
self.width as u32,
self.height as u32,
ExtendedColorType::Rgba8,
)?;
Ok(buffer)
}
fn save_img(bytes: Vec<u8>, image_dir_path: &str) -> Result<(), Box<dyn Error>> {
let img = ImageReader::new(Cursor::new(&bytes))
.with_guessed_format()?
.decode()?;
let name: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
img.save(format!("{}/{}.png", image_dir_path, name))?;
Ok(())
}
}
impl ClipboardData {
pub fn is_text(&self) -> bool {
match self {
ClipboardData::Text(_) => true,
ClipboardData::Image(_) => false,
}
}
pub fn is_image(&self) -> bool {
!self.is_text()
}
}
impl ClipboardEntry {
pub fn new(clipboard: &mut Clipboard) -> Result<ClipboardEntry, Box<dyn Error>> {
let clipboard_data_opt: Option<ClipboardData> = match clipboard.get_text() {
Ok(text) => Some(ClipboardData::Text(text)),
Err(_) => match clipboard.get_image() {
Ok(image) => Some(ClipboardData::Image(image.to_png()?)),
Err(_) => None,
},
};
let Some(clipboard_data) = clipboard_data_opt else {
return Err("Clipboard empty".into());
};
Ok(ClipboardEntry {
content: clipboard_data,
timestamp: SystemTime::now(),
})
}
pub fn init(dir_path: &str) -> Result<(), Box<dyn Error>> {
if !std::fs::exists(dir_path)? {
fs::create_dir(dir_path)?;
} else {
println!("{:?} dir already exists.", { dir_path });
}
let image_path = format!("{}/images", dir_path);
if !std::fs::exists(&image_path)? {
fs::create_dir(&image_path)?;
} else {
println!("{:?} dir already exists.", { image_path });
}
let file_path = Path::new(dir_path).join("clipboard.json");
ClipboardEntry::new_json(file_path.to_str().unwrap())?;
Ok(())
}
pub fn new_json(file_path: &str) -> Result<(), Box<dyn Error>> {
if Path::new(file_path).exists() {
Err("File already exists.".into())
} else {
File::create(file_path)?;
Ok(())
}
}
pub fn append_clipboard_history(
&self,
file_path: &str,
image_dir_path: &str,
) -> Result<(), Box<dyn Error>> {
let mut entries: Vec<ClipboardEntry> = if Path::new(file_path).exists() {
let data = fs::read_to_string(file_path)?;
if data.trim().is_empty() {
Vec::new()
} else {
serde_json::from_str(&data)?
}
} else {
Vec::new()
};
match &self.content {
ClipboardData::Text(_) => {
entries.push(self.clone());
let json = serde_json::to_string_pretty(&entries)?;
fs::write(file_path, json)?;
}
ClipboardData::Image(image) => {
<ImageData<'_> as ImageDataExt>::save_img(image.to_vec(), image_dir_path)?;
}
}
Ok(())
}
pub fn read_text_history_json(file_path: &str) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
if !Path::new(file_path).exists() {
return Ok(Vec::new());
}
let data = fs::read_to_string(file_path)?;
if data.trim().is_empty() {
return Ok(Vec::new());
}
let entries: Vec<ClipboardEntry> = serde_json::from_str(&data)?;
Ok(entries)
}
}

View File

@ -1,46 +1,3 @@
use arboard::Clipboard;
use clipboard_master::Master;
use rklipd::{ClipboardEntry, Handler};
use std::error::Error;
use std::sync::mpsc::channel;
// X11
// #[cfg(feature = "x11")]
fn main() -> Result<(), Box<dyn Error>> {
let mut clipboard = Clipboard::new()?;
let dir_path = "clipboard";
let file_path = "clipboard/clipboard.json";
let image_dir_path = "clipboard/images/";
ClipboardEntry::init(dir_path).unwrap_or(());
let (tx, rx) = channel();
let mut master = Master::new(Handler { clipboard_tx: tx })?;
// let shutdown = master.shutdown_channel();
std::thread::spawn(move || {
if let Err(e) = master.run() {
eprintln!("Clipboard monitor error : {}", e);
}
});
println!("Monitoring clipboard X11...");
for _ in rx {
println!("Clipboard changed!");
if let Ok(entry) = ClipboardEntry::new(&mut clipboard) {
if let Err(e) = entry.append_clipboard_history(file_path, image_dir_path) {
eprintln!("JSON writing error: {}", e);
} else {
println!("JSON edited!");
}
}
}
Ok(())
}
// X11
#[cfg(not(feature = "x11"))]
fn main() -> Result<(), Box<dyn Error>> {
Ok(())
fn main() {
println!("Hello, world!");
}