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(()) }