about summary refs log tree commit diff stats
path: root/pkgs/by-name/ri/river-mk-keymap/src/key_map
diff options
context:
space:
mode:
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.rs396
-rw-r--r--pkgs/by-name/ri/river-mk-keymap/src/key_map/mod.rs130
2 files changed, 393 insertions, 133 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 a4ac0ebd..52a6ba8a 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
@@ -1,109 +1,313 @@
-use std::process::Command;
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// 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 keymaps::key_repr::{KeyValue, MediaKeyCode, MouseKeyValue};
+use std::{env::current_exe, path::Path, process::Command};
 
-use super::{KeyMap, MapMode};
+use anyhow::{bail, Result};
+use keymaps::key_repr::{Key, KeyValue, Keys, MediaKeyCode, ModifierKeyCode, MouseKeyValue};
+use rustix::path::Arg;
+
+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![];
-
-                    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!(),
+    /// # 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 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),
+                )
+            }
+
+            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),
+                )
+            }
+
+            Ok(())
+        })?;
+
+        let mut output: Vec<_> = 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)
+                        });
+
+                let command = if value.once {
+                    vec![
+                        "spawn".to_owned(),
+                        format!(
+                            "riverctl {} && {}",
+                            shlex::try_join(value.command.iter().map(String::as_str))
+                                .expect("Should work"),
+                            shlex::try_join(["riverctl", "enter-mode", "normal"])
+                                .expect("Should work"),
+                        ),
+                    ]
+                } else {
+                    value.command
                 };
+                base.extend(key_to_command(
+                    mapping[0],
+                    &command,
+                    final_mode.as_ref().map_or("normal", |v| v.as_str()),
+                    value.allow_locked,
+                ));
 
-                value
-                    .modes
-                    .iter()
-                    .map(|mode| {
-                        let mut riverctl = Command::new("riverctl");
-                        riverctl.args([value.map_mode.as_command(), mode, &mods, &key_value]);
-
-                        riverctl.args(value.command.iter().map(String::as_str));
-                        riverctl
-                    })
-                    .collect::<Vec<_>>()
+                base
             })
-            .collect()
+            .collect();
+
+        output.sort_by_cached_key(|cmd| format!("{cmd:?}"));
+        output.dedup_by_key(|cmd| format!("{cmd:?}"));
+
+        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 84a16c9d..16dc02f4 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
@@ -1,37 +1,107 @@
-use std::{collections::HashMap, fmt::Display, ops::Deref, str::FromStr};
-
-use anyhow::Context;
-use keymaps::{key_repr::Key, map_tree::MapTrie};
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// 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::{fmt::Display, ops::Deref, str::FromStr};
+
+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>,
+    /// Whether to allow this key mapping in the “locked” mode.
+    #[serde(default)]
+    allow_locked: bool,
+
+    /// Whether to go back to the normal mode, after running this command.
+    #[serde(default)]
+    once: bool,
 
-    #[serde(default = "MapMode::default")]
-    map_mode: MapMode,
+    /// Use a different description to display this command, instead of the `command`.
+    description: Option<String>,
 }
 
 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,
+                    once: false,
+                    description: None,
+                }
+            } 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))
@@ -39,25 +109,11 @@ impl FromStr for KeyMap {
 }
 impl Display for KeyConfig {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.command.join(" ").as_str())
-    }
-}
-
-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)
+        if let Some(desc) = &self.description {
+            f.write_str(desc)
+        } else {
+            f.write_str(self.command.join(" ").as_str())
+        }
     }
 }