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", "zerocopy",
] ]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "android-activity" name = "android-activity"
version = "0.6.0" version = "0.6.0"
@ -303,7 +312,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -338,7 +347,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -484,7 +493,7 @@ checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -610,19 +619,13 @@ dependencies = [
"egui_extras", "egui_extras",
"eyre", "eyre",
"serde", "serde",
"tracing",
"tracing-subscriber",
"wayland-client", "wayland-client",
"wayland-protocols", "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"
@ -775,7 +778,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -977,7 +980,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -998,7 +1001,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -1101,7 +1104,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -1159,7 +1162,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -1210,7 +1213,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -1587,12 +1590,6 @@ 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"
@ -1740,6 +1737,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.5" version = "2.7.5"
@ -1916,6 +1922,15 @@ dependencies = [
"minimal-lexical", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1945,7 +1960,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -2368,7 +2383,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"unicase", "unicase",
] ]
@ -2399,7 +2414,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -2608,6 +2623,23 @@ dependencies = [
"thiserror 2.0.16", "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]] [[package]]
name = "renderdoc-sys" name = "renderdoc-sys"
version = "1.1.0" version = "1.1.0"
@ -2709,7 +2741,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -2720,7 +2752,16 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "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]] [[package]]
@ -2843,30 +2884,6 @@ 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"
@ -2886,18 +2903,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.106", "syn",
]
[[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]]
@ -2919,7 +2925,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -2970,7 +2976,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -2981,7 +2987,16 @@ checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "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]] [[package]]
@ -3069,7 +3084,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -3079,6 +3094,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@ -3161,6 +3206,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -3215,7 +3266,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3250,7 +3301,7 @@ checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3562,13 +3613,6 @@ 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"
@ -3677,7 +3721,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -3688,7 +3732,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -3699,7 +3743,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -3710,7 +3754,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -4152,12 +4196,6 @@ 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"
@ -4178,7 +4216,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"synstructure", "synstructure",
] ]
@ -4233,7 +4271,7 @@ checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"zbus-lockstep", "zbus-lockstep",
"zbus_xml", "zbus_xml",
"zvariant", "zvariant",
@ -4248,7 +4286,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"zbus_names", "zbus_names",
"zvariant", "zvariant",
"zvariant_utils", "zvariant_utils",
@ -4296,7 +4334,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -4316,7 +4354,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"synstructure", "synstructure",
] ]
@ -4350,7 +4388,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
] ]
[[package]] [[package]]
@ -4391,7 +4429,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn",
"zvariant_utils", "zvariant_utils",
] ]
@ -4404,6 +4442,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.106", "syn",
"winnow", "winnow",
] ]

View file

@ -1,6 +1,3 @@
[workspace]
members = [".", "willy", "willy/codegen"]
[package] [package]
name = "clippyboard" name = "clippyboard"
version = "0.1.0" version = "0.1.0"
@ -13,6 +10,8 @@ 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"
tracing = { version = "0.1.41", features = ["attributes"] }
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
wayland-client = "0.31.11" wayland-client = "0.31.11"
wayland-protocols = { version = "0.32.9", features = ["staging"] } wayland-protocols = { version = "0.32.9", features = ["staging"] }
wl-clipboard-rs = "0.9.2" wl-clipboard-rs = "0.9.2"

View file

