Initial commit

This commit is contained in:
Breadway 2026-07-02 20:59:21 +08:00
commit 69bc67e29a
16 changed files with 2376 additions and 0 deletions

11
breadclipd/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "breadclipd"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "breadclipd"
path = "src/main.rs"
[dependencies]
breadclip-core = { path = "../breadclip-core" }

125
breadclipd/src/main.rs Normal file
View file

@ -0,0 +1,125 @@
use breadclip_core::{sha256_hex, HistoryDb};
use std::{env, fs, path::PathBuf, process::Command, thread, time::Duration};
fn get_available_types() -> Vec<String> {
Command::new("wl-paste")
.args(["--list-types"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.lines().map(str::trim).map(String::from).collect())
.unwrap_or_default()
}
fn get_clipboard_text() -> Option<String> {
let output = Command::new("wl-paste")
.args(["--no-newline", "--type", "text/plain"])
.output()
.ok()?;
if !output.status.success() || output.stdout.is_empty() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.filter(|s| !s.trim().is_empty())
}
fn get_clipboard_image() -> Option<Vec<u8>> {
let output = Command::new("wl-paste")
.args(["--type", "image/png"])
.output()
.ok()?;
if !output.status.success() || output.stdout.is_empty() {
return None;
}
Some(output.stdout)
}
fn lock_file() -> PathBuf {
env::var("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
.join("breadclipd.lock")
}
// Returns false if another instance is already running.
fn acquire_lock() -> bool {
let path = lock_file();
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(pid) = content.trim().parse::<u32>() {
let alive = fs::read_to_string(format!("/proc/{}/comm", pid))
.map(|s| s.trim() == "breadclipd")
.unwrap_or(false);
if alive {
eprintln!("breadclipd: already running (pid {})", pid);
return false;
}
}
}
let _ = fs::write(&path, std::process::id().to_string());
true
}
fn main() {
if !acquire_lock() {
std::process::exit(1);
}
// Wait briefly for WAYLAND_DISPLAY — common when started early in the session
let mut retries = 0;
while env::var("WAYLAND_DISPLAY").is_err() && retries < 20 {
thread::sleep(Duration::from_millis(500));
retries += 1;
}
if env::var("WAYLAND_DISPLAY").is_err() {
eprintln!("breadclipd: WAYLAND_DISPLAY not set after waiting, exiting");
let _ = fs::remove_file(lock_file());
std::process::exit(1);
}
let db = match HistoryDb::open() {
Ok(db) => db,
Err(e) => {
eprintln!("breadclipd: failed to open database: {e}");
let _ = fs::remove_file(lock_file());
std::process::exit(1);
}
};
eprintln!("breadclipd: started (pid {})", std::process::id());
let mut last_text_hash: Option<String> = None;
let mut last_image_hash: Option<String> = None;
loop {
let types = get_available_types();
let has_image = types.iter().any(|t| t == "image/png" || t == "image/jpeg");
let has_text = types.iter().any(|t| t.starts_with("text/"));
if has_image && !has_text {
if let Some(bytes) = get_clipboard_image() {
let hash = sha256_hex(&bytes);
if Some(&hash) != last_image_hash.as_ref() {
last_image_hash = Some(hash);
last_text_hash = None;
if let Err(e) = db.insert_image(&bytes) {
eprintln!("breadclipd: insert image: {e}");
}
}
}
} else if has_text {
if let Some(text) = get_clipboard_text() {
let hash = sha256_hex(text.as_bytes());
if Some(&hash) != last_text_hash.as_ref() {
last_text_hash = Some(hash);
if let Err(e) = db.insert_text(&text) {
eprintln!("breadclipd: insert text: {e}");
}
}
}
}
thread::sleep(Duration::from_millis(500));
}
}