1use std::env;
4use std::path::PathBuf;
5
6use clap::{ArgAction, Parser, Subcommand, ValueEnum, ValueHint};
7use clap_complete::Shell;
8
9use crate::management::{backup::BackupStatus, compaction::Strategy};
10
11#[derive(Debug, Parser)]
13#[clap(author, about, version)]
14#[clap(propagate_version = true)]
15pub struct Config {
16 #[arg(short = 'd', long = "dirpath", value_hint = ValueHint::DirPath,
21 default_value_os_t = default_backup_dirpath())]
22 pub backup_dirpath: PathBuf,
23
24 #[command(subcommand)]
26 pub command: Command,
27}
28
29#[derive(Debug, Subcommand)]
31pub enum Command {
32 Save {
42 #[command(flatten)]
44 strategy: StrategyConfig,
45
46 #[arg(long, action = ArgAction::SetTrue)]
48 to_tmux: bool,
49
50 #[arg(long, action = ArgAction::SetTrue)]
52 compact: bool,
53
54 #[arg(
65 short = 'i',
66 long = "ignore-last-lines",
67 value_name = "NUMBER",
68 default_value_t = 0
69 )]
70 num_lines_to_drop: u8,
71 },
72
73 Restore {
83 #[command(flatten)]
85 strategy: StrategyConfig,
86
87 #[arg(long, action = ArgAction::SetTrue)]
89 to_tmux: bool,
90
91 #[arg(value_parser)]
93 backup_filepath: Option<PathBuf>,
94 },
95
96 Catalog {
98 #[command(flatten)]
100 strategy: StrategyConfig,
101
102 #[command(subcommand)]
104 command: CatalogSubcommand,
105 },
106
107 Describe {
109 #[arg(value_parser, value_hint = ValueHint::FilePath)]
111 backup_filepath: PathBuf,
112 },
113
114 GenerateCompletion {
116 #[arg(value_enum, value_parser = clap::value_parser!(Shell))]
118 shell: Shell,
119 },
120
121 Init,
127}
128
129#[derive(Debug, Subcommand)]
131pub enum CatalogSubcommand {
132 List {
143 #[arg(long = "details", action = ArgAction::SetTrue)]
149 details_flag: bool,
150
151 #[arg(long = "only", value_enum, value_parser)]
153 only_backup_status: Option<BackupStatus>,
154
155 #[arg(long = "filepaths", action = ArgAction::SetTrue)]
157 filepaths_flag: bool,
158 },
159
160 Compact,
162}
163
164#[derive(Debug, Clone, ValueEnum)]
166enum StrategyValues {
167 MostRecent,
169
170 Classic,
178}
179
180#[derive(Debug, clap::Args)]
182pub struct StrategyConfig {
183 #[arg(short = 's', long = "strategy", value_enum, default_value_t = StrategyValues::MostRecent)]
184 strategy: StrategyValues,
185
186 #[arg(
188 short = 'n',
189 long,
190 value_name = "NUMBER",
191 value_parser = clap::value_parser!(u16).range(1..),
192 default_value_t = 10,
193 )]
194 num_backups: u16,
195}
196
197impl StrategyConfig {
202 pub fn strategy(&self) -> Strategy {
204 match self.strategy {
205 StrategyValues::MostRecent => Strategy::most_recent(self.num_backups as usize),
206 StrategyValues::Classic => Strategy::Classic,
207 }
208 }
209}
210
211fn default_backup_dirpath() -> PathBuf {
220 let state_home = match env::var("XDG_STATE_HOME") {
221 Ok(v) => PathBuf::from(v),
222 Err(_) => match env::var("HOME") {
223 Ok(v) => PathBuf::from(v).join(".local").join("state"),
224 Err(_) => panic!("Cannot find `$HOME` in the environment"),
225 },
226 };
227
228 state_home.join("tmux-backup")
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use clap::Parser;
235
236 mod strategy_config {
237 use super::*;
238
239 fn parse_save_strategy(subcommand_args: &[&str]) -> Strategy {
242 let mut full_args = vec!["tmux-backup", "save"];
243 full_args.extend(subcommand_args);
244
245 let config = Config::try_parse_from(full_args).unwrap();
246 match config.command {
247 Command::Save { strategy, .. } => strategy.strategy(),
248 _ => panic!("Expected Save command"),
249 }
250 }
251
252 #[test]
253 fn default_strategy_is_most_recent_with_10() {
254 let strategy = parse_save_strategy(&[]);
255
256 match strategy {
257 Strategy::KeepMostRecent { k } => assert_eq!(k, 10),
258 _ => panic!("Expected KeepMostRecent"),
259 }
260 }
261
262 #[test]
263 fn explicit_most_recent_strategy() {
264 let strategy = parse_save_strategy(&["-s", "most-recent"]);
265
266 match strategy {
267 Strategy::KeepMostRecent { k } => assert_eq!(k, 10), _ => panic!("Expected KeepMostRecent"),
269 }
270 }
271
272 #[test]
273 fn most_recent_with_custom_count() {
274 let strategy = parse_save_strategy(&["-s", "most-recent", "-n", "25"]);
275
276 match strategy {
277 Strategy::KeepMostRecent { k } => assert_eq!(k, 25),
278 _ => panic!("Expected KeepMostRecent"),
279 }
280 }
281
282 #[test]
283 fn classic_strategy() {
284 let strategy = parse_save_strategy(&["-s", "classic"]);
285
286 assert!(matches!(strategy, Strategy::Classic));
287 }
288
289 #[test]
290 fn long_form_arguments_work() {
291 let strategy =
292 parse_save_strategy(&["--strategy", "most-recent", "--num-backups", "42"]);
293
294 match strategy {
295 Strategy::KeepMostRecent { k } => assert_eq!(k, 42),
296 _ => panic!("Expected KeepMostRecent"),
297 }
298 }
299
300 #[test]
301 fn num_backups_ignored_for_classic() {
302 let strategy = parse_save_strategy(&["-s", "classic", "-n", "99"]);
304
305 assert!(matches!(strategy, Strategy::Classic));
306 }
307 }
308
309 mod cli_parsing {
310 use super::*;
311
312 #[test]
313 fn save_command_parses() {
314 let config = Config::try_parse_from(["tmux-backup", "save"]).unwrap();
315 assert!(matches!(config.command, Command::Save { .. }));
316 }
317
318 #[test]
319 fn save_with_compact_flag() {
320 let config = Config::try_parse_from(["tmux-backup", "save", "--compact"]).unwrap();
321 match config.command {
322 Command::Save { compact, .. } => assert!(compact),
323 _ => panic!("Expected Save command"),
324 }
325 }
326
327 #[test]
328 fn save_with_to_tmux_flag() {
329 let config = Config::try_parse_from(["tmux-backup", "save", "--to-tmux"]).unwrap();
330 match config.command {
331 Command::Save { to_tmux, .. } => assert!(to_tmux),
332 _ => panic!("Expected Save command"),
333 }
334 }
335
336 #[test]
337 fn save_with_ignore_lines() {
338 let config = Config::try_parse_from(["tmux-backup", "save", "-i", "2"]).unwrap();
339 match config.command {
340 Command::Save {
341 num_lines_to_drop, ..
342 } => assert_eq!(num_lines_to_drop, 2),
343 _ => panic!("Expected Save command"),
344 }
345 }
346
347 #[test]
348 fn restore_command_parses() {
349 let config = Config::try_parse_from(["tmux-backup", "restore"]).unwrap();
350 assert!(matches!(config.command, Command::Restore { .. }));
351 }
352
353 #[test]
354 fn restore_with_specific_file() {
355 let config =
356 Config::try_parse_from(["tmux-backup", "restore", "/path/to/backup.tar.zst"])
357 .unwrap();
358 match config.command {
359 Command::Restore {
360 backup_filepath, ..
361 } => {
362 assert_eq!(
363 backup_filepath,
364 Some(PathBuf::from("/path/to/backup.tar.zst"))
365 );
366 }
367 _ => panic!("Expected Restore command"),
368 }
369 }
370
371 #[test]
372 fn catalog_list_command() {
373 let config = Config::try_parse_from(["tmux-backup", "catalog", "list"]).unwrap();
374 match config.command {
375 Command::Catalog { command, .. } => {
376 assert!(matches!(command, CatalogSubcommand::List { .. }));
377 }
378 _ => panic!("Expected Catalog command"),
379 }
380 }
381
382 #[test]
383 fn catalog_list_with_details() {
384 let config =
385 Config::try_parse_from(["tmux-backup", "catalog", "list", "--details"]).unwrap();
386 match config.command {
387 Command::Catalog { command, .. } => match command {
388 CatalogSubcommand::List { details_flag, .. } => {
389 assert!(details_flag);
390 }
391 _ => panic!("Expected List subcommand"),
392 },
393 _ => panic!("Expected Catalog command"),
394 }
395 }
396
397 #[test]
398 fn catalog_list_with_only_purgeable() {
399 let config =
400 Config::try_parse_from(["tmux-backup", "catalog", "list", "--only", "purgeable"])
401 .unwrap();
402 match config.command {
403 Command::Catalog { command, .. } => match command {
404 CatalogSubcommand::List {
405 only_backup_status, ..
406 } => {
407 assert!(matches!(only_backup_status, Some(BackupStatus::Purgeable)));
408 }
409 _ => panic!("Expected List subcommand"),
410 },
411 _ => panic!("Expected Catalog command"),
412 }
413 }
414
415 #[test]
416 fn catalog_compact_command() {
417 let config = Config::try_parse_from(["tmux-backup", "catalog", "compact"]).unwrap();
418 match config.command {
419 Command::Catalog { command, .. } => {
420 assert!(matches!(command, CatalogSubcommand::Compact));
421 }
422 _ => panic!("Expected Catalog command"),
423 }
424 }
425
426 #[test]
427 fn custom_backup_dirpath() {
428 let config =
429 Config::try_parse_from(["tmux-backup", "-d", "/custom/path", "save"]).unwrap();
430 assert_eq!(config.backup_dirpath, PathBuf::from("/custom/path"));
431 }
432
433 #[test]
434 fn describe_command() {
435 let config =
436 Config::try_parse_from(["tmux-backup", "describe", "/path/to/backup.tar.zst"])
437 .unwrap();
438 match config.command {
439 Command::Describe { backup_filepath } => {
440 assert_eq!(backup_filepath, PathBuf::from("/path/to/backup.tar.zst"));
441 }
442 _ => panic!("Expected Describe command"),
443 }
444 }
445
446 #[test]
447 fn generate_completion_command() {
448 let config =
449 Config::try_parse_from(["tmux-backup", "generate-completion", "bash"]).unwrap();
450 match config.command {
451 Command::GenerateCompletion { shell } => {
452 assert!(matches!(shell, Shell::Bash));
453 }
454 _ => panic!("Expected GenerateCompletion command"),
455 }
456 }
457
458 #[test]
459 fn init_command() {
460 let config = Config::try_parse_from(["tmux-backup", "init"]).unwrap();
461 assert!(matches!(config.command, Command::Init));
462 }
463
464 #[test]
465 fn rejects_invalid_num_backups_zero() {
466 let result = Config::try_parse_from(["tmux-backup", "-n", "0", "save"]);
467 assert!(result.is_err());
468 }
469
470 #[test]
471 fn rejects_negative_num_backups() {
472 let result = Config::try_parse_from(["tmux-backup", "-n", "-5", "save"]);
473 assert!(result.is_err());
474 }
475 }
476
477 }