doot/crates/doot-cli/src/main.rs
2026-02-05 23:18:33 -06:00

212 lines
5.3 KiB
Rust

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<PathBuf>,
/// Log level (overrides -v/-q flags)
#[arg(long, global = true)]
log_level: Option<LogLevelArg>,
/// 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<PathBuf>,
},
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<String>,
},
Snapshot {
name: String,
},
Encrypt {
file: PathBuf,
#[arg(short, long)]
recipient: Option<String>,
},
Decrypt {
file: PathBuf,
#[arg(short, long)]
identity: Option<PathBuf>,
},
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)
}
}
}