Skip to main content

nuviz_cli/tui/
widgets.rs

1use ratatui::{
2    layout::Rect,
3    style::{Color, Modifier, Style},
4    text::{Line, Span},
5    widgets::{Block, Borders, Paragraph},
6};
7
8use crate::tui::app::App;
9use crate::tui::chart;
10
11/// Render a braille chart as a ratatui widget.
12pub struct BrailleChart<'a> {
13    pub app: &'a App,
14    pub metric: &'a str,
15    pub focused: bool,
16}
17
18impl<'a> BrailleChart<'a> {
19    pub fn render_to_paragraph(&self, area: Rect) -> Paragraph<'a> {
20        let border_style = if self.focused {
21            Style::default()
22                .fg(Color::Cyan)
23                .add_modifier(Modifier::BOLD)
24        } else {
25            Style::default().fg(Color::DarkGray)
26        };
27
28        let block = Block::default()
29            .title(format!(" {} ", self.metric))
30            .borders(Borders::ALL)
31            .border_style(border_style);
32
33        // Collect series for all experiments
34        let inner_width = area.width.saturating_sub(2) as usize;
35        let inner_height = area.height.saturating_sub(2) as usize;
36
37        if inner_width < 10 || inner_height < 2 {
38            return Paragraph::new("(too small)").block(block);
39        }
40
41        // Use first experiment for now
42        let lines: Vec<Line<'a>> = if let Some(exp_name) = self.app.experiment_names.first() {
43            let data = self.app.metric_series(exp_name, self.metric);
44            let rendered = chart::plot_series(&data, inner_width, inner_height, self.metric);
45            rendered
46                .into_iter()
47                .map(|s| Line::from(Span::raw(s)))
48                .collect()
49        } else {
50            vec![Line::from("No experiments")]
51        };
52
53        Paragraph::new(lines).block(block)
54    }
55}
56
57/// Render an info panel showing experiment status.
58pub fn info_panel<'a>(app: &App, focused: bool) -> Paragraph<'a> {
59    let border_style = if focused {
60        Style::default()
61            .fg(Color::Cyan)
62            .add_modifier(Modifier::BOLD)
63    } else {
64        Style::default().fg(Color::DarkGray)
65    };
66
67    let block = Block::default()
68        .title(" Info ")
69        .borders(Borders::ALL)
70        .border_style(border_style);
71
72    let mut lines: Vec<Line> = Vec::new();
73
74    for exp_name in &app.experiment_names {
75        lines.push(Line::from(Span::styled(
76            format!("Experiment: {exp_name}"),
77            Style::default()
78                .fg(Color::White)
79                .add_modifier(Modifier::BOLD),
80        )));
81
82        if let Some(step) = app.current_step(exp_name) {
83            lines.push(Line::from(format!("  Step: {step}")));
84        }
85
86        // Show best values for detected metrics
87        let (m1, m2) = &app.chart_metrics;
88        if let Some(best) = app.best_metric(exp_name, m1) {
89            lines.push(Line::from(format!("  Best {m1}: {best:.4}")));
90        }
91        if let Some(best) = app.best_metric(exp_name, m2) {
92            lines.push(Line::from(format!("  Best {m2}: {best:.4}")));
93        }
94
95        lines.push(Line::from(""));
96    }
97
98    // Show alerts
99    if !app.alerts.is_empty() {
100        lines.push(Line::from(Span::styled(
101            "Alerts:",
102            Style::default().fg(Color::Yellow),
103        )));
104        for alert in app.alerts.iter().rev().take(5) {
105            lines.push(Line::from(Span::styled(
106                format!("  ⚠ {alert}"),
107                Style::default().fg(Color::Yellow),
108            )));
109        }
110    }
111
112    // Keybindings help
113    lines.push(Line::from(""));
114    lines.push(Line::from(Span::styled(
115        "q:quit  Tab:focus  [/]:zoom",
116        Style::default().fg(Color::DarkGray),
117    )));
118
119    Paragraph::new(lines).block(block)
120}