From 0e3d078a85cec867aa3f49b9fe3d1261e36eb901 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 15 Jul 2025 15:44:10 +0300 Subject: Implement fetching xkb options from org.freedesktop.locale1 --- resources/default-config.kdl | 3 + src/dbus/freedesktop_locale1.rs | 144 ++++++++++++++++++++++++++++++++++++++++ src/dbus/mod.rs | 18 +++++ src/niri.rs | 38 ++++++++++- wiki/Configuration:-Input.md | 7 ++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/dbus/freedesktop_locale1.rs diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 143550b0..f2fa8609 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -15,6 +15,9 @@ input { // For example: // layout "us,ru" // options "grp:win_space_toggle,compose:ralt,ctrl:nocaps" + + // If this section is empty, niri will fetch xkb settings + // from org.freedesktop.locale1. } // Enable numlock on startup, omitting this setting disables it. diff --git a/src/dbus/freedesktop_locale1.rs b/src/dbus/freedesktop_locale1.rs new file mode 100644 index 00000000..3f7c2484 --- /dev/null +++ b/src/dbus/freedesktop_locale1.rs @@ -0,0 +1,144 @@ +use futures_util::StreamExt; +use niri_config::Xkb; +use zbus::names::InterfaceName; +use zbus::{fdo, zvariant}; + +pub enum Locale1ToNiri { + XkbChanged(Xkb), +} + +pub fn start( + to_niri: calloop::channel::Sender, +) -> anyhow::Result { + let conn = zbus::blocking::Connection::system()?; + + let async_conn = conn.inner().clone(); + let future = async move { + let proxy = fdo::PropertiesProxy::new( + &async_conn, + "org.freedesktop.locale1", + "/org/freedesktop/locale1", + ) + .await; + let proxy = match proxy { + Ok(x) => x, + Err(err) => { + warn!("error creating PropertiesProxy: {err:?}"); + return; + } + }; + + let mut props_changed = match proxy.receive_properties_changed().await { + Ok(x) => x, + Err(err) => { + warn!("error subscribing to PropertiesChanged: {err:?}"); + return; + } + }; + + let props = proxy + .get_all(InterfaceName::try_from("org.freedesktop.locale1").unwrap()) + .await; + let mut props = match props { + Ok(x) => x, + Err(err) => { + warn!("error receiving initial properties: {err:?}"); + return; + } + }; + + trace!("initial properties: {props:?}"); + + let mut get = |name| { + props + .remove(name) + .and_then(|x| String::try_from(x).ok()) + .unwrap_or_default() + }; + + let mut xkb = Xkb { + rules: String::new(), + model: get("X11Model"), + layout: get("X11Layout"), + variant: get("X11Variant"), + options: match get("X11Options") { + x if x.is_empty() => None, + x => Some(x), + }, + file: None, + }; + + // Send the initial properties. + if let Err(err) = to_niri.send(Locale1ToNiri::XkbChanged(xkb.clone())) { + warn!("error sending message to niri: {err:?}"); + return; + }; + + while let Some(changed) = props_changed.next().await { + let args = match changed.args() { + Ok(args) => args, + Err(err) => { + warn!("error parsing locale1 PropertiesChanged args: {err:?}"); + return; + } + }; + + let mut changed = false; + for (name, value) in args.changed_properties() { + trace!("changed property: {name} => {value:?}"); + + let value = zvariant::Str::try_from(value).unwrap_or_default(); + let value = value.as_str(); + + match *name { + "X11Model" => { + if xkb.model != value { + xkb.model = String::from(value); + changed = true; + } + } + "X11Layout" => { + if xkb.layout != value { + xkb.layout = String::from(value); + changed = true; + } + } + "X11Variant" => { + if xkb.variant != value { + xkb.variant = String::from(value); + changed = true; + } + } + "X11Options" => { + let value = match value { + "" => None, + x => Some(x), + }; + if xkb.options.as_deref() != value { + xkb.options = value.map(String::from); + changed = true; + } + } + _ => (), + } + } + + if !changed { + continue; + } + + if let Err(err) = to_niri.send(Locale1ToNiri::XkbChanged(xkb.clone())) { + warn!("error sending message to niri: {err:?}"); + return; + }; + } + }; + + let task = conn + .inner() + .executor() + .spawn(future, "monitor locale1 property changes"); + task.detach(); + + Ok(conn) +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 09d973ad..aaa7c1ae 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -3,6 +3,7 @@ use zbus::object_server::Interface; use crate::niri::State; +pub mod freedesktop_locale1; pub mod freedesktop_screensaver; pub mod gnome_shell_introspect; pub mod gnome_shell_screenshot; @@ -32,6 +33,7 @@ pub struct DBusServers { pub conn_introspect: Option, #[cfg(feature = "xdp-gnome-screencast")] pub conn_screen_cast: Option, + pub conn_locale1: Option, } impl DBusServers { @@ -125,6 +127,22 @@ impl DBusServers { } } + let (to_niri, from_locale1) = calloop::channel::channel(); + niri.event_loop + .insert_source(from_locale1, move |event, _, state| match event { + calloop::channel::Event::Msg(msg) => state.on_locale1_msg(msg), + calloop::channel::Event::Closed => (), + }) + .unwrap(); + match freedesktop_locale1::start(to_niri) { + Ok(conn) => { + dbus.conn_locale1 = Some(conn); + } + Err(err) => { + warn!("error starting locale1 watcher: {err:?}"); + } + } + niri.dbus = Some(dbus); } } diff --git a/src/niri.rs b/src/niri.rs index 60365e0b..03fe6d4f 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -15,7 +15,7 @@ use anyhow::{bail, ensure, Context}; use calloop::futures::Scheduler; use niri_config::{ Config, FloatOrInt, Key, Modifiers, OutputName, PreviewRender, TrackLayout, - WarpMouseToFocusMode, WorkspaceReference, + WarpMouseToFocusMode, WorkspaceReference, Xkb, }; use smithay::backend::allocator::Fourcc; use smithay::backend::input::Keycode; @@ -115,6 +115,8 @@ use crate::backend::tty::SurfaceDmabufFeedback; use crate::backend::{Backend, Headless, RenderResult, Tty, Winit}; use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor}; #[cfg(feature = "dbus")] +use crate::dbus::freedesktop_locale1::Locale1ToNiri; +#[cfg(feature = "dbus")] use crate::dbus::gnome_shell_introspect::{self, IntrospectToNiri, NiriToIntrospect}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; @@ -319,6 +321,9 @@ pub struct Niri { pub is_fdo_idle_inhibited: Arc, pub keyboard_shortcuts_inhibiting_surfaces: HashMap, + /// Most recent XKB settings from org.freedesktop.locale1. + pub xkb_from_locale1: Option, + pub cursor_manager: CursorManager, pub cursor_texture_cache: CursorTextureCache, pub cursor_shape_manager_state: CursorShapeManagerState, @@ -1490,6 +1495,12 @@ impl State { } if set_xkb_config { + // If xkb is unset in the niri config, use settings from locale1. + if xkb == Xkb::default() { + trace!("using xkb from locale1"); + xkb = self.niri.xkb_from_locale1.clone().unwrap_or_default(); + } + let keyboard = self.niri.seat.get_keyboard().unwrap(); if let Err(err) = keyboard.set_xkb_config(self, xkb.to_xkb_config()) { warn!("error updating xkb config: {err:?}"); @@ -2226,6 +2237,30 @@ impl State { warn!("error sending windows to introspect: {err:?}"); } } + + #[cfg(feature = "dbus")] + pub fn on_locale1_msg(&mut self, msg: Locale1ToNiri) { + let Locale1ToNiri::XkbChanged(xkb) = msg; + + trace!("locale1 xkb settings changed: {xkb:?}"); + let xkb = self.niri.xkb_from_locale1.insert(xkb); + + { + let config = self.niri.config.borrow(); + if config.input.keyboard.xkb != Xkb::default() { + trace!("ignoring locale1 xkb change because niri config has xkb settings"); + return; + } + } + + let xkb = xkb.clone(); + let keyboard = self.niri.seat.get_keyboard().unwrap(); + if let Err(err) = keyboard.set_xkb_config(self, xkb.to_xkb_config()) { + warn!("error updating xkb config: {err:?}"); + } + + self.ipc_keyboard_layouts_changed(); + } } impl Niri { @@ -2583,6 +2618,7 @@ impl Niri { idle_inhibiting_surfaces: HashSet::new(), is_fdo_idle_inhibited: Arc::new(AtomicBool::new(false)), keyboard_shortcuts_inhibiting_surfaces: HashMap::new(), + xkb_from_locale1: None, cursor_manager, cursor_texture_cache: Default::default(), cursor_shape_manager_state, diff --git a/wiki/Configuration:-Input.md b/wiki/Configuration:-Input.md index b1e3a279..861bd207 100644 --- a/wiki/Configuration:-Input.md +++ b/wiki/Configuration:-Input.md @@ -143,6 +143,13 @@ input { > } > ``` +> [!NOTE] +> +> Since: next release +> +> If the `xkb` section is empty (like it is by default), niri will fetch xkb settings from systemd-localed at `org.freedesktop.locale1` over D-Bus. +> This way, for example, system installers can dynamically set the niri keyboard layout. + When using multiple layouts, niri can remember the current layout globally (the default) or per-window. You can control this with the `track-layout` option. -- cgit