pub fn char_width(c: char) -> usize {
let cp = c as u32;
if is_zero_width(c) {
return 0;
}
if cp < 0x20 || (0x7F..0xA0).contains(&cp) {
return 0;
}
if cp < 0x7F {
return 1;
}
if is_wide_char(c) {
return 2;
}
1
}
fn is_zero_width(c: char) -> bool {
let cp = c as u32;
if (0x0300..=0x036F).contains(&cp) {
return true;
}
if (0x1AB0..=0x1AFF).contains(&cp) {
return true;
}
if (0x1DC0..=0x1DFF).contains(&cp) {
return true;
}
if (0x20D0..=0x20FF).contains(&cp) {
return true;
}
if (0xFE20..=0xFE2F).contains(&cp) {
return true;
}
if cp == 0x200B || cp == 0x200C || cp == 0x200D {
return true;
}
if (0xFE00..=0xFE0F).contains(&cp) || (0xE0100..=0xE01EF).contains(&cp) {
return true;
}
false
}
fn is_wide_char(c: char) -> bool {
let cp = c as u32;
if (0x4E00..=0x9FFF).contains(&cp) {
return true;
}
if (0x3400..=0x4DBF).contains(&cp) {
return true;
}
if (0x20000..=0x2A6DF).contains(&cp) {
return true;
}
if (0x2A700..=0x2B73F).contains(&cp) {
return true;
}
if (0x2B740..=0x2B81F).contains(&cp) {
return true;
}
if (0xF900..=0xFAFF).contains(&cp) {
return true;
}
if (0xAC00..=0xD7AF).contains(&cp) {
return true;
}
if (0x1100..=0x11FF).contains(&cp) {
return true;
}
if (0x3040..=0x309F).contains(&cp) {
return true;
}
if (0x30A0..=0x30FF).contains(&cp) {
return true;
}
if (0xFF00..=0xFFEF).contains(&cp) {
if (0xFF61..=0xFFDC).contains(&cp) || (0xFFE8..=0xFFEE).contains(&cp) {
return false;
}
return true;
}
if (0x1F300..=0x1F9FF).contains(&cp) {
return true;
}
if (0x1FA00..=0x1FAFF).contains(&cp) {
return true;
}
if (0x2600..=0x26FF).contains(&cp) {
return true;
}
if (0x2700..=0x27BF).contains(&cp) {
return true;
}
false
}
pub fn display_width(s: &str) -> usize {
s.chars().map(char_width).sum()
}
pub fn truncate_to_width(s: &str, max_width: usize) -> &str {
let mut width = 0;
let mut end_idx = 0;
for (i, c) in s.char_indices() {
let cw = char_width(c);
if width + cw > max_width {
break;
}
width += cw;
end_idx = i + c.len_utf8();
}
if end_idx == 0 {
return "";
}
if end_idx >= s.len() {
return s;
}
if !s.is_char_boundary(end_idx) {
for (i, _) in s.char_indices() {
if i >= end_idx {
break;
}
end_idx = i;
}
}
&s[..end_idx]
}
pub fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
truncate_with_suffix(s, max_width, "β¦")
}
pub fn truncate_with_suffix(s: &str, max_width: usize, suffix: &str) -> String {
let width = display_width(s);
if width <= max_width {
return s.to_string();
}
let suffix_width = display_width(suffix);
if max_width <= suffix_width {
return truncate_to_width(suffix, max_width).to_string();
}
let content_width = max_width - suffix_width;
let truncated = truncate_to_width(s, content_width);
format!("{}{}", truncated, suffix)
}
pub fn pad_to_width(s: &str, target_width: usize) -> String {
let width = display_width(s);
if width >= target_width {
s.to_string()
} else {
format!("{}{}", s, " ".repeat(target_width - width))
}
}
pub fn center_to_width(s: &str, target_width: usize) -> String {
let width = display_width(s);
if width >= target_width {
return s.to_string();
}
let padding = target_width - width;
let left = padding / 2;
let right = padding - left;
format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
}
pub fn right_align_to_width(s: &str, target_width: usize) -> String {
let width = display_width(s);
if width >= target_width {
return s.to_string();
}
format!("{}{}", " ".repeat(target_width - width), s)
}
pub fn split_at_width(s: &str, width: usize) -> (&str, &str) {
let left = truncate_to_width(s, width);
let right = &s[left.len()..];
(left, right)
}
pub fn wrap_to_width(s: &str, max_width: usize) -> Vec<String> {
if max_width == 0 {
return vec![];
}
let mut lines = Vec::new();
let mut current_line = String::new();
let mut current_width = 0;
for word in s.split_whitespace() {
let word_width = display_width(word);
if current_width == 0 {
if word_width <= max_width {
current_line = word.to_string();
current_width = word_width;
} else {
let mut remaining = word;
while !remaining.is_empty() {
let (chunk, rest) = split_at_width(remaining, max_width);
if chunk.is_empty() {
break;
}
lines.push(chunk.to_string());
if rest.is_empty() {
break;
}
remaining = rest;
}
}
} else if current_width + 1 + word_width <= max_width {
current_line.push(' ');
current_line.push_str(word);
current_width += 1 + word_width;
} else {
lines.push(current_line);
if word_width <= max_width {
current_line = word.to_string();
current_width = word_width;
} else {
current_line = String::new();
current_width = 0;
let mut remaining = word;
while !remaining.is_empty() {
let (chunk, rest) = split_at_width(remaining, max_width);
if chunk.is_empty() {
break;
}
if rest.is_empty() {
current_line = chunk.to_string();
current_width = display_width(chunk);
} else {
lines.push(chunk.to_string());
}
remaining = rest;
}
}
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}