use super::{
ClipboardContent, ClipboardRequestId, Delivery, MimeType, Read, ReceivedMime, ShellEvent, Step,
Write, X11Shell, ffi, read_wire, selection, window,
};
impl X11Shell {
pub(super) fn handle_selection_clear(&mut self, raw: &[u8]) {
let Some(event) = read_wire::<ffi::SelectionClearEvent>(raw) else {
return;
};
if event.selection != self.conn.atoms.clipboard {
return;
}
self.offers.clear();
self.owner_window = 0;
self.writes.clear();
}
pub(super) fn handle_selection_request(&mut self, raw: &[u8]) {
let Some(event) = read_wire::<ffi::SelectionRequestEvent>(raw) else {
return;
};
let property = self.answer_selection_request(&event);
let notify = ffi::SelectionNotifyEvent {
response_type: ffi::event::SELECTION_NOTIFY,
pad0: 0,
sequence: 0,
time: event.time,
requestor: event.requestor,
selection: event.selection,
target: event.target,
property,
pad1: [0; 8],
};
self.conn.send_event(event.requestor, 0, ¬ify);
self.conn.flush();
}
fn answer_selection_request(&mut self, event: &ffi::SelectionRequestEvent) -> u32 {
if event.selection != self.conn.atoms.clipboard || self.offers.is_empty() {
return 0;
}
let property = if event.property == 0 {
event.target
} else {
event.property
};
if event.target == self.conn.atoms.multiple {
return self.answer_multiple(event);
}
if self.convert_target(event.requestor, event.target, property) {
property
} else {
0
}
}
fn answer_multiple(&mut self, event: &ffi::SelectionRequestEvent) -> u32 {
if event.property == 0 {
return 0;
}
let mut pairs = self
.conn
.get_property_words(event.requestor, event.property);
if pairs.is_empty() || !pairs.len().is_multiple_of(2) {
return 0;
}
for pair in pairs.as_chunks_mut::<2>().0 {
let (target, into) = (pair[0], pair[1]);
let converted = target != self.conn.atoms.multiple
&& into != 0
&& self.convert_target(event.requestor, target, into);
if !converted {
pair[1] = 0;
}
}
self.conn.set_property(
event.requestor,
event.property,
self.conn.atoms.atom_pair,
32,
&window::words_to_bytes(&pairs),
);
event.property
}
fn convert_target(&mut self, requestor: u32, target: u32, property: u32) -> bool {
if target == self.conn.atoms.timestamp {
let acquired = [self.owner_time];
self.conn.set_property(
requestor,
property,
ffi::value::ATOM_INTEGER,
32,
&window::words_to_bytes(&acquired),
);
return true;
}
if target == self.conn.atoms.targets {
let targets = self.advertised_targets();
self.conn.set_property(
requestor,
property,
ffi::value::ATOM_ATOM,
32,
&window::words_to_bytes(&targets),
);
return true;
}
let Some(mime) = self.conn.atoms.mime_for_target(target) else {
return false;
};
let Some((_, bytes)) = self.offers.iter().find(|(offered, _)| *offered == mime) else {
return false;
};
if bytes.len() <= self.conn.max_property_bytes {
self.conn
.set_property(requestor, property, target, 8, bytes);
return true;
}
if self.writes.len() >= selection::MAX_PENDING_WRITES {
crcbl_core::log::warn!(
"a requestor has more INCR clipboard transfers open than this \
client will feed at once; the newest was refused"
);
return false;
}
let estimate = [u32::try_from(bytes.len()).unwrap_or(u32::MAX)];
self.conn.set_property(
requestor,
property,
self.conn.atoms.incr,
32,
&window::words_to_bytes(&estimate),
);
self.select_property_changes(requestor);
self.writes.push(Write::new(
requestor,
property,
target,
bytes.clone(),
self.conn.max_property_bytes,
ffi::monotonic_nanos(),
));
true
}
fn advertised_targets(&self) -> Vec<u32> {
let mut targets = vec![
self.conn.atoms.targets,
self.conn.atoms.timestamp,
self.conn.atoms.multiple,
];
for (mime, _) in &self.offers {
if *mime == MimeType::TextUtf8 {
targets.extend_from_slice(&self.conn.atoms.text_targets());
} else {
let atom = self.conn.atoms.for_mime(*mime);
if atom != 0 {
targets.push(atom);
}
}
}
let mut unique = Vec::with_capacity(targets.len());
for atom in targets {
if atom != 0 && !unique.contains(&atom) {
unique.push(atom);
}
}
unique
}
pub(super) fn handle_selection_notify(&mut self, raw: &[u8]) {
let Some(event) = read_wire::<ffi::SelectionNotifyEvent>(raw) else {
return;
};
let is_targets = event.target == self.conn.atoms.targets;
let Some(index) = self.reads.iter().position(|read| {
read.selection == event.selection
&& self
.windows
.get(read.window.cast())
.is_some_and(|window| window.id == event.requestor)
&& (read.state == selection::ReadState::AwaitingTargets) == is_targets
}) else {
return;
};
let now = ffi::monotonic_nanos();
let xid = event.requestor;
let value = (event.property != 0)
.then(|| self.conn.get_property(xid, event.property))
.flatten()
.filter(|(type_, _, _)| *type_ != 0);
if is_targets {
let offered: Vec<u32> = value.map_or_else(Vec::new, |(_, format, bytes)| {
if format == 32 {
bytes
.as_chunks::<4>()
.0
.iter()
.map(|word| u32::from_ne_bytes([word[0], word[1], word[2], word[3]]))
.collect()
} else {
Vec::new()
}
});
let step = self.reads[index].on_targets(&offered, now);
self.settle(index, step);
return;
}
let (is_incr, bytes) = match value {
Some((type_, _, bytes)) => (type_ == self.conn.atoms.incr, bytes),
None => {
let step = self.reads[index].on_selection_notify(0, event.target, false, &[], now);
self.settle(index, step);
return;
}
};
let step = self.reads[index].on_selection_notify(
event.property,
event.target,
is_incr,
&bytes,
now,
);
self.settle(index, step);
}
pub(super) fn handle_property_notify(&mut self, raw: &[u8]) {
const NEW_VALUE: u8 = 0;
const DELETED: u8 = 1;
let Some(event) = read_wire::<ffi::PropertyNotifyEvent>(raw) else {
return;
};
self.last_server_time = event.time;
if event.atom == self.conn.atoms.crcbl_timestamp {
self.timestamp_probe_seen = true;
}
if event.window == self.conn.root && event.atom == self.conn.atoms.net_workarea {
self.handle_monitors_changed();
return;
}
if event.atom == self.conn.atoms.net_wm_state
&& let Some(window) = self.window_by_xid(event.window)
{
self.refresh_effective_mode(window);
return;
}
if event.state == NEW_VALUE
&& let Some(index) = self.reads.iter().position(|read| {
read.state == selection::ReadState::Incremental
&& read.property == event.atom
&& self
.windows
.get(read.window.cast())
.is_some_and(|window| window.id == event.window)
})
{
let bytes = match self.conn.get_property(event.window, event.atom) {
Some((_, _, bytes)) => bytes,
None => return,
};
let step = self.reads[index].on_chunk(&bytes, ffi::monotonic_nanos());
self.settle(index, step);
return;
}
if event.state == DELETED
&& let Some(index) = self
.writes
.iter()
.position(|write| write.requestor == event.window && write.property == event.atom)
{
self.push_chunk(index);
}
}
fn push_chunk(&mut self, index: usize) {
let now = ffi::monotonic_nanos();
let Some(write) = self.writes.get_mut(index) else {
return;
};
let (requestor, property, target) = (write.requestor, write.property, write.target);
let Some(chunk) = write.next_chunk(now) else {
self.writes.remove(index);
return;
};
let finished = chunk.is_empty();
let chunk = chunk.to_vec();
self.conn
.set_property(requestor, property, target, 8, &chunk);
self.conn.flush();
if finished {
self.writes.remove(index);
}
}
fn settle(&mut self, index: usize, step: Step) {
match step {
Step::AcknowledgeChunk => {
let (xid, property) = {
let read = &self.reads[index];
let xid = self
.windows
.get(read.window.cast())
.map_or(0, |window| window.id);
(xid, read.property)
};
if xid != 0 {
self.conn.delete_property(xid, property);
self.conn.flush();
}
}
Step::Convert(target) => {
let (xid, property) = {
let read = &self.reads[index];
let xid = self
.windows
.get(read.window.cast())
.map_or(0, |window| window.id);
(xid, read.property)
};
if xid == 0 {
let read = self.reads.remove(index);
self.finish_read(&read, ClipboardContent::Unavailable);
return;
}
let selection = self.reads[index].selection;
self.convert(xid, selection, target, property, self.last_server_time);
}
Step::Done(content) => {
let read = self.reads.remove(index);
self.finish_read(&read, content);
}
}
}
fn finish_read(&mut self, read: &Read, content: ClipboardContent) {
let (request, mime) = match read.delivery {
Delivery::Clipboard { request, mime } => (request, mime),
Delivery::Drop { .. } => {
self.finish_drop(read.window, &read.delivery, &content);
return;
}
};
let mime = match content {
ClipboardContent::Bytes(_) => self
.conn
.atoms
.spelling_of(read.answered_target)
.map_or_else(|| ReceivedMime::from(mime), ReceivedMime::new),
ClipboardContent::Empty | ClipboardContent::Unavailable => ReceivedMime::from(mime),
};
self.queue.push_back(ShellEvent::ClipboardData {
window: read.window,
request,
mime,
content,
});
}
pub(super) fn start_read(
&mut self,
window: super::WindowId,
mime: MimeType,
) -> ClipboardRequestId {
let request = ClipboardRequestId(self.next_request_id);
self.next_request_id += 1;
let xid = self.window(window).map_or(0, |state| state.id);
let candidates = self.conn.atoms.candidates_for(mime);
if xid == 0 || candidates.is_empty() || self.conn.selection_owner() == 0 {
self.queue.push_back(ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::from(mime),
content: ClipboardContent::Empty,
});
return request;
}
let property = self.conn.atoms.crcbl_selection;
let selection = self.conn.atoms.clipboard;
self.convert(
xid,
selection,
self.conn.atoms.targets,
property,
self.last_server_time,
);
self.reads.push(Read::clipboard(
request,
window,
mime,
candidates,
selection,
property,
ffi::monotonic_nanos(),
));
request
}
pub(super) fn convert(&self, xid: u32, selection: u32, target: u32, property: u32, time: u32) {
self.conn.delete_property(xid, property);
unsafe {
(self.conn.lib.convert_selection)(
self.conn.raw(),
xid,
selection,
target,
property,
time,
);
}
self.conn.flush();
}
pub(super) fn refresh_server_time(&mut self, xid: u32) {
const DEADLINE: core::time::Duration = core::time::Duration::from_millis(250);
self.timestamp_probe_seen = false;
unsafe {
(self.conn.lib.change_property)(
self.conn.raw(),
ffi::value::PROP_MODE_APPEND,
xid,
self.conn.atoms.crcbl_timestamp,
ffi::value::ATOM_STRING,
8,
0,
core::ptr::null(),
);
}
self.conn.flush();
let deadline =
ffi::monotonic_nanos() + u64::try_from(DEADLINE.as_nanos()).unwrap_or(u64::MAX);
loop {
self.drain();
if self.timestamp_probe_seen || self.lost.is_some() {
return;
}
if ffi::monotonic_nanos() >= deadline {
crcbl_core::log::warn!(
"the X server did not answer a timestamp probe; using CurrentTime"
);
return;
}
let _ = ffi::poll_readable(self.conn.fd, 5);
}
}
pub(super) fn service_transfers(&mut self) {
let now = ffi::monotonic_nanos();
let mut index = 0;
while index < self.reads.len() {
if self.reads[index].is_stalled(now) {
let read = self.reads.remove(index);
crcbl_core::log::warn!(
"an X11 selection transfer for {:?} stalled for {:?}; giving up on it",
read.delivery,
selection::TIMEOUT
);
self.finish_read(&read, ClipboardContent::Unavailable);
continue;
}
index += 1;
}
self.writes.retain(|write| !write.is_stalled(now));
}
pub(super) fn select_property_changes(&self, xid: u32) {
if self.window_by_xid(xid).is_some() {
return;
}
let values = [ffi::event_mask::PROPERTY_CHANGE];
unsafe {
(self.conn.lib.change_window_attributes)(
self.conn.raw(),
xid,
ffi::cw::EVENT_MASK,
values.as_ptr().cast::<core::ffi::c_void>(),
);
}
}
}
impl super::Conn {
pub(super) fn selection_owner(&self) -> u32 {
let reply = unsafe {
let cookie = (self.lib.get_selection_owner)(self.raw(), self.atoms.clipboard);
(self.lib.get_selection_owner_reply)(self.raw(), cookie, core::ptr::null_mut())
};
if reply.is_null() {
return 0;
}
let owner = unsafe { (*reply).owner };
unsafe { ffi::free_reply(reply) };
owner
}
pub(super) fn set_selection_owner(&self, owner: u32, time: u32) -> bool {
unsafe { (self.lib.set_selection_owner)(self.raw(), owner, self.atoms.clipboard, time) };
self.flush();
self.selection_owner() == owner
}
}