Implement fetching xkb options from org.freedesktop.locale1

This commit is contained in:
Ivan Molodetskikh
2025-07-15 15:44:10 +03:00
parent 36efd6e3f9
commit 0e3d078a85
5 changed files with 209 additions and 1 deletions
+3
View File
@@ -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.
+144
View File
@@ -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<Locale1ToNiri>,
) -> anyhow::Result<zbus::blocking::Connection> {
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)
}
+18
View File
@@ -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<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
pub conn_locale1: Option<Connection>,
}
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);
}
}
+37 -1
View File
@@ -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<AtomicBool>,
pub keyboard_shortcuts_inhibiting_surfaces: HashMap<WlSurface, KeyboardShortcutsInhibitor>,
/// Most recent XKB settings from org.freedesktop.locale1.
pub xkb_from_locale1: Option<Xkb>,
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,
+7
View File
@@ -143,6 +143,13 @@ input {
> }
> ```
> [!NOTE]
>
> <sup>Since: next release</sup>
>
> 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.