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,21 +17,73 @@ use wl_clipboard_rs::paste::Seat;
pub(crate) fn handle_peer(
mut peer: UnixStream,
next_id: Arc<AtomicU64>,
last_copied: Arc<AtomicU64>,
items: Arc<Mutex<Vec<Entry>>>,
) -> 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")?;
handle_store(next_id, last_copied, &items).wrap_err("handling store message")?;
}
super::MESSAGE_READ => {
let items = items.lock().unwrap();
ciborium::into_writer(items.as_slice(), BufWriter::new(peer))
.wrap_err("writing items to socket")?;
}
super::MESSAGE_COPY => {
handle_copy(peer, last_copied, items).wrap_err("handling copy message")?;
}
_ => {}
};
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))
@ -48,54 +100,58 @@ pub(crate) fn handle_peer(
.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 {
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(),
});
};
println!("INFO Successfully stored clipboard value of mime type {mime}");
}
super::MESSAGE_READ => {
let items = items.lock().unwrap();
ciborium::into_writer(items.as_slice(), BufWriter::new(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 {
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 entry = items[idx].clone();
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(());
}
// 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),
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
);
if let Err(err) = result {
println!("WARNING: Copy failed: {err:?}");
items.splice(0..=cutoff, []);
}
}
_ => {}
};
println!(
"INFO: Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
);
Ok(())
}
@ -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::<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());
@ -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:?}");
}

View file

@ -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("<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" => {
ui.label("<image>");
@ -60,6 +72,8 @@ impl eframe::App for App {
ui.label("<unsupported mime type>");
}
});
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("<invalid UTF-8>"));
@ -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<Entry> =
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!(

View file

@ -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")?;