we're gaming

This commit is contained in:
nora 2025-09-16 20:32:15 +02:00
parent ecc1a4e6da
commit 0c5b9d26e1
5 changed files with 407 additions and 105 deletions

136
Cargo.lock generated
View file

@ -303,7 +303,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -338,7 +338,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -484,7 +484,7 @@ checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -610,9 +610,19 @@ dependencies = [
"egui_extras", "egui_extras",
"eyre", "eyre",
"serde", "serde",
"wayland-client",
"wayland-protocols",
"wl-clipboard-rs", "wl-clipboard-rs",
] ]
[[package]]
name = "codegen"
version = "0.1.0"
dependencies = [
"heck",
"strong-xml",
]
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.12.0" version = "0.12.0"
@ -765,7 +775,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -967,7 +977,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -988,7 +998,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -1091,7 +1101,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -1149,7 +1159,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -1200,7 +1210,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -1577,6 +1587,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "jetscii"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.21.1" version = "0.21.1"
@ -1636,6 +1652,12 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.175"
@ -1923,7 +1945,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -2346,7 +2368,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"unicase", "unicase",
] ]
@ -2377,7 +2399,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -2687,7 +2709,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -2698,7 +2720,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -2821,6 +2843,30 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "strong-xml"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d19fb3a618e2f1039e32317c9f525e6d45c55af704ec7c429aa74412419bebf"
dependencies = [
"jetscii",
"lazy_static",
"memchr",
"strong-xml-derive",
"xmlparser",
]
[[package]]
name = "strong-xml-derive"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92c781f499321613b112be5d9338189ef1ed19689a01edd23d923ea57ad5c7e1"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.26.3" version = "0.26.3"
@ -2840,7 +2886,18 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn", "syn 2.0.106",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
] ]
[[package]] [[package]]
@ -2862,7 +2919,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -2913,7 +2970,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -2924,7 +2981,7 @@ checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -3012,7 +3069,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -3158,7 +3215,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3193,7 +3250,7 @@ checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3505,6 +3562,13 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "willy"
version = "0.1.0"
dependencies = [
"dirs",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -3613,7 +3677,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -3624,7 +3688,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -3635,7 +3699,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -3646,7 +3710,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -4088,6 +4152,12 @@ version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.0"
@ -4108,7 +4178,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"synstructure", "synstructure",
] ]
@ -4163,7 +4233,7 @@ checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"zbus-lockstep", "zbus-lockstep",
"zbus_xml", "zbus_xml",
"zvariant", "zvariant",
@ -4178,7 +4248,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"zbus_names", "zbus_names",
"zvariant", "zvariant",
"zvariant_utils", "zvariant_utils",
@ -4226,7 +4296,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -4246,7 +4316,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"synstructure", "synstructure",
] ]
@ -4280,7 +4350,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
] ]
[[package]] [[package]]
@ -4321,7 +4391,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.106",
"zvariant_utils", "zvariant_utils",
] ]
@ -4334,6 +4404,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn", "syn 2.0.106",
"winnow", "winnow",
] ]

View file

@ -1,3 +1,6 @@
[workspace]
members = [".", "willy", "willy/codegen"]
[package] [package]
name = "clippyboard" name = "clippyboard"
version = "0.1.0" version = "0.1.0"
@ -10,4 +13,6 @@ eframe = "0.32.2"
egui_extras = { version = "0.32.2", features = ["image"] } egui_extras = { version = "0.32.2", features = ["image"] }
eyre = "0.6.12" eyre = "0.6.12"
serde = "1.0.219" serde = "1.0.219"
wayland-client = "0.31.11"
wayland-protocols = { version = "0.32.9", features = ["staging"] }
wl-clipboard-rs = "0.9.2" wl-clipboard-rs = "0.9.2"

View file

