use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders};
use ratatui::Frame;
use crate::app::state::AppState;
use crate::ui::theme;
pub fn draw(frame: &mut Frame, area: Rect, state: &mut AppState) {
let styles = theme::resolve(&state.config.theme);
let panel = &mut state.terminal;
let focused = panel.shell_focused();
let mut title = vec![Span::styled(" ", styles.border)];
for (index, session) in panel.sessions().iter().enumerate() {
if index > 0 {
title.push(Span::styled(" │ ", styles.border));
}
let label = format!("{}:{}", index + 1, session.label());
let style = if index == panel.active() {
styles
.command
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
} else {
styles.normal
};
title.push(Span::styled(label, style));
}
title.push(Span::styled(" ", styles.border));
let mut block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(styles.border)
.title(Line::from(title));
let inner = block.inner(area);
panel.resize_all((inner.height, inner.width));
let Some(session) = panel.sessions().get(panel.active()) else {
frame.render_widget(block, area);
return;
};
let parser = session.screen();
let screen = parser.screen();
let scrolled = screen.scrollback();
if scrolled > 0 {
block = block.title_bottom(Line::from(Span::styled(
format!(" ↑ {scrolled} lines — type to return "),
styles.command,
)));
}
frame.render_widget(block, area);
copy_screen(screen, inner, frame.buffer_mut());
if focused && scrolled == 0 && !screen.hide_cursor() {
let (row, col) = screen.cursor_position();
if row < inner.height && col < inner.width {
frame.set_cursor_position((inner.x + col, inner.y + row));
}
}
}
fn copy_screen(screen: &vt100::Screen, area: Rect, buf: &mut Buffer) {
for row in 0..area.height {
for col in 0..area.width {
let Some(target) = buf.cell_mut((area.x + col, area.y + row)) else {
continue;
};
let Some(cell) = screen.cell(row, col) else {
target.reset();
continue;
};
if cell.is_wide_continuation() {
target.set_symbol(" ");
continue;
}
let contents = cell.contents();
target.set_symbol(if contents.is_empty() { " " } else { contents });
target.set_style(style_of(cell));
}
}
}
fn style_of(cell: &vt100::Cell) -> Style {
let mut modifiers = Modifier::empty();
if cell.bold() {
modifiers |= Modifier::BOLD;
}
if cell.dim() {
modifiers |= Modifier::DIM;
}
if cell.italic() {
modifiers |= Modifier::ITALIC;
}
if cell.underline() {
modifiers |= Modifier::UNDERLINED;
}
if cell.inverse() {
modifiers |= Modifier::REVERSED;
}
Style::default()
.fg(color_of(cell.fgcolor()))
.bg(color_of(cell.bgcolor()))
.add_modifier(modifiers)
}
fn color_of(color: vt100::Color) -> Color {
match color {
vt100::Color::Default => Color::Reset,
vt100::Color::Idx(index) => Color::Indexed(index),
vt100::Color::Rgb(r, g, b) => Color::Rgb(r, g, b),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_screen_is_copied_with_its_colours() {
let mut parser = vt100::Parser::new(3, 10, 0);
parser.process(b"hi \x1b[31;1mred\x1b[0m");
let area = Rect::new(1, 1, 10, 3);
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 5));
copy_screen(parser.screen(), area, &mut buf);
let row: String = (1..11)
.map(|x| buf.cell((x, 1)).map_or(" ", |c| c.symbol()).to_owned())
.collect();
assert_eq!(row, "hi red ");
let r = buf.cell((4, 1)).unwrap();
assert_eq!(r.fg, Color::Indexed(1));
assert!(r.modifier.contains(Modifier::BOLD));
assert_eq!(buf.cell((1, 1)).unwrap().fg, Color::Reset);
}
}