// nixos-config - My current NixOS configuration // // Copyright (C) 2025 Benedikt Peetz // 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 . use std::{env::current_exe, path::Path, process::Command}; use anyhow::{bail, Result}; use keymaps::key_repr::{Key, KeyValue, Keys, MediaKeyCode, ModifierKeyCode, MouseKeyValue}; use rustix::path::Arg; use super::KeyMap; impl KeyMap { /// # 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> { 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() && [ "".parse().expect("hardcoded"), "".parse().expect("hardcoded"), ] .contains(&last[0]) { bail!( "You cannot use or 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 output = self .0 .into_iter() .flat_map(|(keys, value)| { let (prefix, mapping) = keys.split_at(keys.len() - 1); let (final_mode, mut base): (Option, _) = 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( "".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( "".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(); Ok(output) } } 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 { 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 }