diff --git a/Cargo.lock b/Cargo.lock index e816ec7d..63393c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4256,8 +4256,10 @@ dependencies = [ "prost-types", "sanitise-file-name", "serde", + "toml", "tonic", "uuid", + "ux-config", ] [[package]] diff --git a/client b/client index a3c5853f..7eaf34d0 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit a3c5853f60603730cbc2d762be4bdd4222936816 +Subproject commit 7eaf34d09e49bb0f30225368e20c63fb18c049bf diff --git a/client-gui2 b/client-gui2 index 4a9dc302..cc532d80 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit 4a9dc30249a333df1fdff02b3ad7e9c70886734e +Subproject commit cc532d80270212a81d6d5075df0daaef48fa1d08 diff --git a/komp-app/Cargo.toml b/komp-app/Cargo.toml index 97b5d019..b2fa0ef3 100644 --- a/komp-app/Cargo.toml +++ b/komp-app/Cargo.toml @@ -12,5 +12,7 @@ jiff = { version = "0.2.15", default-features = false, features = ["std", "tzdb- prost-types.workspace = true sanitise-file-name = "1" serde.workspace = true +toml.workspace = true tonic.workspace = true +ux-config = { path = "../tui-pages/ux-config" } uuid = { version = "1.23.3", features = ["v4"] } diff --git a/komp-app/src/keybindings.rs b/komp-app/src/keybindings.rs new file mode 100644 index 00000000..948b014d --- /dev/null +++ b/komp-app/src/keybindings.rs @@ -0,0 +1,164 @@ +//! 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"); + } +} diff --git a/komp-app/src/lib.rs b/komp-app/src/lib.rs index 7f0558e6..50dc168c 100644 --- a/komp-app/src/lib.rs +++ b/komp-app/src/lib.rs @@ -8,6 +8,7 @@ pub mod auth; pub mod csv; pub mod grpc; pub mod import_export; +pub mod keybindings; pub mod navigation; mod search; pub mod session; diff --git a/komp_ac/commands.md b/komp_ac/commands.md deleted file mode 100644 index 85266911..00000000 --- a/komp_ac/commands.md +++ /dev/null @@ -1,71 +0,0 @@ -# Komp účtovníctvo - -Príkazy na vytvorenie základných účtovných evidencií pre Komp. - -## Adresár - -Vytvorenie firmy alebo kontaktu v adresári: - -```text -adresar vytvor \ - --firma \ - --ulica \ - --psc \ - --mesto \ - --stat \ - --banka \ - --ucet \ - --ico \ - --kontakt \ - --telefon -``` - -Polia: - -| Pole | Význam | -| --- | --- | -| `firma` | Názov firmy alebo subjektu | -| `ulica` | Ulica a číslo | -| `psc` | PSČ | -| `mesto` | Mesto | -| `stat` | Štát | -| `banka` | Názov banky | -| `ucet` | Číslo bankového účtu | -| `ico` | IČO | -| `kontakt` | Kontaktná osoba alebo e-mail | -| `telefon` | Telefónne číslo | - -## Skladová karta - -Vytvorenie skladovej karty: - -```text -skladova-karta vytvor \ - --datum \ - --cislo-skladu \ - --cislo-karty \ - --tovar \ - --dph \ - --sarza \ - --balenie \ - --hodnota \ - --dodavatel -``` - -Polia: - -| Pole | Význam | -| --- | --- | -| `datum` | Dátum vytvorenia alebo zaradenia karty | -| `cislo-skladu` | Číslo skladu | -| `cislo-karty` | Číslo skladovej karty | -| `tovar` | Názov alebo identifikácia tovaru | -| `dph` | Sadzba DPH v percentách | -| `sarza` | Číslo alebo označenie šarže | -| `balenie` | Typ alebo množstvo balenia | -| `hodnota` | Hodnota tovaru | -| `dodavatel` | Dodávateľ vybraný záznamom v `adresar` | - -Hodnota `dodavatel` musí odkazovať na existujúci záznam v adresári, napríklad -pomocou IČO alebo interného ID adresára. Dodávateľa nie je potrebné zadávať -ako voľný text. diff --git a/tui-pages b/tui-pages index ef797193..361dbdf0 160000 --- a/tui-pages +++ b/tui-pages @@ -1 +1 @@ -Subproject commit ef797193c1dc9e89b932356718953c743d282000 +Subproject commit 361dbdf071805c97da69bba1dbc0514a4fc8ca62