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
130#[derive(Debug, Args, Default)]
131#[command(name = "packages", about = "List all managed packages.")]
132pub struct PackagesArgs {
133 #[arg(short = 'P', long)]
134 pub profile: Option<String>,
135 #[clap(subcommand)]
136 pub command: Option<PackagesCommand>,
137}
138
139#[derive(Debug, Subcommand)]
140pub enum PackagesCommand {
141 List(PackagesListArgs),
142 Import(ImportArgs),
143 Deploy(DeployArgs),
144 Update(UpdateArgs),
145 Remove(RemovePackageArgs),
146 Diff(DiffArgs),
147}
148
149#[derive(Debug, Args, Default)]
150pub struct PackagesListArgs {
151 #[arg(short, long, default_value_t = false)]
152 pub verbose: bool,
153}
154
155#[derive(Debug, Args, Default)]
156pub struct ProfilesArgs {
157 #[clap(subcommand)]
158 pub command: Option<ProfilesCommand>,
159}
160
161#[derive(Debug, Subcommand)]
162pub enum ProfilesCommand {
163 Add(ProfilesAddArgs),
164 List(ProfilesListArgs),
165 Remove(ProfileRemoveArgs),
166}
167
168#[derive(Debug, Args, Default)]
169pub struct ProfileRemoveArgs {
170 #[arg(value_name = "PROFILE_NAME")]
171 pub name: String,
172
173 #[arg(long, default_value_t = false)]
174 pub dry_run: bool,
175
176 #[arg(long, default_value_t = false)]
177 pub remove_orphans: bool,
178}
179
180#[derive(Debug, Args, Default)]
181pub struct ProfilesListArgs {
182 #[arg(short, long, default_value_t = false)]
183 pub verbose: bool,
184}
185
186#[derive(Debug, Args, Default)]
187pub struct ProfilesAddArgs {
188 #[arg(value_name = "PROFILE_NAME")]
189 pub name: String,
190
191 #[arg(long, default_value_t = false)]
192 pub set_as_current: bool,
193}
194
195const BANNER: &str = r#"
196██████╗ ██████╗ ████████╗██████╗
197██╔══██╗██╔═══██╗╚══██╔══╝██╔══██╗
198██║ ██║██║ ██║ ██║ ██████╔╝
199██║ ██║██║ ██║ ██║ ██╔══██╗
200██████╔╝╚██████╔╝ ██║ ██║ ██║
201╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
202"#;
203
204pub fn run_cli(args: Cli) -> Result<(), anyhow::Error> {
205 let mut working_dir = std::env::current_dir()?;
206 if let Some(wd) = args.working_dir {
207 working_dir = PathBuf::from(wd);
208 }
209
210 if !working_dir.exists() {
211 anyhow::bail!("The specified working directory does not exist");
212 }
213 working_dir = working_dir.canonicalize()?;
214
215 match args.command {
218 Some(Command::Init(_)) => {
219 println!("Initializing configuration...");
220 Config::init(&working_dir)?;
221 println!("Configuration initialized successfully.");
222 }
223 Some(Command::Import(args)) => {
224 let (mut conf, ctx) = init_config(&working_dir, &args.profile, true)?;
225 conf.import_package(&args, &ctx)?;
226 }
227 Some(Command::Deploy(args)) => {
228 let (conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
229 ctx.get_prompted_variables(&conf, &args.packages)?;
230 conf.deploy_packages(&ctx, &args)?;
231 }
232 Some(Command::Update(args)) => {
233 let (conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
234 ctx.get_prompted_variables(&conf, &args.packages)?;
235 conf.backup_packages(&ctx, &args)?;
236 }
237 Some(Command::Diff(args)) => {
238 let (conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
239 ctx.get_prompted_variables(&conf, &args.packages)?;
240 conf.diff_packages(&ctx, &args)?;
241 }
242 Some(Command::PrintVars(args)) => {
243 let (_, ctx) = init_config(&working_dir, &args.profile, false)?;
244 ctx.print_variables();
245 }
246 Some(Command::Remove(args)) => {
247 let (mut conf, ctx) = init_config(&working_dir, &args.profile, false)?;
248 conf.remove_packages(&args, &ctx)?;
249 }
250 Some(Command::Packages(args)) => {
251 let (mut conf, mut ctx) = init_config(&working_dir, &args.profile, false)?;
252 match args.command {
253 Some(PackagesCommand::List(args)) => {
254 conf.list_packages(&ctx, &args)?;
255 }
256 Some(PackagesCommand::Import(import_args)) => {
257 conf.import_package(&import_args, &ctx)?;
258 }
259 Some(PackagesCommand::Deploy(deploy_args)) => {
260 ctx.get_prompted_variables(&conf, &deploy_args.packages)?;
261 conf.deploy_packages(&ctx, &deploy_args)?;
262 }
263 Some(PackagesCommand::Update(update_args)) => {
264 ctx.get_prompted_variables(&conf, &update_args.packages)?;
265 conf.backup_packages(&ctx, &update_args)?;
266 }
267 Some(PackagesCommand::Diff(diff_args)) => {
268 ctx.get_prompted_variables(&conf, &diff_args.packages)?;
269 conf.diff_packages(&ctx, &diff_args)?;
270 }
271 Some(PackagesCommand::Remove(remove_args)) => {
272 conf.remove_packages(&remove_args, &ctx)?;
273 }
274 None => {
275 println!("No packages command provided. Use --help for more information.");
276 }
277 }
278 }
279 Some(Command::Profiles(args)) => {
280 let (mut conf, mut ctx) = init_config(&working_dir, &None, false)?;
281 match args.command {
282 Some(ProfilesCommand::List(list_args)) => {
283 conf.list_profiles(&list_args)?;
284 }
285 Some(ProfilesCommand::Add(add_args)) => {
286 conf.add_profile(&add_args, &mut ctx)?;
287 }
288 Some(ProfilesCommand::Remove(remove_args)) => {
289 conf.remove_profile(&remove_args, &ctx)?;
290 }
291 None => {
292 println!("No profiles command provided. Use --help for more information.");
293 }
294 }
295 }
296 None => {
297 println!("No command provided. Use --help for more information.");
298 }
299 }
300 Ok(())
301}
302
303fn init_config(
304 working_dir: &Path,
305 profile: &Option<String>,
306 create_if_missing: bool,
307) -> anyhow::Result<(Config, Context)> {
308 let mut conf = config::Config::from_path(working_dir)?;
309 if conf.banner {
310 println!("{}", BANNER);
311 }
312 let ctx = Context::new(working_dir, &conf, profile, create_if_missing)?;
314 conf.update_profiles(&ctx.profile, &ctx)?;
315 Ok((conf, ctx))
316}