feat: configure commands and key bindings

This commit is contained in:
Samuel Onoja committed 2026-09-25 02:57:36 +01:00
1 parent 2f83c58040
commit 29af1bec9c
5 files changed
+619 -133

No files matched your search

+27
View File
@@ -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 nested backend, the host compositor remains responsible for physical touchpad
configuration. 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 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 hidden unless the session explicitly opts in, because an input method can
request privileged keyboard access. Enable it only when launching a trusted request privileged keyboard access. Enable it only when launching a trusted
+527 -2
View File
@@ -1,3 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
@@ -7,8 +8,9 @@ use std::path::PathBuf;
use ferese_animation::SpringConfig; use ferese_animation::SpringConfig;
use ferese_core::LayoutMode; use ferese_core::LayoutMode;
use ferese_layout::{ColumnWidth, GapConfig, ViewportFocusStrategy}; use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
use serde::Deserialize; use serde::Deserialize;
use smithay::input::keyboard::{Keycode, keysyms, xkb};
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct Config { pub struct Config {
@@ -19,9 +21,95 @@ pub struct Config {
#[serde(default)] #[serde(default)]
input: InputConfig, input: InputConfig,
#[serde(default)] #[serde(default)]
commands: HashMap<String, Vec<String>>,
#[serde(default)]
bindings: Vec<BindingConfig>,
#[serde(default)]
scrolling: ScrollingConfig, 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<String>),
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<String>,
argument: Option<String>,
#[serde(default)]
disabled: bool,
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct InputSettings { pub struct InputSettings {
pub xkb_layout: String, pub xkb_layout: String,
@@ -231,6 +319,7 @@ pub enum ConfigError {
field: &'static str, field: &'static str,
value: String, value: String,
}, },
InvalidBinding(String),
} }
impl fmt::Display for ConfigError { impl fmt::Display for ConfigError {
@@ -255,6 +344,7 @@ impl fmt::Display for ConfigError {
Self::InvalidInputValue { field, value } => { Self::InvalidInputValue { field, value } => {
write!(formatter, "invalid input.{field} value {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::InvalidColumnWidth { .. }
| Self::InvalidAnimationValue { .. } | Self::InvalidAnimationValue { .. }
| Self::InvalidLayoutValue { .. } | Self::InvalidLayoutValue { .. }
| Self::InvalidInputValue { .. } => None, | Self::InvalidInputValue { .. }
| Self::InvalidBinding(_) => None,
} }
} }
} }
@@ -376,6 +467,43 @@ impl Config {
}) })
} }
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, 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::<Result<Vec<_>, _>>()?;
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 { pub fn animations_enabled(&self) -> bool {
self.animations.enabled && !self.animations.reduced_motion 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<String, Vec<String>>) -> 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<xkb::Keymap, ConfigError> {
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<String, Vec<String>>,
keymap: &xkb::Keymap,
) -> Result<Binding, ConfigError> {
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<BindingIdentity, ConfigError> {
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<u32, ConfigError> {
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<String, Vec<String>>,
) -> Result<BindingAction, ConfigError> {
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<Direction, ConfigError> {
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<u8, ConfigError> {
match argument.parse() {
Ok(workspace) if workspace > 0 => Ok(workspace),
_ => Err(ConfigError::InvalidBinding(format!(
"invalid workspace {argument:?}"
))),
}
}
fn default_bindings() -> Vec<BindingConfig> {
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<String>,
action: impl Into<String>,
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<f64, ConfigError> { fn positive_animation_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
if value.is_finite() && value > 0.0 { if value.is_finite() && value > 0.0 {
Ok(value) Ok(value)
@@ -675,6 +1126,80 @@ mod tests {
assert!(delay.input_settings().is_err()); 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] #[test]
fn derives_critical_viewport_damping() { fn derives_critical_viewport_damping() {
let config = parse(""); let config = parse("");
+57 -129
View File
@@ -1,3 +1,5 @@
use std::process::Command;
use smithay::{ use smithay::{
backend::input::{ backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
@@ -17,8 +19,7 @@ use smithay::{
}, },
}; };
use crate::Ferese; use crate::{Ferese, config::BindingAction};
use ferese_layout::Direction;
impl Ferese { impl Ferese {
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) { pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
@@ -73,140 +74,33 @@ impl Ferese {
return FilterResult::Intercept(()); return FilterResult::Intercept(());
} }
let direction = match symbol { let raw_symbols = keysym
keysyms::KEY_h | keysyms::KEY_H => Some(Direction::Left), .raw_syms()
keysyms::KEY_j | keysyms::KEY_J => Some(Direction::Down), .into_iter()
keysyms::KEY_k | keysyms::KEY_K => Some(Direction::Up), .map(|symbol| symbol.raw())
keysyms::KEY_l | keysyms::KEY_L => Some(Direction::Right), .collect::<Vec<_>>();
_ => None, 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 { if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode); data.intercepted_keys.insert(keycode);
data.resize_direction(direction); data.execute_binding(action);
} }
FilterResult::Intercept(()) 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
}
}, },
); );
} }
@@ -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<i32> { fn virtual_terminal(symbol: u32, ctrl: bool, alt: bool, direct: bool) -> Option<i32> {
+4 -1
View File
@@ -24,12 +24,15 @@ fn main() -> Result<(), Box<dyn Error>> {
let launch = LaunchConfig::from_environment() let launch = LaunchConfig::from_environment()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
let config = Config::load()?; let config = Config::load()?;
let input_settings = config.input_settings()?;
let bindings = config.bindings(&input_settings)?;
let mut event_loop = EventLoop::try_new()?; let mut event_loop = EventLoop::try_new()?;
let display = Display::new()?; let display = Display::new()?;
let runtime = RuntimeConfig { let runtime = RuntimeConfig {
layout_mode: config.layout_mode(), layout_mode: config.layout_mode(),
gap_config: config.gap_config()?, gap_config: config.gap_config()?,
input_settings: config.input_settings()?, input_settings,
bindings,
default_column_width: config.default_column_width()?, default_column_width: config.default_column_width()?,
scrolling_focus_strategy: config.scrolling_focus_strategy(), scrolling_focus_strategy: config.scrolling_focus_strategy(),
column_width_presets: config.width_presets()?, column_width_presets: config.width_presets()?,
+4 -1
View File
@@ -75,7 +75,7 @@ use smithay::{
}, },
}; };
use crate::config::InputSettings; use crate::config::{Binding, InputSettings};
pub struct Ferese { pub struct Ferese {
pub start_time: Instant, pub start_time: Instant,
@@ -95,6 +95,7 @@ pub struct Ferese {
column_width_presets: Vec<ColumnWidth>, column_width_presets: Vec<ColumnWidth>,
gap_config: GapConfig, gap_config: GapConfig,
pub(crate) input_settings: InputSettings, pub(crate) input_settings: InputSettings,
pub(crate) bindings: Vec<Binding>,
animations_enabled: bool, animations_enabled: bool,
animation_speed: f64, animation_speed: f64,
spring_config: SpringConfig, spring_config: SpringConfig,
@@ -144,6 +145,7 @@ pub struct RuntimeConfig {
pub layout_mode: LayoutMode, pub layout_mode: LayoutMode,
pub gap_config: GapConfig, pub gap_config: GapConfig,
pub input_settings: InputSettings, pub input_settings: InputSettings,
pub bindings: Vec<Binding>,
pub default_column_width: ColumnWidth, pub default_column_width: ColumnWidth,
pub scrolling_focus_strategy: ViewportFocusStrategy, pub scrolling_focus_strategy: ViewportFocusStrategy,
pub column_width_presets: Vec<ColumnWidth>, pub column_width_presets: Vec<ColumnWidth>,
@@ -383,6 +385,7 @@ impl Ferese {
column_width_presets: config.column_width_presets, column_width_presets: config.column_width_presets,
gap_config: config.gap_config, gap_config: config.gap_config,
input_settings: config.input_settings, input_settings: config.input_settings,
bindings: config.bindings,
animations_enabled: config.animations_enabled, animations_enabled: config.animations_enabled,
animation_speed: config.animation_speed, animation_speed: config.animation_speed,
spring_config: config.spring_config, spring_config: config.spring_config,