use clap::Parser;
mod cli;
mod commands;
mod connection_meta;
mod daemon;
mod dispatch;
mod error;
mod hints;
mod output;
mod output_controls;
mod output_pipeline;
mod port_owner;
mod tab_target;
use cli::Cli;
use error::AppError;
fn is_type_invocation(args: &[String]) -> bool {
const VALUE_GLOBALS: &[&str] = &[
"--host",
"--port",
"--tab",
"--tab-id",
"--jq",
"--timeout",
"--daemon-timeout",
"--limit",
"--sort",
"--fields",
"--format",
];
let mut iter = args.iter().skip(1); while let Some(a) = iter.next() {
if a == "--" {
break;
}
if let Some(stripped) = a.strip_prefix("--") {
if stripped.contains('=') {
continue;
}
if VALUE_GLOBALS.contains(&a.as_str()) {
let _ = iter.next();
}
continue;
}
return a == "type";
}
false
}
fn main() {
let argv: Vec<String> = std::env::args().collect();
let cli = match Cli::try_parse_from(&argv) {
Ok(cli) => cli,
Err(err) => {
use clap::error::ErrorKind;
let kind = err.kind();
let is_help_or_version =
matches!(kind, ErrorKind::DisplayHelp | ErrorKind::DisplayVersion);
let attach_type_hint = matches!(
kind,
ErrorKind::UnknownArgument | ErrorKind::InvalidSubcommand
) && is_type_invocation(&argv);
err.print().ok();
if attach_type_hint {
eprintln!(
"\nhint: `type` takes selector and text positionally — try `ff-rdp type 'input[type=search]' 'Krankenkasse'`."
);
eprintln!(
" The --selector/--text flag form also works: `ff-rdp type --selector 'input[type=search]' --text 'Krankenkasse'`."
);
}
if is_help_or_version {
std::process::exit(0);
} else {
std::process::exit(2);
}
}
};
let result = dispatch::dispatch(&cli);
match result {
Ok(()) => {}
Err(AppError::User(msg)) => {
eprintln!("error: {msg}");
std::process::exit(1);
}
Err(AppError::Internal(err)) => {
eprintln!("internal error: {err:#}");
std::process::exit(2);
}
Err(AppError::Exit(code)) => {
std::process::exit(code);
}
}
}
#[cfg(test)]
mod main_tests {
use super::is_type_invocation;
#[test]
fn detects_type_subcommand() {
let args: Vec<String> = ["ff-rdp", "type", "input", "hi"]
.iter()
.map(ToString::to_string)
.collect();
assert!(is_type_invocation(&args));
}
#[test]
fn detects_type_after_global_flags() {
let args: Vec<String> = ["ff-rdp", "--port", "6000", "type", "--bogus"]
.iter()
.map(ToString::to_string)
.collect();
assert!(is_type_invocation(&args));
}
#[test]
fn detects_type_with_eq_global_flag() {
let args: Vec<String> = ["ff-rdp", "--port=6000", "type", "--bogus"]
.iter()
.map(ToString::to_string)
.collect();
assert!(is_type_invocation(&args));
}
#[test]
fn rejects_other_subcommand() {
let args: Vec<String> = ["ff-rdp", "click", "input"]
.iter()
.map(ToString::to_string)
.collect();
assert!(!is_type_invocation(&args));
}
#[test]
fn rejects_no_subcommand() {
let args: Vec<String> = ["ff-rdp", "--port", "6000"]
.iter()
.map(ToString::to_string)
.collect();
assert!(!is_type_invocation(&args));
}
#[test]
fn detects_type_after_boolean_global_flag() {
let args: Vec<String> = ["ff-rdp", "--no-daemon", "type", "--bogus"]
.iter()
.map(ToString::to_string)
.collect();
assert!(is_type_invocation(&args));
}
#[test]
fn detects_type_after_mixed_globals() {
let args: Vec<String> = [
"ff-rdp",
"--no-daemon",
"--port",
"6000",
"--detail",
"type",
"--bogus",
]
.iter()
.map(ToString::to_string)
.collect();
assert!(is_type_invocation(&args));
}
}