fix(config): add support for Ctrl and Alt modifiers on function keys in keybindings (#3179)

Previously, it was not possible to define keybindings with a modifier
and a function key. The `Key` enum only supported combinging the Ctrl
and Alt modifiers with letters. This is somewhat limiting: I would like
to make the Zellij keybindings more "distant" than those used in the
programs I use from within Zelilj, so that Zellij does not intefere with
those programs. Thus I would like to move some of the keybindings from
Ctrl+<character> to Ctrl+<function key>.

This change adds:

  * support for function keys with the Ctrl and Alt modifiers in the
    `Key` enum,
  * support for parsing such keybindings from the configuration file,
  * support for such keybindings in the protobuf which communicates the
    keybindings to plugins, and
  * support for these keybindings in the plugin API.

This is tested by modifying one of the e2e tests to include an example
of such keybindings. This verifies that the configuration is correctly
parsed, communicated with the plugin, and rendered.
This commit is contained in:
Bradford Hovinen 2024-03-25 15:04:38 +01:00 committed by GitHub
parent 223730f04a
commit 312817fcf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 120 additions and 72 deletions

View file

@ -62,6 +62,8 @@ impl KeyShortcut {
} else { } else {
match key { match key {
Key::F(c) => format!("{}", c), Key::F(c) => format!("{}", c),
Key::CtrlF(n) => format!("F{}", n),
Key::AltF(n) => format!("F{}", n),
Key::Ctrl(c) => format!("{}", c), Key::Ctrl(c) => format!("{}", c),
Key::Char(_) => format!("{}", key), Key::Char(_) => format!("{}", key),
Key::Alt(c) => format!("{}", c), Key::Alt(c) => format!("{}", c),

View file

@ -333,8 +333,8 @@ pub fn get_common_modifier(keyvec: Vec<&Key>) -> Option<String> {
let mut new_modifier; let mut new_modifier;
for key in keyvec.iter() { for key in keyvec.iter() {
match key { match key {
Key::Ctrl(_) => new_modifier = "Ctrl", Key::Ctrl(_) | Key::CtrlF(_) => new_modifier = "Ctrl",
Key::Alt(_) => new_modifier = "Alt", Key::Alt(_) | Key::AltF(_) => new_modifier = "Alt",
_ => return None, _ => return None,
} }
if modifier.is_empty() { if modifier.is_empty() {
@ -468,7 +468,9 @@ pub fn style_key_with_modifier(
} else { } else {
match key { match key {
Key::Ctrl(c) => format!("{}", Key::Char(*c)), Key::Ctrl(c) => format!("{}", Key::Char(*c)),
Key::CtrlF(n) => format!("{}", Key::F(*n)),
Key::Alt(c) => format!("{}", c), Key::Alt(c) => format!("{}", c),
Key::AltF(n) => format!("{}", Key::F(*n)),
_ => format!("{}", key), _ => format!("{}", key),
} }
} }

View file

@ -25,5 +25,5 @@ expression: last_snapshot
│ │ │ │
│ │ │ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
<F1> LOCK  <F2> PANE  <F3> TAB  <F4> RESIZE  <F5> MOVE  <F6> SEARCH  <F7> SESSION  <F8> QUIT  <F1> LOCK  <F2> PANE  <F3> TAB  <F4> RESIZE  <F5> MOVE  <F6> SEARCH  <Alt+F7> SESSION  <Ctrl+F8> QUIT 
Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size. Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size.

View file

@ -6,7 +6,7 @@ keybinds clear-defaults=true {
bind "F4" { SwitchToMode "Resize"; } bind "F4" { SwitchToMode "Resize"; }
bind "F5" { SwitchToMode "Move"; } bind "F5" { SwitchToMode "Move"; }
bind "F6" { SwitchToMode "Scroll"; } bind "F6" { SwitchToMode "Scroll"; }
bind "F7" { SwitchToMode "Session"; } bind "Alt F7" { SwitchToMode "Session"; }
bind "F8" { Quit; } bind "Ctrl F8" { Quit; }
} }
} }

View file

@ -62,6 +62,8 @@ pub enum Key {
BackTab, BackTab,
Null, Null,
Esc, Esc,
AltF(u8),
CtrlF(u8),
} }
impl FromStr for Key { impl FromStr for Key {
@ -78,14 +80,7 @@ impl FromStr for Key {
} }
match (modifier, main_key) { match (modifier, main_key) {
(Some("Ctrl"), Some(main_key)) => { (Some("Ctrl"), Some(main_key)) => {
let mut key_chars = main_key.chars(); parse_main_key(main_key, key_str, Key::Ctrl, Key::CtrlF)
let key_count = main_key.chars().count();
if key_count == 1 {
let key_char = key_chars.next().unwrap();
Ok(Key::Ctrl(key_char))
} else {
Err(format!("Failed to parse key: {}", key_str).into())
}
}, },
(Some("Alt"), Some(main_key)) => { (Some("Alt"), Some(main_key)) => {
match main_key { match main_key {
@ -96,16 +91,12 @@ impl FromStr for Key {
"Right" => Ok(Key::Alt(CharOrArrow::Direction(Direction::Right))), "Right" => Ok(Key::Alt(CharOrArrow::Direction(Direction::Right))),
"Up" => Ok(Key::Alt(CharOrArrow::Direction(Direction::Up))), "Up" => Ok(Key::Alt(CharOrArrow::Direction(Direction::Up))),
"Down" => Ok(Key::Alt(CharOrArrow::Direction(Direction::Down))), "Down" => Ok(Key::Alt(CharOrArrow::Direction(Direction::Down))),
_ => { _ => parse_main_key(
let mut key_chars = main_key.chars(); main_key,
let key_count = main_key.chars().count(); key_str,
if key_count == 1 { |c| Key::Alt(CharOrArrow::Char(c)),
let key_char = key_chars.next().unwrap(); Key::AltF,
Ok(Key::Alt(CharOrArrow::Char(key_char))) ),
} else {
Err(format!("Failed to parse key: {}", key_str).into())
}
},
} }
}, },
(None, Some(main_key)) => match main_key { (None, Some(main_key)) => match main_key {
@ -124,12 +115,24 @@ impl FromStr for Key {
"Space" => Ok(Key::Char(' ')), "Space" => Ok(Key::Char(' ')),
"Enter" => Ok(Key::Char('\n')), "Enter" => Ok(Key::Char('\n')),
"Esc" => Ok(Key::Esc), "Esc" => Ok(Key::Esc),
_ => { _ => parse_main_key(main_key, key_str, Key::Char, Key::F),
},
_ => Err(format!("Failed to parse key: {}", key_str).into()),
}
}
}
fn parse_main_key(
main_key: &str,
key_str: &str,
to_char_key: impl FnOnce(char) -> Key,
to_fn_key: impl FnOnce(u8) -> Key,
) -> Result<Key, Box<dyn std::error::Error>> {
let mut key_chars = main_key.chars(); let mut key_chars = main_key.chars();
let key_count = main_key.chars().count(); let key_count = main_key.chars().count();
if key_count == 1 { if key_count == 1 {
let key_char = key_chars.next().unwrap(); let key_char = key_chars.next().unwrap();
Ok(Key::Char(key_char)) Ok(to_char_key(key_char))
} else if key_count > 1 { } else if key_count > 1 {
if let Some(first_char) = key_chars.next() { if let Some(first_char) = key_chars.next() {
if first_char == 'F' { if first_char == 'F' {
@ -138,7 +141,7 @@ impl FromStr for Key {
.parse() .parse()
.map_err(|e| format!("Failed to parse F index: {}", e))?; .map_err(|e| format!("Failed to parse F index: {}", e))?;
if f_index >= 1 && f_index <= 12 { if f_index >= 1 && f_index <= 12 {
return Ok(Key::F(f_index)); return Ok(to_fn_key(f_index));
} }
} }
} }
@ -146,11 +149,6 @@ impl FromStr for Key {
} else { } else {
Err(format!("Failed to parse key: {}", key_str).into()) Err(format!("Failed to parse key: {}", key_str).into())
} }
},
},
_ => Err(format!("Failed to parse key: {}", key_str).into()),
}
}
} }
impl fmt::Display for Key { impl fmt::Display for Key {
@ -177,6 +175,8 @@ impl fmt::Display for Key {
}, },
Key::Alt(c) => write!(f, "Alt+{}", c), Key::Alt(c) => write!(f, "Alt+{}", c),
Key::Ctrl(c) => write!(f, "Ctrl+{}", Key::Char(*c)), Key::Ctrl(c) => write!(f, "Ctrl+{}", Key::Char(*c)),
Key::AltF(n) => write!(f, "Alt+F{}", n),
Key::CtrlF(n) => write!(f, "Ctrl+F{}", n),
Key::Null => write!(f, "NULL"), Key::Null => write!(f, "NULL"),
Key::Esc => write!(f, "ESC"), Key::Esc => write!(f, "ESC"),
} }

View file

@ -113,7 +113,15 @@ mod not_wasm {
KeyCode::Tab => Key::BackTab, // TODO: ??? KeyCode::Tab => Key::BackTab, // TODO: ???
KeyCode::Delete => Key::Delete, KeyCode::Delete => Key::Delete,
KeyCode::Insert => Key::Insert, KeyCode::Insert => Key::Insert,
KeyCode::Function(n) => Key::F(n), KeyCode::Function(n) => {
if modifiers.contains(Modifiers::ALT) {
Key::AltF(n)
} else if modifiers.contains(Modifiers::CTRL) {
Key::CtrlF(n)
} else {
Key::F(n)
}
},
KeyCode::Escape => Key::Esc, KeyCode::Escape => Key::Esc,
KeyCode::Enter => Key::Char('\n'), KeyCode::Enter => Key::Char('\n'),
_ => Key::Esc, // there are other keys we can implement here, but we might need additional terminal support to implement them, not just exhausting this enum _ => Key::Esc, // there are other keys we can implement here, but we might need additional terminal support to implement them, not just exhausting this enum

View file

@ -12,12 +12,21 @@ impl TryFrom<ProtobufKey> for Key {
let key_modifier = parse_optional_modifier(&protobuf_key); let key_modifier = parse_optional_modifier(&protobuf_key);
match key_modifier { match key_modifier {
Some(KeyModifier::Ctrl) => { Some(KeyModifier::Ctrl) => {
let character = char_from_main_key(protobuf_key.main_key)?; if let Ok(character) = char_from_main_key(protobuf_key.main_key.clone()) {
Ok(Key::Ctrl(character)) Ok(Key::Ctrl(character))
} else {
let index = fn_index_from_main_key(protobuf_key.main_key)?;
Ok(Key::CtrlF(index))
}
}, },
Some(KeyModifier::Alt) => { Some(KeyModifier::Alt) => {
let char_or_arrow = CharOrArrow::from_main_key(protobuf_key.main_key)?; if let Ok(char_or_arrow) = CharOrArrow::from_main_key(protobuf_key.main_key.clone())
{
Ok(Key::Alt(char_or_arrow)) Ok(Key::Alt(char_or_arrow))
} else {
let index = fn_index_from_main_key(protobuf_key.main_key)?;
Ok(Key::AltF(index))
}
}, },
None => match protobuf_key.main_key.as_ref().ok_or("invalid key")? { None => match protobuf_key.main_key.as_ref().ok_or("invalid key")? {
MainKey::Char(_key_index) => { MainKey::Char(_key_index) => {
@ -81,27 +90,18 @@ impl TryFrom<Key> for ProtobufKey {
modifier: None, modifier: None,
main_key: Some(MainKey::Key(NamedKey::Insert as i32)), main_key: Some(MainKey::Key(NamedKey::Insert as i32)),
}), }),
Key::F(index) => { Key::F(index) => Ok(ProtobufKey {
let main_key = match index {
1 => Some(MainKey::Key(NamedKey::F1 as i32)),
2 => Some(MainKey::Key(NamedKey::F2 as i32)),
3 => Some(MainKey::Key(NamedKey::F3 as i32)),
4 => Some(MainKey::Key(NamedKey::F4 as i32)),
5 => Some(MainKey::Key(NamedKey::F5 as i32)),
6 => Some(MainKey::Key(NamedKey::F6 as i32)),
7 => Some(MainKey::Key(NamedKey::F7 as i32)),
8 => Some(MainKey::Key(NamedKey::F8 as i32)),
9 => Some(MainKey::Key(NamedKey::F9 as i32)),
10 => Some(MainKey::Key(NamedKey::F10 as i32)),
11 => Some(MainKey::Key(NamedKey::F11 as i32)),
12 => Some(MainKey::Key(NamedKey::F12 as i32)),
_ => return Err("Invalid key"),
};
Ok(ProtobufKey {
modifier: None, modifier: None,
main_key, main_key: Some(fn_index_to_main_key(index)?),
}) }),
}, Key::CtrlF(index) => Ok(ProtobufKey {
modifier: Some(KeyModifier::Ctrl as i32),
main_key: Some(fn_index_to_main_key(index)?),
}),
Key::AltF(index) => Ok(ProtobufKey {
modifier: Some(KeyModifier::Alt as i32),
main_key: Some(fn_index_to_main_key(index)?),
}),
Key::Char(character) => Ok(ProtobufKey { Key::Char(character) => Ok(ProtobufKey {
modifier: None, modifier: None,
main_key: Some(MainKey::Char((character as u8) as i32)), main_key: Some(MainKey::Char((character as u8) as i32)),
@ -147,6 +147,24 @@ impl TryFrom<Key> for ProtobufKey {
} }
} }
fn fn_index_to_main_key(index: u8) -> Result<MainKey, &'static str> {
match index {
1 => Ok(MainKey::Key(NamedKey::F1 as i32)),
2 => Ok(MainKey::Key(NamedKey::F2 as i32)),
3 => Ok(MainKey::Key(NamedKey::F3 as i32)),
4 => Ok(MainKey::Key(NamedKey::F4 as i32)),
5 => Ok(MainKey::Key(NamedKey::F5 as i32)),
6 => Ok(MainKey::Key(NamedKey::F6 as i32)),
7 => Ok(MainKey::Key(NamedKey::F7 as i32)),
8 => Ok(MainKey::Key(NamedKey::F8 as i32)),
9 => Ok(MainKey::Key(NamedKey::F9 as i32)),
10 => Ok(MainKey::Key(NamedKey::F10 as i32)),
11 => Ok(MainKey::Key(NamedKey::F11 as i32)),
12 => Ok(MainKey::Key(NamedKey::F12 as i32)),
_ => Err("Invalid key"),
}
}
impl CharOrArrow { impl CharOrArrow {
pub fn from_main_key( pub fn from_main_key(
main_key: std::option::Option<MainKey>, main_key: std::option::Option<MainKey>,
@ -191,6 +209,24 @@ fn char_from_main_key(main_key: Option<MainKey>) -> Result<char, &'static str> {
} }
} }
fn fn_index_from_main_key(main_key: Option<MainKey>) -> Result<u8, &'static str> {
match main_key {
Some(MainKey::Key(n)) if n == NamedKey::F1 as i32 => Ok(1),
Some(MainKey::Key(n)) if n == NamedKey::F2 as i32 => Ok(2),
Some(MainKey::Key(n)) if n == NamedKey::F3 as i32 => Ok(3),
Some(MainKey::Key(n)) if n == NamedKey::F4 as i32 => Ok(4),
Some(MainKey::Key(n)) if n == NamedKey::F5 as i32 => Ok(5),
Some(MainKey::Key(n)) if n == NamedKey::F6 as i32 => Ok(6),
Some(MainKey::Key(n)) if n == NamedKey::F7 as i32 => Ok(7),
Some(MainKey::Key(n)) if n == NamedKey::F8 as i32 => Ok(8),
Some(MainKey::Key(n)) if n == NamedKey::F9 as i32 => Ok(9),
Some(MainKey::Key(n)) if n == NamedKey::F10 as i32 => Ok(10),
Some(MainKey::Key(n)) if n == NamedKey::F11 as i32 => Ok(11),
Some(MainKey::Key(n)) if n == NamedKey::F12 as i32 => Ok(12),
_ => Err("Unsupported key"),
}
}
fn named_key_to_key(named_key: NamedKey) -> Key { fn named_key_to_key(named_key: NamedKey) -> Key {
match named_key { match named_key {
NamedKey::PageDown => Key::PageDown, NamedKey::PageDown => Key::PageDown,