mirror of
https://github.com/Noratrieb/clippyboard.git
synced 2026-01-14 09:55: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",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wl-clipboard-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1123,12 +1122,6 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
|
|
@ -1804,12 +1797,6 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -1913,16 +1900,6 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
|
|
@ -2284,16 +2261,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
|
|
@ -2344,16 +2311,6 @@ version = "2.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
|
|
@ -3127,18 +3084,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
|
|
@ -4109,25 +4054,6 @@ version = "0.45.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
|
|
|
|||
|
|
@ -15,4 +15,3 @@ 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"
|
||||
|
|
|
|||
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::PollFlags;
|
||||
use rustix::fs::OFlags;
|
||||
use rustix::fs::fcntl_setfl;
|
||||
use rustix::io::FdFlags;
|
||||
use std::collections::HashMap;
|
||||
use std::future::poll_fn;
|
||||
use std::io::PipeReader;
|
||||
use std::io::{BufReader, BufWriter, PipeWriter, Read, Write};
|
||||
use std::os::fd::AsFd;
|
||||
|
|
@ -24,11 +21,9 @@ use tracing::info;
|
|||
use tracing::warn;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
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_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::{
|
||||
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::ExtDataControlSourceV1;
|
||||
|
||||
const MIME_TYPES: &[&str] = &["text/plain", "image/png", "image/jpg"];
|
||||
|
||||
struct SharedState {
|
||||
next_item_id: AtomicU64,
|
||||
// for deduplication because the event stream will tell us that we just copied something :)
|
||||
last_copied_item_id: AtomicU64,
|
||||
items: Mutex<Vec<HistoryItem>>,
|
||||
notify_write_send: PipeWriter,
|
||||
|
||||
data_control_manager: OnceLock<ExtDataControlManagerV1>,
|
||||
data_control_devices: Mutex<HashMap</*seat global name */ u32, ExtDataControlDeviceV1>>,
|
||||
qh: QueueHandle<WlState>,
|
||||
d: WlDisplay,
|
||||
}
|
||||
|
||||
struct InProgressOffer {
|
||||
mime_types: HashSet<String>,
|
||||
time: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CurrentSelection {
|
||||
mime_types: HashSet<String>,
|
||||
offer: ExtDataControlOfferV1,
|
||||
mime_types: Mutex<HashSet<String>>,
|
||||
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.
|
||||
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 {
|
||||
|
|
@ -163,6 +133,7 @@ impl Dispatch<ExtDataControlManagerV1, ()> for WlState {
|
|||
_conn: &wayland_client::Connection,
|
||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||
) {
|
||||
// no events at the time of writing
|
||||
}
|
||||
}
|
||||
impl Dispatch<WlSeat, ()> for WlState {
|
||||
|
|
@ -174,6 +145,7 @@ impl Dispatch<WlSeat, ()> for WlState {
|
|||
_conn: &wayland_client::Connection,
|
||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||
) {
|
||||
// we don't care about anything about the seat
|
||||
}
|
||||
}
|
||||
impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
||||
|
|
@ -186,88 +158,49 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
|||
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
// A new offer is being prepared, register it and don't do anything yet
|
||||
ext_data_control_device_v1::Event::DataOffer { id } => {
|
||||
state.offers.insert(
|
||||
id.id(),
|
||||
InProgressOffer {
|
||||
mime_types: Default::default(),
|
||||
time: SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap(),
|
||||
},
|
||||
);
|
||||
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
|
||||
}
|
||||
|
||||
// The selection has been confirmed, we just properly got a new offer that we should use.
|
||||
ext_data_control_device_v1::Event::Selection { id } => {
|
||||
let new_offer = match id {
|
||||
Some(id) => {
|
||||
let offer = state.offers.remove(&id.id());
|
||||
if let Some(offer) = id {
|
||||
let offer_data = offer
|
||||
.data::<InProgressOffer>()
|
||||
.expect("missing InProgressOffer data for ExtDataControlOfferV1");
|
||||
|
||||
offer.map(|offer| CurrentSelection {
|
||||
offer: id,
|
||||
mime_types: offer.mime_types,
|
||||
time: offer.time,
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(current) = &state.current_selection {
|
||||
current.offer.destroy();
|
||||
}
|
||||
|
||||
state.current_selection = new_offer;
|
||||
|
||||
if let Some(offer) = state.current_selection.take() {
|
||||
let Some(mime) = ["text/plain", "image/png"]
|
||||
.iter()
|
||||
.find(|mime| offer.mime_types.contains(**mime))
|
||||
let mime_types = offer_data.mime_types.lock().unwrap();
|
||||
let Some(mime) = MIME_TYPES.iter().find(|mime| mime_types.contains(**mime))
|
||||
else {
|
||||
warn!(
|
||||
"No supported mime type found. Found mime types: {:?}",
|
||||
offer.mime_types
|
||||
offer_data.mime_types
|
||||
);
|
||||
return;
|
||||
};
|
||||
drop(mime_types);
|
||||
|
||||
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 mime = mime.to_string();
|
||||
let time = offer.time;
|
||||
let time = offer_data.time;
|
||||
std::thread::spawn(move || {
|
||||
let result =
|
||||
do_read_clipboard_into_history(&history_state, time, mime, reader);
|
||||
let result = read_fd_into_history(&history_state, time, mime, reader);
|
||||
if let Err(err) = result {
|
||||
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 } => {
|
||||
let new_offer = match id {
|
||||
Some(id) => {
|
||||
let offer = state.offers.remove(&id.id());
|
||||
|
||||
offer.map(|offer| CurrentSelection {
|
||||
offer: id,
|
||||
mime_types: offer.mime_types,
|
||||
time: offer.time,
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(current) = &state.current_primary_selection {
|
||||
current.offer.destroy();
|
||||
if let Some(id) = id {
|
||||
id.destroy();
|
||||
}
|
||||
|
||||
state.current_primary_selection = new_offer;
|
||||
}
|
||||
ext_data_control_device_v1::Event::Finished => {
|
||||
warn!("device finished :(");
|
||||
|
|
@ -277,24 +210,27 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for WlState {
|
|||
}
|
||||
|
||||
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(
|
||||
state: &mut Self,
|
||||
proxy: &ExtDataControlOfferV1,
|
||||
_state: &mut Self,
|
||||
_proxy: &ExtDataControlOfferV1,
|
||||
event: <ExtDataControlOfferV1 as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
data: &InProgressOffer,
|
||||
_conn: &wayland_client::Connection,
|
||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
ext_data_control_offer_v1::Event::Offer { mime_type } => {
|
||||
if let Some(offer) = state.offers.get_mut(&proxy.id()) {
|
||||
offer.mime_types.insert(mime_type);
|
||||
}
|
||||
data.mime_types.lock().unwrap().insert(mime_type);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -312,8 +248,6 @@ impl Dispatch<ExtDataControlSourceV1, OfferData> for WlState {
|
|||
) {
|
||||
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 || {
|
||||
|
|
@ -330,7 +264,6 @@ impl Dispatch<ExtDataControlSourceV1, OfferData> for WlState {
|
|||
});
|
||||
}
|
||||
ext_data_control_source_v1::Event::Cancelled => {
|
||||
info!("We have been replaced.");
|
||||
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<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.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 {
|
||||
next_item_id: AtomicU64::new(0),
|
||||
last_copied_item_id: AtomicU64::new(u64::MAX),
|
||||
items: Mutex::new(Vec::<HistoryItem>::new()),
|
||||
notify_write_send,
|
||||
|
||||
data_control_manager: OnceLock::new(),
|
||||
data_control_devices: Mutex::new(HashMap::new()),
|
||||
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 mut wl_state = WlState {
|
||||
offers: HashMap::new(),
|
||||
current_primary_selection: None,
|
||||
current_selection: None,
|
||||
deferred_seats: Vec::new(),
|
||||
|
||||
shared_state: history_state2,
|
||||
|
|
@ -430,188 +509,3 @@ pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
|
|||
|
||||
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