mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Implement fetching xkb options from org.freedesktop.locale1
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user