sql_cli/utils/
string_utils.rs

1use unicode_width::UnicodeWidthStr;
2
3/// Strip ANSI escape codes from a string and return the display width
4/// This handles ANSI SGR (Select Graphic Rendition) codes like colors and styles
5/// and correctly calculates Unicode character widths (e.g., emoji take 2 columns)
6///
7/// # Examples
8///
9/// ```
10/// use sql_cli::utils::string_utils::display_width;
11///
12/// // Simple ASCII text
13/// assert_eq!(display_width("hello"), 5);
14///
15/// // ANSI colored text (escape codes don't count toward width)
16/// assert_eq!(display_width("\x1b[31mred\x1b[0m"), 3);
17///
18/// // Unicode emoji (takes 2 columns)
19/// assert_eq!(display_width("→"), 1);  // Right arrow
20/// assert_eq!(display_width("⚡"), 2);  // Lightning bolt emoji
21/// ```
22pub fn display_width(s: &str) -> usize {
23    let mut result = String::new();
24    let mut chars = s.chars().peekable();
25
26    while let Some(ch) = chars.next() {
27        if ch == '\x1b' {
28            // Check for ANSI escape sequence
29            if chars.peek() == Some(&'[') {
30                chars.next(); // consume '['
31                              // Skip until we find a letter (the command character)
32                while let Some(&next_ch) = chars.peek() {
33                    chars.next();
34                    if next_ch.is_ascii_alphabetic() {
35                        break;
36                    }
37                }
38            } else {
39                result.push(ch);
40            }
41        } else {
42            result.push(ch);
43        }
44    }
45
46    // Use unicode-width to get the actual display width
47    // This correctly handles:
48    // - ASCII characters (width 1)
49    // - Emoji and other wide characters (width 2)
50    // - Zero-width characters (width 0)
51    // - Combining characters
52    result.width()
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_display_width_ascii() {
61        assert_eq!(display_width("hello"), 5);
62        assert_eq!(display_width("test"), 4);
63        assert_eq!(display_width(""), 0);
64    }
65
66    #[test]
67    fn test_display_width_ansi_codes() {
68        // Red colored text
69        assert_eq!(display_width("\x1b[31mred\x1b[0m"), 3);
70        // Bold text
71        assert_eq!(display_width("\x1b[1mbold\x1b[0m"), 4);
72        // RGB colored text
73        assert_eq!(display_width("\x1b[38;2;255;0;0mRGB red\x1b[0m"), 7);
74    }
75
76    #[test]
77    fn test_display_width_unicode() {
78        // Single-width Unicode
79        assert_eq!(display_width("→"), 1);
80        assert_eq!(display_width("café"), 4);
81
82        // Double-width characters (emoji)
83        assert_eq!(display_width("⚡"), 2);
84        assert_eq!(display_width("😀"), 2);
85    }
86
87    #[test]
88    fn test_display_width_mixed() {
89        // ANSI + Unicode
90        assert_eq!(
91            display_width("\x1b[32m→ CONSEC\x1b[0m"),
92            8 // 1 (arrow) + 1 (space) + 6 (CONSEC)
93        );
94        assert_eq!(
95            display_width("\x1b[31m⚡ REPEAT\x1b[0m"),
96            9 // 2 (bolt) + 1 (space) + 6 (REPEAT)
97        );
98    }
99}