Repository: fujiapple852/trippy (language: rust)

Issue:
Improve error messages
**Describe the feature you'd like**

## --min-round-duration and --max-round-duration

Sub-second float values for min and max round duration would be extremely useful.  When packet loss is 1% or 2%, it takes a while to send enough packets at 1-second round-trip values.  Allowing lower values would substantially increase the speed of detecting the actual packet loss percentage.

If --max-round-duration is set lower than --min-round-duration, --min-round-duration should automatically be set to the requested --max-round-duration value.


Discussion / hints:
@mpenning both of these settings can be specified in various time units including ms, see example:

https://github.com/fujiapple852/trippy?tab=readme-ov-file#usage-examples

```shell
trip example.com -i 250ms -g 50ms
``` 

Aside: note the grace period setting exists, which you want wish to tweak as well to fully saturate the line.

Perhaps the Trippy help output could be improved to make this more discoverable?
Regarding:

> If --max-round-duration is set lower than --min-round-duration, --min-round-duration should automatically be set to the requested --max-round-duration value.

The current behaviour is to validate these values and error out if the max is lower than the min, which I think is reasonable behaviour.  In general I prefer to avoid any “surprising” behaviour where the tool does something the user did not request.
> @mpenning both of these settings can be specified in various time units including ms, see example:
> 
> https://github.com/fujiapple852/trippy?tab=readme-ov-file#usage-examples
> 
> ```shell
> trip example.com -i 250ms -g 50ms
> ```
> 
> Aside: note the grace period setting exists, which you want wish to tweak as well to fully saturate the line.
> 
> Perhaps the Trippy help output could be improved to make this more discoverable?

Yes, specifically I filed this bug after trying this:

```none
$ trip -i 0.05 4.2.2.2
Error: invalid character at 1
$
```

I mistakenly assumed that a sub-second value was not possible... if you could return the same error as this one, it would be more clear...

