cc_switch/cli/
display_utils.rs

1//! Display utilities for consistent styling and layout in terminal interfaces.
2//!
3//! This module provides functions for:
4//! - Chinese/English character width calculation
5//! - Text alignment and padding
6//! - Terminal width detection and adaptive layout
7//! - Consistent formatting for configuration display
8
9/// Calculate the display width of a string considering Chinese/English character differences.
10///
11/// Chinese characters typically take 2 terminal columns while ASCII characters take 1.
12/// This function provides accurate width calculation for mixed Chinese/English text.
13///
14/// # Arguments
15/// * `text` - The text to measure
16///
17/// # Returns
18/// The display width in terminal columns
19///
20/// # Examples
21/// ```
22/// use cc_switch::cli::display_utils::text_display_width;
23///
24/// assert_eq!(text_display_width("Hello"), 5);           // 5 ASCII chars = 5 columns
25/// assert_eq!(text_display_width("你好"), 4);              // 2 Chinese chars = 4 columns  
26/// assert_eq!(text_display_width("Hello你好"), 9);         // 5 ASCII + 2 Chinese = 9 columns
27/// ```
28pub fn text_display_width(text: &str) -> usize {
29    text.chars()
30        .map(|c| {
31            // Check if character is likely a wide character (Chinese, Japanese, Korean, etc.)
32            // Using Unicode properties to detect wide characters
33            match c as u32 {
34                // ASCII range: 1 column
35                0x00..=0x7F => 1,
36                // Latin extended: 1 column
37                0x80..=0x2FF => 1,
38                // Arrows and symbols that render wide in many terminals: 2 columns
39                0x2190..=0x21FF => 2,
40                // CJK symbols and punctuation: 2 columns
41                0x3000..=0x303F => 2,
42                // Hiragana: 2 columns
43                0x3040..=0x309F => 2,
44                // Katakana: 2 columns
45                0x30A0..=0x30FF => 2,
46                // CJK Unified Ideographs: 2 columns
47                0x4E00..=0x9FFF => 2,
48                // Hangul Syllables: 2 columns
49                0xAC00..=0xD7AF => 2,
50                // CJK Unified Ideographs Extension A: 2 columns
51                0x3400..=0x4DBF => 2,
52                // Full-width ASCII and symbols: 2 columns
53                0xFF01..=0xFF60 => 2,
54                // Other characters: assume 1 column (conservative estimate)
55                _ => 1,
56            }
57        })
58        .sum()
59}
60
61/// Pad text to a specific display width, handling Chinese/English character differences.
62///
63/// # Arguments
64/// * `text` - The text to pad
65/// * `width` - Target display width in terminal columns
66/// * `alignment` - Text alignment (Left, Right, Center)
67/// * `pad_char` - Character to use for padding (default: space)
68///
69/// # Returns
70/// Padded text string
71///
72/// # Examples
73/// ```
74/// use cc_switch::cli::display_utils::{pad_text_to_width, TextAlignment};
75///
76/// assert_eq!(pad_text_to_width("Hello", 10, TextAlignment::Left, ' '), "Hello     ");
77/// assert_eq!(pad_text_to_width("你好", 10, TextAlignment::Center, ' '), "   你好   ");
78/// ```
79pub fn pad_text_to_width(
80    text: &str,
81    width: usize,
82    alignment: TextAlignment,
83    pad_char: char,
84) -> String {
85    let text_width = text_display_width(text);
86
87    if text_width >= width {
88        return text.to_string();
89    }
90
91    let padding_needed = width - text_width;
92
93    match alignment {
94        TextAlignment::Left => {
95            format!("{}{}", text, pad_char.to_string().repeat(padding_needed))
96        }
97        TextAlignment::Right => {
98            format!("{}{}", pad_char.to_string().repeat(padding_needed), text)
99        }
100        TextAlignment::Center => {
101            let left_pad = padding_needed / 2;
102            let right_pad = padding_needed - left_pad;
103            format!(
104                "{}{}{}",
105                pad_char.to_string().repeat(left_pad),
106                text,
107                pad_char.to_string().repeat(right_pad)
108            )
109        }
110    }
111}
112
113/// Text alignment options
114#[derive(Debug, Clone, Copy, PartialEq)]
115pub enum TextAlignment {
116    Left,
117    #[allow(dead_code)]
118    Right,
119    #[allow(dead_code)]
120    Center,
121}
122
123/// Detect current terminal width, with fallback to default
124///
125/// # Returns
126/// Terminal width in columns, defaults to 80 if detection fails
127pub fn get_terminal_width() -> usize {
128    if let Ok((width, _)) = crossterm::terminal::size() {
129        width as usize
130    } else {
131        80 // Fallback width
132    }
133}
134
135/// Format a configuration token for safe display
136///
137/// This is a centralized version of the token formatting logic,
138/// ensuring consistent display across the application.
139///
140/// # Arguments
141/// * `token` - The API token to format
142///
143/// # Returns
144/// Safely formatted token string
145pub fn format_token_for_display(token: &str) -> String {
146    const PREFIX_LEN: usize = 12;
147    const SUFFIX_LEN: usize = 8;
148
149    if token.len() <= PREFIX_LEN + SUFFIX_LEN {
150        // If token is short enough, show first half and mask the rest with *
151        if token.len() <= 6 {
152            // Very short token, just show first few chars and mask the rest
153            let visible_chars = token.len().div_ceil(2);
154            format!("{}***", &token[..visible_chars])
155        } else {
156            // Medium length token, show some chars from start and mask the end
157            let visible_chars = token.len() / 2;
158            format!("{}***", &token[..visible_chars])
159        }
160    } else {
161        // Long token, use the original format: first 12 + "..." + last 8
162        format!(
163            "{}...{}",
164            &token[..PREFIX_LEN],
165            &token[token.len() - SUFFIX_LEN..]
166        )
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_text_display_width() {
176        // ASCII characters
177        assert_eq!(text_display_width("Hello"), 5);
178        assert_eq!(text_display_width("World"), 5);
179        assert_eq!(text_display_width("123"), 3);
180
181        // Chinese characters (should be 2 columns each)
182        assert_eq!(text_display_width("你好"), 4);
183        assert_eq!(text_display_width("测试"), 4);
184
185        // Mixed Chinese and English
186        assert_eq!(text_display_width("Hello你好"), 9); // 5 + 4
187        assert_eq!(text_display_width("测试123"), 7); // 4 + 3
188
189        // Empty string
190        assert_eq!(text_display_width(""), 0);
191
192        // Special characters and punctuation
193        assert_eq!(text_display_width("!@#$%"), 5);
194
195        // Full-width punctuation (should be 2 columns each)
196        assert_eq!(text_display_width("!()"), 6);
197    }
198
199    #[test]
200    fn test_pad_text_to_width() {
201        // Left alignment
202        assert_eq!(
203            pad_text_to_width("Hello", 10, TextAlignment::Left, ' '),
204            "Hello     "
205        );
206
207        // Right alignment
208        assert_eq!(
209            pad_text_to_width("Hello", 10, TextAlignment::Right, ' '),
210            "     Hello"
211        );
212
213        // Center alignment
214        assert_eq!(
215            pad_text_to_width("Hi", 6, TextAlignment::Center, ' '),
216            "  Hi  "
217        );
218        assert_eq!(
219            pad_text_to_width("Hi", 7, TextAlignment::Center, ' '),
220            "  Hi   "
221        );
222
223        // Text wider than target width (should return original)
224        assert_eq!(
225            pad_text_to_width("Very long text", 5, TextAlignment::Left, ' '),
226            "Very long text"
227        );
228
229        // Chinese characters
230        assert_eq!(
231            pad_text_to_width("你好", 8, TextAlignment::Left, ' '),
232            "你好    "
233        );
234        assert_eq!(
235            pad_text_to_width("你好", 8, TextAlignment::Center, ' '),
236            "  你好  "
237        );
238
239        // Different padding characters
240        assert_eq!(
241            pad_text_to_width("test", 8, TextAlignment::Center, '-'),
242            "--test--"
243        );
244    }
245
246    #[test]
247    fn test_format_token_for_display() {
248        // Very short token (3 chars: (3+1)/2 = 2 chars visible)
249        assert_eq!(format_token_for_display("abc"), "ab***");
250        // 6 chars: 6/2 = 3 chars visible
251        assert_eq!(format_token_for_display("abcdef"), "abc***");
252
253        // Medium length token
254        assert_eq!(format_token_for_display("abcdefgh"), "abcd***");
255
256        // Long token (standard format)
257        let long_token = "sk-ant-api03_abcdefghijklmnopqrstuvwxyz1234567890abcdefgh";
258        let formatted = format_token_for_display(long_token);
259        assert!(formatted.starts_with("sk-ant-api03"));
260        assert!(formatted.contains("..."));
261        assert!(formatted.ends_with("defgh"));
262        assert_eq!(formatted.len(), 12 + 3 + 8); // prefix + "..." + suffix
263    }
264}