Skip to main content

alopex_cli/tui/
detail.rs

1//! Detail panel for selected row.
2
3use ratatui::layout::Rect;
4use ratatui::style::{Color, Style};
5use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6use ratatui::Frame;
7
8use crate::models::{Column, Row, Value};
9
10/// Detail panel state.
11#[derive(Default)]
12pub struct DetailPanel {
13    visible: bool,
14    scroll: u16,
15}
16
17impl DetailPanel {
18    pub fn toggle(&mut self) {
19        self.visible = !self.visible;
20    }
21
22    pub fn is_visible(&self) -> bool {
23        self.visible
24    }
25
26    pub fn scroll_up(&mut self) {
27        self.scroll = self.scroll.saturating_sub(1);
28    }
29
30    pub fn scroll_down(&mut self) {
31        self.scroll = self.scroll.saturating_add(1);
32    }
33
34    pub fn render(&self, frame: &mut Frame<'_>, area: Rect, columns: &[Column], row: &Row) {
35        let content = format_row_detail(columns, row);
36        let paragraph = Paragraph::new(content)
37            .block(Block::default().borders(Borders::ALL).title("Detail"))
38            .style(Style::default().fg(Color::White))
39            .scroll((self.scroll, 0))
40            .wrap(Wrap { trim: false });
41        frame.render_widget(paragraph, area);
42    }
43
44    pub fn render_empty(&self, frame: &mut Frame<'_>, area: Rect) {
45        let paragraph = Paragraph::new("No row selected")
46            .block(Block::default().borders(Borders::ALL).title("Detail"))
47            .style(Style::default().fg(Color::Gray));
48        frame.render_widget(paragraph, area);
49    }
50}
51
52fn format_row_detail(columns: &[Column], row: &Row) -> String {
53    let mut map = serde_json::Map::new();
54    for (idx, column) in columns.iter().enumerate() {
55        let value = row
56            .columns
57            .get(idx)
58            .map(value_to_json)
59            .unwrap_or(serde_json::Value::Null);
60        map.insert(column.name.clone(), value);
61    }
62    let json = serde_json::Value::Object(map);
63
64    let json_text = serde_json::to_string_pretty(&json).unwrap_or_else(|_| "{}".to_string());
65    let yaml_text = serde_yaml::to_string(&json).unwrap_or_else(|_| "".to_string());
66
67    format!("JSON\n{json_text}\n\nYAML\n{yaml_text}")
68}
69
70fn value_to_json(value: &Value) -> serde_json::Value {
71    match value {
72        Value::Null => serde_json::Value::Null,
73        Value::Bool(value) => serde_json::Value::Bool(*value),
74        Value::Int(value) => serde_json::Value::Number((*value).into()),
75        Value::Float(value) => serde_json::Number::from_f64(*value)
76            .map(serde_json::Value::Number)
77            .unwrap_or(serde_json::Value::Null),
78        Value::Text(value) => serde_json::Value::String(value.clone()),
79        Value::Bytes(bytes) => serde_json::Value::Array(
80            bytes
81                .iter()
82                .map(|byte| serde_json::Value::Number((*byte).into()))
83                .collect(),
84        ),
85        Value::Vector(values) => serde_json::Value::Array(
86            values
87                .iter()
88                .filter_map(|value| serde_json::Number::from_f64(*value as f64))
89                .map(serde_json::Value::Number)
90                .collect(),
91        ),
92    }
93}