mod commands; use clap::{Parser, Subcommand, ValueEnum}; use std::path::PathBuf; use tracing_subscriber::EnvFilter; #[derive(Parser)] #[command(name = "doot")] #[command(about = "A modern dotfiles manager with a typed DSL", long_about = None)] struct Cli { #[command(subcommand)] command: Commands, /// Increase verbosity (-v = info, -vv = debug, -vvv = trace) #[arg(short, long, global = true, action = clap::ArgAction::Count)] verbose: u8, /// Suppress all log output #[arg(short, long, global = true)] quiet: bool, #[arg(short = 'C', long, global = true)] config: Option, /// Log level (overrides -v/-q flags) #[arg(long, global = true)] log_level: Option, /// Log output format #[arg(long, global = true, default_value = "text")] log_format: LogFormatArg, } #[derive(Clone, ValueEnum)] enum LogLevelArg { Trace, Debug, Info, Warn, Error, } #[derive(Clone, ValueEnum)] enum LogFormatArg { Text, Json, } #[derive(Subcommand)] enum Commands { Init { /// Source directory for dotfiles (default: ~/.config/doot) path: Option, }, Apply { #[arg(short = 'n', long)] dry_run: bool, #[arg(short, long)] parallel: bool, }, Diff { #[arg(short, long)] all: bool, }, Status, Check, Fmt { #[arg(short, long)] check: bool, }, Rollback { snapshot: Option, }, Snapshot { name: String, }, Encrypt { file: PathBuf, #[arg(short, long)] recipient: Option, }, Decrypt { file: PathBuf, #[arg(short, long)] identity: Option, }, Package { #[command(subcommand)] action: PackageAction, }, Lsp, Tui, /// Open source file in editor for a deployed target Edit { /// Target path or dotfile name (e.g., ~/.config/nvim or nvim) target: String, /// Apply changes after editing #[arg(short, long)] apply: bool, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, } #[derive(Subcommand)] enum PackageAction { Install, Update, List, } fn main() -> anyhow::Result<()> { let cli = Cli::parse(); // Priority: DOOT_LOG env > --log-level flag > -q/v flags > default (warn) let default_level = if let Some(ref level) = cli.log_level { match level { LogLevelArg::Trace => "trace", LogLevelArg::Debug => "debug", LogLevelArg::Info => "info", LogLevelArg::Warn => "warn", LogLevelArg::Error => "error", } } else if cli.quiet { "off" } else { match cli.verbose { 0 => "warn", 1 => "info", 2 => "debug", _ => "trace", } }; // Scope doot crates to chosen level, third-party at warn // -vvvv+ enables trace for ALL crates including deps let default_directive = if default_level == "off" { "off".to_string() } else if cli.verbose >= 4 { default_level.to_string() } else { format!( "doot_cli={l},doot_core={l},doot_lang={l},warn", l = default_level ) }; let env_filter = EnvFilter::try_from_env("DOOT_LOG").unwrap_or_else(|_| EnvFilter::new(&default_directive)); match cli.log_format { LogFormatArg::Json => { tracing_subscriber::fmt() .json() .with_env_filter(env_filter) .with_target(true) .with_writer(std::io::stderr) .init(); } LogFormatArg::Text => { tracing_subscriber::fmt() .with_env_filter(env_filter) .with_target(true) .with_writer(std::io::stderr) .init(); } } match cli.command { Commands::Init { path } => commands::init::run(path), Commands::Apply { dry_run, parallel } => { commands::apply::run(cli.config, dry_run, parallel) } Commands::Diff { all } => commands::diff::run(cli.config, all), Commands::Status => commands::status::run(cli.config), Commands::Check => commands::check::run(cli.config), Commands::Fmt { check } => commands::fmt::run(cli.config, check), Commands::Rollback { snapshot } => commands::rollback::run(cli.config, snapshot), Commands::Snapshot { name } => commands::snapshot::run(cli.config, name), Commands::Encrypt { file, recipient } => commands::encrypt::run(file, recipient), Commands::Decrypt { file, identity } => commands::decrypt::run(file, identity), Commands::Package { action } => match action { PackageAction::Install => commands::package::install(cli.config), PackageAction::Update => commands::package::update(), PackageAction::List => commands::package::list(cli.config), }, Commands::Lsp => commands::lsp::run(), Commands::Tui => commands::tui::run(cli.config), Commands::Edit { target, apply, yes } => { commands::edit::run(cli.config, target, apply, yes) } } }