1use std::str::FromStr;
2
3use clap::{
4 Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum,
5 builder::{ValueParser, styling::Style},
6};
7use clap_stdin::{FileOrStdin, MaybeStdin};
8use color_eyre::{Result, eyre::eyre};
9use itertools::Itertools;
10use regex::Regex;
11use reqwest::{
12 Method,
13 header::{HeaderName, HeaderValue},
14};
15use tracing::instrument;
16
17use crate::model::SearchMode;
18
19#[derive(Parser)]
27#[cfg_attr(debug_assertions, derive(Debug))]
28#[command(
29 author,
30 version,
31 verbatim_doc_comment,
32 infer_subcommands = true,
33 subcommand_required = true,
34 after_long_help = include_str!("_examples/cli.txt")
35)]
36pub struct Cli {
37 #[arg(long, hide = true)]
41 pub skip_execution: bool,
42
43 #[arg(long, hide = true)]
47 pub extra_line: bool,
48
49 #[arg(long, hide = true)]
54 pub file_output: Option<String>,
55
56 #[command(name = "command", subcommand)]
58 pub process: CliProcess,
59}
60
61#[derive(Subcommand)]
62#[cfg_attr(debug_assertions, derive(Debug))]
63pub enum CliProcess {
64 #[cfg(debug_assertions)]
65 Query(QueryProcess),
67
68 #[command(after_long_help = include_str!("_examples/init.txt"))]
70 Init(InitProcess),
71
72 #[command(after_long_help = include_str!("_examples/new.txt"))]
74 New(Interactive<BookmarkCommandProcess>),
75
76 #[command(after_long_help = include_str!("_examples/search.txt"))]
78 Search(Interactive<SearchCommandsProcess>),
79
80 #[command(after_long_help = include_str!("_examples/replace.txt"))]
86 Replace(Interactive<VariableReplaceProcess>),
87
88 #[command(after_long_help = include_str!("_examples/fix.txt"))]
93 Fix(CommandFixProcess),
94
95 #[command(after_long_help = include_str!("_examples/export.txt"))]
99 Export(Interactive<ExportItemsProcess>),
100
101 #[command(after_long_help = include_str!("_examples/import.txt"))]
103 Import(Interactive<ImportItemsProcess>),
104
105 #[command(name = "tldr", subcommand)]
107 Tldr(TldrProcess),
108
109 #[command(subcommand)]
111 Completion(CompletionProcess),
112}
113
114#[derive(Subcommand)]
115#[cfg_attr(debug_assertions, derive(Debug))]
116pub enum TldrProcess {
117 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
121 Fetch(TldrFetchProcess),
122
123 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
125 Clear(TldrClearProcess),
126}
127
128#[derive(Subcommand)]
129#[cfg_attr(debug_assertions, derive(Debug))]
130pub enum CompletionProcess {
131 #[command(after_long_help = include_str!("_examples/completion_new.txt"))]
133 New(Interactive<CompletionNewProcess>),
134 #[command(after_long_help = include_str!("_examples/completion_delete.txt"))]
136 Delete(CompletionDeleteProcess),
137 #[command(alias = "ls", after_long_help = include_str!("_examples/completion_list.txt"))]
139 List(Interactive<CompletionListProcess>),
140}
141
142#[derive(Args, Debug)]
146pub struct Interactive<T: FromArgMatches + Args> {
147 #[command(flatten)]
149 pub process: T,
150
151 #[command(flatten)]
153 pub opts: InteractiveOptions,
154}
155
156#[derive(Args, Debug)]
158pub struct InteractiveOptions {
159 #[arg(short = 'i', long)]
161 pub interactive: bool,
162
163 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
165 pub inline: bool,
166
167 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
169 pub full_screen: bool,
170}
171
172#[cfg(debug_assertions)]
173#[derive(Args, Debug)]
175pub struct QueryProcess {
176 #[arg(default_value = "-")]
178 pub sql: FileOrStdin,
179}
180
181#[derive(Args, Debug)]
183pub struct InitProcess {
184 #[arg(value_enum)]
186 pub shell: Shell,
187}
188
189#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
190pub enum Shell {
191 Bash,
192 Zsh,
193 Fish,
194 Powershell,
195}
196
197#[derive(Args, Debug)]
199pub struct BookmarkCommandProcess {
200 #[arg(required_unless_present = "interactive")]
204 pub command: Option<String>,
205
206 #[arg(short = 'a', long)]
208 pub alias: Option<String>,
209
210 #[arg(short = 'd', long)]
212 pub description: Option<String>,
213
214 #[arg(long)]
216 pub ai: bool,
217}
218
219#[derive(Args, Debug)]
221pub struct SearchCommandsProcess {
222 pub query: Option<String>,
224
225 #[arg(short = 'm', long)]
227 pub mode: Option<SearchMode>,
228
229 #[arg(short = 'u', long)]
231 pub user_only: bool,
232
233 #[arg(long, requires = "query")]
235 pub ai: bool,
236}
237
238#[derive(Args, Debug)]
240pub struct VariableReplaceProcess {
241 #[arg(default_value = "-")]
245 pub command: MaybeStdin<String>,
246
247 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
251 pub values: Vec<(String, Option<String>)>,
252
253 #[arg(short = 'E', long)]
262 pub use_env: bool,
263}
264
265#[derive(Args, Debug)]
267pub struct CommandFixProcess {
268 pub command: String,
270
271 #[arg(long, value_name = "HISTORY")]
275 pub history: Option<String>,
276}
277
278#[derive(Args, Clone, Debug)]
280pub struct ExportItemsProcess {
281 #[arg(default_value = "-")]
285 pub location: String,
286 #[arg(long, group = "location_type")]
288 pub file: bool,
289 #[arg(long, group = "location_type")]
291 pub http: bool,
292 #[arg(long, group = "location_type")]
294 pub gist: bool,
295 #[arg(long, value_name = "REGEX")]
299 pub filter: Option<Regex>,
300 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
305 pub headers: Vec<(HeaderName, HeaderValue)>,
306 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
310 pub method: HttpMethod,
311}
312
313#[derive(Args, Clone, Debug)]
315pub struct ImportItemsProcess {
316 #[arg(default_value = "-", required_unless_present = "history")]
320 pub location: String,
321 #[arg(long)]
323 pub ai: bool,
324 #[arg(long)]
328 pub dry_run: bool,
329 #[arg(long, group = "location_type")]
331 pub file: bool,
332 #[arg(long, group = "location_type")]
334 pub http: bool,
335 #[arg(long, group = "location_type")]
337 pub gist: bool,
338 #[arg(long, value_enum, group = "location_type", requires = "ai")]
340 pub history: Option<HistorySource>,
341 #[arg(long, value_name = "REGEX")]
345 pub filter: Option<Regex>,
346 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
351 pub tags: Vec<String>,
352 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
357 pub headers: Vec<(HeaderName, HeaderValue)>,
358 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
362 pub method: HttpMethod,
363}
364
365#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
366pub enum HistorySource {
367 Bash,
368 Zsh,
369 Fish,
370 Powershell,
371 Atuin,
372}
373
374#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
375pub enum HttpMethod {
376 GET,
377 POST,
378 PUT,
379 PATCH,
380}
381impl From<HttpMethod> for Method {
382 fn from(value: HttpMethod) -> Self {
383 match value {
384 HttpMethod::GET => Method::GET,
385 HttpMethod::POST => Method::POST,
386 HttpMethod::PUT => Method::PUT,
387 HttpMethod::PATCH => Method::PATCH,
388 }
389 }
390}
391
392#[derive(Args, Debug)]
394pub struct TldrFetchProcess {
395 pub category: Option<String>,
399
400 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
405 pub commands: Vec<String>,
406
407 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
411 pub filter_commands: Option<FileOrStdin>,
412}
413
414#[derive(Args, Debug)]
416pub struct TldrClearProcess {
417 pub category: Option<String>,
421}
422
423#[derive(Args, Debug)]
425pub struct CompletionNewProcess {
426 #[arg(short = 'c', long)]
428 pub command: Option<String>,
429 #[arg(required_unless_present = "interactive")]
431 pub variable: Option<String>,
432 #[arg(required_unless_present_any = ["interactive", "ai"])]
434 pub provider: Option<String>,
435 #[arg(long)]
437 pub ai: bool,
438}
439
440#[derive(Args, Debug)]
442pub struct CompletionDeleteProcess {
443 #[arg(short = 'c', long)]
445 pub command: Option<String>,
446 pub variable: String,
448}
449
450#[derive(Args, Debug)]
452pub struct CompletionListProcess {
453 pub command: Option<String>,
455}
456
457impl Cli {
458 #[instrument]
460 pub fn parse_extended() -> Self {
461 let mut cmd = Self::command_for_update();
463
464 let style = cmd.get_styles().clone();
466 let dimmed = style.get_placeholder().dimmed();
467 let plain_examples_header = "Examples:";
468 let styled_examples_header = format!(
469 "{}Examples:{}",
470 style.get_usage().render(),
471 style.get_usage().render_reset()
472 );
473 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
474
475 let matches = cmd.get_matches();
477
478 match Cli::from_arg_matches(&matches) {
480 Ok(args) => args,
481 Err(err) => err.exit(),
482 }
483 }
484}
485
486fn style_after_long_help(
487 command_ref: &mut Command,
488 dimmed: &Style,
489 plain_examples_header: &str,
490 styled_examples_header: &str,
491) {
492 let mut command = std::mem::take(command_ref);
493 if let Some(after_long_help) = command.get_after_long_help() {
494 let current_help_text = after_long_help.to_string();
495 let modified_help_text = current_help_text
496 .replace(plain_examples_header, styled_examples_header)
498 .lines()
500 .map(|line| {
501 if line.trim_start().starts_with('#') {
502 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
503 } else {
504 line.to_string()
505 }
506 }).join("\n");
507 command = command.after_long_help(modified_help_text);
508 }
509 for subcommand_ref in command.get_subcommands_mut() {
510 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
511 }
512 *command_ref = command;
513}
514
515fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
516 if let Some((var, value)) = env.split_once('=') {
517 Ok((var.to_owned(), Some(value.to_owned())))
518 } else {
519 Ok((env.to_owned(), None))
520 }
521}
522
523fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
524 if let Some((name, value)) = env.split_once(':') {
525 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
526 } else {
527 Err(eyre!("Missing a colon between the header name and value"))
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_cli_asserts() {
537 Cli::command().debug_assert()
538 }
539}