use ratatui::buffer::CellWidth;
use ratatui::text::Span;
use unicode_segmentation::UnicodeSegmentation;
#[expect(
clippy::disallowed_methods,
reason = "ratatui's rule, a grapheme at a time"
)]
pub fn width(s: &str) -> usize {
if s.is_ascii() {
return s.bytes().filter(|b| !b.is_ascii_control()).count();
}
s.graphemes(true)
.filter(|g| !g.contains(char::is_control))
.map(|g| usize::from(g.cell_width()))
.sum()
}
pub fn spans_width<'a, 'b: 'a>(spans: impl IntoIterator<Item = &'a Span<'b>>) -> usize {
spans.into_iter().map(|s| width(&s.content)).sum()
}
pub fn untab((g, w): (&str, usize), col: usize) -> (&str, usize) {
const TAB: &str = " ";
match g {
"\t" => TAB
.get(col % TAB.len()..)
.map_or((TAB, 4), |s| (s, s.len())),
_ => (g, w),
}
}
pub fn is_hidden(c: char) -> bool {
c.is_control()
|| matches!(
c,
'\u{061C}'
| '\u{200B}'
| '\u{200E}'
| '\u{200F}'
| '\u{202A}'..='\u{202E}'
| '\u{2060}'
| '\u{2066}'..='\u{2069}'
| '\u{FEFF}'
)
}
pub fn expand_tabs(s: impl Into<String>) -> String {
let s = s.into();
if !s.contains('\t') {
return s;
}
let mut col = 0;
graphemes(&s)
.map(|g| {
let (g, w) = untab(g, col);
col = if g.contains('\n') { 0 } else { col + w };
g
})
.collect()
}
pub fn graphemes(s: &str) -> impl Iterator<Item = (&str, usize)> {
s.graphemes(true).map(|g| (g, width(g)))
}
pub fn truncate(s: &str, max: usize) -> String {
if width(s) <= max {
return s.to_owned();
}
if max == 0 {
return String::new();
}
let mut out = String::new();
let mut used = 0;
for (g, w) in graphemes(s) {
if used + w > max - 1 {
break;
}
out.push_str(g);
used += w;
}
out.push('…');
out
}
pub fn short_sha(oid: &str) -> &str {
oid.get(..7).unwrap_or(oid)
}
pub fn size(bytes: u64) -> String {
match bytes {
0..1024 => format!("{bytes} B"),
1024..1_048_576 => format!("{:.1} KB", bytes as f64 / 1024.0),
_ => format!("{:.1} MB", bytes as f64 / 1_048_576.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn width_counts_only_what_is_drawn() {
assert_eq!(width("a\u{1b}[31mb"), 6);
assert_eq!(width("a\u{1b}b"), 2);
assert_eq!(width("a\u{202e}b"), 2);
assert_eq!(width("漢字"), 4);
assert!(is_hidden('\u{202e}') && is_hidden('\u{200b}') && !is_hidden('\u{200d}'));
}
#[test]
fn truncates_with_ellipsis() {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("hello world", 6), "hello…");
assert_eq!(truncate("日本語テキスト", 5), "日本…");
assert_eq!(truncate("abc", 0), "");
assert_eq!(truncate("⚠️ Fix warning in build", 5), "⚠️ F…");
assert_eq!(truncate("👨🍳👨🍳👨🍳", 5), "👨🍳👨🍳…");
assert!(width(&truncate("❤️❤️❤️abcdef", 5)) <= 5);
}
}