Skip to main content

netwatch_rs/
display.rs

1use crate::{
2    cli::{DataUnit, TrafficUnit},
3    config::Config,
4    device::{Device, NetworkReader},
5    input::InputEvent,
6    logger::TrafficLogger,
7    stats::StatsCalculator,
8};
9use anyhow::Result;
10use crossterm::event::{self, Event};
11use ratatui::{
12    backend::CrosstermBackend,
13    layout::{Constraint, Direction, Layout},
14    style::{Color, Modifier, Style},
15    text::{Line, Span},
16    widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph},
17    Frame, Terminal,
18};
19use std::{
20    collections::HashMap,
21    time::{Duration, Instant},
22};
23
24pub struct DisplayState {
25    pub current_device_index: usize,
26    pub devices: Vec<Device>,
27    pub show_multiple: bool,
28    pub show_graphs: bool,
29    pub paused: bool,
30    pub traffic_unit: TrafficUnit,
31    pub data_unit: DataUnit,
32    pub max_incoming: u64, // 0 = auto-scale
33    pub max_outgoing: u64, // 0 = auto-scale
34    pub zoom_level: f64,   // Graph zoom multiplier
35    pub show_options: bool,
36    pub settings_message: Option<String>,
37}
38
39impl DisplayState {
40    pub fn new(devices: Vec<String>, config: &Config) -> Self {
41        let devices: Vec<Device> = devices.into_iter().map(Device::new).collect();
42
43        Self {
44            current_device_index: 0,
45            devices,
46            show_multiple: config.multiple_devices,
47            show_graphs: true,
48            paused: false,
49            traffic_unit: config.get_traffic_unit(),
50            data_unit: config.get_data_unit(),
51            max_incoming: config.max_incoming,
52            max_outgoing: config.max_outgoing,
53            zoom_level: 1.0,
54            show_options: false,
55            settings_message: None,
56        }
57    }
58}
59
60pub fn run_ui(
61    interfaces: Vec<String>,
62    reader: Box<dyn NetworkReader>,
63    mut config: Config,
64    log_file: Option<String>,
65) -> Result<()> {
66    let backend = CrosstermBackend::new(std::io::stdout());
67    let mut terminal = Terminal::new(backend)?;
68
69    let mut state = DisplayState::new(interfaces, &config);
70    let mut stats_calculators: HashMap<String, StatsCalculator> = HashMap::new();
71    let mut logger = if log_file.is_some() {
72        Some(TrafficLogger::new(log_file)?)
73    } else {
74        None
75    };
76
77    // Initialize stats calculators for each device
78    for device in &state.devices {
79        stats_calculators.insert(
80            device.name.clone(),
81            StatsCalculator::new(Duration::from_secs(config.average_window as u64)),
82        );
83    }
84
85    let refresh_interval = Duration::from_millis(config.refresh_interval);
86    let mut last_update = Instant::now();
87
88    loop {
89        // Handle input events - scale polling based on refresh rate for performance
90        let poll_interval = if config.high_performance {
91            (config.refresh_interval / 5).clamp(100, 200)
92        } else {
93            (config.refresh_interval / 10).clamp(50, 100)
94        };
95        if event::poll(Duration::from_millis(poll_interval))? {
96            if let Event::Key(key_event) = event::read()? {
97                let input_event = InputEvent::from_key_event(key_event);
98
99                if handle_input(&mut state, &mut stats_calculators, input_event, &mut config)? {
100                    break; // Quit requested
101                }
102            }
103        }
104
105        // Update network statistics
106        if !state.paused && last_update.elapsed() >= refresh_interval {
107            let mut high_traffic_detected = false;
108
109            for device in &mut state.devices {
110                if device.update(reader.as_ref()).is_err() {
111                    // Device unavailable, continue with others
112                    continue;
113                }
114
115                if let Some(calculator) = stats_calculators.get_mut(&device.name) {
116                    calculator.add_sample(device.stats.clone());
117
118                    // Check for high traffic conditions (>100MB/s total or >1000 packets/s)
119                    let (speed_in, speed_out) = calculator.current_speed();
120                    let (packets_in, packets_out) = calculator.total_packets();
121                    let total_speed = speed_in + speed_out;
122                    if total_speed > 100_000_000 || packets_in + packets_out > 1000 {
123                        high_traffic_detected = true;
124                    }
125
126                    // Log traffic if logger is enabled
127                    if let Some(ref mut logger) = logger {
128                        let _ = logger.log_traffic(&device.name, calculator);
129                    }
130                }
131            }
132
133            // Auto-enable high performance security monitoring under heavy load
134            if high_traffic_detected && !config.high_performance {
135                crate::security::enable_high_performance_security(true);
136            }
137
138            last_update = Instant::now();
139        }
140
141        // Draw UI
142        terminal.draw(|f| {
143            draw_ui(f, &state, &stats_calculators, &config);
144        })?;
145    }
146
147    Ok(())
148}
149
150fn handle_input(
151    state: &mut DisplayState,
152    stats_calculators: &mut HashMap<String, StatsCalculator>,
153    event: InputEvent,
154    config: &mut Config,
155) -> Result<bool> {
156    // Handle dashboard-specific events
157    match event {
158        InputEvent::NextPanel
159        | InputEvent::PrevPanel
160        | InputEvent::NextItem
161        | InputEvent::PrevItem => {
162            // These are dashboard-specific events, ignore in legacy mode
163            return Ok(false);
164        }
165        _ => {}
166    }
167
168    // If options window is open, handle settings changes
169    if state.show_options {
170        match event {
171            InputEvent::ShowOptions | InputEvent::Quit => {
172                state.show_options = false;
173                state.settings_message = None; // Clear status message
174                return Ok(false);
175            }
176            // Allow users to change settings while in options window
177            InputEvent::ToggleTrafficUnits => {
178                state.traffic_unit = state.traffic_unit.next();
179                return Ok(false);
180            }
181            InputEvent::ToggleDataUnits => {
182                state.data_unit = state.data_unit.next();
183                return Ok(false);
184            }
185            // These settings only change display mode, which would be confusing
186            // in options window - disable them for better UX
187            InputEvent::ToggleGraphs | InputEvent::ToggleMultiple => {
188                // Ignore view-changing commands while in options
189                return Ok(false);
190            }
191            InputEvent::Pause => {
192                state.paused = !state.paused;
193                return Ok(false);
194            }
195            InputEvent::ZoomIn => {
196                state.zoom_level = (state.zoom_level * 1.5).min(10.0);
197                return Ok(false);
198            }
199            InputEvent::ZoomOut => {
200                state.zoom_level = (state.zoom_level / 1.5).max(0.1);
201                return Ok(false);
202            }
203            InputEvent::SaveSettings => {
204                // Update config with current state values
205                config.traffic_format = state.traffic_unit.to_string().to_string();
206                config.data_format = state.data_unit.to_string().to_string();
207                config.multiple_devices = state.show_multiple;
208                config.max_incoming = state.max_incoming;
209                config.max_outgoing = state.max_outgoing;
210
211                // Save to file
212                match config.save() {
213                    Ok(_) => {
214                        state.settings_message =
215                            Some("✅ Settings saved to ~/.netwatch".to_string())
216                    }
217                    Err(e) => state.settings_message = Some(format!("❌ Save failed: {e}")),
218                }
219                return Ok(false);
220            }
221            InputEvent::IncreaseRefresh => {
222                config.refresh_interval = (config.refresh_interval.saturating_sub(50)).max(50); // Min 50ms
223                return Ok(false);
224            }
225            InputEvent::DecreaseRefresh => {
226                config.refresh_interval = (config.refresh_interval + 50).min(2000); // Max 2000ms
227                return Ok(false);
228            }
229            InputEvent::IncreaseAverage => {
230                config.average_window = (config.average_window + 30).min(1800); // Max 30 minutes
231                return Ok(false);
232            }
233            InputEvent::DecreaseAverage => {
234                config.average_window = (config.average_window.saturating_sub(30)).max(30); // Min 30 seconds
235                return Ok(false);
236            }
237            InputEvent::ReloadSettings => {
238                // Reload settings from config file
239                match Config::load() {
240                    Ok(new_config) => {
241                        *config = new_config;
242                        // Update state with reloaded config
243                        state.traffic_unit = config.get_traffic_unit();
244                        state.data_unit = config.get_data_unit();
245                        state.show_multiple = config.multiple_devices;
246                        state.max_incoming = config.max_incoming;
247                        state.max_outgoing = config.max_outgoing;
248                        state.settings_message =
249                            Some("✅ Settings reloaded from ~/.netwatch".to_string());
250                    }
251                    Err(e) => {
252                        state.settings_message = Some(format!("❌ Reload failed: {e}"));
253                    }
254                }
255                return Ok(false);
256            }
257            _ => {
258                // Ignore navigation and other non-settings commands
259                return Ok(false);
260            }
261        }
262    }
263
264    match event {
265        InputEvent::Quit => return Ok(true),
266
267        InputEvent::NextDevice => {
268            if !state.devices.is_empty() {
269                state.current_device_index = (state.current_device_index + 1) % state.devices.len();
270            }
271        }
272
273        InputEvent::PrevDevice => {
274            if !state.devices.is_empty() {
275                state.current_device_index = if state.current_device_index == 0 {
276                    state.devices.len() - 1
277                } else {
278                    state.current_device_index - 1
279                };
280            }
281        }
282
283        InputEvent::Reset => {
284            // Reset statistics for current device
285            if let Some(device) = state.devices.get(state.current_device_index) {
286                if let Some(calculator) = stats_calculators.get_mut(&device.name) {
287                    calculator.reset();
288                }
289            }
290        }
291
292        InputEvent::Pause => {
293            state.paused = !state.paused;
294        }
295
296        InputEvent::ToggleTrafficUnits => {
297            state.traffic_unit = state.traffic_unit.next();
298        }
299
300        InputEvent::ToggleDataUnits => {
301            state.data_unit = state.data_unit.next();
302        }
303
304        InputEvent::ToggleGraphs => {
305            state.show_graphs = !state.show_graphs;
306        }
307
308        InputEvent::ToggleMultiple => {
309            state.show_multiple = !state.show_multiple;
310        }
311
312        InputEvent::ZoomIn => {
313            state.zoom_level = (state.zoom_level * 1.5).min(10.0);
314        }
315
316        InputEvent::ZoomOut => {
317            state.zoom_level = (state.zoom_level / 1.5).max(0.1);
318        }
319
320        InputEvent::ShowOptions => {
321            state.show_options = !state.show_options;
322        }
323
324        InputEvent::SaveSettings => {
325            // Update config with current state values
326            config.traffic_format = state.traffic_unit.to_string().to_string();
327            config.data_format = state.data_unit.to_string().to_string();
328            config.multiple_devices = state.show_multiple;
329            config.max_incoming = state.max_incoming;
330            config.max_outgoing = state.max_outgoing;
331
332            // Save to file
333            if let Err(e) = config.save() {
334                eprintln!("Failed to save settings: {e}");
335            }
336        }
337
338        InputEvent::ReloadSettings => {
339            // Reload settings from config file
340            if let Ok(new_config) = Config::load() {
341                *config = new_config;
342                // Update state with reloaded config
343                state.traffic_unit = config.get_traffic_unit();
344                state.data_unit = config.get_data_unit();
345                state.show_multiple = config.multiple_devices;
346                state.max_incoming = config.max_incoming;
347                state.max_outgoing = config.max_outgoing;
348            }
349        }
350
351        InputEvent::IncreaseRefresh
352        | InputEvent::DecreaseRefresh
353        | InputEvent::IncreaseAverage
354        | InputEvent::DecreaseAverage => {
355            // These are only handled in options window
356        }
357
358        InputEvent::NextPanel
359        | InputEvent::PrevPanel
360        | InputEvent::NextItem
361        | InputEvent::PrevItem => {
362            // These are dashboard-specific, already handled above
363        }
364
365        InputEvent::Unknown => {
366            // Ignore unknown input
367        }
368    }
369
370    Ok(false)
371}
372
373fn draw_ui(
374    f: &mut Frame,
375    state: &DisplayState,
376    stats_calculators: &HashMap<String, StatsCalculator>,
377    config: &Config,
378) {
379    if state.show_multiple {
380        draw_multiple_devices_view(f, state, stats_calculators);
381    } else {
382        draw_single_device_view(f, state, stats_calculators, config);
383    }
384}
385
386fn draw_single_device_view(
387    f: &mut Frame,
388    state: &DisplayState,
389    stats_calculators: &HashMap<String, StatsCalculator>,
390    config: &Config,
391) {
392    let chunks = Layout::default()
393        .direction(Direction::Vertical)
394        .margin(1)
395        .constraints([
396            Constraint::Length(3), // Header with device name
397            Constraint::Min(10),   // Main graphs/stats area
398            Constraint::Length(3), // Status/help line
399        ])
400        .split(f.area());
401
402    // Get current device and its stats
403    if let Some(device) = state.devices.get(state.current_device_index) {
404        // Header
405        draw_header(f, chunks[0], &device.name, state.paused);
406
407        // Main content area
408        if state.show_graphs {
409            // TODO: Draw traffic graphs
410            draw_placeholder_graphs(f, chunks[1], device, stats_calculators, state);
411        } else {
412            // TODO: Draw statistics table
413            draw_placeholder_stats(f, chunks[1], device, stats_calculators, state);
414        }
415
416        // Status line
417        draw_status_line(f, chunks[2], state);
418
419        // Options overlay (if shown)
420        if state.show_options {
421            draw_options_overlay(f, f.area(), state, config);
422        }
423    }
424}
425
426fn draw_multiple_devices_view(
427    f: &mut Frame,
428    state: &DisplayState,
429    stats_calculators: &HashMap<String, StatsCalculator>,
430) {
431    let chunks = Layout::default()
432        .direction(Direction::Vertical)
433        .margin(1)
434        .constraints([
435            Constraint::Length(3), // Header
436            Constraint::Min(10),   // Device list
437            Constraint::Length(3), // Status/help line
438        ])
439        .split(f.area());
440
441    // Header
442    let header_text = if state.paused {
443        "netwatch - Multiple Devices View [PAUSED]"
444    } else {
445        "netwatch - Multiple Devices View"
446    };
447
448    let header = Paragraph::new(header_text)
449        .style(
450            Style::default()
451                .fg(Color::Cyan)
452                .add_modifier(Modifier::BOLD),
453        )
454        .block(Block::default().borders(Borders::ALL));
455    f.render_widget(header, chunks[0]);
456
457    // Device list area
458    if state.devices.is_empty() {
459        let no_devices = Paragraph::new("No network devices found")
460            .block(Block::default().borders(Borders::ALL).title("Devices"))
461            .style(Style::default().fg(Color::Red));
462        f.render_widget(no_devices, chunks[1]);
463    } else {
464        draw_devices_table(f, chunks[1], state, stats_calculators);
465    }
466
467    // Status line
468    draw_multiple_devices_status_line(f, chunks[2], state);
469}
470
471fn draw_devices_table(
472    f: &mut Frame,
473    area: ratatui::layout::Rect,
474    state: &DisplayState,
475    stats_calculators: &HashMap<String, StatsCalculator>,
476) {
477    // Create table header
478    let mut table_content = String::new();
479    table_content.push_str("┌─────────────────┬──────────────┬──────────────┬──────────────┬──────────────┬─────────────────┐\n");
480    table_content.push_str("│     Device      │   Current    │   Current    │   Average    │   Average    │      Total      │\n");
481    table_content.push_str("│                 │   In (↓)     │   Out (↑)    │   In (↓)     │   Out (↑)    │   In/Out        │\n");
482    table_content.push_str("├─────────────────┼──────────────┼──────────────┼──────────────┼──────────────┼─────────────────┤\n");
483
484    // Add device rows
485    for (i, device) in state.devices.iter().enumerate() {
486        let is_selected = i == state.current_device_index;
487        let prefix = if is_selected { "►" } else { " " };
488
489        if let Some(calculator) = stats_calculators.get(&device.name) {
490            let (current_in, current_out) = calculator.current_speed();
491            let (avg_in, avg_out) = calculator.average_speed();
492            let (total_in, total_out) = calculator.total_bytes();
493
494            table_content.push_str(&format!(
495                "│{} {:13} │ {:>11}/s │ {:>11}/s │ {:>11}/s │ {:>11}/s │ {:>7}/{:<7} │\n",
496                prefix,
497                truncate_device_name(&device.name, 13),
498                format_bytes_short(current_in),
499                format_bytes_short(current_out),
500                format_bytes_short(avg_in),
501                format_bytes_short(avg_out),
502                format_bytes_short(total_in),
503                format_bytes_short(total_out)
504            ));
505        } else {
506            table_content.push_str(&format!(
507                "│{} {:13} │ {:>12} │ {:>12} │ {:>12} │ {:>12} │ {:>15} │\n",
508                prefix,
509                truncate_device_name(&device.name, 13),
510                "No data",
511                "No data",
512                "No data",
513                "No data",
514                "No data"
515            ));
516        }
517    }
518
519    table_content.push_str("└─────────────────┴──────────────┴──────────────┴──────────────┴──────────────┴─────────────────┘\n");
520    table_content.push_str(
521        "\nUse arrow keys to select device, Enter to view details, 'r' to reset selected device",
522    );
523
524    let devices_table = Paragraph::new(table_content)
525        .block(
526            Block::default()
527                .borders(Borders::ALL)
528                .title("Network Devices"),
529        )
530        .style(Style::default().fg(Color::White));
531
532    f.render_widget(devices_table, area);
533}
534
535fn draw_multiple_devices_status_line(
536    f: &mut Frame,
537    area: ratatui::layout::Rect,
538    _state: &DisplayState,
539) {
540    let help_text = vec![Line::from(vec![
541        Span::styled("Press ", Style::default().fg(Color::Gray)),
542        Span::styled(
543            "'q'",
544            Style::default()
545                .fg(Color::Yellow)
546                .add_modifier(Modifier::BOLD),
547        ),
548        Span::styled(" to quit, ", Style::default().fg(Color::Gray)),
549        Span::styled(
550            "arrows",
551            Style::default()
552                .fg(Color::Yellow)
553                .add_modifier(Modifier::BOLD),
554        ),
555        Span::styled(" to select device, ", Style::default().fg(Color::Gray)),
556        Span::styled(
557            "Enter",
558            Style::default()
559                .fg(Color::Yellow)
560                .add_modifier(Modifier::BOLD),
561        ),
562        Span::styled(" for details, ", Style::default().fg(Color::Gray)),
563        Span::styled(
564            "'r'",
565            Style::default()
566                .fg(Color::Yellow)
567                .add_modifier(Modifier::BOLD),
568        ),
569        Span::styled(" to reset", Style::default().fg(Color::Gray)),
570    ])];
571
572    let help = Paragraph::new(help_text)
573        .block(Block::default().borders(Borders::ALL))
574        .style(Style::default().fg(Color::Gray));
575
576    f.render_widget(help, area);
577}
578
579fn draw_header(f: &mut Frame, area: ratatui::layout::Rect, device_name: &str, paused: bool) {
580    let status = if paused { " [PAUSED]" } else { "" };
581    let title = format!("netwatch - Network Traffic Monitor [{device_name}]{status}");
582
583    let header = Paragraph::new(title)
584        .style(
585            Style::default()
586                .fg(Color::Cyan)
587                .add_modifier(Modifier::BOLD),
588        )
589        .block(Block::default().borders(Borders::ALL));
590
591    f.render_widget(header, area);
592}
593
594fn draw_placeholder_graphs(
595    f: &mut Frame,
596    area: ratatui::layout::Rect,
597    device: &Device,
598    stats_calculators: &HashMap<String, StatsCalculator>,
599    state: &DisplayState,
600) {
601    if let Some(calculator) = stats_calculators.get(&device.name) {
602        // Split area into stats section and graph section
603        let chunks = Layout::default()
604            .direction(Direction::Vertical)
605            .constraints([
606                Constraint::Length(6), // Stats display area
607                Constraint::Min(10),   // Graph area
608            ])
609            .split(area);
610
611        // Draw statistics summary
612        draw_stats_summary(f, chunks[0], device, calculator);
613
614        // Draw the actual graphs
615        draw_traffic_graphs_internal(f, chunks[1], calculator, state);
616    } else {
617        let no_data = Paragraph::new("No statistics available for this device")
618            .block(
619                Block::default()
620                    .borders(Borders::ALL)
621                    .title("Traffic Monitor"),
622            )
623            .style(Style::default().fg(Color::Red));
624        f.render_widget(no_data, area);
625    }
626}
627
628fn draw_stats_summary(
629    f: &mut Frame,
630    area: ratatui::layout::Rect,
631    device: &Device,
632    calculator: &StatsCalculator,
633) {
634    let (current_in, current_out) = calculator.current_speed();
635    let (avg_in, avg_out) = calculator.average_speed();
636    let (_min_in, _min_out) = calculator.min_speed();
637    let (max_in, max_out) = calculator.max_speed();
638
639    let stats_text = format!(
640        "📶 Device: {}     Current Traffic: 📥 {}/s down  📤 {}/s up\nAverages: 📊 {}/s down  📊 {}/s up     Peak: 📈 {}/s down  📈 {}/s up",
641        device.name,
642        format_bytes(current_in),
643        format_bytes(current_out),
644        format_bytes(avg_in),
645        format_bytes(avg_out),
646        format_bytes(max_in),
647        format_bytes(max_out)
648    );
649
650    let stats_widget = Paragraph::new(stats_text)
651        .block(Block::default().borders(Borders::ALL).title("Statistics"))
652        .style(Style::default().fg(Color::Cyan));
653
654    f.render_widget(stats_widget, area);
655}
656
657pub fn draw_traffic_graphs(
658    f: &mut Frame,
659    area: ratatui::layout::Rect,
660    device_name: &str,
661    calculator: &StatsCalculator,
662    dashboard_state: &crate::dashboard::DashboardState,
663) {
664    // Create a compatibility DisplayState for the existing function
665    let state = DisplayState {
666        current_device_index: dashboard_state.current_device_index,
667        devices: dashboard_state.devices.clone(),
668        show_multiple: false,
669        show_graphs: true,
670        paused: dashboard_state.paused,
671        traffic_unit: dashboard_state.traffic_unit.clone(),
672        data_unit: dashboard_state.data_unit.clone(),
673        max_incoming: dashboard_state.max_incoming,
674        max_outgoing: dashboard_state.max_outgoing,
675        zoom_level: dashboard_state.zoom_level,
676        show_options: false,
677        settings_message: None,
678    };
679
680    draw_traffic_graphs_with_device_name(f, area, device_name, calculator, &state);
681}
682
683fn draw_traffic_graphs_with_device_name(
684    f: &mut Frame,
685    area: ratatui::layout::Rect,
686    device_name: &str,
687    calculator: &StatsCalculator,
688    state: &DisplayState,
689) {
690    // Split into incoming and outgoing graph areas
691    let chunks = Layout::default()
692        .direction(Direction::Horizontal)
693        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
694        .split(area);
695
696    // Get graph data
697    let graph_data_in = calculator.graph_data_in();
698    let graph_data_out = calculator.graph_data_out();
699
700    // Draw incoming traffic graph with device name
701    draw_single_graph_with_device(
702        f,
703        chunks[0],
704        &format!("{device_name} - Incoming"),
705        graph_data_in,
706        Color::Green,
707        calculator.max_speed().0, // max incoming
708        state,
709    );
710
711    // Draw outgoing traffic graph with device name
712    draw_single_graph_with_device(
713        f,
714        chunks[1],
715        &format!("{device_name} - Outgoing"),
716        graph_data_out,
717        Color::Red,
718        calculator.max_speed().1, // max outgoing
719        state,
720    );
721}
722
723fn draw_traffic_graphs_internal(
724    f: &mut Frame,
725    area: ratatui::layout::Rect,
726    calculator: &StatsCalculator,
727    state: &DisplayState,
728) {
729    // Split into incoming and outgoing graph areas
730    let chunks = Layout::default()
731        .direction(Direction::Horizontal)
732        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
733        .split(area);
734
735    // Get graph data
736    let graph_data_in = calculator.graph_data_in();
737    let graph_data_out = calculator.graph_data_out();
738
739    // Draw incoming traffic graph
740    draw_single_graph(
741        f,
742        chunks[0],
743        "Incoming Traffic",
744        graph_data_in,
745        Color::Green,
746        calculator.max_speed().0, // max incoming
747        state,
748    );
749
750    // Draw outgoing traffic graph
751    draw_single_graph(
752        f,
753        chunks[1],
754        "Outgoing Traffic",
755        graph_data_out,
756        Color::Red,
757        calculator.max_speed().1, // max outgoing
758        state,
759    );
760}
761
762fn draw_single_graph_with_device(
763    f: &mut Frame,
764    area: ratatui::layout::Rect,
765    title: &str,
766    data: &std::collections::VecDeque<(f64, f64)>,
767    color: Color,
768    max_value: u64,
769    state: &DisplayState,
770) {
771    if data.is_empty() {
772        let no_data = Paragraph::new("Collecting data...")
773            .block(Block::default().borders(Borders::ALL).title(title))
774            .style(Style::default().fg(Color::Yellow));
775        f.render_widget(no_data, area);
776        return;
777    }
778
779    if data.len() < 2 {
780        let waiting = Paragraph::new(format!("Waiting for more data... ({})", data.len()))
781            .block(Block::default().borders(Borders::ALL).title(title))
782            .style(Style::default().fg(Color::Yellow));
783        f.render_widget(waiting, area);
784        return;
785    }
786
787    // Calculate bounds with smart scaling first
788    let min_x = 0.0; // Left side starts at "now" (time 0)
789    let max_x = 60.0; // Right side goes to "60 seconds ago"
790
791    // Calculate Y-axis bounds based on network capacity tiers
792    let data_max = data
793        .iter()
794        .map(|(_, y)| *y)
795        .filter(|y| y.is_finite() && *y >= 0.0)
796        .fold(0.0, f64::max);
797    let actual_max = if data_max > 0.0 {
798        data_max as u64
799    } else if max_value > 0 {
800        max_value
801    } else {
802        1024 // 1KB minimum
803    };
804
805    // Use network capacity scale for graph bounds, adjusted by zoom level
806    let base_max_y = get_network_capacity_scale(actual_max) as f64;
807    let max_y = if state.zoom_level > 0.0 && state.zoom_level.is_finite() {
808        base_max_y / state.zoom_level // Higher zoom = smaller Y range = "zoomed in"
809    } else {
810        base_max_y // Fallback if zoom_level is invalid
811    };
812
813    // Convert data to chart format and sort by time (newest to oldest for proper line drawing)
814    let chart_data: Vec<(f64, f64)> = data
815        .iter()
816        .cloned()
817        .filter(|(x, y)| x.is_finite() && y.is_finite() && *x >= 0.0 && *y >= 0.0)
818        .collect();
819    let mut chart_data = chart_data;
820
821    // If no valid data after filtering, show waiting message
822    if chart_data.is_empty() {
823        let waiting = Paragraph::new("Waiting for valid data...")
824            .block(Block::default().borders(Borders::ALL).title(title))
825            .style(Style::default().fg(Color::Yellow));
826        f.render_widget(waiting, area);
827        return;
828    }
829
830    chart_data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); // Sort by time, safe fallback
831
832    // Create dataset
833    let dataset = Dataset::default()
834        .name(title)
835        .marker(ratatui::symbols::Marker::Braille)
836        .graph_type(GraphType::Line)
837        .style(Style::default().fg(color))
838        .data(&chart_data);
839
840    // Try to create chart, fallback to ASCII if it fails
841    let chart = Chart::new(vec![dataset])
842        .block(Block::default().borders(Borders::ALL).title(format!(
843            "{} (Max: {}) - Use ↑/↓ to switch devices",
844            title,
845            format_bytes(max_value)
846        )))
847        .x_axis(
848            Axis::default()
849                .title("Time")
850                .style(Style::default().fg(Color::Gray))
851                .bounds([min_x, max_x])
852                .labels(vec!["Now", "30s ago", "1 min ago"]),
853        )
854        .y_axis(
855            Axis::default()
856                .title("Speed")
857                .style(Style::default().fg(Color::Gray))
858                .bounds([0.0, max_y])
859                .labels(create_smart_y_labels(max_y)),
860        );
861
862    // If chart rendering fails, use ASCII fallback
863    if area.width < 20 || area.height < 8 {
864        draw_ascii_graph_with_device(f, area, title, data, color, max_value);
865    } else {
866        f.render_widget(chart, area);
867    }
868}
869
870fn draw_single_graph(
871    f: &mut Frame,
872    area: ratatui::layout::Rect,
873    title: &str,
874    data: &std::collections::VecDeque<(f64, f64)>,
875    color: Color,
876    max_value: u64,
877    state: &DisplayState,
878) {
879    if data.is_empty() {
880        let no_data = Paragraph::new("Collecting data...")
881            .block(Block::default().borders(Borders::ALL).title(title))
882            .style(Style::default().fg(Color::Yellow));
883        f.render_widget(no_data, area);
884        return;
885    }
886
887    if data.len() < 2 {
888        let waiting = Paragraph::new(format!("Waiting for more data... ({})", data.len()))
889            .block(Block::default().borders(Borders::ALL).title(title))
890            .style(Style::default().fg(Color::Yellow));
891        f.render_widget(waiting, area);
892        return;
893    }
894
895    // Calculate bounds with smart scaling first
896    let min_x = 0.0; // Left side starts at "now" (time 0)
897    let max_x = 60.0; // Right side goes to "60 seconds ago"
898
899    // Calculate Y-axis bounds based on network capacity tiers
900    let data_max = data
901        .iter()
902        .map(|(_, y)| *y)
903        .filter(|y| y.is_finite() && *y >= 0.0)
904        .fold(0.0, f64::max);
905    let actual_max = if data_max > 0.0 {
906        data_max as u64
907    } else if max_value > 0 {
908        max_value
909    } else {
910        1024 // 1KB minimum
911    };
912
913    // Use network capacity scale for graph bounds, adjusted by zoom level
914    let base_max_y = get_network_capacity_scale(actual_max) as f64;
915    let max_y = if state.zoom_level > 0.0 && state.zoom_level.is_finite() {
916        base_max_y / state.zoom_level // Higher zoom = smaller Y range = "zoomed in"
917    } else {
918        base_max_y // Fallback if zoom_level is invalid
919    };
920
921    // Convert data to chart format and sort by time (newest to oldest for proper line drawing)
922    let chart_data: Vec<(f64, f64)> = data
923        .iter()
924        .cloned()
925        .filter(|(x, y)| x.is_finite() && y.is_finite() && *x >= 0.0 && *y >= 0.0)
926        .collect();
927    let mut chart_data = chart_data;
928
929    // If no valid data after filtering, show waiting message
930    if chart_data.is_empty() {
931        let waiting = Paragraph::new("Waiting for valid data...")
932            .block(Block::default().borders(Borders::ALL).title(title))
933            .style(Style::default().fg(Color::Yellow));
934        f.render_widget(waiting, area);
935        return;
936    }
937
938    chart_data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); // Sort by time, safe fallback
939
940    // Create dataset
941    let dataset = Dataset::default()
942        .name(title)
943        .marker(ratatui::symbols::Marker::Braille)
944        .graph_type(GraphType::Line)
945        .style(Style::default().fg(color))
946        .data(&chart_data);
947
948    // Try to create chart, fallback to ASCII if it fails
949    let chart = Chart::new(vec![dataset])
950        .block(Block::default().borders(Borders::ALL).title(format!(
951            "{} (Max: {}) - Use ↑/↓ to switch devices",
952            title,
953            format_bytes(max_value)
954        )))
955        .x_axis(
956            Axis::default()
957                .title("Time")
958                .style(Style::default().fg(Color::Gray))
959                .bounds([min_x, max_x])
960                .labels(vec!["Now", "30s ago", "1 min ago"]),
961        )
962        .y_axis(
963            Axis::default()
964                .title("Speed")
965                .style(Style::default().fg(Color::Gray))
966                .bounds([0.0, max_y])
967                .labels(create_smart_y_labels(max_y)),
968        );
969
970    // If chart rendering fails, use ASCII fallback
971    if area.width < 20 || area.height < 8 {
972        draw_ascii_graph(f, area, title, data, color, max_value);
973    } else {
974        f.render_widget(chart, area);
975    }
976}
977
978fn draw_ascii_graph_with_device(
979    f: &mut Frame,
980    area: ratatui::layout::Rect,
981    title: &str,
982    data: &std::collections::VecDeque<(f64, f64)>,
983    color: Color,
984    max_value: u64,
985) {
986    if data.is_empty() {
987        let no_data = Paragraph::new("No data available")
988            .block(Block::default().borders(Borders::ALL).title(title))
989            .style(Style::default().fg(Color::Yellow));
990        f.render_widget(no_data, area);
991        return;
992    }
993
994    // Create ASCII bar graph
995    let graph_height = (area.height.saturating_sub(3)) as usize; // Account for borders and title
996    let graph_width = (area.width.saturating_sub(2)) as usize; // Account for borders
997
998    if graph_height == 0 || graph_width == 0 {
999        let too_small = Paragraph::new("Area too small for graph")
1000            .block(Block::default().borders(Borders::ALL).title(title))
1001            .style(Style::default().fg(Color::Yellow));
1002        f.render_widget(too_small, area);
1003        return;
1004    }
1005
1006    // Get max value for scaling
1007    let data_max = data
1008        .iter()
1009        .map(|(_, y)| *y)
1010        .filter(|y| y.is_finite() && *y >= 0.0)
1011        .fold(0.0, f64::max);
1012    let scale_max = if data_max > 0.0 {
1013        data_max
1014    } else {
1015        max_value as f64
1016    };
1017
1018    // Sample data points across the width
1019    let mut graph_lines = vec![String::new(); graph_height];
1020    let step = if data.len() > graph_width {
1021        data.len() / graph_width
1022    } else {
1023        1
1024    };
1025
1026    for (i, chunk) in data.iter().step_by(step).take(graph_width).enumerate() {
1027        let (_, value) = chunk;
1028        let normalized = if scale_max > 0.0 {
1029            (value / scale_max * graph_height as f64) as usize
1030        } else {
1031            0
1032        };
1033        let bar_height = normalized.min(graph_height);
1034
1035        // Draw vertical bar using Unicode block characters
1036        for (row, line) in graph_lines.iter_mut().enumerate().take(graph_height) {
1037            let char_to_use = if (graph_height - row - 1) < bar_height {
1038                match ((graph_height - row - 1) * 8) % 8 {
1039                    0..=1 => "█", // Full block
1040                    2..=3 => "▇", // 7/8 block
1041                    4..=5 => "▆", // 3/4 block
1042                    6..=7 => "▅", // 5/8 block
1043                    _ => "█",
1044                }
1045            } else {
1046                " "
1047            };
1048
1049            if i < line.len() {
1050                line.replace_range(i..=i, char_to_use);
1051            } else {
1052                while line.len() < i {
1053                    line.push(' ');
1054                }
1055                line.push_str(char_to_use);
1056            }
1057        }
1058    }
1059
1060    // Add current value and max info
1061    let current_val = data.back().map(|(_, v)| *v).unwrap_or(0.0);
1062    let info_line = format!(
1063        "Current: {}/s | Max: {}/s",
1064        format_bytes(current_val as u64),
1065        format_bytes(scale_max as u64)
1066    );
1067
1068    // Combine all lines
1069    let mut all_lines = graph_lines;
1070    all_lines.push(String::new()); // Empty line
1071    all_lines.push(info_line);
1072
1073    let graph_text: Vec<ratatui::text::Line> = all_lines
1074        .into_iter()
1075        .map(|line| {
1076            ratatui::text::Line::from(ratatui::text::Span::styled(
1077                line,
1078                Style::default().fg(color),
1079            ))
1080        })
1081        .collect();
1082
1083    let ascii_graph = Paragraph::new(graph_text)
1084        .block(
1085            Block::default()
1086                .borders(Borders::ALL)
1087                .title(format!("📊 {title} (ASCII) - Use ↑/↓ to switch devices")),
1088        )
1089        .style(Style::default().fg(color));
1090
1091    f.render_widget(ascii_graph, area);
1092}
1093
1094fn draw_ascii_graph(
1095    f: &mut Frame,
1096    area: ratatui::layout::Rect,
1097    title: &str,
1098    data: &std::collections::VecDeque<(f64, f64)>,
1099    color: Color,
1100    max_value: u64,
1101) {
1102    if data.is_empty() {
1103        let no_data = Paragraph::new("No data available")
1104            .block(Block::default().borders(Borders::ALL).title(title))
1105            .style(Style::default().fg(Color::Yellow));
1106        f.render_widget(no_data, area);
1107        return;
1108    }
1109
1110    // Create ASCII bar graph
1111    let graph_height = (area.height.saturating_sub(3)) as usize; // Account for borders and title
1112    let graph_width = (area.width.saturating_sub(2)) as usize; // Account for borders
1113
1114    if graph_height == 0 || graph_width == 0 {
1115        let too_small = Paragraph::new("Area too small for graph")
1116            .block(Block::default().borders(Borders::ALL).title(title))
1117            .style(Style::default().fg(Color::Yellow));
1118        f.render_widget(too_small, area);
1119        return;
1120    }
1121
1122    // Get max value for scaling
1123    let data_max = data
1124        .iter()
1125        .map(|(_, y)| *y)
1126        .filter(|y| y.is_finite() && *y >= 0.0)
1127        .fold(0.0, f64::max);
1128    let scale_max = if data_max > 0.0 {
1129        data_max
1130    } else {
1131        max_value as f64
1132    };
1133
1134    // Sample data points across the width
1135    let mut graph_lines = vec![String::new(); graph_height];
1136    let step = if data.len() > graph_width {
1137        data.len() / graph_width
1138    } else {
1139        1
1140    };
1141
1142    for (i, chunk) in data.iter().step_by(step).take(graph_width).enumerate() {
1143        let (_, value) = chunk;
1144        let normalized = if scale_max > 0.0 {
1145            (value / scale_max * graph_height as f64) as usize
1146        } else {
1147            0
1148        };
1149        let bar_height = normalized.min(graph_height);
1150
1151        // Draw vertical bar using Unicode block characters
1152        for (row, line) in graph_lines.iter_mut().enumerate().take(graph_height) {
1153            let char_to_use = if (graph_height - row - 1) < bar_height {
1154                match ((graph_height - row - 1) * 8) % 8 {
1155                    0..=1 => "█", // Full block
1156                    2..=3 => "▇", // 7/8 block
1157                    4..=5 => "▆", // 3/4 block
1158                    6..=7 => "▅", // 5/8 block
1159                    _ => "█",
1160                }
1161            } else {
1162                " "
1163            };
1164
1165            if i < line.len() {
1166                line.replace_range(i..=i, char_to_use);
1167            } else {
1168                while line.len() < i {
1169                    line.push(' ');
1170                }
1171                line.push_str(char_to_use);
1172            }
1173        }
1174    }
1175
1176    // Add current value and max info
1177    let current_val = data.back().map(|(_, v)| *v).unwrap_or(0.0);
1178    let info_line = format!(
1179        "Current: {}/s | Max: {}/s",
1180        format_bytes(current_val as u64),
1181        format_bytes(scale_max as u64)
1182    );
1183
1184    // Combine all lines
1185    let mut all_lines = graph_lines;
1186    all_lines.push(String::new()); // Empty line
1187    all_lines.push(info_line);
1188
1189    let graph_text: Vec<ratatui::text::Line> = all_lines
1190        .into_iter()
1191        .map(|line| {
1192            ratatui::text::Line::from(ratatui::text::Span::styled(
1193                line,
1194                Style::default().fg(color),
1195            ))
1196        })
1197        .collect();
1198
1199    let ascii_graph = Paragraph::new(graph_text)
1200        .block(
1201            Block::default()
1202                .borders(Borders::ALL)
1203                .title(format!("📊 {title} (ASCII) - Use ↑/↓ to switch devices")),
1204        )
1205        .style(Style::default().fg(color));
1206
1207    f.render_widget(ascii_graph, area);
1208}
1209
1210fn draw_placeholder_stats(
1211    f: &mut Frame,
1212    area: ratatui::layout::Rect,
1213    device: &Device,
1214    stats_calculators: &HashMap<String, StatsCalculator>,
1215    state: &DisplayState,
1216) {
1217    if let Some(calculator) = stats_calculators.get(&device.name) {
1218        draw_detailed_stats_table(
1219            f,
1220            area,
1221            device,
1222            calculator,
1223            &state.traffic_unit,
1224            &state.data_unit,
1225        );
1226    } else {
1227        let no_data = Paragraph::new("No statistics available for this device")
1228            .block(
1229                Block::default()
1230                    .borders(Borders::ALL)
1231                    .title("Statistics Table"),
1232            )
1233            .style(Style::default().fg(Color::Red));
1234        f.render_widget(no_data, area);
1235    }
1236}
1237
1238fn draw_detailed_stats_table(
1239    f: &mut Frame,
1240    area: ratatui::layout::Rect,
1241    device: &Device,
1242    calculator: &StatsCalculator,
1243    traffic_unit: &TrafficUnit,
1244    data_unit: &DataUnit,
1245) {
1246    // Get statistics
1247    let (current_in, current_out) = calculator.current_speed();
1248    let (avg_in, avg_out) = calculator.average_speed();
1249    let (min_in, min_out) = calculator.min_speed();
1250    let (max_in, max_out) = calculator.max_speed();
1251    let (total_bytes_in, total_bytes_out) = calculator.total_bytes();
1252    let (total_packets_in, total_packets_out) = calculator.total_packets();
1253
1254    // Create table content
1255    let table_content = format!(
1256        "Device: {}\n\
1257        \n\
1258        ┌─────────────────────────────┬──────────────────┬──────────────────┐\n\
1259        │         Statistic           │    Incoming      │    Outgoing      │\n\
1260        ├─────────────────────────────┼──────────────────┼──────────────────┤\n\
1261        │ Current Speed               │ {:>15}/s │ {:>15}/s │\n\
1262        │ Average Speed               │ {:>15}/s │ {:>15}/s │\n\
1263        │ Minimum Speed               │ {:>15}/s │ {:>15}/s │\n\
1264        │ Maximum Speed               │ {:>15}/s │ {:>15}/s │\n\
1265        ├─────────────────────────────┼──────────────────┼──────────────────┤\n\
1266        │ Total Bytes                 │ {:>16} │ {:>16} │\n\
1267        │ Total Packets               │ {:>16} │ {:>16} │\n\
1268        └─────────────────────────────┴──────────────────┴──────────────────┘\n\
1269        \n\
1270        Network Interface Statistics - Press 'g' to toggle back to graphs",
1271        device.name,
1272        format_bytes_with_unit(current_in, traffic_unit),
1273        format_bytes_with_unit(current_out, traffic_unit),
1274        format_bytes_with_unit(avg_in, traffic_unit),
1275        format_bytes_with_unit(avg_out, traffic_unit),
1276        format_bytes_with_unit(min_in, traffic_unit),
1277        format_bytes_with_unit(min_out, traffic_unit),
1278        format_bytes_with_unit(max_in, traffic_unit),
1279        format_bytes_with_unit(max_out, traffic_unit),
1280        format_bytes_with_unit(total_bytes_in, data_unit),
1281        format_bytes_with_unit(total_bytes_out, data_unit),
1282        format_number(total_packets_in),
1283        format_number(total_packets_out),
1284    );
1285
1286    let stats_table = Paragraph::new(table_content)
1287        .block(
1288            Block::default()
1289                .borders(Borders::ALL)
1290                .title("Detailed Network Statistics"),
1291        )
1292        .style(Style::default().fg(Color::Cyan));
1293
1294    f.render_widget(stats_table, area);
1295}
1296
1297fn draw_status_line(f: &mut Frame, area: ratatui::layout::Rect, _state: &DisplayState) {
1298    let help_text = vec![Line::from(vec![
1299        Span::styled("Press ", Style::default().fg(Color::Gray)),
1300        Span::styled(
1301            "'q'",
1302            Style::default()
1303                .fg(Color::Yellow)
1304                .add_modifier(Modifier::BOLD),
1305        ),
1306        Span::styled(" to quit, ", Style::default().fg(Color::Gray)),
1307        Span::styled(
1308            "arrows",
1309            Style::default()
1310                .fg(Color::Yellow)
1311                .add_modifier(Modifier::BOLD),
1312        ),
1313        Span::styled(" to switch devices, ", Style::default().fg(Color::Gray)),
1314        Span::styled(
1315            "'r'",
1316            Style::default()
1317                .fg(Color::Yellow)
1318                .add_modifier(Modifier::BOLD),
1319        ),
1320        Span::styled(" to reset, ", Style::default().fg(Color::Gray)),
1321        Span::styled(
1322            "space",
1323            Style::default()
1324                .fg(Color::Yellow)
1325                .add_modifier(Modifier::BOLD),
1326        ),
1327        Span::styled(" to pause", Style::default().fg(Color::Gray)),
1328    ])];
1329
1330    let help = Paragraph::new(help_text)
1331        .block(Block::default().borders(Borders::ALL))
1332        .style(Style::default().fg(Color::Gray));
1333
1334    f.render_widget(help, area);
1335}
1336
1337// Helper function for formatting bytes
1338fn format_bytes(bytes: u64) -> String {
1339    format_bytes_with_unit(bytes, &TrafficUnit::HumanByte)
1340}
1341
1342// Helper function for formatting bytes with specific unit
1343fn format_bytes_with_unit(bytes: u64, unit: &TrafficUnit) -> String {
1344    match unit {
1345        TrafficUnit::HumanBit => {
1346            let bits = bytes * 8;
1347            format_human_readable(bits, &["bit", "Kbit", "Mbit", "Gbit", "Tbit"], 1000.0)
1348        }
1349        TrafficUnit::HumanByte => {
1350            format_human_readable(bytes, &["B", "KB", "MB", "GB", "TB"], 1024.0)
1351        }
1352        TrafficUnit::Bit => format!("{} bit", bytes * 8),
1353        TrafficUnit::Byte => format!("{bytes} B"),
1354        TrafficUnit::KiloBit => format!("{:.2} kbit", (bytes * 8) as f64 / 1000.0),
1355        TrafficUnit::KiloByte => format!("{:.2} KB", bytes as f64 / 1024.0),
1356        TrafficUnit::MegaBit => format!("{:.2} Mbit", (bytes * 8) as f64 / 1_000_000.0),
1357        TrafficUnit::MegaByte => format!("{:.2} MB", bytes as f64 / 1_048_576.0),
1358        TrafficUnit::GigaBit => format!("{:.2} Gbit", (bytes * 8) as f64 / 1_000_000_000.0),
1359        TrafficUnit::GigaByte => format!("{:.2} GB", bytes as f64 / 1_073_741_824.0),
1360    }
1361}
1362
1363fn format_human_readable(value: u64, units: &[&str], divisor: f64) -> String {
1364    let mut size = value as f64;
1365    let mut unit_index = 0;
1366
1367    while size >= divisor && unit_index < units.len() - 1 {
1368        size /= divisor;
1369        unit_index += 1;
1370    }
1371
1372    if size >= 100.0 {
1373        format!("{:.0} {}", size, units[unit_index])
1374    } else if size >= 10.0 {
1375        format!("{:.1} {}", size, units[unit_index])
1376    } else {
1377        format!("{:.2} {}", size, units[unit_index])
1378    }
1379}
1380
1381// Helper function for formatting large numbers with commas
1382fn format_number(num: u64) -> String {
1383    let num_str = num.to_string();
1384    let mut result = String::new();
1385    let chars: Vec<char> = num_str.chars().collect();
1386
1387    for (i, &ch) in chars.iter().enumerate() {
1388        if i > 0 && (chars.len() - i) % 3 == 0 {
1389            result.push(',');
1390        }
1391        result.push(ch);
1392    }
1393
1394    result
1395}
1396
1397// Helper function for formatting bytes in a shorter format (for tables)
1398fn format_bytes_short(bytes: u64) -> String {
1399    const UNITS: &[&str] = &["B", "K", "M", "G", "T"];
1400    let mut size = bytes as f64;
1401    let mut unit_index = 0;
1402
1403    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
1404        size /= 1024.0;
1405        unit_index += 1;
1406    }
1407
1408    if size >= 100.0 {
1409        format!("{:.0}{}", size, UNITS[unit_index])
1410    } else if size >= 10.0 {
1411        format!("{:.1}{}", size, UNITS[unit_index])
1412    } else {
1413        format!("{:.2}{}", size, UNITS[unit_index])
1414    }
1415}
1416
1417// Helper function to truncate device names for table display
1418fn truncate_device_name(name: &str, max_len: usize) -> String {
1419    if name.len() <= max_len {
1420        name.to_string()
1421    } else {
1422        format!("{}...", &name[..max_len.saturating_sub(3)])
1423    }
1424}
1425
1426// Determine appropriate network capacity scale based on actual traffic
1427fn get_network_capacity_scale(actual_max: u64) -> u64 {
1428    // Convert to bits per second for network capacity comparison
1429    let actual_bits = actual_max * 8;
1430
1431    // Network capacity tiers (in bits per second)
1432    let tiers = vec![
1433        1_000_000,       // 1 Mbps
1434        10_000_000,      // 10 Mbps
1435        100_000_000,     // 100 Mbps
1436        1_000_000_000,   // 1 Gbps
1437        10_000_000_000,  // 10 Gbps
1438        40_000_000_000,  // 40 Gbps
1439        100_000_000_000, // 100 Gbps
1440    ];
1441
1442    // Find the next tier above actual usage
1443    for &tier in &tiers {
1444        if actual_bits <= tier {
1445            return tier / 8; // Convert back to bytes per second
1446        }
1447    }
1448
1449    // If higher than all tiers, use 100 Gbps
1450    100_000_000_000 / 8
1451}
1452
1453// Create network-capacity-aware Y-axis labels for bounds [0.0, max_y]
1454fn create_smart_y_labels(max_y: f64) -> Vec<ratatui::text::Span<'static>> {
1455    let capacity_scale = max_y as u64; // max_y is already the capacity scale
1456
1457    // Labels for Y-axis bounds [0.0, max_y]
1458    // First label = 0.0 (bottom), Last label = max_y (top)
1459    let labels = vec![
1460        "0 B/s".into(),                                               // 0.0 (bottom)
1461        format!("{}/s", format_bytes(capacity_scale / 4)).into(),     // 25% (lower)
1462        format!("{}/s", format_bytes(capacity_scale / 2)).into(),     // 50% (middle)
1463        format!("{}/s", format_bytes(capacity_scale * 3 / 4)).into(), // 75% (upper)
1464        format!("{}/s", format_bytes(capacity_scale)).into(),         // max_y (top)
1465    ];
1466
1467    labels
1468}
1469
1470fn draw_options_overlay(
1471    f: &mut Frame,
1472    area: ratatui::layout::Rect,
1473    state: &DisplayState,
1474    config: &Config,
1475) {
1476    // Create a centered popup area
1477    let popup_area = centered_rect(60, 70, area);
1478
1479    // Clear the area
1480    let clear = Block::default().style(Style::default().bg(Color::Black));
1481    f.render_widget(clear, popup_area);
1482
1483    // Create options content
1484    let options_text = format!(
1485        "═══════════════════ OPTIONS ═══════════════════\n\
1486        \n\
1487        Current Settings:\n\
1488        \n\
1489        • Traffic Unit:     {:?}\n\
1490        • Data Unit:        {:?}\n\
1491        • Show Graphs:      {}\n\
1492        • Multiple View:    {}\n\
1493        • Paused:           {}\n\
1494        • Zoom Level:       {:.1}x\n\
1495        • Max Incoming:     {} (0 = auto)\n\
1496        • Max Outgoing:     {} (0 = auto)\n\
1497        • Average Window:   {}s\n\
1498        • Refresh Rate:     {}ms\n\
1499        • Devices:          {}\n\
1500        \n\
1501        ──────────── Interactive Controls ───────────────\n\
1502        \n\
1503        You can change settings while this window is open:\n\
1504        \n\
1505        • 'u' - Cycle traffic units (speeds - changes above ↑)\n\
1506        • 'U' - Cycle data units (totals - changes above ↑)\n\
1507        • Space - Pause/resume monitoring\n\
1508        • '+/-' - Zoom graph scale\n\
1509        • '</>' - Slower/Faster refresh rate\n\
1510        • '[/]' - Shorter/Longer average window\n\
1511        • F5 - Save current settings to file\n\
1512        • F6 - Reload settings from file\n\
1513        \n\
1514        ────────── View Controls (when closed) ──────────\n\
1515        \n\
1516        • 'g' - Toggle graphs/stats view\n\
1517        • Enter - Toggle single/multiple view\n\
1518        • Arrow keys - Navigate devices\n\
1519        • 'r' - Reset statistics\n\
1520        \n\
1521        Press F2 or ESC to close this options window\n\
1522        \n\
1523        {}",
1524        state.traffic_unit,
1525        state.data_unit,
1526        if state.show_graphs { "Yes" } else { "No" },
1527        if state.show_multiple { "Yes" } else { "No" },
1528        if state.paused { "Yes" } else { "No" },
1529        state.zoom_level,
1530        state.max_incoming,
1531        state.max_outgoing,
1532        config.average_window,
1533        config.refresh_interval,
1534        config.devices,
1535        state.settings_message.as_deref().unwrap_or("")
1536    );
1537
1538    let options_popup = Paragraph::new(options_text)
1539        .block(
1540            Block::default()
1541                .borders(Borders::ALL)
1542                .title("Options & Settings")
1543                .style(Style::default().fg(Color::Yellow)),
1544        )
1545        .style(Style::default().fg(Color::White).bg(Color::Black));
1546
1547    f.render_widget(options_popup, popup_area);
1548}
1549
1550// Helper function to create a centered rectangle
1551fn centered_rect(
1552    percent_x: u16,
1553    percent_y: u16,
1554    r: ratatui::layout::Rect,
1555) -> ratatui::layout::Rect {
1556    let popup_layout = Layout::default()
1557        .direction(Direction::Vertical)
1558        .constraints([
1559            Constraint::Percentage((100 - percent_y) / 2),
1560            Constraint::Percentage(percent_y),
1561            Constraint::Percentage((100 - percent_y) / 2),
1562        ])
1563        .split(r);
1564
1565    Layout::default()
1566        .direction(Direction::Horizontal)
1567        .constraints([
1568            Constraint::Percentage((100 - percent_x) / 2),
1569            Constraint::Percentage(percent_x),
1570            Constraint::Percentage((100 - percent_x) / 2),
1571        ])
1572        .split(popup_layout[1])[1]
1573}