From bb7637ccf150ac7d24b8cb91885430246e0305e2 Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 14:04:15 -0600 Subject: [PATCH 1/9] extend config Add global_shortcuts module to config --- niri-config/src/global_shortcuts.rs | 158 ++++++++++++++++++++++++++++ niri-config/src/lib.rs | 86 ++++++++++++++- 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 niri-config/src/global_shortcuts.rs diff --git a/niri-config/src/global_shortcuts.rs b/niri-config/src/global_shortcuts.rs new file mode 100644 index 0000000000..880929c0fa --- /dev/null +++ b/niri-config/src/global_shortcuts.rs @@ -0,0 +1,158 @@ +use std::collections::HashSet; + +use knuffel::errors::DecodeError; + +use crate::binds::Key; +use crate::utils::RegexEq; + +#[derive(Debug, Default, PartialEq)] +pub struct GlobalShortcuts(pub Vec); + +#[derive(Debug, Clone, PartialEq)] +pub struct GlobalShortcut { + pub trigger: Key, + pub app_id: Selector, + pub shortcut_id: Selector, +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +pub enum Selector { + Exact(#[knuffel(argument)] String), + Match(#[knuffel(argument, str)] RegexEq), + NeverMatch, +} +impl Selector { + /// Compares the selector against a `str` + pub fn matches>(&self, v: T) -> bool { + match self { + Selector::Exact(pred) => pred == v.as_ref(), + Selector::Match(regex_eq) => regex_eq.0.is_match(v.as_ref()), + Selector::NeverMatch => false, + } + } +} + +impl knuffel::Decode for GlobalShortcuts +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + let mut seen_keys = HashSet::new(); + let mut shortcuts = Vec::new(); + + for child in node.children() { + match GlobalShortcut::decode_node(child, ctx) { + Err(e) => ctx.emit_error(e), + Ok(shortcut) => { + if seen_keys.insert(shortcut.trigger) { + shortcuts.push(shortcut); + } else { + // This suffers from the same issue mentioned in the `Binds` Decode impl + ctx.emit_error(DecodeError::unexpected( + &child.node_name, + "keybind", + "duplicate keybind", + )); + } + } + }; + } + + Ok(Self(shortcuts)) + } +} + +impl knuffel::Decode for GlobalShortcut +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + + for val in node.arguments.iter() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "no arguments expected for this node", + )); + } + + for name in node.properties.keys() { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + "no properties expected for this node", + )) + } + + let key = node + .node_name + .parse::() + .map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?; + + let mut app_id = None; + let mut shortcut_id = None; + for child in node.children() { + match &**child.node_name { + "app-id" => app_id = decode_selector_child(child, ctx), + "shortcut-id" => shortcut_id = decode_selector_child(child, ctx), + name_str => { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected child `{}`", name_str.escape_default()), + )); + } + } + } + + let app_id = app_id.unwrap_or(Selector::NeverMatch); + let shortcut_id = shortcut_id.unwrap_or(Selector::NeverMatch); + Ok(Self { + trigger: key, + app_id, + shortcut_id, + }) + } +} + +fn decode_selector_child( + child: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, +) -> Option { + let mut grand_children = child.children(); + if let Some(grand_child) = grand_children.next() { + for unwanted_child in grand_children { + ctx.emit_error(DecodeError::unexpected( + unwanted_child, + "node", + "only one selector is allowed per attribute", + )); + } + match >::decode_node(grand_child, ctx) { + Ok(v) => Some(v), + Err(e) => { + ctx.emit_error(e); + None + } + } + } else { + ctx.emit_error(DecodeError::missing( + child, + "expected a selector for this field", + )); + None + } +} diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index b61fe1c1a0..45335aa77b 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -34,6 +34,7 @@ pub mod binds; pub mod debug; pub mod error; pub mod gestures; +pub mod global_shortcuts; pub mod input; pub mod layer_rule; pub mod layout; @@ -50,6 +51,7 @@ pub use crate::binds::*; pub use crate::debug::Debug; pub use crate::error::{ConfigIncludeError, ConfigParseResult}; pub use crate::gestures::Gestures; +pub use crate::global_shortcuts::*; pub use crate::input::{Input, ModKey, ScrollMethod, TrackLayout, WarpMouseToFocusMode, Xkb}; pub use crate::layer_rule::LayerRule; pub use crate::layout::*; @@ -89,6 +91,7 @@ pub struct Config { pub debug: Debug, pub workspaces: Vec, pub recent_windows: RecentWindows, + pub global_shortcuts: GlobalShortcuts, } #[derive(Debug, Clone)] @@ -225,6 +228,17 @@ where // Add all new binds. binds.extend(part.0); } + "global-shortcuts" => { + // This section follows the same model as `binds` + let part = GlobalShortcuts::decode_node(node, ctx)?; + let mut config = config.borrow_mut(); + let shortcuts = &mut config.global_shortcuts.0; + + shortcuts.retain(|shortcut| { + !part.0.iter().any(|new| new.trigger == shortcut.trigger) + }); + shortcuts.extend(part.0); + } "environment" => { let part = Environment::decode_node(node, ctx)?; config.borrow_mut().environment.0.extend(part.0); @@ -837,7 +851,7 @@ mod tests { window-open { off; } window-close { - curve "cubic-bezier" 0.05 0.7 0.1 1 + curve "cubic-bezier" 0.05 0.7 0.1 1 } recent-windows-close { @@ -909,6 +923,20 @@ mod tests { Super+Alt+S allow-when-locked=true { spawn-sh "pkill orca || exec orca"; } } + global-shortcuts { + Ctrl+Shift+A { + app-id { exact "example"; } + shortcut-id { exact "example-sc-id"; } + } + + Shift+B { + app-id { match r#"^com\..*example$"#; } + shortcut-id { match r#"[0-9]+\-id"#; } + } + + Shift+C {} + } + switch-events { tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; } tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; } @@ -2323,6 +2351,62 @@ mod tests { }, ], }, + global_shortcuts: GlobalShortcuts( + [ + GlobalShortcut { + trigger: Key { + trigger: Keysym( + XK_a, + ), + modifiers: Modifiers( + CTRL | SHIFT, + ), + }, + app_id: Exact( + "example", + ), + shortcut_id: Exact( + "example-sc-id", + ), + }, + GlobalShortcut { + trigger: Key { + trigger: Keysym( + XK_b, + ), + modifiers: Modifiers( + SHIFT, + ), + }, + app_id: Match( + RegexEq( + Regex( + "^com\\..*example$", + ), + ), + ), + shortcut_id: Match( + RegexEq( + Regex( + "[0-9]+\\-id", + ), + ), + ), + }, + GlobalShortcut { + trigger: Key { + trigger: Keysym( + XK_c, + ), + modifiers: Modifiers( + SHIFT, + ), + }, + app_id: NeverMatch, + shortcut_id: NeverMatch, + }, + ], + ), } "#); } From a24f0ed14cc0b5a875b98e286a629e9d6a7a89e1 Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 14:23:39 -0600 Subject: [PATCH 2/9] extend binds config Implement Display for Key related structs. Enables runtime comparison/(de)serialization of Key and incoming dbus requests --- niri-config/src/binds.rs | 81 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/niri-config/src/binds.rs b/niri-config/src/binds.rs index 5b0efb97d2..09d7b68f5e 100644 --- a/niri-config/src/binds.rs +++ b/niri-config/src/binds.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::fmt::Display; use std::str::FromStr; use std::time::Duration; @@ -9,7 +10,9 @@ use niri_ipc::{ ColumnDisplay, LayoutSwitchTarget, PositionChange, SizeChange, WorkspaceReferenceArg, }; use smithay::input::keyboard::keysyms::KEY_NoSymbol; -use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE, KEYSYM_NO_FLAGS}; +use smithay::input::keyboard::xkb::{ + keysym_from_name, keysym_get_name, KEYSYM_CASE_INSENSITIVE, KEYSYM_NO_FLAGS, +}; use smithay::input::keyboard::Keysym; use crate::recent_windows::{MruDirection, MruFilter, MruScope}; @@ -1051,6 +1054,82 @@ impl FromStr for Key { } } +impl Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mods_str = self.modifiers.to_string(); + let trigger_str = self.trigger.to_string(); + + let mut parts = Vec::new(); + if !mods_str.is_empty() { + parts.push(mods_str.as_str()); + } + parts.push(trigger_str.as_str()); + + write!(f, "{}", parts.join("+")) + } +} + +impl Display for Trigger { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Trigger::Keysym(keysym) => &keysym_get_name(*keysym), + Trigger::MouseLeft => "MouseLeft", + Trigger::MouseRight => "MouseRight", + Trigger::MouseMiddle => "MouseMiddle", + Trigger::MouseBack => "MouseBack", + Trigger::MouseForward => "MouseForward", + Trigger::WheelScrollDown => "WheelScrollDown", + Trigger::WheelScrollUp => "WheelScrollUp", + Trigger::WheelScrollLeft => "WheelScrollLeft", + Trigger::WheelScrollRight => "WheelScrollRight", + Trigger::TouchpadScrollDown => "TouchpadScrollDown", + Trigger::TouchpadScrollUp => "TouchpadScrollUp", + Trigger::TouchpadScrollLeft => "TouchpadScrollLeft", + Trigger::TouchpadScrollRight => "TouchpadScrollRight", + }; + + write!(f, "{}", str)?; + Ok(()) + } +} + +impl Display for Modifiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut parts = Vec::new(); + + if self.contains(Modifiers::CTRL) { + parts.push("Ctrl"); + } + if self.contains(Modifiers::SHIFT) { + parts.push("Shift"); + } + if self.contains(Modifiers::ALT) { + parts.push("Alt"); + } + if self.contains(Modifiers::SUPER) { + parts.push("Super"); + } + if self.contains(Modifiers::ISO_LEVEL3_SHIFT) { + parts.push("Iso_Level3_Shift"); + } + if self.contains(Modifiers::ISO_LEVEL5_SHIFT) { + parts.push("Iso_Level5_Shift"); + } + if self.contains(Modifiers::COMPOSITOR) { + parts.push("Mod"); + } + + for (i, part) in parts.iter().enumerate() { + if i > 0 { + write!(f, "+")?; + } + write!(f, "{}", part)?; + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; From 279154708fd074afd63186b1d943df3d20f23bae Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 14:25:03 -0600 Subject: [PATCH 3/9] add tests to binds Added tests for Display, idempotence must be maintained or communication between dbus will result in failed shortcut registration/activation --- niri-config/src/binds.rs | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/niri-config/src/binds.rs b/niri-config/src/binds.rs index 09d7b68f5e..56e012333c 100644 --- a/niri-config/src/binds.rs +++ b/niri-config/src/binds.rs @@ -1191,4 +1191,84 @@ mod tests { }, ); } + + #[test] + fn trigger_display() { + assert_eq!(Trigger::Keysym(Keysym::Return).to_string(), "Return"); + assert_eq!(Trigger::Keysym(Keysym::space).to_string(), "space"); + assert_eq!(Trigger::Keysym(Keysym::Escape).to_string(), "Escape"); + assert_eq!(Trigger::MouseLeft.to_string(), "MouseLeft"); + } + + #[test] + fn modifiers_display() { + assert_eq!(Modifiers::CTRL.to_string(), "Ctrl"); + assert_eq!(Modifiers::SHIFT.to_string(), "Shift"); + assert_eq!(Modifiers::ALT.to_string(), "Alt"); + assert_eq!(Modifiers::SUPER.to_string(), "Super"); + assert_eq!(Modifiers::ISO_LEVEL3_SHIFT.to_string(), "Iso_Level3_Shift"); + assert_eq!(Modifiers::ISO_LEVEL5_SHIFT.to_string(), "Iso_Level5_Shift"); + assert_eq!(Modifiers::COMPOSITOR.to_string(), "Mod"); + + let all_mods = Modifiers::CTRL + | Modifiers::SHIFT + | Modifiers::ALT + | Modifiers::SUPER + | Modifiers::ISO_LEVEL3_SHIFT + | Modifiers::ISO_LEVEL5_SHIFT + | Modifiers::COMPOSITOR; + + assert_eq!( + all_mods.to_string(), + "Ctrl+Shift+Alt+Super+Iso_Level3_Shift+Iso_Level5_Shift+Mod" + ); + assert_eq!(Modifiers::empty().to_string(), ""); + } + + #[test] + fn key_display() { + // Basic + let key = Key { + trigger: Trigger::Keysym(Keysym::a), + modifiers: Modifiers::empty(), + }; + assert_eq!(key.to_string(), "a"); + + // With mods + let key = Key { + trigger: Trigger::Keysym(Keysym::b), + modifiers: Modifiers::CTRL | Modifiers::SHIFT, + }; + assert_eq!(key.to_string(), "Ctrl+Shift+b"); + + // Test mouse trigger + let key = Key { + trigger: Trigger::MouseLeft, + modifiers: Modifiers::CTRL, + }; + assert_eq!(key.to_string(), "Ctrl+MouseLeft"); + } + + #[test] + fn key_idempotence() { + let test_cases = [ + "a", + "Ctrl+b", + "Shift+Alt+c", + "Mod+d", + "Ctrl+Mod+e", + "MouseLeft", + "Ctrl+MouseRight", + "Alt+WheelScrollDown", + "Iso_Level3_Shift+f", + ]; + + for input in test_cases { + let key = input + .parse::() + .unwrap_or_else(|_| panic!("Failed to parse: {}", input)); + let output = key.to_string(); + assert_eq!(input, output, "Round trip failed for: {}", input); + } + } } From 4affeb7931126611f3cd99460d9a4e0ac4939f83 Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 20:43:42 -0600 Subject: [PATCH 4/9] impl gnome.Settings.GlobalShortcutsProvider interface Emulate gnome's settings provider using config file as source of truth, rather than popups/menus --- src/dbus/gnome_settings_shortcuts.rs | 240 +++++++++++++++++++++++++++ src/dbus/mod.rs | 15 ++ src/niri.rs | 44 +++++ 3 files changed, 299 insertions(+) create mode 100644 src/dbus/gnome_settings_shortcuts.rs diff --git a/src/dbus/gnome_settings_shortcuts.rs b/src/dbus/gnome_settings_shortcuts.rs new file mode 100644 index 0000000000..9fa7cedb82 --- /dev/null +++ b/src/dbus/gnome_settings_shortcuts.rs @@ -0,0 +1,240 @@ +use std::collections::HashMap; + +use serde::de::{self, SeqAccess, Visitor}; +use serde::ser::SerializeTuple; +use serde::{Deserialize, Serialize}; +use zbus::fdo::{self, RequestNameFlags}; +use zbus::interface; +use zbus::zvariant::{SerializeDict, Type, Value}; + +use super::Start; + +// Gnome portal converts modifiers into this format and +// back again afterwards. Their settings provider seems to use a different format +// than everything else and their portal expects that. +const GNOME_PORTAL_KEY_MAP: &[(&str, &str)] = &[ + ("", "Ctrl"), + ("", "Shift"), + ("", "Alt"), + ("", "Num_Lock"), + ("", "Super"), +]; + +pub struct ShortcutsProvider { + to_niri: calloop::channel::Sender, +} + +pub enum ShortcutsProviderToNiri { + BindShortcuts { + app_id: String, + parent_window: String, + shortcuts: Vec, + results: async_channel::Sender>, + }, +} + +#[derive(Debug, Type, Clone)] +#[zvariant(signature = "sa{sv}")] +pub struct BindShortcutRequest { + pub id: String, + pub description: String, + pub preferred_trigger: Vec, +} + +#[derive(Debug, Type, Clone)] +#[zvariant(signature = "sa{sv}")] +pub struct BindShortcutResponse { + pub id: String, + pub description: String, + pub shortcuts: Vec, +} + +#[interface(name = "org.gnome.Settings.GlobalShortcutsProvider")] +impl ShortcutsProvider { + async fn bind_shortcuts( + &self, + app_id: String, + parent_window: String, + shortcuts: Vec, + ) -> fdo::Result> { + let (tx, rx) = async_channel::bounded(1); + + if let Err(err) = self.to_niri.send(ShortcutsProviderToNiri::BindShortcuts { + app_id, + parent_window, + shortcuts, + results: tx, + }) { + warn!("error sending bind shortcuts message to niri: {err:?}"); + return Err(fdo::Error::Failed("internal error".to_owned())); + } + + rx.recv().await.map_err(|err| { + warn!("error receiving message from niri: {err:?}"); + fdo::Error::Failed("internal error".to_owned()) + }) + } +} + +impl ShortcutsProvider { + pub fn new(to_niri: calloop::channel::Sender) -> Self { + Self { to_niri } + } +} + +impl<'de> Deserialize<'de> for BindShortcutRequest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct OptsVisitor; + + impl<'de> Visitor<'de> for OptsVisitor { + type Value = BindShortcutRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str( + "a tuple of (string, array of (string, string|array of string) pairs)", + ) + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let id = seq + .next_element()? + .ok_or_else(|| de::Error::missing_field("id"))?; + + let opts: HashMap = seq + .next_element()? + .ok_or_else(|| de::Error::missing_field("options"))?; + + let description = opts + .get("description") + .and_then(|v| match v { + Value::Str(s) => Some(s.clone()), + _ => None, + }) + .unwrap_or_default() + .to_string(); + + // preferred_trigger is a Value wrapping either `s` or `as` + let preferred_trigger = if let Some(v) = opts.get("preferred_trigger").cloned() { + match v { + Value::Str(s) => vec![s.to_string()], + Value::Array(arr) => { + let mut str_vec: Vec = Vec::new(); + for elem in arr.iter() { + match elem.downcast_ref::() { + Ok(s) => str_vec.push(s), + Err(_) => return Err(de::Error::custom("Vardict entry `preferred_trigger` contained array of a variant *other* than string")), + }; + } + str_vec + } + _ => { + return Err(de::Error::custom( + "Vardict entry `preferred_trigger` was neither string nor array", + )) + } + } + } else { + Vec::new() + }; + + // Convert from gnome's formatting `<...>` tags around modifier keys + let formatted_trigger = preferred_trigger + .into_iter() + .map(|trigger| convert_gnome_modifiers(&trigger)) + .collect(); + + Ok(BindShortcutRequest { + id, + description, + preferred_trigger: formatted_trigger, + }) + } + } + + deserializer.deserialize_tuple(2, OptsVisitor) + } +} + +impl Serialize for BindShortcutResponse { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // Replace xkb style modifiers with gnome portals `<...>` tagged modifiers + let _formatted_shortcuts: Vec = { + self.shortcuts + .iter() + .map(|s| { + GNOME_PORTAL_KEY_MAP + .iter() + .fold(s.clone(), |acc, (to, from)| acc.replace(from, to)) + }) + .collect() + }; + + let opts = BindShortcutResponseOpts { + description: self.description.clone(), + shortcuts: self.shortcuts.clone(), + }; + + let mut tuple = serializer.serialize_tuple(2)?; + tuple.serialize_element(&self.id)?; + tuple.serialize_element(&opts)?; + tuple.end() + } +} + +// Helper struct to serialize into dbus dictionaries correctly +#[derive(SerializeDict, Debug, Clone)] +#[zvariant(signature = "dict")] +struct BindShortcutResponseOpts { + description: String, + shortcuts: Vec, +} + +impl Start for ShortcutsProvider { + fn start(self) -> anyhow::Result { + let conn = zbus::blocking::Connection::session()?; + let flags = RequestNameFlags::AllowReplacement + | RequestNameFlags::ReplaceExisting + | RequestNameFlags::DoNotQueue; + + conn.object_server() + .at("/org/gnome/Settings/GlobalShortcutsProvider", self)?; + conn.request_name_with_flags("org.gnome.Settings.GlobalShortcutsProvider", flags)?; + + Ok(conn) + } +} + +fn convert_gnome_modifiers(input: &str) -> String { + let mut parts: Vec<&str> = Vec::new(); + let mut remaining = input; + + while let Some(start) = remaining.find('<') { + if let Some(end) = remaining[start..].find('>') { + let token = &remaining[start..=start + end]; + if let Some(&(_, mapped)) = GNOME_PORTAL_KEY_MAP + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(token)) + { + parts.push(mapped); + } + remaining = &remaining[start + end + 1..]; + } else { + break; + } + } + + if !remaining.is_empty() { + parts.push(remaining); + } + + parts.join("+") +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 5941456f12..632002d753 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -1,12 +1,14 @@ use zbus::blocking::Connection; use zbus::object_server::Interface; +use crate::dbus::gnome_settings_shortcuts::ShortcutsProvider; use crate::niri::State; pub mod freedesktop_a11y; pub mod freedesktop_locale1; pub mod freedesktop_login1; pub mod freedesktop_screensaver; +pub mod gnome_settings_shortcuts; pub mod gnome_shell_introspect; pub mod gnome_shell_screenshot; pub mod mutter_display_config; @@ -33,6 +35,7 @@ pub struct DBusServers { pub conn_display_config: Option, pub conn_screen_saver: Option, pub conn_screen_shot: Option, + pub conn_shortcuts_provider: Option, pub conn_introspect: Option, #[cfg(feature = "xdp-gnome-screencast")] pub conn_screen_cast: Option, @@ -103,6 +106,18 @@ impl DBusServers { let screenshot = gnome_shell_screenshot::Screenshot::new(to_niri, from_niri); dbus.conn_screen_shot = try_start(screenshot); + let (to_niri, from_shortcuts_provider) = calloop::channel::channel(); + niri.event_loop + .insert_source( + from_shortcuts_provider, + move |event, _, state| match event { + calloop::channel::Event::Msg(msg) => state.on_shortcuts_provider_msg(msg), + calloop::channel::Event::Closed => (), + }, + ) + .unwrap(); + let shortcuts_provider = ShortcutsProvider::new(to_niri); + dbus.conn_shortcuts_provider = try_start(shortcuts_provider); let (to_niri, from_introspect) = calloop::channel::channel(); let (to_introspect, from_niri) = async_channel::unbounded(); niri.event_loop diff --git a/src/niri.rs b/src/niri.rs index d84c390abf..43398fb6cc 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -120,6 +120,8 @@ use crate::dbus::freedesktop_locale1::Locale1ToNiri; #[cfg(feature = "dbus")] use crate::dbus::freedesktop_login1::Login1ToNiri; #[cfg(feature = "dbus")] +use crate::dbus::gnome_settings_shortcuts::ShortcutsProviderToNiri; +#[cfg(feature = "dbus")] use crate::dbus::gnome_shell_introspect::{self, IntrospectToNiri, NiriToIntrospect}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; @@ -2084,6 +2086,48 @@ impl State { } } + #[cfg(feature = "dbus")] + pub fn on_shortcuts_provider_msg(&mut self, msg: ShortcutsProviderToNiri) { + let ShortcutsProviderToNiri::BindShortcuts { + app_id, + parent_window, + shortcuts, + results: tx, + } = msg; + + let config = self.niri.config.borrow(); + let permitted: Vec<_> = config + .global_shortcuts + .0 + .iter() + .filter(|shortcut_def| shortcut_def.app_id.matches(&app_id)) + .collect(); + + let result = shortcuts + .iter() + .map(|try_bind| { + use crate::dbus::gnome_settings_shortcuts::BindShortcutResponse; + + // Each matching shortcut here is tied to a a single shortcut_id + let shortcuts_to_bind = permitted + .iter() + .filter(|p| p.shortcut_id.matches(&try_bind.id)) + .map(|to_bind| to_bind.trigger.to_string()) + .collect(); + + BindShortcutResponse { + id: try_bind.id.clone(), + description: try_bind.description.clone(), + shortcuts: shortcuts_to_bind, + } + }) + .collect(); + + if let Err(err) = tx.send_blocking(result) { + warn!("error sending bind shortcuts result to gnome shell: {err:?}"); + } + } + #[cfg(feature = "dbus")] pub fn on_introspect_msg( &mut self, From c9a64c3317add6f66c6c6f23f10dca8a3c3856cc Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 20:45:00 -0600 Subject: [PATCH 5/9] add `inhibit` property to GlobalShortcut config Enable conditional inhibiting of a global shortcut using `inhibit` property, defaults to true --- niri-config/src/global_shortcuts.rs | 22 ++++++++++++++++------ niri-config/src/lib.rs | 5 ++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/niri-config/src/global_shortcuts.rs b/niri-config/src/global_shortcuts.rs index 880929c0fa..e6c32ebd10 100644 --- a/niri-config/src/global_shortcuts.rs +++ b/niri-config/src/global_shortcuts.rs @@ -11,6 +11,7 @@ pub struct GlobalShortcuts(pub Vec); #[derive(Debug, Clone, PartialEq)] pub struct GlobalShortcut { pub trigger: Key, + pub inhibit: bool, pub app_id: Selector, pub shortcut_id: Selector, } @@ -89,12 +90,20 @@ where )); } - for name in node.properties.keys() { - ctx.emit_error(DecodeError::unexpected( - name, - "property", - "no properties expected for this node", - )) + let mut inhibit = true; + for (name, val) in &node.properties { + match &***name { + "inhibit" => { + inhibit = knuffel::traits::DecodeScalar::decode(val, ctx)?; + } + name_str => { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name_str.escape_default()), + )); + } + } } let key = node @@ -122,6 +131,7 @@ where let shortcut_id = shortcut_id.unwrap_or(Selector::NeverMatch); Ok(Self { trigger: key, + inhibit, app_id, shortcut_id, }) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 45335aa77b..d0f47277d7 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -924,7 +924,7 @@ mod tests { } global-shortcuts { - Ctrl+Shift+A { + Ctrl+Shift+A inhibit=false { app-id { exact "example"; } shortcut-id { exact "example-sc-id"; } } @@ -2362,6 +2362,7 @@ mod tests { CTRL | SHIFT, ), }, + inhibit: false, app_id: Exact( "example", ), @@ -2378,6 +2379,7 @@ mod tests { SHIFT, ), }, + inhibit: true, app_id: Match( RegexEq( Regex( @@ -2402,6 +2404,7 @@ mod tests { SHIFT, ), }, + inhibit: true, app_id: NeverMatch, shortcut_id: NeverMatch, }, From 540f7412817d1a7b34475d74e0ec8dc3ebd5dc42 Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 20:48:03 -0600 Subject: [PATCH 6/9] impl gnome.Shell interface Creates `Shell` interface, serves as the interface between niri and the GlobalShortcuts portal interface Stores state of Key -> Action (u32) bindings, emits (De)activation signals on relevant keypresses and intercepts where applicable --- src/dbus/gnome_shell.rs | 283 ++++++++++++++++++++++++++++++++++++++++ src/dbus/mod.rs | 17 +++ src/input/mod.rs | 23 ++++ src/niri.rs | 81 ++++++++++++ 4 files changed, 404 insertions(+) create mode 100644 src/dbus/gnome_shell.rs diff --git a/src/dbus/gnome_shell.rs b/src/dbus/gnome_shell.rs new file mode 100644 index 0000000000..78f9b0031d --- /dev/null +++ b/src/dbus/gnome_shell.rs @@ -0,0 +1,283 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +use niri_config::{GlobalShortcuts, Key, ModKey, Modifiers}; +use serde::{Deserialize, Serialize}; +use smithay::input::keyboard::{Keysym, ModifiersState}; +use zbus::blocking::object_server::InterfaceRef; +use zbus::fdo::{self, RequestNameFlags}; +use zbus::interface; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{SerializeDict, Type, Value}; + +use super::Start; + +type Action = u32; + +#[derive(Clone)] +pub struct Shell { + to_niri: calloop::channel::Sender, + data: Arc>, +} + +#[derive(Debug, Default, Clone)] +struct Data { + next_action: Action, + bound_keys: HashMap>, +} + +pub enum ShellToNiri { + GrabAccelerators { + accelerators: Vec, + results: async_channel::Sender>, + }, + UngrabAccelerators { + actions: Vec, + result: async_channel::Sender, + }, +} + +#[derive(Debug, Clone, Deserialize, Type, Default)] +pub struct AcceleratorGrab { + pub accelerator: String, + + // GNOME parameters, unused by us + // Shell.ActionMode + _mode_flags: u32, + // Meta.KeyBindingFlags + _grab_flags: u32, +} + +#[derive(Debug, Clone, SerializeDict, Type, Value)] +#[zvariant(signature = "dict")] +pub struct ActivationParameters { + // GNOME portal/shell use u32 -- despite GlobalShortcuts interface using `t` (u64) + timestamp: u32, + // GNOME shell uses this to signal state and block shortcuts in some states + // GNOME portal seems to not care what this is set to + // see Shell.ActionMode for the relevant enum + #[zvariant(rename = "action-mode")] + action_mode: u32, + + #[zvariant(rename = "activation-token")] + activation_token: String, +} + +#[interface(name = "org.gnome.Shell")] +impl Shell { + async fn grab_accelerators( + &mut self, + accelerators: Vec, + ) -> fdo::Result> { + debug!("Tried to grab accels: {:?}", accelerators); + + let (tx, rx) = async_channel::bounded(1); + + if let Err(err) = self.to_niri.send(ShellToNiri::GrabAccelerators { + accelerators, + results: tx, + }) { + warn!("error sending grab accelerators message to niri: {err:?}"); + return Err(fdo::Error::Failed("internal error".to_owned())); + } + rx.recv().await.map_err(|err| { + warn!("error receiving message from niri: {err:?}"); + fdo::Error::Failed("internal error".to_owned()) + }) + } + + async fn ungrab_accelerators(&self, actions: Vec) -> fdo::Result { + debug!("Tried to ungrab actions: {:?}", actions); + + let (tx, rx) = async_channel::bounded(1); + + if let Err(err) = self.to_niri.send(ShellToNiri::UngrabAccelerators { + actions, + result: tx, + }) { + warn!("error sending ungrab accelerators message to niri: {err:?}"); + return Err(fdo::Error::Failed("internal error".to_owned())); + } + rx.recv().await.map_err(|err| { + warn!("error receiving message from niri: {err:?}"); + fdo::Error::Failed("internal error".to_owned()) + }) + } + + #[zbus(signal)] + pub async fn accelerator_activated( + ctxt: &SignalEmitter<'_>, + action: u32, + parameters: ActivationParameters, + ) -> zbus::Result<()>; + + #[zbus(signal)] + pub async fn accelerator_deactivated( + ctxt: &SignalEmitter<'_>, + action: u32, + parameters: ActivationParameters, + ) -> zbus::Result<()>; +} + +impl Shell { + pub fn new(to_niri: calloop::channel::Sender) -> Self { + Self { + to_niri, + data: Arc::new(Mutex::new(Data::default())), + } + } + + /// Handles global shortcut (de)activations + /// + /// returns true if any action was triggered + #[allow(clippy::too_many_arguments)] + pub fn process_key( + &self, + keysym: Keysym, + mods: ModifiersState, + mod_key: ModKey, + pressed: bool, + time: u32, + iface: InterfaceRef, + shortcuts: &GlobalShortcuts, + ) -> bool { + let key = niri_config::Key { + trigger: niri_config::Trigger::Keysym(keysym), + modifiers: mod_state_to_modifier(&mods, mod_key), + }; + + let data = self.data.lock().unwrap(); + let Some(actions) = data.bound_keys.get(&key) else { + return false; + }; + + let ctxt = iface.signal_emitter().clone(); + for action in actions { + let ctxt = &ctxt; + async_io::block_on(async move { + // FIXME: implementing activation_token would allow programs to notify on + // shortcut activations + let parameters = ActivationParameters { + timestamp: time, + action_mode: 1, + activation_token: String::default(), + }; + + let result = if pressed { + Self::accelerator_activated(ctxt, *action, parameters.clone()).await + } else { + Self::accelerator_deactivated(ctxt, *action, parameters.clone()).await + }; + + if let Err(err) = result { + warn!("error emitting global shortcut: {err:?}"); + } + }) + } + + // Conditionally inhibit based on config + shortcuts + .0 + .iter() + .find(|shortcut| shortcut.trigger == key) + .map(|shortcut| shortcut.inhibit) + .unwrap_or(false) + } + + /// Adds a key to global shortcut tracking + /// + /// Returns the `Action` id generated for the passed shortcut, returns `None` if grabbing + /// failed. + pub fn grab_key(&mut self, key: Key) -> Option { + let mut data = self.data.lock().unwrap(); + data.gen_action().inspect(|action| { + data.bound_keys.entry(key).or_default().insert(*action); + }) + } + + /// Removes an `Action` from global shortcut tracking. + /// + /// Returns `true` if `Action` had previously been grabbed for any keys. + pub fn ungrab_action(&mut self, action: Action) -> bool { + let mut data = self.data.lock().unwrap(); + + let mut found = false; + let mut drained = Vec::new(); + data.bound_keys.iter_mut().for_each(|(k, v)| { + if v.contains(&action) { + v.remove(&action); + found = true; + } + if v.is_empty() { + drained.push(*k); + } + }); + + for k in drained { + data.bound_keys.remove(&k); + } + + found + } +} + +impl Start for Shell { + fn start(self) -> anyhow::Result { + let conn = zbus::blocking::Connection::session()?; + let flags = RequestNameFlags::AllowReplacement + | RequestNameFlags::ReplaceExisting + | RequestNameFlags::DoNotQueue; + + conn.object_server().at("/org/gnome/Shell", self)?; + conn.request_name_with_flags("org.gnome.Shell", flags)?; + + Ok(conn) + } +} + +impl Data { + fn gen_action(&mut self) -> Option { + let action = self.next_action; + if let Some(new_action) = self.next_action.checked_add(1) { + self.next_action = new_action; + Some(action) + } else { + warn!("global shortcut action ids have been exhausted"); + None + } + } +} + +fn mod_state_to_modifier(mods: &ModifiersState, mod_key: ModKey) -> Modifiers { + let mut out = Modifiers::empty(); + + let mapping = [ + (mods.ctrl, ModKey::Ctrl, Modifiers::CTRL), + (mods.shift, ModKey::Shift, Modifiers::SHIFT), + (mods.alt, ModKey::Alt, Modifiers::ALT), + (mods.logo, ModKey::Super, Modifiers::SUPER), + ( + mods.iso_level3_shift, + ModKey::IsoLevel3Shift, + Modifiers::ISO_LEVEL3_SHIFT, + ), + ( + mods.iso_level5_shift, + ModKey::IsoLevel5Shift, + Modifiers::ISO_LEVEL5_SHIFT, + ), + ]; + + for (is_pressed, mod_key_pred, modifier) in mapping { + if is_pressed { + // `mod_key` could shadow any one modifier + if mod_key == mod_key_pred { + out |= Modifiers::COMPOSITOR; + } else { + out |= modifier; + } + } + } + + out +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 632002d753..7e1600ff15 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -2,6 +2,7 @@ use zbus::blocking::Connection; use zbus::object_server::Interface; use crate::dbus::gnome_settings_shortcuts::ShortcutsProvider; +use crate::dbus::gnome_shell::Shell; use crate::niri::State; pub mod freedesktop_a11y; @@ -9,6 +10,7 @@ pub mod freedesktop_locale1; pub mod freedesktop_login1; pub mod freedesktop_screensaver; pub mod gnome_settings_shortcuts; +pub mod gnome_shell; pub mod gnome_shell_introspect; pub mod gnome_shell_screenshot; pub mod mutter_display_config; @@ -35,6 +37,7 @@ pub struct DBusServers { pub conn_display_config: Option, pub conn_screen_saver: Option, pub conn_screen_shot: Option, + pub conn_gnome_shell: Option, pub conn_shortcuts_provider: Option, pub conn_introspect: Option, #[cfg(feature = "xdp-gnome-screencast")] @@ -106,6 +109,19 @@ impl DBusServers { let screenshot = gnome_shell_screenshot::Screenshot::new(to_niri, from_niri); dbus.conn_screen_shot = try_start(screenshot); + let (to_niri, from_gnome_shell) = calloop::channel::channel(); + niri.event_loop + .insert_source(from_gnome_shell, move |event, _, state| match event { + calloop::channel::Event::Msg(msg) => state.on_gnome_shell_msg(msg), + calloop::channel::Event::Closed => (), + }) + .unwrap(); + let shell = Shell::new(to_niri); + if let Some(x) = try_start(shell.clone()) { + dbus.conn_gnome_shell = Some(x); + niri.gnome_shell = Some(shell); + } + let (to_niri, from_shortcuts_provider) = calloop::channel::channel(); niri.event_loop .insert_source( @@ -118,6 +134,7 @@ impl DBusServers { .unwrap(); let shortcuts_provider = ShortcutsProvider::new(to_niri); dbus.conn_shortcuts_provider = try_start(shortcuts_provider); + let (to_niri, from_introspect) = calloop::channel::channel(); let (to_introspect, from_niri) = async_channel::unbounded(); niri.event_loop diff --git a/src/input/mod.rs b/src/input/mod.rs index d46166a007..35f099f464 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -507,6 +507,29 @@ impl State { this.niri.screenshot_ui.set_space_down(pressed); } + #[cfg(feature = "dbus")] + if let (Some(shell), Some(dbus)) = (&this.niri.gnome_shell, &this.niri.dbus) { + if let Some(conn_shell) = &dbus.conn_gnome_shell { + match conn_shell + .object_server() + .interface::<_, crate::dbus::gnome_shell::Shell>("/org/gnome/Shell") + { + Ok(iface) => { + if let Some(keysym) = raw { + let config = this.niri.config.borrow(); + let shortcuts = &config.global_shortcuts; + if shell.process_key( + keysym, *mods, mod_key, pressed, time, iface, shortcuts, + ) { + return FilterResult::Intercept(None); + } + } + } + Err(err) => warn!("error getting gnome Shell interface: {err:?}"), + } + } + } + let res = { let config = this.niri.config.borrow(); let bindings = diff --git a/src/niri.rs b/src/niri.rs index 43398fb6cc..6b7c1ffad2 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -122,6 +122,8 @@ use crate::dbus::freedesktop_login1::Login1ToNiri; #[cfg(feature = "dbus")] use crate::dbus::gnome_settings_shortcuts::ShortcutsProviderToNiri; #[cfg(feature = "dbus")] +use crate::dbus::gnome_shell::{AcceleratorGrab, ShellToNiri}; +#[cfg(feature = "dbus")] use crate::dbus::gnome_shell_introspect::{self, IntrospectToNiri, NiriToIntrospect}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; @@ -402,6 +404,8 @@ pub struct Niri { pub a11y: A11y, #[cfg(feature = "dbus")] pub inhibit_power_key_fd: Option, + #[cfg(feature = "dbus")] + pub gnome_shell: Option, pub ipc_server: Option, pub ipc_outputs_changed: bool, @@ -2086,6 +2090,75 @@ impl State { } } + #[cfg(feature = "dbus")] + pub fn on_gnome_shell_msg(&mut self, msg: ShellToNiri) { + match msg { + ShellToNiri::GrabAccelerators { + accelerators, + results: tx, + } => self.handle_grab_accelerators(accelerators, tx), + ShellToNiri::UngrabAccelerators { + actions, + result: tx, + } => self.handle_ungrab_accelerators(actions, tx), + } + } + + #[cfg(feature = "dbus")] + fn handle_grab_accelerators( + &mut self, + grabs: Vec, + tx: async_channel::Sender>, + ) { + debug!("[GnomeShell] requesting grab accelerators: {:?}", grabs); + let Some(shell) = &mut self.niri.gnome_shell else { + warn!("gnome shell requested without being created"); + let _ = tx.send_blocking(Vec::new()); + return; + }; + + let results = grabs + .iter() + .map(|to_grab| { + use std::str::FromStr; + + Key::from_str(&to_grab.accelerator) + .ok() + .and_then(|key| shell.grab_key(key)) + .unwrap_or(0) + }) + .collect(); + + debug!("[GnomeShell] returning actions: {:?}", results); + if let Err(err) = tx.send_blocking(results) { + warn!("error sending grab accelerators result to gnome shell: {err:?}"); + } + } + + #[cfg(feature = "dbus")] + fn handle_ungrab_accelerators(&mut self, actions: Vec, tx: async_channel::Sender) { + debug!("[GnomeShell] requesting ungrab accelerators: {:?}", actions); + let Some(shell) = &mut self.niri.gnome_shell else { + warn!("gnome shell requested without being created"); + let _ = tx.send_blocking(false); + return; + }; + + let result = !actions.is_empty() + && actions.iter().fold(true, |acc, action| { + if shell.ungrab_action(*action) { + acc + } else { + false + } + }); + + debug!("[GnomeShell] returning result: {:?}", result); + if let Err(err) = tx.send_blocking(result) { + warn!("error sending ungrab accelerators result to gnome shell: {err:?}"); + } + } + #[cfg(feature = "dbus")] pub fn on_shortcuts_provider_msg(&mut self, msg: ShortcutsProviderToNiri) { let ShortcutsProviderToNiri::BindShortcuts { @@ -2094,6 +2167,10 @@ impl State { shortcuts, results: tx, } = msg; + debug!( + "[ShortcutProvider] Trying to match: `{}::{}` requesting to bind: `{:?}`", + app_id, parent_window, shortcuts + ); let config = self.niri.config.borrow(); let permitted: Vec<_> = config @@ -2123,6 +2200,8 @@ impl State { }) .collect(); + debug!("[ShortcutProvider] responding with: {:?}", result); + if let Err(err) = tx.send_blocking(result) { warn!("error sending bind shortcuts result to gnome shell: {err:?}"); } @@ -2597,6 +2676,8 @@ impl Niri { a11y, #[cfg(feature = "dbus")] inhibit_power_key_fd: None, + #[cfg(feature = "dbus")] + gnome_shell: None, ipc_server, ipc_outputs_changed: false, From bc72f4b8b27242d225998c895d1886b4012fdbb7 Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Mon, 9 Mar 2026 21:10:18 -0600 Subject: [PATCH 7/9] cleanup debug! and unused parameters --- src/dbus/gnome_settings_shortcuts.rs | 16 +--------------- src/dbus/gnome_shell.rs | 4 ---- src/niri.rs | 16 ++++------------ 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/dbus/gnome_settings_shortcuts.rs b/src/dbus/gnome_settings_shortcuts.rs index 9fa7cedb82..be586016f2 100644 --- a/src/dbus/gnome_settings_shortcuts.rs +++ b/src/dbus/gnome_settings_shortcuts.rs @@ -27,7 +27,6 @@ pub struct ShortcutsProvider { pub enum ShortcutsProviderToNiri { BindShortcuts { app_id: String, - parent_window: String, shortcuts: Vec, results: async_channel::Sender>, }, @@ -54,14 +53,13 @@ impl ShortcutsProvider { async fn bind_shortcuts( &self, app_id: String, - parent_window: String, + _parent_window: String, shortcuts: Vec, ) -> fdo::Result> { let (tx, rx) = async_channel::bounded(1); if let Err(err) = self.to_niri.send(ShortcutsProviderToNiri::BindShortcuts { app_id, - parent_window, shortcuts, results: tx, }) { @@ -166,18 +164,6 @@ impl Serialize for BindShortcutResponse { where S: serde::Serializer, { - // Replace xkb style modifiers with gnome portals `<...>` tagged modifiers - let _formatted_shortcuts: Vec = { - self.shortcuts - .iter() - .map(|s| { - GNOME_PORTAL_KEY_MAP - .iter() - .fold(s.clone(), |acc, (to, from)| acc.replace(from, to)) - }) - .collect() - }; - let opts = BindShortcutResponseOpts { description: self.description.clone(), shortcuts: self.shortcuts.clone(), diff --git a/src/dbus/gnome_shell.rs b/src/dbus/gnome_shell.rs index 78f9b0031d..1a69de9bfd 100644 --- a/src/dbus/gnome_shell.rs +++ b/src/dbus/gnome_shell.rs @@ -69,8 +69,6 @@ impl Shell { &mut self, accelerators: Vec, ) -> fdo::Result> { - debug!("Tried to grab accels: {:?}", accelerators); - let (tx, rx) = async_channel::bounded(1); if let Err(err) = self.to_niri.send(ShellToNiri::GrabAccelerators { @@ -87,8 +85,6 @@ impl Shell { } async fn ungrab_accelerators(&self, actions: Vec) -> fdo::Result { - debug!("Tried to ungrab actions: {:?}", actions); - let (tx, rx) = async_channel::bounded(1); if let Err(err) = self.to_niri.send(ShellToNiri::UngrabAccelerators { diff --git a/src/niri.rs b/src/niri.rs index 6b7c1ffad2..d2ab3e5a27 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -2110,7 +2110,7 @@ impl State { grabs: Vec, tx: async_channel::Sender>, ) { - debug!("[GnomeShell] requesting grab accelerators: {:?}", grabs); + debug!("requesting grab global shortcut accelerators: {grabs:?}"); let Some(shell) = &mut self.niri.gnome_shell else { warn!("gnome shell requested without being created"); let _ = tx.send_blocking(Vec::new()); @@ -2129,7 +2129,7 @@ impl State { }) .collect(); - debug!("[GnomeShell] returning actions: {:?}", results); + debug!("returning actions: {results:?}"); if let Err(err) = tx.send_blocking(results) { warn!("error sending grab accelerators result to gnome shell: {err:?}"); } @@ -2137,7 +2137,7 @@ impl State { #[cfg(feature = "dbus")] fn handle_ungrab_accelerators(&mut self, actions: Vec, tx: async_channel::Sender) { - debug!("[GnomeShell] requesting ungrab accelerators: {:?}", actions); + debug!("requesting ungrab global shortcut accelerators: {actions:?}",); let Some(shell) = &mut self.niri.gnome_shell else { warn!("gnome shell requested without being created"); let _ = tx.send_blocking(false); @@ -2153,7 +2153,7 @@ impl State { } }); - debug!("[GnomeShell] returning result: {:?}", result); + debug!("returning result: {result:?}"); if let Err(err) = tx.send_blocking(result) { warn!("error sending ungrab accelerators result to gnome shell: {err:?}"); } @@ -2163,15 +2163,9 @@ impl State { pub fn on_shortcuts_provider_msg(&mut self, msg: ShortcutsProviderToNiri) { let ShortcutsProviderToNiri::BindShortcuts { app_id, - parent_window, shortcuts, results: tx, } = msg; - debug!( - "[ShortcutProvider] Trying to match: `{}::{}` requesting to bind: `{:?}`", - app_id, parent_window, shortcuts - ); - let config = self.niri.config.borrow(); let permitted: Vec<_> = config .global_shortcuts @@ -2200,8 +2194,6 @@ impl State { }) .collect(); - debug!("[ShortcutProvider] responding with: {:?}", result); - if let Err(err) = tx.send_blocking(result) { warn!("error sending bind shortcuts result to gnome shell: {err:?}"); } From 24dd80845f76367fd962a1f4bedbaefe27b38c0f Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Tue, 10 Mar 2026 08:23:50 -0600 Subject: [PATCH 8/9] cleanup and rename `inhibit` to `intercept` Rewrote some leftover badly structured code Rename `inhibit` to `intercept` for better insight into its purpose --- niri-config/src/binds.rs | 12 ++---------- niri-config/src/global_shortcuts.rs | 10 +++++----- niri-config/src/lib.rs | 8 ++++---- src/dbus/gnome_shell.rs | 2 +- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/niri-config/src/binds.rs b/niri-config/src/binds.rs index 56e012333c..0b80a922ee 100644 --- a/niri-config/src/binds.rs +++ b/niri-config/src/binds.rs @@ -1088,8 +1088,7 @@ impl Display for Trigger { Trigger::TouchpadScrollRight => "TouchpadScrollRight", }; - write!(f, "{}", str)?; - Ok(()) + write!(f, "{}", str) } } @@ -1119,14 +1118,7 @@ impl Display for Modifiers { parts.push("Mod"); } - for (i, part) in parts.iter().enumerate() { - if i > 0 { - write!(f, "+")?; - } - write!(f, "{}", part)?; - } - - Ok(()) + write!(f, "{}", parts.join("+")) } } diff --git a/niri-config/src/global_shortcuts.rs b/niri-config/src/global_shortcuts.rs index e6c32ebd10..35c9fa3e9b 100644 --- a/niri-config/src/global_shortcuts.rs +++ b/niri-config/src/global_shortcuts.rs @@ -11,7 +11,7 @@ pub struct GlobalShortcuts(pub Vec); #[derive(Debug, Clone, PartialEq)] pub struct GlobalShortcut { pub trigger: Key, - pub inhibit: bool, + pub intercept: bool, pub app_id: Selector, pub shortcut_id: Selector, } @@ -90,11 +90,11 @@ where )); } - let mut inhibit = true; + let mut intercept = true; for (name, val) in &node.properties { match &***name { - "inhibit" => { - inhibit = knuffel::traits::DecodeScalar::decode(val, ctx)?; + "intercept" => { + intercept = knuffel::traits::DecodeScalar::decode(val, ctx)?; } name_str => { ctx.emit_error(DecodeError::unexpected( @@ -131,7 +131,7 @@ where let shortcut_id = shortcut_id.unwrap_or(Selector::NeverMatch); Ok(Self { trigger: key, - inhibit, + intercept, app_id, shortcut_id, }) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index d0f47277d7..30a6552d16 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -924,7 +924,7 @@ mod tests { } global-shortcuts { - Ctrl+Shift+A inhibit=false { + Ctrl+Shift+A intercept=false { app-id { exact "example"; } shortcut-id { exact "example-sc-id"; } } @@ -2362,7 +2362,7 @@ mod tests { CTRL | SHIFT, ), }, - inhibit: false, + intercept: false, app_id: Exact( "example", ), @@ -2379,7 +2379,7 @@ mod tests { SHIFT, ), }, - inhibit: true, + intercept: true, app_id: Match( RegexEq( Regex( @@ -2404,7 +2404,7 @@ mod tests { SHIFT, ), }, - inhibit: true, + intercept: true, app_id: NeverMatch, shortcut_id: NeverMatch, }, diff --git a/src/dbus/gnome_shell.rs b/src/dbus/gnome_shell.rs index 1a69de9bfd..bfb5e6f4df 100644 --- a/src/dbus/gnome_shell.rs +++ b/src/dbus/gnome_shell.rs @@ -176,7 +176,7 @@ impl Shell { .0 .iter() .find(|shortcut| shortcut.trigger == key) - .map(|shortcut| shortcut.inhibit) + .map(|shortcut| shortcut.intercept) .unwrap_or(false) } From 7e2f1d2f53528141647b5ed17b1fdb62bba88a24 Mon Sep 17 00:00:00 2001 From: Jayden Dumouchel Date: Fri, 13 Mar 2026 15:30:43 -0600 Subject: [PATCH 9/9] cleanup in gnome_shell Refactor `gen_action` and replace with the obvious iterator it should have been. --- src/dbus/gnome_shell.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/dbus/gnome_shell.rs b/src/dbus/gnome_shell.rs index bfb5e6f4df..82343b4496 100644 --- a/src/dbus/gnome_shell.rs +++ b/src/dbus/gnome_shell.rs @@ -20,9 +20,9 @@ pub struct Shell { data: Arc>, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] struct Data { - next_action: Action, + action: std::ops::Range, bound_keys: HashMap>, } @@ -171,7 +171,7 @@ impl Shell { }) } - // Conditionally inhibit based on config + // Conditionally intercept keypresses based on config shortcuts .0 .iter() @@ -186,7 +186,7 @@ impl Shell { /// failed. pub fn grab_key(&mut self, key: Key) -> Option { let mut data = self.data.lock().unwrap(); - data.gen_action().inspect(|action| { + data.action.next().inspect(|action| { data.bound_keys.entry(key).or_default().insert(*action); }) } @@ -231,15 +231,11 @@ impl Start for Shell { } } -impl Data { - fn gen_action(&mut self) -> Option { - let action = self.next_action; - if let Some(new_action) = self.next_action.checked_add(1) { - self.next_action = new_action; - Some(action) - } else { - warn!("global shortcut action ids have been exhausted"); - None +impl Default for Data { + fn default() -> Self { + Self { + action: (0..Action::MAX), + bound_keys: Default::default(), } } }