Diff
diff --git a/src/bar/bar.rs b/src/bar/bar.rs
index f45e59c..567e3cb 100644
--- a/src/bar/bar.rs
+++ b/src/bar/bar.rs
@@ -172,6 +172,7 @@ impl Bar {
occupied_tags: u32,
draw_blocks: bool,
layout_symbol: &str,
+ keychord_indicator: Option<&str>,
) -> Result<(), X11Error> {
if !self.needs_redraw {
return Ok(());
@@ -261,6 +262,25 @@ impl Bar {
layout_symbol
);
+ // Update x_position after layout symbol
+ x_position += font.text_width(layout_symbol) as i16;
+
+ // Draw keychord indicator if present
+ if let Some(indicator) = keychord_indicator {
+ x_position += 10;
+
+ let text_x = x_position;
+ let text_y = top_padding + font.ascent();
+
+ self.font_draw.draw_text(
+ font,
+ self.scheme_selected.foreground,
+ text_x,
+ text_y,
+ indicator,
+ );
+ }
+
if draw_blocks && !self.status_text.is_empty() {
let padding = 10;
let mut x_position = self.width as i16 - padding;
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 2fc4649..ee93417 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,6 +1,6 @@
use crate::bar::{BlockCommand, BlockConfig};
use crate::errors::ConfigError;
-use crate::keyboard::handlers::Key;
+use crate::keyboard::handlers::{KeyBinding, KeyPress};
use crate::keyboard::keycodes;
use crate::keyboard::{Arg, KeyAction};
use serde::Deserialize;
@@ -8,6 +8,7 @@ use x11rb::protocol::xproto::{KeyButMask, Keycode};
#[derive(Debug, Deserialize)]
pub enum ModKey {
+ Mod,
Mod1,
Mod2,
Mod3,
@@ -20,6 +21,7 @@ pub enum ModKey {
impl ModKey {
fn to_keybut_mask(&self) -> KeyButMask {
match self {
+ ModKey::Mod => panic!("ModKey::Mod should be replaced during config parsing"),
ModKey::Mod1 => KeyButMask::MOD1,
ModKey::Mod2 => KeyButMask::MOD2,
ModKey::Mod3 => KeyButMask::MOD3,
@@ -196,13 +198,23 @@ struct ConfigData {
#[derive(Debug, Deserialize)]
struct KeybindingData {
- modifiers: Vec<ModKey>,
- key: KeyData,
+ #[serde(default)]
+ keys: Option<Vec<KeyPressData>>, // New format
+ #[serde(default)]
+ modifiers: Option<Vec<ModKey>>, // Old format (backwards compat)
+ #[serde(default)]
+ key: Option<KeyData>, // Old format (backwards compat)
action: KeyAction,
#[serde(default)]
arg: ArgData,
}
+#[derive(Debug, Deserialize)]
+struct KeyPressData {
+ modifiers: Vec<ModKey>,
+ key: KeyData,
+}
+
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ArgData {
@@ -250,17 +262,46 @@ fn config_data_to_config(data: ConfigData) -> Result<crate::Config, ConfigError>
let mut keybindings = Vec::new();
for kb_data in data.keybindings {
- let modifiers = kb_data
- .modifiers
- .iter()
- .map(|m| m.to_keybut_mask())
- .collect();
+ let keys = if let Some(keys_data) = kb_data.keys {
+ // New format: multiple keys
+ keys_data
+ .into_iter()
+ .map(|kp| {
+ let modifiers = kp
+ .modifiers
+ .iter()
+ .map(|m| match m {
+ ModKey::Mod => modkey,
+ _ => m.to_keybut_mask(),
+ })
+ .collect();
+
+ KeyPress {
+ modifiers,
+ key: kp.key.to_keycode(),
+ }
+ })
+ .collect()
+ } else if let (Some(modifiers), Some(key)) = (kb_data.modifiers, kb_data.key) {
+ // Old format: single key (backwards compatibility)
+ vec![KeyPress {
+ modifiers: modifiers
+ .iter()
+ .map(|m| match m {
+ ModKey::Mod => modkey,
+ _ => m.to_keybut_mask(),
+ })
+ .collect(),
+ key: key.to_keycode(),
+ }]
+ } else {
+ return Err(ConfigError::ValidationError("Keybinding must have either 'keys' or 'modifiers'+'key'".to_string()));
+ };
- let key = kb_data.key.to_keycode();
let action = kb_data.action;
let arg = arg_data_to_arg(kb_data.arg)?;
- keybindings.push(Key::new(modifiers, key, action, arg));
+ keybindings.push(KeyBinding::new(keys, action, arg));
}
let mut status_blocks = Vec::new();
diff --git a/src/errors.rs b/src/errors.rs
index f698f07..9555d90 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -25,6 +25,7 @@ pub enum ConfigError {
UnknownAction(String),
UnknownBlockCommand(String),
MissingCommandArg { command: String, field: String },
+ ValidationError(String),
}
impl std::fmt::Display for WmError {
@@ -67,6 +68,7 @@ impl std::fmt::Display for ConfigError {
Self::MissingCommandArg { command, field } => {
write!(f, "{} command requires {}", command, field)
}
+ Self::ValidationError(msg) => write!(f, "Config validation error: {}", msg),
}
}
}
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 15df5de..c7722c9 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -1,4 +1,6 @@
+use std::collections::HashSet;
use std::io;
+use std::io::ErrorKind;
use std::process::Command;
use serde::Deserialize;
@@ -6,6 +8,7 @@ use x11rb::connection::Connection;
use x11rb::protocol::xproto::*;
use crate::errors::X11Error;
+use crate::keyboard::keycodes;
#[derive(Debug, Copy, Clone, Deserialize)]
pub enum KeyAction {
@@ -42,26 +45,58 @@ impl Arg {
}
}
-#[derive(Clone)]
-pub struct Key {
+// Individual key press in a sequence
+#[derive(Clone, Debug)]
+pub struct KeyPress {
pub(crate) modifiers: Vec<KeyButMask>,
pub(crate) key: Keycode,
+}
+
+// Keybinding that can be a single key or a chord (sequence of keys)
+#[derive(Clone)]
+pub struct KeyBinding {
+ pub(crate) keys: Vec<KeyPress>,
pub(crate) func: KeyAction,
pub(crate) arg: Arg,
}
-impl Key {
- pub fn new(modifiers: Vec<KeyButMask>, key: Keycode, func: KeyAction, arg: Arg) -> Self {
+impl KeyBinding {
+ pub fn new(keys: Vec<KeyPress>, func: KeyAction, arg: Arg) -> Self {
+ Self { keys, func, arg }
+ }
+
+ // Helper for backwards compatibility with single-key bindings
+ pub fn single_key(modifiers: Vec<KeyButMask>, key: Keycode, func: KeyAction, arg: Arg) -> Self {
Self {
- modifiers,
- key,
+ keys: vec![KeyPress { modifiers, key }],
func,
arg,
}
}
}
-fn modifiers_to_mask(modifiers: &[KeyButMask]) -> u16 {
+// For backwards compatibility during migration
+pub type Key = KeyBinding;
+
+#[derive(Debug, Clone)]
+pub enum KeychordState {
+ Idle,
+ InProgress {
+ // Indices of keybindings that could still match
+ candidates: Vec<usize>,
+ // How many keys have been pressed in the sequence
+ keys_pressed: usize,
+ },
+}
+
+pub enum KeychordResult {
+ Completed(KeyAction, Arg),
+ InProgress(Vec<usize>),
+ None,
+ Cancelled,
+}
+
+pub fn modifiers_to_mask(modifiers: &[KeyButMask]) -> u16 {
modifiers
.iter()
.fold(0u16, |acc, &modifier| acc | u16::from(modifier))
@@ -70,47 +105,153 @@ fn modifiers_to_mask(modifiers: &[KeyButMask]) -> u16 {
pub fn setup_keybinds(
connection: &impl Connection,
root: Window,
- keybindings: &[Key],
+ keybindings: &[KeyBinding],
) -> Result<(), X11Error> {
+ let mut grabbed_keys: HashSet<(u16, Keycode)> = HashSet::new();
+
for keybinding in keybindings {
- let modifier_mask = modifiers_to_mask(&keybinding.modifiers);
-
- connection.grab_key(
- false,
- root,
- modifier_mask.into(),
- keybinding.key,
- GrabMode::ASYNC,
- GrabMode::ASYNC,
- )?;
+ if keybinding.keys.is_empty() {
+ continue;
+ }
+
+ let first_key = &keybinding.keys[0];
+ let modifier_mask = modifiers_to_mask(&first_key.modifiers);
+ let key_tuple = (modifier_mask, first_key.key);
+
+ if grabbed_keys.insert(key_tuple) {
+ connection.grab_key(
+ false,
+ root,
+ modifier_mask.into(),
+ first_key.key,
+ GrabMode::ASYNC,
+ GrabMode::ASYNC,
+ )?;
+ }
}
+
+ connection.grab_key(
+ false,
+ root,
+ ModMask::from(0u16),
+ keycodes::ESCAPE,
+ GrabMode::ASYNC,
+ GrabMode::ASYNC,
+ )?;
+
Ok(())
}
-pub fn handle_key_press(event: KeyPressEvent, keybindings: &[Key]) -> (KeyAction, Arg) {
- for keybinding in keybindings {
- let modifier_mask = modifiers_to_mask(&keybinding.modifiers);
+pub fn handle_key_press(
+ event: KeyPressEvent,
+ keybindings: &[KeyBinding],
+ keychord_state: &KeychordState,
+) -> KeychordResult {
+ if event.detail == keycodes::ESCAPE {
+ return match keychord_state {
+ KeychordState::InProgress { .. } => KeychordResult::Cancelled,
+ KeychordState::Idle => KeychordResult::None,
+ };
+ }
+
+ match keychord_state {
+ KeychordState::Idle => {
+ handle_first_key(event, keybindings)
+ }
+ KeychordState::InProgress { candidates, keys_pressed } => {
+ handle_next_key(event, keybindings, candidates, *keys_pressed)
+ }
+ }
+}
+
+fn handle_first_key(
+ event: KeyPressEvent,
+ keybindings: &[KeyBinding],
+) -> KeychordResult {
+ let mut candidates = Vec::new();
+
+ for (keybinding_index, keybinding) in keybindings.iter().enumerate() {
+ if keybinding.keys.is_empty() {
+ continue;
+ }
+
+ let first_key = &keybinding.keys[0];
+ let modifier_mask = modifiers_to_mask(&first_key.modifiers);
- if event.detail == keybinding.key && event.state == modifier_mask.into() {
- return (keybinding.func, keybinding.arg.clone());
+ if event.detail == first_key.key && event.state == modifier_mask.into() {
+ if keybinding.keys.len() == 1 {
+ return KeychordResult::Completed(
+ keybinding.func,
+ keybinding.arg.clone(),
+ );
+ } else {
+ candidates.push(keybinding_index);
+ }
}
}
- (KeyAction::None, Arg::None)
+ if candidates.is_empty() {
+ KeychordResult::None
+ } else {
+ KeychordResult::InProgress(candidates)
+ }
+}
+
+fn handle_next_key(
+ event: KeyPressEvent,
+ keybindings: &[KeyBinding],
+ candidates: &[usize],
+ keys_pressed: usize,
+) -> KeychordResult {
+ let mut new_candidates = Vec::new();
+
+ for &candidate_index in candidates {
+ let keybinding = &keybindings[candidate_index];
+
+ if keys_pressed >= keybinding.keys.len() {
+ continue;
+ }
+
+ let next_key = &keybinding.keys[keys_pressed];
+ let required_mask = modifiers_to_mask(&next_key.modifiers);
+ let event_state: u16 = event.state.into();
+
+ let modifiers_match = if next_key.modifiers.is_empty() {
+ true
+ } else {
+ (event_state & required_mask) == required_mask
+ };
+
+ if event.detail == next_key.key && modifiers_match {
+ if keys_pressed + 1 == keybinding.keys.len() {
+ return KeychordResult::Completed(
+ keybinding.func,
+ keybinding.arg.clone(),
+ );
+ } else {
+ new_candidates.push(candidate_index);
+ }
+ }
+ }
+
+ if new_candidates.is_empty() {
+ KeychordResult::Cancelled
+ } else {
+ KeychordResult::InProgress(new_candidates)
+ }
}
pub fn handle_spawn_action(action: KeyAction, arg: &Arg) -> io::Result<()> {
- use io::ErrorKind;
if let KeyAction::Spawn = action {
match arg {
Arg::Str(command) => match Command::new(command.as_str()).spawn() {
- Err(err) if err.kind() == ErrorKind::NotFound => {
+ Err(error) if error.kind() == ErrorKind::NotFound => {
eprintln!(
"KeyAction::Spawn failed: could not spawn \"{}\", command not found",
command
);
}
- Err(err) => Err(err)?,
+ Err(error) => Err(error)?,
_ => (),
},
Arg::Array(command) => {
@@ -120,13 +261,13 @@ pub fn handle_spawn_action(action: KeyAction, arg: &Arg) -> io::Result<()> {
let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
match Command::new(cmd.as_str()).args(&args_str).spawn() {
- Err(err) if err.kind() == ErrorKind::NotFound => {
+ Err(error) if error.kind() == ErrorKind::NotFound => {
eprintln!(
"KeyAction::Spawn failed: could not spawn \"{}\", command not found",
cmd
);
}
- Err(err) => Err(err)?,
+ Err(error) => Err(error)?,
_ => (),
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 147cdb6..75da74b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,7 +10,7 @@ pub mod prelude {
pub use crate::ColorScheme;
pub use crate::LayoutSymbolOverride;
pub use crate::bar::{BlockCommand, BlockConfig};
- pub use crate::keyboard::{Arg, KeyAction, handlers::Key, keycodes};
+ pub use crate::keyboard::{Arg, KeyAction, handlers::KeyBinding, keycodes};
pub use x11rb::protocol::xproto::KeyButMask;
}
@@ -66,7 +66,7 @@ pub struct ColorScheme {
impl Default for Config {
fn default() -> Self {
- use crate::keyboard::handlers::Key;
+ use crate::keyboard::handlers::KeyBinding;
use crate::keyboard::{Arg, KeyAction, keycodes};
use x11rb::protocol::xproto::KeyButMask;
@@ -93,13 +93,13 @@ impl Default for Config {
.collect(),
layout_symbols: vec![],
keybindings: vec![
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::RETURN,
KeyAction::Spawn,
Arg::Str(TERMINAL.to_string()),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::D,
KeyAction::Spawn,
@@ -109,167 +109,167 @@ impl Default for Config {
"dmenu_run -l 10".to_string(),
]),
),
- Key::new(vec![MODKEY], keycodes::Q, KeyAction::KillClient, Arg::None),
- Key::new(vec![MODKEY], keycodes::N, KeyAction::CycleLayout, Arg::None),
- Key::new(
+ KeyBinding::single_key(vec![MODKEY], keycodes::Q, KeyAction::KillClient, Arg::None),
+ KeyBinding::single_key(vec![MODKEY], keycodes::N, KeyAction::CycleLayout, Arg::None),
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::F,
KeyAction::ToggleFullScreen,
Arg::None,
),
- Key::new(vec![MODKEY], keycodes::A, KeyAction::ToggleGaps, Arg::None),
- Key::new(vec![MODKEY, SHIFT], keycodes::Q, KeyAction::Quit, Arg::None),
- Key::new(
+ KeyBinding::single_key(vec![MODKEY], keycodes::A, KeyAction::ToggleGaps, Arg::None),
+ KeyBinding::single_key(vec![MODKEY, SHIFT], keycodes::Q, KeyAction::Quit, Arg::None),
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::R,
KeyAction::Restart,
Arg::None,
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::F,
KeyAction::ToggleFloating,
Arg::None,
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::J,
KeyAction::FocusStack,
Arg::Int(-1),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::K,
KeyAction::FocusStack,
Arg::Int(1),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::K,
KeyAction::ExchangeClient,
Arg::Int(0), // UP
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::J,
KeyAction::ExchangeClient,
Arg::Int(1), // DOWN
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::H,
KeyAction::ExchangeClient,
Arg::Int(2), // LEFT
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::L,
KeyAction::ExchangeClient,
Arg::Int(3), // RIGHT
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_1,
KeyAction::ViewTag,
Arg::Int(0),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_2,
KeyAction::ViewTag,
Arg::Int(1),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_3,
KeyAction::ViewTag,
Arg::Int(2),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_4,
KeyAction::ViewTag,
Arg::Int(3),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_5,
KeyAction::ViewTag,
Arg::Int(4),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_6,
KeyAction::ViewTag,
Arg::Int(5),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_7,
KeyAction::ViewTag,
Arg::Int(6),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_8,
KeyAction::ViewTag,
Arg::Int(7),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY],
keycodes::KEY_9,
KeyAction::ViewTag,
Arg::Int(8),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_1,
KeyAction::MoveToTag,
Arg::Int(0),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_2,
KeyAction::MoveToTag,
Arg::Int(1),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_3,
KeyAction::MoveToTag,
Arg::Int(2),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_4,
KeyAction::MoveToTag,
Arg::Int(3),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_5,
KeyAction::MoveToTag,
Arg::Int(4),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_6,
KeyAction::MoveToTag,
Arg::Int(5),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_7,
KeyAction::MoveToTag,
Arg::Int(6),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_8,
KeyAction::MoveToTag,
Arg::Int(7),
),
- Key::new(
+ KeyBinding::single_key(
vec![MODKEY, SHIFT],
keycodes::KEY_9,
KeyAction::MoveToTag,
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 5ef536e..a30d97e 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -68,6 +68,7 @@ pub struct WindowManager {
previous_focused: Option<Window>,
display: *mut x11::xlib::Display,
font: crate::bar::font::Font,
+ keychord_state: keyboard::handlers::KeychordState,
}
type WmResult<T> = Result<T, WmError>;
@@ -176,6 +177,7 @@ impl WindowManager {
previous_focused: None,
display,
font,
+ keychord_state: keyboard::handlers::KeychordState::Idle,
};
window_manager.scan_existing_windows()?;
@@ -799,8 +801,75 @@ impl WindowManager {
.unwrap_or_else(|| self.layout.symbol().to_string())
}
+ fn get_keychord_indicator(&self) -> Option<String> {
+ match &self.keychord_state {
+ keyboard::handlers::KeychordState::Idle => None,
+ keyboard::handlers::KeychordState::InProgress { candidates, keys_pressed } => {
+ if candidates.is_empty() {
+ return None;
+ }
+
+ let binding = &self.config.keybindings[candidates[0]];
+ let mut indicator = String::new();
+
+ for (i, key_press) in binding.keys.iter().take(*keys_pressed).enumerate() {
+ if i > 0 {
+ indicator.push(' ');
+ }
+
+ for modifier in &key_press.modifiers {
+ indicator.push_str(Self::format_modifier(*modifier));
+ indicator.push('+');
+ }
+
+ indicator.push_str(&self.format_keycode(key_press.key));
+ }
+
+ indicator.push('-');
+ Some(indicator)
+ }
+ }
+ }
+
+ fn format_modifier(modifier: KeyButMask) -> &'static str {
+ match modifier {
+ KeyButMask::MOD1 => "Alt",
+ KeyButMask::MOD4 => "Super",
+ KeyButMask::SHIFT => "Shift",
+ KeyButMask::CONTROL => "Ctrl",
+ _ => "Mod",
+ }
+ }
+
+ fn format_keycode(&self, keycode: Keycode) -> String {
+ unsafe {
+ let keysym = x11::xlib::XKeycodeToKeysym(self.display, keycode, 0);
+ if keysym == 0 {
+ return "?".to_string();
+ }
+
+ let name_ptr = x11::xlib::XKeysymToString(keysym);
+ if name_ptr.is_null() {
+ return "?".to_string();
+ }
+
+ let c_str = std::ffi::CStr::from_ptr(name_ptr);
+ match c_str.to_str() {
+ Ok(s) => {
+ if s.len() == 1 {
+ s.to_lowercase()
+ } else {
+ s.to_string()
+ }
+ }
+ Err(_) => "?".to_string(),
+ }
+ }
+ }
+
fn update_bar(&mut self) -> WmResult<()> {
let layout_symbol = self.get_layout_symbol();
+ let keychord_indicator = self.get_keychord_indicator();
for (monitor_index, monitor) in self.monitors.iter().enumerate() {
if let Some(bar) = self.bars.get_mut(monitor_index) {
@@ -821,6 +890,7 @@ impl WindowManager {
occupied_tags,
draw_blocks,
&layout_symbol,
+ keychord_indicator.as_deref(),
)?;
}
}
@@ -1124,6 +1194,39 @@ impl WindowManager {
Ok(())
}
+ fn grab_next_keys(&self, candidates: &[usize], keys_pressed: usize) -> WmResult<()> {
+ let mut grabbed_keys: HashSet<(u16, Keycode)> = HashSet::new();
+
+ for &candidate_index in candidates {
+ let binding = &self.config.keybindings[candidate_index];
+ if keys_pressed < binding.keys.len() {
+ let next_key = &binding.keys[keys_pressed];
+ let modifier_mask = keyboard::handlers::modifiers_to_mask(&next_key.modifiers);
+ let key_tuple = (modifier_mask, next_key.key);
+
+ if grabbed_keys.insert(key_tuple) {
+ self.connection.grab_key(
+ false,
+ self.root,
+ modifier_mask.into(),
+ next_key.key,
+ GrabMode::ASYNC,
+ GrabMode::ASYNC,
+ )?;
+ }
+ }
+ }
+ self.connection.flush()?;
+ Ok(())
+ }
+
+ fn ungrab_chord_keys(&self) -> WmResult<()> {
+ self.connection.ungrab_key(x11rb::protocol::xproto::Grab::ANY, self.root, ModMask::ANY)?;
+ keyboard::setup_keybinds(&self.connection, self.root, &self.config.keybindings)?;
+ self.connection.flush()?;
+ Ok(())
+ }
+
pub fn set_focus(&mut self, window: Window) -> WmResult<()> {
let old_focused = self.previous_focused;
@@ -1372,11 +1475,43 @@ impl WindowManager {
}
}
Event::KeyPress(event) => {
- let (action, arg) = keyboard::handle_key_press(event, &self.config.keybindings);
- match action {
- KeyAction::Quit => return Ok(Some(false)),
- KeyAction::Restart => return Ok(Some(true)),
- _ => self.handle_key_action(action, &arg)?,
+ let result = keyboard::handle_key_press(
+ event,
+ &self.config.keybindings,
+ &self.keychord_state,
+ );
+
+ match result {
+ keyboard::handlers::KeychordResult::Completed(action, arg) => {
+ self.keychord_state = keyboard::handlers::KeychordState::Idle;
+ self.ungrab_chord_keys()?;
+ self.update_bar()?;
+
+ match action {
+ KeyAction::Quit => return Ok(Some(false)),
+ KeyAction::Restart => return Ok(Some(true)),
+ _ => self.handle_key_action(action, &arg)?,
+ }
+ }
+ keyboard::handlers::KeychordResult::InProgress(candidates) => {
+ let keys_pressed = match &self.keychord_state {
+ keyboard::handlers::KeychordState::Idle => 1,
+ keyboard::handlers::KeychordState::InProgress { keys_pressed, .. } => keys_pressed + 1,
+ };
+
+ self.keychord_state = keyboard::handlers::KeychordState::InProgress {
+ candidates: candidates.clone(),
+ keys_pressed,
+ };
+
+ self.grab_next_keys(&candidates, keys_pressed)?;
+ self.update_bar()?;
+ }
+ keyboard::handlers::KeychordResult::Cancelled | keyboard::handlers::KeychordResult::None => {
+ self.keychord_state = keyboard::handlers::KeychordState::Idle;
+ self.ungrab_chord_keys()?;
+ self.update_bar()?;
+ }
}
}
Event::ButtonPress(event) => {
diff --git a/templates/config.ron b/templates/config.ron
index 69af9f1..8cdbb9a 100644
--- a/templates/config.ron
+++ b/templates/config.ron
@@ -23,7 +23,22 @@
(name: "normie", symbol: "[F]"),
],
+ // Keybinding Format:
+ //
+ // New format (supports keychords - multi-key sequences):
+ // (keys: [(modifiers: [...], key: X), (modifiers: [...], key: Y)], action: ..., arg: ...)
+ //
+ // Old format (single key, backwards compatible):
+ // (modifiers: [...], key: X, action: ..., arg: ...)
+ //
+ // Examples:
+ // Single key: (keys: [(modifiers: [Mod4], key: Return)], action: Spawn, arg: "st")
+ // Keychord: (keys: [(modifiers: [Mod4], key: Space), (modifiers: [], key: F)], action: Spawn, arg: "firefox")
+ //
+ // You can cancel any in-progress keychord by pressing Escape.
+
keybindings: [
+ // Basic keybindings (old format for backwards compatibility)
(modifiers: [Mod4], key: Return, action: Spawn, arg: "st"),
(modifiers: [Mod4], key: D, action: Spawn, arg: ["sh", "-c", "dmenu_run -l 10"]),
(modifiers: [Mod4], key: S, action: Spawn, arg: ["sh", "-c", "maim -s | xclip -selection clipboard -t image/png"]),
@@ -70,6 +85,19 @@
(modifiers: [Mod4, Shift], key: J, action: ExchangeClient, arg: 1), // DOWN
(modifiers: [Mod4, Shift], key: H, action: ExchangeClient, arg: 2), // LEFT
(modifiers: [Mod4, Shift], key: L, action: ExchangeClient, arg: 3), // RIGHT
+
+ // Example keychord bindings (uncomment to use):
+ // Press Mod4+Space, then f to spawn firefox
+ // (keys: [(modifiers: [Mod4], key: Space), (modifiers: [], key: F)], action: Spawn, arg: "firefox"),
+
+ // Press Mod4+Space, then t to spawn terminal
+ // (keys: [(modifiers: [Mod4], key: Space), (modifiers: [], key: T)], action: Spawn, arg: "st"),
+
+ // Press Mod4+e, then e to spawn a text editor (same key twice)
+ // (keys: [(modifiers: [Mod4], key: E), (modifiers: [], key: E)], action: Spawn, arg: "nvim"),
+
+ // Press Mod4+x, then Shift+c to kill client (modifier on second key)
+ // (keys: [(modifiers: [Mod4], key: X), (modifiers: [Shift], key: C)], action: KillClient),
],
status_blocks: [
diff --git a/test-config.ron b/test-config.ron
index ac3303a..eb258fc 100644
--- a/test-config.ron
+++ b/test-config.ron
@@ -31,6 +31,7 @@
keybindings: [
(modifiers: [Mod1], key: Return, action: Spawn, arg: "st"),
+ (keys: [(modifiers: [Mod1], key: Space), (modifiers: [], key: T)], action: Spawn, arg: "st"),
(modifiers: [Mod1], key: D, action: Spawn, arg: ["sh", "-c", "dmenu_run -l 10"]),
(modifiers: [Mod1], key: S, action: Spawn, arg: ["sh", "-c", "maim -s | xclip -selection clipboard -t image/png"]),
(modifiers: [Mod1], key: Q, action: KillClient),