pub mod banner;
pub mod embed_terminal;
pub mod key_encode;
pub mod layout;
pub mod modal;
pub mod mouse_encode;
pub mod preview;
pub mod section_preview;
pub mod session_list;
pub mod statusbar;
pub mod tab_strip;
pub mod theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::Frame;
use crate::app::AppState;
use crate::ui::embed_terminal::EmbedTerminal;
pub use theme::Theme;
pub fn draw(
frame: &mut Frame<'_>,
state: &AppState,
theme: &Theme,
embed: Option<&EmbedTerminal>,
embed_focused: bool,
) {
draw_inner(frame, state, theme, embed, embed_focused);
if !theme::terminal_truecolor() {
theme::degrade_buffer_to_256(frame.buffer_mut());
}
}
fn draw_inner(
frame: &mut Frame<'_>,
state: &AppState,
theme: &Theme,
embed: Option<&EmbedTerminal>,
embed_focused: bool,
) {
let area = frame.area();
let collapsed = state.single_window_mode && embed_focused && state.sidebar_hidden;
let l = layout::compute(area, state.divider_x, collapsed);
if state.single_window_mode && embed_focused && l.preview.is_none() {
let body = Rect::new(l.list.x, l.list.y, l.list.width, l.list.height);
preview::render(frame, body, state, theme, embed, false);
statusbar::render(frame, l.statusbar, state, theme);
state.modals.render(frame, area, theme);
return;
}
let list_content = if state.single_window_mode {
inset_one(l.list)
} else {
l.list
};
session_list::render(frame, list_content, state, theme);
if let Some(preview_area) = l.preview {
preview::render(frame, preview_area, state, theme, embed, true);
}
if let Some(divider_area) = l.divider {
render_divider(frame, divider_area, state, theme);
}
statusbar::render(frame, l.statusbar, state, theme);
if state.single_window_mode {
let active = if embed_focused {
l.preview.map(|p| {
if has_tabstrip(state) && p.height >= 2 {
Rect::new(p.x, p.y + 1, p.width, p.height - 1)
} else {
p
}
})
} else {
Some(l.list)
};
if let Some(rect) = active {
draw_focus_border(frame.buffer_mut(), rect, theme.accent, theme.bg);
}
}
state.modals.render(frame, area, theme);
}
fn inset_one(area: Rect) -> Rect {
if area.width < 2 || area.height < 2 {
return Rect::new(area.x, area.y, 0, 0);
}
Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2)
}
fn has_tabstrip(state: &AppState) -> bool {
state
.sidebar
.visible()
.get(state.selected)
.map(|e| e.container().is_some())
.unwrap_or(false)
}
fn draw_focus_border(buf: &mut Buffer, area: Rect, fg: Color, bg: Color) {
if area.width < 2 || area.height < 2 {
return;
}
let style = Style::default().fg(fg).bg(bg);
let left = area.left();
let right = area.right() - 1;
let top = area.top();
let bottom = area.bottom() - 1;
for x in left..=right {
let cell = &mut buf[(x, top)];
cell.set_char('─');
cell.set_style(style);
let cell = &mut buf[(x, bottom)];
cell.set_char('─');
cell.set_style(style);
}
for y in top..=bottom {
let cell = &mut buf[(left, y)];
cell.set_char('│');
cell.set_style(style);
let cell = &mut buf[(right, y)];
cell.set_char('│');
cell.set_style(style);
}
let cell = &mut buf[(left, top)];
cell.set_char('╭');
cell.set_style(style);
let cell = &mut buf[(right, top)];
cell.set_char('╮');
cell.set_style(style);
let cell = &mut buf[(left, bottom)];
cell.set_char('╰');
cell.set_style(style);
let cell = &mut buf[(right, bottom)];
cell.set_char('╯');
cell.set_style(style);
}
fn render_divider(frame: &mut Frame<'_>, area: Rect, state: &AppState, theme: &Theme) {
let fg = if state.dragging_divider {
theme.accent
} else {
theme.text_muted
};
let style = Style::default().fg(fg).bg(theme.bg);
let buf = frame.buffer_mut();
for y in area.top()..area.bottom() {
let cell = &mut buf[(area.left(), y)];
cell.set_char('│');
cell.set_style(style);
}
}