From 55a208fdbbc6468b732192bf63ec69877ee20b34 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sun, 9 Aug 2026 14:39:54 +0800 Subject: feat(config): add cfg --pair flag and refactor config path Add `--pair` flag to output config values as `"key" = "value"` pairs, and provide escaped formatting for display. Move config path resolution to a helper function and expose the underlying hash map for rendering and completion. --- mingling_cli/src/config.rs | 24 ++++++-- mingling_cli/src/config/cmd_cfg.rs | 110 +++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 mingling_cli/src/config/cmd_cfg.rs (limited to 'mingling_cli/src') diff --git a/mingling_cli/src/config.rs b/mingling_cli/src/config.rs index 834b98c..32a0994 100644 --- a/mingling_cli/src/config.rs +++ b/mingling_cli/src/config.rs @@ -1,14 +1,20 @@ -use std::collections::HashMap; - use mingling::{LazyInit, Program, macros::program_setup}; +use std::collections::HashMap; use crate::ThisProgram; +pub mod cmd_cfg; + #[derive(Debug, Default, Clone)] pub struct ResMlingConfig { kvp: HashMap, } +/// Get the path to the config file. +fn config_path() -> Option { + dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("mling-cfg.json")) +} + impl ResMlingConfig { /// Edit a KVP entry. Empty string removes the key. pub fn edit(&mut self, key: &str, value: &str) { @@ -43,8 +49,10 @@ impl ResMlingConfig { /// Read the config from disk, defaulting to empty if not present. pub fn read() -> Self { - let path = std::path::Path::new(""); - Self::read_from_path(path) + match config_path() { + Some(path) => Self::read_from_path(&path), + None => Self::default(), + } } /// Read the config from a file at the given path. @@ -81,11 +89,15 @@ impl ResMlingConfig { /// Write the config to the default data directory path. pub fn write(&self) { - if let Some(data_dir) = dirs::data_dir() { - let path = data_dir.join("mingling").join("mling-cfg.json"); + if let Some(path) = config_path() { self.write_to_path(&path); } } + + /// Get the underlying hash map as a reference. + pub fn get_hash_map(&self) -> &HashMap { + &self.kvp + } } impl From> for ResMlingConfig { diff --git a/mingling_cli/src/config/cmd_cfg.rs b/mingling_cli/src/config/cmd_cfg.rs new file mode 100644 index 0000000..f0e4c2b --- /dev/null +++ b/mingling_cli/src/config/cmd_cfg.rs @@ -0,0 +1,110 @@ +use mingling::{ + LazyRes, Routable, ShellContext, Suggest, + macros::{arg, buffer, chain, command, completion, pack, r_println, renderer, suggest}, + picker::{EntryPicker, PickerArg, value::Flag}, +}; + +use crate::{Entry, Next, config::ResMlingConfig}; + +const FLAG_PAIR: PickerArg = arg![pair: Flag]; + +pack!(StateConfigEdit = (String, String)); +pack!(ResultConfigKeyValuePair = String); +pack!(ResultConfigValue = String); +pack!(ResultConfig = ()); + +#[command] +pub fn cfg(args: Entry) -> Next { + let (key, value, show_pair) = args + .pick(&arg![Option]) + .pick(&arg![Option]) + .pick(&FLAG_PAIR) + .unwrap(); + + match (key, value) { + (Some(k), Some(v)) => { + // Edit + StateConfigEdit::new((k, v)).into() + } + (Some(k), None) => { + // Display + if *show_pair { + ResultConfigKeyValuePair::new(k).to_render() + } else { + ResultConfigValue::new(k).to_render() + } + } + (None, None) => { + // List + ResultConfig::new(()).to_render() + } + _ => { + unreachable!("This path is unreachable given the positional parsing done by arg-picker") + } + } +} + +#[chain] +pub fn handle_state_config_edit(kv: StateConfigEdit, config: &mut LazyRes) { + let config = config.get_mut(); + config.edit(&kv.0, &kv.1); +} + +#[renderer(buffer)] +pub fn render_config_kvp(r: ResultConfigKeyValuePair, config: &mut LazyRes) { + let config = config.get_ref(); + let key = r.inner; + let value = config.get(&key); + r_println!( + "\"{}\" = \"{}\"", + escape_config_value(&key), + escape_config_value(value) + ) +} + +#[renderer(buffer)] +pub fn render_config_value(r: ResultConfigValue, config: &mut LazyRes) { + let config = config.get_ref(); + let key = r.inner; + let value = config.get(&key); + r_println!("{}", value) +} + +#[renderer(buffer)] +pub fn render_config(_: ResultConfig, config: &mut LazyRes) { + let config = config.get_ref(); + for (k, v) in config.get_hash_map().iter() { + r_println!( + "\"{}\" = \"{}\"", + escape_config_value(k), + escape_config_value(v) + ) + } +} + +/// Utility function: escapes `\t`, `\n`, `\b`, `\r` in the string to their +/// literal representations `\\t`, `\\n`, `\\b`, `\\r`, then trims leading and +/// trailing whitespace, and escapes `"` to `\"`. +pub fn escape_config_value(input: &str) -> String { + let mut result = String::with_capacity(input.len()); + for ch in input.trim().chars() { + match ch { + '\t' => result.push_str("\\t"), + '\n' => result.push_str("\\n"), + '\u{0008}' => result.push_str("\\b"), + '\r' => result.push_str("\\r"), + '"' => result.push_str("\\\""), + _ => result.push(ch), + } + } + result +} + +#[completion(EntryCfg)] +pub fn complete_config(_ctx: &ShellContext, config: &mut LazyRes) -> Suggest { + let config = config.get_ref(); + let keys = config.get_hash_map().keys().cloned().collect::>(); + Suggest::from(keys).combine(suggest! { + FLAG_PAIR + }) +} -- cgit