pub mod chips;
pub mod dot;
pub mod gauge;
pub mod inspector;
pub mod kv;
pub mod page;
pub mod panel;
use ratatui::style::Style;
use ratatui::text::Span;
use ratatui::widgets::TableState;
use crate::tui::theme::Theme;
pub fn or_dash<T: ToString>(value: Option<T>) -> String {
value.map_or_else(|| "—".to_string(), |v| v.to_string())
}
pub fn row_style(theme: &Theme, selected: bool) -> Style {
let style = Style::default().fg(theme.text);
if selected {
style.bg(theme.surface0)
} else {
style
}
}
pub fn scroll_to(selected: Option<usize>) -> TableState {
TableState::default().with_selected(selected)
}
pub fn wrap(text: &str, width: u16) -> Vec<String> {
let width = usize::from(width);
let mut lines = Vec::new();
let mut line = String::new();
for word in text.split(' ').filter(|w| !w.is_empty()) {
if line.is_empty() {
line.push_str(word);
} else if Span::raw(line.as_str()).width() + 1 + Span::raw(word).width() <= width {
line.push(' ');
line.push_str(word);
} else {
lines.push(std::mem::replace(&mut line, word.to_string()));
}
}
if !line.is_empty() {
lines.push(line);
}
lines
}
pub fn wrap_hard(text: &str, width: u16) -> Vec<String> {
let max = usize::from(width.max(1));
let col = |s: &str| Span::raw(s).width();
fn push(lines: &mut Vec<String>, line: &mut String) {
let kept = line.trim_end();
if !kept.is_empty() {
lines.push(kept.to_string());
}
line.clear();
}
let mut lines = Vec::new();
let mut line = String::new();
for c in text.chars() {
let mut buf = [0u8; 4];
let w = col(c.encode_utf8(&mut buf));
if col(&line) + w > max && !line.is_empty() {
if c == ' ' {
push(&mut lines, &mut line);
continue;
}
match line.rfind(' ') {
Some(at) => {
let rest = line.split_off(at + 1);
push(&mut lines, &mut line);
line = rest;
}
None => push(&mut lines, &mut line),
}
if col(&line) + w > max && !line.is_empty() {
push(&mut lines, &mut line);
}
}
if !(line.is_empty() && c == ' ' && !lines.is_empty()) {
line.push(c);
}
}
push(&mut lines, &mut line);
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_hard_splits_words_wider_than_the_line() {
assert_eq!(
wrap_hard("v=DKIM1 abcdefghij", 4),
["v=DK", "IM1", "abcd", "efgh", "ij"]
);
assert_eq!(wrap_hard("fits", 10), ["fits"]);
assert!(wrap_hard("", 5).is_empty());
assert_eq!(wrap_hard("ab", 0), ["a", "b"]);
}
#[test]
fn wrap_hard_breaks_again_after_carrying_a_word() {
let got = wrap_hard(" ab漢", 3);
assert!(
got.iter().all(|l| Span::raw(l.as_str()).width() <= 3),
"{got:?}"
);
assert_eq!(got.concat(), "ab漢");
}
#[test]
fn wrap_hard_keeps_the_spacing_inside_a_line() {
assert_eq!(wrap_hard("a b", 10), ["a b"]);
assert_eq!(wrap_hard("v=spf1 -all", 7), ["v=spf1", "-all"]);
assert!(wrap_hard(" ", 5).is_empty());
}
#[test]
fn wrap_fills_lines_greedily_at_spaces() {
assert_eq!(
wrap("a random sibling resolves too", 12),
["a random", "sibling", "resolves too"]
);
assert_eq!(wrap("fits on one line", 40), ["fits on one line"]);
assert!(wrap("", 10).is_empty());
assert!(wrap(" ", 10).is_empty());
}
#[test]
fn wrap_measures_display_width_and_keeps_long_words_whole() {
assert_eq!(wrap("resolves — this", 15), ["resolves — this"]);
assert_eq!(
wrap("see seer-probe-3f9a1c2e7b.seer.test now", 10),
["see", "seer-probe-3f9a1c2e7b.seer.test", "now"]
);
}
}