//! Parsing komp_ac's application keybindings into renderer-neutral data. use serde::de::Error as DeError; use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; use toml::{Value, map::Map}; use ux_config::keybindings::{KeybindingDocument, ParsedKeybindings}; /// Parsed contents of komp_ac's `[keybindings]` table. /// /// Bare actions are global and nested tables are named modes. Modes remain /// dynamic so either frontend can add one without changing this parser. #[derive(Debug, Default)] pub struct AppKeybindings { pub global: HashMap>, pub modes: HashMap>>, } impl AppKeybindings { pub fn mode_mut(&mut self, mode: impl Into) -> &mut HashMap> { self.modes.entry(mode.into()).or_default() } pub fn to_document(&self) -> ParsedKeybindings { let mut keymap = Map::new(); insert_section(&mut keymap, "global", &self.global); for (mode, bindings) in &self.modes { insert_section(&mut keymap, mode, bindings); } let mut root = Map::new(); root.insert("keymap".to_string(), Value::Table(keymap)); KeybindingDocument::from_root(&root) } } impl<'de> Deserialize<'de> for AppKeybindings { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let raw = HashMap::::deserialize(deserializer)?; let mut keybindings = Self::default(); for (key, value) in raw { match (key.as_str(), value) { (mode, value) if value.is_table() => { keybindings.modes.insert(mode.to_string(), mode_table(mode, value)?); } (action, value) => { keybindings .global .insert(action.to_string(), key_list(action, value)?); } } } Ok(keybindings) } } impl Serialize for AppKeybindings { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mode_count = self.modes.values().filter(|bindings| !bindings.is_empty()).count(); let mut map = serializer.serialize_map(Some(self.global.len() + mode_count))?; for (action, keys) in &self.global { map.serialize_entry(action, keys)?; } for (mode, bindings) in &self.modes { if !bindings.is_empty() { map.serialize_entry(mode, bindings)?; } } map.end() } } #[derive(Deserialize, Default)] struct ApplicationConfig { #[serde(default)] keybindings: AppKeybindings, } /// Parse komp_ac's complete configuration and return neutral keybindings. pub fn parse_config(source: &str) -> Result { let config = if source.trim().is_empty() { ApplicationConfig::default() } else { toml::from_str::(source)? }; Ok(config.keybindings.to_document()) } fn mode_table(mode: &str, value: Value) -> Result>, E> where E: DeError, { value .try_into() .map_err(|error| E::custom(format!("invalid keybindings.{mode} table: {error}"))) } fn key_list(action: &str, value: Value) -> Result, E> where E: DeError, { value .try_into() .map_err(|error| E::custom(format!("invalid keybindings.{action} binding list: {error}"))) } fn insert_section( keymap: &mut Map, mode: &str, bindings: &HashMap>, ) { if bindings.is_empty() { return; } let mut section = Map::new(); for (action, keys) in bindings { section.insert( action.clone(), Value::Array(keys.iter().cloned().map(Value::String).collect()), ); } keymap.insert(mode.to_string(), Value::Table(section)); } #[cfg(test)] mod tests { use super::*; #[test] fn translates_flattened_globals_and_named_modes() { let parsed = parse_config( "[keybindings]\nopen = [\"ctrl+o\"]\n\n[keybindings.common]\nsave = [\"ctrl+s\"]\n", ) .unwrap(); assert!(parsed.issues.is_empty()); assert_eq!(parsed.document.sections[0].name, "common"); assert_eq!(parsed.document.sections[1].name, "global"); assert_eq!(parsed.document.sections[1].bindings[0].name, "open"); } #[test] fn preserves_frontend_specific_modes() { let parsed = parse_config( "[keybindings.tauri]\nreload_window = [\"ctrl+r\"]\n", ) .unwrap(); assert!(parsed.issues.is_empty()); assert_eq!(parsed.document.sections[0].name, "tauri"); assert_eq!(parsed.document.sections[0].bindings[0].name, "reload_window"); } }