diff --git a/modules/nixos/desktop/desktop-niri.ulu.nix b/modules/nixos/desktop/desktop-niri.ulu.nix index db4c083..f8000a0 100644 --- a/modules/nixos/desktop/desktop-niri.ulu.nix +++ b/modules/nixos/desktop/desktop-niri.ulu.nix @@ -4,16 +4,6 @@ { pkgs, ... }: { programs.niri.enable = true; - nixpkgs.overlays = [ - (final: prev: { - xwayland-satellite = prev.xwayland-satellite.overrideAttrs (old: { - patches = - (old.patches or [ ]) - ++ prev.lib.optional (prev.lib.versionOlder old.version "0.8.3") ./xwayland-satellite-no-focus-override-redirect.patch - ++ prev.lib.optional (prev.lib.versionOlder old.version "0.8.4") ./xwayland-satellite-resizable-dialog-toplevel.patch; - }); - }) - ]; environment.systemPackages = with pkgs; [ xwayland-satellite diff --git a/modules/nixos/desktop/xwayland-satellite-no-focus-override-redirect.patch b/modules/nixos/desktop/xwayland-satellite-no-focus-override-redirect.patch deleted file mode 100644 index 5460222..0000000 --- a/modules/nixos/desktop/xwayland-satellite-no-focus-override-redirect.patch +++ /dev/null @@ -1,437 +0,0 @@ -diff --git a/src/lib.rs b/src/lib.rs -index 5c861ea0..d3bf003a 100644 ---- a/src/lib.rs -+++ b/src/lib.rs -@@ -20,6 +20,7 @@ pub trait XConnection: Sized + 'static { - fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState) -> bool; - fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool); - fn focus_window(&mut self, window: x::Window, output_name: Option); -+ fn send_take_focus(&mut self, window: x::Window); - fn close_window(&mut self, window: x::Window); - fn unmap_window(&mut self, window: x::Window); - fn raise_to_top(&mut self, window: x::Window); -diff --git a/src/server/event.rs b/src/server/event.rs -index c75b9f19..4fcf9230 100644 ---- a/src/server/event.rs -+++ b/src/server/event.rs -@@ -438,6 +438,7 @@ impl SurfaceEvents { - window, - output_name: None, - is_popup: true, -+ has_take_focus: window_data.attrs.has_take_focus, - }); - } - } -@@ -864,10 +865,13 @@ impl Event for client::wl_keyboard::Event { - serial, - )); - let output_name = get_output_name(output, &state.world); -+ let window_data = data.get::<&WindowData>(); -+ let has_take_focus = window_data.as_ref().is_some_and(|d| d.attrs.has_take_focus); - state.to_focus = Some(FocusData { - window: *window, - output_name, - is_popup: false, -+ has_take_focus, - }); - keyboard.enter(serial, surface, keys); - } -diff --git a/src/server/mod.rs b/src/server/mod.rs -index 379df771..a436331b 100644 ---- a/src/server/mod.rs -+++ b/src/server/mod.rs -@@ -101,9 +101,10 @@ where - - #[derive(Default, Debug)] - struct WindowAttributes { -- acquire_input_via_wm: bool, -+ accepts_input: bool, - has_take_focus: bool, - role: WindowRole, -+ override_redirect: bool, - dims: WindowDims, - size_hints: Option, - title: Option, -@@ -114,9 +115,8 @@ struct WindowAttributes { - } - - impl WindowAttributes { -- /// AKA "Passive" input model - fn require_wm_focus(&self) -> bool { -- self.acquire_input_via_wm && !self.has_take_focus -+ !self.override_redirect && (self.has_take_focus || self.accepts_input) - } - } - -@@ -140,6 +140,7 @@ impl WindowData { - mapped: false, - attrs: WindowAttributes { - role: WindowRole::new_basic(override_redirect), -+ override_redirect, - dims, - ..Default::default() - }, -@@ -398,6 +399,7 @@ struct FocusData { - window: x::Window, - output_name: Option, - is_popup: bool, -+ has_take_focus: bool, - } - - #[derive(Copy, Clone, Default)] -@@ -422,6 +424,9 @@ impl XConnection for NoConnection { - fn focus_window(&mut self, _: x::Window, _: Option) { - debug!("could not focus window without XWayland initialized"); - } -+ fn send_take_focus(&mut self, _: x::Window) { -+ debug!("could not send take focus without XWayland initialized"); -+ } - fn close_window(&mut self, _: x::Window) { - debug!("could not close window without XWayland initialized"); - } -@@ -742,15 +747,20 @@ impl ServerState { - window, - output_name, - is_popup, -+ has_take_focus, - }) = self.to_focus.take() - { - debug!( -- "focusing {} {window:?}", -+ "focusing (take_focus={has_take_focus:?}) {} {window:?}", - if is_popup { "popup" } else { "window" } - ); -- self.connection.focus_window(window, output_name); -- if !is_popup { -- self.last_focused_toplevel = Some(window); -+ if has_take_focus { -+ self.connection.send_take_focus(window); -+ } else { -+ self.connection.focus_window(window, output_name); -+ if !is_popup { -+ self.last_focused_toplevel = Some(window); -+ } - } - } else if self.unfocus { - self.connection.focus_window(x::WINDOW_NONE, None); -@@ -888,9 +898,24 @@ impl InnerServerState { - self.windows.insert(window, id); - } - -+ pub fn set_override_redirect(&mut self, window: x::Window, override_redirect: bool) { -+ let Some(id) = self.windows.get(&window).copied() else { -+ debug!( -+ "not setting override_redirect {override_redirect} for unknown window {window:?}" -+ ); -+ return; -+ }; -+ -+ self.world -+ .get::<&mut WindowData>(id) -+ .unwrap() -+ .attrs -+ .override_redirect = override_redirect; -+ } -+ - pub fn set_window_role(&mut self, window: x::Window, role: WindowRole) { - let Some(id) = self.windows.get(&window).copied() else { -- debug!("not setting popup for unknown window {window:?}"); -+ debug!("not setting role {role:?} for unknown window {window:?}"); - return; - }; - -@@ -969,7 +994,7 @@ impl InnerServerState { - - let attrs = &mut self.world.get::<&mut WindowData>(id).unwrap().attrs; - attrs.group = hints.window_group; -- attrs.acquire_input_via_wm = hints.acquire_input_via_wm; -+ attrs.accepts_input = hints.accepts_input; - } - - pub fn set_take_focus(&mut self, window: x::Window, has_take_focus: bool) { -diff --git a/src/server/tests.rs b/src/server/tests.rs -index 0651bb2a..4e1735ee 100644 ---- a/src/server/tests.rs -+++ b/src/server/tests.rs -@@ -160,6 +160,7 @@ struct WindowData { - #[derive(Default)] - struct FakeXConnection { - focused_window: Option, -+ send_take_focus_window: Option, - windows: HashMap, - set_window_dims_counter: usize, - } -@@ -234,6 +235,15 @@ impl super::XConnection for FakeXConnection { - self.focused_window = window.into(); - } - -+ #[track_caller] -+ fn send_take_focus(&mut self, window: x::Window) { -+ assert!( -+ self.windows.contains_key(&window), -+ "Unknown window: {window:?}" -+ ); -+ self.send_take_focus_window = window.into(); -+ } -+ - fn raise_to_top(&mut self, window: Window) { - assert!( - self.windows.contains_key(&window), -@@ -858,6 +868,55 @@ impl TestFixture { - (surface, popup_id) - } - -+ #[track_caller] -+ fn setup_popup_focus_case( -+ &mut self, -+ comp: &Compositor, -+ override_redirect: bool, -+ accepts_input: bool, -+ take_focus: bool, -+ ) -> (Window, Window) { -+ let win_toplevel = Window::new(1); -+ self.create_toplevel(comp, win_toplevel); -+ -+ let win_popup = Window::new(2); -+ let (buffer, surface) = comp.create_surface(); -+ let data = WindowData { -+ mapped: true, -+ dims: WindowDims { -+ x: 10, -+ y: 20, -+ width: 100, -+ height: 50, -+ }, -+ fullscreen: false, -+ }; -+ self.new_window(win_popup, override_redirect, data); -+ if !override_redirect { -+ self.satellite -+ .set_window_role(win_popup, crate::xstate::WindowRole::Popup); -+ } -+ -+ self.satellite.set_win_hints( -+ win_popup, -+ super::WmHints { -+ window_group: None, -+ accepts_input, -+ }, -+ ); -+ if take_focus { -+ self.satellite.set_take_focus(win_popup, true); -+ } -+ self.map_window(comp, win_popup, &surface.obj, &buffer); -+ self.run(); -+ -+ let popup_id = self.check_new_surface(); -+ self.testwl.configure_popup(popup_id); -+ self.run(); -+ -+ (win_toplevel, win_popup) -+ } -+ - #[track_caller] - fn assert_window_dimensions( - &self, -@@ -1342,7 +1401,7 @@ fn window_group_properties() { - win, - super::WmHints { - window_group: Some(prop_win), -- acquire_input_via_wm: false, -+ accepts_input: false, - }, - ); - f.satellite.map_window(win); -@@ -1647,103 +1706,51 @@ fn override_redirect_choose_hover_window() { - } - - #[test] --fn popup_no_focus_without_input_hint() { -- let (mut f, comp) = TestFixture::new_with_compositor(); -+fn popup_override_redirect_never_focused_nor_offered() { -+ for accepts_input in [true, false] { -+ for take_focus in [false, true] { -+ let (mut f, comp) = TestFixture::new_with_compositor(); -+ -+ let (win_toplevel, _) = -+ f.setup_popup_focus_case(&comp, true, accepts_input, take_focus); -+ assert_eq!( -+ f.connection().focused_window, -+ Some(win_toplevel), -+ "accepts_input={accepts_input:?} take_focus={take_focus}" -+ ); -+ assert_eq!( -+ f.connection().send_take_focus_window, -+ None, -+ "accepts_input={accepts_input:?} take_focus={take_focus}" -+ ); -+ } -+ } -+} - -- let win_toplevel = Window::new(1); -- let (_, toplevel_id) = f.create_toplevel(&comp, win_toplevel); -+#[test] -+fn popup_send_take_focus_when_advertised() { -+ for accepts_input in [true, false] { -+ let (mut f, comp) = TestFixture::new_with_compositor(); - -- // A popup without acquire_input_via_wm should not receive focus. -- let win_popup = Window::new(2); -- f.create_popup( -- &comp, -- PopupBuilder::new(win_popup, win_toplevel, toplevel_id), -- ); -- assert_eq!(f.connection().focused_window, Some(win_toplevel)); -+ let (win_toplevel, win_popup) = f.setup_popup_focus_case(&comp, false, accepts_input, true); -+ assert_eq!(f.connection().focused_window, Some(win_toplevel)); -+ assert_eq!(f.connection().send_take_focus_window, Some(win_popup)); -+ } - } - - #[test] - fn popup_focus_on_map_with_input_hint() { - let (mut f, comp) = TestFixture::new_with_compositor(); - -- let win_toplevel = Window::new(1); -- let (_, toplevel_id) = f.create_toplevel(&comp, win_toplevel); -- -- // A popup with acquire_input_via_wm should receive X11 keyboard focus -- // at map time, matching what a real X11 WM does for windows with -- // WM_HINTS input=True. -- let win_popup = Window::new(2); -- let (buffer, surface) = comp.create_surface(); -- let dims = WindowDims { -- x: 10, -- y: 20, -- width: 100, -- height: 50, -- }; -- let data = WindowData { -- mapped: true, -- dims, -- fullscreen: false, -- }; -- f.new_window(win_popup, true, data); -- f.satellite.set_win_hints( -- win_popup, -- super::WmHints { -- window_group: None, -- acquire_input_via_wm: true, -- }, -- ); -- f.map_window(&comp, win_popup, &surface.obj, &buffer); -- f.run(); -- -- let popup_id = f.check_new_surface(); -- assert_ne!(popup_id, toplevel_id); -- -- f.testwl.configure_popup(popup_id); -- f.run(); -- -- // Focus should have been given directly to the popup at map time. -+ let (_, win_popup) = f.setup_popup_focus_case(&comp, false, true, false); - assert_eq!(f.connection().focused_window, Some(win_popup)); - } - - #[test] --fn popup_no_focus_input_hint_wm_take_focus() { -+fn popup_no_focus_without_input_hint() { - let (mut f, comp) = TestFixture::new_with_compositor(); - -- let win_toplevel = Window::new(1); -- let (_, toplevel_id) = f.create_toplevel(&comp, win_toplevel); -- -- let win_popup = Window::new(2); -- let (buffer, surface) = comp.create_surface(); -- let dims = WindowDims { -- x: 10, -- y: 20, -- width: 100, -- height: 50, -- }; -- let data = WindowData { -- mapped: true, -- dims, -- fullscreen: false, -- }; -- f.new_window(win_popup, true, data); -- f.satellite.set_win_hints( -- win_popup, -- super::WmHints { -- window_group: None, -- acquire_input_via_wm: true, -- }, -- ); -- f.satellite.set_take_focus(win_popup, true); -- f.map_window(&comp, win_popup, &surface.obj, &buffer); -- f.run(); -- -- let popup_id = f.check_new_surface(); -- assert_ne!(popup_id, toplevel_id); -- -- f.testwl.configure_popup(popup_id); -- f.run(); -- -+ let (win_toplevel, _) = f.setup_popup_focus_case(&comp, false, false, false); - assert_eq!(f.connection().focused_window, Some(win_toplevel)); - } - -diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs -index efcb13de..7a7e02d2 100644 ---- a/src/xstate/mod.rs -+++ b/src/xstate/mod.rs -@@ -653,6 +653,7 @@ impl XState { - - let transient_for = self.get_transient_for(window)?; - let override_redirect = self.get_override_redirect(window)?; -+ server_state.set_override_redirect(window, override_redirect); - - let window_types = self - .get_net_wm_window_types(window)? -@@ -1010,7 +1011,7 @@ impl From<&[u32]> for WmNormalHints { - #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] - pub struct WmHints { - pub window_group: Option, -- pub acquire_input_via_wm: bool, -+ pub accepts_input: bool, - } - - impl From<&[u32]> for WmHints { -@@ -1022,8 +1023,11 @@ impl From<&[u32]> for WmHints { - let window = x::Window::new(value[8]); - ret.window_group = Some(window); - } -- if flags.contains(WmHintsFlags::Input) { -- ret.acquire_input_via_wm = value[1] == 1; -+ -+ // if input hint isn't present, set it to true anyways in case the client is misbehaving -+ ret.accepts_input = true; -+ if flags.contains(WmHintsFlags::Input) && value[1] == 0 { -+ ret.accepts_input = false; - } - - ret -@@ -1429,6 +1433,22 @@ impl XConnection for RealConnection { - } - } - -+ fn send_take_focus(&mut self, window: x::Window) { -+ let resource_id = self.atoms.wm_take_focus.resource_id(); -+ let data = [resource_id, x::CURRENT_TIME, 0, 0, 0]; -+ let event = &x::ClientMessageEvent::new( -+ window, -+ self.atoms.wm_protocols, -+ x::ClientMessageData::Data32(data), -+ ); -+ unwrap_or_skip_bad_window_ret!(self.connection.send_and_check_request(&x::SendEvent { -+ destination: x::SendEventDest::Window(window), -+ propagate: false, -+ event_mask: x::EventMask::empty(), -+ event, -+ })); -+ } -+ - fn close_window(&mut self, window: x::Window) { - let cookie = self.connection.send_request(&x::GetProperty { - window, diff --git a/modules/nixos/desktop/xwayland-satellite-resizable-dialog-toplevel.patch b/modules/nixos/desktop/xwayland-satellite-resizable-dialog-toplevel.patch deleted file mode 100644 index c7e028d..0000000 --- a/modules/nixos/desktop/xwayland-satellite-resizable-dialog-toplevel.patch +++ /dev/null @@ -1,49 +0,0 @@ ---- a/src/xstate/mod.rs 2026-10-10 02:55:59.663368893 +0300 -+++ b/src/xstate/mod.rs 2026-10-10 02:55:59.699369500 +0300 -@@ -1189,7 +1189,9 @@ - match ty { - x if x == window_atoms.normal => return WindowRole::Toplevel, - x if x == window_atoms.dialog => { -- return WindowRole::new_basic(self.has_transient_for && motif_no_decor); -+ return WindowRole::new_basic( -+ self.has_transient_for && motif_no_decor && forced_size, -+ ); - } - x if x == window_atoms.utility => { - return WindowRole::new_basic(motif_no_decor && forced_size); ---- a/src/xstate/tests.rs 2026-10-10 02:55:59.665368927 +0300 -+++ b/src/xstate/tests.rs 2026-10-10 02:56:08.382515889 +0300 -@@ -473,6 +473,33 @@ - assert_eq!(win.guess_window_role(&win_types), WindowRole::Popup); - } - -+ // https://github.com/Supreeeme/xwayland-satellite/issues/470 -+ // A DIALOG with TRANSIENT_FOR and no Motif decorations which is resizable -+ // (min_size != max_size) should be a top-level, mirroring the UTILITY fix in #383. -+ // Otherwise the popup parent resolves to a hidden WM_CLIENT_LEADER and the -+ // window never appears (e.g. DaVinci Resolve's Project Manager). -+ // Window attributes are taken from the full xprop posted in #470 (DIALOG + -+ // NORMAL window types, _MOTIF_WM_HINTS decorations=0, resizable normal hints). -+ #[test] -+ fn davinci_resolve_project_manager() { -+ let win_types = WindowTypes::new(); -+ let wm_normal_hints = WmNormalHints::new() -+ .user_pos(978, 270) -+ .user_size(1884, 1620) -+ .min_size(910, 640) -+ .max_size(2880, 1620) -+ .win_gravity(Gravity::Static); -+ let win = WindowRoleHeuristics { -+ has_transient_for: true, -+ motif_wm_hints: Some(motif::Hints::from([0x2_u32, 0x1, 0, 0, 0].as_slice())), -+ window_types: vec![win_types.dialog, win_types.normal], -+ wm_normal_hints: Some(wm_normal_hints.into()), -+ wm_class: Some("resolve".into()), -+ ..Default::default() -+ }; -+ assert_eq!(win.guess_window_role(&win_types), WindowRole::Toplevel); -+ } -+ - // https://github.com/Supreeeme/xwayland-satellite/pull/390 - // With Motif decorations, even a dialog with TRANSIENT_FOR is better treated as a top-level - #[test]