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 Init,
30 Add { path: PathBuf },
32 Remove { path: PathBuf },
34 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}