srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/xwayland.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/xwayland.rs')
-rw-r--r--crates/wayland/src/xwayland.rs380
1 files changed, 378 insertions, 2 deletions
diff --git a/crates/wayland/src/xwayland.rs b/crates/wayland/src/xwayland.rs
index ac71492..f18a485 100644
--- a/crates/wayland/src/xwayland.rs
+++ b/crates/wayland/src/xwayland.rs
@@ -75,7 +75,11 @@ pub(crate) fn spawn(handle: &LoopHandle<'static, CompState>, display_handle: &sm
XWaylandEvent::Ready { x11_socket, display_number } => {
log::info!("XWayland ready on display :{display_number}");
match X11Wm::start_wm(handle_for_ready.clone(), x11_socket, client.clone()) {
- Ok(wm) => data.xwm = Some(wm),
+ Ok(wm) => {
+ data.xwm = Some(wm);
+ fix_wm_name(display_number);
+ data.ewmh = EwmhState::connect(display_number);
+ }
Err(e) => log::error!("failed to start X11 window manager for XWayland: {e}"),
}
}
@@ -85,6 +89,289 @@ pub(crate) fn spawn(handle: &LoopHandle<'static, CompState>, display_handle: &sm
Ok(())
}
+/// Overwrites `_NET_WM_NAME` on XWayland's WM-check window from "Smithay X
+/// WM" to "srdwm".
+///
+/// `X11Wm::start_wm` hardcodes that string with no override hook exposed --
+/// no public method on `X11Wm`, and `wm_window`/its connection are private
+/// fields, so it can't be reached through smithay's API at all. Every X11
+/// client that asks "who is the window manager" (`xprop`, `wmctrl`,
+/// `xdotool`, fetch tools, app-compat shims that branch on WM identity)
+/// gets told the name of the *library*, not the compositor - which is
+/// actively misleading, not just cosmetic: it broke a shell function that
+/// resolved the WM's process name from this exact property to kill it on
+/// logout, `pkill`ing "Smithay" and matching nothing.
+///
+/// Worked around by opening a second, independent X11 connection of our
+/// own to the same XWayland display - exactly what `xprop` itself would
+/// do - and rewriting the property directly. `_NET_SUPPORTING_WM_CHECK`
+/// (which `start_wm` does set correctly) is how a plain client is meant to
+/// find the WM-check window in the first place, so reading it back off the
+/// root window rather than assuming a window ID keeps this from silently
+/// going stale if smithay ever changes how it allocates that window.
+fn fix_wm_name(display_number: u32) {
+ use smithay::reexports::x11rb::connection::Connection;
+ use smithay::reexports::x11rb::protocol::xproto::{AtomEnum, ConnectionExt as _, PropMode};
+ use smithay::reexports::x11rb::rust_connection::RustConnection;
+ use smithay::reexports::x11rb::wrapper::ConnectionExt as _;
+
+ let display = format!(":{display_number}");
+ let (conn, screen_num) = match RustConnection::connect(Some(&display)) {
+ Ok(c) => c,
+ Err(e) => {
+ log::warn!("xwayland: couldn't open a second connection to fix _NET_WM_NAME: {e}");
+ return;
+ }
+ };
+ let root = conn.setup().roots[screen_num].root;
+
+ let intern = |name: &str| -> Option<u32> { conn.intern_atom(false, name.as_bytes()).ok()?.reply().ok().map(|r| r.atom) };
+ let (Some(supporting_wm_check), Some(net_wm_name), Some(utf8_string)) =
+ (intern("_NET_SUPPORTING_WM_CHECK"), intern("_NET_WM_NAME"), intern("UTF8_STRING"))
+ else {
+ log::warn!("xwayland: couldn't intern EWMH atoms to fix _NET_WM_NAME");
+ return;
+ };
+
+ let reply = conn.get_property(false, root, supporting_wm_check, AtomEnum::WINDOW, 0, 1).ok().and_then(|c| c.reply().ok());
+ let wm_window = reply.as_ref().and_then(|r| r.value32()).and_then(|mut it| it.next());
+ let Some(wm_window) = wm_window else {
+ log::warn!("xwayland: _NET_SUPPORTING_WM_CHECK unset on the XWayland root; can't fix _NET_WM_NAME");
+ return;
+ };
+
+ if let Err(e) = conn.change_property8(PropMode::REPLACE, wm_window, net_wm_name, utf8_string, b"srdwm") {
+ log::warn!("xwayland: failed to set _NET_WM_NAME: {e}");
+ return;
+ }
+ let _ = conn.flush();
+}
+
+/// Keeps `_NET_ACTIVE_WINDOW`/`_NET_CLIENT_LIST`/`_NET_CLIENT_LIST_STACKING`
+/// on the XWayland root window up to date.
+///
+/// srdwm declares all three in `_NET_SUPPORTED` (smithay's `X11Wm` sets that
+/// part up on its own), but never actually wrote them: `_NET_ACTIVE_WINDOW`
+/// stayed `0x0` and `_NET_CLIENT_LIST` stayed empty regardless of what was
+/// focused or mapped. Confirmed this is not something `X11Wm` does for us
+/// automatically - it only updates `_NET_ACTIVE_WINDOW` in response to a
+/// real X11 `FocusIn`/`FocusOut` event on the window, which requires an
+/// actual `SetInputFocus` request to have been issued in the first place,
+/// and nothing in this codebase ever issues one (Wayland keyboard focus and
+/// X11 input focus are separate things; only the former was ever set). The
+/// practical effect: any X11-aware client trying to answer "what's the
+/// focused window" or "what windows exist" - `xdotool`, `wmctrl`, and
+/// (per a downstream report) an AGS global-menu widget resolving the
+/// focused window to query its DBusMenu registrar - got nothing.
+///
+/// Rather than depend on `X11Wm`'s `FocusIn`-triggered path (which would
+/// also need us to issue real `SetInputFocus` requests, itself a bigger
+/// change with its own risk of fighting Wayland focus), this writes both
+/// properties directly, from srdwm's own already-authoritative focus and
+/// window-list state - exactly the "no new protocol needed" shape a
+/// real EWMH-maintaining WM uses. Same reasoning as `fix_wm_name` for using
+/// a second, independent connection rather than reaching into `X11Wm`'s
+/// private one: there is no public accessor for it.
+pub(crate) struct EwmhState {
+ conn: smithay::reexports::x11rb::rust_connection::RustConnection,
+ root: u32,
+ net_active_window: u32,
+ net_client_list: u32,
+ net_client_list_stacking: u32,
+ /// Global-menu atoms - see `read_global_menu`. Individually optional
+ /// (unlike the EWMH atoms above): a server old enough, or configured
+ /// oddly enough, to not know these names is still a fully functional
+ /// X server for everything else this module does, so a failure to
+ /// intern any one of them just means that field never resolves rather
+ /// than aborting `connect` entirely.
+ gtk_unique_bus_name: Option<u32>,
+ gtk_application_object_path: Option<u32>,
+ gtk_window_object_path: Option<u32>,
+ gtk_menubar_object_path: Option<u32>,
+ gtk_app_menu_object_path: Option<u32>,
+ unity_object_path: Option<u32>,
+}
+
+impl EwmhState {
+ fn connect(display_number: u32) -> Option<Self> {
+ use smithay::reexports::x11rb::connection::Connection;
+ use smithay::reexports::x11rb::protocol::xproto::ConnectionExt as _;
+ use smithay::reexports::x11rb::rust_connection::RustConnection;
+
+ let display = format!(":{display_number}");
+ let (conn, screen_num) = match RustConnection::connect(Some(&display)) {
+ Ok(c) => c,
+ Err(e) => {
+ log::warn!("xwayland: couldn't open a connection for EWMH property updates: {e}");
+ return None;
+ }
+ };
+ let root = conn.setup().roots[screen_num].root;
+ let intern = |name: &str| -> Option<u32> { conn.intern_atom(false, name.as_bytes()).ok()?.reply().ok().map(|r| r.atom) };
+ let (Some(net_active_window), Some(net_client_list), Some(net_client_list_stacking)) =
+ (intern("_NET_ACTIVE_WINDOW"), intern("_NET_CLIENT_LIST"), intern("_NET_CLIENT_LIST_STACKING"))
+ else {
+ log::warn!("xwayland: couldn't intern EWMH atoms; _NET_ACTIVE_WINDOW/_NET_CLIENT_LIST won't be maintained");
+ return None;
+ };
+ let gtk_unique_bus_name = intern("_GTK_UNIQUE_BUS_NAME");
+ let gtk_application_object_path = intern("_GTK_APPLICATION_OBJECT_PATH");
+ let gtk_window_object_path = intern("_GTK_WINDOW_OBJECT_PATH");
+ let gtk_menubar_object_path = intern("_GTK_MENUBAR_OBJECT_PATH");
+ let gtk_app_menu_object_path = intern("_GTK_APP_MENU_OBJECT_PATH");
+ let unity_object_path = intern("_UNITY_OBJECT_PATH");
+ let state = Self {
+ conn,
+ root,
+ net_active_window,
+ net_client_list,
+ net_client_list_stacking,
+ gtk_unique_bus_name,
+ gtk_application_object_path,
+ gtk_window_object_path,
+ gtk_menubar_object_path,
+ gtk_app_menu_object_path,
+ unity_object_path,
+ };
+ // `_NET_CLIENT_LIST`/`_STACKING` are properties on the X root window,
+ // which XWayland recreates fresh on every launch - but nothing
+ // guarantees a *client* reading them does so only after this
+ // compositor's own first `update_net_client_list()` call, and until
+ // that first real add/remove there is no guarantee the property even
+ // has a defined initial value. Clearing it here, before any window
+ // has ever mapped, means a freshly connected client can never read a
+ // leftover or undefined list - it always starts empty and correct.
+ state.set_client_list(&[]);
+ Some(state)
+ }
+
+ /// Reads `xid`'s global-menu D-Bus address straight off its own X11
+ /// properties - `_GTK_UNIQUE_BUS_NAME` plus whichever menu-path atom
+ /// the client actually set. `_GTK_MENUBAR_OBJECT_PATH` (a real menu
+ /// bar) wins over `_GTK_APP_MENU_OBJECT_PATH` (the single-item
+ /// fallback simpler/older clients export) if a client somehow sets
+ /// both; `_UNITY_OBJECT_PATH` is the pre-`_GTK_*` name some
+ /// still-relevant toolkits (older Qt builds with the appmenu-qt5
+ /// platform theme) use instead, tried last. No bus name means no menu
+ /// at all - the paths are meaningless without it - so this returns
+ /// `None` rather than a `GlobalMenu` with an empty `bus_name`.
+ ///
+ /// Which atom actually won is recorded as `source` - a consumer needs
+ /// it to pick the right D-Bus action-group prefix (`app`/`win` for a
+ /// real `GMenuModel`, `unity` for the older export), and
+ /// `appmenu-gtk-module` is known to set the `_GTK_*` atoms *and*
+ /// `_UNITY_OBJECT_PATH` simultaneously in some configurations - a
+ /// consumer with only the resolved path string, and no record of
+ /// which one it came from, can't tell the two cases apart even though
+ /// picking the wrong prefix means every menu item renders permanently
+ /// insensitive (a silent failure that reads exactly like a broken
+ /// app, not a wiring bug). Reported by the AGS peer session building
+ /// the consumer, from hitting exactly this live.
+ fn read_global_menu(&self, xid: u32) -> Option<srdwm_core::GlobalMenu> {
+ use smithay::reexports::x11rb::protocol::xproto::{AtomEnum, ConnectionExt as _};
+
+ let read_string = |atom: Option<u32>| -> Option<String> {
+ let atom = atom?;
+ let reply = self.conn.get_property(false, xid, atom, AtomEnum::ANY, 0, u32::MAX).ok()?.reply().ok()?;
+ if reply.value.is_empty() {
+ return None;
+ }
+ String::from_utf8(reply.value).ok().filter(|s| !s.is_empty())
+ };
+
+ let bus_name = read_string(self.gtk_unique_bus_name)?;
+ let gtk_menu_path = read_string(self.gtk_menubar_object_path).or_else(|| read_string(self.gtk_app_menu_object_path));
+ let (menu_path, source) = match gtk_menu_path {
+ Some(path) => (Some(path), srdwm_core::MenuSource::Gtk),
+ None => match read_string(self.unity_object_path) {
+ Some(path) => (Some(path), srdwm_core::MenuSource::Unity),
+ None => (None, srdwm_core::MenuSource::Gtk),
+ },
+ };
+ let app_path = read_string(self.gtk_application_object_path);
+ let window_path = read_string(self.gtk_window_object_path);
+ Some(srdwm_core::GlobalMenu { bus_name, menu_path, app_path, window_path, source })
+ }
+
+ /// `xid` is `None` when focus is on a native Wayland window (or
+ /// nothing) rather than an X11 one - `_NET_ACTIVE_WINDOW`'s value is
+ /// only meaningful for X11 clients, so this writes `0` (the documented
+ /// "no active window" sentinel) rather than leaving the last X11
+ /// window's id stale and misleading.
+ fn set_active_window(&self, xid: Option<u32>) {
+ use smithay::reexports::x11rb::connection::Connection;
+ use smithay::reexports::x11rb::protocol::xproto::{AtomEnum, PropMode};
+ use smithay::reexports::x11rb::wrapper::ConnectionExt as _;
+ if let Err(e) = self.conn.change_property32(PropMode::REPLACE, self.root, self.net_active_window, AtomEnum::WINDOW, &[xid.unwrap_or(0)]) {
+ log::warn!("xwayland: failed to set _NET_ACTIVE_WINDOW: {e}");
+ return;
+ }
+ let _ = self.conn.flush();
+ }
+
+ fn set_client_list(&self, xids: &[u32]) {
+ use smithay::reexports::x11rb::connection::Connection;
+ use smithay::reexports::x11rb::protocol::xproto::{AtomEnum, PropMode};
+ use smithay::reexports::x11rb::wrapper::ConnectionExt as _;
+ // Same order for both: EWMH only defines a strict order for the
+ // `_STACKING` variant (bottom-to-top), and `stacking_order` is
+ // already srdwm's one authoritative ordering of its windows - a
+ // second, differently-ordered list for plain `_NET_CLIENT_LIST`
+ // would need tracking mapping order separately for no real benefit.
+ for (atom, name) in [(self.net_client_list, "_NET_CLIENT_LIST"), (self.net_client_list_stacking, "_NET_CLIENT_LIST_STACKING")] {
+ if let Err(e) = self.conn.change_property32(PropMode::REPLACE, self.root, atom, AtomEnum::WINDOW, xids) {
+ log::warn!("xwayland: failed to set {name}: {e}");
+ return;
+ }
+ }
+ let _ = self.conn.flush();
+ }
+}
+
+impl CompState {
+ /// Call on every focus change (from `set_keyboard_focus`, the single
+ /// chokepoint every focus path already goes through). `surface` is
+ /// whatever just gained keyboard focus; resolves to an X11 window id
+ /// only if that surface's window is XWayland-backed.
+ pub(crate) fn update_net_active_window(&self, surface: Option<&smithay::reexports::wayland_server::protocol::wl_surface::WlSurface>) {
+ let Some(ewmh) = &self.ewmh else { return };
+ let id = surface.and_then(|s| self.surface_to_id.get(s)).copied();
+ let xid = id.and_then(|id| self.id_to_window.get(&id)).and_then(|w| w.x11_surface()).map(|x| x.window_id());
+ ewmh.set_active_window(xid);
+ // Global-menu properties are usually set once, shortly after a
+ // client registers on the session bus - which can race a window's
+ // own initial map, so reading them only at map time would miss a
+ // client that finished that registration a moment later. Refreshed
+ // here instead: every real focus change is a natural, already-
+ // existing hook, and a menu only actually needs to be current for
+ // whichever window is focused right now anyway. `read_global_menu`
+ // returning `None` (the common case for anything non-GTK, or a
+ // GTK app with no menu to export) correctly clears a stale value
+ // from a previous window that used to occupy this `id`.
+ if let (Some(id), Some(xid)) = (id, xid) {
+ let menu = ewmh.read_global_menu(xid);
+ if let Some(w) = self.wm.borrow_mut().window_mut(id) {
+ w.global_menu = menu;
+ }
+ }
+ }
+
+ /// Call whenever the set of mapped windows changes (X11 window map,
+ /// unmap, or destroy - see the `XwmHandler` methods below).
+ pub(crate) fn update_net_client_list(&self) {
+ let Some(ewmh) = &self.ewmh else { return };
+ let xids: Vec<u32> = self
+ .wm
+ .borrow()
+ .stacking_order()
+ .filter_map(|w| self.id_to_window.get(&w.id))
+ .filter_map(|w| w.x11_surface())
+ .map(|x| x.window_id())
+ .collect();
+ ewmh.set_client_list(&xids);
+ }
+}
+
/// Writes a small shell script named `Xwayland` to a private directory and
/// prepends that directory to this process's own `PATH` - the next
/// `Command::new("Xwayland")` (namely `XWayland::spawn`'s, which copies
@@ -195,10 +482,23 @@ impl CompState {
let _ = surface.configure(Rectangle::new((geom.x, geom.y + TITLEBAR_HEIGHT as i32).into(), (geom.width as i32, (geom.height - TITLEBAR_HEIGHT) as i32).into()));
self.space.map_element(dwindow.clone(), (geom.x, geom.y + TITLEBAR_HEIGHT as i32), true);
- self.surface_to_id.insert(wl_surface, id);
+ self.surface_to_id.insert(wl_surface.clone(), id);
self.id_to_window.insert(id, dwindow);
self.redraw_decoration_buffer(id);
+ // `WindowManager::add_window` already made this the focused window
+ // in srdwm's own bookkeeping (it unconditionally does, for every
+ // new window), but that's purely internal state - without this, a
+ // freshly-opened XWayland app never receives a single keystroke
+ // until it's clicked, and (found investigating a downstream EWMH
+ // report) `_NET_ACTIVE_WINDOW` never updates either, since this is
+ // `set_keyboard_focus`'s only caller for X11 windows and that's the
+ // sole place `_NET_ACTIVE_WINDOW` gets written. The xdg-shell path
+ // (`new_managed_window` in state.rs) already does this; this is the
+ // equivalent X11 creation path, which never got the same fix.
+ self.set_keyboard_focus(Some(wl_surface));
self.pending.borrow_mut().push(CoreEvent::WindowCreated(id));
+ self.update_net_client_list();
+ crate::foreign_toplevel::window_created(self, id);
}
fn remove_x11_window(&mut self, xid: X11Window) {
@@ -207,8 +507,20 @@ impl CompState {
self.space.unmap_elem(&w);
}
self.decorations.remove(&id);
+ // Same reason as `state.rs`'s native `remove_window`: don't leave
+ // the context menu open against a window that's about to stop
+ // existing.
+ if self.context_menu.as_ref().is_some_and(|m| m.window == id) {
+ self.close_context_menu();
+ }
self.wm.borrow_mut().remove_window(id);
self.pending.borrow_mut().push(CoreEvent::WindowDestroyed(id));
+ crate::foreign_toplevel::window_closed(self, id);
+ // Same reason as the equivalent call in `state.rs`'s native
+ // `remove_window`: core may have already moved focus to whatever's
+ // now on top, and the Wayland/X11 side needs to be told to follow.
+ crate::input::sync_keyboard_focus(self);
+ self.update_net_client_list();
}
}
@@ -320,6 +632,70 @@ impl XwmHandler for CompState {
}
}
+ /// The same six requests found missing for native Wayland windows
+ /// (`XdgShellHandler`'s `maximize_request`/`unmaximize_request`/
+ /// `fullscreen_request`/`unfullscreen_request`/`minimize_request`,
+ /// see `protocols.rs`) exist here too, under EWMH/ICCCM naming --
+ /// `_NET_WM_STATE_MAXIMIZED_VERT`/`_HORZ`, `_NET_WM_STATE_FULLSCREEN`,
+ /// `_NET_WM_STATE_HIDDEN` toggled via a client message - and were
+ /// equally unimplemented, silently doing nothing for any XWayland
+ /// app's own window-menu maximize/minimize/fullscreen action. `move_
+ /// request`/`resize_request` right below were already implemented,
+ /// which is what made this omission easy to miss; the drag/resize
+ /// half of this class of gap already had parity, only the state-
+ /// toggle half didn't. `unminimize_request` has no native-Wayland
+ /// equivalent to mirror - xdg-shell has no client-initiated "restore
+ /// from minimized" request at all, only EWMH does.
+ fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
+ let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
+ if !self.wm.borrow().window(id).is_some_and(|w| w.maximized) {
+ self.wm.borrow_mut().toggle_maximize(id);
+ self.sync_geometry(id);
+ crate::foreign_toplevel::send_state(self, id);
+ }
+ }
+
+ fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
+ let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
+ if self.wm.borrow().window(id).is_some_and(|w| w.maximized) {
+ self.wm.borrow_mut().toggle_maximize(id);
+ self.sync_geometry(id);
+ crate::foreign_toplevel::send_state(self, id);
+ }
+ }
+
+ fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
+ let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
+ if !self.wm.borrow().is_fullscreen(id) {
+ self.wm.borrow_mut().toggle_fullscreen(id);
+ self.redraw_decoration_buffer(id);
+ self.sync_geometry(id);
+ crate::foreign_toplevel::send_state(self, id);
+ }
+ }
+
+ fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
+ let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
+ if self.wm.borrow().is_fullscreen(id) {
+ self.wm.borrow_mut().toggle_fullscreen(id);
+ self.redraw_decoration_buffer(id);
+ self.sync_geometry(id);
+ crate::foreign_toplevel::send_state(self, id);
+ }
+ }
+
+ fn minimize_request(&mut self, _xwm: XwmId, window: X11Surface) {
+ let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
+ self.wm.borrow_mut().minimize_window(id);
+ crate::foreign_toplevel::send_state(self, id);
+ }
+
+ fn unminimize_request(&mut self, _xwm: XwmId, window: X11Surface) {
+ let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
+ self.wm.borrow_mut().restore_window(id);
+ crate::foreign_toplevel::send_state(self, id);
+ }
+
fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, resize_edge: X11ResizeEdge) {
let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return };
let pos = self.seat.get_pointer().map(|p| p.current_location()).unwrap_or_default();