mirror of
https://github.com/Noratrieb/clippyboard.git
synced 2026-01-14 18:05:04 +01:00
cleanup
This commit is contained in:
parent
fce09d16cd
commit
cead214aa1
4 changed files with 210 additions and 373 deletions
74
Cargo.lock
generated
74
Cargo.lock
generated
|
|
@ -624,7 +624,6 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
"wl-clipboard-rs",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1123,12 +1122,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 = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
|
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fixedbitset"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -1804,12 +1797,6 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -1913,16 +1900,6 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.1"
|
version = "0.50.1"
|
||||||
|
|
@ -2284,16 +2261,6 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "os_pipe"
|
|
||||||
version = "1.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
|
|
@ -2344,16 +2311,6 @@ version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "petgraph"
|
|
||||||
version = "0.6.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
|
||||||
dependencies = [
|
|
||||||
"fixedbitset",
|
|
||||||
"indexmap",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
|
@ -3127,18 +3084,6 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tree_magic_mini"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"nom",
|
|
||||||
"once_cell",
|
|
||||||
"petgraph",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
|
|
@ -4109,25 +4054,6 @@ version = "0.45.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wl-clipboard-rs"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"os_pipe",
|
|
||||||
"rustix 0.38.44",
|
|
||||||
"tempfile",
|
|
||||||
"thiserror 2.0.16",
|
|
||||||
"tree_magic_mini",
|
|
||||||
"wayland-backend",
|
|
||||||
"wayland-client",
|
|
||||||
"wayland-protocols",
|
|
||||||
"wayland-protocols-wlr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,3 @@ tracing = { version = "0.1.41", features = ["attributes"] }
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
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"
|
|
||||||
|
|
|
||||||
18
README.md
Normal file
18
README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# clippyboard
|
||||||
|
|
||||||
|
clippyboard is a Wayland clipboard manager daemon and UI.
|
||||||
|
|
||||||
|
It provides a daemon that stores a clipboard history in memory and provides a socket to read and manage it.
|
||||||
|
A client program can then connect to it and read the contents and choose an item to copy to the clipboard again.
|
||||||
|
|
||||||
|
A barebones egui-based client is provided for doing this.
|
||||||
|
|
||||||
|
clippyboard provides first-class support for images!
|
||||||
|
|
||||||
|
clippyboard currently supports the following MIME types:
|
||||||
|
- `text/plain`
|
||||||
|
- `image/png`
|
||||||
|
- `image/jpg`
|
||||||
|
|
||||||
|
It will try to read out one of them (in descending preference) and store that value and provide it later.
|
||||||
|
If no supported MIME type is found, the clipboard entry is not stored.
|
||||||
490
src/daemon.rs
490
src/daemon.rs
|
|
@ -7,10 +7,7 @@ use eyre::bail;
|
||||||
use rustix::event::PollFd;
|
use rustix::event::PollFd;
|
||||||
use rustix::event::PollFlags;
|
use rustix::event::PollFlags;
|
||||||
use rustix::fs::OFlags;
|
use rustix::fs::OFlags;
|
||||||
use rustix::fs::fcntl_setfl;
|
|
||||||
use rustix::io::FdFlags;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::poll_fn;
|
|
||||||
use std::io::PipeReader;
|
use std::io::PipeReader;
|
||||||
use std::io::{BufReader, BufWriter, PipeWriter, Read, Write};
|
use std::io::{BufReader, BufWriter, PipeWriter, Read, Write};
|
||||||
use std::os::fd::AsFd;
|
use std::os::fd::AsFd;
|
||||||
|
|
@ -24,11 +21,9 @@ use tracing::info;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use wayland_client::EventQueue;
|
use wayland_client::EventQueue;
|
||||||
use wayland_client::protocol::wl_callback::WlCallback;
|
|
||||||
use wayland_client::protocol::wl_display::WlDisplay;
|
|
||||||
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_client::{Dispatch, Proxy, QueueHandle, backend::ObjectId, event_created_child};
|
use wayland_client::{Dispatch, Proxy, QueueHandle, event_created_child};
|
||||||
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;
|
||||||
use wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::{
|
use wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::{
|
||||||
EVT_DATA_OFFER_OPCODE, ExtDataControlDeviceV1,
|
EVT_DATA_OFFER_OPCODE, ExtDataControlDeviceV1,
|
||||||
|
|
@ -39,28 +34,20 @@ use wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1:
|
||||||
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;
|
||||||
use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1;
|
use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1;
|
||||||
|
|
||||||
|
const MIME_TYPES: &[&str] = &["text/plain", "image/png", "image/jpg"];
|
||||||
|
|
||||||
struct SharedState {
|
struct SharedState {
|
||||||
next_item_id: AtomicU64,
|
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>>,
|
items: Mutex<Vec<HistoryItem>>,
|
||||||
notify_write_send: PipeWriter,
|
notify_write_send: PipeWriter,
|
||||||
|
|
||||||
data_control_manager: OnceLock<ExtDataControlManagerV1>,
|
data_control_manager: OnceLock<ExtDataControlManagerV1>,
|
||||||
data_control_devices: Mutex<HashMap</*seat global name */ u32, ExtDataControlDeviceV1>>,
|
data_control_devices: Mutex<HashMap</*seat global name */ u32, ExtDataControlDeviceV1>>,
|
||||||
qh: QueueHandle<WlState>,
|
qh: QueueHandle<WlState>,
|
||||||
d: WlDisplay,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InProgressOffer {
|
struct InProgressOffer {
|
||||||
mime_types: HashSet<String>,
|
mime_types: Mutex<HashSet<String>>,
|
||||||
time: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct CurrentSelection {
|
|
||||||
mime_types: HashSet<String>,
|
|
||||||
offer: ExtDataControlOfferV1,
|
|
||||||
time: Duration,
|
time: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,23 +56,6 @@ struct WlState {
|
||||||
|
|
||||||
/// wl_seat that arrived before the data control manager so we weren't able to grab their device immediatly.
|
/// wl_seat that arrived before the data control manager so we weren't able to grab their device immediatly.
|
||||||
deferred_seats: Vec<WlSeat>,
|
deferred_seats: Vec<WlSeat>,
|
||||||
|
|
||||||
offers: HashMap<ObjectId, InProgressOffer>,
|
|
||||||
current_primary_selection: Option<CurrentSelection>,
|
|
||||||
current_selection: Option<CurrentSelection>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dispatch<WlCallback, String> for WlState {
|
|
||||||
fn event(
|
|
||||||
_state: &mut Self,
|
|
||||||
_proxy: &WlCallback,
|
|
||||||
_event: <WlCallback as Proxy>::Event,
|
|
||||||
data: &String,
|
|
||||||
_conn: &wayland_client::Connection,
|
|
||||||
_qhandle: &QueueHandle<Self>,
|
|
||||||
) {
|
|
||||||
info!("Received sync back {data}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dispatch<WlRegistry, ()> for WlState {
|
impl Dispatch<WlRegistry, ()> for WlState {
|
||||||
|
|
@ -163,6 +133,7 @@ impl Dispatch<ExtDataControlManagerV1, ()> for WlState {
|
||||||
_conn: &wayland_client::Connection,
|
_conn: &wayland_client::Connection,
|
||||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||||
) {
|
) {
|
||||||
|
// no events at the time of writing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Dispatch<WlSeat, ()> for WlState {
|
impl Dispatch<WlSeat, ()> for WlState {
|
||||||
|
|
@ -174,6 +145,7 @@ impl Dispatch<WlSeat, ()> for WlState {
|
||||||
_conn: &wayland_client::Connection,
|
_conn: &wayland_client::Connection,
|
||||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||||
) {
|
) {
|
||||||
|
// we don't care about anything about the seat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
||||||
|
|
@ -186,88 +158,49 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
||||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
// A new offer is being prepared, register it and don't do anything yet
|
ext_data_control_device_v1::Event::DataOffer { id: _ } => {
|
||||||
ext_data_control_device_v1::Event::DataOffer { id } => {
|
// A new offer is being prepared, we created the associated data in its creation and don't need to do anything
|
||||||
state.offers.insert(
|
|
||||||
id.id(),
|
|
||||||
InProgressOffer {
|
|
||||||
mime_types: Default::default(),
|
|
||||||
time: SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The selection has been confirmed, we just properly got a new offer that we should use.
|
// The selection has been confirmed, we just properly got a new offer that we should use.
|
||||||
ext_data_control_device_v1::Event::Selection { id } => {
|
ext_data_control_device_v1::Event::Selection { id } => {
|
||||||
let new_offer = match id {
|
if let Some(offer) = id {
|
||||||
Some(id) => {
|
let offer_data = offer
|
||||||
let offer = state.offers.remove(&id.id());
|
.data::<InProgressOffer>()
|
||||||
|
.expect("missing InProgressOffer data for ExtDataControlOfferV1");
|
||||||
|
|
||||||
offer.map(|offer| CurrentSelection {
|
let mime_types = offer_data.mime_types.lock().unwrap();
|
||||||
offer: id,
|
let Some(mime) = MIME_TYPES.iter().find(|mime| mime_types.contains(**mime))
|
||||||
mime_types: offer.mime_types,
|
|
||||||
time: offer.time,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(current) = &state.current_selection {
|
|
||||||
current.offer.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
state.current_selection = new_offer;
|
|
||||||
|
|
||||||
if let Some(offer) = state.current_selection.take() {
|
|
||||||
let Some(mime) = ["text/plain", "image/png"]
|
|
||||||
.iter()
|
|
||||||
.find(|mime| offer.mime_types.contains(**mime))
|
|
||||||
else {
|
else {
|
||||||
warn!(
|
warn!(
|
||||||
"No supported mime type found. Found mime types: {:?}",
|
"No supported mime type found. Found mime types: {:?}",
|
||||||
offer.mime_types
|
offer_data.mime_types
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
drop(mime_types);
|
||||||
|
|
||||||
let (reader, writer) = std::io::pipe().unwrap();
|
let (reader, writer) = std::io::pipe().unwrap();
|
||||||
offer.offer.receive(mime.to_string(), writer.as_fd());
|
offer.receive(mime.to_string(), writer.as_fd());
|
||||||
|
|
||||||
let history_state = state.shared_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_data.time;
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result =
|
let result = read_fd_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 {
|
||||||
warn!("Failed to read clipboard: {:?}", err)
|
warn!("Failed to read clipboard: {:?}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
offer.offer.destroy();
|
offer.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The offer has been confirmed to be a primary selection, do the necessary bookkeeping but we don't really care.
|
||||||
ext_data_control_device_v1::Event::PrimarySelection { id } => {
|
ext_data_control_device_v1::Event::PrimarySelection { id } => {
|
||||||
let new_offer = match id {
|
if let Some(id) = id {
|
||||||
Some(id) => {
|
id.destroy();
|
||||||
let offer = state.offers.remove(&id.id());
|
|
||||||
|
|
||||||
offer.map(|offer| CurrentSelection {
|
|
||||||
offer: id,
|
|
||||||
mime_types: offer.mime_types,
|
|
||||||
time: offer.time,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(current) = &state.current_primary_selection {
|
|
||||||
current.offer.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
state.current_primary_selection = new_offer;
|
|
||||||
}
|
}
|
||||||
ext_data_control_device_v1::Event::Finished => {
|
ext_data_control_device_v1::Event::Finished => {
|
||||||
warn!("device finished :(");
|
warn!("device finished :(");
|
||||||
|
|
@ -277,24 +210,27 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
||||||
}
|
}
|
||||||
|
|
||||||
event_created_child!(WlState, ExtDataControlDeviceV1, [
|
event_created_child!(WlState, ExtDataControlDeviceV1, [
|
||||||
EVT_DATA_OFFER_OPCODE => (ExtDataControlOfferV1, ()),
|
EVT_DATA_OFFER_OPCODE => (ExtDataControlOfferV1, InProgressOffer {
|
||||||
|
mime_types: Default::default(),
|
||||||
|
time: SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dispatch<ExtDataControlOfferV1, ()> for WlState {
|
impl Dispatch<ExtDataControlOfferV1, InProgressOffer> for WlState {
|
||||||
fn event(
|
fn event(
|
||||||
state: &mut Self,
|
_state: &mut Self,
|
||||||
proxy: &ExtDataControlOfferV1,
|
_proxy: &ExtDataControlOfferV1,
|
||||||
event: <ExtDataControlOfferV1 as wayland_client::Proxy>::Event,
|
event: <ExtDataControlOfferV1 as wayland_client::Proxy>::Event,
|
||||||
_data: &(),
|
data: &InProgressOffer,
|
||||||
_conn: &wayland_client::Connection,
|
_conn: &wayland_client::Connection,
|
||||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
ext_data_control_offer_v1::Event::Offer { mime_type } => {
|
ext_data_control_offer_v1::Event::Offer { mime_type } => {
|
||||||
if let Some(offer) = state.offers.get_mut(&proxy.id()) {
|
data.mime_types.lock().unwrap().insert(mime_type);
|
||||||
offer.mime_types.insert(mime_type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -312,8 +248,6 @@ impl Dispatch<ExtDataControlSourceV1, OfferData> for WlState {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
ext_data_control_source_v1::Event::Send { mime_type: _, fd } => {
|
ext_data_control_source_v1::Event::Send { mime_type: _, fd } => {
|
||||||
info!("pasting {:?}", std::str::from_utf8(&data.0));
|
|
||||||
|
|
||||||
let data = data.0.clone();
|
let data = data.0.clone();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|
@ -330,7 +264,6 @@ impl Dispatch<ExtDataControlSourceV1, OfferData> for WlState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ext_data_control_source_v1::Event::Cancelled => {
|
ext_data_control_source_v1::Event::Cancelled => {
|
||||||
info!("We have been replaced.");
|
|
||||||
proxy.destroy();
|
proxy.destroy();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -338,6 +271,164 @@ impl Dispatch<ExtDataControlSourceV1, OfferData> for WlState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn do_copy_into_clipboard(
|
||||||
|
entry: HistoryItem,
|
||||||
|
shared_state: &SharedState,
|
||||||
|
) -> Result<(), eyre::Error> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data_source.offer(entry.mime.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
device.1.set_selection(Some(&data_source));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_wayland(
|
||||||
|
mut queue: EventQueue<WlState>,
|
||||||
|
mut wl_state: WlState,
|
||||||
|
notify_write_recv: PipeReader,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
loop {
|
||||||
|
queue
|
||||||
|
.dispatch_pending(&mut wl_state)
|
||||||
|
.wrap_err("dispatching Wayland events")?;
|
||||||
|
|
||||||
|
let read_guard = queue
|
||||||
|
.prepare_read()
|
||||||
|
.wrap_err("preparing read from Wayland socket")?;
|
||||||
|
let _ = queue.flush();
|
||||||
|
|
||||||
|
let pollfd1_read = PollFd::from_borrowed_fd(read_guard.connection_fd(), PollFlags::IN);
|
||||||
|
let pollfd_signal = PollFd::from_borrowed_fd(notify_write_recv.as_fd(), PollFlags::IN);
|
||||||
|
|
||||||
|
let _ = rustix::event::poll(&mut [pollfd1_read, pollfd_signal], None);
|
||||||
|
|
||||||
|
read_guard
|
||||||
|
.read_without_dispatch()
|
||||||
|
.wrap_err("reading from wayland socket")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(peer, shared_state))]
|
||||||
|
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_READ => {
|
||||||
|
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_message(peer, shared_state).wrap_err("handling copy message")?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OfferData(Arc<[u8]>);
|
||||||
|
|
||||||
|
fn handle_copy_message(
|
||||||
|
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 = shared_state.items.lock().unwrap();
|
||||||
|
let Some(idx) = items.iter().position(|item| item.id == id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let item = items.remove(idx);
|
||||||
|
items.push(item.clone());
|
||||||
|
|
||||||
|
drop(items);
|
||||||
|
|
||||||
|
do_copy_into_clipboard(item, &shared_state).wrap_err("doing copy")?;
|
||||||
|
|
||||||
|
(&shared_state.notify_write_send)
|
||||||
|
.write_all(&[0])
|
||||||
|
.wrap_err("notifying wayland thread")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_fd_into_history(
|
||||||
|
history_state: &SharedState,
|
||||||
|
time: std::time::Duration,
|
||||||
|
mime: String,
|
||||||
|
data_reader: impl Read,
|
||||||
|
) -> Result<(), eyre::Error> {
|
||||||
|
let mut data_reader = BufReader::new(data_reader).take(MAX_ENTRY_SIZE);
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data_reader
|
||||||
|
.read_to_end(&mut data)
|
||||||
|
.wrap_err("reading content data")?;
|
||||||
|
let new_entry = HistoryItem {
|
||||||
|
id: history_state
|
||||||
|
.next_item_id
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||||
|
mime: mime.to_string(),
|
||||||
|
data: data.into(),
|
||||||
|
created_time: u64::try_from(time.as_millis()).unwrap(),
|
||||||
|
};
|
||||||
|
let mut items = history_state.items.lock().unwrap();
|
||||||
|
if items
|
||||||
|
.last()
|
||||||
|
.is_some_and(|last| last.mime == new_entry.mime && last.data == new_entry.data)
|
||||||
|
{
|
||||||
|
info!("INFO: Skipping store of new item because it is identical to last one");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(new_entry);
|
||||||
|
let mut running_total = 0;
|
||||||
|
let mut cutoff = None;
|
||||||
|
for (idx, item) in items.iter().enumerate().rev() {
|
||||||
|
running_total += item.data.len() + std::mem::size_of::<HistoryItem>();
|
||||||
|
if running_total > crate::MAX_HISTORY_BYTE_SIZE {
|
||||||
|
cutoff = Some(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(cutoff) = cutoff {
|
||||||
|
info!(
|
||||||
|
"Dropping old {} items because limit of {} bytes was reached for the history",
|
||||||
|
cutoff + 1,
|
||||||
|
crate::MAX_HISTORY_BYTE_SIZE
|
||||||
|
);
|
||||||
|
items.splice(0..=cutoff, []);
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
|
pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info")))
|
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info")))
|
||||||
|
|
@ -356,29 +447,17 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
|
||||||
|
|
||||||
let shared_state = Arc::new(SharedState {
|
let shared_state = Arc::new(SharedState {
|
||||||
next_item_id: AtomicU64::new(0),
|
next_item_id: AtomicU64::new(0),
|
||||||
last_copied_item_id: AtomicU64::new(u64::MAX),
|
|
||||||
items: Mutex::new(Vec::<HistoryItem>::new()),
|
items: Mutex::new(Vec::<HistoryItem>::new()),
|
||||||
notify_write_send,
|
notify_write_send,
|
||||||
|
|
||||||
data_control_manager: OnceLock::new(),
|
data_control_manager: OnceLock::new(),
|
||||||
data_control_devices: Mutex::new(HashMap::new()),
|
data_control_devices: Mutex::new(HashMap::new()),
|
||||||
qh: queue.handle(),
|
qh: queue.handle(),
|
||||||
d: conn.display(),
|
|
||||||
});
|
|
||||||
|
|
||||||
shared_state.items.lock().unwrap().push(HistoryItem {
|
|
||||||
id: 3548235782,
|
|
||||||
mime: "text/plain".into(),
|
|
||||||
data: b"meow".to_vec().into(),
|
|
||||||
created_time: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let history_state2 = shared_state.clone();
|
let history_state2 = shared_state.clone();
|
||||||
|
|
||||||
let mut wl_state = WlState {
|
let mut wl_state = WlState {
|
||||||
offers: HashMap::new(),
|
|
||||||
current_primary_selection: None,
|
|
||||||
current_selection: None,
|
|
||||||
deferred_seats: Vec::new(),
|
deferred_seats: Vec::new(),
|
||||||
|
|
||||||
shared_state: history_state2,
|
shared_state: history_state2,
|
||||||
|
|
@ -430,188 +509,3 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_wayland(
|
|
||||||
mut queue: EventQueue<WlState>,
|
|
||||||
mut wl_state: WlState,
|
|
||||||
notify_write_recv: PipeReader,
|
|
||||||
) -> eyre::Result<()> {
|
|
||||||
loop {
|
|
||||||
queue
|
|
||||||
.dispatch_pending(&mut wl_state)
|
|
||||||
.wrap_err("dispatching Wayland events")?;
|
|
||||||
|
|
||||||
let read_guard = queue
|
|
||||||
.prepare_read()
|
|
||||||
.wrap_err("preparing read from Wayland socket")?;
|
|
||||||
let _ = queue.flush();
|
|
||||||
|
|
||||||
let pollfd1_read = PollFd::from_borrowed_fd(read_guard.connection_fd(), PollFlags::IN);
|
|
||||||
let pollfd_signal = PollFd::from_borrowed_fd(notify_write_recv.as_fd(), PollFlags::IN);
|
|
||||||
|
|
||||||
let _ = rustix::event::poll(&mut [pollfd1_read, pollfd_signal], None);
|
|
||||||
|
|
||||||
read_guard
|
|
||||||
.read_without_dispatch()
|
|
||||||
.wrap_err("reading from wayland socket")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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_READ => {
|
|
||||||
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, shared_state).wrap_err("handling copy message")?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = shared_state.items.lock().unwrap();
|
|
||||||
let Some(idx) = items.iter().position(|item| item.id == id) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let item = items.remove(idx);
|
|
||||||
items.push(item.clone());
|
|
||||||
|
|
||||||
drop(items);
|
|
||||||
|
|
||||||
do_copy_into_clipboard(item, &shared_state).wrap_err("doing copy")?;
|
|
||||||
|
|
||||||
(&shared_state.notify_write_send)
|
|
||||||
.write_all(&[0])
|
|
||||||
.wrap_err("notifying wayland thread")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_copy_into_clipboard(
|
|
||||||
entry: HistoryItem,
|
|
||||||
shared_state: &SharedState,
|
|
||||||
) -> Result<(), eyre::Error> {
|
|
||||||
for device in &*shared_state.data_control_devices.lock().unwrap() {
|
|
||||||
shared_state
|
|
||||||
.d
|
|
||||||
.sync(&shared_state.qh, "before create_data_source".into());
|
|
||||||
let data_source = shared_state
|
|
||||||
.data_control_manager
|
|
||||||
.get()
|
|
||||||
.expect("data manger not found")
|
|
||||||
.create_data_source(&shared_state.qh, OfferData(entry.data.clone()));
|
|
||||||
|
|
||||||
shared_state
|
|
||||||
.d
|
|
||||||
.sync(&shared_state.qh, "after create_data_source".into());
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data_source.offer(entry.mime.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
shared_state
|
|
||||||
.d
|
|
||||||
.sync(&shared_state.qh, "before set_selection".into());
|
|
||||||
|
|
||||||
info!("setting the selection");
|
|
||||||
|
|
||||||
device.1.set_selection(Some(&data_source));
|
|
||||||
|
|
||||||
shared_state
|
|
||||||
.d
|
|
||||||
.sync(&shared_state.qh, "setting the selection".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
shared_state
|
|
||||||
.last_copied_item_id
|
|
||||||
.store(entry.id, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_read_clipboard_into_history(
|
|
||||||
history_state: &SharedState,
|
|
||||||
time: std::time::Duration,
|
|
||||||
mime: String,
|
|
||||||
data_reader: impl Read,
|
|
||||||
) -> Result<(), eyre::Error> {
|
|
||||||
let mut data_reader = BufReader::new(data_reader).take(MAX_ENTRY_SIZE);
|
|
||||||
let mut data = Vec::new();
|
|
||||||
data_reader
|
|
||||||
.read_to_end(&mut data)
|
|
||||||
.wrap_err("reading content data")?;
|
|
||||||
let new_entry = HistoryItem {
|
|
||||||
id: history_state
|
|
||||||
.next_item_id
|
|
||||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
|
||||||
mime: mime.to_string(),
|
|
||||||
data: data.into(),
|
|
||||||
created_time: u64::try_from(time.as_millis()).unwrap(),
|
|
||||||
};
|
|
||||||
let mut items = history_state.items.lock().unwrap();
|
|
||||||
if items
|
|
||||||
.last()
|
|
||||||
.is_some_and(|last| last.mime == new_entry.mime && last.data == new_entry.data)
|
|
||||||
{
|
|
||||||
info!("INFO: Skipping store of new item because it is identical to last one");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let last_copied = history_state
|
|
||||||
.last_copied_item_id
|
|
||||||
.load(std::sync::atomic::Ordering::Relaxed);
|
|
||||||
if let Some(item) = items.iter().find(|item| item.id == last_copied)
|
|
||||||
&& item.mime == new_entry.mime
|
|
||||||
&& item.data == new_entry.data
|
|
||||||
{
|
|
||||||
info!("Skipping store of new item because the copy came from us");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
items.push(new_entry);
|
|
||||||
let mut running_total = 0;
|
|
||||||
let mut cutoff = None;
|
|
||||||
for (idx, item) in items.iter().enumerate().rev() {
|
|
||||||
running_total += item.data.len() + std::mem::size_of::<HistoryItem>();
|
|
||||||
if running_total > crate::MAX_HISTORY_BYTE_SIZE {
|
|
||||||
cutoff = Some(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(cutoff) = cutoff {
|
|
||||||
info!(
|
|
||||||
"Dropping old {} items because limit of {} bytes was reached for the history",
|
|
||||||
cutoff + 1,
|
|
||||||
crate::MAX_HISTORY_BYTE_SIZE
|
|
||||||
);
|
|
||||||
items.splice(0..=cutoff, []);
|
|
||||||
}
|
|
||||||
info!(
|
|
||||||
"Successfully stored clipboard value of mime type {mime} (new history size {running_total})"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue