use clap::{ArgAction, Parser, Subcommand, ValueEnum};
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
#[value(rename_all = "UPPER")]
pub(crate) enum Method {
#[default]
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
impl Method {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Patch => "PATCH",
Self::Head => "HEAD",
Self::Options => "OPTIONS",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub(crate) enum OutputFormat {
#[default]
Auto,
Json,
Text,
Raw,
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub(crate) enum ColorMode {
#[default]
Auto,
On,
Off,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Commands {
#[command(subcommand)]
Dl(DlCommands),
}
#[derive(Debug, Subcommand)]
pub(crate) enum DlCommands {
Add {
url: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long, default_value = "normal")]
priority: String,
},
Pause {
id: String,
},
Resume {
id: String,
},
Remove {
id: String,
},
List,
Status {
id: String,
},
}
#[derive(Debug, Parser)]
#[command(
name = "hpx",
about = "High-performance HTTP client and download engine",
disable_help_flag = true,
after_help = "Examples:\n hpx httpbin.org/get\n hpx -X POST httpbin.org/post -d '{\"key\":\"value\"}'\n hpx -H 'Authorization: Bearer token' https://api.example.com\n hpx ws://echo.websocket.org"
)]
pub(crate) struct Cli {
pub url: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(short = 'X', long = "method", value_enum, default_value_t)]
pub method: Method,
#[arg(short = 'H', long = "header", value_name = "NAME:VALUE")]
pub headers: Vec<String>,
#[arg(short = 'd', long = "data", value_name = "[@]VALUE")]
pub data: Option<String>,
#[arg(short = 'j', long = "json", value_name = "[@]VALUE")]
pub json: Option<String>,
#[arg(short = 'o', long = "output", value_name = "PATH")]
pub output: Option<String>,
#[arg(long, value_name = "USER:PASS")]
pub basic: Option<String>,
#[arg(long, value_name = "TOKEN")]
pub bearer: Option<String>,
#[arg(short = 't', long, value_name = "SECONDS")]
pub timeout: Option<f64>,
#[arg(long, value_name = "NUM")]
pub redirects: Option<usize>,
#[arg(long, value_enum, default_value_t)]
pub format: OutputFormat,
#[arg(long, value_enum, default_value_t)]
pub color: ColorMode,
#[arg(short = 'v', long, action = ArgAction::Count)]
pub verbose: u8,
#[arg(short = 's', long)]
pub silent: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(short = 'L', long)]
pub follow: bool,
#[arg(short = 'T', long = "timing")]
pub timing: bool,
#[arg(short = 'f', long = "form", value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
pub form: Vec<String>,
#[arg(long = "multipart", value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
pub multipart: Vec<String>,
#[arg(long = "multipart-file", value_name = "KEY=@PATH", action = clap::ArgAction::Append)]
pub multipart_file: Vec<String>,
#[arg(long = "cookie", value_name = "NAME=VALUE")]
pub cookie: Vec<String>,
#[arg(long = "cookie-jar", value_name = "PATH")]
pub cookie_jar: Option<String>,
#[arg(long, value_name = "URL")]
pub proxy: Option<String>,
#[arg(long, value_name = "NUM", default_value = "0")]
pub retry: u32,
#[arg(long)]
pub clipboard: bool,
#[arg(short = 'V', long)]
pub version: bool,
#[arg(short = 'h', long)]
pub help: bool,
}
impl Cli {
pub(crate) fn is_websocket_url(&self) -> bool {
self.url.as_deref().is_some_and(|u| {
let lower = u.to_ascii_lowercase();
lower.starts_with("ws://") || lower.starts_with("wss://")
})
}
pub(crate) fn parsed_headers(&self) -> Vec<(String, String)> {
self.headers
.iter()
.filter_map(|h| {
let (name, value) = h.split_once(':')?;
Some((name.trim().to_string(), value.trim().to_string()))
})
.collect()
}
pub(crate) fn parsed_form_fields(&self) -> Vec<(String, String)> {
self.form
.iter()
.filter_map(|f| {
let (key, value) = f.split_once('=')?;
Some((key.to_string(), value.to_string()))
})
.collect()
}
pub(crate) fn parsed_multipart_fields(&self) -> Vec<(String, String)> {
self.multipart
.iter()
.filter_map(|f| {
let (key, value) = f.split_once('=')?;
Some((key.to_string(), value.to_string()))
})
.collect()
}
pub(crate) fn parsed_multipart_files(&self) -> Vec<(String, String)> {
self.multipart_file
.iter()
.filter_map(|f| {
let (key, value) = f.split_once('=')?;
Some((key.to_string(), value.strip_prefix('@')?.to_string()))
})
.collect()
}
pub(crate) fn parsed_cookies(&self) -> Vec<(String, String)> {
self.cookie
.iter()
.filter_map(|c| {
let (name, value) = c.split_once('=')?;
Some((name.trim().to_string(), value.trim().to_string()))
})
.collect()
}
}