pub mod theme;
use std::path::Path;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::app::{App, Mode};
use crate::ids::PaneId;
use crate::ui::theme::{State, Theme};
mod borders;
mod git;
mod help;
mod panes;
mod picker;
mod settings;
mod sidebar;
mod status;
mod tabbar;
pub fn render(f: &mut Frame, app: &mut App) {
let t = app.theme.clone();
let cat = app.catalog;
let area = f.area();
f.render_widget(Block::new().style(Style::new().bg(t.mantle)), area);
let [main, status] = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(area);
let (sidebar, content) = if app.sidebar_visible {
let sw = app.sidebar_width.min(main.width.saturating_sub(24));
if sw >= crate::app::SIDEBAR_WIDTH_MIN {
let [s, c] =
Layout::horizontal([Constraint::Length(sw), Constraint::Min(0)]).areas(main);
(Some(s), c)
} else {
(None, main)
}
} else {
(None, main)
};
let [tabbar, pane_area] =
Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(content);
app.last_pane_area = pane_area;
let focus = app.layout().focus;
let rects: Vec<(PaneId, Rect)> = if app.zoomed {
vec![(focus, pane_area)]
} else {
app.layout()
.panes(pane_area)
.into_iter()
.map(|p| (p.id, p.rect))
.collect()
};
let bordered = rects.len() > 1;
for (id, rect) in &rects {
if let Some(content) = pane_content(*rect, bordered) {
if let Some(p) = app.panes.get_mut(id) {
p.resize(content.width, content.height);
}
}
}
let (ws_rects, agent_rects, session_rects, session_del_rects, new_ws_rect) =
if let Some(s) = sidebar {
sidebar::draw_sidebar(f, s, app, &t)
} else {
(Vec::new(), Vec::new(), Vec::new(), Vec::new(), None)
};
let (tab_rects, tab_close_rects, tab_prev, tab_next) = tabbar::draw_tabbar(f, tabbar, app, &t);
f.render_widget(Block::new().style(Style::new().bg(t.mantle)), pane_area);
app.pane_close_rect = if bordered {
rects
.iter()
.find(|(id, _)| *id == focus)
.and_then(|(_, r)| pane_close_rect(*r, bordered))
} else {
None
};
let mut git_section_rects = Vec::new();
let cursor = if let Some(g) = app.active_git_mut() {
git_section_rects = git::render(f, pane_area, g, cat, &t);
None
} else {
let cursor = panes::draw_panes(f, &rects, bordered, app, &t);
if bordered {
borders::render_pane_borders(f, &rects, focus, &t);
if app.config.layout.show_titles {
panes::draw_pane_titles(f, &rects, focus, app, &t);
}
}
cursor
};
app.git_section_rects = git_section_rects;
status::draw_status(f, status, app, &t);
let settings_hits = app
.settings
.is_some()
.then(|| settings::draw_settings(f, area, app, &t));
if let Some(h) = &settings_hits {
app.settings_modal_rect = Some(h.modal);
app.settings_close_rect = Some(h.close);
app.settings_tab_rects = h.tabs.clone();
app.settings_ctl_rects = h.ctls.clone();
app.settings_arrow_rects = h.arrows.clone();
} else {
app.settings_modal_rect = None;
app.settings_close_rect = None;
app.settings_tab_rects.clear();
app.settings_ctl_rects.clear();
app.settings_arrow_rects.clear();
}
let picker_open = app.picker.is_some();
let mut picker_rects = Vec::new();
if let Some(p) = &app.picker {
picker_rects = picker::draw_picker(f, area, p, cat, &t);
}
app.picker_rects = picker_rects;
if app.help_open {
help::draw_help(f, area, app, &t);
}
if let Some(buf) = &app.worktree_prompt {
picker::draw_worktree_prompt(f, area, buf, app.worktree_error.as_deref(), cat, &t);
}
let cursor =
if settings_hits.is_some() || picker_open || app.help_open || app.worktree_prompt.is_some()
{
None
} else {
cursor
};
if let Some(p) = cursor {
f.set_cursor_position(p);
}
app.last_cursor = cursor;
app.pane_rects = rects;
app.tab_rects = tab_rects;
app.tab_close_rects = tab_close_rects;
app.tab_prev_rect = tab_prev;
app.tab_next_rect = tab_next;
app.ws_rects = ws_rects;
app.agent_rects = agent_rects;
app.session_rects = session_rects;
app.session_del_rects = session_del_rects;
app.new_ws_rect = new_ws_rect;
}
fn pane_inner(rect: Rect, bordered: bool) -> Option<Rect> {
if !bordered {
if rect.width < 1 || rect.height < 1 {
return None;
}
return Some(rect);
}
if rect.width < 4 || rect.height < 4 {
return None;
}
Some(Rect::new(
rect.x + 1,
rect.y + 1,
rect.width - 2,
rect.height - 2,
))
}
pub(super) const LONE_PANE_HPAD: u16 = 1;
pub(super) fn hint_line(pairs: &[(&str, &str)], t: &Theme) -> Line<'static> {
let mut spans = vec![Span::raw(" ")];
for (i, (key, label)) in pairs.iter().enumerate() {
if i > 0 {
spans.push(Span::styled(" · ", Style::new().fg(t.overlay0)));
}
spans.push(Span::styled(
key.to_string(),
Style::new().fg(t.accent).bold(),
));
if !label.is_empty() {
spans.push(Span::styled(
format!(" {label}"),
Style::new().fg(t.subtext1),
));
}
}
Line::from(spans)
}
pub(super) fn display_width(s: &str) -> usize {
use unicode_width::UnicodeWidthStr;
s.width()
}
pub(super) fn lone_pad(width: u16) -> u16 {
if width > 2 * LONE_PANE_HPAD + 2 {
LONE_PANE_HPAD
} else {
0
}
}
fn pane_content(rect: Rect, bordered: bool) -> Option<Rect> {
if bordered {
return pane_inner(rect, true);
}
let pad = lone_pad(rect.width);
let c = Rect::new(
rect.x + pad,
rect.y + 1,
rect.width.saturating_sub(2 * pad),
rect.height.saturating_sub(1),
);
if c.width < 1 || c.height < 1 {
return None;
}
Some(c)
}
fn pane_close_rect(area: Rect, bordered: bool) -> Option<Rect> {
if !bordered || area.width < 9 {
return None;
}
Some(Rect::new(area.x + area.width - 4, area.y, 3, 1))
}
fn pane_state(app: &App, id: PaneId) -> State {
app.status
.get(&id)
.map(|s| s.state)
.unwrap_or(State::Unknown)
}
fn short_path(p: &Path, max: u16) -> String {
let mut s = p.display().to_string();
if let Some(home) = crate::platform::home_dir() {
if let Some(rest) = s.strip_prefix(home.to_string_lossy().as_ref()) {
s = format!("~{rest}");
}
}
let max = max as usize;
if s.chars().count() > max && max > 1 {
let tail: String = s
.chars()
.rev()
.take(max - 1)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
format!("…{tail}")
} else {
s
}
}