1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use anstyle::{AnsiColor, Color::Ansi, Style};
use clap::builder::styling::Styles;
use clap::{Parser, Subcommand};

use std::io::{Error, ErrorKind};
use std::path::PathBuf;

const CMD_STYLE: Style = Style::new()
    .bold()
    .fg_color(Some(Ansi(AnsiColor::BrightCyan)));
const HEADER_STYLE: Style = Style::new().bold().fg_color(Some(Ansi(AnsiColor::Green)));
const PLACEHOLDER_STYLE: Style = Style::new().fg_color(Some(Ansi(AnsiColor::BrightCyan)));
const STYLES: Styles = Styles::styled()
    .literal(AnsiColor::BrightCyan.on_default().bold())
    .placeholder(AnsiColor::BrightCyan.on_default());

const OPTIONS_PLACEHOLDER: &str = "{options}";
const SUBCOMMANDS_PLACEHOLDER: &str = "{subcommands}";

fn help_template(template: &str) -> String {
    let header = HEADER_STYLE.render();
    let rheader = HEADER_STYLE.render_reset();
    let rip_s = CMD_STYLE.render();
    let rrip_s = CMD_STYLE.render_reset();
    let place = PLACEHOLDER_STYLE.render();
    let rplace = PLACEHOLDER_STYLE.render_reset();

    match template {
        "rip" => format!(
            "\
rip: a safe and ergonomic alternative to rm

{header}Usage{rheader}: {rip_s}rip{rrip_s} [{place}OPTIONS{rplace}] [{place}FILES{rplace}]...
       {rip_s}rip{rrip_s} [{place}SUBCOMMAND{rplace}]

{header}Arguments{rheader}:
    [{place}FILES{rplace}]...  Files or directories to remove

{header}Options{rheader}:
{OPTIONS_PLACEHOLDER}

{header}Subcommands{rheader}:
{SUBCOMMANDS_PLACEHOLDER}
"
        ),
        "completions" => format!(
            "\
Generate the shell completions file

{header}Usage{rheader}: {rip_s}rip completions{rrip_s} <{place}SHELL{rplace}>

{header}Arguments{rheader}:
    <{place}SHELL{rplace}>  The shell to generate completions for

{header}Options{rheader}:
{OPTIONS_PLACEHOLDER}
"
        ),
        "graveyard" => format!(
            "\
Print the graveyard path

{header}Usage{rheader}: {rip_s}rip graveyard{rrip_s} [{place}OPTIONS{rplace}]

{header}Options{rheader}:
{OPTIONS_PLACEHOLDER}
"
        ),
        _ => unreachable!(),
    }
}

#[derive(Parser, Debug, Default)]
#[command(
    name = "rip",
    version,
    about,
    long_about = None,
    styles=STYLES,
    help_template = help_template("rip"),
)]
pub struct Args {
    /// Files and directories to remove
    pub targets: Vec<PathBuf>,

    /// Directory where deleted files rest
    #[arg(long)]
    pub graveyard: Option<PathBuf>,

    /// Permanently deletes the graveyard
    #[arg(short, long)]
    pub decompose: bool,

    /// Prints files that were deleted
    /// in the current directory
    #[arg(short, long)]
    pub seance: bool,

    /// Restore the specified
    /// files or the last file
    /// if none are specified
    #[arg(short, long, num_args = 0)]
    pub unbury: Option<Vec<PathBuf>>,

    /// Print some info about TARGET before
    /// burying
    #[arg(short, long)]
    pub inspect: bool,

    #[command(subcommand)]
    pub command: Option<Commands>,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Generate shell completions file
    #[command(styles=STYLES, help_template=help_template("completions"))]
    Completions {
        /// The shell to generate completions for
        #[arg(value_name = "SHELL")]
        shell: String,
    },

    /// Print the graveyard path
    #[command(styles=STYLES, help_template=help_template("graveyard"))]
    Graveyard {
        /// Get the graveyard subdirectory
        /// of the current directory
        #[arg(short, long)]
        seance: bool,
    },
}

struct IsDefault {
    graveyard: bool,
    decompose: bool,
    seance: bool,
    unbury: bool,
    inspect: bool,
    completions: bool,
}

impl IsDefault {
    fn new(cli: &Args) -> IsDefault {
        let defaults = Args::default();
        IsDefault {
            graveyard: cli.graveyard == defaults.graveyard,
            decompose: cli.decompose == defaults.decompose,
            seance: cli.seance == defaults.seance,
            unbury: cli.unbury == defaults.unbury,
            inspect: cli.inspect == defaults.inspect,
            completions: cli.command.is_none(),
        }
    }
}

#[allow(clippy::nonminimal_bool)]
pub fn validate_args(cli: &Args) -> Result<(), Error> {
    let defaults = IsDefault::new(cli);

    // [completions] can only be used by itself
    if !defaults.completions
        && !(defaults.graveyard
            && defaults.decompose
            && defaults.seance
            && defaults.unbury
            && defaults.inspect)
    {
        return Err(Error::new(
            ErrorKind::InvalidInput,
            "--completions can only be used by itself",
        ));
    }
    if !defaults.decompose && !(defaults.seance && defaults.unbury && defaults.inspect) {
        return Err(Error::new(
            ErrorKind::InvalidInput,
            "-d,--decompose can only be used with --graveyard",
        ));
    }

    Ok(())
}