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 Update(UpdateProcess),
115}
116
117#[derive(Subcommand)]
118#[cfg_attr(debug_assertions, derive(Debug))]
119pub enum TldrProcess {
120 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
124 Fetch(TldrFetchProcess),
125
126 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
128 Clear(TldrClearProcess),
129}
130
131#[derive(Subcommand)]
132#[cfg_attr(debug_assertions, derive(Debug))]
133pub enum CompletionProcess {
134 #[command(after_long_help = include_str!("_examples/completion_new.txt"))]
136 New(Interactive<CompletionNewProcess>),
137 #[command(after_long_help = include_str!("_examples/completion_delete.txt"))]
139 Delete(CompletionDeleteProcess),
140 #[command(alias = "ls", after_long_help = include_str!("_examples/completion_list.txt"))]
142 List(Interactive<CompletionListProcess>),
143}
144
145#[derive(Args, Debug)]
149pub struct Interactive<T: FromArgMatches + Args> {
150 #[command(flatten)]
152 pub process: T,
153
154 #[command(flatten)]
156 pub opts: InteractiveOptions,
157}
158
159#[derive(Args, Debug)]
161pub struct InteractiveOptions {
162 #[arg(short = 'i', long)]
164 pub interactive: bool,
165
166 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
168 pub inline: bool,
169
170 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
172 pub full_screen: bool,
173}
174
175#[cfg(debug_assertions)]
176#[derive(Args, Debug)]
178pub struct QueryProcess {
179 #[arg(default_value = "-")]
181 pub sql: FileOrStdin,
182}
183
184#[derive(Args, Debug)]
186pub struct InitProcess {
187 #[arg(value_enum)]
189 pub shell: Shell,
190}
191
192#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
193pub enum Shell {
194 Bash,
195 Zsh,
196 Fish,
197 Powershell,
198}
199
200#[derive(Args, Debug)]
202pub struct BookmarkCommandProcess {
203 #[arg(required_unless_present = "interactive")]
207 pub command: Option<String>,
208
209 #[arg(short = 'a', long)]
211 pub alias: Option<String>,
212
213 #[arg(short = 'd', long)]
215 pub description: Option<String>,
216
217 #[arg(long)]
219 pub ai: bool,
220}
221
222#[derive(Args, Debug)]
224pub struct SearchCommandsProcess {
225 pub query: Option<String>,
227
228 #[arg(short = 'm', long)]
230 pub mode: Option<SearchMode>,
231
232 #[arg(short = 'u', long)]
234 pub user_only: bool,
235
236 #[arg(long, requires = "query")]
238 pub ai: bool,
239}
240
241#[derive(Args, Debug)]
243pub struct VariableReplaceProcess {
244 #[arg(default_value = "-")]
248 pub command: MaybeStdin<String>,
249
250 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
254 pub values: Vec<(String, Option<String>)>,
255
256 #[arg(short = 'E', long)]
265 pub use_env: bool,
266}
267
268#[derive(Args, Debug)]
270pub struct CommandFixProcess {
271 pub command: String,
273
274 #[arg(long, value_name = "HISTORY")]
278 pub history: Option<String>,
279}
280
281#[derive(Args, Clone, Debug)]
283pub struct ExportItemsProcess {
284 #[arg(default_value = "-")]
288 pub location: String,
289 #[arg(long, group = "location_type")]
291 pub file: bool,
292 #[arg(long, group = "location_type")]
294 pub http: bool,
295 #[arg(long, group = "location_type")]
297 pub gist: bool,
298 #[arg(long, value_name = "REGEX")]
302 pub filter: Option<Regex>,
303 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
308 pub headers: Vec<(HeaderName, HeaderValue)>,
309 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
313 pub method: HttpMethod,
314}
315
316#[derive(Args, Clone, Debug)]
318pub struct ImportItemsProcess {
319 #[arg(default_value = "-", required_unless_present = "history")]
323 pub location: String,
324 #[arg(long)]
326 pub ai: bool,
327 #[arg(long)]
331 pub dry_run: bool,
332 #[arg(long, group = "location_type")]
334 pub file: bool,
335 #[arg(long, group = "location_type")]
337 pub http: bool,
338 #[arg(long, group = "location_type")]
340 pub gist: bool,
341 #[arg(long, value_enum, group = "location_type", requires = "ai")]
343 pub history: Option<HistorySource>,
344 #[arg(long, value_name = "REGEX")]
348 pub filter: Option<Regex>,
349 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
354 pub tags: Vec<String>,
355 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
360 pub headers: Vec<(HeaderName, HeaderValue)>,
361 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
365 pub method: HttpMethod,
366}
367
368#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
369pub enum HistorySource {
370 Bash,
371 Zsh,
372 Fish,
373 Powershell,
374 Atuin,
375}
376
377#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
378pub enum HttpMethod {
379 GET,
380 POST,
381 PUT,
382 PATCH,
383}
384impl From<HttpMethod> for Method {
385 fn from(value: HttpMethod) -> Self {
386 match value {
387 HttpMethod::GET => Method::GET,
388 HttpMethod::POST => Method::POST,
389 HttpMethod::PUT => Method::PUT,
390 HttpMethod::PATCH => Method::PATCH,
391 }
392 }
393}
394
395#[derive(Args, Debug)]
397pub struct TldrFetchProcess {
398 pub category: Option<String>,
402
403 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
408 pub commands: Vec<String>,
409
410 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
414 pub filter_commands: Option<FileOrStdin>,
415}
416
417#[derive(Args, Debug)]
419pub struct TldrClearProcess {
420 pub category: Option<String>,
424}
425
426#[derive(Args, Debug)]
428pub struct CompletionNewProcess {
429 #[arg(short = 'c', long)]
431 pub command: Option<String>,
432 #[arg(required_unless_present = "interactive")]
434 pub variable: Option<String>,
435 #[arg(required_unless_present_any = ["interactive", "ai"])]
437 pub provider: Option<String>,
438 #[arg(long)]
440 pub ai: bool,
441}
442
443#[derive(Args, Debug)]
445pub struct CompletionDeleteProcess {
446 #[arg(short = 'c', long)]
448 pub command: Option<String>,
449 pub variable: String,
451}
452
453#[derive(Args, Debug)]
455pub struct CompletionListProcess {
456 pub command: Option<String>,
458}
459
460#[derive(Args, Debug)]
461pub struct UpdateProcess {}
462
463impl Cli {
464 #[instrument]
466 pub fn parse_extended() -> Self {
467 let mut cmd = Self::command_for_update();
469
470 let style = cmd.get_styles().clone();
472 let dimmed = style.get_placeholder().dimmed();
473 let plain_examples_header = "Examples:";
474 let styled_examples_header = format!(
475 "{}Examples:{}",
476 style.get_usage().render(),
477 style.get_usage().render_reset()
478 );
479 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
480
481 let matches = cmd.get_matches();
483
484 match Cli::from_arg_matches(&matches) {
486 Ok(args) => args,
487 Err(err) => err.exit(),
488 }
489 }
490}
491
492fn style_after_long_help(
493 command_ref: &mut Command,
494 dimmed: &Style,
495 plain_examples_header: &str,
496 styled_examples_header: &str,
497) {
498 let mut command = std::mem::take(command_ref);
499 if let Some(after_long_help) = command.get_after_long_help() {
500 let current_help_text = after_long_help.to_string();
501 let modified_help_text = current_help_text
502 .replace(plain_examples_header, styled_examples_header)
504 .lines()
506 .map(|line| {
507 if line.trim_start().starts_with('#') {
508 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
509 } else {
510 line.to_string()
511 }
512 }).join("\n");
513 command = command.after_long_help(modified_help_text);
514 }
515 for subcommand_ref in command.get_subcommands_mut() {
516 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
517 }
518 *command_ref = command;
519}
520
521fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
522 if let Some((var, value)) = env.split_once('=') {
523 Ok((var.to_owned(), Some(value.to_owned())))
524 } else {
525 Ok((env.to_owned(), None))
526 }
527}
528
529fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
530 if let Some((name, value)) = env.split_once(':') {
531 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
532 } else {
533 Err(eyre!("Missing a colon between the header name and value"))
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn test_cli_asserts() {
543 Cli::command().debug_assert()
544 }
545}