From 68cd6543b38379f7bd2084026187b70a54bb3119 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 09:15:21 +0100 Subject: [PATCH] fixed long nested vars in the ui --- Cargo.lock | 2 +- README.md | 16 +++++++- config.example.toml | 46 ++++++++++++++++++++++ src/cli.rs | 4 +- src/config.rs | 38 +++++++++++++----- src/error.rs | 2 +- src/format/hierarchical.rs | 42 +++++++++++++------- src/format/mod.rs | 2 +- src/main.rs | 36 ++++++++++------- src/runner.rs | 2 +- src/ui.rs | 81 ++++++++++++++++++++++++++------------ 11 files changed, 200 insertions(+), 71 deletions(-) create mode 100644 config.example.toml diff --git a/Cargo.lock b/Cargo.lock index 65986b8..75cf015 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "mould" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "clap", diff --git a/README.md b/README.md index f4ec789..62fa897 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ mould is a modern Terminal User Interface (TUI) tool designed for interactively - **Modern UI**: A polished, rounded interface featuring the Catppuccin Mocha palette. - **Highly Configurable**: Customize keybindings and themes via a simple TOML configuration. - **Dynamic Alignment**: Automatically aligns keys and values for perfect vertical readability. +- **Default Value Visibility**: Keep track of original template values while editing. +- **Incremental Merging**: Load existing values from an output file to continue where you left off. ## Installation @@ -72,7 +74,19 @@ quit = ":q" normal_mode = "Esc" [theme] -name = "catppuccin_mocha" +# Enable transparency to let your terminal background show through +transparent = false + +# Custom color palette (Catppuccin Mocha defaults) +crust = "#11111b" +surface0 = "#313244" +surface1 = "#45475a" +text = "#cdd6f4" +blue = "#89b4fa" +green = "#a6e3a1" +lavender = "#b4befe" +mauve = "#cba6f7" +peach = "#fab387" ``` ## License diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..e48616c --- /dev/null +++ b/config.example.toml @@ -0,0 +1,46 @@ +# mould Configuration Example +# Place this file at ~/.config/mould/config.toml (Linux/macOS) +# or %AppData%\mould\config.toml (Windows) + +[theme] +# If true, skip rendering the background block to let the terminal's transparency show. +transparent = false + +# Colors are specified in hex format ("#RRGGBB"). +# Default values follow the Catppuccin Mocha palette. + +# Main background color (used when transparent = false). +crust = "#11111b" + +# Status bar background and other secondary UI elements. +surface0 = "#313244" + +# Border and separator color. +surface1 = "#45475a" + +# Default text color. +text = "#cdd6f4" + +# Selection highlight and "NORMAL" mode tag color. +blue = "#89b4fa" + +# Insert mode highlight and "INSERT" mode tag color. +green = "#a6e3a1" + +# Keys/labels color. +lavender = "#b4befe" + +# Section titles color. +mauve = "#cba6f7" + +# Input field titles color. +peach = "#fab387" + +[keybinds] +# Keybindings for navigation and application control. +down = "j" +up = "k" +edit = "i" +save = ":w" +quit = ":q" +normal_mode = "Esc" diff --git a/src/cli.rs b/src/cli.rs index 1d4c73d..0e8751f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; /// mould: A TUI tool to generate and edit configuration files (.env, json, yaml, toml) #[derive(Parser, Debug)] #[command( - author, - version, + author, + version, about = "mould: A TUI tool to generate and edit configuration files (.env, json, yaml, toml)", long_about = "mould allows you to interactively edit and generate configuration files using templates. It supports various formats including .env, JSON, YAML, and TOML. It features a modern TUI with Vim-inspired keybindings and out-of-the-box support for theming.", after_help = "EXAMPLES:\n mould .env.example\n mould docker-compose.yml\n mould config.template.json -o config.json" diff --git a/src/config.rs b/src/config.rs index 8ea7df6..223785e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ +use ratatui::style::Color; use serde::{Deserialize, Serialize}; use std::fs; -use ratatui::style::Color; /// Configuration for the application's appearance. #[derive(Debug, Deserialize, Serialize, Clone)] @@ -42,15 +42,33 @@ impl ThemeConfig { } } - pub fn crust(&self) -> Color { Self::parse_hex(&self.crust) } - pub fn surface0(&self) -> Color { Self::parse_hex(&self.surface0) } - pub fn surface1(&self) -> Color { Self::parse_hex(&self.surface1) } - pub fn text(&self) -> Color { Self::parse_hex(&self.text) } - pub fn blue(&self) -> Color { Self::parse_hex(&self.blue) } - pub fn green(&self) -> Color { Self::parse_hex(&self.green) } - pub fn lavender(&self) -> Color { Self::parse_hex(&self.lavender) } - pub fn mauve(&self) -> Color { Self::parse_hex(&self.mauve) } - pub fn peach(&self) -> Color { Self::parse_hex(&self.peach) } + pub fn crust(&self) -> Color { + Self::parse_hex(&self.crust) + } + pub fn surface0(&self) -> Color { + Self::parse_hex(&self.surface0) + } + pub fn surface1(&self) -> Color { + Self::parse_hex(&self.surface1) + } + pub fn text(&self) -> Color { + Self::parse_hex(&self.text) + } + pub fn blue(&self) -> Color { + Self::parse_hex(&self.blue) + } + pub fn green(&self) -> Color { + Self::parse_hex(&self.green) + } + pub fn lavender(&self) -> Color { + Self::parse_hex(&self.lavender) + } + pub fn mauve(&self) -> Color { + Self::parse_hex(&self.mauve) + } + pub fn peach(&self) -> Color { + Self::parse_hex(&self.peach) + } } impl Default for ThemeConfig { diff --git a/src/error.rs b/src/error.rs index db04b67..1ee2036 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ -use thiserror::Error; use std::io; +use thiserror::Error; /// Custom error types for the mould application. #[derive(Error, Debug)] diff --git a/src/format/hierarchical.rs b/src/format/hierarchical.rs index ca7fdd1..d124e58 100644 --- a/src/format/hierarchical.rs +++ b/src/format/hierarchical.rs @@ -16,9 +16,12 @@ impl HierarchicalHandler { fn read_value(&self, path: &Path) -> io::Result { let content = fs::read_to_string(path)?; let value = match self.format_type { - FormatType::Json => serde_json::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, - FormatType::Yaml => serde_yaml::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, - FormatType::Toml => toml::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + FormatType::Json => serde_json::from_str(&content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + FormatType::Yaml => serde_yaml::from_str(&content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + FormatType::Toml => toml::from_str(&content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, _ => unreachable!(), }; Ok(value) @@ -26,15 +29,22 @@ impl HierarchicalHandler { fn write_value(&self, path: &Path, value: &Value) -> io::Result<()> { let content = match self.format_type { - FormatType::Json => serde_json::to_string_pretty(value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, - FormatType::Yaml => serde_yaml::to_string(value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + FormatType::Json => serde_json::to_string_pretty(value) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + FormatType::Yaml => serde_yaml::to_string(value) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, FormatType::Toml => { // toml requires the root to be a table if value.is_object() { - let toml_value: toml::Value = serde_json::from_value(value.clone()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - toml::to_string_pretty(&toml_value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + let toml_value: toml::Value = serde_json::from_value(value.clone()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + toml::to_string_pretty(&toml_value) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? } else { - return Err(io::Error::new(io::ErrorKind::InvalidData, "Root of TOML must be an object")); + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Root of TOML must be an object", + )); } } _ => unreachable!(), @@ -143,7 +153,7 @@ fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str) { *current = Value::Object(Map::new()); } let map = current.as_object_mut().unwrap(); - + let next_node = map.entry(key.to_string()).or_insert_with(|| { if idx.is_some() { Value::Array(Vec::new()) @@ -171,7 +181,7 @@ fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str) { *current = Value::Object(Map::new()); } let map = current.as_object_mut().unwrap(); - + // Attempt basic type inference let final_val = if let Ok(n) = new_val_str.parse::() { Value::Number(n.into()) @@ -182,7 +192,9 @@ fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str) { }; if let Some(i) = final_idx { - let next_node = map.entry(final_key.to_string()).or_insert_with(|| Value::Array(Vec::new())); + let next_node = map + .entry(final_key.to_string()) + .or_insert_with(|| Value::Array(Vec::new())); if !next_node.is_array() { *next_node = Value::Array(Vec::new()); } @@ -200,7 +212,7 @@ fn parse_array_key(part: &str) -> (&str, Option) { if part.ends_with(']') && part.contains('[') { let start_idx = part.find('[').unwrap(); let key = &part[..start_idx]; - let idx = part[start_idx+1..part.len()-1].parse::().ok(); + let idx = part[start_idx + 1..part.len() - 1].parse::().ok(); (key, idx) } else { (part, None) @@ -224,15 +236,15 @@ mod tests { } } }); - + flatten(&json, "", &mut vars); assert_eq!(vars.len(), 2); - + let mut root = Value::Object(Map::new()); for var in vars { insert_into_value(&mut root, &var.key, &var.value); } - + // When unflattening, it parses bool back let unflattened_json = serde_json::to_string(&root).unwrap(); assert!(unflattened_json.contains("\"8080:80\"")); diff --git a/src/format/mod.rs b/src/format/mod.rs index 9950c4d..d8814ab 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -1,5 +1,5 @@ -use std::path::Path; use std::io; +use std::path::Path; pub mod env; pub mod hierarchical; diff --git a/src/main.rs b/src/main.rs index 27ecd2c..222991a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,32 +18,32 @@ use std::path::{Path, PathBuf}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; -use ratatui::{backend::CrosstermBackend, Terminal}; +use ratatui::{Terminal, backend::CrosstermBackend}; /// Helper to automatically determine the output file path based on common naming conventions. fn determine_output_path(input: &Path) -> PathBuf { let file_name = input.file_name().unwrap_or_default().to_string_lossy(); - + if file_name == ".env.example" { return input.with_file_name(".env"); } - - if file_name == "docker-compose.yml" { - return input.with_file_name("docker-compose.override.yml"); + + if file_name == "docker-compose.yml" || file_name == "compose.yml" { + return input.with_file_name("compose.override.yml"); } - if file_name == "docker-compose.yaml" { - return input.with_file_name("docker-compose.override.yaml"); + if file_name == "docker-compose.yaml" || file_name == "compose.yaml" { + return input.with_file_name("compose.override.yaml"); } - + if file_name.ends_with(".example.json") { return input.with_file_name(file_name.replace(".example.json", ".json")); } if file_name.ends_with(".template.json") { return input.with_file_name(file_name.replace(".template.json", ".json")); } - + input.with_extension(format!( "{}.out", input.extension().unwrap_or_default().to_string_lossy() @@ -98,7 +98,8 @@ fn main() -> anyhow::Result<()> { let mut app = App::new(vars); // Terminal lifecycle - enable_raw_mode().map_err(|e| MouldError::Terminal(format!("Failed to enable raw mode: {}", e)))?; + enable_raw_mode() + .map_err(|e| MouldError::Terminal(format!("Failed to enable raw mode: {}", e)))?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture) .map_err(|e| MouldError::Terminal(format!("Failed to enter alternate screen: {}", e)))?; @@ -106,7 +107,13 @@ fn main() -> anyhow::Result<()> { let mut terminal = Terminal::new(backend) .map_err(|e| MouldError::Terminal(format!("Failed to create terminal backend: {}", e)))?; - let mut runner = AppRunner::new(&mut terminal, &mut app, &config, &output_path, handler.as_ref()); + let mut runner = AppRunner::new( + &mut terminal, + &mut app, + &config, + &output_path, + handler.as_ref(), + ); let res = runner.run(); // Restoration @@ -115,14 +122,15 @@ fn main() -> anyhow::Result<()> { terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture - ).ok(); + ) + .ok(); terminal.show_cursor().ok(); match res { Ok(_) => { info!("Successfully finished mould session."); Ok(()) - }, + } Err(e) => { error!("Application error during run: {}", e); Err(e.into()) diff --git a/src/runner.rs b/src/runner.rs index c31e3a4..7084de8 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -2,8 +2,8 @@ use crate::app::{App, Mode}; use crate::config::Config; use crate::format::FormatHandler; use crossterm::event::{self, Event, KeyCode, KeyEvent}; -use ratatui::backend::Backend; use ratatui::Terminal; +use ratatui::backend::Backend; use std::io; use std::path::Path; use tui_input::backend::crossterm::EventHandler; diff --git a/src/ui.rs b/src/ui.rs index 8beb046..4b59599 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,11 +1,11 @@ use crate::app::{App, Mode}; use crate::config::Config; use ratatui::{ + Frame, layout::{Constraint, Direction, Layout}, style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, - Frame, }; /// Renders the main application interface using ratatui. @@ -43,15 +43,6 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { ]) .split(outer_layout[1]); - // Calculate alignment for the key-value separator based on the longest key. - let max_key_len = app - .vars - .iter() - .map(|v| v.key.len()) - .max() - .unwrap_or(20) - .min(40); - // Build the interactive list of configuration variables. let items: Vec = app .vars @@ -81,14 +72,25 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { Style::default().fg(theme.text()) }; - let line = Line::from(vec![ - Span::styled( - format!(" {: " navigation | i: edit | :w: save | :q: quit ", - Mode::Insert => " Esc: back to normal | Enter: commit ", - }); + let status_msg = app + .status_message + .as_deref() + .unwrap_or_else(|| match app.mode { + Mode::Normal => " navigation | i: edit | :w: save | :q: quit ", + Mode::Insert => " Esc: back to normal | Enter: commit ", + }); let status_line = Line::from(vec![ Span::styled(mode_str, mode_style),