use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Widget;
use super::layout::Layout;
pub const LEFT_TITLE: &str = "[AGENT STATUS]";
pub const CHAT_TITLE: &str = "[AGENT LOG STREAM]";
pub const RIGHT_TITLE: &str = "[SYSTEM]";
#[derive(Clone, Copy, Debug)]
pub struct TopFrame<'a> {
layout: &'a Layout,
style: Style,
}
impl<'a> TopFrame<'a> {
pub fn new(layout: &'a Layout) -> Self {
Self {
layout,
style: Style::default().fg(Color::Green),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl<'a> Widget for TopFrame<'a> {
fn render(self, _area: Rect, buf: &mut Buffer) {
let l = self.layout;
if l.top_frame.height == 0 || l.top_frame.width == 0 {
return;
}
let y = l.top_frame.y;
paint_titled_horizontal(buf, 0, y, l.chat_v_left_col, LEFT_TITLE, '─', self.style);
if l.chat_v_left_col < l.top_frame.width {
buf[(l.chat_v_left_col, y)]
.set_char('╭')
.set_style(self.style);
}
let chat_inner_w = l
.chat_v_right_col
.saturating_sub(l.chat_v_left_col)
.saturating_sub(1);
paint_titled_horizontal(
buf,
l.chat_v_left_col.saturating_add(1),
y,
chat_inner_w,
CHAT_TITLE,
'─',
self.style,
);
if l.chat_v_right_col < l.top_frame.width {
buf[(l.chat_v_right_col, y)]
.set_char('╮')
.set_style(self.style);
}
let right_start = l.chat_v_right_col.saturating_add(1);
let right_w = l.top_frame.width.saturating_sub(right_start);
paint_titled_horizontal(buf, right_start, y, right_w, RIGHT_TITLE, '─', self.style);
}
}
#[derive(Clone, Copy, Debug)]
pub struct ChatBotFrame<'a> {
layout: &'a Layout,
style: Style,
}
impl<'a> ChatBotFrame<'a> {
pub fn new(layout: &'a Layout) -> Self {
Self {
layout,
style: Style::default().fg(Color::Green),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl<'a> Widget for ChatBotFrame<'a> {
fn render(self, _area: Rect, buf: &mut Buffer) {
let l = self.layout;
if l.chat_bot_frame.height == 0 {
return;
}
let y = l.chat_bot_frame.y;
if l.chat_v_left_col < l.chat_bot_frame.width {
buf[(l.chat_v_left_col, y)]
.set_char('╰')
.set_style(self.style);
}
let inner_w = l
.chat_v_right_col
.saturating_sub(l.chat_v_left_col)
.saturating_sub(1);
for i in 0..inner_w {
let x = l.chat_v_left_col.saturating_add(1).saturating_add(i);
if x >= l.chat_bot_frame.width {
break;
}
buf[(x, y)].set_char('─').set_style(self.style);
}
if l.chat_v_right_col < l.chat_bot_frame.width {
buf[(l.chat_v_right_col, y)]
.set_char('╯')
.set_style(self.style);
}
}
}
fn paint_titled_horizontal(
buf: &mut Buffer,
x: u16,
y: u16,
width: u16,
title: &str,
fill: char,
style: Style,
) {
if width == 0 {
return;
}
let tw = title.chars().count() as u16;
if tw >= width {
for i in 0..width {
buf[(x + i, y)].set_char(fill).set_style(style);
}
return;
}
let pad = width - tw;
let left = pad / 2;
for i in 0..left {
buf[(x + i, y)].set_char(fill).set_style(style);
}
for (i, ch) in title.chars().enumerate() {
buf[(x + left + i as u16, y)].set_char(ch).set_style(style);
}
let right_start = left + tw;
for i in right_start..width {
buf[(x + i, y)].set_char(fill).set_style(style);
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
#[test]
fn top_frame_wide_terminal_layout() {
let layout = Layout::new(60, 10, 1);
let _ = layout;
let layout = Layout::new(160, 30, 1);
assert_eq!(layout.chat_v_left_col, 19);
assert_eq!(layout.chat_v_right_col, 140);
let mut backend = TestBackend::new(160, 30);
let mut terminal = Terminal::new(backend.clone()).unwrap();
terminal
.draw(|f| {
let area = f.area();
f.render_widget(TopFrame::new(&layout), area);
})
.unwrap();
backend = terminal.backend().clone();
let row0: String = (0..160)
.map(|x| {
backend
.buffer()
.cell((x, 0))
.unwrap()
.symbol()
.chars()
.next()
.unwrap()
})
.collect();
let expected_left = format!("{}{}{}", "─".repeat(2), LEFT_TITLE, "─".repeat(3));
let expected_chat = format!("{}{}{}", "─".repeat(51), CHAT_TITLE, "─".repeat(51));
let expected_right = format!("{}{}{}", "─".repeat(5), RIGHT_TITLE, "─".repeat(6));
let expected = format!("{}╭{}╮{}", expected_left, expected_chat, expected_right);
assert_eq!(row0, expected, "top frame row 0 mismatch");
}
#[test]
fn top_frame_narrow_terminal() {
let layout = Layout::new(40, 10, 1);
assert_eq!(layout.chat_v_left_col, 0);
assert_eq!(layout.chat_v_right_col, 39);
let mut backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend.clone()).unwrap();
terminal
.draw(|f| {
let area = f.area();
f.render_widget(TopFrame::new(&layout), area);
})
.unwrap();
backend = terminal.backend().clone();
let row0: String = (0..40)
.map(|x| {
backend
.buffer()
.cell((x, 0))
.unwrap()
.symbol()
.chars()
.next()
.unwrap()
})
.collect();
let expected = format!("╭{}{}{}╮", "─".repeat(10), CHAT_TITLE, "─".repeat(10));
assert_eq!(row0, expected);
}
#[test]
fn chat_bot_frame_only_paints_chat_band() {
let layout = Layout::new(160, 30, 1);
let mut backend = TestBackend::new(160, 30);
let mut terminal = Terminal::new(backend.clone()).unwrap();
terminal
.draw(|f| {
let area = f.area();
f.render_widget(ChatBotFrame::new(&layout), area);
})
.unwrap();
backend = terminal.backend().clone();
let row = layout.chat_bot_frame.y;
for x in 0..layout.chat_v_left_col {
let c = backend
.buffer()
.cell((x, row))
.unwrap()
.symbol()
.chars()
.next()
.unwrap();
assert_eq!(c, ' ', "left side at col {x} should be blank, got {c:?}");
}
for x in (layout.chat_v_right_col + 1)..160 {
let c = backend
.buffer()
.cell((x, row))
.unwrap()
.symbol()
.chars()
.next()
.unwrap();
assert_eq!(c, ' ', "right side at col {x} should be blank, got {c:?}");
}
assert_eq!(
backend
.buffer()
.cell((layout.chat_v_left_col, row))
.unwrap()
.symbol(),
"╰"
);
assert_eq!(
backend
.buffer()
.cell((layout.chat_v_right_col, row))
.unwrap()
.symbol(),
"╯"
);
for x in (layout.chat_v_left_col + 1)..layout.chat_v_right_col {
assert_eq!(
backend.buffer().cell((x, row)).unwrap().symbol(),
"─",
"expected ─ at col {x}"
);
}
}
#[test]
fn titled_horizontal_drops_title_when_too_narrow() {
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
paint_titled_horizontal(&mut buf, 0, 0, 5, "[AGENT STATUS]", '─', Style::default());
let row: String = (0..5)
.map(|x| buf.cell((x, 0)).unwrap().symbol().chars().next().unwrap())
.collect();
assert_eq!(row, "─────");
}
}