it's all coming together

This commit is contained in:
nora 2025-09-16 22:03:47 +02:00
parent 0c5b9d26e1
commit ea487d598c
4 changed files with 390 additions and 234 deletions

228
Cargo.lock generated
View file

@ -127,6 +127,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-activity"
version = "0.6.0"
@ -303,7 +312,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -338,7 +347,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -484,7 +493,7 @@ checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -610,19 +619,13 @@ dependencies = [
"egui_extras",
"eyre",
"serde",
"tracing",
"tracing-subscriber",
"wayland-client",
"wayland-protocols",
"wl-clipboard-rs",
]
[[package]]
name = "codegen"
version = "0.1.0"
dependencies = [
"heck",
"strong-xml",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
@ -775,7 +778,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -977,7 +980,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -998,7 +1001,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -1101,7 +1104,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -1159,7 +1162,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -1210,7 +1213,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -1587,12 +1590,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "jetscii"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
[[package]]
name = "jni"
version = "0.21.1"
@ -1740,6 +1737,15 @@ dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.7.5"
@ -1916,6 +1922,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1945,7 +1960,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -2368,7 +2383,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"unicase",
]
@ -2399,7 +2414,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -2608,6 +2623,23 @@ dependencies = [
"thiserror 2.0.16",
]
[[package]]
name = "regex-automata"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@ -2709,7 +2741,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -2720,7 +2752,16 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
@ -2843,30 +2884,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "strum"
version = "0.26.3"
@ -2886,18 +2903,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"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",
"syn",
]
[[package]]
@ -2919,7 +2925,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -2970,7 +2976,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -2981,7 +2987,16 @@ checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
@ -3069,7 +3084,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -3079,6 +3094,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -3161,6 +3206,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
@ -3215,7 +3266,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"wasm-bindgen-shared",
]
@ -3250,7 +3301,7 @@ checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3562,13 +3613,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "willy"
version = "0.1.0"
dependencies = [
"dirs",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3677,7 +3721,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -3688,7 +3732,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -3699,7 +3743,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -3710,7 +3754,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -4152,12 +4196,6 @@ version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "yoke"
version = "0.8.0"
@ -4178,7 +4216,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"synstructure",
]
@ -4233,7 +4271,7 @@ checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"zbus-lockstep",
"zbus_xml",
"zvariant",
@ -4248,7 +4286,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"zbus_names",
"zvariant",
"zvariant_utils",
@ -4296,7 +4334,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -4316,7 +4354,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"synstructure",
]
@ -4350,7 +4388,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
]
[[package]]
@ -4391,7 +4429,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.106",
"syn",
"zvariant_utils",
]
@ -4404,6 +4442,6 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.106",
"syn",
"winnow",
]

View file

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

View file

