Compare commits
2 Commits
4d0a381f12
...
27ede7fc64
| Author | SHA1 | Date | |
|---|---|---|---|
| 27ede7fc64 | |||
| 55e33bf497 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
||||
/rklipd/target
|
||||
/rklipd/*.json
|
||||
/rklipd/clipboard
|
||||
/rklipdtmp
|
||||
|
||||
1839
rklipd/Cargo.lock
generated
Normal file
1839
rklipd/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,3 +4,12 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
arboard = "3.6.1"
|
||||
image = "0.25.9"
|
||||
clipboard-master = "4.0.0"
|
||||
uuid = {version = "1.22.0", features = ["v4", "serde"]}
|
||||
rusqlite = "0.38.0"
|
||||
|
||||
[features]
|
||||
x11 = []
|
||||
wayland = []
|
||||
|
||||
68
rklipd/src/clipboard.rs
Normal file
68
rklipd/src/clipboard.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use arboard::{Clipboard, ImageData};
|
||||
use image::{ExtendedColorType, ImageEncoder, codecs::png::PngEncoder};
|
||||
use std::error::Error;
|
||||
use std::time::SystemTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||
|
||||
use clipboard_master::{CallbackResult, ClipboardHandler};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct Handler {
|
||||
pub clipboard_tx: Sender<()>,
|
||||
}
|
||||
|
||||
impl ClipboardHandler for Handler {
|
||||
fn on_clipboard_change(&mut self) -> CallbackResult {
|
||||
if let Err(e) = self.clipboard_tx.send(()) {
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
CallbackResult::Next
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ImageDataExt {
|
||||
fn to_png(&self) -> Result<Vec<u8>, 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)
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
let id = Uuid::new_v4();
|
||||
Some(ClipboardData::Image(Image {
|
||||
bytes: Some(image.to_png()?),
|
||||
id,
|
||||
}))
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
};
|
||||
|
||||
let Some(clipboard_data) = clipboard_data_opt else {
|
||||
return Err("Clipboard empty".into());
|
||||
};
|
||||
|
||||
Ok(ClipboardEntry {
|
||||
content: clipboard_data,
|
||||
timestamp: SystemTime::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
114
rklipd/src/database.rs
Normal file
114
rklipd/src/database.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||
use rusqlite::Connection;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
dir_path: String,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn init(dir_path: &str) -> Result<Self, 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 db_path = format!("{}/clipboard.db", dir_path);
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
conn,
|
||||
dir_path: dir_path.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn append(&self, entry: ClipboardEntry) -> Result<(), Box<dyn Error>> {
|
||||
let timestamp_millis = entry.timestamp.duration_since(UNIX_EPOCH)?.as_millis() as i64;
|
||||
|
||||
let (entry_type, content) = match &entry.content {
|
||||
ClipboardData::Text(text) => ("text", text.clone()),
|
||||
ClipboardData::Image(img) => {
|
||||
if let Some(bytes) = &img.bytes {
|
||||
let img_path = format!("{}/images/{}.png", self.dir_path, img.id);
|
||||
fs::write(&img_path, bytes)?;
|
||||
}
|
||||
("image", img.id.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT INTO history (type, content, timestamp) VALUES (?1, ?2, ?3)",
|
||||
(entry_type, content, timestamp_millis),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_history(&self) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT type, content, timestamp FROM history ORDER BY timestamp ASC")?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let ty: String = row.get(0)?;
|
||||
let content: String = row.get(1)?;
|
||||
let timestamp: i64 = row.get(2)?;
|
||||
Ok((ty, content, timestamp))
|
||||
})?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
let (ty, content, timestamp) = row?;
|
||||
|
||||
let timestamp = UNIX_EPOCH + Duration::from_millis(timestamp as u64);
|
||||
let data = if ty == "text" {
|
||||
ClipboardData::Text(content)
|
||||
} else {
|
||||
let id = Uuid::parse_str(&content)?;
|
||||
ClipboardData::Image(Image { id, bytes: None })
|
||||
};
|
||||
|
||||
// let data = if ty == "text" {
|
||||
// ClipboardData::Text(content)
|
||||
// } else {
|
||||
// let id = Uuid::parse_str(&content)?;
|
||||
// let img_path = format!("{}/images/{}.png", self.dir_path, id);
|
||||
// let bytes = fs::read(&img_path).unwrap_or_default();
|
||||
// ClipboardData::Image(Image {
|
||||
// bytes: Some(bytes),
|
||||
// id,
|
||||
// })
|
||||
// };
|
||||
|
||||
entries.push(ClipboardEntry {
|
||||
content: data,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,22 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use arboard::Clipboard;
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
mod clipboard;
|
||||
mod database;
|
||||
mod models;
|
||||
mod monitor;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let clipboard = Clipboard::new()?;
|
||||
|
||||
let dir_path = "clipboard";
|
||||
|
||||
let db = Database::init(dir_path)?;
|
||||
|
||||
println!("{:#?}", db.read_history());
|
||||
|
||||
monitor::start(db, clipboard)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
31
rklipd/src/models.rs
Normal file
31
rklipd/src/models.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use std::time::SystemTime;
|
||||
use std::{fs, io};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardEntry {
|
||||
pub content: ClipboardData,
|
||||
pub timestamp: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardData {
|
||||
Text(String),
|
||||
Image(Image),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Image {
|
||||
pub bytes: Option<Vec<u8>>,
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn load_bytes(&self, dir_path: &str) -> io::Result<Vec<u8>> {
|
||||
if let Some(b) = &self.bytes {
|
||||
return Ok(b.clone());
|
||||
}
|
||||
let img_path = format!("{}/images/{}.png", dir_path, self.id);
|
||||
fs::read(img_path)
|
||||
}
|
||||
}
|
||||
32
rklipd/src/monitor.rs
Normal file
32
rklipd/src/monitor.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::{error::Error, sync::mpsc::channel};
|
||||
|
||||
use arboard::Clipboard;
|
||||
use clipboard_master::Master;
|
||||
|
||||
use crate::clipboard::Handler;
|
||||
use crate::database::Database;
|
||||
use crate::models::ClipboardEntry;
|
||||
|
||||
pub fn start(db: Database, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut master = Master::new(Handler { clipboard_tx: tx })?;
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = master.run() {
|
||||
eprintln!("Clipboard monitor error : {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
for _ in rx {
|
||||
print!("Clipboard changed!");
|
||||
if let Ok(entry) = ClipboardEntry::new(&mut clipboard) {
|
||||
if let Err(e) = db.append(entry) {
|
||||
eprintln!("SQLite writing error: {}", e);
|
||||
} else {
|
||||
println!("SQLite edited!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user