Skip to main content

yui/
cli.rs

1use anyhow::Result;
2use camino::Utf8PathBuf;
3use clap::builder::styling::{AnsiColor, Effects, Styles};
4use clap::{Parser, Subcommand};
5
6use crate::cmd;
7use crate::config::IconsMode;
8
9/// Explicit colour palette for `--help` output. clap honours `NO_COLOR`
10/// and falls back to monochrome when stdout isn't a TTY, so this is
11/// safe to leave on unconditionally — the styled bytes are only ever
12/// emitted when a real terminal is reading them. The palette mirrors
13/// the bind-points / icon colours used in `yui list` / `yui status` so
14/// help, list, and status all feel like the same tool.
15const HELP_STYLES: Styles = Styles::styled()
16    // "Commands:" / "Options:" / etc. section headers.
17    .header(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD))
18    // The "Usage:" heading label itself (NOT the binary name — that
19    // falls under `literal` below).
20    .usage(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD))
21    // Binary name in the usage line + every subcommand / option
22    // literal (`init`, `--source`, …).
23    .literal(AnsiColor::Magenta.on_default().effects(Effects::BOLD))
24    // <PLACEHOLDER> values inside option signatures.
25    .placeholder(AnsiColor::Cyan.on_default())
26    .error(AnsiColor::Red.on_default().effects(Effects::BOLD))
27    .valid(AnsiColor::Green.on_default())
28    .invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD));
29
30#[derive(Parser, Debug)]
31#[command(version, about, long_about = None, styles = HELP_STYLES)]
32pub struct Cli {
33    /// Path to dotfiles source repository ($DOTFILES)
34    #[arg(short, long, env = "YUI_SOURCE", global = true)]
35    pub source: Option<Utf8PathBuf>,
36
37    /// Increase log verbosity (-v, -vv, -vvv)
38    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
39    pub verbose: u8,
40
41    #[command(subcommand)]
42    pub command: Command,
43}
44
45#[derive(Subcommand, Debug)]
46pub enum Command {
47    /// Initialize source repo skeleton
48    Init {
49        /// Install git pre-commit/pre-push hooks for render-drift check
50        #[arg(long)]
51        git_hooks: bool,
52    },
53
54    /// Render templates + link targets + auto-absorb (default workflow)
55    Apply {
56        #[arg(long)]
57        dry_run: bool,
58    },
59
60    /// Render templates only
61    Render {
62        /// Fail with non-zero exit if rendered output diverges (CI hook)
63        #[arg(long)]
64        check: bool,
65        #[arg(long)]
66        dry_run: bool,
67    },
68
69    /// Link / relink targets only
70    Link {
71        #[arg(long)]
72        dry_run: bool,
73    },
74
75    /// Unlink targets
76    Unlink { paths: Vec<Utf8PathBuf> },
77
78    /// Show drift status (link-broken / replaced / template-drift)
79    Status {
80        /// Override [ui] icons mode for this invocation
81        #[arg(long, value_name = "MODE")]
82        icons: Option<IconsMode>,
83        /// Disable color output (also respected via NO_COLOR env)
84        #[arg(long)]
85        no_color: bool,
86    },
87
88    /// List all src→dst link mappings (mount entries + .yuilink overrides)
89    List {
90        /// Include entries whose `when` evaluates false on the current host
91        #[arg(long)]
92        all: bool,
93        /// Override [ui] icons mode for this invocation
94        #[arg(long, value_name = "MODE")]
95        icons: Option<IconsMode>,
96        /// Disable color output (also respected via NO_COLOR env)
97        #[arg(long)]
98        no_color: bool,
99    },
100
101    /// Manually absorb a target into source (when auto-absorb skipped)
102    Absorb {
103        target: Utf8PathBuf,
104        #[arg(long)]
105        dry_run: bool,
106    },
107
108    /// Diagnose environment (symlink capability, source detection, etc)
109    Doctor,
110
111    /// Garbage-collect old backups
112    GcBackup {
113        /// e.g. "30d", "6m"
114        #[arg(long)]
115        older_than: Option<String>,
116    },
117
118    /// Manage `[[hook]]` scripts
119    Hooks {
120        #[command(subcommand)]
121        action: HookAction,
122    },
123}
124
125#[derive(Subcommand, Debug)]
126pub enum HookAction {
127    /// List configured hooks with their last-run state
128    List,
129    /// Run a hook (or every hook). The `when` filter is always honored;
130    /// `--force` bypasses the `when_run` state check (so a `once` hook
131    /// can be re-run, an `onchange` hook re-runs even with matching
132    /// hash).
133    Run {
134        /// Hook name (omit to run every hook per its `when_run` rule)
135        name: Option<String>,
136        /// Bypass the `when_run` state check
137        #[arg(long)]
138        force: bool,
139    },
140}
141
142impl Cli {
143    pub fn run(self) -> Result<()> {
144        let source = self.source;
145        match self.command {
146            Command::Init { git_hooks } => cmd::init(source, git_hooks),
147            Command::Apply { dry_run } => cmd::apply(source, dry_run),
148            Command::Render { check, dry_run } => cmd::render(source, check, dry_run),
149            Command::Link { dry_run } => cmd::link(source, dry_run),
150            Command::Unlink { paths } => cmd::unlink(source, paths),
151            Command::Status { icons, no_color } => cmd::status(source, icons, no_color),
152            Command::List {
153                all,
154                icons,
155                no_color,
156            } => cmd::list(source, all, icons, no_color),
157            Command::Absorb { target, dry_run } => cmd::absorb(source, target, dry_run),
158            Command::Doctor => cmd::doctor(source),
159            Command::GcBackup { older_than } => cmd::gc_backup(source, older_than),
160            Command::Hooks { action } => match action {
161                HookAction::List => cmd::hooks_list(source),
162                HookAction::Run { name, force } => cmd::hooks_run(source, name, force),
163            },
164        }
165    }
166}