@ -2,24 +2,19 @@ use super::HistoryItem;
use super::MAX_ENTRY_SIZE;
use eframe::egui::ahash::HashSet;
use eyre::Context;
use eyre::bail;
use std::collections::HashMap;
use std::io::BufWriter;
use std::io::Read;
use std::io::{BufReader, BufWriter, PipeWriter, Read, Write};
use std::os::fd::AsFd;
use std::os::unix::net::UnixListener;
use std::os::unix::net::UnixStream;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex, OnceLock, atomic::AtomicU64};
use std::time::Duration;
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 tracing::info;
use tracing::warn;
use tracing_subscriber::EnvFilter;
use wayland_client::{Dispatch, Proxy, QueueHandle, backend::ObjectId, event_created_child};
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;
@ -29,14 +24,18 @@ use wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1
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::MimeType;
use wl_clipboard_rs::paste::Seat;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1;
struct HistoryState {
next_item_id: Arc<AtomicU64>,
last_copied_item_id: Arc<AtomicU64>,
items: Arc<Mutex<Vec<HistoryItem>>>,
struct SharedState {
next_item_id: AtomicU64,
// for deduplication because the event stream will tell us that we just copied something :)
last_copied_item_id: AtomicU64,
items: Mutex<Vec<HistoryItem>>,
data_control_manager: OnceLock<ExtDataControlManagerV1>,
data_control_devices: Mutex<HashMap</*seat global name */ u32, ExtDataControlDeviceV1>>,
qh: QueueHandle<WlState>,
}
struct InProgressOffer {
@ -52,22 +51,80 @@ struct CurrentSelection {
}
struct WlState {
history_state: Arc<HistoryState>,
shared_state: Arc<SharedState>,
/// wl_seat that arrived before the data control manager so we weren't able to grab their device immediatly.
deferred_seats: Vec<WlSeat>,
offers: HashMap<ObjectId, InProgressOffer>,
current_primary_selection: Option<CurrentSelection>,
current_selection: Option<CurrentSelection>,
}
impl Dispatch<WlRegistry, GlobalListContents> for WlState {
impl Dispatch<WlRegistry, ()> for WlState {
fn event(
_state: &mut Self,
_proxy: &WlRegistry,
_event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
state: &mut Self,
proxy: &WlRegistry,
event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
qhandle: &wayland_client::QueueHandle<Self>,
) {
match event {
wayland_client::protocol::wl_registry::Event::Global {
name,
interface,
version: _, // we only need version 1
} => {
if interface == WlSeat::interface().name {
info!("A new seat was connected");
let seat: WlSeat = proxy.bind(name, 1, qhandle, ());
match state.shared_state.data_control_manager.get() {
None => {
state.deferred_seats.push(seat);
}
Some(manager) => {
let device = manager.get_data_device(&seat, qhandle, ());
state
.shared_state
.data_control_devices
.lock()
.unwrap()
.insert(name, device);
}
}
} else if interface == ExtDataControlManagerV1::interface().name {
let manager: ExtDataControlManagerV1 = proxy.bind(name, 1, qhandle, ());
for seat in state.deferred_seats.drain(..) {
let device = manager.get_data_device(&seat, qhandle, ());
state
.shared_state
.data_control_devices
.lock()
.unwrap()
.insert(name, device);
}
state
.shared_state
.data_control_manager
.set(manager)
.expect("ext_data_control_manager_v1 already set, global appeared twice?");
}
}
wayland_client::protocol::wl_registry::Event::GlobalRemove { name } => {
// try to remove, if it's not a wl_seat it may not exist
state
.shared_state
.data_control_devices
.lock()
.unwrap()
.remove(&name);
}
_ => {}
}
}
}
impl Dispatch<ExtDataControlManagerV1, ()> for WlState {
@ -93,6 +150,12 @@ impl Dispatch<WlSeat, ()> for WlState {
}
}
impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
#[tracing::instrument(
skip(state, _proxy, event, _data, _conn, _qhandle),
level = "info",
ret,
target = "ExtDataControlDeviceV1::event"
)]
fn event(
state: &mut Self,
_proxy: &ExtDataControlDeviceV1,
@ -141,8 +204,8 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
.iter()
.find(|mime| offer.mime_types.contains(**mime))
else {
eprintln!(
"WARN: No supported mime type found. Found mime types: {:?}",
warn!(
"No supported mime type found. Found mime types: {:?}",
offer.mime_types
);
return;
@ -151,14 +214,14 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
let (reader, writer) = std::io::pipe().unwrap();
offer.offer.receive(mime.to_string(), writer.as_fd());
let history_state = state.history_state.clone();
let history_state = state.shared_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)
warn!("Failed to read clipboard: {:?}", err)
}
});
}
@ -183,6 +246,9 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
state.current_primary_selection = new_offer;
}
ext_data_control_device_v1::Event::Finished => {
warn!("device finished :(");
}
_ => {}
}
}
@ -212,7 +278,54 @@ impl Dispatch<ExtDataControlOfferV1, ()> for WlState {
}
}
impl Dispatch<ExtDataControlSourceV1, OfferData> for WlState {
#[tracing::instrument(
skip(_state, proxy, event, data, _conn, _qhandle),
level = "info",
ret,
target = "ExtDataControlSourceV1::event"
)]
fn event(
_state: &mut Self,
proxy: &ExtDataControlSourceV1,
event: <ExtDataControlSourceV1 as Proxy>::Event,
data: &OfferData,
_conn: &wayland_client::Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
ext_data_control_source_v1::Event::Send { mime_type: _, fd } => {
info!("pasting {:?}", std::str::from_utf8(&data.0));
let data = data.0.clone();
std::thread::spawn(move || {
let mut writer = BufWriter::new(PipeWriter::from(fd));
let result = writer.write_all(&data);
if let Err(err) = result {
warn!("Failed to write to requester: {:?}", err);
}
let result = writer.into_inner();
if let Err(err) = result {
warn!("Failed to write to requester: {:?}", err);
}
});
}
ext_data_control_source_v1::Event::Cancelled => {
info!("We have been replaced.");
proxy.destroy();
}
_ => {}
}
}
}
pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info")))
.init();
let _ = std::fs::remove_file(&socket_path); // lol
let socket = UnixListener::bind(&socket_path)
.wrap_err_with(|| format!("binding path {}", socket_path.display()))?;
@ -220,53 +333,70 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
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 mut queue = conn.new_event_queue::<WlState>();
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 shared_state = Arc::new(SharedState {
next_item_id: AtomicU64::new(0),
last_copied_item_id: AtomicU64::new(u64::MAX),
items: Mutex::new(Vec::<HistoryItem>::new()),
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 :)
data_control_manager: OnceLock::new(),
data_control_devices: Mutex::new(HashMap::new()),
qh: queue.handle(),
});
let history_state2 = history_state.clone();
std::thread::spawn(move || {
let mut state = WlState {
offers: HashMap::new(),
current_primary_selection: None,
current_selection: None,
let history_state2 = shared_state.clone();
history_state: history_state2,
};
let mut wl_state = WlState {
offers: HashMap::new(),
current_primary_selection: None,
current_selection: None,
deferred_seats: Vec::new(),
shared_state: history_state2,
};
conn.display().get_registry(&queue.handle(), ());
queue
.roundtrip(&mut wl_state)
.wrap_err("failed to set up wayland state")?;
if wl_state.shared_state.data_control_manager.get().is_none() {
bail!(
"{} not found, the ext-data-control-v1 Wayland extension is likely unsupported by your compositor.\n\
check https://wayland.app/protocols/ext-data-control-v1#compositor-support
",
ExtDataControlManagerV1::interface().name
);
}
std::thread::spawn(move || {
loop {
queue.blocking_dispatch(&mut state);
let result = queue
.blocking_dispatch(&mut wl_state)
.wrap_err("handling wayland");
if let Err(err) = result {
warn!("Received error from Wayland: {:?}", err);
}
}
});
println!("INFO: Listening on {}", socket_path.display());
info!("Listening on {}", socket_path.display());
for peer in socket.incoming() {
match peer {
Ok(peer) => {
let history_state = history_state.clone();
let history_state = shared_state.clone();
std::thread::spawn(move || {
let result = handle_peer(peer, &history_state);
if let Err(err) = result {
eprintln!("ERROR: Error handling peer: {err:?}");
warn!("Error handling peer: {err:?}");
}
});
}
Err(err) => {
eprintln!("ERROR: Error accepting peer: {err}");
warn!("Error accepting peer: {err}");
}
}
}
@ -274,91 +404,84 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
Ok(())
}
fn handle_peer(mut peer: UnixStream, history_state: &HistoryState) -> eyre::Result<()> {
#[tracing::instrument(skip(peer, shared_state), level = "info")]
fn handle_peer(mut peer: UnixStream, shared_state: &SharedState) -> eyre::Result<()> {
let mut request = [0; 1];
let Ok(()) = peer.read_exact(&mut request) else {
return Ok(());
};
match request[0] {
super::MESSAGE_STORE => {
handle_store(history_state).wrap_err("handling store message")?;
}
super::MESSAGE_READ => {
let items = history_state.items.lock().unwrap();
let items = shared_state.items.lock().unwrap();
ciborium::into_writer(items.as_slice(), BufWriter::new(peer))
.wrap_err("writing items to socket")?;
}
super::MESSAGE_COPY => {
handle_copy(peer, history_state).wrap_err("handling copy message")?;
handle_copy(peer, shared_state).wrap_err("handling copy message")?;
}
_ => {}
};
Ok(())
}
fn handle_copy(mut peer: UnixStream, history_state: &HistoryState) -> Result<(), eyre::Error> {
struct OfferData(Arc<[u8]>);
fn handle_copy(mut peer: UnixStream, shared_state: &SharedState) -> 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 = history_state.items.lock().unwrap();
let mut items = shared_state.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),
);
history_state
drop(items);
for device in &*shared_state.data_control_devices.lock().unwrap() {
let data_source = shared_state
.data_control_manager
.get()
.expect("data manger not found")
.create_data_source(&shared_state.qh, OfferData(entry.data.clone()));
if entry.mime == "text/plain" {
// Just like wl_clipboard_rs, we also offer some extra mimes for text.
let text_mimes = [
"text/plain;charset=utf-8",
"text/plain",
"STRING",
"UTF8_STRING",
"TEXT",
];
for mime in text_mimes {
data_source.offer(mime.to_string());
}
}
data_source.offer(entry.mime.clone());
info!("setting the selection");
device.1.set_selection(Some(&data_source));
}
shared_state
.last_copied_item_id
.store(entry.id, std::sync::atomic::Ordering::Relaxed);
if let Err(err) = result {
println!("WARNING: Copy failed: {err:?}");
}
Ok(())
}
fn handle_store(history_state: &HistoryState) -> 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")?;
do_read_clipboard_into_history(&history_state, time, mime.to_string(), data_readear)?;
Ok(())
}
fn do_read_clipboard_into_history(
history_state: &HistoryState,
history_state: &SharedState,
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_reader = BufReader::new(data_reader).take(MAX_ENTRY_SIZE);
let mut data = Vec::new();
data_reader
.read_to_end(&mut data)
@ -368,7 +491,7 @@ fn do_read_clipboard_into_history(
.next_item_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
mime: mime.to_string(),
data,
data: data.into(),
created_time: u64::try_from(time.as_millis()).unwrap(),
};
let mut items = history_state.items.lock().unwrap();
@ -376,7 +499,7 @@ fn do_read_clipboard_into_history(
.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");
info!("INFO: Skipping store of new item because it is identical to last one");
return Ok(());
}
let last_copied = history_state
@ -386,7 +509,7 @@ fn do_read_clipboard_into_history(
&& item.mime == new_entry.mime
&& item.data == new_entry.data
{
println!("INFO: Skipping store of new item because the copy came from us");
info!("Skipping store of new item because the copy came from us");
return Ok(());
}
items.push(new_entry);
@ -399,15 +522,15 @@ fn do_read_clipboard_into_history(
}
}
if let Some(cutoff) = cutoff {
println!(
"INFO: Dropping old {} items because limit of {} bytes was reached for the history",
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})"
info!(
"Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
);
Ok(())
}

