#[inline]
pub fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
s.char_indices()
.nth(char_idx)
.map(|(i, _)| i)
.unwrap_or(s.len())
}
#[inline]
pub fn char_to_byte_index_with_char(s: &str, char_idx: usize) -> (usize, Option<char>) {
s.char_indices()
.nth(char_idx)
.map(|(i, c)| (i, Some(c)))
.unwrap_or((s.len(), None))
}
#[inline]
pub fn byte_to_char_index(s: &str, byte_idx: usize) -> usize {
let byte_idx = byte_idx.min(s.len());
let safe_idx = if s.is_char_boundary(byte_idx) {
byte_idx
} else {
let mut safe = 0;
for (i, _) in s.char_indices() {
if i <= byte_idx && s.is_char_boundary(i) {
safe = i;
} else if i > byte_idx {
break;
}
}
safe
};
s[..safe_idx].chars().count()
}
#[inline]
pub fn char_count(s: &str) -> usize {
s.chars().count()
}
pub fn char_slice(s: &str, start: usize, end: usize) -> &str {
if start >= end || start >= char_count(s) {
return "";
}
let start_byte = char_to_byte_index(s, start);
let end_byte = char_to_byte_index(s, end).min(s.len());
if !s.is_char_boundary(start_byte) || !s.is_char_boundary(end_byte) {
return "";
}
&s[start_byte..end_byte]
}
pub fn insert_at_char(s: &mut String, char_idx: usize, insert: &str) -> usize {
let byte_idx = char_to_byte_index(s, char_idx);
s.insert_str(byte_idx, insert);
char_idx + insert.chars().count()
}
pub fn remove_char_at(s: &mut String, char_idx: usize) -> Option<char> {
let (byte_idx, maybe_char) = char_to_byte_index_with_char(s, char_idx);
if let Some(ch) = maybe_char {
s.drain(byte_idx..byte_idx + ch.len_utf8());
Some(ch)
} else {
None
}
}
pub fn remove_char_range(s: &mut String, start: usize, end: usize) {
if start >= end {
return;
}
let start_byte = char_to_byte_index(s, start);
let end_byte = char_to_byte_index(s, end);
s.drain(start_byte..end_byte);
}
pub fn truncate(text: &str, max_width: usize) -> String {
crate::utils::unicode::truncate_with_ellipsis(text, max_width)
}
pub fn truncate_start(text: &str, max_width: usize) -> String {
use crate::utils::unicode::display_width as dw;
let width = dw(text);
if width <= max_width {
return text.to_string();
}
if max_width <= 1 {
return String::from("…");
}
let keep_width = max_width.saturating_sub(1); let mut kept = Vec::new();
let mut acc_width = 0;
for c in text.chars().rev() {
let cw = crate::utils::unicode::char_width(c);
if acc_width + cw > keep_width {
break;
}
acc_width += cw;
kept.push(c);
}
kept.reverse();
let mut result = String::with_capacity(acc_width * 3 + 3);
result.push('…');
for c in kept {
result.push(c);
}
result
}
pub fn center(text: &str, width: usize) -> String {
crate::utils::unicode::center_to_width(text, width)
}
pub fn pad_left(text: &str, width: usize) -> String {
crate::utils::unicode::right_align_to_width(text, width)
}
pub fn pad_right(text: &str, width: usize) -> String {
crate::utils::unicode::pad_to_width(text, width)
}
pub fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
if max_width == 0 || text.is_empty() {
return vec![];
}
let mut lines = Vec::new();
for paragraph in text.lines() {
if paragraph.is_empty() {
lines.push(String::new());
continue;
}
lines.extend(crate::utils::unicode::wrap_to_width(paragraph, max_width));
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
pub fn split_fixed_width(text: &str, width: usize) -> Vec<String> {
if width == 0 {
return vec![];
}
let mut chunks = Vec::new();
let mut remaining = text;
while !remaining.is_empty() {
let (chunk, rest) = crate::utils::unicode::split_at_width(remaining, width);
if chunk.is_empty() {
break;
}
chunks.push(chunk.to_string());
remaining = rest;
}
if chunks.is_empty() {
chunks.push(String::new());
}
chunks
}
pub fn display_width(text: &str) -> usize {
crate::utils::unicode::display_width(text)
}
pub fn repeat_char(ch: char, count: usize) -> String {
std::iter::repeat_n(ch, count).collect()
}
pub fn progress_bar(value: f64, width: usize) -> String {
let value = value.clamp(0.0, 1.0);
let filled = (value * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
let capacity = width * 3;
let mut result = String::with_capacity(capacity);
for _ in 0..filled {
result.push('█');
}
for _ in 0..empty {
result.push('░');
}
result
}
pub fn progress_bar_precise(value: f64, width: usize) -> String {
let value = value.clamp(0.0, 1.0);
let total_eighths = (value * width as f64 * 8.0).round() as usize;
let full_blocks = total_eighths / 8;
let remainder = total_eighths % 8;
let partial = match remainder {
0 => "",
1 => "▏",
2 => "▎",
3 => "▍",
4 => "▌",
5 => "▋",
6 => "▊",
7 => "▉",
_ => "█",
};
let empty = width
.saturating_sub(full_blocks)
.saturating_sub(if remainder > 0 { 1 } else { 0 });
let capacity = (full_blocks + partial.len() + empty) * 3;
let mut result = String::with_capacity(capacity);
for _ in 0..full_blocks {
result.push('█');
}
result.push_str(partial);
for _ in 0..empty {
result.push(' ');
}
result
}