//! Beautiful CLI UI module for marathonctl using ratatui //! //! Provides simple builder APIs for rendering beautiful terminal output //! without taking over the terminal (inline mode). //! //! # Examples //! //! ```rust //! // Render a status table //! ui::table("Session Status") //! .row("Node ID", node_id) //! .row("Session", session_id) //! .row("Queue Size", queue_size) //! .render(); //! //! // Render a list //! ui::list("Connected Peers") //! .item(peer1) //! .item(peer2) //! .render(); //! //! // Render a data grid //! ui::grid("Sessions") //! .header(&["ID", "State", "Entities"]) //! .row(&[id1, state1, count1]) //! .row(&[id2, state2, count2]) //! .render(); //! ``` use ratatui::prelude::*; use ratatui::widgets::*; use ratatui::{TerminalOptions, Viewport}; /// Create a key-value table (like status output) pub fn table(title: &str) -> TableBuilder { TableBuilder::new(title) } /// Create a simple list pub fn list(title: &str) -> ListBuilder { ListBuilder::new(title) } /// Create a data grid with headers pub fn grid(title: &str) -> GridBuilder { GridBuilder::new(title) } /// Builder for key-value tables pub struct TableBuilder { title: String, rows: Vec<(String, String)>, } impl TableBuilder { fn new(title: &str) -> Self { Self { title: title.to_string(), rows: Vec::new(), } } /// Add a key-value row pub fn row(mut self, key: impl std::fmt::Display, value: impl std::fmt::Display) -> Self { self.rows.push((key.to_string(), value.to_string())); self } /// Render the table to the terminal pub fn render(self) { let height = self.rows.len() + 4; // rows + borders + header + padding render_widget(height, |frame| { let area = frame.area(); let rows: Vec = self .rows .iter() .map(|(k, v)| Row::new(vec![k.clone(), v.clone()])) .collect(); let table = Table::new( rows, [Constraint::Length(20), Constraint::Min(30)], ) .header( Row::new(vec!["Field", "Value"]) .bold() .style(Style::default().fg(Color::Green)), ) .block( Block::default() .title(format!(" {} ", self.title)) .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)), ) .column_spacing(2); frame.render_widget(table, area); }); } } /// Builder for simple lists pub struct ListBuilder { title: String, items: Vec, } impl ListBuilder { fn new(title: &str) -> Self { Self { title: title.to_string(), items: Vec::new(), } } /// Add an item to the list pub fn item(mut self, item: impl std::fmt::Display) -> Self { self.items.push(item.to_string()); self } /// Render the list to the terminal pub fn render(self) { let height = self.items.len() + 3; // items + borders + title render_widget(height, |frame| { let area = frame.area(); let items: Vec = self .items .iter() .map(|i| ListItem::new(format!(" • {}", i))) .collect(); let list = List::new(items) .block( Block::default() .title(format!(" {} ", self.title)) .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)), ); frame.render_widget(list, area); }); } } /// Builder for data grids with headers pub struct GridBuilder { title: String, headers: Vec, rows: Vec>, } impl GridBuilder { fn new(title: &str) -> Self { Self { title: title.to_string(), headers: Vec::new(), rows: Vec::new(), } } /// Set the header row pub fn header(mut self, headers: &[impl std::fmt::Display]) -> Self { self.headers = headers.iter().map(|h| h.to_string()).collect(); self } /// Add a data row pub fn row(mut self, cells: &[impl std::fmt::Display]) -> Self { self.rows.push(cells.iter().map(|c| c.to_string()).collect()); self } /// Render the grid to the terminal pub fn render(self) { let height = self.rows.len() + 4; // rows + borders + header + padding render_widget(height, |frame| { let area = frame.area(); // Create constraints based on number of columns let col_count = self.headers.len().max( self.rows.iter().map(|r| r.len()).max().unwrap_or(0) ); let constraints = vec![Constraint::Ratio(1, col_count as u32); col_count]; let header = Row::new(self.headers.clone()) .bold() .style(Style::default().fg(Color::Green)); let rows: Vec = self .rows .iter() .map(|r| Row::new(r.clone())) .collect(); let table = Table::new(rows, constraints) .header(header) .block( Block::default() .title(format!(" {} ", self.title)) .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)), ) .column_spacing(2); frame.render_widget(table, area); }); } } /// Internal helper to render a widget using inline viewport fn render_widget(height: usize, render_fn: F) where F: FnOnce(&mut Frame), { let mut terminal = ratatui::init_with_options(TerminalOptions { viewport: Viewport::Inline(height as u16), }); terminal.draw(render_fn).unwrap(); ratatui::restore(); }