diff --git a/Cargo.toml b/Cargo.toml index 17b9b34..1e6accc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ publish = false axum = { version = "0.7", features = ["ws"] } chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } clap = { version = "4.5", features = ["derive", "env"] } +crossterm = { version = "0.28", features = ["event-stream"] } dirs = "5" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } jsonwebtoken = { version = "9", default-features = false } diff --git a/crates/maestro-config/src/lib.rs b/crates/maestro-config/src/lib.rs index 2f5893c..322eb15 100644 --- a/crates/maestro-config/src/lib.rs +++ b/crates/maestro-config/src/lib.rs @@ -27,6 +27,7 @@ //! URL derivation helper. Each binary applies its own env-var precedence and //! defaults on top of [`MaestroConfigFile`]. +use std::collections::BTreeMap; use std::net::SocketAddr; use std::path::{Path, PathBuf}; @@ -76,6 +77,8 @@ pub struct MaestroConfigFile { pub registry: RegistrySection, #[serde(default, skip_serializing_if = "WebSection::is_empty")] pub web: WebSection, + #[serde(default, skip_serializing_if = "TuiSection::is_empty")] + pub tui: TuiSection, } /// Fields consumed by more than one binary (CLI + daemon, daemon + registry). @@ -228,6 +231,38 @@ impl WebSection { } } +/// `[tui]` — `maestro tui` settings (NAP-275 epic, phase 2 NAP-277). +/// +/// Leader key, chord timeout, and per-mode keymap overrides. The TUI ships +/// with a default keymap; entries in `keymap_normal` / `keymap_insert` are +/// applied **over** the defaults, so a partial override is enough. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct TuiSection { + /// Canonical key form, e.g. `"ctrl-a"`. Default `ctrl-a`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub leader: Option, + /// Maximum interval between chord keys before a held prefix is dropped. + /// Default `1000`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chord_timeout_ms: Option, + /// Normal-mode bindings: canonical input → action string. Applied over + /// the shipped defaults. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub keymap_normal: BTreeMap, + /// Insert-mode bindings, same semantics as `keymap_normal`. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub keymap_insert: BTreeMap, +} + +impl TuiSection { + pub fn is_empty(&self) -> bool { + self.leader.is_none() + && self.chord_timeout_ms.is_none() + && self.keymap_normal.is_empty() + && self.keymap_insert.is_empty() + } +} + impl MaestroConfigFile { /// Strict load — error with [`ConfigError::MissingFile`] if absent. pub fn load(path: &Path) -> Result { diff --git a/crates/maestro-tui/Cargo.toml b/crates/maestro-tui/Cargo.toml index 9bf8e12..13e091c 100644 --- a/crates/maestro-tui/Cargo.toml +++ b/crates/maestro-tui/Cargo.toml @@ -9,4 +9,6 @@ description = "Terminal UI client over the local maestro daemon's `/api/*` surfa [dependencies] clap.workspace = true +crossterm.workspace = true +maestro-config.workspace = true thiserror.workspace = true