wayland: attach clipboard devices for seats surfaced via capabilities

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I127da48080483de626931a1038f3c38d6a6a6964
This commit is contained in:
raf 2026-06-25 16:20:56 +03:00
commit ce24da6bc1
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
2 changed files with 29 additions and 7 deletions

View file

@ -92,14 +92,8 @@ impl SeatHandler for App {
fn new_seat(&mut self, _: &Connection, qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) { fn new_seat(&mut self, _: &Connection, qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
// Clipboard/primary devices are seat-scoped, not capability-scoped. // Clipboard/primary devices are seat-scoped, not capability-scoped.
let data_device = Some(self.data_device_manager.get_data_device(qh, &seat));
let primary_device = self
.primary_manager
.as_ref()
.map(|m| m.get_selection_device(qh, &seat));
let i = self.seat_index(&seat); let i = self.seat_index(&seat);
self.seats[i].data_device = data_device; self.ensure_clipboard_devices(qh, &seat, i);
self.seats[i].primary_device = primary_device;
} }
fn new_capability( fn new_capability(
@ -110,6 +104,9 @@ impl SeatHandler for App {
capability: Capability, capability: Capability,
) { ) {
let i = self.seat_index(&seat); let i = self.seat_index(&seat);
// A pre-existing seat may reach us via its capabilities without a
// `new_seat` call, so make sure the clipboard devices are attached.
self.ensure_clipboard_devices(qh, &seat, i);
if capability == Capability::Keyboard && self.seats[i].keyboard.is_none() { if capability == Capability::Keyboard && self.seats[i].keyboard.is_none() {
// get_keyboard_with_repeat drives key repeat off a calloop timer and // get_keyboard_with_repeat drives key repeat off a calloop timer and
// delivers each repeat through the callback. // delivers each repeat through the callback.

View file

@ -1130,6 +1130,28 @@ impl App {
self.seats.get(self.active_seat) self.seats.get(self.active_seat)
} }
/// Attach the seat-scoped clipboard and primary-selection devices to seat
/// `i` if not already present. Idempotent, so it can run from either
/// `new_seat` or `new_capability` - some compositors surface a pre-existing
/// seat's capabilities without ever firing `new_seat`.
fn ensure_clipboard_devices(
&mut self,
qh: &QueueHandle<Self>,
seat: &wl_seat::WlSeat,
i: usize,
) {
if self.seats[i].data_device.is_none() {
let dd = self.data_device_manager.get_data_device(qh, seat);
self.seats[i].data_device = Some(dd);
}
if self.seats[i].primary_device.is_none()
&& let Some(m) = self.primary_manager.as_ref()
{
let pd = m.get_selection_device(qh, seat);
self.seats[i].primary_device = Some(pd);
}
}
/// The active seat's clipboard device, if any. /// The active seat's clipboard device, if any.
fn data_device(&self) -> Option<&DataDevice> { fn data_device(&self) -> Option<&DataDevice> {
self.active().and_then(|s| s.data_device.as_ref()) self.active().and_then(|s| s.data_device.as_ref())
@ -1509,9 +1531,11 @@ impl App {
/// Paste the CLIPBOARD selection into the shell (Ctrl+Shift+V). /// Paste the CLIPBOARD selection into the shell (Ctrl+Shift+V).
fn paste_clipboard(&mut self) { fn paste_clipboard(&mut self) {
let Some(offer) = self.data_device().and_then(|d| d.data().selection_offer()) else { let Some(offer) = self.data_device().and_then(|d| d.data().selection_offer()) else {
tracing::debug!("paste: no clipboard selection offer");
return; return;
}; };
let Some(mime) = offer.with_mime_types(pick_mime) else { let Some(mime) = offer.with_mime_types(pick_mime) else {
tracing::debug!("paste: no acceptable text mime type offered");
return; return;
}; };
if let Ok(pipe) = offer.receive(mime) { if let Ok(pipe) = offer.receive(mime) {
@ -1547,6 +1571,7 @@ impl App {
let mut tmp = [0u8; 4096]; let mut tmp = [0u8; 4096];
match f.read(&mut tmp) { match f.read(&mut tmp) {
Ok(0) => { Ok(0) => {
tracing::debug!("paste: read {} bytes from clipboard", data.len());
app.paste_bytes(&data); app.paste_bytes(&data);
PostAction::Remove PostAction::Remove
} }