View file

@ -1,8 +1,9 @@
mod daemon;
mod display;
use eyre::{Context, OptionExt, bail};
use std::{io::Write, os::unix::net::UnixStream};
use eyre::{OptionExt, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
const MAX_ENTRY_SIZE: u64 = 50_000_000;
const MAX_HISTORY_BYTE_SIZE: usize = 100_000_000;
@ -11,11 +12,23 @@ const MAX_HISTORY_BYTE_SIZE: usize = 100_000_000;
struct HistoryItem {
id: u64,
mime: String,
data: Vec<u8>,
#[serde(
deserialize_with = "deserialize_data",
serialize_with = "serialize_data"
)]
data: Arc<[u8]>,
created_time: u64,
}
const MESSAGE_STORE: u8 = 0;
fn deserialize_data<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Arc<[u8]>, D::Error> {
Box::<[u8]>::deserialize(deserializer).map(Into::into)
}
fn serialize_data<S: Serializer>(data: &Arc<[u8]>, serializer: S) -> Result<S::Ok, S::Error> {
let data: &[u8] = data;
data.serialize(serializer)
}
const MESSAGE_READ: u8 = 1;
/// Argument: One u64-bit LE value, the ID
const MESSAGE_COPY: u8 = 2;
@ -31,25 +44,8 @@ fn main() -> eyre::Result<()> {
match mode.as_str() {
"daemon" => daemon::main(&socket_path)?,
"store" => {
let mut socket = UnixStream::connect(&socket_path).wrap_err_with(|| {
format!(
"connecting to socket at {}. is the daemon running?",
socket_path.display()
)
})?;
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")?;
}
"display" => display::main(&socket_path)?,
_ => panic!("invalid mode"),
_ => panic!("invalid mode, supported: daemon, display"),
}
Ok(())