use std::mem;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn truncate(text: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
if text.width() <= width {
return text.to_string();
}
let budget = width.saturating_sub(1);
let mut result = String::new();
let mut used = 0;
for ch in text.chars() {
let char_width = ch.width().unwrap_or(0);
if used + char_width > budget {
break;
}
result.push(ch);
used += char_width;
}
result.push('\u{2026}');
result
}
pub fn window(text: &str, start_col: usize, width: usize) -> String {
if width == 0 {
return String::new();
}
let mut result = String::new();
let mut column = 0; let mut used = 0; for ch in text.chars() {
let char_width = ch.width().unwrap_or(0);
let char_start = column;
column += char_width;
if char_start < start_col {
continue;
}
if used + char_width > width {
break;
}
result.push(ch);
used += char_width;
}
result
}
pub fn pad_end(text: &str, width: usize) -> String {
let current = text.width();
if current >= width {
return text.to_string();
}
let mut result = text.to_string();
result.push_str(&" ".repeat(width - current));
result
}
pub fn wrap(text: &str, width: usize) -> Vec<String> {
let width = width.max(1);
let mut lines = Vec::new();
for segment in text.split('\n') {
wrap_segment(segment, width, &mut lines);
}
lines
}
fn wrap_segment(segment: &str, width: usize, out: &mut Vec<String>) {
let mut line = String::new();
let mut line_width = 0;
for word in segment.split_whitespace() {
let word_width = word.width();
if word_width > width {
if !line.is_empty() {
out.push(mem::take(&mut line));
}
let chunks = split_to_width(word, width);
let last = chunks.len() - 1;
for (index, chunk) in chunks.into_iter().enumerate() {
if index < last {
out.push(chunk);
} else {
line_width = chunk.width();
line = chunk;
}
}
continue;
}
let separator = usize::from(!line.is_empty());
if !line.is_empty() && line_width + separator + word_width > width {
out.push(mem::take(&mut line));
line_width = 0;
}
if !line.is_empty() {
line.push(' ');
line_width += 1;
}
line.push_str(word);
line_width += word_width;
}
out.push(line);
}
fn split_to_width(text: &str, width: usize) -> Vec<String> {
let mut pieces = Vec::new();
let mut current = String::new();
let mut used = 0;
for ch in text.chars() {
let char_width = ch.width().unwrap_or(0);
if used + char_width > width && !current.is_empty() {
pieces.push(mem::take(&mut current));
used = 0;
}
current.push(ch);
used += char_width;
}
if !current.is_empty() {
pieces.push(current);
}
pieces
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_leaves_short_text_untouched() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn truncate_appends_ellipsis_when_clipped() {
assert_eq!(truncate("hello world", 5), "hell\u{2026}");
}
#[test]
fn truncate_to_zero_is_empty() {
assert_eq!(truncate("hello", 0), "");
}
#[test]
fn window_from_start_takes_leading_columns() {
assert_eq!(window("hello world", 0, 5), "hello");
}
#[test]
fn window_offset_skips_leading_columns() {
assert_eq!(window("hello world", 6, 5), "world");
}
#[test]
fn window_drops_wide_glyph_straddling_the_start() {
assert_eq!(window("\u{3042}b", 1, 2), "b");
}
#[test]
fn window_never_emits_a_partial_wide_cell() {
assert_eq!(window("a\u{3042}b", 0, 2), "a");
}
#[test]
fn window_to_zero_is_empty() {
assert_eq!(window("hello", 3, 0), "");
}
#[test]
fn pad_end_fills_to_width() {
assert_eq!(pad_end("ab", 5), "ab ");
}
#[test]
fn pad_end_leaves_full_text_untouched() {
assert_eq!(pad_end("abcde", 5), "abcde");
assert_eq!(pad_end("abcdef", 5), "abcdef");
}
#[test]
fn wrap_breaks_at_word_boundaries() {
assert_eq!(
wrap("the quick brown fox", 9),
vec!["the quick", "brown fox"],
);
}
#[test]
fn wrap_hard_splits_an_overlong_word() {
assert_eq!(
wrap("supercalifragilistic", 5),
vec!["super", "calif", "ragil", "istic"],
);
}
#[test]
fn wrap_keeps_wrapping_after_a_split_word() {
assert_eq!(wrap("abcdef gh", 5), vec!["abcde", "f gh"]);
}
#[test]
fn wrap_preserves_blank_lines_from_newlines() {
assert_eq!(wrap("a\n\nb", 10), vec!["a", "", "b"]);
}
#[test]
fn wrap_measures_wide_glyphs_as_two_columns() {
assert_eq!(
wrap("\u{4e16}\u{754c} x", 4),
vec!["\u{4e16}\u{754c}", "x"]
);
}
#[test]
fn wrap_zero_width_behaves_as_one() {
assert_eq!(wrap("ab", 0), vec!["a", "b"]);
}
}