Skip to main content

mtr_ng/
args.rs

1use clap::{Parser, ValueEnum};
2
3#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)]
4pub enum SparklineScale {
5    Linear,
6    Logarithmic,
7}
8
9#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)]
10pub enum Column {
11    /// Hop number
12    Hop,
13    /// Hostname/IP address
14    Host,
15    /// Packet loss percentage
16    Loss,
17    /// Number of packets sent
18    Sent,
19    /// Last RTT measurement
20    Last,
21    /// Average RTT
22    Avg,
23    /// Exponential moving average RTT
24    Ema,
25    /// Last jitter value
26    Jitter,
27    /// Average jitter
28    JitterAvg,
29    /// Best (minimum) RTT
30    Best,
31    /// Worst (maximum) RTT
32    Worst,
33    /// RTT sparkline graph
34    Graph,
35}
36
37impl Column {
38    /// Get all available columns in default order
39    pub fn all() -> Vec<Column> {
40        vec![
41            Column::Hop,
42            Column::Host,
43            Column::Loss,
44            Column::Sent,
45            Column::Last,
46            Column::Avg,
47            Column::Ema,
48            Column::Jitter,
49            Column::JitterAvg,
50            Column::Best,
51            Column::Worst,
52            Column::Graph,
53        ]
54    }
55
56    /// Get default columns (excludes jitter by default for backwards compatibility)
57    pub fn default_columns() -> Vec<Column> {
58        vec![
59            Column::Hop,
60            Column::Host,
61            Column::Loss,
62            Column::Sent,
63            Column::Last,
64            Column::Avg,
65            Column::Ema,
66            Column::Best,
67            Column::Worst,
68            Column::Graph,
69        ]
70    }
71
72    /// Get column header text
73    pub fn header(&self) -> &'static str {
74        match self {
75            Column::Hop => "",
76            Column::Host => "Hostname",
77            Column::Loss => "Loss%",
78            Column::Sent => "Pkts",
79            Column::Last => "LastRTT",
80            Column::Avg => "AvgRTT",
81            Column::Ema => "EmaRTT",
82            Column::Jitter => "Jitter",
83            Column::JitterAvg => "JitAvg",
84            Column::Best => "BestRTT",
85            Column::Worst => "WorstRTT",
86            Column::Graph => "RTT History",
87        }
88    }
89
90    /// Get column width for formatting
91    pub fn width(&self) -> usize {
92        match self {
93            Column::Hop => 3,
94            Column::Host => 21,
95            Column::Loss => 7,
96            Column::Sent => 4,
97            Column::Last => 8,
98            Column::Avg => 8,
99            Column::Ema => 8,
100            Column::Jitter => 8,
101            Column::JitterAvg => 8,
102            Column::Best => 8,
103            Column::Worst => 8,
104            Column::Graph => 20, // Minimum width for sparkline
105        }
106    }
107}
108
109#[derive(Parser, Debug, Clone)]
110#[command(name = "mtr-ng")]
111#[command(
112    about = "A modern implementation of mtr (My Traceroute) with unicode and terminal graphics"
113)]
114#[command(version = env!("CARGO_PKG_VERSION"))]
115pub struct Args {
116    /// Target hostname or IP address
117    pub target: String,
118
119    /// Number of pings per round (default: infinite)
120    #[arg(short, long)]
121    pub count: Option<usize>,
122
123    /// Wait time between pings in milliseconds
124    #[arg(short, long, default_value = "1000")]
125    pub interval: u64,
126
127    /// Maximum number of hops
128    #[arg(short = 'M', long, default_value = "30")]
129    pub max_hops: u8,
130
131    /// Enable report mode (non-interactive)
132    #[arg(short, long)]
133    pub report: bool,
134
135    /// Show IP addresses instead of hostnames
136    #[arg(short, long)]
137    pub numeric: bool,
138
139    /// Sparkline scaling mode: linear or logarithmic (default: logarithmic)
140    #[arg(long, value_enum, default_value = "logarithmic")]
141    pub sparkline_scale: SparklineScale,
142
143    /// Exponential smoothing factor for EMA (0.0-1.0). Higher values = more responsive to recent changes
144    #[arg(long, default_value = "0.1")]
145    pub ema_alpha: f64,
146
147    /// Select which columns to display (default: hop,host,loss,sent,last,avg,ema,best,worst,graph)
148    #[arg(long, value_enum, value_delimiter = ',')]
149    pub fields: Option<Vec<Column>>,
150
151    /// Enable Sixel graphics for sparklines (requires compatible terminal)
152    #[arg(long, help = "Use Sixel graphics for enhanced sparklines")]
153    pub sixel: bool,
154
155    /// Show all available columns including jitter metrics
156    #[arg(long, help = "Display all available columns")]
157    pub show_all: bool,
158}
159
160impl Args {
161    /// Get the columns to display based on command-line arguments
162    pub fn get_columns(&self) -> Vec<Column> {
163        if self.show_all {
164            Column::all()
165        } else if let Some(ref fields) = self.fields {
166            fields.clone()
167        } else {
168            Column::default_columns()
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_args_default_values() {
179        let args = Args::try_parse_from(["mtr-ng", "example.com"]).unwrap();
180        assert_eq!(args.target, "example.com");
181        assert_eq!(args.count, None); // Default is now infinite (None)
182        assert_eq!(args.interval, 1000);
183        assert_eq!(args.max_hops, 30);
184        assert!(!args.report);
185        assert!(!args.numeric);
186        assert_eq!(args.sparkline_scale, SparklineScale::Logarithmic);
187        assert_eq!(args.ema_alpha, 0.1);
188        assert!(args.fields.is_none());
189        assert!(!args.show_all);
190    }
191
192    #[test]
193    fn test_args_custom_values() {
194        let args = Args::try_parse_from([
195            "mtr-ng",
196            "--count",
197            "20",
198            "--interval",
199            "500",
200            "--max-hops",
201            "50",
202            "--report",
203            "--numeric",
204            "google.com",
205        ])
206        .unwrap();
207
208        assert_eq!(args.target, "google.com");
209        assert_eq!(args.count, Some(20));
210        assert_eq!(args.interval, 500);
211        assert_eq!(args.max_hops, 50);
212        assert!(args.report);
213        assert!(args.numeric);
214        assert_eq!(args.sparkline_scale, SparklineScale::Logarithmic);
215        assert_eq!(args.ema_alpha, 0.1);
216        assert!(args.fields.is_none());
217        assert!(!args.show_all);
218    }
219
220    #[test]
221    fn test_args_short_flags() {
222        let args = Args::try_parse_from([
223            "mtr-ng",
224            "-c",
225            "15",
226            "-i",
227            "2000",
228            "-M",
229            "25",
230            "-r",
231            "-n",
232            "test.example.com",
233        ])
234        .unwrap();
235
236        assert_eq!(args.target, "test.example.com");
237        assert_eq!(args.count, Some(15));
238        assert_eq!(args.interval, 2000);
239        assert_eq!(args.max_hops, 25);
240        assert!(args.report);
241        assert!(args.numeric);
242        assert_eq!(args.sparkline_scale, SparklineScale::Logarithmic);
243        assert_eq!(args.ema_alpha, 0.1);
244        assert!(args.fields.is_none());
245        assert!(!args.show_all);
246    }
247}