feat: add clipboard history tracking

This commit is contained in:
ByteAtATime 2025-11-30 15:48:45 -08:00
parent bd703cbf36
commit 87ffb66766
No known key found for this signature in database
4 changed files with 216 additions and 2 deletions

76
Cargo.lock generated
View file

@ -189,6 +189,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@ -411,7 +412,7 @@ dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"nom 8.0.0",
"num-rational",
"v_frame",
]
@ -1430,6 +1431,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flare"
version = "0.1.0"
@ -2954,6 +2961,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@ -3109,6 +3122,16 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
@ -3671,6 +3694,16 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.45.0",
]
[[package]]
name = "owned_ttf_parser"
version = "0.25.1"
@ -3727,6 +3760,16 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "phf"
version = "0.13.1"
@ -5212,6 +5255,18 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c"
dependencies = [
"memchr",
"nom 7.1.3",
"once_cell",
"petgraph",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@ -6510,6 +6565,25 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 0.38.44",
"tempfile",
"thiserror 2.0.17",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.2"

View file

@ -20,7 +20,7 @@ phf = { version = "0.13.1", features = ["macros"] }
rmp-serde = "1.3.0"
dirs = "6.0.0"
webbrowser = "1.0.6"
arboard = "3.6.1"
arboard = { version = "3.6.1", features = ["wayland-data-control"] }
walkdir = "2.5.0"
rayon = "1.11.0"
which = "8.0.0"

135
src/clipboard_history.rs Normal file
View file

@ -0,0 +1,135 @@
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex, Once};
use std::thread;
use std::time::Duration;
const MAX_HISTORY_SIZE: usize = 100;
const POLL_INTERVAL_MS: u64 = 500;
const HISTORY_FILE: &str = "clipboard_history.json";
static CLIPBOARD_HISTORY: LazyLock<Mutex<VecDeque<ClipboardEntry>>> =
LazyLock::new(|| Mutex::new(VecDeque::new()));
static LAST_CONTENT: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new()));
static INIT: Once = Once::new();
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ClipboardEntry {
pub content: String,
pub timestamp: u64,
}
fn get_history_path() -> PathBuf {
if let Some(data_dir) = dirs::data_dir() {
return data_dir.join("flare").join(HISTORY_FILE);
}
if let Some(home_dir) = dirs::home_dir() {
return home_dir.join(".flare").join(HISTORY_FILE);
}
PathBuf::from(HISTORY_FILE)
}
fn load_history() -> VecDeque<ClipboardEntry> {
let path = get_history_path();
if let Ok(data) = fs::read_to_string(&path) {
if let Ok(entries) = serde_json::from_str::<Vec<ClipboardEntry>>(&data) {
return VecDeque::from(entries);
}
}
VecDeque::new()
}
fn save_history(history: &VecDeque<ClipboardEntry>) {
let path = get_history_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let entries: Vec<_> = history.iter().collect();
if let Ok(data) = serde_json::to_string(&entries) {
let _ = fs::write(&path, data);
}
}
fn add_entry(content: String) {
let mut history = CLIPBOARD_HISTORY.lock().unwrap();
if let Some(front) = history.front() {
if front.content == content {
return;
}
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let entry = ClipboardEntry { content, timestamp };
history.push_front(entry);
while history.len() > MAX_HISTORY_SIZE {
history.pop_back();
}
save_history(&history);
}
fn poll_clipboard() {
loop {
let result = arboard::Clipboard::new().and_then(|mut c| c.get_text());
if let Ok(text) = result {
if !text.is_empty() {
let mut last = LAST_CONTENT.lock().unwrap();
if *last != text {
*last = text.clone();
drop(last);
add_entry(text);
}
}
}
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
}
pub fn init() {
INIT.call_once(|| {
let loaded = load_history();
{
let mut history = CLIPBOARD_HISTORY.lock().unwrap();
*history = loaded;
}
if let Some(entry) = CLIPBOARD_HISTORY.lock().unwrap().front() {
let mut last = LAST_CONTENT.lock().unwrap();
*last = entry.content.clone();
}
thread::spawn(poll_clipboard);
});
}
pub fn get_history() -> Vec<ClipboardEntry> {
let history = CLIPBOARD_HISTORY.lock().unwrap();
history.iter().cloned().collect()
}
pub fn clear_history() {
let mut history = CLIPBOARD_HISTORY.lock().unwrap();
history.clear();
save_history(&history);
}
pub fn remove_entry(index: usize) {
let mut history = CLIPBOARD_HISTORY.lock().unwrap();
if index < history.len() {
history.remove(index);
save_history(&history);
}
}

View file

@ -1,5 +1,6 @@
mod apps;
mod clipboard;
mod clipboard_history;
mod components;
mod deep_link;
mod encryption;
@ -243,6 +244,8 @@ fn run_daemon() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Failed to register deep links: {}", e);
}
clipboard_history::init();
let flare_settings = preferences::FlareSettings::load();
let result = if flare_settings.use_layer_shell {
@ -291,6 +294,8 @@ fn run_daemon() -> Result<(), Box<dyn std::error::Error>> {
}
}
clipboard_history::init();
let result = iced::daemon(boot, update, daemon_view)
.subscription(subscription)
.title("Flare")