Skip to main content

dot/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, Subcommand};
4use log::LevelFilter;
5
6use crate::commands::{AddCommand, Command, InitCommand, RemoveCommand, SyncCommand};
7use crate::error::Result;
8
9#[derive(Parser)]
10#[command(version, about = "A simple dotfiles manager")]
11pub struct Cli {
12    #[arg(short, long, global = true, help = "Suppress all output")]
13    quiet: bool,
14    #[arg(
15        short,
16        long,
17        global = true,
18        help = "Enable verbose output",
19        conflicts_with = "quiet"
20    )]
21    verbose: bool,
22    #[command(subcommand)]
23    command: CliCommand,
24}
25
26#[derive(Subcommand)]
27enum CliCommand {
28    /// Initialize a new dot repository
29    Init,
30    /// Track a file by moving it here and creating a symlink
31    Add { path: PathBuf },
32    /// Stop tracking a file and restore it
33    Remove { path: PathBuf },
34    /// Create symlinks for all tracked files
35    Sync,
36}
37
38pub fn run() -> Result<()> {
39    let cli = Cli::parse();
40
41    let level = if cli.quiet {
42        LevelFilter::Off
43    } else if cli.verbose {
44        LevelFilter::Debug
45    } else {
46        LevelFilter::Info
47    };
48    crate::logger::init(level);
49
50    match cli.command {
51        CliCommand::Init => InitCommand::new().execute(),
52        CliCommand::Add { path } => AddCommand::new(path).execute(),
53        CliCommand::Remove { path } => RemoveCommand::new(path).execute(),
54        CliCommand::Sync => SyncCommand::new().execute(),
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use clap::CommandFactory;
62
63    #[test]
64    fn verify_cli() {
65        Cli::command().debug_assert();
66    }
67
68    #[test]
69    fn parse_init() {
70        let cli = Cli::try_parse_from(["dot", "init"]).unwrap();
71        assert!(matches!(cli.command, CliCommand::Init));
72    }
73
74    #[test]
75    fn parse_add() {
76        let cli = Cli::try_parse_from(["dot", "add", "/path/file"]).unwrap();
77        assert!(
78            matches!(cli.command, CliCommand::Add { path } if path == PathBuf::from("/path/file"))
79        );
80    }
81
82    #[test]
83    fn parse_remove() {
84        let cli = Cli::try_parse_from(["dot", "remove", "myfile"]).unwrap();
85        assert!(
86            matches!(cli.command, CliCommand::Remove { path } if path == PathBuf::from("myfile"))
87        );
88    }
89
90    #[test]
91    fn parse_sync() {
92        let cli = Cli::try_parse_from(["dot", "sync"]).unwrap();
93        assert!(matches!(cli.command, CliCommand::Sync));
94    }
95
96    #[test]
97    fn add_requires_path() {
98        assert!(Cli::try_parse_from(["dot", "add"]).is_err());
99    }
100
101    #[test]
102    fn parse_quiet_flag() {
103        let cli = Cli::try_parse_from(["dot", "--quiet", "sync"]).unwrap();
104        assert!(cli.quiet);
105        assert!(!cli.verbose);
106    }
107
108    #[test]
109    fn parse_verbose_flag() {
110        let cli = Cli::try_parse_from(["dot", "--verbose", "sync"]).unwrap();
111        assert!(cli.verbose);
112        assert!(!cli.quiet);
113    }
114
115    #[test]
116    fn parse_quiet_short_flag() {
117        let cli = Cli::try_parse_from(["dot", "-q", "sync"]).unwrap();
118        assert!(cli.quiet);
119    }
120
121    #[test]
122    fn parse_verbose_short_flag() {
123        let cli = Cli::try_parse_from(["dot", "-v", "sync"]).unwrap();
124        assert!(cli.verbose);
125    }
126
127    #[test]
128    fn quiet_and_verbose_are_mutually_exclusive() {
129        assert!(Cli::try_parse_from(["dot", "--quiet", "--verbose", "sync"]).is_err());
130    }
131}