use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Wrap};
pub(super) fn wrapped_line_count(text: &str, width: usize) -> usize {
if width == 0 {
return 1;
}
let mut rows = 1usize;
let mut col = 0usize;
for (i, word) in text.split(' ').enumerate() {
let w = word.chars().count();
let need = if i == 0 { w } else { col + 1 + w };
if need <= width {
col = need;
} else {
rows += 1;
col = w;
}
while col > width {
rows += 1;
col -= width;
}
}
rows
}
pub(super) fn titled(name: &str, count: usize) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.title(format!(" {name} ({count}) "))
}
pub(super) fn empty(f: &mut Frame, area: Rect, name: &str, msg: &str) {
let para = Paragraph::new(msg)
.block(titled(name, 0))
.style(Style::default().add_modifier(Modifier::DIM))
.wrap(Wrap { trim: false });
f.render_widget(para, area);
}
pub(super) fn shell_join_display(argv: &[String]) -> String {
let shown: Vec<String> = argv
.iter()
.map(|a| {
let c = crate::ini::collapse_tilde(a);
match c.strip_prefix("~/") {
Some(rest) if !needs_quoting(rest) => c,
_ => shell_join(std::slice::from_ref(&c)),
}
})
.collect();
shown.join(" ")
}
fn needs_quoting(s: &str) -> bool {
s.is_empty()
|| s.chars()
.any(|c| c.is_whitespace() || "\"'\\$`;&|<>()*?[]{}!#~".contains(c))
}
pub(super) fn shell_join(argv: &[String]) -> String {
argv.iter()
.map(|a| {
if needs_quoting(a) {
format!("'{}'", a.replace('\'', r"'\''"))
} else {
a.clone()
}
})
.collect::<Vec<_>>()
.join(" ")
}
pub(super) const BOX_MIN_W: u16 = 24;
pub(super) const BOX_MAX_W: u16 = 88;
pub(super) const BOX_CHROME_H: u16 = 3;
pub(super) const BOX_CHROME_W: u16 = 6;
pub(super) fn box_width(screen_w: u16) -> u16 {
screen_w.saturating_sub(4).clamp(BOX_MIN_W, BOX_MAX_W)
}
pub(super) fn box_inner_width(width: u16) -> usize {
width.saturating_sub(BOX_CHROME_W).max(1) as usize
}
pub(super) fn box_height(body_rows: u16, screen_h: u16) -> u16 {
let floor = BOX_CHROME_H + 1;
body_rows
.saturating_add(BOX_CHROME_H)
.clamp(floor, screen_h.max(floor))
}
pub(super) fn box_area(area: Rect, w: u16, h: u16) -> Rect {
let (w, h) = (w.min(area.width), h.min(area.height));
Rect {
x: area.x + area.width.saturating_sub(w) / 2,
y: area.y + area.height.saturating_sub(h) / 2,
width: w,
height: h,
}
}
pub(super) fn box_block(colour: Color, title: &str) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(colour))
.padding(Padding::new(2, 2, 1, 0))
.title(format!(" {} ", title.trim()))
}
pub(super) fn box_hint(keys: &str) -> Line<'static> {
Line::from(Span::styled(
keys.to_string(),
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::DIM),
))
}
pub(super) fn box_buttons(colour: Color, yes: bool) -> Line<'static> {
let button = |label: &str, picked: bool| {
let style = if picked {
Style::default()
.fg(Color::Black)
.bg(colour)
.add_modifier(Modifier::BOLD)
} else {
Style::default().add_modifier(Modifier::DIM)
};
Span::styled(format!(" {label} "), style)
};
Line::from(vec![
button("Yes (y)", yes),
Span::raw(" "),
button("No (n)", !yes),
])
}