From e72fdd9fcb5fd917c5d51f935b4ef0758f3d764c Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Mar 2026 17:55:10 +0100 Subject: [PATCH 1/5] added documentation --- src/app.rs | 22 +++++++++++---- src/cli.rs | 6 +++-- src/config.rs | 17 ++++++++++++ src/main.rs | 18 +++++++++++++ src/runner.rs | 20 +++++++++++++- src/ui.rs | 75 ++++++++++++++++++++++++++++++--------------------- 6 files changed, 119 insertions(+), 39 deletions(-) diff --git a/src/app.rs b/src/app.rs index 11a7bc7..bac4047 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,21 +1,32 @@ use crate::format::EnvVar; use tui_input::Input; +/// Represents the current operating mode of the application. pub enum Mode { + /// Standard navigation and command mode. Normal, + /// Active text entry mode for modifying values. Insert, } +/// The core application state, holding all configuration variables and UI status. pub struct App { + /// The list of configuration variables being edited. pub vars: Vec, + /// Index of the currently selected variable in the list. pub selected: usize, + /// The current interaction mode (Normal or Insert). pub mode: Mode, + /// Whether the main application loop should continue running. pub running: bool, + /// An optional message to display in the status bar (e.g., "Saved to .env"). pub status_message: Option, + /// The active text input buffer for the selected variable. pub input: Input, } impl App { + /// Initializes a new application instance with the provided variables. pub fn new(vars: Vec) -> Self { let initial_input = vars.get(0).map(|v| v.value.clone()).unwrap_or_default(); Self { @@ -28,6 +39,7 @@ impl App { } } + /// Moves the selection to the next variable in the list, wrapping around if necessary. pub fn next(&mut self) { if !self.vars.is_empty() { self.selected = (self.selected + 1) % self.vars.len(); @@ -35,6 +47,7 @@ impl App { } } + /// Moves the selection to the previous variable in the list, wrapping around if necessary. pub fn previous(&mut self) { if !self.vars.is_empty() { if self.selected == 0 { @@ -46,30 +59,29 @@ impl App { } } + /// Updates the input buffer to reflect the value of the currently selected variable. pub fn sync_input_with_selected(&mut self) { if let Some(var) = self.vars.get(self.selected) { self.input = Input::new(var.value.clone()); } } + /// Commits the current text in the input buffer back to the selected variable's value. pub fn commit_input(&mut self) { if let Some(var) = self.vars.get_mut(self.selected) { var.value = self.input.value().to_string(); } } + /// Transitions the application into Insert Mode. pub fn enter_insert(&mut self) { self.mode = Mode::Insert; self.status_message = None; } + /// Commits the current input and transitions the application into Normal Mode. pub fn enter_normal(&mut self) { self.commit_input(); self.mode = Mode::Normal; } - - #[allow(dead_code)] - pub fn quit(&mut self) { - self.running = false; - } } diff --git a/src/cli.rs b/src/cli.rs index 447e9d5..1a19274 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,14 @@ use clap::Parser; use std::path::PathBuf; +/// mould: A TUI tool to generate and edit configuration files (.env, json, yaml, toml) #[derive(Parser, Debug)] -#[command(author, version, about = "mould: A TUI tool to generate and edit configuration files (.env, json, yaml, toml)")] +#[command(author, version, about)] pub struct Cli { /// The input template file (e.g., .env.example, config.json.template, docker-compose.yml) pub input: PathBuf, - /// Optional output file. If not provided, it will be inferred (e.g., .env.example -> .env, docker-compose.yml -> docker-compose.override.yml) + /// Optional output file. If not provided, it will be inferred. #[arg(short, long)] pub output: Option, @@ -16,6 +17,7 @@ pub struct Cli { pub format: Option, } +/// Parses and returns the command-line arguments. pub fn parse() -> Cli { Cli::parse() } diff --git a/src/config.rs b/src/config.rs index 8029cd2..8ea7df6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,22 +2,34 @@ use serde::{Deserialize, Serialize}; use std::fs; use ratatui::style::Color; +/// Configuration for the application's appearance. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ThemeConfig { + /// If true, skip rendering the background block to let the terminal's transparency show. #[serde(default)] pub transparent: bool, + /// Color for standard background areas (when not transparent). pub crust: String, + /// Dark surface color for UI elements like the status bar. pub surface0: String, + /// Light surface color for borders and dividers. pub surface1: String, + /// Default text color. pub text: String, + /// Color for selection highlighting and normal mode tag. pub blue: String, + /// Color for insert mode highlighting and success status. pub green: String, + /// Accent color for configuration keys. pub lavender: String, + /// Accent color for primary section titles. pub mauve: String, + /// Accent color for input field titles. pub peach: String, } impl ThemeConfig { + /// Internal helper to parse a hex color string ("#RRGGBB") into a TUI Color. fn parse_hex(hex: &str) -> Color { let hex = hex.trim_start_matches('#'); if hex.len() == 6 { @@ -42,6 +54,7 @@ impl ThemeConfig { } impl Default for ThemeConfig { + /// Default theme: Catppuccin Mocha. fn default() -> Self { Self { transparent: false, @@ -58,6 +71,7 @@ impl Default for ThemeConfig { } } +/// Custom keybindings for navigation and application control. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct KeybindsConfig { pub down: String, @@ -81,6 +95,7 @@ impl Default for KeybindsConfig { } } +/// Root configuration structure for mould. #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct Config { #[serde(default)] @@ -89,6 +104,8 @@ pub struct Config { pub keybinds: KeybindsConfig, } +/// Loads the configuration from the user's home config directory (~/.config/mould/config.toml). +/// Falls back to default settings if no configuration is found. pub fn load_config() -> Config { if let Some(mut config_dir) = dirs::config_dir() { config_dir.push("mould"); diff --git a/src/main.rs b/src/main.rs index ed076ab..d5e067c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,23 +20,32 @@ use crossterm::{ }; use ratatui::{backend::CrosstermBackend, Terminal}; +/// 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(); + + // .env.example -> .env if file_name == ".env.example" { return input.with_file_name(".env"); } + + // docker-compose.yml -> docker-compose.override.yml if file_name == "docker-compose.yml" { return input.with_file_name("docker-compose.override.yml"); } if file_name == "docker-compose.yaml" { return input.with_file_name("docker-compose.override.yaml"); } + + // config.example.json -> config.json 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")); } + + // Fallback: append .out to the extension input.with_extension(format!( "{}.out", input.extension().unwrap_or_default().to_string_lossy() @@ -44,6 +53,7 @@ fn determine_output_path(input: &Path) -> PathBuf { } fn main() -> Result<(), Box> { + // Parse CLI arguments let args = cli::parse(); let input_path = args.input; @@ -52,13 +62,16 @@ fn main() -> Result<(), Box> { return Ok(()); } + // Detect format and select appropriate handler let format_type = detect_format(&input_path, args.format); let handler = get_handler(format_type); + // Determine where to save the result let output_path = args .output .unwrap_or_else(|| determine_output_path(&input_path)); + // Initial parsing of the template file let mut vars = handler.parse(&input_path).unwrap_or_else(|err| { println!("Error parsing input file: {}", err); vec![] @@ -72,22 +85,27 @@ fn main() -> Result<(), Box> { return Ok(()); } + // Merge values from an existing output file if it exists if let Err(e) = handler.merge(&output_path, &mut vars) { println!("Warning: Could not merge existing output file: {}", e); } + // Load user configuration and initialize application state let config = load_config(); let mut app = App::new(vars); + // Initialize terminal into raw mode and enter alternate screen enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; + // Instantiate the runner and start the application loop let mut runner = AppRunner::new(&mut terminal, &mut app, &config, &output_path, handler.as_ref()); let res = runner.run(); + // Clean up terminal state on exit disable_raw_mode()?; execute!( terminal.backend_mut(), diff --git a/src/runner.rs b/src/runner.rs index 5598d63..c31e3a4 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -8,12 +8,19 @@ use std::io; use std::path::Path; use tui_input::backend::crossterm::EventHandler; +/// Manages the main application execution loop, event handling, and terminal interaction. pub struct AppRunner<'a, B: Backend> { + /// Reference to the terminal instance. terminal: &'a mut Terminal, + /// Mutable reference to the application state. app: &'a mut App, + /// Loaded user configuration. config: &'a Config, + /// Path where the final configuration will be saved. output_path: &'a Path, + /// Handler for the specific file format (env, json, yaml, toml). handler: &'a dyn FormatHandler, + /// Buffer for storing active command entry (e.g., ":w"). command_buffer: String, } @@ -21,6 +28,7 @@ impl<'a, B: Backend> AppRunner<'a, B> where io::Error: From, { + /// Creates a new runner instance. pub fn new( terminal: &'a mut Terminal, app: &'a mut App, @@ -38,9 +46,11 @@ where } } + /// Starts the main application loop. pub fn run(&mut self) -> io::Result<()> { while self.app.running { - self.terminal.draw(|f| crate::ui::draw(f, self.app, self.config))?; + self.terminal + .draw(|f| crate::ui::draw(f, self.app, self.config))?; if let Event::Key(key) = event::read()? { self.handle_key_event(key)?; @@ -49,6 +59,7 @@ where Ok(()) } + /// Primary dispatcher for all keyboard events. fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { match self.app.mode { Mode::Normal => self.handle_normal_mode(key), @@ -56,6 +67,7 @@ where } } + /// Handles keys in Normal mode, separating navigation from command entry. fn handle_normal_mode(&mut self, key: KeyEvent) -> io::Result<()> { if !self.command_buffer.is_empty() { self.handle_command_mode(key) @@ -64,6 +76,7 @@ where } } + /// Logic for entering and executing ":" style commands. fn handle_command_mode(&mut self, key: KeyEvent) -> io::Result<()> { match key.code { KeyCode::Enter => { @@ -90,6 +103,7 @@ where } } + /// Handles primary navigation (j/k) and transitions to insert or command modes. fn handle_navigation_mode(&mut self, key: KeyEvent) -> io::Result<()> { if let KeyCode::Char(c) = key.code { let c_str = c.to_string(); @@ -116,6 +130,7 @@ where Ok(()) } + /// Delegates key events to the `tui_input` handler during active editing. fn handle_insert_mode(&mut self, key: KeyEvent) -> io::Result<()> { match key.code { KeyCode::Esc | KeyCode::Enter => { @@ -128,6 +143,7 @@ where Ok(()) } + /// Logic to map command strings (like ":w") to internal application actions. fn execute_command(&mut self, cmd: &str) -> io::Result<()> { if cmd == self.config.keybinds.save { self.save_file() @@ -144,6 +160,7 @@ where } } + /// Attempts to write the current app state to the specified output file. fn save_file(&mut self) -> io::Result<()> { if self.handler.write(self.output_path, &self.app.vars).is_ok() { self.app.status_message = Some(format!("Saved to {}", self.output_path.display())); @@ -153,6 +170,7 @@ where Ok(()) } + /// Synchronizes the status bar display with the active command buffer. fn sync_command_status(&mut self) { if self.command_buffer.is_empty() { self.app.status_message = None; diff --git a/src/ui.rs b/src/ui.rs index b35743e..8beb046 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -8,36 +8,42 @@ use ratatui::{ Frame, }; +/// Renders the main application interface using ratatui. pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { let theme = &config.theme; let size = f.area(); - // Background + // Render the main background (optional based on transparency config). if !theme.transparent { - f.render_widget(Block::default().style(Style::default().bg(theme.crust())), size); + f.render_widget( + Block::default().style(Style::default().bg(theme.crust())), + size, + ); } - // Main layout with horizontal padding + // Horizontal layout with 1-character side margins. let outer_layout = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Length(1), // Left padding - Constraint::Min(0), // Content - Constraint::Length(1), // Right padding + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1), ]) .split(size); + // Vertical layout for the main UI components. let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(1), // Top padding - Constraint::Min(3), // List - Constraint::Length(3), // Input area - Constraint::Length(1), // Bottom padding + Constraint::Length(1), // Top margin + Constraint::Min(3), // Main list + Constraint::Length(3), // Focused input field + Constraint::Length(1), // Spacer Constraint::Length(1), // Status bar ]) .split(outer_layout[1]); + // Calculate alignment for the key-value separator based on the longest key. let max_key_len = app .vars .iter() @@ -46,14 +52,15 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { .unwrap_or(20) .min(40); - // List + // Build the interactive list of configuration variables. let items: Vec = app .vars .iter() .enumerate() .map(|(i, var)| { let is_selected = i == app.selected; - + + // Show live input text for the selected item if in Insert mode. let val = if is_selected && matches!(app.mode, Mode::Insert) { app.input.value() } else { @@ -61,7 +68,9 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { }; let key_style = if is_selected { - Style::default().fg(theme.crust()).add_modifier(Modifier::BOLD) + Style::default() + .fg(theme.crust()) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(theme.lavender()) }; @@ -73,7 +82,10 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { }; let line = Line::from(vec![ - Span::styled(format!(" {: ( " NORMAL ", @@ -159,16 +171,17 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { ), }; - 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_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), - Span::styled(format!(" {} ", status_msg), Style::default().bg(theme.surface0()).fg(theme.text())), + Span::styled( + format!(" {} ", status_msg), + Style::default().bg(theme.surface0()).fg(theme.text()), + ), ]); let status = Paragraph::new(status_line).style(Style::default().bg(theme.surface0())); -- 2.49.1 From f413d5e2eb5e485a8dd16450ba9f36d7b1637327 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Mar 2026 17:57:44 +0100 Subject: [PATCH 2/5] updated clap output --- Cargo.lock | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 +++ src/cli.rs | 17 ++++++++-- src/error.rs | 18 +++++++++++ src/main.rs | 83 +++++++++++++++++++++++++++---------------------- 5 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 0b61e3c..65986b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstream" version = "1.0.0" @@ -24,7 +39,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -38,6 +53,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-parse" version = "1.0.0" @@ -179,7 +203,7 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -405,6 +429,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -632,6 +679,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -784,14 +855,18 @@ dependencies = [ name = "mould" version = "0.2.0" dependencies = [ + "anyhow", "clap", "crossterm", "dirs", + "env_logger", + "log", "ratatui", "serde", "serde_json", "serde_yaml", "tempfile", + "thiserror 2.0.18", "toml", "tui-input", ] @@ -1005,6 +1080,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index eb75373..193dda4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,17 @@ name = "mould" path = "src/main.rs" [dependencies] +anyhow = "1.0.102" clap = { version = "4.6.0", features = ["derive"] } crossterm = "0.29.0" dirs = "6.0.0" +env_logger = "0.11.9" +log = "0.4.29" ratatui = "0.30.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" serde_yaml = "0.9.34" +thiserror = "2.0.18" toml = "1.0.6" tui-input = "0.15.0" diff --git a/src/cli.rs b/src/cli.rs index 1a19274..1d4c73d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,18 +3,29 @@ use std::path::PathBuf; /// mould: A TUI tool to generate and edit configuration files (.env, json, yaml, toml) #[derive(Parser, Debug)] -#[command(author, version, about)] +#[command( + 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" +)] pub struct Cli { /// The input template file (e.g., .env.example, config.json.template, docker-compose.yml) + #[arg(required = true, value_name = "INPUT_FILE")] pub input: PathBuf, /// Optional output file. If not provided, it will be inferred. - #[arg(short, long)] + #[arg(short, long, value_name = "OUTPUT_FILE")] pub output: Option, /// Override the format detection (env, json, yaml, toml) - #[arg(short, long)] + #[arg(short, long, value_name = "FORMAT", value_parser = ["env", "json", "yaml", "toml"])] pub format: Option, + + /// Increase verbosity for logging (can be used multiple times) + #[arg(short, long, action = clap::ArgAction::Count)] + pub verbose: u8, } /// Parses and returns the command-line arguments. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..db04b67 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,18 @@ +use thiserror::Error; +use std::io; + +/// Custom error types for the mould application. +#[derive(Error, Debug)] +pub enum MouldError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("Format error: {0}")] + Format(String), + + #[error("File not found: {0}")] + FileNotFound(String), + + #[error("Terminal error: {0}")] + Terminal(String), +} diff --git a/src/main.rs b/src/main.rs index d5e067c..27ecd2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,17 @@ mod app; mod cli; mod config; +mod error; mod format; mod runner; mod ui; use app::App; use config::load_config; +use error::MouldError; use format::{detect_format, get_handler}; +use log::{error, info, warn}; use runner::AppRunner; -use std::error::Error; use std::io; use std::path::{Path, PathBuf}; @@ -24,12 +26,10 @@ use ratatui::{backend::CrosstermBackend, Terminal}; fn determine_output_path(input: &Path) -> PathBuf { let file_name = input.file_name().unwrap_or_default().to_string_lossy(); - // .env.example -> .env if file_name == ".env.example" { return input.with_file_name(".env"); } - // docker-compose.yml -> docker-compose.override.yml if file_name == "docker-compose.yml" { return input.with_file_name("docker-compose.override.yml"); } @@ -37,7 +37,6 @@ fn determine_output_path(input: &Path) -> PathBuf { return input.with_file_name("docker-compose.override.yaml"); } - // config.example.json -> config.json if file_name.ends_with(".example.json") { return input.with_file_name(file_name.replace(".example.json", ".json")); } @@ -45,78 +44,88 @@ fn determine_output_path(input: &Path) -> PathBuf { return input.with_file_name(file_name.replace(".template.json", ".json")); } - // Fallback: append .out to the extension input.with_extension(format!( "{}.out", input.extension().unwrap_or_default().to_string_lossy() )) } -fn main() -> Result<(), Box> { - // Parse CLI arguments +fn main() -> anyhow::Result<()> { let args = cli::parse(); + // Initialize logger with verbosity from CLI + let log_level = match args.verbose { + 0 => log::LevelFilter::Warn, + 1 => log::LevelFilter::Info, + _ => log::LevelFilter::Debug, + }; + env_logger::Builder::new() + .filter_level(log_level) + .format_timestamp(None) + .init(); + let input_path = args.input; if !input_path.exists() { - println!("Input file does not exist: {}", input_path.display()); - return Ok(()); + error!("Input file not found: {}", input_path.display()); + return Err(MouldError::FileNotFound(input_path.display().to_string()).into()); } - // Detect format and select appropriate handler + info!("Input: {}", input_path.display()); + let format_type = detect_format(&input_path, args.format); let handler = get_handler(format_type); - // Determine where to save the result let output_path = args .output .unwrap_or_else(|| determine_output_path(&input_path)); - // Initial parsing of the template file - let mut vars = handler.parse(&input_path).unwrap_or_else(|err| { - println!("Error parsing input file: {}", err); - vec![] - }); + info!("Output: {}", output_path.display()); + + let mut vars = handler.parse(&input_path).map_err(|e| { + error!("Failed to parse input file: {}", e); + MouldError::Format(format!("Failed to parse {}: {}", input_path.display(), e)) + })?; if vars.is_empty() { - println!( - "No variables found in {} or file could not be parsed.", - input_path.display() - ); - return Ok(()); + warn!("No variables found in {}", input_path.display()); } - // Merge values from an existing output file if it exists if let Err(e) = handler.merge(&output_path, &mut vars) { - println!("Warning: Could not merge existing output file: {}", e); + warn!("Could not merge existing output file: {}", e); } - // Load user configuration and initialize application state let config = load_config(); let mut app = App::new(vars); - // Initialize terminal into raw mode and enter alternate screen - enable_raw_mode()?; + // Terminal lifecycle + enable_raw_mode().map_err(|e| MouldError::Terminal(format!("Failed to enable raw mode: {}", e)))?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture) + .map_err(|e| MouldError::Terminal(format!("Failed to enter alternate screen: {}", e)))?; let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + let mut terminal = Terminal::new(backend) + .map_err(|e| MouldError::Terminal(format!("Failed to create terminal backend: {}", e)))?; - // Instantiate the runner and start the application loop let mut runner = AppRunner::new(&mut terminal, &mut app, &config, &output_path, handler.as_ref()); let res = runner.run(); - // Clean up terminal state on exit - disable_raw_mode()?; + // Restoration + disable_raw_mode().ok(); execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture - )?; - terminal.show_cursor()?; + ).ok(); + terminal.show_cursor().ok(); - if let Err(err) = res { - println!("{:?}", err); + match res { + Ok(_) => { + info!("Successfully finished mould session."); + Ok(()) + }, + Err(e) => { + error!("Application error during run: {}", e); + Err(e.into()) + } } - - Ok(()) } -- 2.49.1 From 7a7ae1ffdbbad20f817904ba8b99950aaf29dad8 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Mar 2026 17:59:10 +0100 Subject: [PATCH 3/5] removed old comment --- src/format/hierarchical.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/format/hierarchical.rs b/src/format/hierarchical.rs index 5105d2a..ca7fdd1 100644 --- a/src/format/hierarchical.rs +++ b/src/format/hierarchical.rs @@ -121,14 +121,6 @@ impl FormatHandler for HierarchicalHandler { } fn write(&self, path: &Path, vars: &[EnvVar]) -> io::Result<()> { - // For writing hierarchical formats, we ideally want to preserve the original structure. - // But we don't have it here. We should parse the template again to get the structure! - // Oh wait, `write` is called with only `vars`. - // If we want to construct the tree from scratch, it's very difficult to guess array vs object - // and data types without the original template. - // Let's change the trait or just keep a copy of the template? - // Actually, if we require the user to have the template, we can just parse the template, update the leaves, and write. - // We'll write a reconstruction algorithm that just creates objects based on keys. let mut root = Value::Object(Map::new()); for var in vars { insert_into_value(&mut root, &var.key, &var.value); @@ -246,4 +238,4 @@ mod tests { assert!(unflattened_json.contains("\"8080:80\"")); assert!(unflattened_json.contains("true")); } -} \ No newline at end of file +} -- 2.49.1 From 53902af934977150453d6dc32bc0246687952caa Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Mar 2026 17:59:52 +0100 Subject: [PATCH 4/5] updated license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index db498fc..1d4760c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 The mould Contributors +Copyright (c) 2026 Nils Pukropp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -- 2.49.1 From 6c8fc7268bc7126c61b3abd0c6750341c02aa1d2 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Mar 2026 18:00:04 +0100 Subject: [PATCH 5/5] version bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 193dda4..ea1a818 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mould" -version = "0.2.0" +version = "0.2.1" edition = "2024" authors = ["Nils Pukropp "] -- 2.49.1