mirror of
https://github.com/Noratrieb/clippyboard.git
synced 2026-01-14 18:05:04 +01:00
toll
This commit is contained in:
parent
1164ed1e3e
commit
fbce93b927
3 changed files with 154 additions and 71 deletions
137
src/daemon.rs
137
src/daemon.rs
|
|
@ -17,21 +17,73 @@ 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")?;
|
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()
|
let time = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap();
|
.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"]
|
let Some(mime) = ["text/plain", "image/png"]
|
||||||
.iter()
|
.iter()
|
||||||
.find(|mime| mime_types.contains(**mime))
|
.find(|mime| mime_types.contains(**mime))
|
||||||
|
|
@ -48,54 +100,58 @@ pub(crate) fn handle_peer(
|
||||||
.wrap_err("getting contents")?;
|
.wrap_err("getting contents")?;
|
||||||
|
|
||||||
let mut data_reader = data_readear.take(MAX_ENTRY_SIZE);
|
let mut data_reader = data_readear.take(MAX_ENTRY_SIZE);
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
data_reader
|
data_reader
|
||||||
.read_to_end(&mut data)
|
.read_to_end(&mut data)
|
||||||
.wrap_err("reading content 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),
|
id: next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||||
mime: mime.to_string(),
|
mime: mime.to_string(),
|
||||||
data,
|
data,
|
||||||
created_time: u64::try_from(time.as_millis()).unwrap(),
|
created_time: u64::try_from(time.as_millis()).unwrap(),
|
||||||
});
|
};
|
||||||
|
|
||||||
println!("INFO Successfully stored clipboard value of mime type {mime}");
|
let mut items = items.lock().unwrap();
|
||||||
}
|
if items
|
||||||
super::MESSAGE_READ => {
|
.last()
|
||||||
let items = items.lock().unwrap();
|
.is_some_and(|last| last.mime == new_entry.mime && last.data == new_entry.data)
|
||||||
|
{
|
||||||
ciborium::into_writer(items.as_slice(), BufWriter::new(peer))
|
println!("INFO: Skipping store of new item because it is identical to last one");
|
||||||
.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(());
|
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
|
items.push(new_entry);
|
||||||
let mut opts = wl_clipboard_rs::copy::Options::new();
|
|
||||||
opts.clipboard(wl_clipboard_rs::copy::ClipboardType::Regular);
|
let mut running_total = 0;
|
||||||
let result = wl_clipboard_rs::copy::copy(
|
let mut cutoff = None;
|
||||||
opts,
|
for (idx, item) in items.iter().enumerate().rev() {
|
||||||
wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()),
|
running_total += item.data.len() + std::mem::size_of::<Entry>();
|
||||||
wl_clipboard_rs::copy::MimeType::Specific(entry.mime),
|
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 {
|
items.splice(0..=cutoff, []);
|
||||||
println!("WARNING: Copy failed: {err:?}");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
println!(
|
||||||
};
|
"INFO: Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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:?}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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!(
|
||||||
|
|
|
||||||
12
src/main.rs
12
src/main.rs
|
|
@ -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")?;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue