atagen

monolith

public
Star0
Watch0

Commit cce3ef7e0162

tarball

atagen <boss@atagen.co>2026-10-16 22:41 UTC

15 files changed+622-50
monolith: start Xwayland lazily on the first X client

monolith claims an X display at startup and hands its sockets to XWayland::spawn as extra -listenfd arguments, so the connection that woke Xwayland is served by it. Spawning our own Xwayland was not an option, since xwayland-shell only trusts clients carrying smithay's private XWaylandClientData. Keyboard focus became an enum because Xwayland routes keys only to the window the WM gave X input focus.
Modified file: crates/monolith/Cargo.toml+1 additions
@@ -32,6 +32,7 @@ smithay = { workspace = true, features = [
3232 "backend_libinput",
3333 "backend_udev",
3434 "backend_session_libseat",
35+ "xwayland",
3536 ] }
3637 smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "023b78b1c5a05ca512767cacc07efca55fae8be4", default-features = false }
3738 xcursor = "0.3"
Modified file: crates/monolith/src/activation.rs+5 additions−1 deletions
@@ -5,6 +5,7 @@
55 use crate::state::State;
66 use monolith_layout::{OutputId, TagId};
77 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8+use smithay::wayland::seat::WaylandFocus;
89 use smithay::wayland::xdg_activation::{
910 XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
1011 };
@@ -27,7 +28,10 @@ impl XdgActivationHandler for State {
2728
2829 fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
2930 let keyboard = self.seat.get_keyboard();
30- let focused = keyboard.as_ref().and_then(|k| k.current_focus());
31+ let focused = keyboard
32+ .as_ref()
33+ .and_then(|k| k.current_focus())
34+ .and_then(|f| f.wl_surface().map(|s| s.into_owned()));
3135 let may_focus = data.surface.is_some() && data.surface == focused;
3236 let (output, tag) = self.launch_context();
3337 data.user_data.insert_if_missing(|| Context {
Modified file: crates/monolith/src/capture.rs+1 additions
@@ -220,6 +220,7 @@ impl State {
220220 &self.config.decoration,
221221 &mut self.borders,
222222 Some(output),
223+ self.xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
223224 )),
224225 }
225226 elements
Modified file: crates/monolith/src/commands.rs+11 additions−5 deletions
@@ -62,11 +62,14 @@ impl State {
6262 match cmd {
6363 Command::Spawn { argv } => self.spawn(&argv)?,
6464 Command::Close => {
65- if let Some(t) = focused
66- .and_then(|w| self.windows.get(&w))
67- .and_then(|w| w.toplevel())
68- {
69- t.send_close();
65+ if let Some(w) = focused.and_then(|w| self.windows.get(&w)) {
66+ match (w.toplevel(), w.x11_surface()) {
67+ (Some(t), _) => t.send_close(),
68+ (_, Some(x)) => {
69+ let _ = x.close();
70+ }
71+ _ => {}
72+ }
7073 }
7174 }
7275 Command::Quit => self.signal.stop(),
@@ -177,6 +180,9 @@ impl State {
177180 cmd.env("XDG_CURRENT_DESKTOP", "monolith")
178181 .env("XDG_SESSION_TYPE", "wayland");
179182 }
183+ if let Some(x) = &self.xwayland {
184+ cmd.env("DISPLAY", format!(":{}", x.display));
185+ }
180186 let token = self.launch_token();
181187 cmd.env("XDG_ACTIVATION_TOKEN", &token)
182188 .env("DESKTOP_STARTUP_ID", &token);
Modified file: crates/monolith/src/drm.rs+2 additions
@@ -592,6 +592,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
592592 pointer_pos,
593593 lock,
594594 captures,
595+ xwayland,
595596 ..
596597 } = state
597598 else {
@@ -652,6 +653,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
652653 &config.decoration,
653654 borders,
654655 Some(&surface.output),
656+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
655657 )),
656658 }
657659 let scene_ns = scene_began.elapsed().as_nanos() as u64;
Added file: crates/monolith/src/focus.rs+92 additions
@@ -0,0 +1,92 @@
1+//! Keyboard focus targets. X11 windows need their own target, since
2+//! Xwayland only routes keys to the window the WM gave X input focus.
3+
4+use crate::state::State;
5+use smithay::backend::input::KeyState;
6+use smithay::input::Seat;
7+use smithay::input::keyboard::{KeyboardTarget, KeysymHandle, ModifiersState};
8+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
9+use smithay::utils::{IsAlive, Serial};
10+use smithay::wayland::seat::WaylandFocus;
11+use smithay::xwayland::X11Surface;
12+use std::borrow::Cow;
13+
14+#[derive(Debug, Clone, PartialEq)]
15+pub enum Focus {
16+ Surface(WlSurface),
17+ X11(Box<X11Surface>),
18+}
19+
20+impl From<WlSurface> for Focus {
21+ fn from(s: WlSurface) -> Self {
22+ Focus::Surface(s)
23+ }
24+}
25+
26+impl IsAlive for Focus {
27+ fn alive(&self) -> bool {
28+ match self {
29+ Focus::Surface(s) => s.alive(),
30+ Focus::X11(x) => x.alive(),
31+ }
32+ }
33+}
34+
35+impl WaylandFocus for Focus {
36+ fn wl_surface(&self) -> Option<Cow<'_, WlSurface>> {
37+ match self {
38+ Focus::Surface(s) => Some(Cow::Borrowed(s)),
39+ Focus::X11(x) => x.wl_surface().map(Cow::Owned),
40+ }
41+ }
42+}
43+
44+impl KeyboardTarget<State> for Focus {
45+ fn enter(
46+ &self,
47+ seat: &Seat<State>,
48+ data: &mut State,
49+ keys: Vec<KeysymHandle<'_>>,
50+ serial: Serial,
51+ ) {
52+ match self {
53+ Focus::Surface(s) => KeyboardTarget::enter(s, seat, data, keys, serial),
54+ Focus::X11(x) => KeyboardTarget::enter(x.as_ref(), seat, data, keys, serial),
55+ }
56+ }
57+
58+ fn leave(&self, seat: &Seat<State>, data: &mut State, serial: Serial) {
59+ match self {
60+ Focus::Surface(s) => KeyboardTarget::leave(s, seat, data, serial),
61+ Focus::X11(x) => KeyboardTarget::leave(x.as_ref(), seat, data, serial),
62+ }
63+ }
64+
65+ fn key(
66+ &self,
67+ seat: &Seat<State>,
68+ data: &mut State,
69+ key: KeysymHandle<'_>,
70+ state: KeyState,
71+ serial: Serial,
72+ time: smithay::backend::input::InputTime,
73+ ) {
74+ match self {
75+ Focus::Surface(s) => KeyboardTarget::key(s, seat, data, key, state, serial, time),
76+ Focus::X11(x) => KeyboardTarget::key(x.as_ref(), seat, data, key, state, serial, time),
77+ }
78+ }
79+
80+ fn modifiers(
81+ &self,
82+ seat: &Seat<State>,
83+ data: &mut State,
84+ modifiers: ModifiersState,
85+ serial: Serial,
86+ ) {
87+ match self {
88+ Focus::Surface(s) => KeyboardTarget::modifiers(s, seat, data, modifiers, serial),
89+ Focus::X11(x) => KeyboardTarget::modifiers(x.as_ref(), seat, data, modifiers, serial),
90+ }
91+ }
92+}
Modified file: crates/monolith/src/handlers.rs+11 additions−5 deletions
@@ -25,6 +25,7 @@ use smithay::wayland::dmabuf::{
2525 DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf,
2626 };
2727 use smithay::wayland::output::OutputHandler;
28+use smithay::wayland::seat::WaylandFocus;
2829 use smithay::wayland::selection::SelectionHandler;
2930 use smithay::wayland::selection::primary_selection::{
3031 PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
@@ -51,6 +52,9 @@ impl CompositorHandler for State {
5152 }
5253
5354 fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
55+ if let Some(x) = client.get_data::<smithay::xwayland::XWaylandClientData>() {
56+ return &x.compositor_state;
57+ }
5458 &client
5559 .get_data::<ClientState>()
5660 .expect("every client has ClientState")
@@ -73,7 +77,7 @@ impl CompositorHandler for State {
7377 .windows
7478 .values()
7579 .chain(&self.unmapped)
76- .find(|w| w.toplevel().is_some_and(|t| t.wl_surface() == &root))
80+ .find(|w| crate::state::root(w).as_ref() == Some(&root))
7781 {
7882 window.on_commit();
7983 }
@@ -85,7 +89,7 @@ impl CompositorHandler for State {
8589 {
8690 let window = self.unmapped.remove(i);
8791 let toplevel = window.toplevel().cloned();
88- self.map_window(window);
92+ self.map_window(window, None);
8993 if let Some(t) = toplevel {
9094 t.send_configure();
9195 }
@@ -174,7 +178,7 @@ impl XdgShellHandler for State {
174178 }
175179
176180 impl SeatHandler for State {
177- type KeyboardFocus = WlSurface;
181+ type KeyboardFocus = crate::focus::Focus;
178182 type PointerFocus = WlSurface;
179183 type TouchFocus = WlSurface;
180184
@@ -187,8 +191,10 @@ impl SeatHandler for State {
187191 self.needs_redraw = true;
188192 }
189193
190- fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
191- let client = focused.and_then(|s| self.display.get_client(s.id()).ok());
194+ fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&crate::focus::Focus>) {
195+ let client = focused
196+ .and_then(|f| f.wl_surface())
197+ .and_then(|s| self.display.get_client(s.id()).ok());
192198 set_data_device_focus(&self.display, seat, client.clone());
193199 set_primary_focus(&self.display, seat, client);
194200 }
Modified file: crates/monolith/src/idle.rs+1 additions−1 deletions
@@ -43,7 +43,7 @@ impl State {
4343 }
4444 self.space
4545 .elements()
46- .any(|w| w.toplevel().is_some_and(|t| t.wl_surface() == &root))
46+ .any(|w| crate::state::root(w).as_ref() == Some(&root))
4747 || self.layer_maps_contain(&root)
4848 });
4949 if shown != self.inhibited {
Modified file: crates/monolith/src/input.rs+2 additions−2 deletions
@@ -153,8 +153,8 @@ fn focus_under_pointer(state: &mut State) {
153153 let id = state
154154 .space
155155 .element_under(pos)
156- .and_then(|(w, _)| w.toplevel())
157- .and_then(|t| state.window_id(t.wl_surface()));
156+ .and_then(|(w, _)| crate::state::root(w))
157+ .and_then(|s| state.window_id(&s));
158158 if let Some(id) = id
159159 && state.layout.focused() != Some(id)
160160 {
Modified file: crates/monolith/src/ipc.rs+1 additions−6 deletions
@@ -172,12 +172,7 @@ fn snapshot(state: &State) -> monolith_ipc::State {
172172 let mut windows: Vec<monolith_ipc::Window> = l
173173 .windows()
174174 .map(|(id, w)| {
175- let (app_id, title) = state
176- .windows
177- .get(&id)
178- .and_then(|w| w.toplevel())
179- .map(identity)
180- .unwrap_or_default();
175+ let (app_id, title) = state.windows.get(&id).map(identity).unwrap_or_default();
181176 monolith_ipc::Window {
182177 id: slotmap::Key::data(&id).as_ffi(),
183178 app_id,
Modified file: crates/monolith/src/main.rs+9 additions
@@ -4,6 +4,7 @@ mod capture;
44 mod commands;
55 mod cursor;
66 mod drm;
7+mod focus;
78 mod handlers;
89 mod idle;
910 mod input;
@@ -14,6 +15,7 @@ mod lock;
1415 mod nested;
1516 mod scene;
1617 mod state;
18+mod xwayland;
1719
1820 #[cfg(feature = "spans")]
1921 mod slow_spans;
@@ -140,6 +142,12 @@ fn export_session(state: &State) {
140142 ))
141143 .arg("XDG_CURRENT_DESKTOP=monolith")
142144 .arg("XDG_SESSION_TYPE=wayland")
145+ .args(
146+ state
147+ .xwayland
148+ .as_ref()
149+ .map(|x| format!("DISPLAY=:{}", x.display)),
150+ )
143151 .stdin(std::process::Stdio::null())
144152 .spawn();
145153 match result {
@@ -172,6 +180,7 @@ fn run(
172180 None => nested::init(&mut event_loop, &mut state)?,
173181 }
174182 ipc::init(&event_loop, &mut state)?;
183+ xwayland::init(&mut state);
175184 tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running");
176185
177186 if gpu.is_some() {
Modified file: crates/monolith/src/nested/mod.rs+2 additions
@@ -181,6 +181,7 @@ pub fn render(state: &mut State) {
181181 output_id,
182182 lock,
183183 captures,
184+ xwayland,
184185 ..
185186 } = state;
186187 let Some(host) = host.as_mut() else { return };
@@ -236,6 +237,7 @@ pub fn render(state: &mut State) {
236237 &config.decoration,
237238 borders,
238239 Some(&host.output),
240+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
239241 )
240242 });
241243 match host
Modified file: crates/monolith/src/scene.rs+17 additions−2 deletions
@@ -8,6 +8,7 @@ use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
88 use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
99 use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
1010 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
11+use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
1112 use smithay::backend::renderer::element::texture::TextureRenderElement;
1213 use smithay::backend::renderer::element::utils::{CropRenderElement, RescaleRenderElement};
1314 use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};
@@ -19,6 +20,7 @@ use smithay::output::Output;
1920 use smithay::utils::user_data::UserDataMap;
2021 use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
2122 use smithay::wayland::shell::wlr_layer::Layer;
23+use smithay::xwayland::X11Surface;
2224 use std::collections::HashMap;
2325
2426 smithay::backend::renderer::element::render_elements! {
@@ -50,6 +52,7 @@ pub fn build(
5052 deco: &Decoration,
5153 borders: &mut Borders,
5254 output: Option<&Output>,
55+ unmanaged: &[X11Surface],
5356 ) -> Vec<Draw> {
5457 let Some(area) = layout.output(o).map(|out| out.area) else {
5558 return Vec::new();
@@ -90,6 +93,18 @@ pub fn build(
9093 borders.0.retain(|id, _| windows.contains_key(id));
9194 let focused = layout.focused();
9295 let mut out = Vec::new();
96+ for x in unmanaged.iter().rev() {
97+ let Some(surface) = x.wl_surface() else {
98+ continue;
99+ };
100+ let loc = x.geometry().loc;
101+ let at = Point::<i32, Physical>::from((loc.x - area.x as i32, loc.y - area.y as i32));
102+ out.extend(
103+ render_elements_from_surface_tree(renderer, &surface, at, 1.0, 1.0, Kind::Unspecified)
104+ .into_iter()
105+ .map(Draw::Surface),
106+ );
107+ }
93108 if let Some(output) = output {
94109 crate::layers::elements(&mut out, renderer, output, Layer::Overlay);
95110 if !fullscreen_at_rest {
@@ -185,7 +200,7 @@ fn window_elements(
185200 .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
186201 .map(Draw::Cropped),
187202 );
188- let Some(root) = window.toplevel().map(|t| t.wl_surface().clone()) else {
203+ let Some(root) = crate::state::root(window) else {
189204 return;
190205 };
191206 let context = renderer.context_id();
@@ -302,7 +317,7 @@ fn effect_elements(
302317 (at, size): ((i32, i32), (i32, i32)),
303318 effect: Effect,
304319 ) {
305- let Some(root) = window.toplevel().map(|t| t.wl_surface().clone()) else {
320+ let Some(root) = crate::state::root(window) else {
306321 return;
307322 };
308323 let root_id = Id::from_wayland_resource(&root);
Modified file: crates/monolith/src/state.rs+85 additions−28 deletions
@@ -1,5 +1,6 @@
11 use crate::anim::{Animator, Ghost};
22 use crate::commands::Binds;
3+use crate::focus::Focus;
34 use crate::nested::Host;
45 use monolith_config::Config;
56 use monolith_layout::{Layout, OutputId, Rect, WindowId};
@@ -9,6 +10,7 @@ use monolith_render_vk::VulkanTexture;
910 use smithay::backend::renderer::ImportDma;
1011 use smithay::backend::renderer::Renderer;
1112 use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
13+use smithay::desktop::utils::under_from_surface_tree;
1214 use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};
1315 use smithay::input::keyboard::{Keycode, XkbConfig};
1416 use smithay::input::pointer::CursorImageStatus;
@@ -21,7 +23,7 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::Sta
2123 use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
2224 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
2325 use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};
24-use smithay::utils::{Clock, Logical, Monotonic, Point, SERIAL_COUNTER, Size};
26+use smithay::utils::{Clock, Logical, Monotonic, Point, Rectangle, SERIAL_COUNTER, Size};
2527 use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states};
2628 use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};
2729 use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
@@ -29,6 +31,7 @@ use smithay::wayland::idle_notify::IdleNotifierState;
2931 use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
3032 use smithay::wayland::image_copy_capture::ImageCopyCaptureState;
3133 use smithay::wayland::output::OutputManagerState;
34+use smithay::wayland::seat::WaylandFocus;
3235 use smithay::wayland::selection::data_device::DataDeviceState;
3336 use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
3437 use smithay::wayland::selection::primary_selection::PrimarySelectionState;
@@ -40,6 +43,7 @@ use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};
4043 use smithay::wayland::shm::ShmState;
4144 use smithay::wayland::socket::ListeningSocketSource;
4245 use smithay::wayland::xdg_activation::XdgActivationState;
46+use smithay::wayland::xwayland_shell::XWaylandShellState;
4347 use std::collections::{HashMap, HashSet};
4448 use std::ffi::OsString;
4549 use std::path::PathBuf;
@@ -90,6 +94,8 @@ pub struct State {
9094 pub placed_by_rule: HashSet<WindowId>,
9195 /// A layer surface given the keyboard by a click, like a launcher.
9296 pub layer_focus: Option<WlSurface>,
97+ pub xwayland: Option<crate::xwayland::Xwayland>,
98+ pub xwayland_shell_state: XWaylandShellState,
9399 pub captures: crate::capture::Captures,
94100 /// The render node clients get dmabufs for, known on bare metal.
95101 pub render_node: Option<smithay::backend::drm::DrmNode>,
@@ -205,6 +211,8 @@ impl State {
205211 awaiting: HashSet::new(),
206212 placed_by_rule: HashSet::new(),
207213 layer_focus: None,
214+ xwayland: None,
215+ xwayland_shell_state: XWaylandShellState::new::<State>(&dh),
208216 captures: Default::default(),
209217 render_node: None,
210218 lock: Default::default(),
@@ -277,14 +285,18 @@ impl State {
277285
278286 /// Adds a toplevel to the layout on its first commit, where its app_id
279287 /// and title are known and the initial configure can carry its size.
280- pub fn map_window(&mut self, window: Window) {
281- let (app_id, title) = window.toplevel().map(identity).unwrap_or_default();
288+ pub fn map_window(&mut self, window: Window, float: Option<Rectangle<i32, Logical>>) {
289+ let (app_id, title) = identity(&window);
282290 let rule = self.config.placement(&app_id, &title);
283- let launch = window
284- .toplevel()
285- .and_then(|t| t.wl_surface().client())
286- .and_then(|c| c.get_credentials(&self.display).ok())
287- .and_then(|c| self.launches.find(c.pid as u32));
291+ let pid = match window.x11_surface() {
292+ Some(x) => x.pid(),
293+ None => window
294+ .toplevel()
295+ .and_then(|t| t.wl_surface().client())
296+ .and_then(|c| c.get_credentials(&self.display).ok())
297+ .map(|c| c.pid as u32),
298+ };
299+ let launch = pid.and_then(|pid| self.launches.find(pid));
288300 let output = rule
289301 .output
290302 .as_deref()
@@ -299,7 +311,15 @@ impl State {
299311 Some(t) => Some(self.layout.tag(t)),
300312 None => launch.map(|l| l.1),
301313 };
302- let id = self.layout.add_window(output, tag, None);
314+ let float = float.map(|r| {
315+ Rect::new(
316+ f64::from(r.loc.x),
317+ f64::from(r.loc.y),
318+ f64::from(r.size.w),
319+ f64::from(r.size.h),
320+ )
321+ });
322+ let id = self.layout.add_window(output, tag, float);
303323 if rule.tag.is_some() || rule.output.is_some() {
304324 self.placed_by_rule.insert(id);
305325 }
@@ -353,19 +373,22 @@ impl State {
353373 }
354374
355375 pub fn unmap_window(&mut self, surface: &WlSurface) {
356- self.unmapped
357- .retain(|w| w.toplevel().is_some_and(|t| t.wl_surface() != surface));
376+ self.unmapped.retain(|w| root(w).as_ref() != Some(surface));
358377 if let Some(id) = self.window_id(surface) {
359- if let Some(window) = self.windows.remove(&id) {
360- self.space.unmap_elem(&window);
361- self.leave_ghost(id, &window);
362- }
363- self.layout.remove_window(id);
364- self.placed_by_rule.remove(&id);
365- self.apply_layout();
378+ self.remove_window(id);
366379 }
367380 }
368381
382+ pub fn remove_window(&mut self, id: WindowId) {
383+ if let Some(window) = self.windows.remove(&id) {
384+ self.space.unmap_elem(&window);
385+ self.leave_ghost(id, &window);
386+ }
387+ self.layout.remove_window(id);
388+ self.placed_by_rule.remove(&id);
389+ self.apply_layout();
390+ }
391+
369392 /// Keeps a visible tiled window's last frame shrinking to its seed while
370393 /// its neighbours close over it.
371394 fn leave_ghost(&mut self, id: WindowId, window: &Window) {
@@ -388,7 +411,7 @@ impl State {
388411 else {
389412 return;
390413 };
391- let Some(surface) = window.toplevel().map(|t| t.wl_surface().clone()) else {
414+ let Some(surface) = root(window) else {
392415 return;
393416 };
394417 let _ = import_surface_tree(&mut self.renderer, &surface);
@@ -416,7 +439,7 @@ impl State {
416439 pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {
417440 self.windows
418441 .iter()
419- .find(|(_, w)| w.toplevel().is_some_and(|t| t.wl_surface() == surface))
442+ .find(|(_, w)| root(w).as_ref() == Some(surface))
420443 .map(|(id, _)| *id)
421444 }
422445
@@ -541,14 +564,15 @@ impl State {
541564 t.send_pending_configure();
542565 }
543566 }
544- let target = if self.lock.active() {
545- self.lock_keyboard_focus()
567+ let target: Option<Focus> = if self.lock.active() {
568+ self.lock_keyboard_focus().map(Focus::from)
546569 } else {
547- self.layer_keyboard_focus().or_else(|| {
548- focused
549- .and_then(|id| self.windows.get(&id))
550- .and_then(|w| w.toplevel())
551- .map(|t| t.wl_surface().clone())
570+ self.layer_keyboard_focus().map(Focus::from).or_else(|| {
571+ let w = focused.and_then(|id| self.windows.get(&id))?;
572+ match w.x11_surface() {
573+ Some(x) => Some(Focus::X11(Box::new(x.clone()))),
574+ None => w.toplevel().map(|t| Focus::from(t.wl_surface().clone())),
575+ }
552576 })
553577 };
554578 if let Some(keyboard) = self.seat.get_keyboard()
@@ -611,6 +635,21 @@ impl State {
611635 if self.lock.active() {
612636 return self.lock_surface_under(pos);
613637 }
638+ let unmanaged = self.xwayland.iter().flat_map(|x| x.unmanaged.iter().rev());
639+ for x in unmanaged {
640+ let geo = x.geometry();
641+ if let Some(s) = x.wl_surface()
642+ && geo.to_f64().contains(pos)
643+ && let Some((s, p)) = under_from_surface_tree(
644+ &s,
645+ pos - geo.loc.to_f64(),
646+ (0, 0),
647+ WindowSurfaceType::ALL,
648+ )
649+ {
650+ return Some((s, (p + geo.loc).to_f64()));
651+ }
652+ }
614653 let fullscreen = self.layout.outputs().any(|(o, out)| {
615654 let a = out.area;
616655 (a.x..a.right()).contains(&pos.x)
@@ -653,6 +692,12 @@ fn configure(window: &Window, rect: Rect, kind: Kind, maximized: bool) -> bool {
653692 if w < 1 || h < 1 {
654693 return false;
655694 }
695+ let (x0, y0) = (x as i32, y as i32);
696+ if let Some(x) = window.x11_surface() {
697+ let _ = x.configure(Rectangle::new((x0, y0).into(), (w, h).into()));
698+ let _ = x.set_fullscreen(kind == Kind::Fullscreen);
699+ let _ = x.set_maximized(maximized && kind == Kind::Tiled);
700+ }
656701 if let Some(t) = window.toplevel() {
657702 t.with_pending_state(|s| {
658703 s.size = Some((w, h).into());
@@ -691,7 +736,19 @@ pub fn now_ns() -> u64 {
691736 Duration::from(Clock::<Monotonic>::new().now()).as_nanos() as u64
692737 }
693738
694-pub fn identity(t: &smithay::wayland::shell::xdg::ToplevelSurface) -> (String, String) {
739+/// A window's surface, for xdg toplevels and X11 windows alike.
740+pub fn root(window: &Window) -> Option<WlSurface> {
741+ window.wl_surface().map(|s| s.into_owned())
742+}
743+
744+/// A window's app id and title. X11 windows give their class.
745+pub fn identity(window: &Window) -> (String, String) {
746+ if let Some(x) = window.x11_surface() {
747+ return (x.class(), x.title());
748+ }
749+ let Some(t) = window.toplevel() else {
750+ return Default::default();
751+ };
695752 with_states(t.wl_surface(), |s| {
696753 let data = s
697754 .data_map
Added file: crates/monolith/src/xwayland.rs+382 additions
@@ -0,0 +1,382 @@
1+//! Lazy Xwayland. monolith owns an X display socket from startup and only
2+//! starts Xwayland when a client connects, handing it the socket so the
3+//! connection that woke it is served. Xwayland exits once its last client
4+//! leaves and the socket is watched again.
5+
6+use crate::state::State;
7+use smithay::desktop::Window;
8+use smithay::reexports::calloop::generic::Generic;
9+use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};
10+use smithay::reexports::rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};
11+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12+use smithay::utils::{Logical, Rectangle};
13+use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
14+use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowType, XwmId};
15+use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};
16+use std::os::fd::AsFd;
17+use std::os::linux::net::SocketAddrExt;
18+use std::os::unix::net::{SocketAddr, UnixListener};
19+use std::process::Stdio;
20+
21+pub struct Xwayland {
22+ pub display: u32,
23+ listeners: Vec<UnixListener>,
24+ watch: Vec<(usize, RegistrationToken)>,
25+ running: bool,
26+ pub wm: Option<X11Wm>,
27+ /// Override-redirect windows such as menus and tooltips, bottom to top.
28+ pub unmanaged: Vec<X11Surface>,
29+}
30+
31+impl Drop for Xwayland {
32+ fn drop(&mut self) {
33+ let _ = std::fs::remove_file(socket_path(self.display));
34+ let _ = std::fs::remove_file(lock_path(self.display));
35+ }
36+}
37+
38+fn socket_path(n: u32) -> String {
39+ format!("/tmp/.X11-unix/X{n}")
40+}
41+
42+fn lock_path(n: u32) -> String {
43+ format!("/tmp/.X{n}-lock")
44+}
45+
46+/// Claims the first free X display, taking over stale locks left by dead
47+/// servers.
48+fn claim() -> Option<(u32, Vec<UnixListener>)> {
49+ let _ = std::fs::create_dir_all("/tmp/.X11-unix");
50+ for n in 0..64 {
51+ let lock = lock_path(n);
52+ let created = std::fs::OpenOptions::new()
53+ .write(true)
54+ .create_new(true)
55+ .open(&lock);
56+ let file = match created {
57+ Ok(f) => f,
58+ Err(_) => {
59+ let owner = std::fs::read_to_string(&lock)
60+ .ok()
61+ .and_then(|s| s.trim().parse::<u32>().ok());
62+ let stale = owner
63+ .is_some_and(|pid| !std::path::Path::new(&format!("/proc/{pid}")).exists());
64+ if !stale || std::fs::remove_file(&lock).is_err() {
65+ continue;
66+ }
67+ match std::fs::OpenOptions::new()
68+ .write(true)
69+ .create_new(true)
70+ .open(&lock)
71+ {
72+ Ok(f) => f,
73+ Err(_) => continue,
74+ }
75+ }
76+ };
77+ use std::io::Write;
78+ let _ = writeln!(&file, "{:>10}", std::process::id());
79+ let path = socket_path(n);
80+ let _ = std::fs::remove_file(&path);
81+ let Ok(fs) = UnixListener::bind(&path) else {
82+ let _ = std::fs::remove_file(&lock);
83+ continue;
84+ };
85+ let mut listeners = vec![fs];
86+ if let Ok(addr) = SocketAddr::from_abstract_name(path.as_bytes())
87+ && let Ok(abs) = UnixListener::bind_addr(&addr)
88+ {
89+ listeners.push(abs);
90+ }
91+ return Some((n, listeners));
92+ }
93+ None
94+}
95+
96+pub fn init(state: &mut State) {
97+ let Some((number, listeners)) = claim() else {
98+ tracing::warn!("no free X display, X11 clients will not run");
99+ return;
100+ };
101+ let mut watch = Vec::new();
102+ for (i, l) in listeners.iter().enumerate() {
103+ let Ok(l) = l.try_clone() else { continue };
104+ let token = state.handle.insert_source(
105+ Generic::new(l, Interest::READ, Mode::Level),
106+ move |_, _, state| {
107+ start(state, i);
108+ Ok(PostAction::Disable)
109+ },
110+ );
111+ if let Ok(t) = token {
112+ watch.push((i, t));
113+ }
114+ }
115+ tracing::info!("X11 clients will start Xwayland on :{number}");
116+ state.xwayland = Some(Xwayland {
117+ display: number,
118+ listeners,
119+ watch,
120+ running: false,
121+ wm: None,
122+ unmanaged: Vec::new(),
123+ });
124+}
125+
126+/// Waits for the next X client to knock.
127+fn rearm(state: &mut State) {
128+ let Some(x) = state.xwayland.as_mut() else {
129+ return;
130+ };
131+ x.running = false;
132+ for (_, t) in &x.watch {
133+ let _ = state.handle.enable(t);
134+ }
135+}
136+
137+fn set_inherit(listeners: &[UnixListener], inherit: bool) {
138+ for l in listeners {
139+ let flags = fcntl_getfd(l.as_fd()).unwrap_or(FdFlags::CLOEXEC);
140+ let flags = if inherit {
141+ flags - FdFlags::CLOEXEC
142+ } else {
143+ flags | FdFlags::CLOEXEC
144+ };
145+ let _ = fcntl_setfd(l.as_fd(), flags);
146+ }
147+}
148+
149+fn start(state: &mut State, fired: usize) {
150+ let Some(x) = state.xwayland.as_mut() else {
151+ return;
152+ };
153+ if x.running {
154+ return;
155+ }
156+ x.running = true;
157+ for (i, t) in &x.watch {
158+ if *i != fired {
159+ state.handle.disable(t).ok();
160+ }
161+ }
162+ let mut args = Vec::new();
163+ for l in &x.listeners {
164+ args.push("-listenfd".to_string());
165+ args.push(std::os::fd::AsRawFd::as_raw_fd(l).to_string());
166+ }
167+ // smithay only passes on the sockets it creates itself, so ours are
168+ // made inheritable just for this one spawn.
169+ set_inherit(&x.listeners, true);
170+ let spawned = XWayland::spawn(
171+ &state.display,
172+ None,
173+ std::iter::empty::<(String, String)>(),
174+ args,
175+ false,
176+ Stdio::null(),
177+ Stdio::null(),
178+ |_| {},
179+ );
180+ set_inherit(&x.listeners, false);
181+ let (xwayland, client) = match spawned {
182+ Ok(s) => s,
183+ Err(e) => {
184+ tracing::error!("cannot start Xwayland, X11 clients will not run: {e}");
185+ return;
186+ }
187+ };
188+ tracing::info!("starting Xwayland");
189+ let inserted = state
190+ .handle
191+ .insert_source(xwayland, move |event, _, state| match event {
192+ XWaylandEvent::Ready { x11_socket, .. } => {
193+ let dh = state.display.clone();
194+ match X11Wm::start_wm(state.handle.clone(), &dh, x11_socket, client.clone()) {
195+ Ok(wm) => {
196+ tracing::info!("Xwayland ready");
197+ if let Some(x) = state.xwayland.as_mut() {
198+ x.wm = Some(wm);
199+ }
200+ }
201+ Err(e) => tracing::error!("cannot manage Xwayland windows: {e}"),
202+ }
203+ }
204+ XWaylandEvent::Error => {
205+ tracing::warn!("Xwayland failed to start");
206+ rearm(state);
207+ }
208+ });
209+ if let Err(e) = inserted {
210+ tracing::error!("cannot watch Xwayland: {e}");
211+ }
212+}
213+
214+impl XWaylandShellHandler for State {
215+ fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
216+ &mut self.xwayland_shell_state
217+ }
218+
219+ fn surface_associated(&mut self, _xwm: XwmId, surface: WlSurface, _window: X11Surface) {
220+ self.on_buffer(&surface);
221+ self.needs_redraw = true;
222+ }
223+}
224+
225+impl XwmHandler for State {
226+ fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
227+ self.xwayland
228+ .as_mut()
229+ .and_then(|x| x.wm.as_mut())
230+ .expect("X11 events only arrive while the WM runs")
231+ }
232+
233+ fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
234+
235+ fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
236+
237+ fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
238+ if let Err(e) = window.set_mapped(true) {
239+ tracing::warn!("cannot map X11 window: {e}");
240+ return;
241+ }
242+ let float = floats(&window).then(|| window.geometry());
243+ self.map_window(Window::new_x11_window(window), float);
244+ }
245+
246+ fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
247+ if let Some(x) = self.xwayland.as_mut() {
248+ x.unmanaged.retain(|w| w != &window);
249+ x.unmanaged.push(window);
250+ }
251+ self.needs_redraw = true;
252+ }
253+
254+ fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
255+ self.forget_x11(&window);
256+ if !window.is_override_redirect() {
257+ let _ = window.set_mapped(false);
258+ }
259+ }
260+
261+ fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) {
262+ self.forget_x11(&window);
263+ }
264+
265+ fn configure_request(
266+ &mut self,
267+ _xwm: XwmId,
268+ window: X11Surface,
269+ x: Option<i32>,
270+ y: Option<i32>,
271+ w: Option<u32>,
272+ h: Option<u32>,
273+ _reorder: Option<Reorder>,
274+ ) {
275+ if self.x11_window_id(&window).is_some() {
276+ let _ = window.configure(None);
277+ return;
278+ }
279+ let mut geo = window.geometry();
280+ geo.loc.x = x.unwrap_or(geo.loc.x);
281+ geo.loc.y = y.unwrap_or(geo.loc.y);
282+ geo.size.w = w.map_or(geo.size.w, |w| w as i32);
283+ geo.size.h = h.map_or(geo.size.h, |h| h as i32);
284+ let _ = window.configure(geo);
285+ }
286+
287+ fn configure_notify(
288+ &mut self,
289+ _xwm: XwmId,
290+ window: X11Surface,
291+ _geometry: Rectangle<i32, Logical>,
292+ _above: Option<smithay::xwayland::xwm::X11Window>,
293+ ) {
294+ if window.is_override_redirect() {
295+ self.needs_redraw = true;
296+ }
297+ }
298+
299+ fn resize_request(
300+ &mut self,
301+ _xwm: XwmId,
302+ _window: X11Surface,
303+ _button: u32,
304+ _edge: ResizeEdge,
305+ ) {
306+ }
307+
308+ fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}
309+
310+ fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
311+ self.x11_fullscreen(&window, true);
312+ }
313+
314+ fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
315+ self.x11_fullscreen(&window, false);
316+ }
317+
318+ fn disconnected(&mut self, _xwm: XwmId) {
319+ tracing::info!("Xwayland exited");
320+ let gone: Vec<_> = self
321+ .windows
322+ .iter()
323+ .filter(|(_, w)| w.x11_surface().is_some())
324+ .map(|(id, _)| *id)
325+ .collect();
326+ for id in gone {
327+ self.remove_window(id);
328+ }
329+ if let Some(x) = self.xwayland.as_mut() {
330+ x.wm = None;
331+ x.unmanaged.clear();
332+ }
333+ rearm(self);
334+ }
335+}
336+
337+/// Dialogs, splash screens and fixed-size windows float at their own size.
338+fn floats(window: &X11Surface) -> bool {
339+ let kind = matches!(
340+ window.window_type(),
341+ Some(
342+ WmWindowType::Dialog
343+ | WmWindowType::Splash
344+ | WmWindowType::Utility
345+ | WmWindowType::Toolbar
346+ | WmWindowType::Menu
347+ )
348+ );
349+ let fixed = matches!(
350+ (window.min_size(), window.max_size()),
351+ (Some(min), Some(max)) if min == max && min.w > 0
352+ );
353+ kind || fixed || window.is_transient_for().is_some()
354+}
355+
356+impl State {
357+ pub fn x11_window_id(&self, window: &X11Surface) -> Option<monolith_layout::WindowId> {
358+ self.windows
359+ .iter()
360+ .find(|(_, w)| w.x11_surface() == Some(window))
361+ .map(|(id, _)| *id)
362+ }
363+
364+ fn forget_x11(&mut self, window: &X11Surface) {
365+ if let Some(x) = self.xwayland.as_mut() {
366+ x.unmanaged.retain(|w| w != window);
367+ }
368+ if let Some(id) = self.x11_window_id(window) {
369+ self.remove_window(id);
370+ }
371+ self.needs_redraw = true;
372+ }
373+
374+ fn x11_fullscreen(&mut self, window: &X11Surface, on: bool) {
375+ if let Some(id) = self.x11_window_id(window)
376+ && self.layout.window(id).is_some_and(|w| w.fullscreen != on)
377+ {
378+ self.layout.toggle_fullscreen(id);
379+ self.apply_layout();
380+ }
381+ }
382+}