@ -2,24 +2,19 @@ use super::HistoryItem;
use super::MAX_ENTRY_SIZE; use super::MAX_ENTRY_SIZE;
use eframe::egui::ahash::HashSet; use eframe::egui::ahash::HashSet;
use eyre::Context; use eyre::Context;
use eyre::bail;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::BufWriter; use std::io::{BufReader, BufWriter, PipeWriter, Read, Write};
use std::io::Read;
use std::os::fd::AsFd; use std::os::fd::AsFd;
use std::os::unix::net::UnixListener; use std::os::unix::net::{UnixListener, UnixStream};
use std::os::unix::net::UnixStream;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::{Arc, Mutex, OnceLock, atomic::AtomicU64};
use std::sync::Mutex;
use std::sync::atomic::AtomicU64;
use std::time::Duration; use std::time::Duration;
use std::time::SystemTime; use std::time::SystemTime;
use wayland_client::Dispatch; use tracing::info;
use wayland_client::Proxy; use tracing::warn;
use wayland_client::backend::ObjectId; use tracing_subscriber::EnvFilter;
use wayland_client::event_created_child; use wayland_client::{Dispatch, Proxy, QueueHandle, backend::ObjectId, 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_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat; 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;
@ -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_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;
use wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1; use wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1;
use wl_clipboard_rs::paste::ClipboardType; use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1;
use wl_clipboard_rs::paste::MimeType; use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1;
use wl_clipboard_rs::paste::Seat;
struct HistoryState { struct SharedState {
next_item_id: Arc<AtomicU64>, next_item_id: AtomicU64,
last_copied_item_id: Arc<AtomicU64>, // for deduplication because the event stream will tell us that we just copied something :)
items: Arc<Mutex<Vec<HistoryItem>>>, 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 { struct InProgressOffer {
@ -52,22 +51,80 @@ struct CurrentSelection {
} }
struct WlState { 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>, offers: HashMap<ObjectId, InProgressOffer>,
current_primary_selection: Option<CurrentSelection>, current_primary_selection: Option<CurrentSelection>,
current_selection: Option<CurrentSelection>, current_selection: Option<CurrentSelection>,
} }
impl Dispatch<WlRegistry, GlobalListContents> for WlState { impl Dispatch<WlRegistry, ()> for WlState {
fn event( fn event(
_state: &mut Self, state: &mut Self,
_proxy: &WlRegistry, proxy: &WlRegistry,
_event: <WlRegistry as wayland_client::Proxy>::Event, event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents, _data: &(),
_conn: &wayland_client::Connection, _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 { impl Dispatch<ExtDataControlManagerV1, ()> for WlState {
@ -93,6 +150,12 @@ impl Dispatch<WlSeat, ()> for WlState {
} }
} }
impl Dispatch<ExtDataControlDeviceV1, ()> for WlState { impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
#[tracing::instrument(
skip(state, _proxy, event, _data, _conn, _qhandle),
level = "info",
ret,
target = "ExtDataControlDeviceV1::event"
)]
fn event( fn event(
state: &mut Self, state: &mut Self,
_proxy: &ExtDataControlDeviceV1, _proxy: &ExtDataControlDeviceV1,
@ -141,8 +204,8 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
.iter() .iter()
.find(|mime| offer.mime_types.contains(**mime)) .find(|mime| offer.mime_types.contains(**mime))
else { else {
eprintln!( warn!(
"WARN: No supported mime type found. Found mime types: {:?}", "No supported mime type found. Found mime types: {:?}",
offer.mime_types offer.mime_types
); );
return; return;
@ -151,14 +214,14 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
let (reader, writer) = std::io::pipe().unwrap(); let (reader, writer) = std::io::pipe().unwrap();
offer.offer.receive(mime.to_string(), writer.as_fd()); 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 mime = mime.to_string();
let time = offer.time; let time = offer.time;
std::thread::spawn(move || { std::thread::spawn(move || {
let result = let result =
do_read_clipboard_into_history(&history_state, time, mime, reader); do_read_clipboard_into_history(&history_state, time, mime, reader);
if let Err(err) = result { 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; 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<()> { 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 _ = std::fs::remove_file(&socket_path); // lol
let socket = UnixListener::bind(&socket_path) let socket = UnixListener::bind(&socket_path)
.wrap_err_with(|| format!("binding path {}", socket_path.display()))?; .wrap_err_with(|| format!("binding path {}", socket_path.display()))?;
@ -220,53 +333,70 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
let conn = let conn =
wayland_client::Connection::connect_to_env().wrap_err("connecting to the compositor")?; wayland_client::Connection::connect_to_env().wrap_err("connecting to the compositor")?;
let (globals, mut queue) = let mut queue = conn.new_event_queue::<WlState>();
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 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 data_control_manager: OnceLock::new(),
.bind::<WlSeat, _, _>(&queue.handle(), 1..=1, ()) data_control_devices: Mutex::new(HashMap::new()),
.wrap_err("getting seat")?; qh: queue.handle(),
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(); let history_state2 = shared_state.clone();
std::thread::spawn(move || {
let mut state = WlState {
offers: HashMap::new(),
current_primary_selection: None,
current_selection: None,
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 { 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() { for peer in socket.incoming() {
match peer { match peer {
Ok(peer) => { Ok(peer) => {
let history_state = history_state.clone(); let history_state = shared_state.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let result = handle_peer(peer, &history_state); let result = handle_peer(peer, &history_state);
if let Err(err) = result { if let Err(err) = result {
eprintln!("ERROR: Error handling peer: {err:?}"); warn!("Error handling peer: {err:?}");
} }
}); });
} }
Err(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(()) 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 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 => {
handle_store(history_state).wrap_err("handling store message")?;
}
super::MESSAGE_READ => { 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)) 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, history_state).wrap_err("handling copy message")?; handle_copy(peer, shared_state).wrap_err("handling copy message")?;
} }
_ => {} _ => {}
}; };
Ok(()) 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]; 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 = history_state.items.lock().unwrap(); let mut items = shared_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(());
}; };
let entry = items.remove(idx); let entry = items.remove(idx);
items.push(entry.clone()); items.push(entry.clone());
let mut opts = wl_clipboard_rs::copy::Options::new();
opts.clipboard(wl_clipboard_rs::copy::ClipboardType::Regular) drop(items);
.seat(wl_clipboard_rs::copy::Seat::All);
let result = wl_clipboard_rs::copy::copy( for device in &*shared_state.data_control_devices.lock().unwrap() {
opts, let data_source = shared_state
wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()), .data_control_manager
wl_clipboard_rs::copy::MimeType::Specific(entry.mime), .get()
); .expect("data manger not found")
history_state .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 .last_copied_item_id
.store(entry.id, std::sync::atomic::Ordering::Relaxed); .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(()) Ok(())
} }
fn do_read_clipboard_into_history( fn do_read_clipboard_into_history(
history_state: &HistoryState, history_state: &SharedState,
time: std::time::Duration, time: std::time::Duration,
mime: String, mime: String,
data_reader: impl Read, data_reader: impl Read,
) -> Result<(), eyre::Error> { ) -> 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(); let mut data = Vec::new();
data_reader data_reader
.read_to_end(&mut data) .read_to_end(&mut data)
@ -368,7 +491,7 @@ fn do_read_clipboard_into_history(
.next_item_id .next_item_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed), .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
mime: mime.to_string(), mime: mime.to_string(),
data, data: data.into(),
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 = history_state.items.lock().unwrap();
@ -376,7 +499,7 @@ fn do_read_clipboard_into_history(
.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)
{ {
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(()); return Ok(());
} }
let last_copied = history_state let last_copied = history_state
@ -386,7 +509,7 @@ fn do_read_clipboard_into_history(
&& item.mime == new_entry.mime && item.mime == new_entry.mime
&& item.data == new_entry.data && 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(()); return Ok(());
} }
items.push(new_entry); items.push(new_entry);
@ -399,15 +522,15 @@ fn do_read_clipboard_into_history(
} }
} }
if let Some(cutoff) = cutoff { if let Some(cutoff) = cutoff {
println!( info!(
"INFO: Dropping old {} items because limit of {} bytes was reached for the history", "Dropping old {} items because limit of {} bytes was reached for the history",
cutoff + 1, cutoff + 1,
crate::MAX_HISTORY_BYTE_SIZE crate::MAX_HISTORY_BYTE_SIZE
); );
items.splice(0..=cutoff, []); items.splice(0..=cutoff, []);
} }
println!( info!(
"INFO: Successfully stored clipboard value of mime type {mime} (new history size {running_total})" "Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
); );
Ok(()) Ok(())
} }

View file

@ -1,8 +1,9 @@
mod daemon; mod daemon;
mod display; mod display;
use eyre::{Context, OptionExt, bail}; use eyre::{OptionExt, bail};
use std::{io::Write, os::unix::net::UnixStream}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
const MAX_ENTRY_SIZE: u64 = 50_000_000; 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;
@ -11,11 +12,23 @@ const MAX_HISTORY_BYTE_SIZE: usize = 100_000_000;
struct HistoryItem { struct HistoryItem {
id: u64, id: u64,
mime: String, mime: String,
data: Vec<u8>, #[serde(
deserialize_with = "deserialize_data",
serialize_with = "serialize_data"
)]
data: Arc<[u8]>,
created_time: u64, 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; const MESSAGE_READ: u8 = 1;
/// Argument: One u64-bit LE value, the ID /// Argument: One u64-bit LE value, the ID
const MESSAGE_COPY: u8 = 2; const MESSAGE_COPY: u8 = 2;
@ -31,25 +44,8 @@ fn main() -> eyre::Result<()> {
match mode.as_str() { match mode.as_str() {
"daemon" => daemon::main(&socket_path)?, "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)?, "display" => display::main(&socket_path)?,
_ => panic!("invalid mode"), _ => panic!("invalid mode, supported: daemon, display"),
} }
Ok(()) Ok(())