@ -1,56 +1,307 @@
use super::Entry; use super::HistoryItem;
use super::MAX_ENTRY_SIZE; use super::MAX_ENTRY_SIZE;
use eframe::egui::ahash::HashSet;
use eyre::Context; use eyre::Context;
use std::collections::HashMap;
use std::io::BufWriter; use std::io::BufWriter;
use std::io::Read; use std::io::Read;
use std::os::fd::AsFd;
use std::os::unix::net::UnixListener; use std::os::unix::net::UnixListener;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
use std::time::Duration;
use std::time::SystemTime; use std::time::SystemTime;
use wayland_client::Dispatch;
use wayland_client::Proxy;
use wayland_client::backend::ObjectId;
use wayland_client::event_created_child;
use wayland_client::globals::GlobalListContents;
use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::{
EVT_DATA_OFFER_OPCODE, ExtDataControlDeviceV1,
};
use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1;
use wl_clipboard_rs::paste::ClipboardType; use wl_clipboard_rs::paste::ClipboardType;
use wl_clipboard_rs::paste::MimeType; use wl_clipboard_rs::paste::MimeType;
use wl_clipboard_rs::paste::Seat; use wl_clipboard_rs::paste::Seat;
pub(crate) fn handle_peer( struct HistoryState {
mut peer: UnixStream, next_item_id: Arc<AtomicU64>,
next_id: Arc<AtomicU64>, last_copied_item_id: Arc<AtomicU64>,
last_copied: Arc<AtomicU64>, items: Arc<Mutex<Vec<HistoryItem>>>,
items: Arc<Mutex<Vec<Entry>>>, }
) -> eyre::Result<()> {
struct InProgressOffer {
mime_types: HashSet<String>,
time: Duration,
}
#[derive(Debug)]
struct CurrentSelection {
mime_types: HashSet<String>,
offer: ExtDataControlOfferV1,
time: Duration,
}
struct WlState {
history_state: Arc<HistoryState>,
offers: HashMap<ObjectId, InProgressOffer>,
current_primary_selection: Option<CurrentSelection>,
current_selection: Option<CurrentSelection>,
}
impl Dispatch<WlRegistry, GlobalListContents> for WlState {
fn event(
_state: &mut Self,
_proxy: &WlRegistry,
_event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl Dispatch<ExtDataControlManagerV1, ()> for WlState {
fn event(
_state: &mut Self,
_proxy: &ExtDataControlManagerV1,
_event: <ExtDataControlManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl Dispatch<WlSeat, ()> for WlState {
fn event(
_state: &mut Self,
_proxy: &WlSeat,
_event: <WlSeat as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
fn event(
state: &mut Self,
_proxy: &ExtDataControlDeviceV1,
event: <ExtDataControlDeviceV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
match event {
// A new offer is being prepared, register it and don't do anything yet
ext_data_control_device_v1::Event::DataOffer { id } => {
state.offers.insert(
id.id(),
InProgressOffer {
mime_types: Default::default(),
time: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(),
},
);
}
// The selection has been confirmed, we just properly got a new offer that we should use.
ext_data_control_device_v1::Event::Selection { id } => {
let new_offer = match id {
Some(id) => {
let offer = state.offers.remove(&id.id());
offer.map(|offer| CurrentSelection {
offer: id,
mime_types: offer.mime_types,
time: offer.time,
})
}
None => None,
};
if let Some(current) = &state.current_selection {
current.offer.destroy();
}
state.current_selection = new_offer;
if let Some(offer) = &state.current_selection {
let Some(mime) = ["text/plain", "image/png"]
.iter()
.find(|mime| offer.mime_types.contains(**mime))
else {
eprintln!(
"WARN: No supported mime type found. Found mime types: {:?}",
offer.mime_types
);
return;
};
let (reader, writer) = std::io::pipe().unwrap();
offer.offer.receive(mime.to_string(), writer.as_fd());
let history_state = state.history_state.clone();
let mime = mime.to_string();
let time = offer.time;
std::thread::spawn(move || {
let result =
do_read_clipboard_into_history(&history_state, time, mime, reader);
if let Err(err) = result {
eprintln!("WARN: Failed to read clipboard: {:?}", err)
}
});
}
}
ext_data_control_device_v1::Event::PrimarySelection { id } => {
let new_offer = match id {
Some(id) => {
let offer = state.offers.remove(&id.id());
offer.map(|offer| CurrentSelection {
offer: id,
mime_types: offer.mime_types,
time: offer.time,
})
}
None => None,
};
if let Some(current) = &state.current_primary_selection {
current.offer.destroy();
}
state.current_primary_selection = new_offer;
}
_ => {}
}
}
event_created_child!(WlState, ExtDataControlDeviceV1, [
EVT_DATA_OFFER_OPCODE => (ExtDataControlOfferV1, ()),
]);
}
impl Dispatch<ExtDataControlOfferV1, ()> for WlState {
fn event(
state: &mut Self,
proxy: &ExtDataControlOfferV1,
event: <ExtDataControlOfferV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
match event {
ext_data_control_offer_v1::Event::Offer { mime_type } => {
if let Some(offer) = state.offers.get_mut(&proxy.id()) {
offer.mime_types.insert(mime_type);
}
}
_ => {}
}
}
}
pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
let _ = std::fs::remove_file(&socket_path); // lol
let socket = UnixListener::bind(&socket_path)
.wrap_err_with(|| format!("binding path {}", socket_path.display()))?;
let conn =
wayland_client::Connection::connect_to_env().wrap_err("connecting to the compositor")?;
let (globals, mut queue) =
registry_queue_init::<WlState>(&conn).wrap_err("initializing wayland connection")?;
let data_manager = globals.bind::<ExtDataControlManagerV1, _, _>(&queue.handle(), 1..=1, ()).wrap_err("getting ext_data_control_manager_v1, is ext-data-control-v1 not supported by the compositor?")?;
let seat = globals
.bind::<WlSeat, _, _>(&queue.handle(), 1..=1, ())
.wrap_err("getting seat")?;
let data_device = data_manager.get_data_device(&seat, &queue.handle(), ());
let history_state = Arc::new(HistoryState {
next_item_id: Arc::new(AtomicU64::new(0)),
last_copied_item_id: Arc::new(AtomicU64::new(u64::MAX)),
items: Arc::new(Mutex::new(Vec::<HistoryItem>::new())),
// for deduplication because the event stream will tell us that we just copied something :)
});
let history_state2 = history_state.clone();
std::thread::spawn(move || {
let mut state = WlState {
offers: HashMap::new(),
current_primary_selection: None,
current_selection: None,
history_state: history_state2,
};
loop {
queue.blocking_dispatch(&mut state);
}
});
println!("INFO: Listening on {}", socket_path.display());
for peer in socket.incoming() {
match peer {
Ok(peer) => {
let history_state = history_state.clone();
std::thread::spawn(move || {
let result = handle_peer(peer, &history_state);
if let Err(err) = result {
eprintln!("ERROR: Error handling peer: {err:?}");
}
});
}
Err(err) => {
eprintln!("ERROR: Error accepting peer: {err}");
}
}
}
Ok(())
}
fn handle_peer(mut peer: UnixStream, history_state: &HistoryState) -> eyre::Result<()> {
let mut request = [0; 1]; let mut request = [0; 1];
let Ok(()) = peer.read_exact(&mut request) else { let Ok(()) = peer.read_exact(&mut request) else {
return Ok(()); return Ok(());
}; };
match request[0] { match request[0] {
super::MESSAGE_STORE => { super::MESSAGE_STORE => {
handle_store(next_id, last_copied, &items).wrap_err("handling store message")?; handle_store(history_state).wrap_err("handling store message")?;
} }
super::MESSAGE_READ => { super::MESSAGE_READ => {
let items = items.lock().unwrap(); let items = history_state.items.lock().unwrap();
ciborium::into_writer(items.as_slice(), BufWriter::new(peer)) ciborium::into_writer(items.as_slice(), BufWriter::new(peer))
.wrap_err("writing items to socket")?; .wrap_err("writing items to socket")?;
} }
super::MESSAGE_COPY => { super::MESSAGE_COPY => {
handle_copy(peer, last_copied, items).wrap_err("handling copy message")?; handle_copy(peer, history_state).wrap_err("handling copy message")?;
} }
_ => {} _ => {}
}; };
Ok(()) Ok(())
} }
fn handle_copy( fn handle_copy(mut peer: UnixStream, history_state: &HistoryState) -> Result<(), eyre::Error> {
mut peer: UnixStream,
last_copied: Arc<AtomicU64>,
items: Arc<Mutex<Vec<Entry>>>,
) -> Result<(), eyre::Error> {
let mut id = [0; 8]; let mut id = [0; 8];
peer.read_exact(&mut id).wrap_err("failed to read id")?; peer.read_exact(&mut id).wrap_err("failed to read id")?;
let id = u64::from_le_bytes(id); let id = u64::from_le_bytes(id);
let mut items = items.lock().unwrap(); let mut items = history_state.items.lock().unwrap();
let Some(idx) = items.iter().position(|item| item.id == id) else { let Some(idx) = items.iter().position(|item| item.id == id) else {
return Ok(()); return Ok(());
}; };
@ -64,18 +315,16 @@ fn handle_copy(
wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()), wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()),
wl_clipboard_rs::copy::MimeType::Specific(entry.mime), wl_clipboard_rs::copy::MimeType::Specific(entry.mime),
); );
last_copied.store(entry.id, std::sync::atomic::Ordering::Relaxed); history_state
.last_copied_item_id
.store(entry.id, std::sync::atomic::Ordering::Relaxed);
if let Err(err) = result { if let Err(err) = result {
println!("WARNING: Copy failed: {err:?}"); println!("WARNING: Copy failed: {err:?}");
} }
Ok(()) Ok(())
} }
fn handle_store( fn handle_store(history_state: &HistoryState) -> Result<(), eyre::Error> {
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();
@ -99,20 +348,30 @@ fn handle_store(
) )
.wrap_err("getting contents")?; .wrap_err("getting contents")?;
let mut data_reader = data_readear.take(MAX_ENTRY_SIZE); do_read_clipboard_into_history(&history_state, time, mime.to_string(), data_readear)?;
Ok(())
}
fn do_read_clipboard_into_history(
history_state: &HistoryState,
time: std::time::Duration,
mime: String,
data_reader: impl Read,
) -> Result<(), eyre::Error> {
let mut data_reader = data_reader.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")?;
let new_entry = HistoryItem {
let new_entry = Entry { id: history_state
id: next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), .next_item_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(),
}; };
let mut items = history_state.items.lock().unwrap();
let mut items = items.lock().unwrap();
if items if items
.last() .last()
.is_some_and(|last| last.mime == new_entry.mime && last.data == new_entry.data) .is_some_and(|last| last.mime == new_entry.mime && last.data == new_entry.data)
@ -120,8 +379,9 @@ fn handle_store(
println!("INFO: Skipping store of new item because it is identical to last one"); println!("INFO: Skipping store of new item because it is identical to last one");
return Ok(()); return Ok(());
} }
let last_copied = history_state
let last_copied = last_copied.load(std::sync::atomic::Ordering::Relaxed); .last_copied_item_id
.load(std::sync::atomic::Ordering::Relaxed);
if let Some(item) = items.iter().find(|item| item.id == last_copied) if let Some(item) = items.iter().find(|item| item.id == last_copied)
&& item.mime == new_entry.mime && item.mime == new_entry.mime
&& item.data == new_entry.data && item.data == new_entry.data
@ -129,13 +389,11 @@ fn handle_store(
println!("INFO: Skipping store of new item because the copy came from us"); println!("INFO: Skipping store of new item because the copy came from us");
return Ok(()); return Ok(());
} }
items.push(new_entry); items.push(new_entry);
let mut running_total = 0; let mut running_total = 0;
let mut cutoff = None; let mut cutoff = None;
for (idx, item) in items.iter().enumerate().rev() { for (idx, item) in items.iter().enumerate().rev() {
running_total += item.data.len() + std::mem::size_of::<Entry>(); running_total += item.data.len() + std::mem::size_of::<HistoryItem>();
if running_total > crate::MAX_HISTORY_BYTE_SIZE { if running_total > crate::MAX_HISTORY_BYTE_SIZE {
cutoff = Some(idx); cutoff = Some(idx);
} }
@ -148,43 +406,8 @@ fn handle_store(
); );
items.splice(0..=cutoff, []); items.splice(0..=cutoff, []);
} }
println!( println!(
"INFO: Successfully stored clipboard value of mime type {mime} (new history size {running_total})" "INFO: Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
); );
Ok(()) Ok(())
} }
pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
let _ = std::fs::remove_file(&socket_path); // lol
let socket = UnixListener::bind(&socket_path)
.wrap_err_with(|| format!("binding path {}", socket_path.display()))?;
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());
for peer in socket.incoming() {
match peer {
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, last_copied, items);
if let Err(err) = result {
eprintln!("ERROR: Error handling peer: {err:?}");
}
});
}
Err(err) => {
eprintln!("ERROR: Error accepting peer: {err}");
}
}
}
Ok(())
}

View file

@ -12,10 +12,10 @@ use std::{
time::Instant, time::Instant,
}; };
use super::Entry; use super::HistoryItem;
pub(crate) struct App { pub(crate) struct App {
pub(crate) items: Vec<Entry>, pub(crate) items: Vec<HistoryItem>,
pub(crate) selected_idx: usize, pub(crate) selected_idx: usize,
pub(crate) socket: UnixStream, pub(crate) socket: UnixStream,
} }
@ -85,6 +85,10 @@ impl eframe::App for App {
ui.add_space(10.0); ui.add_space(10.0);
ui.label(format!("MIME type: {}", item.mime));
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>"));
@ -117,7 +121,7 @@ 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 mut items: Vec<Entry> = let mut items: Vec<HistoryItem> =
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 {:?}",

View file

@ -8,7 +8,7 @@ const MAX_ENTRY_SIZE: u64 = 50_000_000;
const MAX_HISTORY_BYTE_SIZE: usize = 100_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 HistoryItem {
id: u64, id: u64,
mime: String, mime: String,
data: Vec<u8>, data: Vec<u8>,