aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ri/river-mk-keymap/src/key_map
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-06-29 10:32:13 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-06-29 10:32:13 +0200
commit3d507acb42554b2551024ee3ca8490c203a1a9f8 (patch)
treececa79f3696cf9eab522be55c07c32e38de5edaf /pkgs/by-name/ri/river-mk-keymap/src/key_map
parentflake.lock: Update (diff)
downloadnixos-config-3d507acb42554b2551024ee3ca8490c203a1a9f8.zip
pkgs/river-mk-keymap: Improve with key-chord support and which-key interface
Diffstat (limited to 'pkgs/by-name/ri/river-mk-keymap/src/key_map')
-rw-r--r--pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs367
-rw-r--r--pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs105
2 files changed, 341 insertions, 131 deletions
diff --git a/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs b/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs
index e948ccfe..07c41918 100644
--- a/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs
+++ b/pkgs/by-name/ri/river-mk-keymap/src/key_map/commands.rs
@@ -8,112 +8,289 @@
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-use std::process::Command;
+use std::{env::current_exe, path::Path, process::Command};
-use keymaps::key_repr::{KeyValue, MediaKeyCode, MouseKeyValue};
+use anyhow::{bail, Result};
+use keymaps::key_repr::{Key, KeyValue, Keys, MediaKeyCode, ModifierKeyCode, MouseKeyValue};
+use rustix::path::Arg;
-use super::{KeyMap, MapMode};
+use super::KeyMap;
impl KeyMap {
- #[must_use]
- pub fn to_commands(self) -> Vec<Command> {
- self.0
- .iter()
- .flat_map(|(key, value)| {
- let key = key.last().expect("Will exist");
- let mods = {
- let modifiers = key.modifiers();
- let mut output = vec![];
+ /// # Errors
+ /// If impossible requests are made.
+ ///
+ /// # Panics
+ /// If internal assertions fail.
+ #[allow(clippy::too_many_lines)]
+ pub fn to_commands(self, keymap_path: &Path) -> Result<Vec<Command>> {
+ self.0.iter().try_for_each(|(keys, value)| {
+ let (prefix, last) = keys.split_at(keys.len() - 1);
+ let prefix = prefix.to_owned();
- if modifiers.alt() {
- output.push("Alt");
- }
- if modifiers.ctrl() {
- output.push("Control");
- }
- if modifiers.meta() {
- output.push("Super");
- }
- if modifiers.shift() {
- output.push("Shift");
- }
- if output.is_empty() {
- "None".to_owned()
- } else {
- output.join("+")
- }
- };
- let key_value = match key.value() {
- KeyValue::Backspace => "BackSpace".to_owned(),
- KeyValue::Enter => "Enter".to_owned(),
- KeyValue::Left => "Left".to_owned(),
- KeyValue::Right => "Right".to_owned(),
- KeyValue::Up => "Up".to_owned(),
- KeyValue::Down => "Down".to_owned(),
- KeyValue::Home => "Home".to_owned(),
- KeyValue::End => "End".to_owned(),
- KeyValue::PageUp => "Page_Up".to_owned(),
- KeyValue::PageDown => "Page_Down".to_owned(),
- KeyValue::Tab => "Tab".to_owned(),
- KeyValue::BackTab => "BackTab".to_owned(),
- KeyValue::Delete => "Delete".to_owned(),
- KeyValue::Insert => "Insert".to_owned(),
- KeyValue::F(num) => format!("F{num}"),
- KeyValue::Char(a) => a.to_string(),
- KeyValue::Null => "Null".to_owned(),
- KeyValue::Esc => "Esc".to_owned(),
- KeyValue::CapsLock => "CapsLock".to_owned(),
- KeyValue::ScrollLock => "ScrollLock".to_owned(),
- KeyValue::NumLock => "NumLock".to_owned(),
- KeyValue::PrintScreen => "Print".to_owned(),
- KeyValue::Pause => "Pause".to_owned(),
- KeyValue::Menu => "Menu".to_owned(),
- KeyValue::KeypadBegin => "KeypadBegin".to_owned(),
- KeyValue::Media(media_key_code) => match media_key_code {
- MediaKeyCode::Play => "XF86AudioPlay".to_owned(),
- MediaKeyCode::Pause => "XF86AudioPause".to_owned(),
- MediaKeyCode::PlayPause => "XF86AudioPlayPause".to_owned(),
- MediaKeyCode::Reverse => "XF86AudioReverse".to_owned(),
- MediaKeyCode::Stop => "XF86AudioStop".to_owned(),
- MediaKeyCode::FastForward => "XF86AudioFastForward".to_owned(),
- MediaKeyCode::Rewind => "XF86AudioRewind".to_owned(),
- MediaKeyCode::TrackNext => "XF86AudioTrackNext".to_owned(),
- MediaKeyCode::TrackPrevious => "XF86AudioTrackPrevious".to_owned(),
- MediaKeyCode::Record => "XF86AudioRecord".to_owned(),
- MediaKeyCode::LowerVolume => "XF86AudioLowerVolume".to_owned(),
- MediaKeyCode::RaiseVolume => "XF86AudioRaiseVolume".to_owned(),
- MediaKeyCode::MuteVolume => "XF86AudioMuteVolume".to_owned(),
- },
- KeyValue::MouseKey(mouse_key_value) => match mouse_key_value {
- MouseKeyValue::Left => "BTN_LEFT".to_owned(),
- MouseKeyValue::Right => "BTN_RIGHT".to_owned(),
- MouseKeyValue::Middle => "BTN_MIDDLE".to_owned(),
- },
- _ => todo!(),
- };
+ if value.allow_locked && !prefix.is_empty() {
+ bail!(
+ "Only single key mappings can be used \
+ in locked mode, but '{}' contains multiple ('{}').",
+ Keys::from(keys),
+ Keys::from(prefix),
+ )
+ }
- value
- .modes
- .iter()
- .map(|mode| {
- let mut riverctl = Command::new("riverctl");
- riverctl.args([value.map_mode.as_command(), mode, &mods, &key_value]);
+ if !prefix.is_empty()
+ && [
+ "<ESC>".parse().expect("hardcoded"),
+ "<BACKSPACE>".parse().expect("hardcoded"),
+ ]
+ .contains(&last[0])
+ {
+ bail!(
+ "You cannot use <ESC> or <BACKSPACE> as the final part of a \
+ prefixed mapping, as that is used to return \
+ to 'normal' or the upper mode; found in '{}'",
+ Keys::from(keys),
+ )
+ }
- riverctl.args(value.command.iter().map(String::as_str));
- riverctl
- })
- .collect::<Vec<_>>()
+ Ok(())
+ })?;
+
+ let output = self
+ .0
+ .into_iter()
+ .flat_map(|(keys, value)| {
+ let (prefix, mapping) = keys.split_at(keys.len() - 1);
+
+ let (final_mode, mut base): (Option<String>, _) =
+ prefix
+ .iter()
+ .fold((None, vec![]), |(acc_mode, mut acc_vec), key| {
+ // Declare intermediate modes for each key.
+ let mode_name: String = {
+ let base = key.to_string_repr();
+
+ if let Some(result) = &acc_mode {
+ result.to_owned() + base.as_str()
+ } else {
+ base
+ }
+ };
+
+ let mut riverctl = Command::new("riverctl");
+ riverctl.args(["declare-mode", mode_name.as_str()]);
+
+ let mut output = vec![riverctl];
+
+ // Provide keymaps for entering and leaving the mode
+ if let Some(acc_mode) = acc_mode.clone() {
+ output.extend(key_to_command(
+ key.to_owned(),
+ &["enter-mode".to_owned(), mode_name.clone()],
+ &acc_mode,
+ false,
+ ));
+ } else {
+ // Also spawn the help display if we start from the “normal” mode.
+ output.extend(key_to_command(
+ key.to_owned(),
+ &[
+ "spawn".to_owned(),
+ format!(
+ "{} && sleep 1 && {}",
+ shlex::try_join([
+ "riverctl",
+ "enter-mode",
+ mode_name.as_str()
+ ])
+ .expect("Should work"),
+ shlex::try_join([
+ current_exe()
+ .expect("Should have a current exe")
+ .as_os_str()
+ .as_str()
+ .expect("Should be valid utf8"),
+ "--keymap",
+ keymap_path.to_str().expect("Should be valid utf8"),
+ "show-help",
+ ])
+ .expect("Should work"),
+ ),
+ ],
+ "normal",
+ false,
+ ));
+ }
+
+ // Provide a mapping for going up a mode
+ output.extend(key_to_command(
+ "<BACKSPACE>".parse().expect("Hardcoded"),
+ &[
+ "enter-mode".to_owned(),
+ acc_mode.unwrap_or("normal".to_owned()),
+ ],
+ &mode_name,
+ false,
+ ));
+
+ // Another one for going back to normal.
+ output.extend(key_to_command(
+ "<ESC>".parse().expect("Hardcoded"),
+ &["enter-mode".to_owned(), "normal".to_owned()],
+ &mode_name,
+ false,
+ ));
+
+ acc_vec.extend(output);
+
+ (Some(mode_name), acc_vec)
+ });
+
+ base.extend(key_to_command(
+ mapping[0],
+ &value.command,
+ final_mode.as_ref().map_or("normal", |v| v.as_str()),
+ value.allow_locked,
+ ));
+
+ base
})
- .collect()
+ .collect();
+
+ Ok(output)
}
}
-impl MapMode {
- pub(crate) fn as_command(self) -> &'static str {
- match self {
- MapMode::Map => "map",
- MapMode::MapMouse => "map-pointer",
- MapMode::Unmap => "unmap",
+fn key_value_to_xkb_common_name(value: KeyValue) -> (String, Vec<&'static str>) {
+ let mut extra_modifiers = vec![];
+
+ let output = match value {
+ KeyValue::Backspace => "BackSpace".to_owned(),
+ KeyValue::Enter => "Return".to_owned(),
+ KeyValue::Left => "Left".to_owned(),
+ KeyValue::Right => "Right".to_owned(),
+ KeyValue::Up => "Up".to_owned(),
+ KeyValue::Down => "Down".to_owned(),
+ KeyValue::Home => "Home".to_owned(),
+ KeyValue::End => "End".to_owned(),
+ KeyValue::PageUp => "Page_Up".to_owned(),
+ KeyValue::PageDown => "Page_Down".to_owned(),
+ KeyValue::Tab => "Tab".to_owned(),
+ KeyValue::BackTab => "BackTab".to_owned(),
+ KeyValue::Delete => "Delete".to_owned(),
+ KeyValue::Insert => "Insert".to_owned(),
+ KeyValue::F(num) => format!("F{num}"),
+ KeyValue::Char(a) => {
+ // River does not differentiate between 'a' and 'A',
+ // so we need to do it beforehand.
+ if a.is_ascii_uppercase() {
+ extra_modifiers.push("Shift");
+ }
+
+ if a == ' ' {
+ "Space".to_string()
+ } else {
+ a.to_string()
+ }
}
+ KeyValue::Null => "Null".to_owned(),
+ KeyValue::Esc => "Escape".to_owned(),
+ KeyValue::CapsLock => "CapsLock".to_owned(),
+ KeyValue::ScrollLock => "ScrollLock".to_owned(),
+ KeyValue::NumLock => "NumLock".to_owned(),
+ KeyValue::PrintScreen => "Print".to_owned(),
+ KeyValue::Pause => "Pause".to_owned(),
+ KeyValue::Menu => "Menu".to_owned(),
+ KeyValue::KeypadBegin => "KeypadBegin".to_owned(),
+ KeyValue::Media(media_key_code) => match media_key_code {
+ MediaKeyCode::Play => "XF86AudioPlay".to_owned(),
+ MediaKeyCode::Pause => "XF86AudioPause".to_owned(),
+ MediaKeyCode::PlayPause => "XF86AudioPlayPause".to_owned(),
+ MediaKeyCode::Reverse => "XF86AudioReverse".to_owned(),
+ MediaKeyCode::Stop => "XF86AudioStop".to_owned(),
+ MediaKeyCode::FastForward => "XF86AudioFastForward".to_owned(),
+ MediaKeyCode::Rewind => "XF86AudioRewind".to_owned(),
+ MediaKeyCode::TrackNext => "XF86AudioTrackNext".to_owned(),
+ MediaKeyCode::TrackPrevious => "XF86AudioTrackPrevious".to_owned(),
+ MediaKeyCode::Record => "XF86AudioRecord".to_owned(),
+ MediaKeyCode::LowerVolume => "XF86AudioLowerVolume".to_owned(),
+ MediaKeyCode::RaiseVolume => "XF86AudioRaiseVolume".to_owned(),
+ MediaKeyCode::MuteVolume => "XF86AudioMute".to_owned(),
+ },
+ KeyValue::MouseKey(mouse_key_value) => match mouse_key_value {
+ MouseKeyValue::Left => "BTN_LEFT".to_owned(),
+ MouseKeyValue::Right => "BTN_RIGHT".to_owned(),
+ MouseKeyValue::Middle => "BTN_MIDDLE".to_owned(),
+ },
+ KeyValue::ModifierKey(modifier_key_code) => match modifier_key_code {
+ ModifierKeyCode::LeftAlt => "ALT_L".to_owned(),
+ ModifierKeyCode::RightAlt => "ALT_R".to_owned(),
+ ModifierKeyCode::LeftCtrl => "CTRL_L".to_owned(),
+ ModifierKeyCode::RightCtrl => "CTRL_R".to_owned(),
+ ModifierKeyCode::LeftMeta => "SUPER_L".to_owned(),
+ ModifierKeyCode::RightMeta => "SUPER_R".to_owned(),
+ ModifierKeyCode::LeftShift => "SHIFT_L".to_owned(),
+ ModifierKeyCode::RightShift => "SHIFT_R".to_owned(),
+ },
+ other => todo!("Key value: {other} not known."),
+ };
+
+ (output, extra_modifiers)
+}
+
+fn key_to_command(key: Key, command: &[String], mode: &str, allow_locked: bool) -> Vec<Command> {
+ let mut modifiers = {
+ let modifiers = key.modifiers();
+ let mut output = vec![];
+
+ if modifiers.alt() {
+ output.push("Alt");
+ }
+ if modifiers.ctrl() {
+ output.push("Control");
+ }
+ if modifiers.meta() {
+ output.push("Super");
+ }
+ if modifiers.shift() {
+ output.push("Shift");
+ }
+ output
+ };
+
+ let (key_value, extra_modifiers) = key_value_to_xkb_common_name(key.value());
+ modifiers.extend(extra_modifiers);
+
+ let map_mode = if let KeyValue::MouseKey(_) = key.value() {
+ "map-pointer"
+ } else {
+ "map"
+ };
+
+ let modifiers = if modifiers.is_empty() {
+ "None".to_owned()
+ } else {
+ modifiers.join("+")
+ };
+
+ let mut output = vec![{
+ let mut riverctl = Command::new("riverctl");
+ riverctl.args([map_mode, mode, &modifiers, &key_value]);
+
+ riverctl.args(command.iter().map(String::as_str));
+
+ riverctl
+ }];
+
+ if allow_locked {
+ output.push({
+ let mut riverctl = Command::new("riverctl");
+ riverctl.args([map_mode, "locked", &modifiers, &key_value]);
+
+ riverctl.args(command.iter().map(String::as_str));
+
+ riverctl
+ });
}
+
+ output
}
diff --git a/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs b/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs
index 2c82ee05..60ed41b8 100644
--- a/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs
+++ b/pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs
@@ -8,40 +8,91 @@
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-use std::{collections::HashMap, fmt::Display, ops::Deref, str::FromStr};
+use std::{fmt::Display, ops::Deref, str::FromStr};
-use anyhow::Context;
-use keymaps::{key_repr::Key, map_tree::MapTrie};
+use anyhow::{anyhow, bail, Context, Result};
+use keymaps::{
+ key_repr::{Key, Keys},
+ map_tree::MapTrie,
+};
use serde::{Deserialize, Serialize};
+use serde_json::{Map, Value};
pub mod commands;
-#[derive(Deserialize, Serialize, Debug)]
-#[allow(clippy::module_name_repetitions)]
-pub struct RawKeyMap(HashMap<Key, KeyConfig>);
-
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, PartialOrd)]
-/// What values to use for: `riverctl <map_mode> <mode> <mods> <key> <command..>`
+/// What values to use for: `riverctl <command..>`
+#[serde(deny_unknown_fields)]
pub struct KeyConfig {
command: Vec<String>,
- #[serde(default = "default_mode")]
- modes: Vec<String>,
-
- #[serde(default = "MapMode::default")]
- map_mode: MapMode,
+ /// Whether to allow this key mapping in the “locked” mode.
+ #[serde(default)]
+ allow_locked: bool,
}
impl FromStr for KeyMap {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let raw: RawKeyMap =
- serde_json::from_str(s).context("Failed to parse the keymap config file as json.")?;
+ fn decode_value(
+ output: &mut MapTrie<KeyConfig>,
+ current_key: Vec<Key>,
+ value: &Value,
+ ) -> Result<()> {
+ let key_config = if let Some(value) = value.as_array() {
+ KeyConfig {
+ command: value
+ .iter()
+ .map(|v| v.as_str().map(ToOwned::to_owned))
+ .collect::<Option<_>>()
+ .ok_or(anyhow!("A array contained a non-string value: {value:#?}"))?,
+ allow_locked: false,
+ }
+ } else if let Some(object) = value.as_object() {
+ if object.contains_key("command") {
+ serde_json::from_value(value.to_owned())
+ .with_context(|| format!("Failed to parse key config: {value:#?}"))?
+ } else {
+ for (key, value) in object {
+ let mut local_current_key = current_key.clone();
+ local_current_key.push(
+ Key::from_str(key)
+ .with_context(|| format!("Failed to parse key '{key}'"))?,
+ );
+
+ decode_value(output, local_current_key, value)?;
+ }
+ return Ok(());
+ }
+ } else {
+ bail!("Value ({}) is invalid (not array or object).", value)
+ };
+
+ output
+ .insert(&current_key, key_config.clone())
+ .with_context(|| {
+ format!(
+ "Failed to insert mapping {} -> {key_config}",
+ Keys::from(current_key)
+ )
+ })?;
+
+ Ok(())
+ }
+
let mut out = MapTrie::<KeyConfig>::new();
- for (key, value) in raw.0 {
- out.insert(&[key], value.clone())
- .with_context(|| format!("Failed to insert mapping {key} -> {value}"))?;
+
+ let raw: Map<String, Value> =
+ serde_json::from_str(s).context("Failed to parse the keymap config file as json.")?;
+
+ for (key, value) in raw {
+ decode_value(
+ &mut out,
+ vec![Key::from_str(&key)
+ .with_context(|| format!("Failed to parse key ('{key}')"))?],
+ &value,
+ )?;
}
Ok(Self(out))
@@ -53,24 +104,6 @@ impl Display for KeyConfig {
}
}
-fn default_mode() -> Vec<String> {
- vec!["normal".to_owned()]
-}
-
-#[derive(Copy, Deserialize, Serialize, Debug, Clone, Default, PartialEq, PartialOrd)]
-enum MapMode {
- #[default]
- Map,
- MapMouse,
- Unmap,
-}
-
-impl Display for MapMode {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- <Self as std::fmt::Debug>::fmt(self, f)
- }
-}
-
#[derive(Debug)]
pub struct KeyMap(MapTrie<KeyConfig>);