//! Terminal-cell-safe plain-text fitting shared by HUD and terminal chrome. use unicode_width::UnicodeWidthChar; /// Sanitize controls and fit plain text to exactly `width` terminal cells. /// /// C0 controls and DEL become spaces. Characters that would cross the cell /// boundary are omitted without splitting UTF-8. Zero-width characters are /// retained only after a visible base character, preventing detached leading /// combining marks. This is scalar-width fitting, not full grapheme shaping. #[must_use] pub fn fit_terminal_cells(value: &str, width: usize) -> String { let mut output = String::new(); let mut cells = 0usize; let mut has_base = false; for mut character in value.chars() { if character <= '\u{1f}' || character == '\u{7f}' { character = ' '; } let character_width = UnicodeWidthChar::width(character).unwrap_or(0); if character_width == 0 { if has_base { output.push(character); } continue; } if cells.saturating_add(character_width) > width { break; } output.push(character); cells += character_width; has_base = true; } while cells < width { output.push(' '); cells += 1; } output } #[cfg(test)] mod tests { use super::*; use unicode_width::UnicodeWidthStr; #[test] fn fits_ascii_controls_wide_and_combining_scalars() { assert_eq!(fit_terminal_cells("lamp\u{1b}[31m", 8), "lamp [31"); assert_eq!(fit_terminal_cells("深淵灯", 2), "深"); assert_eq!(fit_terminal_cells("深淵灯", 5), "深淵 "); assert_eq!(fit_terminal_cells("e\u{301}lan", 3), "e\u{301}la"); assert_eq!(fit_terminal_cells("\u{301}a", 1), "a"); assert_eq!(fit_terminal_cells("ignored", 0), ""); for value in ["plain", "深淵灯", "e\u{301}lan", "\u{301}a"] { for width in 0..8 { let fitted = fit_terminal_cells(value, width); assert_eq!(UnicodeWidthStr::width(fitted.as_str()), width); } } } }