Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 152 additions & 1 deletion niri-config/src/binds.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::fmt::Display;
use std::str::FromStr;
use std::time::Duration;

Expand All @@ -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};
Expand Down Expand Up @@ -1051,6 +1054,74 @@ 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)
}
}

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");
}

write!(f, "{}", parts.join("+"))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1112,4 +1183,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::<Key>()
.unwrap_or_else(|_| panic!("Failed to parse: {}", input));
let output = key.to_string();
assert_eq!(input, output, "Round trip failed for: {}", input);
}
}
}
168 changes: 168 additions & 0 deletions niri-config/src/global_shortcuts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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<GlobalShortcut>);

#[derive(Debug, Clone, PartialEq)]
pub struct GlobalShortcut {
pub trigger: Key,
pub intercept: bool,
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<T: AsRef<str>>(&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<S> knuffel::Decode<S> for GlobalShortcuts
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
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<S> knuffel::Decode<S> for GlobalShortcut
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
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",
));
}

let mut intercept = true;
for (name, val) in &node.properties {
match &***name {
"intercept" => {
intercept = 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
.node_name
.parse::<Key>()
.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,
intercept,
app_id,
shortcut_id,
})
}
}

fn decode_selector_child<S: knuffel::traits::ErrorSpan>(
child: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Option<Selector> {
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 <Selector as knuffel::Decode<S>>::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
}
}
Loading