pub mod annotations;
pub mod bars;
pub mod chips;
pub mod chrome;
pub mod comment;
pub mod diff_doc;
pub mod diff_view;
pub mod fetched;
pub mod file_tree;
pub mod form;
pub mod icons;
pub mod markdown;
pub mod overlays;
pub mod page;
pub mod pages;
pub mod review_sheets;
pub mod text;
pub mod time;
use crate::theme::{Bg, Theme};
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, Widget};
pub use fetched::Fetched;
pub use icons::Icons;
pub const PAD_X: u16 = 2;
pub const PAD_Y: u16 = 1;
#[derive(Clone, Copy)]
pub struct Ctx<'a> {
pub theme: &'a Theme,
pub icons: Icons,
pub now: u64,
}
pub fn cols(n: usize) -> u16 {
u16::try_from(n).unwrap_or(u16::MAX)
}
pub fn idx(n: usize) -> u32 {
u32::try_from(n).unwrap_or(u32::MAX)
}
pub fn put(buf: &mut Buffer, x: u16, y: u16, text: &str, style: Style) {
if !buf.area.contains(Position { x, y }) {
return;
}
let room = usize::from(buf.area.right().saturating_sub(x));
#[expect(
clippy::disallowed_methods,
reason = "the one sanctioned unclipped write: the checks above clip"
)]
buf.set_stringn(x, y, text, room, style);
}
pub fn fill(buf: &mut Buffer, area: Rect, theme: &Theme, bg: Bg) {
Clear.render(area, buf);
buf.set_style(area, theme.fill(bg));
}
#[deny(clippy::arithmetic_side_effects)]
fn render_split(area: Rect, buf: &mut Buffer, left: &[Span<'_>], right: &[Span<'_>], gap: u16) {
let right_width = cols(text::spans_width(right)).min(area.width);
let left_area = Rect {
width: area.width.saturating_sub(right_width.saturating_add(gap)),
..area
};
let right_area = Rect {
x: area.right().saturating_sub(right_width),
width: right_width,
..area
};
Line::from(fit_spans(left, left_area.width)).render(left_area, buf);
Line::from(fit_spans(right, right_width)).render(right_area, buf);
}
fn fit_spans(spans: &[Span<'_>], room: u16) -> Vec<Span<'static>> {
let mut room = usize::from(room);
spans
.iter()
.map(|s| {
let shown = text::truncate(&s.content, room);
room = room.saturating_sub(text::width(&shown));
Span::styled(shown, s.style)
})
.collect()
}
fn list_row(
buf: &mut Buffer,
theme: &Theme,
panel: Rect,
y: u16,
selected: bool,
base: Bg,
) -> (Rect, Bg) {
let bg = if selected { Bg::SelectedHigh } else { base };
let row = Rect::new(panel.x, y, panel.width, 1).intersection(panel);
if selected && !row.is_empty() {
fill(buf, row, theme, bg);
Span::styled("▌", theme.accent(bg)).render(row, buf);
}
(inset(row, PAD_X, 0), bg)
}
#[deny(clippy::arithmetic_side_effects)]
fn label_hint(buf: &mut Buffer, row: Rect, left: &[Span<'_>], hint: Span<'_>) {
let hint_w = cols(text::width(&hint.content)).min(row.width);
let gap = if hint_w > 0 {
hint_w.saturating_add(2)
} else {
0
};
Line::from(fit_spans(left, row.width.saturating_sub(gap))).render(row, buf);
let hint_area = Rect {
x: row.right().saturating_sub(hint_w),
width: hint_w,
..row
};
hint.render(hint_area, buf);
}
fn key_hints(theme: &Theme, bg: Bg, hints: &[(&str, &str)]) -> Vec<Span<'static>> {
let mut spans = Vec::new();
for (i, (key, what)) in hints.iter().enumerate() {
if i > 0 {
spans.push(Span::styled(" · ", theme.meta(bg)));
}
spans.push(Span::styled(
key.to_string(),
theme.accent(bg).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(format!(" {what}"), theme.meta(bg)));
}
spans
}
pub fn padded(area: Rect) -> Rect {
inset(area, PAD_X, PAD_Y)
}
#[deny(clippy::arithmetic_side_effects)]
pub fn inset(area: Rect, x: u16, y: u16) -> Rect {
let x = x.min(area.width / 2);
let y = y.min(area.height / 2);
Rect {
x: area.x.saturating_add(x),
y: area.y.saturating_add(y),
width: area.width.saturating_sub(x).saturating_sub(x),
height: area.height.saturating_sub(y).saturating_sub(y),
}
}
#[deny(clippy::arithmetic_side_effects)]
pub fn centered(area: Rect, width: u16, height: u16, top: u16) -> Rect {
let width = width.min(area.width);
let top = top.min(area.height.saturating_sub(1));
let height = height.min(area.height.saturating_sub(top));
Rect {
x: area.x.saturating_add(area.width.saturating_sub(width) / 2),
y: area.y.saturating_add(top),
width,
height,
}
}