claude_code_status_line/
utils.rs1use crate::config::Config;
2use unicode_width::UnicodeWidthChar;
3
4pub const RESET: &str = "\x1b[0m";
6
7pub fn visible_len(s: &str) -> usize {
9 let mut width = 0;
10 let mut chars = s.chars().peekable();
11 while let Some(c) = chars.next() {
12 if c == '\x1b' {
13 if let Some(&next) = chars.peek() {
14 if next == '[' {
15 chars.next(); for c2 in chars.by_ref() {
18 if ('@'..='~').contains(&c2) {
19 break;
20 }
21 }
22 }
23 }
24 } else {
25 width += c.width().unwrap_or(0);
26 }
27 }
28 width
29}
30
31pub fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
33 if visible_len(s) <= max_width {
34 return s.to_string();
35 }
36
37 const ELLIPSIS: &str = "…";
39 const ELLIPSIS_WIDTH: usize = 1;
40
41 if max_width <= ELLIPSIS_WIDTH {
42 return ELLIPSIS.chars().take(max_width).collect();
43 }
44
45 let target_width = max_width - ELLIPSIS_WIDTH;
46 let mut current_width = 0;
47 let mut result = String::with_capacity(s.len());
48 let mut chars = s.chars().peekable();
49
50 while let Some(c) = chars.next() {
51 if c == '\x1b' {
52 result.push(c);
53 if let Some(&next) = chars.peek() {
54 if next == '[' {
55 result.push(chars.next().unwrap()); for c2 in chars.by_ref() {
58 result.push(c2);
59 if ('@'..='~').contains(&c2) {
60 break;
61 }
62 }
63 }
64 }
65 } else {
66 let char_width = c.width().unwrap_or(0);
68 if current_width + char_width > target_width {
69 break;
70 }
71 result.push(c);
72 current_width += char_width;
73 }
74 }
75
76 result.push_str(ELLIPSIS);
77 result.push_str(RESET);
78 result
79}
80
81pub fn get_terminal_width(config: &Config) -> usize {
83 if let Ok(w_str) = std::env::var("CLAUDE_TERMINAL_WIDTH") {
84 if let Ok(w) = w_str.parse::<usize>() {
85 return w;
86 }
87 }
88
89 if let Ok(w_str) = std::env::var("COLUMNS") {
90 if let Ok(w) = w_str.parse::<usize>() {
91 return w;
92 }
93 }
94
95 if let Some((w, _)) = terminal_size::terminal_size() {
96 return w.0 as usize;
97 }
98
99 #[cfg(unix)]
100 {
101 if let Ok(f) = std::fs::File::open("/dev/tty") {
102 if let Some((w, _)) = terminal_size::terminal_size_of(&f) {
103 return w.0 as usize;
104 }
105 }
106 }
107
108 config.display.default_terminal_width
109}
110
111pub fn get_username() -> Option<String> {
113 std::env::var("USER")
114 .or_else(|_| std::env::var("USERNAME"))
115 .ok()
116}
117
118pub fn get_thinking_mode_enabled() -> bool {
120 use serde::Deserialize;
121
122 #[derive(Deserialize)]
123 struct ClaudeSettings {
124 #[serde(rename = "alwaysThinkingEnabled")]
125 always_thinking_enabled: Option<bool>,
126 }
127
128 #[cfg(unix)]
129 let home = std::env::var_os("HOME");
130 #[cfg(windows)]
131 let home = std::env::var_os("USERPROFILE");
132
133 let home = match home {
134 Some(h) => h,
135 None => return false,
136 };
137
138 let settings_path = std::path::Path::new(&home)
139 .join(".claude")
140 .join("settings.json");
141
142 let content = match std::fs::read_to_string(&settings_path) {
143 Ok(c) => c,
144 Err(_) => return false,
145 };
146
147 let settings: ClaudeSettings = match serde_json::from_str(&content) {
148 Ok(s) => s,
149 Err(_) => return false,
150 };
151
152 settings.always_thinking_enabled.unwrap_or(true)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_visible_len() {
161 assert_eq!(visible_len("Hello"), 5);
162 assert_eq!(visible_len("\x1b[31mHello\x1b[0m"), 5);
163 assert_eq!(visible_len("\x1b[38;2;10;20;30mHello"), 5);
164 assert_eq!(visible_len(""), 0);
165 assert_eq!(visible_len("foo bar"), 7);
166 assert_eq!(visible_len("\x1b[1;34mBoldBlue"), 8);
167 }
168
169 #[test]
170 fn test_truncate_with_ellipsis() {
171 assert_eq!(truncate_with_ellipsis("Hello World", 11), "Hello World");
172 assert_eq!(truncate_with_ellipsis("Hello World", 5), "Hell…\x1b[0m");
173 let s = "\x1b[31mHello World\x1b[0m";
174 assert_eq!(truncate_with_ellipsis(s, 5), "\x1b[31mHell…\x1b[0m");
175 }
176
177 #[test]
178 fn test_visible_len_wide_glyphs() {
179 assert_eq!(visible_len("🧪"), 2);
180 assert_eq!(visible_len("Test🧪"), 6);
181 assert_eq!(visible_len("🧪🔬"), 4);
182 assert_eq!(visible_len("日本語"), 6);
183 assert_eq!(visible_len("Hello日本"), 9);
184 assert_eq!(visible_len("Test🧪日本"), 10);
185 assert_eq!(visible_len("\x1b[31m日本語\x1b[0m"), 6);
186 assert_eq!(visible_len("\x1b[31m🧪Test\x1b[0m"), 6);
187 }
188
189 #[test]
190 fn test_truncate_wide_glyphs() {
191 let emoji_str = "Test🧪Data";
192 assert_eq!(truncate_with_ellipsis(emoji_str, 7), "Test🧪…\x1b[0m");
193 assert_eq!(truncate_with_ellipsis(emoji_str, 6), "Test…\x1b[0m");
194
195 let cjk_str = "日本語";
196 assert_eq!(truncate_with_ellipsis(cjk_str, 6), "日本語");
197 assert_eq!(truncate_with_ellipsis(cjk_str, 5), "日本…\x1b[0m");
198 assert_eq!(truncate_with_ellipsis(cjk_str, 3), "日…\x1b[0m");
199
200 let mixed = "Test日本語";
201 assert_eq!(truncate_with_ellipsis(mixed, 7), "Test日…\x1b[0m");
202 assert_eq!(truncate_with_ellipsis(mixed, 10), "Test日本語");
203
204 let path = "/home/user/日本語/test";
205 let truncated = truncate_with_ellipsis(path, 15);
206 assert!(visible_len(&truncated) <= 15);
207 }
208
209 #[test]
210 fn test_truncate_wide_boundary() {
211 let s = "abc日";
212 assert_eq!(truncate_with_ellipsis(s, 5), "abc日");
213 assert_eq!(truncate_with_ellipsis(s, 4), "abc…\x1b[0m");
214 }
215}