use unicode_width::UnicodeWidthStr;
mod wrap;
#[cfg(test)]
mod tests;
pub(in crate::pager) use wrap::{WrappedLine, wrap_spans_with_ranges};
pub fn display_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
pub fn truncate_to_width(s: &str, max_width: usize) -> String {
if display_width(s) <= max_width {
return s.to_string();
}
if max_width == 0 {
return String::new();
}
if max_width == 1 {
return "…".to_string();
}
let target = max_width - 1; let mut col = 0;
let mut end = 0;
for (i, ch) in s.char_indices() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
if col + w > target {
break;
}
col += w;
end = i + ch.len_utf8();
}
let mut out = s[..end].to_string();
out.push('…');
out
}
pub fn exceeds_width(s: &str, max_width: usize) -> bool {
display_width(s) > max_width
}
pub fn scroll_display(s: &str, max_width: usize, scroll_pos: usize) -> (String, usize) {
if max_width == 0 {
return (String::new(), 0);
}
let total = display_width(s);
if total <= max_width {
return (s.to_string(), 0);
}
if max_width == 1 {
return ("…".to_string(), total);
}
let max_scroll = total.saturating_sub(max_width.saturating_sub(1));
let eff = scroll_pos.min(max_scroll);
if eff == 0 {
let (text, actual_vis) = take_columns(s, max_width - 1);
let right_pad = (max_width - 1).saturating_sub(actual_vis);
(format!("{}{}…", text, " ".repeat(right_pad)), max_scroll)
} else if eff >= max_scroll {
let skip_cols = total.saturating_sub(max_width - 1);
let (remainder, actual_skipped) = skip_columns(s, skip_cols);
let overshoot = actual_skipped.saturating_sub(skip_cols);
(
format!("…{}{}", " ".repeat(overshoot), remainder),
max_scroll,
)
} else {
let inner_cols = max_width.saturating_sub(2);
let (after_skip, actual_skipped) = skip_columns(s, eff);
let overshoot = actual_skipped.saturating_sub(eff).min(inner_cols);
let content_cols = inner_cols.saturating_sub(overshoot);
let (visible, actual_vis) = take_columns(&after_skip, content_cols);
let right_pad = inner_cols.saturating_sub(overshoot + actual_vis);
(
format!(
"…{}{}{}…",
" ".repeat(overshoot),
visible,
" ".repeat(right_pad)
),
max_scroll,
)
}
}
pub fn pad_to_width(s: &str, width: usize) -> String {
let w = display_width(s);
if w >= width {
let (text, actual) = take_columns(s, width);
if actual < width {
format!("{}{}", text, " ".repeat(width - actual))
} else {
text
}
} else {
format!("{}{}", s, " ".repeat(width - w))
}
}
fn take_columns(s: &str, cols: usize) -> (String, usize) {
let mut col = 0;
let mut end = 0;
for (i, ch) in s.char_indices() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
if col + w > cols {
break;
}
col += w;
end = i + ch.len_utf8();
}
(s[..end].to_string(), col)
}
fn skip_columns(s: &str, cols: usize) -> (String, usize) {
let mut col = 0;
for (i, ch) in s.char_indices() {
if col >= cols {
return (s[i..].to_string(), col);
}
col += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
}
(String::new(), col)
}