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 pub targets: Vec<PathBuf>,
85
86 #[arg(long)]
88 pub graveyard: Option<PathBuf>,
89
90 #[arg(short, long)]
92 pub decompose: bool,
93
94 #[arg(short, long)]
97 pub seance: bool,
98
99 #[arg(short, long, num_args = 0..)]
103 pub unbury: Option<Vec<PathBuf>>,
104
105 #[arg(short, long)]
108 pub inspect: bool,
109
110 #[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 #[command(styles=STYLES, help_template=help_template("completions"))]
122 Completions {
123 #[arg(value_name = "SHELL")]
125 shell: String,
126 },
127
128 #[command(styles=STYLES, help_template=help_template("graveyard"))]
130 Graveyard {
131 #[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 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 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}