diff --git a/src/daemon.rs b/src/daemon.rs index e4e769e..7425894 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -17,51 +17,16 @@ use wl_clipboard_rs::paste::Seat; pub(crate) fn handle_peer( mut peer: UnixStream, next_id: Arc, + last_copied: Arc, items: Arc>>, ) -> eyre::Result<()> { let mut request = [0; 1]; - peer.read_exact(&mut request) - .wrap_err("failed to read message type")?; + let Ok(()) = peer.read_exact(&mut request) else { + return Ok(()); + }; match request[0] { super::MESSAGE_STORE => { - let mime_types = - wl_clipboard_rs::paste::get_mime_types(ClipboardType::Regular, Seat::Unspecified) - .wrap_err("getting mime types")?; - - let time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - - let Some(mime) = ["text/plain", "image/png"] - .iter() - .find(|mime| mime_types.contains(**mime)) - else { - eprintln!("WARN: No supported mime type found. Found mime types: {mime_types:?}"); - return Ok(()); - }; - - let (data_readear, _) = wl_clipboard_rs::paste::get_contents( - ClipboardType::Regular, - Seat::Unspecified, - MimeType::Specific(mime), - ) - .wrap_err("getting contents")?; - - let mut data_reader = data_readear.take(MAX_ENTRY_SIZE); - - let mut data = Vec::new(); - data_reader - .read_to_end(&mut data) - .wrap_err("reading content data")?; - - items.lock().unwrap().push(Entry { - id: next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), - mime: mime.to_string(), - data, - created_time: u64::try_from(time.as_millis()).unwrap(), - }); - - println!("INFO Successfully stored clipboard value of mime type {mime}"); + handle_store(next_id, last_copied, &items).wrap_err("handling store message")?; } super::MESSAGE_READ => { let items = items.lock().unwrap(); @@ -70,35 +35,126 @@ pub(crate) fn handle_peer( .wrap_err("writing items to socket")?; } super::MESSAGE_COPY => { - let mut id = [0; 8]; - peer.read_exact(&mut id).wrap_err("failed to read id")?; - let id = u64::from_le_bytes(id); - - let items = items.lock().unwrap(); - - let Some(idx) = items.iter().position(|item| item.id == id) else { - return Ok(()); - }; - - let entry = items[idx].clone(); - - // select - let mut opts = wl_clipboard_rs::copy::Options::new(); - opts.clipboard(wl_clipboard_rs::copy::ClipboardType::Regular); - let result = wl_clipboard_rs::copy::copy( - opts, - wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()), - wl_clipboard_rs::copy::MimeType::Specific(entry.mime), - ); - if let Err(err) = result { - println!("WARNING: Copy failed: {err:?}"); - } + handle_copy(peer, last_copied, items).wrap_err("handling copy message")?; } _ => {} }; Ok(()) } +fn handle_copy( + mut peer: UnixStream, + last_copied: Arc, + items: Arc>>, +) -> Result<(), eyre::Error> { + let mut id = [0; 8]; + peer.read_exact(&mut id).wrap_err("failed to read id")?; + let id = u64::from_le_bytes(id); + let mut items = items.lock().unwrap(); + let Some(idx) = items.iter().position(|item| item.id == id) else { + return Ok(()); + }; + let entry = items.remove(idx); + items.push(entry.clone()); + let mut opts = wl_clipboard_rs::copy::Options::new(); + opts.clipboard(wl_clipboard_rs::copy::ClipboardType::Regular) + .seat(wl_clipboard_rs::copy::Seat::All); + let result = wl_clipboard_rs::copy::copy( + opts, + wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()), + wl_clipboard_rs::copy::MimeType::Specific(entry.mime), + ); + last_copied.store(entry.id, std::sync::atomic::Ordering::Relaxed); + if let Err(err) = result { + println!("WARNING: Copy failed: {err:?}"); + } + Ok(()) +} + +fn handle_store( + next_id: Arc, + last_copied: Arc, + items: &Arc>>, +) -> Result<(), eyre::Error> { + let time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + + let mime_types = + wl_clipboard_rs::paste::get_mime_types(ClipboardType::Regular, Seat::Unspecified) + .wrap_err("getting mime types")?; + + let Some(mime) = ["text/plain", "image/png"] + .iter() + .find(|mime| mime_types.contains(**mime)) + else { + eprintln!("WARN: No supported mime type found. Found mime types: {mime_types:?}"); + return Ok(()); + }; + + let (data_readear, _) = wl_clipboard_rs::paste::get_contents( + ClipboardType::Regular, + Seat::Unspecified, + MimeType::Specific(mime), + ) + .wrap_err("getting contents")?; + + let mut data_reader = data_readear.take(MAX_ENTRY_SIZE); + let mut data = Vec::new(); + data_reader + .read_to_end(&mut data) + .wrap_err("reading content data")?; + + let new_entry = Entry { + id: next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), + mime: mime.to_string(), + data, + created_time: u64::try_from(time.as_millis()).unwrap(), + }; + + let mut items = items.lock().unwrap(); + if items + .last() + .is_some_and(|last| last.mime == new_entry.mime && last.data == new_entry.data) + { + println!("INFO: Skipping store of new item because it is identical to last one"); + return Ok(()); + } + + let last_copied = last_copied.load(std::sync::atomic::Ordering::Relaxed); + if let Some(item) = items.iter().find(|item| item.id == last_copied) + && item.mime == new_entry.mime + && item.data == new_entry.data + { + println!("INFO: Skipping store of new item because the copy came from us"); + return Ok(()); + } + + items.push(new_entry); + + let mut running_total = 0; + let mut cutoff = None; + for (idx, item) in items.iter().enumerate().rev() { + running_total += item.data.len() + std::mem::size_of::(); + if running_total > crate::MAX_HISTORY_BYTE_SIZE { + cutoff = Some(idx); + } + } + if let Some(cutoff) = cutoff { + println!( + "INFO: Dropping old {} items because limit of {} bytes was reached for the history", + cutoff + 1, + crate::MAX_HISTORY_BYTE_SIZE + ); + items.splice(0..=cutoff, []); + } + + println!( + "INFO: Successfully stored clipboard value of mime type {mime} (new history size {running_total})" + ); + Ok(()) +} + pub fn main(socket_path: &PathBuf) -> eyre::Result<()> { let _ = std::fs::remove_file(&socket_path); // lol let socket = UnixListener::bind(&socket_path) @@ -106,6 +162,8 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> { let next_id = Arc::new(AtomicU64::new(0)); let items = Arc::new(Mutex::new(Vec::::new())); + // for deduplication because the event stream will tell us that we just copied something :) + let last_copied = Arc::new(AtomicU64::new(u64::MAX)); println!("INFO: Listening on {}", socket_path.display()); @@ -114,8 +172,9 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> { Ok(peer) => { let next_id = next_id.clone(); let items = items.clone(); + let last_copied = last_copied.clone(); std::thread::spawn(move || { - let result = handle_peer(peer, next_id, items); + let result = handle_peer(peer, next_id, last_copied, items); if let Err(err) = result { eprintln!("ERROR: Error handling peer: {err:?}"); } diff --git a/src/display.rs b/src/display.rs index d80fa46..87ebf1d 100644 --- a/src/display.rs +++ b/src/display.rs @@ -5,7 +5,12 @@ use crate::MESSAGE_READ; use super::MESSAGE_COPY; -use std::{io::{BufReader, Write}, os::unix::net::UnixStream, path::Path, time::Instant}; +use std::{ + io::{BufReader, Write}, + os::unix::net::UnixStream, + path::Path, + time::Instant, +}; use super::Entry; @@ -44,14 +49,21 @@ impl eframe::App for App { .show_inside(ui, |ui| { ui.heading("History"); + ui.add_space(10.0); + for (idx, item) in self.items.iter().enumerate() { - let mut frame = egui::Frame::new(); + let mut frame = egui::Frame::new().inner_margin(3.0); if self.selected_idx == idx { frame = frame.stroke(egui::Stroke::new(1.0, egui::Color32::PURPLE)); } frame.show(ui, |ui| match item.mime.as_str() { "text/plain" => { - ui.label(str::from_utf8(&item.data).unwrap_or("")); + let mut full = + str::from_utf8(&item.data).unwrap_or(""); + if full.len() > 1000 { + full = &full[..1000]; + } + ui.label(full); } "image/png" => { ui.label(""); @@ -60,6 +72,8 @@ impl eframe::App for App { ui.label(""); } }); + + ui.separator(); } }); @@ -69,6 +83,8 @@ impl eframe::App for App { return; }; + ui.add_space(10.0); + match item.mime.as_str() { "text/plain" => { ui.label(str::from_utf8(&item.data).unwrap_or("")); @@ -101,13 +117,15 @@ pub fn main(socket_path: &Path) -> eyre::Result<()> { println!("INFO: Reading clipboard history from socket"); let start = Instant::now(); - let items = + let mut items: Vec = ciborium::from_reader(BufReader::new(socket)).wrap_err("reading items from socket")?; println!( "INFO: Read clipboard history from socket in {:?}", start.elapsed() ); + items.reverse(); + // heh. good design. let socket = UnixStream::connect(&socket_path).wrap_err_with(|| { format!( diff --git a/src/main.rs b/src/main.rs index c7dfe08..b57f71f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ -mod display; mod daemon; +mod display; -use std::{io::Write, os::unix::net::UnixStream}; use eyre::{Context, OptionExt, bail}; +use std::{io::Write, os::unix::net::UnixStream}; -const MAX_ENTRY_SIZE: u64 = 100_000_000; +const MAX_ENTRY_SIZE: u64 = 50_000_000; +const MAX_HISTORY_BYTE_SIZE: usize = 100_000_000; #[derive(Clone, serde::Deserialize, serde::Serialize)] struct Entry { @@ -38,6 +39,11 @@ fn main() -> eyre::Result<()> { ) })?; + if std::env::args().any(|arg| arg == "--wl-copy") { + std::io::copy(&mut std::io::stdin(), &mut std::io::empty()) + .wrap_err("reading stdin in --wl-copy mode")?; + } + socket .write_all(&[MESSAGE_STORE]) .wrap_err("writing request type")?;