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}