Skip to main content

mtr_ng/
ui.rs

1//! User Interface Module
2//!
3//! This module provides a terminal-based user interface for the mtr-ng network diagnostic tool.
4//! It includes colorblind-friendly visualizations, sparkline graphs, interactive controls,
5//! and support for various terminal color modes.
6
7use crate::args::Column;
8use crate::sixel::SixelRenderer;
9use crate::SparklineScale;
10use crate::{HopStats, MtrSession, Result};
11use crossterm::{
12    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
13    execute,
14    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
15};
16use ratatui::{
17    backend::CrosstermBackend,
18    buffer::Buffer,
19    layout::{Alignment, Constraint, Direction, Layout, Rect},
20    style::{Color, Style},
21    text::{Line, Span},
22    widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table, Widget},
23    Frame, Terminal,
24};
25use std::{
26    io,
27    sync::{Arc, Mutex},
28    time::{Duration, Instant},
29};
30use tokio::sync::mpsc;
31use tracing::debug;
32
33// ========================================
34// UI State Management
35// ========================================
36
37#[derive(Debug, Clone)]
38pub struct UiState {
39    pub current_sparkline_scale: SparklineScale,
40    pub color_support: ColorSupport,
41    pub columns: Vec<Column>,
42    pub current_column_index: usize,
43    pub sixel_renderer: SixelRenderer,
44    pub show_help: bool,
45    pub visualization_mode: VisualizationMode,
46    pub show_hostnames: bool, // Toggle between hostnames and IP addresses
47    pub show_column_selector: bool, // Show column selection popup
48    pub column_selector_state: ColumnSelectorState, // State for column selector
49}
50
51#[derive(Debug, Clone, Copy)]
52pub enum ColorSupport {
53    None,      // No color support
54    Basic,     // 16 colors
55    Extended,  // 256 colors
56    TrueColor, // 24-bit RGB
57}
58
59#[derive(Debug, Clone, Copy)]
60pub enum VisualizationMode {
61    Sparkline, // Variable height characters (▁▂▃▄▅▆▇█)
62    Heatmap,   // Full height blocks (█) with colors only
63}
64
65// ========================================
66// Column Selector State
67// ========================================
68
69#[derive(Debug, Clone)]
70pub struct ColumnSelectorState {
71    pub selected_index: usize,
72    pub available_columns: Vec<(Column, bool)>, // (column, is_enabled)
73}
74
75impl ColumnSelectorState {
76    pub fn new(enabled_columns: &[Column]) -> Self {
77        let all_columns = Column::all();
78        let available_columns = all_columns
79            .into_iter()
80            .map(|col| (col, enabled_columns.contains(&col)))
81            .collect();
82
83        Self {
84            selected_index: 0,
85            available_columns,
86        }
87    }
88
89    pub fn move_up(&mut self) {
90        if self.selected_index > 0 {
91            self.selected_index -= 1;
92        }
93    }
94
95    pub fn move_down(&mut self) {
96        if self.selected_index < self.available_columns.len().saturating_sub(1) {
97            self.selected_index += 1;
98        }
99    }
100
101    pub fn toggle_selected(&mut self) {
102        if let Some((_col, enabled)) = self.available_columns.get_mut(self.selected_index) {
103            *enabled = !*enabled;
104        }
105    }
106
107    pub fn move_selected_up(&mut self) {
108        if self.selected_index > 0 {
109            self.available_columns
110                .swap(self.selected_index - 1, self.selected_index);
111            self.selected_index -= 1;
112        }
113    }
114
115    pub fn move_selected_down(&mut self) {
116        if self.selected_index < self.available_columns.len().saturating_sub(1) {
117            self.available_columns
118                .swap(self.selected_index, self.selected_index + 1);
119            self.selected_index += 1;
120        }
121    }
122
123    pub fn get_enabled_columns(&self) -> Vec<Column> {
124        self.available_columns
125            .iter()
126            .filter(|(_, enabled)| *enabled)
127            .map(|(col, _)| *col)
128            .collect()
129    }
130}
131
132impl UiState {
133    pub fn new(scale: SparklineScale, columns: Vec<Column>, enable_sixel: bool) -> Self {
134        let column_selector_state = ColumnSelectorState::new(&columns);
135        Self {
136            current_sparkline_scale: scale,
137            color_support: detect_color_support(),
138            columns,
139            current_column_index: 0,
140            sixel_renderer: SixelRenderer::new(enable_sixel),
141            show_help: false,
142            visualization_mode: VisualizationMode::Sparkline,
143            show_hostnames: true, // Start with hostnames enabled by default
144            show_column_selector: false,
145            column_selector_state,
146        }
147    }
148
149    pub fn toggle_help(&mut self) {
150        self.show_help = !self.show_help;
151    }
152
153    pub fn toggle_column_selector(&mut self) {
154        if self.show_column_selector {
155            // Just close - changes were already applied immediately
156        } else {
157            // Reset selector state when opening
158            self.column_selector_state = ColumnSelectorState::new(&self.columns);
159        }
160        self.show_column_selector = !self.show_column_selector;
161    }
162
163    // Immediate update methods for live preview
164    pub fn toggle_selected_column_immediate(&mut self) {
165        self.column_selector_state.toggle_selected();
166        self.apply_column_changes_immediate();
167    }
168
169    pub fn move_selected_column_up_immediate(&mut self) {
170        self.column_selector_state.move_selected_up();
171        self.apply_column_changes_immediate();
172    }
173
174    pub fn move_selected_column_down_immediate(&mut self) {
175        self.column_selector_state.move_selected_down();
176        self.apply_column_changes_immediate();
177    }
178
179    fn apply_column_changes_immediate(&mut self) {
180        self.columns = self.column_selector_state.get_enabled_columns();
181        // Ensure at least one column remains
182        if self.columns.is_empty() {
183            self.columns.push(Column::Host);
184            self.column_selector_state
185                .available_columns
186                .iter_mut()
187                .find(|(col, _)| matches!(col, Column::Host))
188                .map(|(_, enabled)| *enabled = true);
189        }
190    }
191
192    pub fn toggle_visualization_mode(&mut self) {
193        self.visualization_mode = match self.visualization_mode {
194            VisualizationMode::Sparkline => VisualizationMode::Heatmap,
195            VisualizationMode::Heatmap => VisualizationMode::Sparkline,
196        };
197    }
198
199    pub fn toggle_hostnames(&mut self) {
200        self.show_hostnames = !self.show_hostnames;
201    }
202
203    pub fn toggle_sparkline_scale(&mut self) {
204        self.current_sparkline_scale = match self.current_sparkline_scale {
205            SparklineScale::Linear => SparklineScale::Logarithmic,
206            SparklineScale::Logarithmic => SparklineScale::Linear,
207        };
208    }
209
210    pub fn cycle_color_mode(&mut self) {
211        self.color_support = match self.color_support {
212            ColorSupport::None => ColorSupport::Basic,
213            ColorSupport::Basic => ColorSupport::Extended,
214            ColorSupport::Extended => ColorSupport::TrueColor,
215            ColorSupport::TrueColor => ColorSupport::None,
216        };
217    }
218
219    pub fn toggle_column(&mut self) {
220        if !self.columns.is_empty() {
221            self.current_column_index = (self.current_column_index + 1) % self.columns.len();
222            let all_columns = Column::all();
223            let removed_column = self.columns.remove(self.current_column_index);
224
225            for col in &all_columns {
226                if !self.columns.contains(col) && *col != removed_column {
227                    self.columns.insert(self.current_column_index, *col);
228                    break;
229                }
230            }
231
232            if self.current_column_index >= self.columns.len() {
233                self.current_column_index = 0;
234            }
235        }
236    }
237
238    pub fn add_column(&mut self, column: Column) {
239        if !self.columns.contains(&column) {
240            self.columns.push(column);
241        }
242    }
243
244    pub fn remove_column(&mut self, column: Column) {
245        if let Some(pos) = self.columns.iter().position(|&c| c == column) {
246            self.columns.remove(pos);
247            if self.current_column_index >= self.columns.len() && self.current_column_index > 0 {
248                self.current_column_index = self.columns.len() - 1;
249            }
250        }
251    }
252
253    pub fn get_header(&self) -> String {
254        let mut header = String::from("  ");
255        for (i, column) in self.columns.iter().enumerate() {
256            if i > 0 {
257                header.push(' ');
258            }
259            match column {
260                Column::Hop => {} // No header for hop number column (3 chars: "XX.")
261                Column::Host => header.push_str(&format!("{:21}", column.header())), // 21 chars
262                Column::Loss => header.push_str(&format!("{:>7}", column.header())), // 7 chars for "XX.X%"
263                Column::Sent => header.push_str(&format!("{:>4}", column.header())), // 4 chars
264                Column::Last | Column::Avg | Column::Ema | Column::Best | Column::Worst => {
265                    header.push_str(&format!("{:>9}", column.header())); // 9 chars for "XXX.Xms"
266                }
267                Column::Jitter | Column::JitterAvg => {
268                    header.push_str(&format!("{:>9}", column.header())); // 9 chars for "XXX.Xms"
269                }
270                Column::Graph => header.push_str(column.header()), // Variable width
271            }
272        }
273        header
274    }
275}
276
277/// Detect the terminal's color support capabilities
278fn detect_color_support() -> ColorSupport {
279    if let Ok(colorterm) = std::env::var("COLORTERM") {
280        if colorterm.contains("truecolor") || colorterm.contains("24bit") {
281            return ColorSupport::TrueColor;
282        }
283    }
284
285    if let Ok(term) = std::env::var("TERM") {
286        if term.contains("256") || term.contains("256color") {
287            return ColorSupport::Extended;
288        }
289        if term.contains("color") || term.starts_with("screen") {
290            return ColorSupport::Basic;
291        }
292    }
293
294    ColorSupport::Basic
295}
296
297// ========================================
298// Color Management
299// ========================================
300
301/// Color scheme functions for RTT visualization
302mod colors {
303    use super::{Color, ColorSupport};
304
305    pub fn get_rtt_color(ratio: f64, color_support: ColorSupport) -> (char, Color) {
306        let level = (ratio.clamp(0.0, 1.0) * 8.0) as usize;
307        let char = ['▁', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][level.min(8)];
308
309        let color = match color_support {
310            ColorSupport::None => Color::White,
311            ColorSupport::Basic => [
312                Color::Green,
313                Color::Green,
314                Color::Cyan,
315                Color::Yellow,
316                Color::Yellow,
317                Color::Magenta,
318                Color::Magenta,
319                Color::Red,
320                Color::Red,
321            ][level.min(8)],
322            ColorSupport::Extended => [17, 21, 39, 75, 111, 179, 215, 208, 130]
323                .get(level)
324                .map(|&i| Color::Indexed(i))
325                .unwrap_or(Color::Indexed(130)),
326            ColorSupport::TrueColor => {
327                let colors = [
328                    (0, 50, 150),
329                    (0, 100, 200),
330                    (50, 150, 255),
331                    (100, 200, 255),
332                    (150, 220, 255),
333                    (255, 200, 100),
334                    (255, 150, 50),
335                    (220, 120, 0),
336                    (150, 80, 0),
337                ];
338                let (r, g, b) = colors[level.min(8)];
339                Color::Rgb(r, g, b)
340            }
341        };
342
343        (char, color)
344    }
345
346    pub fn get_smooth_gradient_color(ratio: f64, color_support: ColorSupport) -> Color {
347        let ratio = ratio.clamp(0.0, 1.0);
348
349        match color_support {
350            ColorSupport::None => Color::White,
351            ColorSupport::Basic => [
352                Color::Blue,
353                Color::Cyan,
354                Color::Yellow,
355                Color::Magenta,
356                Color::Red,
357            ][(ratio * 4.0) as usize],
358            ColorSupport::Extended => {
359                let steps = [17, 21, 33, 39, 75, 111, 179, 215];
360                let index = (ratio * (steps.len() - 1) as f64) as usize;
361                Color::Indexed(steps[index.min(steps.len() - 1)])
362            }
363            ColorSupport::TrueColor => {
364                let (r, g, b) = if ratio < 0.5 {
365                    let t = ratio * 2.0;
366                    interpolate_rgb((0.0, 50.0, 150.0), (150.0, 220.0, 255.0), t)
367                } else {
368                    let t = (ratio - 0.5) * 2.0;
369                    interpolate_rgb((150.0, 220.0, 255.0), (220.0, 120.0, 0.0), t)
370                };
371                Color::Rgb(r as u8, g as u8, b as u8)
372            }
373        }
374    }
375
376    pub fn get_loss_color(color_support: ColorSupport) -> Color {
377        match color_support {
378            ColorSupport::None => Color::White,
379            ColorSupport::Basic => Color::Red,
380            ColorSupport::Extended => Color::Indexed(196),
381            ColorSupport::TrueColor => Color::Rgb(255, 0, 0),
382        }
383    }
384
385    pub fn get_pending_color(color_support: ColorSupport) -> Color {
386        match color_support {
387            ColorSupport::None => Color::White,
388            ColorSupport::Basic => Color::Blue,
389            ColorSupport::Extended => Color::Indexed(27),
390            ColorSupport::TrueColor => Color::Rgb(100, 100, 255),
391        }
392    }
393
394    fn interpolate_rgb(
395        start: (f64, f64, f64),
396        end: (f64, f64, f64),
397        ratio: f64,
398    ) -> (f64, f64, f64) {
399        let ratio = ratio.clamp(0.0, 1.0);
400        (
401            start.0 + (end.0 - start.0) * ratio,
402            start.1 + (end.1 - start.1) * ratio,
403            start.2 + (end.2 - start.2) * ratio,
404        )
405    }
406}
407
408// ========================================
409// Sparkline Visualization
410// ========================================
411
412/// Generate colored sparkline spans for RTT visualization
413fn create_sparkline_spans(
414    hop: &HopStats,
415    global_min_rtt: u64,
416    global_max_rtt: u64,
417    scale: SparklineScale,
418    color_support: ColorSupport,
419    max_width: usize,
420) -> Vec<Span<'static>> {
421    if hop.sent == 0 || max_width == 0 {
422        return vec![];
423    }
424
425    let packet_outcomes: Vec<_> = hop.packet_history.iter().collect();
426    if packet_outcomes.is_empty() {
427        return vec![Span::raw(" ".repeat(max_width))];
428    }
429
430    let data_to_show = if packet_outcomes.len() > max_width {
431        &packet_outcomes[packet_outcomes.len() - max_width..]
432    } else {
433        &packet_outcomes[..]
434    };
435
436    let mut spans: Vec<Span<'static>> = data_to_show
437        .iter()
438        .map(|outcome| match outcome {
439            crate::hop_stats::PacketOutcome::Received(rtt) => {
440                let rtt_ms = (rtt.as_secs_f64() * 1000.0) as u64;
441                let ratio = calculate_rtt_ratio(rtt_ms, global_min_rtt, global_max_rtt, scale);
442                let (char, color) = colors::get_rtt_color(ratio, color_support);
443                Span::styled(char.to_string(), Style::default().fg(color))
444            }
445            crate::hop_stats::PacketOutcome::Lost => {
446                let color = colors::get_loss_color(color_support);
447                Span::styled("·".to_string(), Style::default().fg(color))
448            }
449            crate::hop_stats::PacketOutcome::Pending => {
450                let color = colors::get_pending_color(color_support);
451                Span::styled("?".to_string(), Style::default().fg(color))
452            }
453        })
454        .collect();
455
456    if spans.len() < max_width {
457        spans.push(Span::raw(" ".repeat(max_width - spans.len())));
458    }
459
460    spans
461}
462
463/// Generate colored heatmap spans for RTT visualization (full-height blocks)
464fn create_heatmap_spans(
465    hop: &HopStats,
466    global_min_rtt: u64,
467    global_max_rtt: u64,
468    scale: SparklineScale,
469    color_support: ColorSupport,
470    max_width: usize,
471) -> Vec<Span<'static>> {
472    if hop.sent == 0 || max_width == 0 {
473        return vec![];
474    }
475
476    let packet_outcomes: Vec<_> = hop.packet_history.iter().collect();
477    if packet_outcomes.is_empty() {
478        return vec![Span::raw(" ".repeat(max_width))];
479    }
480
481    let data_to_show = if packet_outcomes.len() > max_width {
482        &packet_outcomes[packet_outcomes.len() - max_width..]
483    } else {
484        &packet_outcomes[..]
485    };
486
487    let mut spans: Vec<Span<'static>> = data_to_show
488        .iter()
489        .map(|outcome| {
490            match outcome {
491                crate::hop_stats::PacketOutcome::Received(rtt) => {
492                    let rtt_ms = (rtt.as_secs_f64() * 1000.0) as u64;
493                    let ratio = calculate_rtt_ratio(rtt_ms, global_min_rtt, global_max_rtt, scale);
494                    // Use full-height block with color based on RTT ratio
495                    let color = colors::get_smooth_gradient_color(ratio, color_support);
496                    Span::styled("█".to_string(), Style::default().fg(color))
497                }
498                crate::hop_stats::PacketOutcome::Lost => {
499                    let color = colors::get_loss_color(color_support);
500                    Span::styled("·".to_string(), Style::default().fg(color))
501                }
502                crate::hop_stats::PacketOutcome::Pending => {
503                    let color = colors::get_pending_color(color_support);
504                    Span::styled("·".to_string(), Style::default().fg(color))
505                }
506            }
507        })
508        .collect();
509
510    if spans.len() < max_width {
511        spans.push(Span::raw(" ".repeat(max_width - spans.len())));
512    }
513
514    spans
515}
516
517fn calculate_rtt_ratio(
518    rtt_ms: u64,
519    global_min: u64,
520    global_max: u64,
521    scale: SparklineScale,
522) -> f64 {
523    if global_min == global_max || rtt_ms == 0 {
524        return 0.0;
525    }
526
527    match scale {
528        SparklineScale::Linear => rtt_ms as f64 / global_max as f64,
529        SparklineScale::Logarithmic => {
530            let log_rtt = ((rtt_ms + 1) as f64).log10();
531            let log_min = ((global_min + 1) as f64).log10();
532            let log_max = ((global_max + 1) as f64).log10();
533            (log_rtt - log_min) / (log_max - log_min)
534        }
535    }
536}
537
538// ========================================
539// Table Components
540// ========================================
541
542/// Table with optional Sixel support
543pub struct EnhancedTable<'a> {
544    table: Table<'a>,
545    sixel_renderer: &'a SixelRenderer,
546    columns: &'a [Column],
547}
548
549impl<'a> EnhancedTable<'a> {
550    pub fn new(table: Table<'a>, sixel_renderer: &'a SixelRenderer, columns: &'a [Column]) -> Self {
551        Self {
552            table,
553            sixel_renderer,
554            columns,
555        }
556    }
557}
558
559impl<'a> Widget for EnhancedTable<'a> {
560    fn render(self, area: Rect, buf: &mut Buffer) {
561        self.table.render(area, buf);
562
563        if !self.sixel_renderer.enabled {
564            return;
565        }
566
567        // Add Sixel graphics for Graph column if present
568        if let Some(_graph_col_idx) = self
569            .columns
570            .iter()
571            .position(|col| matches!(col, Column::Graph))
572        {
573            // Sixel rendering logic would go here
574            // For now, skip detailed implementation since it's complex
575        }
576    }
577}
578
579/// Generate table cells for a hop
580fn create_table_cells(
581    hop: &HopStats,
582    hostname: &str,
583    sparkline_spans: &[Span<'static>],
584    columns: &[Column],
585    sixel_enabled: bool,
586) -> Vec<Cell<'static>> {
587    columns
588        .iter()
589        .map(|column| {
590            match column {
591                Column::Hop => {
592                    if hop.has_multiple_paths() {
593                        Cell::from(format!("{:>1}*", hop.hop))
594                    } else {
595                        Cell::from(format!("{:>2}", hop.hop))
596                    }
597                }
598                Column::Host => Cell::from(hostname.to_owned()),
599                Column::Loss => {
600                    let loss_pct = hop.loss_percent;
601                    let color = if loss_pct > 50.0 {
602                        Color::Red
603                    } else if loss_pct > 10.0 {
604                        Color::Yellow
605                    } else {
606                        Color::Green
607                    };
608                    Cell::from(format!("{:>4.1}%", loss_pct)).style(Style::default().fg(color))
609                }
610                Column::Sent => Cell::from(format!("{:>3}", hop.sent)),
611                Column::Last => {
612                    if let Some(last_rtt) = hop.rtts.back() {
613                        Cell::from(format!("{:>6.1}", last_rtt.as_secs_f64() * 1000.0))
614                    } else {
615                        Cell::from(format!("{:>6}", "???"))
616                    }
617                }
618                Column::Avg => {
619                    if let Some(avg_rtt) = hop.avg_rtt {
620                        Cell::from(format!("{:>6.1}", avg_rtt.as_secs_f64() * 1000.0))
621                    } else {
622                        Cell::from(format!("{:>6}", "???"))
623                    }
624                }
625                Column::Ema => {
626                    if let Some(ema_rtt) = hop.ema_rtt {
627                        Cell::from(format!("{:>6.1}", ema_rtt.as_secs_f64() * 1000.0))
628                    } else {
629                        Cell::from(format!("{:>6}", "???"))
630                    }
631                }
632                Column::Jitter => {
633                    if let Some(jitter) = hop.last_jitter {
634                        Cell::from(format!("{:>6.1}", jitter.as_secs_f64() * 1000.0))
635                    } else {
636                        Cell::from(format!("{:>6}", "???"))
637                    }
638                }
639                Column::JitterAvg => {
640                    if let Some(jitter_avg) = hop.jitter_avg {
641                        Cell::from(format!("{:>6.1}", jitter_avg.as_secs_f64() * 1000.0))
642                    } else {
643                        Cell::from(format!("{:>6}", "???"))
644                    }
645                }
646                Column::Best => {
647                    if let Some(best_rtt) = hop.best_rtt {
648                        Cell::from(format!("{:>6.1}", best_rtt.as_secs_f64() * 1000.0))
649                    } else {
650                        Cell::from(format!("{:>6}", "???"))
651                    }
652                }
653                Column::Worst => {
654                    if let Some(worst_rtt) = hop.worst_rtt {
655                        Cell::from(format!("{:>6.1}", worst_rtt.as_secs_f64() * 1000.0))
656                    } else {
657                        Cell::from(format!("{:>6}", "???"))
658                    }
659                }
660                Column::Graph => {
661                    if sixel_enabled {
662                        Cell::from("") // Sixel will fill this
663                    } else if !sparkline_spans.is_empty() {
664                        Cell::from(Line::from(sparkline_spans.to_vec()))
665                    } else {
666                        Cell::from("")
667                    }
668                }
669            }
670        })
671        .collect()
672}
673
674/// Generate column constraints with dynamic sizing for Host and Graph columns
675fn create_column_constraints(columns: &[Column]) -> Vec<Constraint> {
676    let has_graph = columns.iter().any(|col| matches!(col, Column::Graph));
677
678    columns
679        .iter()
680        .map(|column| {
681            match column {
682                Column::Hop => Constraint::Length(3),
683                Column::Host => {
684                    if has_graph {
685                        // Use 35% of available space when graph is present (increased for multi-path)
686                        Constraint::Percentage(35)
687                    } else {
688                        Constraint::Min(20)
689                    }
690                }
691                Column::Loss => Constraint::Length(5),
692                Column::Sent => Constraint::Length(3),
693                Column::Last | Column::Avg | Column::Ema | Column::Best | Column::Worst => {
694                    if has_graph {
695                        Constraint::Length(6)
696                    } else {
697                        Constraint::Length(9)
698                    }
699                }
700                Column::Jitter | Column::JitterAvg => {
701                    if has_graph {
702                        Constraint::Length(6)
703                    } else {
704                        Constraint::Length(9)
705                    }
706                }
707                Column::Graph => Constraint::Percentage(65), // Use 65% of available space (reduced to accommodate larger hostname column)
708            }
709        })
710        .collect()
711}
712
713// ========================================
714// Widget Creation Functions
715// ========================================
716
717/// Create inline status text without borders
718fn create_status_text(session: &MtrSession, ui_state: &UiState) -> Line<'static> {
719    let total_sent: usize = session.hops.iter().map(|h| h.sent).sum();
720    let total_received: usize = session.hops.iter().map(|h| h.received).sum();
721    let overall_loss = if total_sent > 0 {
722        ((total_sent - total_received) as f64 / total_sent as f64) * 100.0
723    } else {
724        0.0
725    };
726
727    let active_hops = session.hops.iter().filter(|h| h.sent > 0).count();
728    let scale_name = match ui_state.current_sparkline_scale {
729        SparklineScale::Linear => "Linear",
730        SparklineScale::Logarithmic => "Log",
731    };
732
733    let viz_mode = match ui_state.visualization_mode {
734        VisualizationMode::Sparkline => "Sparkline",
735        VisualizationMode::Heatmap => "Heatmap",
736    };
737
738    let hostname_mode = if ui_state.show_hostnames {
739        "Hostnames"
740    } else {
741        "IPs"
742    };
743
744    let main_text = format!(
745        "mtr-ng: {} → {} | Hops: {} | Sent: {} | Loss: {:.1}% | Scale: {} | Mode: {} | Display: {}",
746        session.target,
747        session.target_addr,
748        active_hops,
749        total_sent,
750        overall_loss,
751        scale_name,
752        viz_mode,
753        hostname_mode
754    );
755
756    Line::from(vec![
757        Span::raw(main_text),
758        Span::raw(" | "),
759        Span::styled("? for help", Style::default().fg(Color::Gray)),
760    ])
761}
762
763/// Create column selection popup
764fn create_column_selector_popup(state: &ColumnSelectorState) -> Paragraph<'static> {
765    let mut lines = vec![
766        Line::from(vec![Span::styled(
767            "Column Selection & Ordering",
768            Style::default().fg(Color::Yellow),
769        )]),
770        Line::from(""),
771        Line::from(vec![
772            Span::styled("↑/↓", Style::default().fg(Color::Green)),
773            Span::raw(" - Navigate  "),
774            Span::styled("Space", Style::default().fg(Color::Green)),
775            Span::raw(" - Toggle"),
776        ]),
777        Line::from(vec![
778            Span::styled("←/→", Style::default().fg(Color::Green)),
779            Span::raw(" or "),
780            Span::styled("Shift+↑/↓", Style::default().fg(Color::Green)),
781            Span::raw(" - Reorder columns"),
782        ]),
783        Line::from(""),
784    ];
785
786    for (i, (column, enabled)) in state.available_columns.iter().enumerate() {
787        let column_name = match column {
788            Column::Hop => "Hop Number",
789            Column::Host => "Hostname/IP",
790            Column::Loss => "Packet Loss %",
791            Column::Sent => "Packets Sent",
792            Column::Last => "Last RTT",
793            Column::Avg => "Average RTT",
794            Column::Ema => "EMA RTT",
795            Column::Jitter => "Last Jitter",
796            Column::JitterAvg => "Average Jitter",
797            Column::Best => "Best RTT",
798            Column::Worst => "Worst RTT",
799            Column::Graph => "RTT Graph",
800        };
801
802        let checkbox = if *enabled { "☑" } else { "☐" };
803        let is_selected = i == state.selected_index;
804
805        let style = if is_selected {
806            Style::default().fg(Color::Black).bg(Color::White)
807        } else {
808            Style::default()
809        };
810
811        let checkbox_style = if *enabled {
812            Style::default().fg(Color::Green)
813        } else {
814            Style::default().fg(Color::Gray)
815        };
816
817        // Add position indicator (no cursor needed)
818        let position_indicator = format!("{:2}.", i + 1);
819
820        lines.push(Line::from(vec![
821            Span::styled(
822                format!("{} ", position_indicator),
823                if is_selected {
824                    Style::default().fg(Color::Yellow).bg(Color::White)
825                } else {
826                    Style::default().fg(Color::Gray)
827                },
828            ),
829            Span::styled(
830                format!(" {} ", checkbox),
831                if is_selected {
832                    checkbox_style.bg(Color::White)
833                } else {
834                    checkbox_style
835                },
836            ),
837            Span::styled(format!("{}", column_name), style),
838        ]));
839    }
840
841    lines.push(Line::from(""));
842    lines.push(Line::from(vec![
843        Span::styled("Esc", Style::default().fg(Color::Green)),
844        Span::raw(" - Close"),
845    ]));
846
847    // Add debug info showing current selection
848    lines.push(Line::from(vec![Span::styled(
849        format!(
850            "Selection: {} of {}",
851            state.selected_index + 1,
852            state.available_columns.len()
853        ),
854        Style::default().fg(Color::Cyan),
855    )]));
856
857    Paragraph::new(lines)
858        .block(
859            Block::default()
860                .borders(Borders::ALL)
861                .title("Column Settings")
862                .title_alignment(Alignment::Center),
863        )
864        .alignment(Alignment::Left)
865}
866
867/// Create help overlay with keyboard shortcuts
868fn create_help_overlay() -> Paragraph<'static> {
869    let help_text = vec![
870        Line::from(vec![Span::styled(
871            "Keyboard Shortcuts",
872            Style::default().fg(Color::Yellow),
873        )]),
874        Line::from(""),
875        Line::from(vec![
876            Span::styled("q", Style::default().fg(Color::Green)),
877            Span::raw(" / "),
878            Span::styled("ESC", Style::default().fg(Color::Green)),
879            Span::raw("  - Quit application"),
880        ]),
881        Line::from(vec![
882            Span::styled("r", Style::default().fg(Color::Green)),
883            Span::raw("        - Reset statistics"),
884        ]),
885        Line::from(vec![
886            Span::styled("s", Style::default().fg(Color::Green)),
887            Span::raw("        - Toggle sparkline scale (Linear/Log)"),
888        ]),
889        Line::from(vec![
890            Span::styled("c", Style::default().fg(Color::Green)),
891            Span::raw("        - Cycle color modes"),
892        ]),
893        Line::from(vec![
894            Span::styled("f", Style::default().fg(Color::Green)),
895            Span::raw("        - Toggle column fields"),
896        ]),
897        Line::from(vec![
898            Span::styled("o", Style::default().fg(Color::Green)),
899            Span::raw("        - Open column selector"),
900        ]),
901        Line::from(vec![
902            Span::styled("v", Style::default().fg(Color::Green)),
903            Span::raw("        - Toggle visualization (Sparkline/Heatmap)"),
904        ]),
905        Line::from(vec![
906            Span::styled("h", Style::default().fg(Color::Green)),
907            Span::raw("        - Toggle hostnames/IP addresses"),
908        ]),
909        Line::from(vec![
910            Span::styled("?", Style::default().fg(Color::Green)),
911            Span::raw("        - Toggle this help"),
912        ]),
913        Line::from(""),
914        Line::from(vec![Span::styled(
915            "Press ? again to close",
916            Style::default().fg(Color::Cyan),
917        )]),
918    ];
919
920    Paragraph::new(help_text)
921        .block(
922            Block::default()
923                .borders(Borders::ALL)
924                .title("Help")
925                .title_alignment(Alignment::Center),
926        )
927        .alignment(Alignment::Left)
928}
929
930/// Create compact scale visualization with multiple x-axis labels
931fn create_scale_widget(
932    min_rtt: u64,
933    max_rtt: u64,
934    scale: SparklineScale,
935    color_support: ColorSupport,
936    width: usize,
937) -> Paragraph<'static> {
938    if min_rtt == max_rtt {
939        return Paragraph::new("No RTT data");
940    }
941
942    let scale_width = (width / 2).clamp(40, 80);
943    let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
944
945    let scale_spans: Vec<Span> = (0..scale_width)
946        .map(|i| {
947            let ratio = i as f64 / (scale_width - 1) as f64;
948            let level = (ratio * 7.0) as usize;
949            let char = chars[level.min(7)];
950            let color = colors::get_smooth_gradient_color(ratio, color_support);
951            Span::styled(char.to_string(), Style::default().fg(color))
952        })
953        .collect();
954
955    let scale_name = match scale {
956        SparklineScale::Linear => "Linear",
957        SparklineScale::Logarithmic => "Log₁₀",
958    };
959
960    // Create x-axis labels - use 5 evenly spaced points
961    let num_labels = 5;
962    let mut label_info = Vec::new();
963
964    for i in 0..num_labels {
965        let ratio = i as f64 / (num_labels - 1) as f64;
966
967        let value = match scale {
968            SparklineScale::Linear => min_rtt + (ratio * (max_rtt - min_rtt) as f64) as u64,
969            SparklineScale::Logarithmic => {
970                let log_min = (min_rtt as f64 + 1.0).ln();
971                let log_max = (max_rtt as f64 + 1.0).ln();
972                let log_value = log_min + ratio * (log_max - log_min);
973                (log_value.exp() - 1.0) as u64
974            }
975        };
976
977        let label = if value < 1000 {
978            format!("{}ms", value)
979        } else {
980            format!("{:.1}s", value as f64 / 1000.0)
981        };
982
983        // Calculate the center position for this label on the gradient
984        let center_pos = (ratio * (scale_width - 1) as f64) as usize;
985
986        label_info.push((label, center_pos));
987    }
988
989    // Build the label line with centered positioning
990    let mut label_spans = Vec::new();
991    let mut current_pos = 0;
992
993    for (i, (label, center_pos)) in label_info.iter().enumerate() {
994        // Calculate where this label should start to be centered at center_pos
995        let label_len = label.len();
996        let label_start = center_pos.saturating_sub(label_len / 2);
997
998        // Add spacing to reach the label start position
999        if label_start > current_pos {
1000            let padding = label_start - current_pos;
1001            label_spans.push(Span::raw(" ".repeat(padding)));
1002            current_pos += padding;
1003        }
1004
1005        // Add the label
1006        label_spans.push(Span::raw(label.clone()));
1007        current_pos += label_len;
1008
1009        // For the last label, add scale type if there's space
1010        if i == label_info.len() - 1 {
1011            let remaining_space = scale_width.saturating_sub(current_pos);
1012            if remaining_space > scale_name.len() + 4 {
1013                label_spans.push(Span::raw(
1014                    " ".repeat(remaining_space - scale_name.len() - 3),
1015                ));
1016                label_spans.push(Span::styled(
1017                    format!("({})", scale_name),
1018                    Style::default().fg(Color::Gray),
1019                ));
1020            }
1021        }
1022    }
1023
1024    let scale_text = vec![Line::from(label_spans), Line::from(scale_spans)];
1025
1026    Paragraph::new(scale_text)
1027}
1028
1029// ========================================
1030// Main UI Rendering
1031// ========================================
1032
1033/// Main UI rendering function - now much more compact
1034/// Renders the main UI layout with status, table, and scale components
1035///
1036/// This function creates a 3-section layout:
1037/// 1. Status line - Shows connection info, statistics, and current modes  
1038/// 2. Main table - Displays hop data with optional graph visualization
1039/// 3. Scale widget - Shows RTT scale with gradient and labeled axis
1040///
1041/// The function also handles the help overlay when toggled by the user.
1042pub fn render_ui(f: &mut Frame, session: &MtrSession, ui_state: &UiState) {
1043    let area = f.area();
1044
1045    // Minimum size check
1046    if area.height < 10 || area.width < 50 {
1047        let fallback = Paragraph::new(format!(
1048            "Terminal too small: {}x{}\nMinimum: 50x10\nPress 'q' to quit",
1049            area.width, area.height
1050        ));
1051        f.render_widget(fallback, area);
1052        return;
1053    }
1054
1055    // Compact layout - no margins, minimal spacing
1056    let chunks = Layout::default()
1057        .direction(Direction::Vertical)
1058        .constraints([
1059            Constraint::Length(1), // Status line
1060            Constraint::Min(5),    // Main table
1061            Constraint::Length(2), // Scale (compact)
1062        ])
1063        .split(area);
1064
1065    // Get RTT range for scaling
1066    let rtt_values: Vec<u64> = session
1067        .hops
1068        .iter()
1069        .filter(|hop| hop.sent > 0)
1070        .flat_map(|hop| hop.rtts.iter())
1071        .map(|d| (d.as_secs_f64() * 1000.0) as u64)
1072        .collect();
1073
1074    let global_max_rtt = rtt_values.iter().max().copied().unwrap_or(1);
1075    let global_min_rtt = rtt_values.iter().min().copied().unwrap_or(1);
1076
1077    // Status line (no borders)
1078    let status_line = create_status_text(session, ui_state);
1079    let status = Paragraph::new(vec![status_line]);
1080    f.render_widget(status, chunks[0]);
1081
1082    // Main table
1083    let header_cells = ui_state.columns.iter().map(|col| match col {
1084        Column::Loss
1085        | Column::Sent
1086        | Column::Last
1087        | Column::Avg
1088        | Column::Ema
1089        | Column::Jitter
1090        | Column::JitterAvg
1091        | Column::Best
1092        | Column::Worst => Cell::from(format!("{:>width$}", col.header(), width = col.width())),
1093        _ => Cell::from(col.header()),
1094    });
1095
1096    let header = Row::new(header_cells).style(Style::default().fg(Color::Yellow));
1097
1098    let mut rows = Vec::new();
1099
1100    for hop in session.hops.iter().filter(|hop| hop.sent > 0) {
1101        let hostname = format_hostname(session, hop, ui_state);
1102        let graph_width = calculate_graph_width(&chunks[1], &ui_state.columns);
1103
1104        let graph_spans = match ui_state.visualization_mode {
1105            VisualizationMode::Sparkline => create_sparkline_spans(
1106                hop,
1107                global_min_rtt,
1108                global_max_rtt,
1109                ui_state.current_sparkline_scale,
1110                ui_state.color_support,
1111                graph_width,
1112            ),
1113            VisualizationMode::Heatmap => create_heatmap_spans(
1114                hop,
1115                global_min_rtt,
1116                global_max_rtt,
1117                ui_state.current_sparkline_scale,
1118                ui_state.color_support,
1119                graph_width,
1120            ),
1121        };
1122
1123        let cells = create_table_cells(
1124            hop,
1125            &hostname,
1126            &graph_spans,
1127            &ui_state.columns,
1128            ui_state.sixel_renderer.enabled,
1129        );
1130
1131        rows.push(Row::new(cells));
1132
1133        // Add alternate paths if multi-path is detected
1134        if hop.has_multiple_paths() {
1135            for alt_path in hop.get_alternate_paths() {
1136                let percentage = hop.get_path_percentage(alt_path);
1137
1138                // Format hostname with proper length, including percentage
1139                let alt_hostname = if let Some(hostname) = &alt_path.hostname {
1140                    let full_name =
1141                        format!("  ↳ {} ({}) ({:.0}%)", hostname, alt_path.addr, percentage);
1142                    if full_name.len() > 50 {
1143                        format!(
1144                            "  ↳ {}...{} ({:.0}%)",
1145                            &hostname[..15],
1146                            alt_path.addr,
1147                            percentage
1148                        )
1149                    } else {
1150                        full_name
1151                    }
1152                } else {
1153                    format!("  ↳ {} ({:.0}%)", alt_path.addr, percentage)
1154                };
1155
1156                let alt_rtt = alt_path.last_rtt.unwrap_or_default().as_secs_f64() * 1000.0;
1157
1158                // Create cells for each column, focusing on key info
1159                let mut alt_cells = Vec::new();
1160                for column in &ui_state.columns {
1161                    match column {
1162                        Column::Hop => alt_cells.push(Cell::from("")),
1163                        Column::Host => alt_cells.push(Cell::from(alt_hostname.clone())),
1164                        Column::Loss => alt_cells.push(Cell::from("")), // Empty - percentage is now in hostname
1165                        Column::Sent => alt_cells.push(Cell::from("")),
1166                        Column::Last => alt_cells.push(Cell::from(format!("{:.1}", alt_rtt))),
1167                        Column::Avg => alt_cells.push(Cell::from("")),
1168                        Column::Ema => alt_cells.push(Cell::from("")),
1169                        Column::Best => alt_cells.push(Cell::from("")),
1170                        Column::Worst => alt_cells.push(Cell::from("")),
1171                        Column::Jitter => alt_cells.push(Cell::from("")),
1172                        Column::JitterAvg => alt_cells.push(Cell::from("")),
1173                        Column::Graph => alt_cells.push(Cell::from("")),
1174                    }
1175                }
1176
1177                let alt_row = Row::new(alt_cells);
1178                rows.push(alt_row);
1179            }
1180        }
1181    }
1182
1183    let constraints = create_column_constraints(&ui_state.columns);
1184    let table = Table::new(rows, &constraints).header(header);
1185
1186    // Use enhanced table for Sixel support
1187    if ui_state.sixel_renderer.enabled {
1188        let enhanced_table = EnhancedTable::new(table, &ui_state.sixel_renderer, &ui_state.columns);
1189        f.render_widget(enhanced_table, chunks[1]);
1190    } else {
1191        f.render_widget(table, chunks[1]);
1192    }
1193
1194    // Compact scale visualization
1195    let scale_widget = create_scale_widget(
1196        global_min_rtt,
1197        global_max_rtt,
1198        ui_state.current_sparkline_scale,
1199        ui_state.color_support,
1200        chunks[2].width as usize,
1201    );
1202    f.render_widget(scale_widget, chunks[2]);
1203
1204    // Show help overlay if enabled
1205    if ui_state.show_help {
1206        let area = f.area();
1207        // Center the help overlay
1208        let help_width = 50.min(area.width.saturating_sub(4));
1209        let help_height = 12.min(area.height.saturating_sub(4));
1210        let help_x = (area.width.saturating_sub(help_width)) / 2;
1211        let help_y = (area.height.saturating_sub(help_height)) / 2;
1212
1213        let help_area = Rect {
1214            x: help_x,
1215            y: help_y,
1216            width: help_width,
1217            height: help_height,
1218        };
1219
1220        // Clear the background and render help
1221        f.render_widget(Clear, help_area);
1222        f.render_widget(create_help_overlay(), help_area);
1223    }
1224
1225    // Show column selector popup if enabled
1226    if ui_state.show_column_selector {
1227        let area = f.area();
1228        // Center the column selector popup - make it larger than help
1229        let popup_width = 60.min(area.width.saturating_sub(4));
1230        let popup_height = (ui_state.column_selector_state.available_columns.len() + 8)
1231            .min(area.height.saturating_sub(4) as usize) as u16;
1232        let popup_x = (area.width.saturating_sub(popup_width)) / 2;
1233        let popup_y = (area.height.saturating_sub(popup_height)) / 2;
1234
1235        let popup_area = Rect {
1236            x: popup_x,
1237            y: popup_y,
1238            width: popup_width,
1239            height: popup_height,
1240        };
1241
1242        // Clear the background and render column selector
1243        f.render_widget(Clear, popup_area);
1244        f.render_widget(
1245            create_column_selector_popup(&ui_state.column_selector_state),
1246            popup_area,
1247        );
1248    }
1249}
1250
1251fn format_hostname(session: &MtrSession, hop: &HopStats, ui_state: &UiState) -> String {
1252    let base_hostname = if session.args.numeric || !ui_state.show_hostnames {
1253        // Show IP addresses when numeric mode or hostname toggle is off
1254        hop.addr
1255            .map(|a| a.to_string())
1256            .unwrap_or_else(|| "???".to_string())
1257    } else {
1258        // Show hostnames when available, fallback to IP
1259        hop.hostname.clone().unwrap_or_else(|| {
1260            hop.addr
1261                .map(|a| a.to_string())
1262                .unwrap_or_else(|| "???".to_string())
1263        })
1264    };
1265
1266    // Add primary path percentage if multi-path
1267    let hostname = if hop.has_multiple_paths() {
1268        if hop.addr.is_some() {
1269            let primary_percentage = hop.get_primary_path_percentage();
1270            format!("{} ({:.0}%)", base_hostname, primary_percentage)
1271        } else {
1272            base_hostname
1273        }
1274    } else {
1275        base_hostname
1276    };
1277
1278    // With 20% width allocation, truncate longer hostnames appropriately
1279    const MAX_HOSTNAME_LEN: usize = 40; // Increased to accommodate percentage
1280    const TRUNCATED_LEN: usize = 37;
1281
1282    if hostname.len() > MAX_HOSTNAME_LEN {
1283        format!("{}...", &hostname[..TRUNCATED_LEN])
1284    } else {
1285        hostname
1286    }
1287}
1288
1289fn calculate_graph_width(table_area: &Rect, columns: &[Column]) -> usize {
1290    if columns.contains(&Column::Graph) {
1291        // Calculate actual width available for graph column (80% of remaining space)
1292        let total_width = table_area.width.saturating_sub(4) as usize; // Account for borders
1293
1294        // Calculate space used by fixed-width columns
1295        let fixed_width: usize = columns
1296            .iter()
1297            .map(|col| {
1298                match col {
1299                    Column::Hop => 3,
1300                    Column::Loss => 5,
1301                    Column::Sent => 3,
1302                    Column::Last | Column::Avg | Column::Ema | Column::Best | Column::Worst => 6,
1303                    Column::Jitter | Column::JitterAvg => 6,
1304                    Column::Host | Column::Graph => 0, // These use percentage-based sizing
1305                }
1306            })
1307            .sum();
1308
1309        // Remaining space for Host (35%) and Graph (65%) columns
1310        let remaining_width = total_width.saturating_sub(fixed_width);
1311        let graph_width = (remaining_width * 65) / 100;
1312
1313        // Ensure minimum usable width but no upper cap
1314        graph_width.max(20)
1315    } else {
1316        20
1317    }
1318}
1319
1320// ========================================
1321// Interactive Event Loop
1322// ========================================
1323
1324pub async fn run_interactive(session: MtrSession) -> Result<()> {
1325    enable_raw_mode()?;
1326    let mut stdout = io::stdout();
1327    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
1328    let backend = CrosstermBackend::new(stdout);
1329    let mut terminal = Terminal::new(backend)?;
1330
1331    let session_arc = Arc::new(Mutex::new(session.clone()));
1332    let session_clone = Arc::clone(&session_arc);
1333
1334    let mut ui_state = UiState::new(
1335        session.args.sparkline_scale,
1336        session.args.get_columns(),
1337        session.args.sixel,
1338    );
1339
1340    let (update_tx, mut update_rx) = mpsc::unbounded_channel::<()>();
1341
1342    {
1343        let mut session_guard = session_arc.lock().unwrap();
1344        let update_tx_for_callback = update_tx.clone();
1345        session_guard.set_update_callback(Arc::new(move || {
1346            let _ = update_tx_for_callback.send(());
1347        }));
1348    }
1349
1350    let trace_handle = {
1351        let session_for_trace = Arc::clone(&session_clone);
1352        tokio::spawn(async move {
1353            if let Err(e) = MtrSession::run_trace_with_realtime_updates(session_for_trace).await {
1354                debug!("Real-time trace failed: {}", e);
1355            }
1356        })
1357    };
1358
1359    let mut last_tick = Instant::now();
1360    let tick_rate = Duration::from_millis(100);
1361
1362    loop {
1363        let should_update = update_rx.try_recv().is_ok() || last_tick.elapsed() >= tick_rate;
1364
1365        if should_update {
1366            // Lock session only during rendering to get live updates
1367            terminal.draw(|f| {
1368                let session_guard = session_clone.lock().unwrap();
1369                render_ui(f, &session_guard, &ui_state)
1370            })?;
1371            last_tick = Instant::now();
1372        }
1373
1374        let timeout = Duration::from_millis(10);
1375        if crossterm::event::poll(timeout)? {
1376            if let Event::Key(key) = event::read()? {
1377                // Handle column selector popup inputs first
1378                if ui_state.show_column_selector {
1379                    match key.code {
1380                        KeyCode::Esc => {
1381                            // Close column selector
1382                            ui_state.show_column_selector = false;
1383                        }
1384                        KeyCode::Up => {
1385                            ui_state.column_selector_state.move_up();
1386                        }
1387                        KeyCode::Down => {
1388                            ui_state.column_selector_state.move_down();
1389                        }
1390                        KeyCode::Char(' ') => {
1391                            ui_state.toggle_selected_column_immediate();
1392                        }
1393                        KeyCode::Left => {
1394                            // Move selected column up in list
1395                            ui_state.move_selected_column_up_immediate();
1396                        }
1397                        KeyCode::Right => {
1398                            // Move selected column down in list
1399                            ui_state.move_selected_column_down_immediate();
1400                        }
1401                        _ => {
1402                            // Check for Shift+Up/Down for reordering (alternative to Left/Right)
1403                            if key.modifiers == crossterm::event::KeyModifiers::SHIFT {
1404                                match key.code {
1405                                    KeyCode::Up => {
1406                                        ui_state.move_selected_column_up_immediate();
1407                                    }
1408                                    KeyCode::Down => {
1409                                        ui_state.move_selected_column_down_immediate();
1410                                    }
1411                                    _ => {}
1412                                }
1413                            }
1414                        }
1415                    }
1416                } else {
1417                    // Handle normal keyboard shortcuts
1418                    match key.code {
1419                        KeyCode::Char('q') | KeyCode::Esc => break,
1420                        KeyCode::Char('r') => {
1421                            let mut session_guard = session_clone.lock().unwrap();
1422                            for hop in &mut session_guard.hops {
1423                                *hop = HopStats::new(hop.hop);
1424                            }
1425                        }
1426                        KeyCode::Char('s') => ui_state.toggle_sparkline_scale(),
1427                        KeyCode::Char('c') => ui_state.cycle_color_mode(),
1428                        KeyCode::Char('f') => ui_state.toggle_column(),
1429                        KeyCode::Char('o') => ui_state.toggle_column_selector(),
1430                        KeyCode::Char('v') => ui_state.toggle_visualization_mode(),
1431                        KeyCode::Char('h') => ui_state.toggle_hostnames(),
1432                        KeyCode::Char('?') => ui_state.toggle_help(),
1433                        _ => {}
1434                    }
1435                }
1436            }
1437        }
1438    }
1439
1440    trace_handle.abort();
1441    disable_raw_mode()?;
1442    execute!(
1443        terminal.backend_mut(),
1444        LeaveAlternateScreen,
1445        DisableMouseCapture
1446    )?;
1447    terminal.show_cursor()?;
1448
1449    Ok(())
1450}