use ratatui::{
Frame,
buffer::Buffer,
layout::{Position, Rect},
style::{Style, Stylize},
text::{Line, Span},
widgets::{Paragraph, Widget},
};
use tui_input::Input;
use unicode_width::UnicodeWidthStr;
use super::{ACCENT, RULE, text::fit_tail};
use crate::{
app::App,
index::{Session, short_path},
};
pub(crate) fn hrule(buf: &mut Buffer, area: Rect, label: Option<&str>) {
let mut spans: Vec<Span> = vec![" ".into()];
let mut used = 1;
if let Some(label) = label {
spans.push(label.fg(ACCENT));
spans.push(" ".into());
used += label.width() + 1;
}
spans.push(RULE.repeat((area.width as usize).saturating_sub(used + 1)).dim());
Line::from(spans).render(area, buf);
}
pub(crate) fn vrule(buf: &mut Buffer, area: Rect) {
for y in area.y .. area.bottom() {
buf[(area.x, y)].set_symbol("│").set_style(Style::new().dim());
}
}
pub(crate) fn search(frame: &mut Frame, app: &App, query: &Input, area: Rect) {
let right = match app.indexer.progress {
Some((done, total)) => Line::from(vec![meter(done, total, 8).fg(ACCENT), format!(" {done}/{total} ").dim()]),
None => {
let scope = if app.cwd_only { format!("in {} ", fit_tail(&short_path(&app.cwd), 32)) } else { String::new() };
Line::from(vec![
scope.fg(ACCENT),
app.matches.len().to_string().into(),
format!("/{} ", app.sessions.len()).dim(),
])
}
};
let right_width = right.width() as u16;
frame.render_widget(right.right_aligned(), area);
let prompt = " ⌕ ";
frame.render_widget(prompt.fg(ACCENT).bold(), area);
let text = Rect {
x: area.x + prompt.width() as u16,
width: area.width.saturating_sub(prompt.width() as u16 + right_width + 1),
..area
};
let scroll = query.visual_scroll((text.width as usize).saturating_sub(1));
frame.render_widget(Paragraph::new(query.value()).scroll((0, scroll as u16)), text);
frame.set_cursor_position(Position::new(text.x + (query.visual_cursor() - scroll) as u16, area.y));
}
fn meter(done: usize, total: usize, cells: usize) -> String {
let filled = (done * cells).checked_div(total).unwrap_or(0);
return format!("{}{}", "▰".repeat(filled), "▱".repeat(cells - filled));
}
pub(crate) fn footer(frame: &mut Frame, app: &App, status: Option<&str>, area: Rect, selection: Option<&Session>) {
let keys: [(&str, &str); 8] = [
("⏎", "select"),
("⇥", "preview"),
("^d ^e", "scroll"),
("^g", "end"),
("^y", "copy"),
("^r", "rescan"),
("^t", if app.cwd_only { "everywhere" } else { "here" }),
("esc", "quit"),
];
let mut spans: Vec<Span> = vec![" ".into()];
for (key, what) in keys {
spans.push(key.fg(ACCENT));
spans.push(format!(" {what} ").dim());
}
let hints = Line::from(spans);
let hints_width = hints.width() as u16;
frame.render_widget(hints, area);
let accent = Style::new().fg(ACCENT).italic();
let dim = Style::new().dim();
let warnings = app.indexer.warnings.len();
let note = None
.or(status.map(|status| (status.to_string(), accent)))
.or(app.indexer.failure.as_ref().map(|why| (format!("indexer stopped: {why}"), accent)))
.or((warnings > 0).then(|| (format!("{warnings} files with skipped records, see ccf --verify"), dim)))
.or(selection.map(|s| (format!("{} {}", s.short_cwd(), s.short_id()), dim)));
let Some((note, style)) = note
else {
return;
};
let room = Rect {
x: area.x + hints_width,
width: area.width.saturating_sub(hints_width + 1),
..area
};
if room.width < 8 {
return;
}
let note = fit_tail(¬e, room.width as usize);
frame.render_widget(Line::from(Span::styled(note, style)).right_aligned(), room);
}