Skip to main content

dotr_dear/cli/
mod.rs

1use std::path::{Path, PathBuf};
2
3use clap::{Args, Parser, Subcommand};
4
5use crate::{
6    config::{self, Config},
7    context::Context,
8};
9
10#[derive(Debug, Parser)]
11#[command(version)]
12pub struct Cli {
13    #[clap(subcommand)]
14    pub command: Option<Command>,
15    #[clap(short, long, global = true)]
16    pub working_dir: Option<String>,
17}
18
19#[derive(Debug, Subcommand)]
20pub enum Command {
21    Init(InitArgs),
22    Import(ImportArgs),
23    Deploy(DeployArgs),
24    Update(UpdateArgs),
25    Diff(DiffArgs),
26    Remove(RemovePackageArgs),
27    PrintVars(PrintVarsArgs),
28    Packages(PackagesArgs),
29    Profiles(ProfilesArgs),
30}
31
32#[derive(Debug, Args, Default)]
33#[command(name = "remove", about = "Remove a managed package.")]
34pub struct RemovePackageArgs {
35    #[arg(num_args(0..))]
36    pub packages: Option<Vec<String>>,
37
38    #[arg(short, long, default_value_t = false)]
39    pub force: bool,
40
41    #[arg(long, default_value_t = false)]
42    pub remove_orphans: bool,
43
44    #[arg(long, default_value_t = false)]
45    pub dry_run: bool,
46
47    #[arg(short = 'P', long)]
48    pub profile: Option<String>,
49}
50
51#[derive(Debug, Args)]
52#[command(name = "init", about = "Intialize dotfiles repository.")]
53pub struct InitArgs {}
54
55#[derive(Debug, Args, Default)]
56#[command(name = "print-vars", about = "Print all user variables.")]
57pub struct PrintVarsArgs {
58    #[arg(short, long)]
59    pub profile: Option<String>,
60}
61
62#[derive(Debug, Args)]
63#[command(name = "import", about = "Import dotfile and update configuration.")]
64#[derive(Default)]
65pub struct ImportArgs {
66    #[arg(value_name = "IMPORT_PATH")]
67    pub path: String,
68
69    #[arg(short, long, default_value_t = false)]
70    pub symlink: bool,
71
72    #[arg(short, long)]
73    pub name: Option<String>,
74
75    #[arg(short, long)]
76    pub profile: Option<String>,
77}
78
79#[derive(Debug, Args, Default)]
80#[command(name = "diff", about = "Show differences between dotfiles.")]
81pub struct DiffArgs {
82    #[arg(num_args(0..), short, long)]
83    pub packages: Option<Vec<String>>,
84
85    #[arg(short = 'P', long)]
86    pub profile: Option<String>,
87
88    #[arg(long, default_value_t = false)]
89    pub ignore_errors: bool,
90}
91
92#[derive(Debug, Args, Default)]
93#[command(name = "update", about = "Update dotfiles from deployed versions.")]
94pub struct UpdateArgs {
95    #[arg(num_args(0..), short, long)]
96    pub packages: Option<Vec<String>>,
97
98    #[arg(short = 'P', long)]
99    pub profile: Option<String>,
100
101    #[arg(long, default_value_t = false)]
102    pub ignore_errors: bool,
103
104    #[arg(long)]
105    pub clean: Option<bool>,
106
107    #[arg(long, default_value_t = false)]
108    pub dry_run: bool,
109}
110
111#[derive(Debug, Args, Default)]
112#[command(name = "deploy", about = "Deploy dotfiles from repository.")]
113pub struct DeployArgs {
114    #[arg(num_args(0..), short, long)]
115    pub packages: Option<Vec<String>>,
116
117    #[arg(short = 'P', long)]
118    pub profile: Option<String>,
119
120    #[arg(long, default_value_t = false)]
121    pub ignore_errors: bool,
122
123    #[arg(long)]
124    pub clean: Option<bool>,
125
126    #[arg(long, default_value_t = false)]
127    pub dry_run: bool,
128
129    #[arg(long, default_value_t = false)]
130    pub skip_actions: bool,
131
132    #[arg(long, default_value_t = false)]
133    pub skip_pre_actions: bool,
134
135    #[arg(long, default_value_t = false)]
136    pub skip_post_actions: bool,
137
138    #[arg(long, default_value_t = false)]
139    pub ignore_dependencies: bool,
140}
141
142#[derive(Debug, Args, Default)]
143#[command(name = "packages", about = "List all managed packages.")]
144pub struct PackagesArgs {
145    #[arg(short = 'P', long)]
146    pub profile: Option<String>,
147    #[clap(subcommand)]
148    pub command: Option<PackagesCommand>,
149}
150
151#[derive(Debug, Subcommand)]
152pub enum PackagesCommand {
153    List(PackagesListArgs),
154    Import(ImportArgs),
155    Deploy(DeployArgs),
156    Update(UpdateArgs),
157    Remove(RemovePackageArgs),
158    Diff(DiffArgs),
159}
160
161#[derive(Debug, Args, Default)]
162pub struct PackagesListArgs {
163    #[arg(short, long, default_value_t = false)]
164    pub verbose: bool,
165}
166
167#[derive(Debug, Args, Default)]
168pub struct ProfilesArgs {
169    #[clap(subcommand)]
170    pub command: Option<ProfilesCommand>,
171}
172
173#[derive(Debug, Subcommand)]
174pub enum ProfilesCommand {
175    Add(ProfilesAddArgs),
176    List(ProfilesListArgs),
177    Remove(ProfileRemoveArgs),
178}
179
180#[derive(Debug, Args, Default)]
181pub struct ProfileRemoveArgs {
182    #[arg(value_name = "PROFILE_NAME")]
183    pub name: String,
184
185    #[arg(long, default_value_t = false)]
186    pub dry_run: bool,
187
188    #[arg(long, default_value_t = false)]
189    pub remove_orphans: bool,
190}
191
192#[derive(Debug, Args, Default)]
193pub struct ProfilesListArgs {
194    #[arg(short, long, default_value_t = false)]
195    pub verbose: bool,
196}
197
198#[derive(Debug, Args, Default)]
199pub struct ProfilesAddArgs {
200    #[arg(value_name = "PROFILE_NAME")]
201    pub name: String,
202
203    #[arg(long, default_value_t = false)]
204    pub set_as_current: bool,
205}
206
207const BANNER: &str = r#"
208██████╗  ██████╗ ████████╗██████╗ 
209██╔══██╗██╔═══██╗╚══██╔══╝██╔══██╗
210██║  ██║██║   ██║   ██║   ██████╔╝
211██║  ██║██║   ██║   ██║   ██╔══██╗
212██████╔╝╚██████╔╝   ██║   ██║  ██║
213╚═════╝  ╚═════╝    ╚═╝   ╚═╝  ╚═╝
214"#;
215
216pub fn run_cli(args: Cli) -> Result<(), anyhow::Error> {
217    let mut working_dir = std::env::current_dir()?;
218    if let Some(wd) = args.working_dir {
219        working_dir = PathBuf::from(wd);
220    }
221
222    if !working_dir.exists() {
223        anyhow::bail!("The specified working directory does not exist");
224    }
225    working_dir = working_dir.canonicalize()?;
226
227    match args.command {
228        Some(Command::Init(_)) => {
229            println!("Initializing configuration...");
230            Config::init(&working_dir)?;
231            println!("Configuration initialized successfully.");
232        }
233        Some(Command::Import(args)) => {
234            let (mut conf, ctx) = init_config(&working_dir, &args.profile, true)?;
235            conf.import_package(&args, &ctx)?;
236        }
237        Some(Command::Deploy(args)) => {
238            let (conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
239            ctx.get_prompted_variables(&conf, &args.packages)?;
240            conf.deploy_packages(&ctx, &args)?;
241        }
242        Some(Command::Update(args)) => {
243            let (conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
244            ctx.get_prompted_variables(&conf, &args.packages)?;
245            conf.backup_packages(&ctx, &args)?;
246        }
247        Some(Command::Diff(args)) => {
248            let (conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
249            ctx.get_prompted_variables(&conf, &args.packages)?;
250            conf.diff_packages(&ctx, &args)?;
251        }
252        Some(Command::PrintVars(args)) => {
253            let (_, ctx) = init_config(&working_dir, &args.profile, false)?;
254            ctx.print_variables();
255        }
256        Some(Command::Remove(args)) => {
257            let (mut conf, ctx) = init_config(&working_dir, &args.profile, false)?;
258            conf.remove_packages(&args, &ctx)?;
259        }
260        Some(Command::Packages(args)) => {
261            let (mut conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
262            match args.command {
263                Some(PackagesCommand::List(args)) => {
264                    conf.list_packages(&ctx, &args)?;
265                }
266                Some(PackagesCommand::Import(import_args)) => {
267                    conf.import_package(&import_args, &ctx)?;
268                }
269                Some(PackagesCommand::Deploy(deploy_args)) => {
270                    ctx.get_prompted_variables(&conf, &deploy_args.packages)?;
271                    conf.deploy_packages(&ctx, &deploy_args)?;
272                }
273                Some(PackagesCommand::Update(update_args)) => {
274                    ctx.get_prompted_variables(&conf, &update_args.packages)?;
275                    conf.backup_packages(&ctx, &update_args)?;
276                }
277                Some(PackagesCommand::Diff(diff_args)) => {
278                    ctx.get_prompted_variables(&conf, &diff_args.packages)?;
279                    conf.diff_packages(&ctx, &diff_args)?;
280                }
281                Some(PackagesCommand::Remove(remove_args)) => {
282                    conf.remove_packages(&remove_args, &ctx)?;
283                }
284                None => {
285                    println!("No packages command provided. Use --help for more information.");
286                }
287            }
288        }
289        Some(Command::Profiles(args)) => {
290            let (mut conf, mut ctx) = init_config(&working_dir, &None, false)?;
291            match args.command {
292                Some(ProfilesCommand::List(list_args)) => {
293                    conf.list_profiles(&list_args)?;
294                }
295                Some(ProfilesCommand::Add(add_args)) => {
296                    conf.add_profile(&add_args, &mut ctx)?;
297                }
298                Some(ProfilesCommand::Remove(remove_args)) => {
299                    conf.remove_profile(&remove_args, &ctx)?;
300                }
301                None => {
302                    println!("No profiles command provided. Use --help for more information.");
303                }
304            }
305        }
306        None => {
307            println!("No command provided. Use --help for more information.");
308        }
309    }
310    Ok(())
311}
312
313fn init_config(
314    working_dir: &Path,
315    profile: &Option<String>,
316    create_if_missing: bool,
317) -> anyhow::Result<(Config, Context)> {
318    let mut conf = config::Config::from_path(working_dir)?;
319    if conf.banner {
320        println!("{}", BANNER);
321    }
322    let (ctx, profile_created) = Context::new(working_dir, &conf, profile, create_if_missing)?;
323    if profile_created {
324        conf.update_profiles(&ctx.profile, &ctx)?;
325    }
326    Ok((conf, ctx))
327}