From 29af1bec9cd2b42d28dfaf53a7545fcaeba2a914 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 02:57:36 +0100 Subject: [PATCH] feat: configure commands and key bindings --- README.md | 27 ++ compositor/src/config.rs | 529 ++++++++++++++++++++++++++++++++++++++- compositor/src/input.rs | 194 +++++--------- compositor/src/main.rs | 5 +- compositor/src/state.rs | 5 +- 5 files changed, 623 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 1df2711..91ba18e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,33 @@ Touchpad settings apply to libinput devices in the direct DRM session. In the nested backend, the host compositor remains responsible for physical touchpad configuration. +Commands are argv arrays and are started directly, without a shell. The +default `Super+Enter` binding runs the built-in `terminal = ["foot"]` command: + +```toml +[commands] +terminal = ["foot", "--app-id", "terminal"] + +[[bindings]] +keys = "Super+Enter" +action = "spawn" +argument = "terminal" +``` + +A configured binding replaces the built-in binding with the same normalized +chord and match mode. Disable a default explicitly with no action: + +```toml +[[bindings]] +keys = "Super+Q" +disabled = true +``` + +Bindings follow the active layout by default. To bind a keyboard position +instead, set `match = "physical"` and use an XKB physical key name such as +`AD06`. Invalid chords, actions, arguments, duplicate bindings, and missing +command references stop startup with an error. + Text-input-v3 is available to applications by default. Input-method-v2 is hidden unless the session explicitly opts in, because an input method can request privileged keyboard access. Enable it only when launching a trusted diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 8f54840..36267a9 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -1,3 +1,4 @@ +use std::collections::{HashMap, HashSet}; use std::env; use std::error::Error; use std::fmt; @@ -7,8 +8,9 @@ use std::path::PathBuf; use ferese_animation::SpringConfig; use ferese_core::LayoutMode; -use ferese_layout::{ColumnWidth, GapConfig, ViewportFocusStrategy}; +use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy}; use serde::Deserialize; +use smithay::input::keyboard::{Keycode, keysyms, xkb}; #[derive(Debug, Default, Deserialize)] pub struct Config { @@ -19,9 +21,95 @@ pub struct Config { #[serde(default)] input: InputConfig, #[serde(default)] + commands: HashMap>, + #[serde(default)] + bindings: Vec, + #[serde(default)] scrolling: ScrollingConfig, } +#[derive(Clone, Debug, PartialEq)] +pub struct Binding { + modifiers: BindingModifiers, + trigger: BindingTrigger, + pub action: BindingAction, +} + +#[derive(Clone, Debug, PartialEq)] +enum BindingTrigger { + Keysym(u32), + Physical(Keycode), +} + +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +struct BindingModifiers { + logo: bool, + ctrl: bool, + alt: bool, + shift: bool, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum BindingAction { + Spawn(Vec), + Close, + Focus(ferese_layout::Direction), + Move(ferese_layout::Direction), + Resize(ferese_layout::Direction), + SwitchWorkspace(u8), + MoveToWorkspace(u8), + ToggleFullscreen, + ToggleLayout, + CycleColumnWidth, + CenterColumn, + Consume, + Expel, + ToggleFloating, +} + +impl Binding { + pub fn matches( + &self, + keycode: Keycode, + keysyms: &[u32], + logo: bool, + ctrl: bool, + alt: bool, + shift: bool, + ) -> bool { + self.modifiers + == BindingModifiers { + logo, + ctrl, + alt, + shift, + } + && match self.trigger { + BindingTrigger::Keysym(expected) => keysyms.contains(&expected), + BindingTrigger::Physical(expected) => expected == keycode, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)] +#[serde(rename_all = "lowercase")] +enum BindingMatch { + #[default] + Keysym, + Physical, +} + +#[derive(Clone, Debug, Deserialize)] +struct BindingConfig { + keys: String, + #[serde(default, rename = "match")] + match_mode: BindingMatch, + action: Option, + argument: Option, + #[serde(default)] + disabled: bool, +} + #[derive(Clone, Debug, PartialEq)] pub struct InputSettings { pub xkb_layout: String, @@ -231,6 +319,7 @@ pub enum ConfigError { field: &'static str, value: String, }, + InvalidBinding(String), } impl fmt::Display for ConfigError { @@ -255,6 +344,7 @@ impl fmt::Display for ConfigError { Self::InvalidInputValue { field, value } => { write!(formatter, "invalid input.{field} value {value}") } + Self::InvalidBinding(message) => write!(formatter, "invalid binding: {message}"), } } } @@ -267,7 +357,8 @@ impl Error for ConfigError { Self::InvalidColumnWidth { .. } | Self::InvalidAnimationValue { .. } | Self::InvalidLayoutValue { .. } - | Self::InvalidInputValue { .. } => None, + | Self::InvalidInputValue { .. } + | Self::InvalidBinding(_) => None, } } } @@ -376,6 +467,43 @@ impl Config { }) } + pub fn bindings(&self, input: &InputSettings) -> Result, ConfigError> { + let mut commands = HashMap::from([("terminal".to_owned(), vec!["foot".to_owned()])]); + commands.extend(self.commands.clone()); + validate_commands(&commands)?; + + let keymap = physical_keymap(input)?; + let mut bindings = default_bindings() + .into_iter() + .map(|binding| parse_binding(&binding, &commands, &keymap)) + .collect::, _>>()?; + let mut supplied = HashSet::new(); + + for configured in &self.bindings { + let identity = binding_identity(configured, &keymap)?; + if !supplied.insert(identity.clone()) { + return Err(ConfigError::InvalidBinding(format!( + "duplicate binding {:?}", + configured.keys + ))); + } + + bindings.retain(|binding| binding_identity_for_runtime(binding) != identity); + if configured.disabled { + if configured.action.is_some() || configured.argument.is_some() { + return Err(ConfigError::InvalidBinding(format!( + "disabled binding {:?} cannot have an action or argument", + configured.keys + ))); + } + } else { + bindings.push(parse_binding(configured, &commands, &keymap)?); + } + } + + Ok(bindings) + } + pub fn animations_enabled(&self) -> bool { self.animations.enabled && !self.animations.reduced_motion } @@ -425,6 +553,329 @@ impl Config { } } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct BindingIdentity { + modifiers: BindingModifiers, + match_mode: BindingMatch, + key: u32, +} + +fn validate_commands(commands: &HashMap>) -> Result<(), ConfigError> { + for (name, argv) in commands { + if name.trim().is_empty() { + return Err(ConfigError::InvalidBinding( + "command names cannot be empty".to_owned(), + )); + } + if argv.is_empty() || argv[0].is_empty() { + return Err(ConfigError::InvalidBinding(format!( + "command {name:?} must contain a program" + ))); + } + } + + Ok(()) +} + +fn physical_keymap(input: &InputSettings) -> Result { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let options = (!input.xkb_options.is_empty()).then(|| input.xkb_options.join(",")); + + xkb::Keymap::new_from_names( + &context, + "", + "", + &input.xkb_layout, + &input.xkb_variant, + options, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) + .ok_or_else(|| ConfigError::InvalidBinding("failed to compile the XKB keymap".to_owned())) +} + +fn parse_binding( + configured: &BindingConfig, + commands: &HashMap>, + keymap: &xkb::Keymap, +) -> Result { + if configured.disabled { + return Err(ConfigError::InvalidBinding(format!( + "disabled binding {:?} cannot be executed", + configured.keys + ))); + } + + let identity = binding_identity(configured, keymap)?; + let action = configured.action.as_deref().ok_or_else(|| { + ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys)) + })?; + let action = parse_action(action, configured.argument.as_deref(), commands)?; + let trigger = match identity.match_mode { + BindingMatch::Keysym => BindingTrigger::Keysym(identity.key), + BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)), + }; + + Ok(Binding { + modifiers: identity.modifiers, + trigger, + action, + }) +} + +fn binding_identity( + configured: &BindingConfig, + keymap: &xkb::Keymap, +) -> Result { + let (modifiers, key) = parse_chord(&configured.keys)?; + let key = match configured.match_mode { + BindingMatch::Keysym => parse_keysym(&key)?, + BindingMatch::Physical => keymap + .key_by_name(&key.to_ascii_uppercase()) + .map(Keycode::raw) + .ok_or_else(|| { + ConfigError::InvalidBinding(format!( + "unknown XKB physical key name {key:?} in {:?}", + configured.keys + )) + })?, + }; + + Ok(BindingIdentity { + modifiers, + match_mode: configured.match_mode, + key, + }) +} + +fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity { + let (match_mode, key) = match binding.trigger { + BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key), + BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()), + }; + + BindingIdentity { + modifiers: binding.modifiers, + match_mode, + key, + } +} + +fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> { + let mut modifiers = BindingModifiers::default(); + let mut key = None; + + for component in chord.split('+').map(str::trim) { + if component.is_empty() { + return Err(ConfigError::InvalidBinding(format!( + "invalid key chord {chord:?}" + ))); + } + + let slot = match component.to_ascii_lowercase().as_str() { + "super" | "logo" | "mod4" => Some(&mut modifiers.logo), + "ctrl" | "control" => Some(&mut modifiers.ctrl), + "alt" => Some(&mut modifiers.alt), + "shift" => Some(&mut modifiers.shift), + _ => None, + }; + if let Some(slot) = slot { + if *slot { + return Err(ConfigError::InvalidBinding(format!( + "duplicate modifier in {chord:?}" + ))); + } + *slot = true; + } else if key.replace(component.to_owned()).is_some() { + return Err(ConfigError::InvalidBinding(format!( + "key chord {chord:?} contains more than one key" + ))); + } + } + + let key = key.ok_or_else(|| { + ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")) + })?; + Ok((modifiers, key)) +} + +fn parse_keysym(name: &str) -> Result { + let normalized = match name { + "Enter" | "enter" => "Return".to_owned(), + "Space" | "space" => "space".to_owned(), + "[" => "bracketleft".to_owned(), + "]" => "bracketright".to_owned(), + name if name.len() == 1 => name.to_ascii_lowercase(), + name => name.to_owned(), + }; + let mut symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_NO_FLAGS); + if symbol.raw() == keysyms::KEY_NoSymbol { + symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE); + } + if symbol.raw() == keysyms::KEY_NoSymbol { + Err(ConfigError::InvalidBinding(format!( + "unknown keysym {name:?}" + ))) + } else { + Ok(symbol.raw()) + } +} + +fn parse_action( + action: &str, + argument: Option<&str>, + commands: &HashMap>, +) -> Result { + let no_argument = || { + if argument.is_some() { + Err(ConfigError::InvalidBinding(format!( + "action {action:?} does not accept an argument" + ))) + } else { + Ok(()) + } + }; + let required_argument = || { + argument.ok_or_else(|| { + ConfigError::InvalidBinding(format!("action {action:?} requires an argument")) + }) + }; + + match action { + "spawn" => { + let command = required_argument()?; + let argv = commands.get(command).ok_or_else(|| { + ConfigError::InvalidBinding(format!("unknown command {command:?}")) + })?; + Ok(BindingAction::Spawn(argv.clone())) + } + "close" => { + no_argument()?; + Ok(BindingAction::Close) + } + "focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)), + "move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)), + "resize" => Ok(BindingAction::Resize( + parse_direction(required_argument()?)?, + )), + "workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace( + required_argument()?, + )?)), + "move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace( + required_argument()?, + )?)), + "toggle-fullscreen" => { + no_argument()?; + Ok(BindingAction::ToggleFullscreen) + } + "toggle-layout" => { + no_argument()?; + Ok(BindingAction::ToggleLayout) + } + "cycle-column-width" => { + no_argument()?; + Ok(BindingAction::CycleColumnWidth) + } + "center-column" => { + no_argument()?; + Ok(BindingAction::CenterColumn) + } + "consume" => { + no_argument()?; + Ok(BindingAction::Consume) + } + "expel" => { + no_argument()?; + Ok(BindingAction::Expel) + } + "toggle-floating" => { + no_argument()?; + Ok(BindingAction::ToggleFloating) + } + _ => Err(ConfigError::InvalidBinding(format!( + "unknown action {action:?}" + ))), + } +} + +fn parse_direction(argument: &str) -> Result { + match argument { + "left" => Ok(Direction::Left), + "right" => Ok(Direction::Right), + "up" => Ok(Direction::Up), + "down" => Ok(Direction::Down), + _ => Err(ConfigError::InvalidBinding(format!( + "invalid direction {argument:?}" + ))), + } +} + +fn parse_workspace(argument: &str) -> Result { + match argument.parse() { + Ok(workspace) if workspace > 0 => Ok(workspace), + _ => Err(ConfigError::InvalidBinding(format!( + "invalid workspace {argument:?}" + ))), + } +} + +fn default_bindings() -> Vec { + let mut bindings = vec![ + binding("Super+Enter", "spawn", Some("terminal")), + binding("Super+Q", "close", None), + binding("Super+F", "toggle-fullscreen", None), + binding("Super+M", "toggle-layout", None), + binding("Super+R", "cycle-column-width", None), + binding("Super+C", "center-column", None), + binding("Super+[", "consume", None), + binding("Super+]", "expel", None), + binding("Super+Shift+Space", "toggle-floating", None), + ]; + + for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] { + bindings.push(binding("Super+".to_owned() + key, "focus", Some(direction))); + bindings.push(binding( + "Super+Shift+".to_owned() + key, + "move", + Some(direction), + )); + bindings.push(binding( + "Super+Ctrl+".to_owned() + key, + "resize", + Some(direction), + )); + } + + for workspace in 1..=9 { + let workspace = workspace.to_string(); + bindings.push(binding( + format!("Super+{workspace}"), + "workspace", + Some(&workspace), + )); + bindings.push(binding( + format!("Super+Shift+{workspace}"), + "move-to-workspace", + Some(&workspace), + )); + } + + bindings +} + +fn binding( + keys: impl Into, + action: impl Into, + argument: Option<&str>, +) -> BindingConfig { + BindingConfig { + keys: keys.into(), + match_mode: BindingMatch::Keysym, + action: Some(action.into()), + argument: argument.map(str::to_owned), + disabled: false, + } +} + fn positive_animation_value(value: f64, field: &'static str) -> Result { if value.is_finite() && value > 0.0 { Ok(value) @@ -675,6 +1126,80 @@ mod tests { assert!(delay.input_settings().is_err()); } + #[test] + fn supplies_complete_v0_bindings_and_terminal_command() { + let config = parse(""); + let input = config.input_settings().unwrap(); + let bindings = config.bindings(&input).unwrap(); + + assert_eq!(bindings.len(), 39); + assert!(bindings.iter().any(|binding| { + binding.modifiers.logo + && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return) + && binding.action == BindingAction::Spawn(vec!["foot".to_owned()]) + })); + } + + #[test] + fn replaces_and_unbinds_default_bindings() { + let replaced = parse( + "[commands]\nterm = [\"foot\", \"--app-id\", \"work\"]\n\n[[bindings]]\nkeys = \"Super+Enter\"\naction = \"spawn\"\nargument = \"term\"", + ); + let unbound = parse("[[bindings]]\nkeys = \"Super+Q\"\ndisabled = true"); + + let input = replaced.input_settings().unwrap(); + let bindings = replaced.bindings(&input).unwrap(); + assert_eq!(bindings.len(), 39); + assert!(bindings.iter().any(|binding| { + binding.action + == BindingAction::Spawn(vec![ + "foot".to_owned(), + "--app-id".to_owned(), + "work".to_owned(), + ]) + })); + + let input = unbound.input_settings().unwrap(); + let bindings = unbound.bindings(&input).unwrap(); + assert_eq!(bindings.len(), 38); + assert!( + !bindings + .iter() + .any(|binding| binding.action == BindingAction::Close) + ); + } + + #[test] + fn accepts_physical_xkb_key_names() { + let config = parse( + "[[bindings]]\nkeys = \"Super+AD06\"\nmatch = \"physical\"\naction = \"focus\"\nargument = \"left\"", + ); + let input = config.input_settings().unwrap(); + let bindings = config.bindings(&input).unwrap(); + + assert!(bindings.iter().any(|binding| { + matches!(binding.trigger, BindingTrigger::Physical(_)) + && binding.action == BindingAction::Focus(Direction::Left) + })); + } + + #[test] + fn rejects_duplicate_or_invalid_user_bindings() { + let duplicate = parse( + "[[bindings]]\nkeys = \"Super+Q\"\naction = \"close\"\n\n[[bindings]]\nkeys = \"logo+q\"\naction = \"close\"", + ); + let missing_command = parse( + "[[bindings]]\nkeys = \"Super+Enter\"\naction = \"spawn\"\nargument = \"missing\"", + ); + let invalid_argument = + parse("[[bindings]]\nkeys = \"Super+Q\"\naction = \"close\"\nargument = \"left\""); + + for config in [duplicate, missing_command, invalid_argument] { + let input = config.input_settings().unwrap(); + assert!(config.bindings(&input).is_err()); + } + } + #[test] fn derives_critical_viewport_damping() { let config = parse(""); diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 395a95a..2c99357 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -1,3 +1,5 @@ +use std::process::Command; + use smithay::{ backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, @@ -17,8 +19,7 @@ use smithay::{ }, }; -use crate::Ferese; -use ferese_layout::Direction; +use crate::{Ferese, config::BindingAction}; impl Ferese { pub fn process_input_event(&mut self, event: InputEvent) { @@ -73,140 +74,33 @@ impl Ferese { return FilterResult::Intercept(()); } - let direction = match symbol { - keysyms::KEY_h | keysyms::KEY_H => Some(Direction::Left), - keysyms::KEY_j | keysyms::KEY_J => Some(Direction::Down), - keysyms::KEY_k | keysyms::KEY_K => Some(Direction::Up), - keysyms::KEY_l | keysyms::KEY_L => Some(Direction::Right), - _ => None, + let raw_symbols = keysym + .raw_syms() + .into_iter() + .map(|symbol| symbol.raw()) + .collect::>(); + let action = data.bindings.iter().find_map(|binding| { + binding + .matches( + keycode, + &raw_symbols, + modifiers.logo, + modifiers.ctrl, + modifiers.alt, + modifiers.shift, + ) + .then(|| binding.action.clone()) + }); + let Some(action) = action else { + return FilterResult::Forward; }; - if modifiers.logo - && modifiers.ctrl - && !modifiers.alt - && !modifiers.shift - && let Some(direction) = direction - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.resize_direction(direction); - } - - FilterResult::Intercept(()) - } else if modifiers.logo - && !modifiers.ctrl - && !modifiers.alt - && let Some(direction) = direction - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - - if modifiers.shift { - data.move_direction(direction); - } else { - data.focus_direction(direction); - } - } - - FilterResult::Intercept(()) - } else if modifiers.logo && !modifiers.ctrl && !modifiers.alt { - let workspace = match symbol { - keysyms::KEY_1 => Some(1), - keysyms::KEY_2 => Some(2), - keysyms::KEY_3 => Some(3), - keysyms::KEY_4 => Some(4), - keysyms::KEY_5 => Some(5), - keysyms::KEY_6 => Some(6), - keysyms::KEY_7 => Some(7), - keysyms::KEY_8 => Some(8), - keysyms::KEY_9 => Some(9), - _ => None, - }; - - if let Some(workspace) = workspace { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - - if modifiers.shift { - data.move_focused_to_workspace(workspace); - } else { - data.switch_workspace(workspace); - } - } - - FilterResult::Intercept(()) - } else if !modifiers.shift - && matches!(symbol, keysyms::KEY_q | keysyms::KEY_Q) - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.close_focused_window(); - } - - FilterResult::Intercept(()) - } else if !modifiers.shift - && matches!(symbol, keysyms::KEY_f | keysyms::KEY_F) - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.toggle_focused_fullscreen(); - } - - FilterResult::Intercept(()) - } else if !modifiers.shift - && matches!(symbol, keysyms::KEY_m | keysyms::KEY_M) - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.toggle_layout_mode(); - } - - FilterResult::Intercept(()) - } else if !modifiers.shift - && matches!(symbol, keysyms::KEY_r | keysyms::KEY_R) - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.cycle_focused_column_width(); - } - - FilterResult::Intercept(()) - } else if !modifiers.shift - && matches!(symbol, keysyms::KEY_c | keysyms::KEY_C) - { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.center_focused_column(); - } - - FilterResult::Intercept(()) - } else if !modifiers.shift && symbol == keysyms::KEY_bracketleft { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.consume_focused_window(); - } - - FilterResult::Intercept(()) - } else if !modifiers.shift && symbol == keysyms::KEY_bracketright { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.expel_focused_window(); - } - - FilterResult::Intercept(()) - } else if modifiers.shift && symbol == keysyms::KEY_space { - if state == KeyState::Pressed { - data.intercepted_keys.insert(keycode); - data.toggle_focused_floating(); - } - - FilterResult::Intercept(()) - } else { - FilterResult::Forward - } - } else { - FilterResult::Forward + if state == KeyState::Pressed { + data.intercepted_keys.insert(keycode); + data.execute_binding(action); } + + FilterResult::Intercept(()) }, ); } @@ -520,6 +414,40 @@ impl Ferese { } }); } + + fn execute_binding(&mut self, action: BindingAction) { + match action { + BindingAction::Spawn(mut argv) => { + let program = argv.remove(0); + + match Command::new(&program).args(argv).spawn() { + Ok(child) => { + tracing::info!(%program, pid = child.id(), "spawned binding command") + } + Err(error) => { + tracing::warn!(%program, %error, "failed to spawn binding command") + } + } + } + BindingAction::Close => self.close_focused_window(), + BindingAction::Focus(direction) => self.focus_direction(direction), + BindingAction::Move(direction) => self.move_direction(direction), + BindingAction::Resize(direction) => self.resize_direction(direction), + BindingAction::SwitchWorkspace(workspace) => { + self.switch_workspace(u32::from(workspace)); + } + BindingAction::MoveToWorkspace(workspace) => { + self.move_focused_to_workspace(u32::from(workspace)); + } + BindingAction::ToggleFullscreen => self.toggle_focused_fullscreen(), + BindingAction::ToggleLayout => self.toggle_layout_mode(), + BindingAction::CycleColumnWidth => self.cycle_focused_column_width(), + BindingAction::CenterColumn => self.center_focused_column(), + BindingAction::Consume => self.consume_focused_window(), + BindingAction::Expel => self.expel_focused_window(), + BindingAction::ToggleFloating => self.toggle_focused_floating(), + } + } } fn virtual_terminal(symbol: u32, ctrl: bool, alt: bool, direct: bool) -> Option { diff --git a/compositor/src/main.rs b/compositor/src/main.rs index fb904ad..00a5048 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -24,12 +24,15 @@ fn main() -> Result<(), Box> { let launch = LaunchConfig::from_environment() .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; let config = Config::load()?; + let input_settings = config.input_settings()?; + let bindings = config.bindings(&input_settings)?; let mut event_loop = EventLoop::try_new()?; let display = Display::new()?; let runtime = RuntimeConfig { layout_mode: config.layout_mode(), gap_config: config.gap_config()?, - input_settings: config.input_settings()?, + input_settings, + bindings, default_column_width: config.default_column_width()?, scrolling_focus_strategy: config.scrolling_focus_strategy(), column_width_presets: config.width_presets()?, diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 1b14078..bf8bc12 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -75,7 +75,7 @@ use smithay::{ }, }; -use crate::config::InputSettings; +use crate::config::{Binding, InputSettings}; pub struct Ferese { pub start_time: Instant, @@ -95,6 +95,7 @@ pub struct Ferese { column_width_presets: Vec, gap_config: GapConfig, pub(crate) input_settings: InputSettings, + pub(crate) bindings: Vec, animations_enabled: bool, animation_speed: f64, spring_config: SpringConfig, @@ -144,6 +145,7 @@ pub struct RuntimeConfig { pub layout_mode: LayoutMode, pub gap_config: GapConfig, pub input_settings: InputSettings, + pub bindings: Vec, pub default_column_width: ColumnWidth, pub scrolling_focus_strategy: ViewportFocusStrategy, pub column_width_presets: Vec, @@ -383,6 +385,7 @@ impl Ferese { column_width_presets: config.column_width_presets, gap_config: config.gap_config, input_settings: config.input_settings, + bindings: config.bindings, animations_enabled: config.animations_enabled, animation_speed: config.animation_speed, spring_config: config.spring_config,