diff --git a/Cargo.lock b/Cargo.lock index 9049c24..cc635f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 51c87f9..10e1f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/daemon.rs b/src/daemon.rs index 6a63ee4..a9f463c 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -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, - last_copied_item_id: Arc, - items: Arc>>, +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>, + + data_control_manager: OnceLock, + data_control_devices: Mutex>, + qh: QueueHandle, } struct InProgressOffer { @@ -52,22 +51,80 @@ struct CurrentSelection { } struct WlState { - history_state: Arc, + shared_state: Arc, + + /// wl_seat that arrived before the data control manager so we weren't able to grab their device immediatly. + deferred_seats: Vec, offers: HashMap, current_primary_selection: Option, current_selection: Option, } -impl Dispatch for WlState { +impl Dispatch for WlState { fn event( - _state: &mut Self, - _proxy: &WlRegistry, - _event: ::Event, - _data: &GlobalListContents, + state: &mut Self, + proxy: &WlRegistry, + event: ::Event, + _data: &(), _conn: &wayland_client::Connection, - _qhandle: &wayland_client::QueueHandle, + qhandle: &wayland_client::QueueHandle, ) { + 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 for WlState { @@ -93,6 +150,12 @@ impl Dispatch for WlState { } } impl Dispatch 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 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 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 for WlState { state.current_primary_selection = new_offer; } + ext_data_control_device_v1::Event::Finished => { + warn!("device finished :("); + } _ => {} } } @@ -212,7 +278,54 @@ impl Dispatch for WlState { } } +impl Dispatch 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: ::Event, + data: &OfferData, + _conn: &wayland_client::Connection, + _qhandle: &QueueHandle, + ) { + 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::(&conn).wrap_err("initializing wayland connection")?; + let mut queue = conn.new_event_queue::(); - let data_manager = globals.bind::(&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::::new()), - let seat = globals - .bind::(&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::::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(()) } diff --git a/src/main.rs b/src/main.rs index 04f7ce7..b5ef861 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, + #[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, D::Error> { + Box::<[u8]>::deserialize(deserializer).map(Into::into) +} + +fn serialize_data(data: &Arc<[u8]>, serializer: S) -> Result { + 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(())