Commit 8c987154f02d
tarball2 files changed+82-1
@@ -53,7 +53,7 @@ impl SelectionHandler for State {
5353
};5454
*x.wayland_selection(ty) = mimes.clone();5555
if let Some(wm) = x.wm.as_mut()56
- && let Err(e) = wm.new_selection(ty, mimes)56
+ && let Err(e) = wm.new_selection(ty, mimes.map(crate::xwayland::with_string_target))5757
{5858
tracing::warn!("cannot set the X11 {ty:?} selection: {e:?}");5959
}@@ -9,6 +9,7 @@ use monolith_config::SelectionAccess;
99
use smithay::desktop::Window;1010
use smithay::reexports::calloop::generic::Generic;1111
use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};12
+use smithay::reexports::rustix::fs::{OFlags, fcntl_setfl};1213
use smithay::reexports::rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};1314
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;1415
use smithay::utils::{Logical, Rectangle, Size};@@ -24,12 +25,54 @@ use smithay::wayland::selection::primary_selection::{
2425
use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};2526
use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowProperty, WmWindowType, XwmId};2627
use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};28
+use std::io::{Read, Write};2729
use std::os::fd::AsFd;2830
use std::os::fd::OwnedFd;2931
use std::os::linux::net::SocketAddrExt;3032
use std::os::unix::net::{SocketAddr, UnixListener};3133
use std::process::Stdio;3234
35
+const UTF8_TEXT: [&str; 4] = [36
+ "text/plain;charset=utf-8",37
+ "text/plain;charset=UTF-8",38
+ "UTF8_STRING",39
+ "text/plain",40
+];41
+42
+fn utf8_source(mimes: &[String]) -> Option<&str> {43
+ UTF8_TEXT.into_iter().find(|t| mimes.iter().any(|m| m == t))44
+}45
+46
+/// X11 requestors that predate UTF8_STRING only ask for STRING, which smithay47
+/// leaves out of the targets it offers for a Wayland-owned text selection.48
+pub fn with_string_target(mut mimes: Vec<String>) -> Vec<String> {49
+ if utf8_source(&mimes).is_some() && !mimes.iter().any(|m| m == "STRING") {50
+ mimes.push("STRING".into());51
+ }52
+ mimes53
+}54
+55
+fn latin1(text: &[u8]) -> Vec<u8> {56
+ String::from_utf8_lossy(text)57
+ .chars()58
+ .map(|c| u8::try_from(u32::from(c)).unwrap_or(b'?'))59
+ .collect()60
+}61
+62
+/// Hands back the write end of a pipe whose UTF-8 contents reach `out` as63
+/// Latin-1, which is what ICCCM says STRING holds.64
+fn latin1_relay(out: OwnedFd) -> std::io::Result<OwnedFd> {65
+ let (mut read, write) = std::io::pipe()?;66
+ fcntl_setfl(&out, OFlags::empty())?;67
+ std::thread::spawn(move || {68
+ let mut text = Vec::new();69
+ if read.read_to_end(&mut text).is_ok() {70
+ let _ = std::fs::File::from(out).write_all(&latin1(&text));71
+ }72
+ });73
+ Ok(write.into())74
+}75
+3376
pub struct Xwayland {3477
pub display: u32,3578
listeners: Vec<UnixListener>,@@ -222,6 +265,7 @@ fn start(state: &mut State, fired: usize) {
222265
if let Some(x) = state.services.xwayland.as_mut() {223266
for ty in [SelectionTarget::Clipboard, SelectionTarget::Primary] {224267
if let Some(mimes) = x.wayland_selection(ty).clone() {268
+ let mimes = with_string_target(mimes);225269
logged("selection", wm.new_selection(ty, Some(mimes)));226270
}227271
}@@ -372,6 +416,23 @@ impl XwmHandler for State {
372416
mime_type: String,373417
fd: OwnedFd,374418
) {419
+ let (mime_type, fd) = if mime_type == "STRING" {420
+ let source = self.services.xwayland.as_mut().and_then(|x| {421
+ let mimes = x.wayland_selection(selection).as_deref()?;422
+ utf8_source(mimes).map(str::to_owned)423
+ });424
+ let relay = source.map(|s| latin1_relay(fd).map(|w| (s, w)));425
+ match relay {426
+ Some(Ok(relay)) => relay,427
+ Some(Err(e)) => {428
+ tracing::warn!("cannot relay {selection:?} to X11 as STRING: {e}");429
+ return;430
+ }431
+ None => return,432
+ }433
+ } else {434
+ (mime_type, fd)435
+ };375436
let seat = &self.input.seat;376437
let result = match selection {377438
SelectionTarget::Clipboard => request_data_device_client_selection(seat, mime_type, fd)@@ -502,3 +563,23 @@ fn holds_display(pid: u32) -> bool {
502563
std::fs::read_to_string(format!("/proc/{pid}/comm"))503564
.is_ok_and(|comm| comm.starts_with('X') || comm.trim() == "monolith")504565
}566
+567
+#[cfg(test)]568
+mod tests {569
+ use super::*;570
+571
+ #[test]572
+ fn latin1_keeps_the_range_and_replaces_the_rest() {573
+ assert_eq!(latin1("café ÿ".as_bytes()), b"caf\xe9 \xff");574
+ assert_eq!(latin1("a€😀".as_bytes()), b"a??");575
+ assert_eq!(latin1(b"a\xc3"), b"a?");576
+ }577
+578
+ #[test]579
+ fn string_is_offered_for_text_only() {580
+ let text = vec!["text/plain;charset=utf-8".to_string()];581
+ assert_eq!(with_string_target(text).last().unwrap(), "STRING");582
+ let image = vec!["image/png".to_string()];583
+ assert_eq!(with_string_target(image.clone()), image);584
+ }585
+}