pub mod cards;
pub mod chat;
pub mod overlay;
pub mod rail;
pub mod status_bar;
pub mod stream;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use ratatui::Frame;
use crate::tui::app::DashboardApp;
use crate::tui::rail::Tone;
pub const MIN_WIDTH: u16 = 40;
pub const MIN_HEIGHT: u16 = 24;
pub const TWO_COLUMN_WIDTH: u16 = 80;
const STACKED_CANVAS_PERCENT: u16 = 55;
const STACKED_STREAM_MIN: u16 = 6;
const LEFT_MIN: u16 = 38;
const LEFT_MAX: u16 = 76;
const LEFT_PERCENT: u16 = 48;
const RIGHT_MIN: u16 = 36;
pub fn draw(frame: &mut Frame, app: &mut DashboardApp) {
app.hits.clear();
let area = frame.area();
if area.width < MIN_WIDTH || area.height < MIN_HEIGHT {
let notice = Paragraph::new(format!(
"Terminal too small: {}x{} (need {}x{}). Resize the window.",
area.width, area.height, MIN_WIDTH, MIN_HEIGHT
))
.style(app.styles.warning);
frame.render_widget(notice, area);
return;
}
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(area);
let body = rows[0];
let status = rows[1];
let (left, right) = if body.width >= TWO_COLUMN_WIDTH {
let left_width = ((body.width as u32 * LEFT_PERCENT as u32) / 100) as u16;
let left_width = left_width
.clamp(LEFT_MIN, LEFT_MAX)
.min(body.width.saturating_sub(RIGHT_MIN));
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(left_width), Constraint::Min(RIGHT_MIN)])
.split(body);
(columns[0], columns[1])
} else {
let canvas_height = ((body.height as u32 * STACKED_CANVAS_PERCENT as u32) / 100) as u16;
let canvas_height = canvas_height.min(
body.height
.saturating_sub(STACKED_STREAM_MIN + chat::input_height(app)),
);
let stack = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(canvas_height), Constraint::Min(1)])
.split(body);
(stack[0], stack[1])
};
cards::draw(frame, app, left);
let input_height = chat::input_height(app);
let right_rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(4), Constraint::Length(input_height)])
.split(right);
stream::draw(frame, app, right_rows[0]);
chat::draw_input(frame, app, right_rows[1]);
status_bar::draw(frame, app, status);
if app.modal().is_some() {
overlay::draw(frame, app, area);
}
}
pub fn pane_block<'a>(app: &DashboardApp, focused: bool) -> Block<'a> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(if focused {
app.styles.accent
} else {
app.styles.separator
})
}
pub fn tone_style(app: &DashboardApp, tone: Tone) -> Style {
match tone {
Tone::Normal => app.styles.normal,
Tone::Dim => app.styles.dimmed,
Tone::Good => app.styles.success,
Tone::Warn => app.styles.warning,
Tone::Bad => app.styles.error,
Tone::Accent => app.styles.info,
Tone::Server => app.styles.server,
Tone::Client => app.styles.client,
Tone::Title => app.styles.title,
Tone::Reasoning => app.styles.reasoning,
}
}
pub fn centered(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
centered_capped(area, percent_x, percent_y, u16::MAX, u16::MAX)
}
pub fn centered_capped(
area: Rect,
percent_x: u16,
percent_y: u16,
max_cols: u16,
max_rows: u16,
) -> Rect {
let width = (area.width * percent_x / 100)
.min(max_cols)
.max(24)
.min(area.width);
let height = (area.height * percent_y / 100)
.min(max_rows)
.max(5)
.min(area.height);
Rect {
x: area.x + (area.width.saturating_sub(width)) / 2,
y: area.y + (area.height.saturating_sub(height)) / 2,
width,
height,
}
}