```none
$ trip -i 2 4.2.2.2
Error: time unit needed, for example 2sec or 2ms
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22012b42f..f43c2bdb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 ### Changed
 
 - Separate library and binary crates ([#1141](https://github.com/fujiapple852/trippy/issues/1141))
+- Improved error messages ([#1150](https://github.com/fujiapple852/trippy/issues/1150))
 
 ### Fixed
 
diff --git a/Cargo.lock b/Cargo.lock
index c73117afa..18cb7871b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -271,6 +271,7 @@ dependencies = [
  "anstream",
  "anstyle",
  "clap_lex",
+ "strsim",
  "terminal_size",
 ]
 
diff --git a/crates/trippy-core/src/error.rs b/crates/trippy-core/src/error.rs
index cc6e74009..cfff3b793 100644
--- a/crates/trippy-core/src/error.rs
+++ b/crates/trippy-core/src/error.rs
@@ -23,9 +23,9 @@ pub enum TracerError {
     IoError(#[from] IoError),
     #[error("insufficient buffer capacity")]
     InsufficientCapacity,
-    #[error("address not available: {0}")]
+    #[error("address {0} not available")]
     AddressNotAvailable(SocketAddr),
-    #[error("invalid source IP address: {0}")]
+    #[error("source IP address {0} could not be bound")]
     InvalidSourceAddr(IpAddr),
     #[error("missing address from socket call")]
     MissingAddr,
diff --git a/crates/trippy/Cargo.toml b/crates/trippy/Cargo.toml
index 73f41a608..8b9f53037 100644
--- a/crates/trippy/Cargo.toml
+++ b/crates/trippy/Cargo.toml
@@ -29,7 +29,7 @@ tracing.workspace = true
 serde.workspace = true
 derive_more.workspace = true
 parking_lot.workspace = true
-clap = { version = "4.4.0",  default-features = false, features = [ "cargo", "derive", "wrap_help", "usage", "unstable-styles" ] }
+clap = { version = "4.4.0",  default-features = false, features = [ "cargo", "derive", "wrap_help", "usage", "unstable-styles", "color", "suggestions", "error-context" ] }
 clap_complete = "4.4.9"
 humantime = "2.1.0"
 ratatui = "0.26.3"
diff --git a/crates/trippy/src/config.rs b/crates/trippy/src/config.rs
index 8b17e7bd1..563e28beb 100644
--- a/crates/trippy/src/config.rs
+++ b/crates/trippy/src/config.rs
@@ -2,12 +2,10 @@ use anyhow::anyhow;
 use clap::ValueEnum;
 use clap_complete::Shell;
 use file::ConfigFile;
-use humantime::format_duration;
 use itertools::Itertools;
 use serde::Deserialize;
 use std::collections::HashMap;
 use std::net::IpAddr;
-use std::str::FromStr;
 use std::time::Duration;
 use trippy_core::{
     defaults, IcmpExtensionParseMode, MultipathStrategy, PortDirection, PrivilegeMode, Protocol,
@@ -409,17 +407,17 @@ impl TrippyConfig {
         );
         let target_port = cfg_layer_opt(args.target_port, cfg_file_strategy.target_port);
         let source_port = cfg_layer_opt(args.source_port, cfg_file_strategy.source_port);
-        let source_address = cfg_layer_opt(args.source_address, cfg_file_strategy.source_address);
+        let source_addr = cfg_layer_opt(args.source_address, cfg_file_strategy.source_address);
         let interface = cfg_layer_opt(args.interface, cfg_file_strategy.interface);
         let min_round_duration = cfg_layer(
             args.min_round_duration,
             cfg_file_strategy.min_round_duration,
-            format_duration(defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION).to_string(),
+            defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION,
         );
         let max_round_duration = cfg_layer(
             args.max_round_duration,
             cfg_file_strategy.max_round_duration,
-            format_duration(defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION).to_string(),
+            defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION,
         );
         let initial_sequence = cfg_layer(
             args.initial_sequence,
@@ -434,7 +432,7 @@ impl TrippyConfig {
         let grace_duration = cfg_layer(
             args.grace_duration,
             cfg_file_strategy.grace_duration,
-            format_duration(defaults::DEFAULT_STRATEGY_GRACE_DURATION).to_string(),
+            defaults::DEFAULT_STRATEGY_GRACE_DURATION,
         );
         let max_inflight = cfg_layer(
             args.max_inflight,
@@ -479,7 +477,7 @@ impl TrippyConfig {
         let read_timeout = cfg_layer(
             args.read_timeout,
             cfg_file_strategy.read_timeout,
-            format_duration(defaults::DEFAULT_STRATEGY_READ_TIMEOUT).to_string(),
+            defaults::DEFAULT_STRATEGY_READ_TIMEOUT,
         );
         let tui_max_samples = cfg_layer(
             args.tui_max_samples,
@@ -499,7 +497,7 @@ impl TrippyConfig {
         let tui_refresh_rate = cfg_layer(
             args.tui_refresh_rate,
             cfg_file_tui.tui_refresh_rate,
-            String::from(constants::DEFAULT_TUI_REFRESH_RATE),
+            constants::DEFAULT_TUI_REFRESH_RATE,
         );
         let tui_privacy_max_ttl = cfg_layer(
             args.tui_privacy_max_ttl,
@@ -546,7 +544,7 @@ impl TrippyConfig {
         let dns_timeout = cfg_layer(
             args.dns_timeout,
             cfg_file_dns.dns_timeout,
-            String::from(constants::DEFAULT_DNS_TIMEOUT),
+            constants::DEFAULT_DNS_TIMEOUT,
         );
         let report_cycles = cfg_layer(
             args.report_cycles,
@@ -559,18 +557,6 @@ impl TrippyConfig {
             (false, false, false, ProtocolConfig::Tcp) | (_, true, _, _) => Protocol::Tcp,
             (false, false, false, ProtocolConfig::Icmp) | (_, _, true, _) => Protocol::Icmp,
         };
-        let read_timeout = humantime::parse_duration(&read_timeout)?;
-        let min_round_duration = humantime::parse_duration(&min_round_duration)?;
-        let max_round_duration = humantime::parse_duration(&max_round_duration)?;
-        let grace_duration = humantime::parse_duration(&grace_duration)?;
-        let source_addr = source_address
-            .as_ref()
-            .map(|addr| {
-                IpAddr::from_str(addr)
-                    .map_err(|_| anyhow!("invalid source IP address format: {}", addr))
-            })
-            .transpose()?;
-
         #[allow(clippy::match_same_arms)]
         let addr_family = match (
             args.ipv4,
@@ -622,14 +608,12 @@ impl TrippyConfig {
                 ));
             }
         };
-        let tui_refresh_rate = humantime::parse_duration(&tui_refresh_rate)?;
         let dns_resolve_method = match dns_resolve_method_config {
             DnsResolveMethodConfig::System => ResolveMethod::System,
             DnsResolveMethodConfig::Resolv => ResolveMethod::Resolv,
             DnsResolveMethodConfig::Google => ResolveMethod::Google,
             DnsResolveMethodConfig::Cloudflare => ResolveMethod::Cloudflare,
         };
-        let dns_timeout = humantime::parse_duration(&dns_timeout)?;
         let max_rounds = match mode {
             Mode::Stream | Mode::Tui => None,
             Mode::Pretty
@@ -744,13 +728,13 @@ impl Default for TrippyConfig {
             interface: None,
             multipath_strategy: defaults::DEFAULT_STRATEGY_MULTIPATH,
             port_direction: PortDirection::None,
-            dns_timeout: duration(constants::DEFAULT_DNS_TIMEOUT),
+            dns_timeout: constants::DEFAULT_DNS_TIMEOUT,
             dns_resolve_method: dns_resolve_method(constants::DEFAULT_DNS_RESOLVE_METHOD),
             dns_lookup_as_info: constants::DEFAULT_DNS_LOOKUP_AS_INFO,
             tui_max_samples: constants::DEFAULT_TUI_MAX_SAMPLES,
             tui_max_flows: constants::DEFAULT_TUI_MAX_FLOWS,
             tui_preserve_screen: constants::DEFAULT_TUI_PRESERVE_SCREEN,
-            tui_refresh_rate: duration(constants::DEFAULT_TUI_REFRESH_RATE),
+            tui_refresh_rate: constants::DEFAULT_TUI_REFRESH_RATE,
             tui_privacy_max_ttl: constants::DEFAULT_TUI_PRIVACY_MAX_TTL,
             tui_address_mode: constants::DEFAULT_TUI_ADDRESS_MODE,
             tui_as_mode: constants::DEFAULT_TUI_AS_MODE,
@@ -774,10 +758,6 @@ impl Default for TrippyConfig {
     }
 }
 
-fn duration(duration: &str) -> Duration {
-    humantime::parse_duration(duration).expect("valid duration")
-}
-
 fn dns_resolve_method(dns_resolve_method: DnsResolveMethodConfig) -> ResolveMethod {
     match dns_resolve_method {
         DnsResolveMethodConfig::System => ResolveMethod::System,
@@ -1118,6 +1098,7 @@ mod tests {
     use super::*;
     use crossterm::event::KeyCode;
     use std::net::{Ipv4Addr, Ipv6Addr};
+    use std::str::FromStr;
     use test_case::test_case;
     use trippy_core::Port;
 
@@ -1174,7 +1155,7 @@ mod tests {
     #[test_case("trip example.com --mode flows --udp -R paris", Ok(cfg().mode(Mode::Flows).max_rounds(Some(10)).multipath_strategy(MultipathStrategy::Paris).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "flows mode")]
     #[test_case("trip example.com --mode silent", Ok(cfg().mode(Mode::Silent).max_rounds(Some(10)).build()); "silent mode")]
     #[test_case("trip example.com -m tui", Ok(cfg().mode(Mode::Tui).build()); "tui mode short")]
-    #[test_case("trip example.com --mode foo", Err(anyhow!(format!("error: one of the values isn't valid for an argument"))); "invalid mode")]
+    #[test_case("trip example.com --mode foo", Err(anyhow!(format!("error: invalid value 'foo' for '--mode <MODE>' [possible values: tui, stream, pretty, markdown, csv, json, dot, flows, silent] For more information, try '--help'."))); "invalid mode")]
     #[test_case("trip example.com --mode dot", Err(anyhow!(format!("this mode requires the paris or dublin multipath strategy"))); "invalid dot mode")]
     #[test_case("trip example.com --mode flows", Err(anyhow!(format!("this mode requires the paris or dublin multipath strategy"))); "invalid flows mode")]
     fn test_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
@@ -1192,9 +1173,9 @@ mod tests {
         compare(parse_config(cmd), expected);
     }
 
-    #[test_case("trip example.com --dummy", Err(anyhow!("error: unexpected argument found")); "invalid argument")]
+    #[test_case("trip example.com --dummy", Err(anyhow!("error: unexpected argument '--dummy' found")); "invalid argument")]
     fn test_unexpected(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
-        compare(parse_config(cmd), expected);
+        compare_lines(parse_config(cmd), expected, Some(1));
     }
 
     #[test_case("trip example.com", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "default strategy")]
@@ -1202,7 +1183,7 @@ mod tests {
     #[test_case("trip example.com -R classic", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "classic strategy short")]
     #[test_case("trip example.com --multipath-strategy paris --udp", Ok(cfg().multipath_strategy(MultipathStrategy::Paris).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "paris strategy")]
     #[test_case("trip example.com --multipath-strategy dublin --udp", Ok(cfg().multipath_strategy(MultipathStrategy::Dublin).protocol(Protocol::Udp).addr_family(IpAddrFamily::Ipv4Only).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "dublin strategy")]
-    #[test_case("trip example.com --multipath-strategy tokyo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid strategy")]
+    #[test_case("trip example.com --multipath-strategy tokyo", Err(anyhow!("error: invalid value 'tokyo' for '--multipath-strategy <MULTIPATH_STRATEGY>' [possible values: classic, paris, dublin] For more information, try '--help'.")); "invalid strategy")]
     #[test_case("trip example.com --icmp --multipath-strategy paris", Err(anyhow!("Paris multipath strategy not support for icmp")); "paris with invalid protocol icmp")]
     #[test_case("trip example.com --icmp --multipath-strategy dublin", Err(anyhow!("Dublin multipath strategy not support for icmp")); "dublin with invalid protocol icmp")]
     #[test_case("trip example.com --tcp --multipath-strategy paris", Err(anyhow!("Paris multipath strategy not yet supported for tcp")); "paris with invalid protocol tcp")]
@@ -1215,11 +1196,11 @@ mod tests {
     #[test_case("trip example.com --protocol icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol")]
     #[test_case("trip example.com --protocol udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol")]
     #[test_case("trip example.com --protocol tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol")]
-    #[test_case("trip example.com --protocol foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid protocol")]
+    #[test_case("trip example.com --protocol foo", Err(anyhow!("error: invalid value 'foo' for '--protocol <PROTOCOL>' [possible values: icmp, udp, tcp] For more information, try '--help'.")); "invalid protocol")]
     #[test_case("trip example.com -p icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol short")]
     #[test_case("trip example.com -p udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol short")]
     #[test_case("trip example.com -p tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol short")]
-    #[test_case("trip example.com -p foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid protocol short")]
+    #[test_case("trip example.com -p foo", Err(anyhow!("error: invalid value 'foo' for '--protocol <PROTOCOL>' [possible values: icmp, udp, tcp] For more information, try '--help'.")); "invalid protocol short")]
     #[test_case("trip example.com --icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol shortcut")]
     #[test_case("trip example.com --udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol shortcut")]
     #[test_case("trip example.com --tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol shortcut")]
@@ -1253,10 +1234,10 @@ mod tests {
     #[test_case("trip example.com --addr-family ipv4-then-ipv6", Ok(cfg().addr_family(IpAddrFamily::Ipv4thenIpv6).build()); "ipv4 then ipv6 address family")]
     #[test_case("trip example.com --addr-family ipv6-then-ipv4", Ok(cfg().addr_family(IpAddrFamily::Ipv6thenIpv4).build()); "ipv6 then ipv4 address family")]
     #[test_case("trip example.com -F ipv4", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); "custom address family short")]
-    #[test_case("trip example.com --addr-family foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid address family")]
+    #[test_case("trip example.com --addr-family foo", Err(anyhow!("error: invalid value 'foo' for '--addr-family <ADDR_FAMILY>' [possible values: ipv4, ipv6, ipv6-then-ipv4, ipv4-then-ipv6] For more information, try '--help'.")); "invalid address family")]
     #[test_case("trip example.com -4", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); "ipv4 address family shortcut")]
     #[test_case("trip example.com -6", Ok(cfg().addr_family(IpAddrFamily::Ipv6Only).build()); "ipv6 address family shortcut")]
-    #[test_case("trip example.com -5", Err(anyhow!("error: unexpected argument found")); "invalid address family shortcut")]
+    #[test_case("trip example.com -5", Err(anyhow!("error: unexpected argument '-5' found tip: to pass '-5' as a value, use '-- -5' Usage: trip [OPTIONS] [TARGETS]... For more information, try '--help'.")); "invalid address family shortcut")]
     fn test_addr_family(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1265,12 +1246,12 @@ mod tests {
     #[test_case("trip example.com --first-ttl 5", Ok(cfg().first_ttl(5).build()); "custom first ttl")]
     #[test_case("trip example.com -f 5", Ok(cfg().first_ttl(5).build()); "custom first ttl short")]
     #[test_case("trip example.com --first-ttl 0", Err(anyhow!("first-ttl (0) must be in the range 1..255")); "invalid low first ttl")]
-    #[test_case("trip example.com --first-ttl 500", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high first ttl")]
+    #[test_case("trip example.com --first-ttl 500", Err(anyhow!("error: invalid value '500' for '--first-ttl <FIRST_TTL>': 500 is not in 0..=255 For more information, try '--help'.")); "invalid high first ttl")]
     #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default max ttl")]
     #[test_case("trip example.com --max-ttl 5", Ok(cfg().max_ttl(5).build()); "custom max ttl")]
     #[test_case("trip example.com -t 5", Ok(cfg().max_ttl(5).build()); "custom max ttl short")]
     #[test_case("trip example.com --max-ttl 0", Err(anyhow!("max-ttl (0) must be in the range 1..255")); "invalid low max ttl")]
-    #[test_case("trip example.com --max-ttl 500", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high max ttl")]
+    #[test_case("trip example.com --max-ttl 500", Err(anyhow!("error: invalid value '500' for '--max-ttl <MAX_TTL>': 500 is not in 0..=255 For more information, try '--help'.")); "invalid high max ttl")]
     #[test_case("trip example.com --first-ttl 3 --max-ttl 2", Err(anyhow!("first-ttl (3) must be less than or equal to max-ttl (2)")); "first ttl higher than max ttl")]
     #[test_case("trip example.com --first-ttl 5 --max-ttl 5", Ok(cfg().first_ttl(5).max_ttl(5).build()); "custom first and max ttl")]
     fn test_ttl(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
@@ -1280,11 +1261,11 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); "default min round duration")]
     #[test_case("trip example.com --min-round-duration 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); "custom min round duration")]
     #[test_case("trip example.com -i 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); "custom min round duration short")]
-    #[test_case("trip example.com --min-round-duration 0", Err(anyhow!("time unit needed, for example 0sec or 0ms")); "invalid format min round duration")]
+    #[test_case("trip example.com --min-round-duration 0", Err(anyhow!("error: invalid value '0' for '--min-round-duration <MIN_ROUND_DURATION>': time unit needed, for example 0sec or 0ms For more information, try '--help'.")); "invalid format min round duration")]
     #[test_case("trip example.com", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); "default max round duration")]
     #[test_case("trip example.com --max-round-duration 1250ms", Ok(cfg().max_round_duration(Duration::from_millis(1250)).build()); "custom max round duration")]
     #[test_case("trip example.com -T 2s", Ok(cfg().max_round_duration(Duration::from_millis(2000)).build()); "custom max round duration short")]
-    #[test_case("trip example.com --min-round-duration 0", Err(anyhow!("time unit needed, for example 0sec or 0ms")); "invalid format max round duration")]
+    #[test_case("trip example.com --max-round-duration 0", Err(anyhow!("error: invalid value '0' for '--max-round-duration <MAX_ROUND_DURATION>': time unit needed, for example 0sec or 0ms For more information, try '--help'.")); "invalid format max round duration")]
     #[test_case("trip example.com -i 250ms -T 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).max_round_duration(Duration::from_millis(250)).build()); "custom min and max round duration")]
     #[test_case("trip example.com -i 300ms -T 250ms", Err(anyhow!("max-round-duration (250ms) must not be less than min-round-duration (300ms)")); "min round duration greater than max")]
     fn test_round_duration(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
@@ -1294,7 +1275,7 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().grace_duration(Duration::from_millis(100)).build()); "default grace duration")]
     #[test_case("trip example.com --grace-duration 10ms", Ok(cfg().grace_duration(Duration::from_millis(10)).build()); "custom grace duration")]
     #[test_case("trip example.com -g 50ms", Ok(cfg().grace_duration(Duration::from_millis(50)).build()); "custom grace duration short")]
-    #[test_case("trip example.com --grace-duration 0", Err(anyhow!("time unit needed, for example 0sec or 0ms")); "invalid format grace duration")]
+    #[test_case("trip example.com --grace-duration 0", Err(anyhow!("error: invalid value '0' for '--grace-duration <GRACE_DURATION>': time unit needed, for example 0sec or 0ms For more information, try '--help'.")); "invalid format grace duration")]
     #[test_case("trip example.com --grace-duration 9ms", Err(anyhow!("grace-duration (9ms) must be between 10ms and 1s inclusive")); "invalid low grace duration")]
     #[test_case("trip example.com --grace-duration 1001ms", Err(anyhow!("grace-duration (1.001s) must be between 10ms and 1s inclusive")); "invalid high grace duration")]
     fn test_grace_duration(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
@@ -1304,17 +1285,17 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().max_inflight(24).build()); "default max inflight")]
     #[test_case("trip example.com --max-inflight 12", Ok(cfg().max_inflight(12).build()); "custom max inflight")]
     #[test_case("trip example.com -U 20", Ok(cfg().max_inflight(20).build()); "custom max inflight short")]
-    #[test_case("trip example.com --max-inflight foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format max inflight")]
+    #[test_case("trip example.com --max-inflight foo", Err(anyhow!("error: invalid value 'foo' for '--max-inflight <MAX_INFLIGHT>': invalid digit found in string For more information, try '--help'.")); "invalid format max inflight")]
     #[test_case("trip example.com --max-inflight 0", Err(anyhow!("max-inflight (0) must be greater than zero")); "invalid low max inflight")]
-    #[test_case("trip example.com --max-inflight 300", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high max inflight")]
+    #[test_case("trip example.com --max-inflight 300", Err(anyhow!("error: invalid value '300' for '--max-inflight <MAX_INFLIGHT>': 300 is not in 0..=255 For more information, try '--help'.")); "invalid high max inflight")]
     fn test_max_inflight(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
 
     #[test_case("trip example.com", Ok(cfg().initial_sequence(33000).build()); "default initial sequence")]
     #[test_case("trip example.com --initial-sequence 5000", Ok(cfg().initial_sequence(5000).build()); "custom initial sequence")]
-    #[test_case("trip example.com --initial-sequence foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format initial sequence")]
-    #[test_case("trip example.com --initial-sequence 100000", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high initial sequence")]
+    #[test_case("trip example.com --initial-sequence foo", Err(anyhow!("error: invalid value 'foo' for '--initial-sequence <INITIAL_SEQUENCE>': invalid digit found in string For more information, try '--help'. ")); "invalid format initial sequence")]
+    #[test_case("trip example.com --initial-sequence 100000", Err(anyhow!("error: invalid value '100000' for '--initial-sequence <INITIAL_SEQUENCE>': 100000 is not in 0..=65535 For more information, try '--help'.")); "invalid high initial sequence")]
     fn test_init_seq(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1322,8 +1303,8 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().tos(0).build()); "default tos")]
     #[test_case("trip example.com --tos 255", Ok(cfg().tos(0xFF).build()); "custom tos")]
     #[test_case("trip example.com -Q 255", Ok(cfg().tos(0xFF).build()); "custom tos short")]
-    #[test_case("trip example.com --tos foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format tos")]
-    #[test_case("trip example.com --tos 300", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high tos")]
+    #[test_case("trip example.com --tos foo", Err(anyhow!("error: invalid value 'foo' for '--tos <TOS>': invalid digit found in string For more information, try '--help'.")); "invalid format tos")]
+    #[test_case("trip example.com --tos 300", Err(anyhow!("error: invalid value '300' for '--tos <TOS>': 300 is not in 0..=255 For more information, try '--help'.")); "invalid high tos")]
     fn test_tos(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1337,7 +1318,7 @@ mod tests {
 
     #[test_case("trip example.com", Ok(cfg().read_timeout(Duration::from_millis(10)).build()); "default read timeout")]
     #[test_case("trip example.com --read-timeout 20ms", Ok(cfg().read_timeout(Duration::from_millis(20)).build()); "custom read timeout")]
-    #[test_case("trip example.com --read-timeout 20", Err(anyhow!("time unit needed, for example 20sec or 20ms")); "invalid custom read timeout")]
+    #[test_case("trip example.com --read-timeout 20", Err(anyhow!("error: invalid value '20' for '--read-timeout <READ_TIMEOUT>': time unit needed, for example 20sec or 20ms For more information, try '--help'.")); "invalid custom read timeout")]
     #[test_case("trip example.com --read-timeout 9ms", Err(anyhow!("read-timeout (9ms) must be between 10ms and 100ms inclusive")); "invalid low custom read timeout")]
     #[test_case("trip example.com --read-timeout 101ms", Err(anyhow!("read-timeout (101ms) must be between 10ms and 100ms inclusive")); "invalid high custom read timeout")]
     fn test_read_timeout(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
@@ -1346,22 +1327,22 @@ mod tests {
 
     #[test_case("trip example.com", Ok(cfg().packet_size(84).build()); "default packet size")]
     #[test_case("trip example.com --packet-size 120", Ok(cfg().packet_size(120).build()); "custom packet size")]
-    #[test_case("trip example.com --packet-size foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format packet size")]
+    #[test_case("trip example.com --packet-size foo", Err(anyhow!("error: invalid value 'foo' for '--packet-size <PACKET_SIZE>': invalid digit found in string For more information, try '--help'.")); "invalid format packet size")]
     #[test_case("trip example.com --packet-size 47 -F ipv4-then-ipv6", Err(anyhow!("packet-size (47) must be between 48 and 1024 inclusive for Ipv4thenIpv6")); "invalid low packet size for ipv4 then ipv6")]
     #[test_case("trip example.com --packet-size 47 -F ipv6-then-ipv4", Err(anyhow!("packet-size (47) must be between 48 and 1024 inclusive for Ipv6thenIpv4")); "invalid low packet size for ipv6 then ipv4")]
     #[test_case("trip example.com --packet-size 27 -F ipv4", Err(anyhow!("packet-size (27) must be between 28 and 1024 inclusive for Ipv4Only")); "invalid low packet size for ipv4")]
     #[test_case("trip example.com --packet-size 1025 -F ipv4", Err(anyhow!("packet-size (1025) must be between 28 and 1024 inclusive for Ipv4Only")); "invalid high packet size for ipv4")]
     #[test_case("trip example.com --packet-size 47 -F ipv6", Err(anyhow!("packet-size (47) must be between 48 and 1024 inclusive for Ipv6Only")); "invalid low packet size for ipv6")]
     #[test_case("trip example.com --packet-size 1025 -F ipv6", Err(anyhow!("packet-size (1025) must be between 48 and 1024 inclusive for Ipv6Only")); "invalid high packet size for ipv6")]
-    #[test_case("trip example.com --packet-size 100000", Err(anyhow!("error: invalid value for one of the arguments")); "invalid out of range packet size")]
+    #[test_case("trip example.com --packet-size 100000", Err(anyhow!("error: invalid value '100000' for '--packet-size <PACKET_SIZE>': 100000 is not in 0..=65535 For more information, try '--help'.")); "invalid out of range packet size")]
     fn test_packet_size(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
 
     #[test_case("trip example.com", Ok(cfg().payload_pattern(0).build()); "default payload pattern size")]
     #[test_case("trip example.com --payload-pattern 255", Ok(cfg().payload_pattern(0xFF).build()); "custom payload pattern")]
-    #[test_case("trip example.com --payload-pattern foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format payload pattern")]
-    #[test_case("trip example.com --payload-pattern 256", Err(anyhow!("error: invalid value for one of the arguments")); "invalid out of range payload pattern")]
+    #[test_case("trip example.com --payload-pattern foo", Err(anyhow!("error: invalid value 'foo' for '--payload-pattern <PAYLOAD_PATTERN>': invalid digit found in string For more information, try '--help'.")); "invalid format payload pattern")]
+    #[test_case("trip example.com --payload-pattern 256", Err(anyhow!("error: invalid value '256' for '--payload-pattern <PAYLOAD_PATTERN>': 256 is not in 0..=255 For more information, try '--help'.")); "invalid out of range payload pattern")]
     fn test_payload_pattern(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1370,7 +1351,7 @@ mod tests {
     #[test_case("trip example.com --source-address 10.0.0.1", Ok(cfg().source_addr(Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))).build()); "custom ipv4 source address")]
     #[test_case("trip example.com --source-address 2404:6800:4005:81a::200e", Ok(cfg().source_addr(Some(IpAddr::V6(Ipv6Addr::from_str("2404:6800:4005:81a::200e").unwrap()))).build()); "custom ipv6 source address")]
     #[test_case("trip example.com -A 10.0.0.1", Ok(cfg().source_addr(Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))).build()); "custom ipv4 source address short")]
-    #[test_case("trip example.com --source-address foobar", Err(anyhow!("invalid source IP address format: foobar")); "invalid source address")]
+    #[test_case("trip example.com --source-address foobar", Err(anyhow!("error: invalid value 'foobar' for '--source-address <SOURCE_ADDRESS>': invalid IP address syntax For more information, try '--help'.")); "invalid source address")]
     fn test_source_address(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1384,7 +1365,7 @@ mod tests {
 
     #[test_case("trip example.com", Ok(cfg().dns_timeout(Duration::from_millis(5000)).build()); "default dns timeout")]
     #[test_case("trip example.com --dns-timeout 20ms", Ok(cfg().dns_timeout(Duration::from_millis(20)).build()); "custom dns timeout")]
-    #[test_case("trip example.com --dns-timeout 20", Err(anyhow!("time unit needed, for example 20sec or 20ms")); "invalid custom dns timeout")]
+    #[test_case("trip example.com --dns-timeout 20", Err(anyhow!("error: invalid value '20' for '--dns-timeout <DNS_TIMEOUT>': time unit needed, for example 20sec or 20ms For more information, try '--help'.")); "invalid custom dns timeout")]
     fn test_dns_timeout(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1395,7 +1376,7 @@ mod tests {
     #[test_case("trip example.com --dns-resolve-method google", Ok(cfg().dns_resolve_method(ResolveMethod::Google).build()); "custom resolve method google")]
     #[test_case("trip example.com --dns-resolve-method cloudflare", Ok(cfg().dns_resolve_method(ResolveMethod::Cloudflare).build()); "custom resolve method cloudflare")]
     #[test_case("trip example.com --dns-resolve-method resolv", Ok(cfg().dns_resolve_method(ResolveMethod::Resolv).build()); "custom resolve method resolv")]
-    #[test_case("trip example.com --dns-resolve-method foobar", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid resolve method")]
+    #[test_case("trip example.com --dns-resolve-method foobar", Err(anyhow!("error: invalid value 'foobar' for '--dns-resolve-method <DNS_RESOLVE_METHOD>' [possible values: system, resolv, google, cloudflare] For more information, try '--help'.")); "invalid resolve method")]
     fn test_dns_resolve(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1418,14 +1399,14 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().tui_max_samples(256).build()); "default max samples")]
     #[test_case("trip example.com --tui-max-samples 100", Ok(cfg().tui_max_samples(100).build()); "custom max samples")]
     #[test_case("trip example.com -s 100", Ok(cfg().tui_max_samples(100).build()); "custom max samples short")]
-    #[test_case("trip example.com --tui-max-samples foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid max samples")]
+    #[test_case("trip example.com --tui-max-samples foo", Err(anyhow!("error: invalid value 'foo' for '--tui-max-samples <TUI_MAX_SAMPLES>': invalid digit found in string For more information, try '--help'.")); "invalid max samples")]
     fn test_tui_max_samples(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
 
     #[test_case("trip example.com", Ok(cfg().tui_max_flows(64).build()); "default max flows")]
     #[test_case("trip example.com --tui-max-flows 100", Ok(cfg().tui_max_flows(100).build()); "custom max flows")]
-    #[test_case("trip example.com --tui-max-flows foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid max flows")]
+    #[test_case("trip example.com --tui-max-flows foo", Err(anyhow!("error: invalid value 'foo' for '--tui-max-flows <TUI_MAX_FLOWS>': invalid digit found in string For more information, try '--help'.")); "invalid max flows")]
     fn test_tui_max_flows(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1440,14 +1421,15 @@ mod tests {
     #[test_case("trip example.com --tui-refresh-rate 200ms", Ok(cfg().tui_refresh_rate(Duration::from_millis(200)).build()); "custom tui refresh rate")]
     #[test_case("trip example.com --tui-refresh-rate 49ms", Err(anyhow!("tui-refresh-rate (49ms) must be between 50ms and 1s inclusive")); "invalid low tui refresh rate")]
     #[test_case("trip example.com --tui-refresh-rate 1001ms", Err(anyhow!("tui-refresh-rate (1.001s) must be between 50ms and 1s inclusive")); "invalid high tui refresh rate")]
-    #[test_case("trip example.com --tui-refresh-rate foo", Err(anyhow!("expected number at 0")); "invalid format tui refresh rate")]
+    #[test_case("trip example.com --tui-refresh-rate foo", Err(anyhow!("error: invalid value 'foo' for '--tui-refresh-rate <TUI_REFRESH_RATE>': expected number at 0 For more information, try '--help'.")); "invalid format tui refresh rate")]
+    #[test_case("trip example.com --tui-refresh-rate 10xx", Err(anyhow!("error: invalid value '10xx' for '--tui-refresh-rate <TUI_REFRESH_RATE>': unknown time unit \"xx\", supported units: ns, us, ms, sec, min, hours, days, weeks, months, years (and few variations) For more information, try '--help'.")); "invalid time unit tui refresh rate")]
     fn test_tui_refresh_rate(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
 
     #[test_case("trip example.com", Ok(cfg().tui_privacy_max_ttl(0).build()); "default tui privacy max ttl")]
     #[test_case("trip example.com --tui-privacy-max-ttl 4", Ok(cfg().tui_privacy_max_ttl(4).build()); "custom tui privacy max ttl")]
-    #[test_case("trip example.com --tui-privacy-max-ttl foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui privacy max ttl")]
+    #[test_case("trip example.com --tui-privacy-max-ttl foo", Err(anyhow!("error: invalid value 'foo' for '--tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>': invalid digit found in string For more information, try '--help'.")); "invalid tui privacy max ttl")]
     fn test_tui_privacy_max_ttl(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1457,7 +1439,7 @@ mod tests {
     #[test_case("trip example.com --tui-address-mode host", Ok(cfg().tui_address_mode(AddressMode::Host).build()); "host tui address mode")]
     #[test_case("trip example.com --tui-address-mode both", Ok(cfg().tui_address_mode(AddressMode::Both).build()); "both tui address mode")]
     #[test_case("trip example.com -a both", Ok(cfg().tui_address_mode(AddressMode::Both).build()); "custom tui address mode short")]
-    #[test_case("trip example.com --tui-address-mode foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid tui address mode")]
+    #[test_case("trip example.com --tui-address-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-address-mode <TUI_ADDRESS_MODE>' [possible values: ip, host, both] For more information, try '--help'.")); "invalid tui address mode")]
     fn test_tui_address_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1469,7 +1451,7 @@ mod tests {
     #[test_case("trip example.com --tui-as-mode registry", Ok(cfg().tui_as_mode(AsMode::Registry).build()); "registry tui as mode")]
     #[test_case("trip example.com --tui-as-mode allocated", Ok(cfg().tui_as_mode(AsMode::Allocated).build()); "allocated tui as mode")]
     #[test_case("trip example.com --tui-as-mode name", Ok(cfg().tui_as_mode(AsMode::Name).build()); "name tui as mode")]
-    #[test_case("trip example.com --tui-as-mode foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid tui as mode")]
+    #[test_case("trip example.com --tui-as-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-as-mode <TUI_AS_MODE>' [possible values: asn, prefix, country-code, registry, allocated, name] For more information, try '--help'.")); "invalid tui as mode")]
     fn test_tui_as_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1487,7 +1469,7 @@ mod tests {
     #[test_case("trip example.com --tui-icmp-extension-mode mpls", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Mpls).build()); "mpls tui icmp extension mode")]
     #[test_case("trip example.com --tui-icmp-extension-mode full", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Full).build()); "full tui icmp extension mode")]
     #[test_case("trip example.com --tui-icmp-extension-mode all", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::All).build()); "all tui icmp extension mode")]
-    #[test_case("trip example.com --tui-icmp-extension-mode foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid tui icmp extension mode")]
+    #[test_case("trip example.com --tui-icmp-extension-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>' [possible values: off, mpls, full, all] For more information, try '--help'.")); "invalid tui icmp extension mode")]
     fn test_tui_icmp_extension_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1498,7 +1480,7 @@ mod tests {
     #[test_case("trip example.com --tui-geoip-mode long --geoip-mmdb-file foo.mmdb", Ok(cfg().tui_geoip_mode(GeoIpMode::Long).geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "long tui geoip mode")]
     #[test_case("trip example.com --tui-geoip-mode location --geoip-mmdb-file foo.mmdb", Ok(cfg().tui_geoip_mode(GeoIpMode::Location).geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "location tui geoip mode")]
     #[test_case("trip example.com --tui-geoip-mode short", Err(anyhow!("geoip-mmdb-file must be given for tui-geoip-mode of `Short`")); "custom tui geoip mode without geoip")]
-    #[test_case("trip example.com --tui-geoip-mode foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid tui geoip mode")]
+    #[test_case("trip example.com --tui-geoip-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-geoip-mode <TUI_GEOIP_MODE>' [possible values: off, short, long, location] For more information, try '--help'.")); "invalid tui geoip mode")]
     fn test_tui_geoip_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1506,7 +1488,7 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().tui_max_addrs(None).build()); "default tui max addrs")]
     #[test_case("trip example.com --tui-max-addrs 5", Ok(cfg().tui_max_addrs(Some(5)).build()); "custom tui max addrs")]
     #[test_case("trip example.com -M 7", Ok(cfg().tui_max_addrs(Some(7)).build()); "custom tui max addrs short")]
-    #[test_case("trip example.com --tui-max-addrs foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui max addrs")]
+    #[test_case("trip example.com --tui-max-addrs foo", Err(anyhow!("error: invalid value 'foo' for '--tui-max-addrs <TUI_MAX_ADDRS>': invalid digit found in string For more information, try '--help'.")); "invalid tui max addrs")]
     fn test_tui_max_addrs(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1515,11 +1497,11 @@ mod tests {
     #[test_case("trip example.com --tui-theme-colors bg-color=red", Ok(cfg().tui_theme(TuiTheme { bg_color: TuiColor::Red, ..Default::default() }).build()); "custom tui theme named color")]
     #[test_case("trip example.com --tui-theme-colors bg-color=010203", Ok(cfg().tui_theme(TuiTheme { bg_color: TuiColor::Rgb(1, 2, 3), ..Default::default() }).build()); "custom tui theme hex color")]
     #[test_case("trip example.com --tui-theme-colors bg-color=red,text-color=blue", Ok(cfg().tui_theme(TuiTheme { bg_color: TuiColor::Red, text_color: TuiColor::Blue, ..Default::default() }).build()); "custom tui theme multiple")]
-    #[test_case("trip example.com --tui-theme-colors bg-color=0", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui theme truncated hex value")]
-    #[test_case("trip example.com --tui-theme-colors bg-color=foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui theme invalid named color")]
-    #[test_case("trip example.com --tui-theme-colors foo-color=red", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui theme invalid item")]
-    #[test_case("trip example.com --tui-theme-colors foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui theme invalid syntax")]
-    #[test_case("trip example.com --tui-theme-colors bg-color=red, text-color=blue", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui theme invalid multiple with space")]
+    #[test_case("trip example.com --tui-theme-colors bg-color=0", Err(anyhow!("error: invalid value 'bg-color=0' for '--tui-theme-colors <TUI_THEME_COLORS>': unknown color: 0 For more information, try '--help'.")); "invalid tui theme truncated hex value")]
+    #[test_case("trip example.com --tui-theme-colors bg-color=foo", Err(anyhow!("error: invalid value 'bg-color=foo' for '--tui-theme-colors <TUI_THEME_COLORS>': unknown color: foo For more information, try '--help'. ")); "invalid tui theme invalid named color")]
+    #[test_case("trip example.com --tui-theme-colors foo-color=red", Err(anyhow!("error: invalid value 'foo-color=red' for '--tui-theme-colors <TUI_THEME_COLORS>': Matching variant not found For more information, try '--help'.")); "invalid tui theme invalid item")]
+    #[test_case("trip example.com --tui-theme-colors foo", Err(anyhow!("error: invalid value 'foo' for '--tui-theme-colors <TUI_THEME_COLORS>': invalid theme value: expected format `item=value` For more information, try '--help'.")); "invalid tui theme invalid syntax")]
+    #[test_case("trip example.com --tui-theme-colors bg-color=red, text-color=blue", Err(anyhow!("error: invalid value '' for '--tui-theme-colors <TUI_THEME_COLORS>': invalid theme value: expected format `item=value` For more information, try '--help'.")); "invalid tui theme invalid multiple with space")]
     fn test_tui_theme(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1527,10 +1509,10 @@ mod tests {
     #[test_case("trip example.com", Ok(cfg().tui_bindings(TuiBindings::default()).build()); "default tui bindings")]
     #[test_case("trip example.com --tui-key-bindings toggle-help=h", Ok(cfg().tui_bindings(TuiBindings { toggle_help: TuiKeyBinding::new(KeyCode::Char('h')), ..Default::default() }).build()); "custom tui bindings")]
     #[test_case("trip example.com --tui-key-bindings toggle-help=h,toggle-map=m", Ok(cfg().tui_bindings(TuiBindings { toggle_help: TuiKeyBinding::new(KeyCode::Char('h')), toggle_map: TuiKeyBinding::new(KeyCode::Char('m')), ..Default::default() }).build()); "custom tui bindings multiple")]
-    #[test_case("trip example.com --tui-key-bindings foo=h", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui binding command")]
-    #[test_case("trip example.com --tui-key-bindings toggle-help=123", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui binding key")]
+    #[test_case("trip example.com --tui-key-bindings foo=h", Err(anyhow!("error: invalid value 'foo=h' for '--tui-key-bindings <TUI_KEY_BINDINGS>': Matching variant not found For more information, try '--help'.")); "invalid tui binding command")]
+    #[test_case("trip example.com --tui-key-bindings toggle-help=123", Err(anyhow!("error: invalid value 'toggle-help=123' for '--tui-key-bindings <TUI_KEY_BINDINGS>': unknown key binding '123' For more information, try '--help'.")); "invalid tui binding key")]
     #[test_case("trip example.com --tui-key-bindings toggle-help=h,toggle-map=h", Err(anyhow!("Duplicate key bindings: h: [toggle-map and toggle-help]")); "invalid tui binding duplicate binding")]
-    #[test_case("trip example.com --tui-key-bindings toggle-help=h, toggle-map=m", Err(anyhow!("error: invalid value for one of the arguments")); "invalid tui binding multiple with space")]
+    #[test_case("trip example.com --tui-key-bindings toggle-help=h, toggle-map=m", Err(anyhow!("error: invalid value '' for '--tui-key-bindings <TUI_KEY_BINDINGS>': invalid binding value: expected format `item=value` For more information, try '--help'.")); "invalid tui binding multiple with space")]
     fn test_tui_bindings(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1539,7 +1521,7 @@ mod tests {
     #[test_case("trip example.com --mode csv --report-cycles 5", Ok(cfg().report_cycles(5).mode(Mode::Csv).max_rounds(Some(5)).build()); "custom report cycles")]
     #[test_case("trip example.com --mode pretty -C 5", Ok(cfg().report_cycles(5).mode(Mode::Pretty).max_rounds(Some(5)).build()); "custom report cycles short")]
     #[test_case("trip example.com --report-cycles 0", Err(anyhow!("report-cycles (0) must be greater than zero")); "invalid low report cycles")]
-    #[test_case("trip example.com --report-cycles foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid report cycles")]
+    #[test_case("trip example.com --report-cycles foo", Err(anyhow!("error: invalid value 'foo' for '--report-cycles <REPORT_CYCLES>': invalid digit found in string For more information, try '--help'.")); "invalid report cycles")]
     fn test_report_cycles(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1570,7 +1552,7 @@ mod tests {
     #[test_case("trip example.com --log-format pretty", Ok(cfg().log_format(LogFormat::Pretty).build()); "pretty log format")]
     #[test_case("trip example.com --log-format json", Ok(cfg().log_format(LogFormat::Json).build()); "json log format")]
     #[test_case("trip example.com --log-format chrome", Ok(cfg().log_format(LogFormat::Chrome).build()); "chrome log format")]
-    #[test_case("trip example.com --log-format foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid log format")]
+    #[test_case("trip example.com --log-format foo", Err(anyhow!("error: invalid value 'foo' for '--log-format <LOG_FORMAT>' [possible values: compact, pretty, json, chrome] For more information, try '--help'.")); "invalid log format")]
     fn test_log_format(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1579,7 +1561,7 @@ mod tests {
     #[test_case("trip example.com --log-span-events off", Ok(cfg().log_span_events(LogSpanEvents::Off).build()); "off log span")]
     #[test_case("trip example.com --log-span-events active", Ok(cfg().log_span_events(LogSpanEvents::Active).build()); "active log span")]
     #[test_case("trip example.com --log-span-events full", Ok(cfg().log_span_events(LogSpanEvents::Full).build()); "full log span")]
-    #[test_case("trip example.com --log-span-events foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid log span")]
+    #[test_case("trip example.com --log-span-events foo", Err(anyhow!("error: invalid value 'foo' for '--log-span-events <LOG_SPAN_EVENTS>' [possible values: off, active, full] For more information, try '--help'.")); "invalid log span")]
     fn test_log_span(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
         compare(parse_config(cmd), expected);
     }
@@ -1615,7 +1597,7 @@ mod tests {
     #[test_case("trip --generate powershell", Ok(TrippyAction::PrintShellCompletions(Shell::PowerShell)); "generate powershell shell completions")]
     #[test_case("trip --generate zsh", Ok(TrippyAction::PrintShellCompletions(Shell::Zsh)); "generate zsh shell completions")]
     #[test_case("trip --generate bash", Ok(TrippyAction::PrintShellCompletions(Shell::Bash)); "generate bash shell completions")]
-    #[test_case("trip --generate foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "generate invalid shell completions")]
+    #[test_case("trip --generate foo", Err(anyhow!("error: invalid value 'foo' for '--generate <GENERATE>' [possible values: bash, elvish, fish, powershell, zsh] For more information, try '--help'.")); "generate invalid shell completions")]
     #[test_case("trip --generate-man", Ok(TrippyAction::PrintManPage); "generate man page")]
     fn test_action(cmd: &str, expected: anyhow::Result<TrippyAction>) {
         compare(parse_action(cmd), expected);
@@ -1650,16 +1632,44 @@ mod tests {
         )?)
     }
 
-    fn compare<T: PartialEq + Eq + std::fmt::Debug>(
+    fn compare<T>(actual: anyhow::Result<T>, expected: anyhow::Result<T>)
+    where
+        T: PartialEq + Eq + std::fmt::Debug,
+    {
+        compare_lines(actual, expected, None);
+    }
+
+    fn compare_lines<T>(
         actual: anyhow::Result<T>,
         expected: anyhow::Result<T>,
-    ) {
+        lines: Option<usize>,
+    ) where
+        T: PartialEq + Eq + std::fmt::Debug,
+    {
         match (actual, expected) {
             (Ok(cfg), Ok(exp)) => {
                 pretty_assertions::assert_eq!(cfg, exp);
             }
             (Err(err), Err(exp_err)) => {
-                if remove_whitespace(err.to_string()) != remove_whitespace(exp_err.to_string()) {
+                if let Some(lines) = lines {
+                    let fst = err
+                        .to_string()
+                        .lines()
+                        .nth(lines)
+                        .map(ToString::to_string)
+                        .unwrap_or_default();
+                    let snd = exp_err
+                        .to_string()
+                        .lines()
+                        .nth(lines)
+                        .map(ToString::to_string)
+                        .unwrap_or_default();
+                    if remove_whitespace(fst) != remove_whitespace(snd) {
+                        pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string());
+                    }
+                } else if remove_whitespace(err.to_string())
+                    != remove_whitespace(exp_err.to_string())
+                {
                     pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string());
                 }
             }
diff --git a/crates/trippy/src/config/cmd.rs b/crates/trippy/src/config/cmd.rs
index 2d69be050..356652811 100644
--- a/crates/trippy/src/config/cmd.rs
+++ b/crates/trippy/src/config/cmd.rs
@@ -9,6 +9,9 @@ use anyhow::anyhow;
 use clap::builder::Styles;
 use clap::Parser;
 use clap_complete::Shell;
+use std::net::IpAddr;
+use std::str::FromStr;
+use std::time::Duration;
 
 /// Trace a route to a host and record statistics
 #[allow(clippy::doc_markdown)]
@@ -93,25 +96,25 @@ pub struct Args {
     pub source_port: Option<u16>,
 
     /// The source IP address [default: auto]
-    #[arg(short = 'A', long, conflicts_with = "interface")]
-    pub source_address: Option<String>,
+    #[arg(short = 'A', long, value_parser = parse_addr, conflicts_with = "interface")]
+    pub source_address: Option<IpAddr>,
 
     /// The network interface [default: auto]
     #[arg(short = 'I', long)]
     pub interface: Option<String>,
 
     /// The minimum duration of every round [default: 1s]
-    #[arg(short = 'i', long)]
-    pub min_round_duration: Option<String>,
+    #[arg(short = 'i', long, value_parser = parse_duration)]
+    pub min_round_duration: Option<Duration>,
 
     /// The maximum duration of every round [default: 1s]
-    #[arg(short = 'T', long)]
-    pub max_round_duration: Option<String>,
+    #[arg(short = 'T', long, value_parser = parse_duration)]
+    pub max_round_duration: Option<Duration>,
 
     /// The period of time to wait for additional ICMP responses after the target has responded
     /// [default: 100ms]
-    #[arg(short = 'g', long)]
-    pub grace_duration: Option<String>,
+    #[arg(short = 'g', long, value_parser = parse_duration)]
+    pub grace_duration: Option<Duration>,
 
     /// The initial sequence number [default: 33000]
     #[arg(long)]
@@ -150,8 +153,8 @@ pub struct Args {
     pub icmp_extensions: bool,
 
     /// The socket read timeout [default: 10ms]
-    #[arg(long)]
-    pub read_timeout: Option<String>,
+    #[arg(long, value_parser = parse_duration)]
+    pub read_timeout: Option<Duration>,
 
     /// How to perform DNS queries [default: system]
     #[arg(value_enum, short = 'r', long)]
@@ -162,8 +165,8 @@ pub struct Args {
     pub dns_resolve_all: bool,
 
     /// The maximum time to wait to perform DNS queries [default: 5s]
-    #[arg(long)]
-    pub dns_timeout: Option<String>,
+    #[arg(long, value_parser = parse_duration)]
+    pub dns_timeout: Option<Duration>,
 
     /// Lookup autonomous system (AS) information during DNS queries [default: false]
     #[arg(long, short = 'z')]
@@ -206,8 +209,8 @@ pub struct Args {
     pub tui_preserve_screen: bool,
 
     /// The Tui refresh rate [default: 100ms]
-    #[arg(long)]
-    pub tui_refresh_rate: Option<String>,
+    #[arg(long, value_parser = parse_duration)]
+    pub tui_refresh_rate: Option<Duration>,
 
     /// The maximum ttl of hops which will be masked for privacy [default: 0]
     #[arg(long)]
@@ -283,3 +286,11 @@ fn parse_tui_binding_value(value: &str) -> anyhow::Result<(TuiCommandItem, TuiKe
     let binding = TuiKeyBinding::try_from(&value[pos + 1..])?;
     Ok((item, binding))
 }
+
+fn parse_duration(value: &str) -> anyhow::Result<Duration> {
+    Ok(humantime::parse_duration(value)?)
+}
+
+fn parse_addr(value: &str) -> anyhow::Result<IpAddr> {
+    Ok(IpAddr::from_str(value)?)
+}
diff --git a/crates/trippy/src/config/constants.rs b/crates/trippy/src/config/constants.rs
index 5d8eb3547..29b16dd2d 100644
--- a/crates/trippy/src/config/constants.rs
+++ b/crates/trippy/src/config/constants.rs
@@ -53,7 +53,7 @@ pub const DEFAULT_TUI_MAX_ADDRS: u8 = 0;
 pub const DEFAULT_TUI_ADDRESS_MODE: AddressMode = AddressMode::Host;
 
 /// The default value for `tui-refresh-rate`.
-pub const DEFAULT_TUI_REFRESH_RATE: &str = "100ms";
+pub const DEFAULT_TUI_REFRESH_RATE: Duration = Duration::from_millis(100);
 
 /// The default value for `tui-privacy-max-ttl`.
 pub const DEFAULT_TUI_PRIVACY_MAX_TTL: u8 = 0;
@@ -68,7 +68,7 @@ pub const DEFAULT_ADDR_FAMILY: AddressFamilyConfig = AddressFamilyConfig::Ipv4Th
 pub const DEFAULT_DNS_LOOKUP_AS_INFO: bool = false;
 
 /// The default value for `dns-timeout`.
-pub const DEFAULT_DNS_TIMEOUT: &str = "5s";
+pub const DEFAULT_DNS_TIMEOUT: Duration = Duration::from_millis(5000);
 
 /// The default value for `report-cycles`.
 pub const DEFAULT_REPORT_CYCLES: usize = 10;
diff --git a/crates/trippy/src/config/file.rs b/crates/trippy/src/config/file.rs
index 7e4113e82..4f74b93a7 100644
--- a/crates/trippy/src/config/file.rs
+++ b/crates/trippy/src/config/file.rs
@@ -7,11 +7,13 @@ use crate::config::{
 use anyhow::Context;
 use encoding_rs_io::DecodeReaderBytes;
 use etcetera::BaseStrategy;
-use humantime::format_duration;
 use serde::Deserialize;
 use std::fs::File;
 use std::io::{BufReader, Read};
+use std::net::IpAddr;
 use std::path::Path;
+use std::str::FromStr;
+use std::time::Duration;
 use trippy_core::defaults;
 
 const DEFAULT_CONFIG_FILE: &str = "trippy.toml";
@@ -130,13 +132,21 @@ pub struct ConfigStrategy {
     pub addr_family: Option<AddressFamilyConfig>,
     pub target_port: Option<u16>,
     pub source_port: Option<u16>,
-    pub source_address: Option<String>,
+    #[serde(default)]
+    #[serde(deserialize_with = "addr_deser")]
+    pub source_address: Option<IpAddr>,
     pub interface: Option<String>,
-    pub min_round_duration: Option<String>,
-    pub max_round_duration: Option<String>,
+    #[serde(default)]
+    #[serde(deserialize_with = "humantime_deser")]
+    pub min_round_duration: Option<Duration>,
+    #[serde(default)]
+    #[serde(deserialize_with = "humantime_deser")]
+    pub max_round_duration: Option<Duration>,
     pub initial_sequence: Option<u16>,
     pub multipath_strategy: Option<MultipathStrategyConfig>,
-    pub grace_duration: Option<String>,
+    #[serde(default)]
+    #[serde(deserialize_with = "humantime_deser")]
+    pub grace_duration: Option<Duration>,
     pub max_inflight: Option<u8>,
     pub first_ttl: Option<u8>,
     pub max_ttl: Option<u8>,
@@ -144,7 +154,9 @@ pub struct ConfigStrategy {
     pub payload_pattern: Option<u8>,
     pub tos: Option<u8>,
     pub icmp_extensions: Option<bool>,
-    pub read_timeout: Option<String>,
+    #[serde(default)]
+    #[serde(deserialize_with = "humantime_deser")]
+    pub read_timeout: Option<Duration>,
 }
 
 impl Default for ConfigStrategy {
@@ -156,19 +168,13 @@ impl Default for ConfigStrategy {
             source_port: None,
             source_address: None,
             interface: None,
-            min_round_duration: Some(
-                format_duration(defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION).to_string(),
-            ),
-            max_round_duration: Some(
-                format_duration(defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION).to_string(),
-            ),
+            min_round_duration: Some(defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION),
+            max_round_duration: Some(defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION),
             initial_sequence: Some(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE),
             multipath_strategy: Some(MultipathStrategyConfig::from(
                 defaults::DEFAULT_STRATEGY_MULTIPATH,
             )),
-            grace_duration: Some(
-                format_duration(defaults::DEFAULT_STRATEGY_GRACE_DURATION).to_string(),
-            ),
+            grace_duration: Some(defaults::DEFAULT_STRATEGY_GRACE_DURATION),
             max_inflight: Some(defaults::DEFAULT_STRATEGY_MAX_INFLIGHT),
             first_ttl: Some(defaults::DEFAULT_STRATEGY_FIRST_TTL),
             max_ttl: Some(defaults::DEFAULT_STRATEGY_MAX_TTL),
@@ -176,9 +182,7 @@ impl Default for ConfigStrategy {
             payload_pattern: Some(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN),
             tos: Some(defaults::DEFAULT_STRATEGY_TOS),
             icmp_extensions: Some(defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE.is_enabled()),
-            read_timeout: Some(
-                format_duration(defaults::DEFAULT_STRATEGY_READ_TIMEOUT).to_string(),
-            ),
+            read_timeout: Some(defaults::DEFAULT_STRATEGY_READ_TIMEOUT),
         }
     }
 }
@@ -189,7 +193,9 @@ pub struct ConfigDns {
     pub dns_resolve_method: Option<DnsResolveMethodConfig>,
     pub dns_resolve_all: Option<bool>,
     pub dns_lookup_as_info: Option<bool>,
-    pub dns_timeout: Option<String>,
+    #[serde(default)]
+    #[serde(deserialize_with = "humantime_deser")]
+    pub dns_timeout: Option<Duration>,
 }
 
 impl Default for ConfigDns {
@@ -198,7 +204,7 @@ impl Default for ConfigDns {
             dns_resolve_method: Some(super::constants::DEFAULT_DNS_RESOLVE_METHOD),
             dns_resolve_all: Some(super::constants::DEFAULT_DNS_RESOLVE_ALL),
             dns_lookup_as_info: Some(super::constants::DEFAULT_DNS_LOOKUP_AS_INFO),
-            dns_timeout: Some(String::from(super::constants::DEFAULT_DNS_TIMEOUT)),
+            dns_timeout: Some(super::constants::DEFAULT_DNS_TIMEOUT),
         }
     }
 }
@@ -223,7 +229,9 @@ pub struct ConfigTui {
     pub tui_max_samples: Option<usize>,
     pub tui_max_flows: Option<usize>,
     pub tui_preserve_screen: Option<bool>,
-    pub tui_refresh_rate: Option<String>,
+    #[serde(default)]
+    #[serde(deserialize_with = "humantime_deser")]
+    pub tui_refresh_rate: Option<Duration>,
     pub tui_privacy_max_ttl: Option<u8>,
     pub tui_address_mode: Option<AddressMode>,
     pub tui_as_mode: Option<AsMode>,
@@ -240,7 +248,7 @@ impl Default for ConfigTui {
             tui_max_samples: Some(super::constants::DEFAULT_TUI_MAX_SAMPLES),
             tui_max_flows: Some(super::constants::DEFAULT_TUI_MAX_FLOWS),
             tui_preserve_screen: Some(super::constants::DEFAULT_TUI_PRESERVE_SCREEN),
-            tui_refresh_rate: Some(String::from(super::constants::DEFAULT_TUI_REFRESH_RATE)),
+            tui_refresh_rate: Some(super::constants::DEFAULT_TUI_REFRESH_RATE),
             tui_privacy_max_ttl: Some(super::constants::DEFAULT_TUI_PRIVACY_MAX_TTL),
             tui_address_mode: Some(super::constants::DEFAULT_TUI_ADDRESS_MODE),
             tui_as_mode: Some(super::constants::DEFAULT_TUI_AS_MODE),
@@ -399,6 +407,24 @@ impl Default for ConfigBindings {
     }
 }
 
+fn humantime_deser<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    humantime::parse_duration(&String::deserialize(deserializer)?)
+        .map_err(serde::de::Error::custom)
+        .map(Some)
+}
+
+fn addr_deser<'de, D>(deserializer: D) -> Result<Option<IpAddr>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    IpAddr::from_str(&String::deserialize(deserializer)?)
+        .map_err(serde::de::Error::custom)
+        .map(Some)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;


Test patch: