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(test, 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(test, 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 Config(ConfigProcess),
74
75 Logs(LogsProcess),
77
78 #[command(after_long_help = include_str!("_examples/new.txt"))]
80 New(Interactive<BookmarkCommandProcess>),
81
82 #[command(after_long_help = include_str!("_examples/search.txt"))]
84 Search(Interactive<SearchCommandsProcess>),
85
86 #[command(after_long_help = include_str!("_examples/replace.txt"))]
92 Replace(Interactive<VariableReplaceProcess>),
93
94 #[command(after_long_help = include_str!("_examples/fix.txt"))]
99 Fix(CommandFixProcess),
100
101 #[command(after_long_help = include_str!("_examples/export.txt"))]
105 Export(Interactive<ExportItemsProcess>),
106
107 #[command(after_long_help = include_str!("_examples/import.txt"))]
109 Import(Interactive<ImportItemsProcess>),
110
111 #[cfg(feature = "tldr")]
112 #[command(name = "tldr", subcommand)]
114 Tldr(TldrProcess),
115
116 #[command(subcommand)]
118 Completion(CompletionProcess),
119
120 #[cfg(feature = "self-update")]
121 Update(UpdateProcess),
123}
124
125#[cfg(feature = "tldr")]
126#[derive(Subcommand)]
127#[cfg_attr(test, derive(Debug))]
128pub enum TldrProcess {
129 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
133 Fetch(TldrFetchProcess),
134
135 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
137 Clear(TldrClearProcess),
138}
139
140#[derive(Subcommand)]
141#[cfg_attr(test, derive(Debug))]
142pub enum CompletionProcess {
143 #[command(after_long_help = include_str!("_examples/completion_new.txt"))]
145 New(Interactive<CompletionNewProcess>),
146 #[command(after_long_help = include_str!("_examples/completion_delete.txt"))]
148 Delete(CompletionDeleteProcess),
149 #[command(alias = "ls", after_long_help = include_str!("_examples/completion_list.txt"))]
151 List(Interactive<CompletionListProcess>),
152}
153
154#[derive(Args, Debug)]
158pub struct Interactive<T: FromArgMatches + Args> {
159 #[command(flatten)]
161 pub process: T,
162
163 #[command(flatten)]
165 pub opts: InteractiveOptions,
166}
167
168#[derive(Args, Debug)]
170pub struct InteractiveOptions {
171 #[arg(short = 'i', long)]
173 pub interactive: bool,
174
175 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
177 pub inline: bool,
178
179 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
181 pub full_screen: bool,
182}
183
184#[cfg(debug_assertions)]
185#[derive(Args, Debug)]
187pub struct QueryProcess {
188 #[arg(default_value = "-")]
190 pub sql: FileOrStdin,
191}
192
193#[derive(Args, Debug)]
195pub struct InitProcess {
196 #[arg(value_enum)]
198 pub shell: Shell,
199}
200
201#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
202pub enum Shell {
203 Bash,
204 Zsh,
205 Fish,
206 #[value(alias("pwsh"))]
207 Powershell,
208 #[value(alias("nu"))]
209 Nushell,
210}
211
212#[derive(Args, Debug)]
214pub struct ConfigProcess {
215 #[arg(short = 'p', long)]
217 pub path: bool,
218}
219
220#[derive(Args, Debug)]
222pub struct LogsProcess {
223 #[arg(short = 'p', long)]
225 pub path: bool,
226}
227
228#[derive(Args, Debug)]
230pub struct BookmarkCommandProcess {
231 #[arg(required_unless_present = "interactive")]
235 pub command: Option<String>,
236
237 #[arg(short = 'a', long)]
239 pub alias: Option<String>,
240
241 #[arg(short = 'd', long)]
243 pub description: Option<String>,
244
245 #[arg(long)]
247 pub ai: bool,
248}
249
250#[derive(Args, Debug)]
252pub struct SearchCommandsProcess {
253 pub query: Option<String>,
255
256 #[arg(short = 'm', long)]
258 pub mode: Option<SearchMode>,
259
260 #[arg(short = 'u', long)]
262 pub user_only: bool,
263
264 #[arg(long, requires = "query")]
266 pub ai: bool,
267}
268
269#[derive(Args, Debug)]
271pub struct VariableReplaceProcess {
272 #[arg(default_value = "-")]
276 pub command: MaybeStdin<String>,
277
278 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
282 pub values: Vec<(String, Option<String>)>,
283
284 #[arg(short = 'E', long)]
293 pub use_env: bool,
294}
295
296#[derive(Args, Debug)]
298pub struct CommandFixProcess {
299 pub command: String,
301
302 #[arg(long, value_name = "HISTORY")]
306 pub history: Option<String>,
307}
308
309#[derive(Args, Clone, Debug)]
311pub struct ExportItemsProcess {
312 #[arg(default_value = "-")]
316 pub location: String,
317 #[arg(long, group = "location_type")]
319 pub file: bool,
320 #[arg(long, group = "location_type")]
322 pub http: bool,
323 #[arg(long, group = "location_type")]
325 pub gist: bool,
326 #[arg(long, value_name = "REGEX")]
330 pub filter: Option<Regex>,
331 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
336 pub headers: Vec<(HeaderName, HeaderValue)>,
337 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
341 pub method: HttpMethod,
342}
343
344#[derive(Args, Clone, Debug)]
346pub struct ImportItemsProcess {
347 #[arg(default_value = "-", required_unless_present = "history")]
351 pub location: String,
352 #[arg(long)]
354 pub ai: bool,
355 #[arg(long)]
359 pub dry_run: bool,
360 #[arg(long, group = "location_type")]
362 pub file: bool,
363 #[arg(long, group = "location_type")]
365 pub http: bool,
366 #[arg(long, group = "location_type")]
368 pub gist: bool,
369 #[arg(long, value_enum, group = "location_type", requires = "ai")]
371 pub history: Option<HistorySource>,
372 #[arg(long, value_name = "REGEX")]
376 pub filter: Option<Regex>,
377 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
382 pub tags: Vec<String>,
383 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
388 pub headers: Vec<(HeaderName, HeaderValue)>,
389 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
393 pub method: HttpMethod,
394}
395
396#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
397pub enum HistorySource {
398 Bash,
399 Zsh,
400 Fish,
401 #[value(alias("pwsh"))]
402 Powershell,
403 #[value(alias("nu"))]
404 Nushell,
405 Atuin,
406}
407
408#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
409pub enum HttpMethod {
410 GET,
411 POST,
412 PUT,
413 PATCH,
414}
415impl From<HttpMethod> for Method {
416 fn from(value: HttpMethod) -> Self {
417 match value {
418 HttpMethod::GET => Method::GET,
419 HttpMethod::POST => Method::POST,
420 HttpMethod::PUT => Method::PUT,
421 HttpMethod::PATCH => Method::PATCH,
422 }
423 }
424}
425
426#[cfg(feature = "tldr")]
427#[derive(Args, Debug)]
429pub struct TldrFetchProcess {
430 pub category: Option<String>,
434
435 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
440 pub commands: Vec<String>,
441
442 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
446 pub filter_commands: Option<FileOrStdin>,
447}
448
449#[cfg(feature = "tldr")]
450#[derive(Args, Debug)]
452pub struct TldrClearProcess {
453 pub category: Option<String>,
457}
458
459#[derive(Args, Debug)]
461pub struct CompletionNewProcess {
462 #[arg(short = 'c', long)]
464 pub command: Option<String>,
465 #[arg(required_unless_present = "interactive")]
467 pub variable: Option<String>,
468 #[arg(required_unless_present_any = ["interactive", "ai"])]
470 pub provider: Option<String>,
471 #[arg(long)]
473 pub ai: bool,
474}
475
476#[derive(Args, Debug)]
478pub struct CompletionDeleteProcess {
479 #[arg(short = 'c', long)]
481 pub command: Option<String>,
482 pub variable: String,
484}
485
486#[derive(Args, Debug)]
488pub struct CompletionListProcess {
489 pub command: Option<String>,
491}
492
493#[cfg(feature = "self-update")]
494#[derive(Args, Debug)]
495pub struct UpdateProcess {}
496
497impl Cli {
498 #[instrument]
500 pub fn parse_extended() -> Self {
501 let mut cmd = Self::command_for_update();
503
504 let style = cmd.get_styles().clone();
506 let dimmed = style.get_placeholder().dimmed();
507 let plain_examples_header = "Examples:";
508 let styled_examples_header = format!(
509 "{}Examples:{}",
510 style.get_usage().render(),
511 style.get_usage().render_reset()
512 );
513 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
514
515 let matches = cmd.get_matches();
517
518 match Cli::from_arg_matches(&matches) {
520 Ok(args) => args,
521 Err(err) => err.exit(),
522 }
523 }
524}
525
526fn style_after_long_help(
527 command_ref: &mut Command,
528 dimmed: &Style,
529 plain_examples_header: &str,
530 styled_examples_header: &str,
531) {
532 let mut command = std::mem::take(command_ref);
533 if let Some(after_long_help) = command.get_after_long_help() {
534 let current_help_text = after_long_help.to_string();
535 let modified_help_text = current_help_text
536 .replace(plain_examples_header, styled_examples_header)
538 .lines()
540 .map(|line| {
541 if line.trim_start().starts_with('#') {
542 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
543 } else {
544 line.to_string()
545 }
546 }).join("\n");
547 command = command.after_long_help(modified_help_text);
548 }
549 for subcommand_ref in command.get_subcommands_mut() {
550 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
551 }
552 *command_ref = command;
553}
554
555fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
556 if let Some((var, value)) = env.split_once('=') {
557 Ok((var.to_owned(), Some(value.to_owned())))
558 } else {
559 Ok((env.to_owned(), None))
560 }
561}
562
563fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
564 if let Some((name, value)) = env.split_once(':') {
565 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
566 } else {
567 Err(eyre!("Missing a colon between the header name and value"))
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn test_cli_asserts() {
577 Cli::command().debug_assert()
578 }
579}