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 #[cfg(feature = "tldr")]
106 #[command(name = "tldr", subcommand)]
108 Tldr(TldrProcess),
109
110 #[command(subcommand)]
112 Completion(CompletionProcess),
113
114 #[cfg(feature = "self-update")]
115 Update(UpdateProcess),
117}
118
119#[cfg(feature = "tldr")]
120#[derive(Subcommand)]
121#[cfg_attr(debug_assertions, derive(Debug))]
122pub enum TldrProcess {
123 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
127 Fetch(TldrFetchProcess),
128
129 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
131 Clear(TldrClearProcess),
132}
133
134#[derive(Subcommand)]
135#[cfg_attr(debug_assertions, derive(Debug))]
136pub enum CompletionProcess {
137 #[command(after_long_help = include_str!("_examples/completion_new.txt"))]
139 New(Interactive<CompletionNewProcess>),
140 #[command(after_long_help = include_str!("_examples/completion_delete.txt"))]
142 Delete(CompletionDeleteProcess),
143 #[command(alias = "ls", after_long_help = include_str!("_examples/completion_list.txt"))]
145 List(Interactive<CompletionListProcess>),
146}
147
148#[derive(Args, Debug)]
152pub struct Interactive<T: FromArgMatches + Args> {
153 #[command(flatten)]
155 pub process: T,
156
157 #[command(flatten)]
159 pub opts: InteractiveOptions,
160}
161
162#[derive(Args, Debug)]
164pub struct InteractiveOptions {
165 #[arg(short = 'i', long)]
167 pub interactive: bool,
168
169 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
171 pub inline: bool,
172
173 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
175 pub full_screen: bool,
176}
177
178#[cfg(debug_assertions)]
179#[derive(Args, Debug)]
181pub struct QueryProcess {
182 #[arg(default_value = "-")]
184 pub sql: FileOrStdin,
185}
186
187#[derive(Args, Debug)]
189pub struct InitProcess {
190 #[arg(value_enum)]
192 pub shell: Shell,
193}
194
195#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
196pub enum Shell {
197 Bash,
198 Zsh,
199 Fish,
200 #[value(alias("pwsh"))]
201 Powershell,
202 #[value(alias("nu"))]
203 Nushell,
204}
205
206#[derive(Args, Debug)]
208pub struct BookmarkCommandProcess {
209 #[arg(required_unless_present = "interactive")]
213 pub command: Option<String>,
214
215 #[arg(short = 'a', long)]
217 pub alias: Option<String>,
218
219 #[arg(short = 'd', long)]
221 pub description: Option<String>,
222
223 #[arg(long)]
225 pub ai: bool,
226}
227
228#[derive(Args, Debug)]
230pub struct SearchCommandsProcess {
231 pub query: Option<String>,
233
234 #[arg(short = 'm', long)]
236 pub mode: Option<SearchMode>,
237
238 #[arg(short = 'u', long)]
240 pub user_only: bool,
241
242 #[arg(long, requires = "query")]
244 pub ai: bool,
245}
246
247#[derive(Args, Debug)]
249pub struct VariableReplaceProcess {
250 #[arg(default_value = "-")]
254 pub command: MaybeStdin<String>,
255
256 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
260 pub values: Vec<(String, Option<String>)>,
261
262 #[arg(short = 'E', long)]
271 pub use_env: bool,
272}
273
274#[derive(Args, Debug)]
276pub struct CommandFixProcess {
277 pub command: String,
279
280 #[arg(long, value_name = "HISTORY")]
284 pub history: Option<String>,
285}
286
287#[derive(Args, Clone, Debug)]
289pub struct ExportItemsProcess {
290 #[arg(default_value = "-")]
294 pub location: String,
295 #[arg(long, group = "location_type")]
297 pub file: bool,
298 #[arg(long, group = "location_type")]
300 pub http: bool,
301 #[arg(long, group = "location_type")]
303 pub gist: bool,
304 #[arg(long, value_name = "REGEX")]
308 pub filter: Option<Regex>,
309 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
314 pub headers: Vec<(HeaderName, HeaderValue)>,
315 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
319 pub method: HttpMethod,
320}
321
322#[derive(Args, Clone, Debug)]
324pub struct ImportItemsProcess {
325 #[arg(default_value = "-", required_unless_present = "history")]
329 pub location: String,
330 #[arg(long)]
332 pub ai: bool,
333 #[arg(long)]
337 pub dry_run: bool,
338 #[arg(long, group = "location_type")]
340 pub file: bool,
341 #[arg(long, group = "location_type")]
343 pub http: bool,
344 #[arg(long, group = "location_type")]
346 pub gist: bool,
347 #[arg(long, value_enum, group = "location_type", requires = "ai")]
349 pub history: Option<HistorySource>,
350 #[arg(long, value_name = "REGEX")]
354 pub filter: Option<Regex>,
355 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
360 pub tags: Vec<String>,
361 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
366 pub headers: Vec<(HeaderName, HeaderValue)>,
367 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
371 pub method: HttpMethod,
372}
373
374#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
375pub enum HistorySource {
376 Bash,
377 Zsh,
378 Fish,
379 #[value(alias("pwsh"))]
380 Powershell,
381 #[value(alias("nu"))]
382 Nushell,
383 Atuin,
384}
385
386#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
387pub enum HttpMethod {
388 GET,
389 POST,
390 PUT,
391 PATCH,
392}
393impl From<HttpMethod> for Method {
394 fn from(value: HttpMethod) -> Self {
395 match value {
396 HttpMethod::GET => Method::GET,
397 HttpMethod::POST => Method::POST,
398 HttpMethod::PUT => Method::PUT,
399 HttpMethod::PATCH => Method::PATCH,
400 }
401 }
402}
403
404#[cfg(feature = "tldr")]
405#[derive(Args, Debug)]
407pub struct TldrFetchProcess {
408 pub category: Option<String>,
412
413 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
418 pub commands: Vec<String>,
419
420 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
424 pub filter_commands: Option<FileOrStdin>,
425}
426
427#[cfg(feature = "tldr")]
428#[derive(Args, Debug)]
430pub struct TldrClearProcess {
431 pub category: Option<String>,
435}
436
437#[derive(Args, Debug)]
439pub struct CompletionNewProcess {
440 #[arg(short = 'c', long)]
442 pub command: Option<String>,
443 #[arg(required_unless_present = "interactive")]
445 pub variable: Option<String>,
446 #[arg(required_unless_present_any = ["interactive", "ai"])]
448 pub provider: Option<String>,
449 #[arg(long)]
451 pub ai: bool,
452}
453
454#[derive(Args, Debug)]
456pub struct CompletionDeleteProcess {
457 #[arg(short = 'c', long)]
459 pub command: Option<String>,
460 pub variable: String,
462}
463
464#[derive(Args, Debug)]
466pub struct CompletionListProcess {
467 pub command: Option<String>,
469}
470
471#[cfg(feature = "self-update")]
472#[derive(Args, Debug)]
473pub struct UpdateProcess {}
474
475impl Cli {
476 #[instrument]
478 pub fn parse_extended() -> Self {
479 let mut cmd = Self::command_for_update();
481
482 let style = cmd.get_styles().clone();
484 let dimmed = style.get_placeholder().dimmed();
485 let plain_examples_header = "Examples:";
486 let styled_examples_header = format!(
487 "{}Examples:{}",
488 style.get_usage().render(),
489 style.get_usage().render_reset()
490 );
491 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
492
493 let matches = cmd.get_matches();
495
496 match Cli::from_arg_matches(&matches) {
498 Ok(args) => args,
499 Err(err) => err.exit(),
500 }
501 }
502}
503
504fn style_after_long_help(
505 command_ref: &mut Command,
506 dimmed: &Style,
507 plain_examples_header: &str,
508 styled_examples_header: &str,
509) {
510 let mut command = std::mem::take(command_ref);
511 if let Some(after_long_help) = command.get_after_long_help() {
512 let current_help_text = after_long_help.to_string();
513 let modified_help_text = current_help_text
514 .replace(plain_examples_header, styled_examples_header)
516 .lines()
518 .map(|line| {
519 if line.trim_start().starts_with('#') {
520 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
521 } else {
522 line.to_string()
523 }
524 }).join("\n");
525 command = command.after_long_help(modified_help_text);
526 }
527 for subcommand_ref in command.get_subcommands_mut() {
528 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
529 }
530 *command_ref = command;
531}
532
533fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
534 if let Some((var, value)) = env.split_once('=') {
535 Ok((var.to_owned(), Some(value.to_owned())))
536 } else {
537 Ok((env.to_owned(), None))
538 }
539}
540
541fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
542 if let Some((name, value)) = env.split_once(':') {
543 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
544 } else {
545 Err(eyre!("Missing a colon between the header name and value"))
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_cli_asserts() {
555 Cli::command().debug_assert()
556 }
557}