rip2/
args.rs

1use anstyle::{AnsiColor, Color::Ansi, Style};
2use clap::builder::styling::Styles;
3use clap::{Parser, Subcommand};
4
5use std::io::{Error, ErrorKind};
6use std::path::PathBuf;
7
8const CMD_STYLE: Style = Style::new()
9    .bold()
10    .fg_color(Some(Ansi(AnsiColor::BrightCyan)));
11const HEADER_STYLE: Style = Style::new().bold().fg_color(Some(Ansi(AnsiColor::Green)));
12const PLACEHOLDER_STYLE: Style = Style::new().fg_color(Some(Ansi(AnsiColor::BrightCyan)));
13const STYLES: Styles = Styles::styled()
14    .literal(AnsiColor::BrightCyan.on_default().bold())
15    .placeholder(AnsiColor::BrightCyan.on_default());
16
17const OPTIONS_PLACEHOLDER: &str = "{options}";
18const SUBCOMMANDS_PLACEHOLDER: &str = "{subcommands}";
19
20fn help_template(template: &str) -> String {
21    let header = HEADER_STYLE.render();
22    let rheader = HEADER_STYLE.render_reset();
23    let rip_s = CMD_STYLE.render();
24    let rrip_s = CMD_STYLE.render_reset();
25    let place = PLACEHOLDER_STYLE.render();
26    let rplace = PLACEHOLDER_STYLE.render_reset();
27
28    match template {
29        "rip" => format!(
30            "\
31rip: a safe and ergonomic alternative to rm
32
33{header}Usage{rheader}: {rip_s}rip{rrip_s} [{place}OPTIONS{rplace}] [{place}FILES{rplace}]...
34       {rip_s}rip{rrip_s} [{place}SUBCOMMAND{rplace}]
35
36{header}Arguments{rheader}:
37    [{place}FILES{rplace}]...  Files or directories to remove
38
39{header}Options{rheader}:
40{OPTIONS_PLACEHOLDER}
41
42{header}Subcommands{rheader}:
43{SUBCOMMANDS_PLACEHOLDER}
44"
45        ),
46        "completions" => format!(
47            "\
48Generate the shell completions file
49
50{header}Usage{rheader}: {rip_s}rip completions{rrip_s} <{place}SHELL{rplace}>
51
52{header}Arguments{rheader}:
53    <{place}SHELL{rplace}>  The shell to generate completions for (bash, elvish, fish, powershell, zsh, nushell)
54
55{header}Options{rheader}:
56{OPTIONS_PLACEHOLDER}
57"
58        ),
59        "graveyard" => format!(
60            "\
61Print the graveyard path
62
63{header}Usage{rheader}: {rip_s}rip graveyard{rrip_s} [{place}OPTIONS{rplace}]
64
65{header}Options{rheader}:
66{OPTIONS_PLACEHOLDER}
67"
68        ),
69        _ => unreachable!(),
70    }
71}
72
73#[derive(Parser, Debug, Default)]
74#[command(
75    name = "rip",
76    version,
77    about,
78    long_about = None,
79    styles=STYLES,
80    help_template = help_template("rip"),
81)]
82pub struct Args {
83    /// Files and directories to remove
84    pub targets: Vec<PathBuf>,
85
86    /// Directory where deleted files rest
87    #[arg(long)]
88    pub graveyard: Option<PathBuf>,
89
90    /// Permanently deletes the graveyard
91    #[arg(short, long)]
92    pub decompose: bool,
93
94    /// Prints files that were deleted
95    /// in the current directory
96    #[arg(short, long)]
97    pub seance: bool,
98
99    /// Restore the specified
100    /// files or the last file
101    /// if none are specified
102    #[arg(short, long, num_args = 0..)]
103    pub unbury: Option<Vec<PathBuf>>,
104
105    /// Print some info about FILES before
106    /// burying
107    #[arg(short, long)]
108    pub inspect: bool,
109
110    /// Non-interactive mode
111    #[arg(short, long)]
112    pub force: bool,
113
114    #[command(subcommand)]
115    pub command: Option<Commands>,
116}
117
118#[derive(Subcommand, Debug)]
119pub enum Commands {
120    /// Generate shell completions file
121    #[command(styles=STYLES, help_template=help_template("completions"))]
122    Completions {
123        /// The shell to generate completions for
124        #[arg(value_name = "SHELL")]
125        shell: String,
126    },
127
128    /// Print the graveyard path
129    #[command(styles=STYLES, help_template=help_template("graveyard"))]
130    Graveyard {
131        /// Get the graveyard subdirectory
132        /// of the current directory
133        #[arg(short, long)]
134        seance: bool,
135    },
136}
137
138struct IsDefault {
139    graveyard: bool,
140    decompose: bool,
141    seance: bool,
142    unbury: bool,
143    inspect: bool,
144    force: bool,
145    completions: bool,
146}
147
148impl IsDefault {
149    fn new(cli: &Args) -> Self {
150        let defaults = Args::default();
151        Self {
152            graveyard: cli.graveyard == defaults.graveyard,
153            decompose: cli.decompose == defaults.decompose,
154            seance: cli.seance == defaults.seance,
155            unbury: cli.unbury == defaults.unbury,
156            inspect: cli.inspect == defaults.inspect,
157            force: cli.force == defaults.force,
158            completions: cli.command.is_none(),
159        }
160    }
161}
162
163#[allow(clippy::nonminimal_bool)]
164pub fn validate_args(cli: &Args) -> Result<(), Error> {
165    let defaults = IsDefault::new(cli);
166
167    // [completions] can only be used by itself
168    if !defaults.completions
169        && !(defaults.graveyard
170            && defaults.decompose
171            && defaults.seance
172            && defaults.unbury
173            && defaults.inspect
174            && defaults.force)
175    {
176        return Err(Error::new(
177            ErrorKind::InvalidInput,
178            "--completions can only be used by itself",
179        ));
180    }
181    if !defaults.decompose && !(defaults.seance && defaults.unbury && defaults.inspect) {
182        return Err(Error::new(
183            ErrorKind::InvalidInput,
184            "-d,--decompose can only be used with --graveyard",
185        ));
186    }
187
188    // Force and inspect are incompatible
189    if !defaults.force && !defaults.inspect {
190        return Err(Error::new(
191            ErrorKind::InvalidInput,
192            "-f,--force and -i,--inspect cannot be used together",
193        ));
194    }
195
196    Ok(())
197}