This commit is contained in:
nora 2025-09-07 01:52:57 +02:00
parent 1164ed1e3e
commit fbce93b927
3 changed files with 154 additions and 71 deletions

View file

@ -17,51 +17,16 @@ use wl_clipboard_rs::paste::Seat;
pub(crate) fn handle_peer( pub(crate) fn handle_peer(
mut peer: UnixStream, mut peer: UnixStream,
next_id: Arc<AtomicU64>, next_id: Arc<AtomicU64>,
last_copied: Arc<AtomicU64>,
items: Arc<Mutex<Vec<Entry>>>, items: Arc<Mutex<Vec<Entry>>>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let mut request = [0; 1]; let mut request = [0; 1];
peer.read_exact(&mut request) let Ok(()) = peer.read_exact(&mut request) else {
.wrap_err("failed to read message type")?; return Ok(());
};
match request[0] { match request[0] {
super::MESSAGE_STORE => { super::MESSAGE_STORE => {
let mime_types = handle_store(next_id, last_copied, &items).wrap_err("handling store message")?;
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}");
} }
super::MESSAGE_READ => { super::MESSAGE_READ => {
let items = items.lock().unwrap(); let items = items.lock().unwrap();
@ -70,35 +35,126 @@ pub(crate) fn handle_peer(
.wrap_err("writing items to socket")?; .wrap_err("writing items to socket")?;
} }
super::MESSAGE_COPY => { super::MESSAGE_COPY => {
let mut id = [0; 8]; handle_copy(peer, last_copied, items).wrap_err("handling copy message")?;
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:?}");
}
} }
_ => {} _ => {}
}; };
Ok(()) Ok(())
} }
fn handle_copy(
mut peer: UnixStream,
last_copied: Arc<AtomicU64>,
items: Arc<Mutex<Vec<Entry>>>,
) -> 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<AtomicU64>,
last_copied: Arc<AtomicU64>,
items: &Arc<Mutex<Vec<Entry>>>,
) -> 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::<Entry>();
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<()> { pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
let _ = std::fs::remove_file(&socket_path); // lol let _ = std::fs::remove_file(&socket_path); // lol
let socket = UnixListener::bind(&socket_path) 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 next_id = Arc::new(AtomicU64::new(0));
let items = Arc::new(Mutex::new(Vec::<Entry>::new())); let items = Arc::new(Mutex::new(Vec::<Entry>::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()); println!("INFO: Listening on {}", socket_path.display());
@ -114,8 +172,9 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
Ok(peer) => { Ok(peer) => {
let next_id = next_id.clone(); let next_id = next_id.clone();
let items = items.clone(); let items = items.clone();
let last_copied = last_copied.clone();
std::thread::spawn(move || { 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 { if let Err(err) = result {
eprintln!("ERROR: Error handling peer: {err:?}"); eprintln!("ERROR: Error handling peer: {err:?}");
} }

View file

@ -5,7 +5,12 @@ use crate::MESSAGE_READ;
use super::MESSAGE_COPY; 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; use super::Entry;
@ -44,14 +49,21 @@ impl eframe::App for App {
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.heading("History"); ui.heading("History");
ui.add_space(10.0);
for (idx, item) in self.items.iter().enumerate() { 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 { if self.selected_idx == idx {
frame = frame.stroke(egui::Stroke::new(1.0, egui::Color32::PURPLE)); frame = frame.stroke(egui::Stroke::new(1.0, egui::Color32::PURPLE));
} }
frame.show(ui, |ui| match item.mime.as_str() { frame.show(ui, |ui| match item.mime.as_str() {
"text/plain" => { "text/plain" => {
ui.label(str::from_utf8(&item.data).unwrap_or("<invalid UTF-8>")); let mut full =
str::from_utf8(&item.data).unwrap_or("<invalid UTF-8>");
if full.len() > 1000 {
full = &full[..1000];
}
ui.label(full);
} }
"image/png" => { "image/png" => {
ui.label("<image>"); ui.label("<image>");
@ -60,6 +72,8 @@ impl eframe::App for App {
ui.label("<unsupported mime type>"); ui.label("<unsupported mime type>");
} }
}); });
ui.separator();
} }
}); });
@ -69,6 +83,8 @@ impl eframe::App for App {
return; return;
}; };
ui.add_space(10.0);
match item.mime.as_str() { match item.mime.as_str() {
"text/plain" => { "text/plain" => {
ui.label(str::from_utf8(&item.data).unwrap_or("<invalid UTF-8>")); ui.label(str::from_utf8(&item.data).unwrap_or("<invalid UTF-8>"));
@ -101,13 +117,15 @@ pub fn main(socket_path: &Path) -> eyre::Result<()> {
println!("INFO: Reading clipboard history from socket"); println!("INFO: Reading clipboard history from socket");
let start = Instant::now(); let start = Instant::now();
let items = let mut items: Vec<Entry> =
ciborium::from_reader(BufReader::new(socket)).wrap_err("reading items from socket")?; ciborium::from_reader(BufReader::new(socket)).wrap_err("reading items from socket")?;
println!( println!(
"INFO: Read clipboard history from socket in {:?}", "INFO: Read clipboard history from socket in {:?}",
start.elapsed() start.elapsed()
); );
items.reverse();
// heh. good design. // heh. good design.
let socket = UnixStream::connect(&socket_path).wrap_err_with(|| { let socket = UnixStream::connect(&socket_path).wrap_err_with(|| {
format!( format!(

View file

@ -1,10 +1,11 @@
mod display;
mod daemon; mod daemon;
mod display;
use std::{io::Write, os::unix::net::UnixStream};
use eyre::{Context, OptionExt, bail}; 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)] #[derive(Clone, serde::Deserialize, serde::Serialize)]
struct Entry { 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 socket
.write_all(&[MESSAGE_STORE]) .write_all(&[MESSAGE_STORE])
.wrap_err("writing request type")?; .wrap_err("writing request type")?;