1use clap::{ArgAction, Command, CommandFactory, FromArgMatches, Parser, Subcommand};
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum Action {
7 Init,
9
10 Channels(ChannelsAction),
12
13 Config(ConfigAction),
15
16 Run(RunArgs),
18
19 Help,
21
22 Version,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RunArgs {
29 pub channels: Vec<String>,
31
32 pub command: Vec<String>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum ChannelsAction {
39 List,
41
42 Validate,
44
45 Test { channel_id: String },
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum ConfigAction {
52 Path,
54}
55
56#[derive(Debug, Error)]
58pub enum CliError {
59 #[error("`brb channels test` requires a <channel-id>")]
60 MissingChannelId,
61 #[error("`--channel` requires a value")]
62 MissingChannelFlagValue,
63 #[error("no command provided")]
64 MissingCommand,
65 #[error("{0}")]
66 Clap(String),
67}
68
69#[derive(Debug, Parser)]
70#[command(
71 name = "brb",
72 about = "run a command and notify when it completes",
73 long_about = None,
74 disable_help_subcommand = true
75)]
76struct CliArgs {
77 #[arg(long = "channel", value_name = "channel-id", action = ArgAction::Append)]
79 channels: Vec<String>,
80
81 #[command(subcommand)]
83 subcommand: Option<CliCommand>,
84
85 #[arg(
87 value_name = "command",
88 trailing_var_arg = true,
89 allow_hyphen_values = true
90 )]
91 command: Vec<String>,
92}
93
94#[derive(Debug, Subcommand)]
95enum CliCommand {
96 Init,
98
99 Channels {
101 #[command(subcommand)]
102 action: Option<CliChannelsAction>,
103 },
104
105 Config {
107 #[command(subcommand)]
108 action: Option<CliConfigAction>,
109 },
110}
111
112#[derive(Debug, Subcommand)]
113enum CliChannelsAction {
114 List,
116
117 Validate,
119
120 Test {
122 #[arg(value_name = "channel-id")]
124 channel_id: String,
125 },
126}
127
128#[derive(Debug, Subcommand)]
129enum CliConfigAction {
130 Path,
132}
133
134pub fn usage() -> String {
136 cli_command().render_long_help().to_string()
137}
138
139pub fn parse_args(args: Vec<String>) -> Result<Action, CliError> {
141 if args.is_empty() {
142 return Ok(Action::Help);
143 }
144
145 let first = args[0].as_str();
146 if matches!(first, "-h" | "--help") {
147 return Ok(Action::Help);
148 }
149
150 if matches!(first, "-V" | "--version") {
151 return Ok(Action::Version);
152 }
153
154 if first == "channels"
155 && args.get(1).map(String::as_str) == Some("test")
156 && args.get(2).is_none()
157 {
158 return Err(CliError::MissingChannelId);
159 }
160
161 if args.last().map(String::as_str) == Some("--channel") {
162 return Err(CliError::MissingChannelFlagValue);
163 }
164
165 let mut argv = Vec::with_capacity(args.len() + 1);
166 argv.push("brb".to_string());
167 argv.extend(args);
168
169 let matches = cli_command()
170 .try_get_matches_from(argv)
171 .map_err(|error| CliError::Clap(error.to_string()))?;
172 let parsed =
173 CliArgs::from_arg_matches(&matches).map_err(|error| CliError::Clap(error.to_string()))?;
174
175 if let Some(subcommand) = parsed.subcommand {
176 return match subcommand {
177 CliCommand::Init => Ok(Action::Init),
178 CliCommand::Channels { action } => {
179 let action = match action {
180 Some(CliChannelsAction::List) | None => ChannelsAction::List,
181 Some(CliChannelsAction::Validate) => ChannelsAction::Validate,
182 Some(CliChannelsAction::Test { channel_id }) => {
183 ChannelsAction::Test { channel_id }
184 }
185 };
186 Ok(Action::Channels(action))
187 }
188 CliCommand::Config { action } => {
189 let action = match action {
190 Some(CliConfigAction::Path) | None => ConfigAction::Path,
191 };
192 Ok(Action::Config(action))
193 }
194 };
195 }
196
197 if parsed.command.is_empty() {
198 return Err(CliError::MissingCommand);
199 }
200
201 Ok(Action::Run(RunArgs {
202 channels: parsed.channels,
203 command: parsed.command,
204 }))
205}
206
207fn cli_command() -> Command {
208 CliArgs::command().override_usage(include_str!("../assets/usage.txt").trim_end())
209}