1use crate::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum TruncateStrategy {
14 #[default]
16 End,
17 Start,
19 Middle,
21 Path,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum ColumnAlign {
28 #[default]
30 Left,
31 Right,
33 Center,
35}
36
37#[must_use]
54pub fn truncate(s: &str, max_width: usize, strategy: TruncateStrategy) -> String {
55 if max_width == 0 {
56 return String::new();
57 }
58
59 let char_count = s.chars().count();
60
61 if char_count <= max_width {
62 return s.to_string();
63 }
64
65 if max_width == 1 {
66 return "\u{2026}".to_string();
67 }
68
69 match strategy {
70 TruncateStrategy::End => {
71 let chars: String = s.chars().take(max_width - 1).collect();
72 format!("{chars}\u{2026}")
73 }
74 TruncateStrategy::Start => {
75 let chars: String = s.chars().skip(char_count - max_width + 1).collect();
76 format!("\u{2026}{chars}")
77 }
78 TruncateStrategy::Middle => {
79 let left_len = (max_width - 1) / 2;
80 let right_len = max_width - 1 - left_len;
81 let left: String = s.chars().take(left_len).collect();
82 let right: String = s.chars().skip(char_count - right_len).collect();
83 format!("{left}\u{2026}{right}")
84 }
85 TruncateStrategy::Path => truncate_path(s, max_width),
86 }
87}
88
89#[must_use]
100pub fn truncate_path(path: &str, max_width: usize) -> String {
101 if max_width == 0 {
102 return String::new();
103 }
104
105 let char_count = path.chars().count();
106
107 if char_count <= max_width {
108 return path.to_string();
109 }
110
111 if let Some(last_sep) = path.rfind('/') {
112 let filename = &path[last_sep..];
113 let filename_len = filename.chars().count();
114
115 if filename_len >= max_width {
116 return truncate(path, max_width, TruncateStrategy::End);
117 }
118
119 let dir_space = max_width.saturating_sub(filename_len).saturating_sub(1);
120
121 if dir_space == 0 {
122 let result = format!("\u{2026}{filename}");
123 if result.chars().count() <= max_width {
124 return result;
125 }
126 return truncate(path, max_width, TruncateStrategy::End);
127 }
128
129 let dir = &path[..last_sep];
130 let dir_chars: Vec<char> = dir.chars().collect();
131
132 if dir_chars.len() <= dir_space {
133 return path.to_string();
134 }
135
136 let truncated_dir: String = dir_chars.iter().take(dir_space).collect();
137 let result = format!("{truncated_dir}\u{2026}{filename}");
138
139 if result.chars().count() <= max_width {
140 result
141 } else {
142 truncate(path, max_width, TruncateStrategy::End)
143 }
144 } else {
145 truncate(path, max_width, TruncateStrategy::End)
146 }
147}
148
149#[must_use]
165pub fn format_column(
166 text: &str,
167 width: usize,
168 align: ColumnAlign,
169 truncate_strategy: TruncateStrategy,
170) -> String {
171 let char_count = text.chars().count();
172
173 let truncated = if char_count > width {
174 truncate(text, width, truncate_strategy)
175 } else {
176 text.to_string()
177 };
178
179 let truncated_len = truncated.chars().count();
180 let padding = width.saturating_sub(truncated_len);
181
182 match align {
183 ColumnAlign::Left => {
184 let mut result = truncated;
185 for _ in 0..padding {
186 result.push(' ');
187 }
188 result
189 }
190 ColumnAlign::Right => {
191 let mut result = String::with_capacity(width);
192 for _ in 0..padding {
193 result.push(' ');
194 }
195 result.push_str(&truncated);
196 result
197 }
198 ColumnAlign::Center => {
199 let left_pad = padding / 2;
200 let right_pad = padding - left_pad;
201 let mut result = String::with_capacity(width);
202 for _ in 0..left_pad {
203 result.push(' ');
204 }
205 result.push_str(&truncated);
206 for _ in 0..right_pad {
207 result.push(' ');
208 }
209 result
210 }
211 }
212}
213
214#[must_use]
222pub fn format_bytes_column(bytes: u64, width: usize) -> String {
223 let formatted = fmt::format_bytes_si(bytes);
224 format_column(&formatted, width, ColumnAlign::Right, TruncateStrategy::End)
225}
226
227#[must_use]
235pub fn format_percent_column(value: f64, width: usize) -> String {
236 let formatted = fmt::format_percent(value);
237 format_column(&formatted, width, ColumnAlign::Right, TruncateStrategy::End)
238}
239
240#[must_use]
242pub fn format_number_column(n: u64, width: usize) -> String {
243 let formatted = fmt::format_number(n);
244 format_column(&formatted, width, ColumnAlign::Right, TruncateStrategy::End)
245}
246
247pub trait WithDimensions: Sized {
274 fn set_dimensions(&mut self, width: u32, height: u32);
276
277 #[must_use]
279 fn dimensions(mut self, width: u32, height: u32) -> Self {
280 self.set_dimensions(width, height);
281 self
282 }
283}
284
285#[must_use]
303pub fn truncate_str(s: &str, max_len: usize) -> String {
304 if s.len() <= max_len {
305 return s.to_string();
306 }
307 if max_len <= 3 {
308 return ".".repeat(max_len);
309 }
310 let end = s
311 .char_indices()
312 .nth(max_len - 3)
313 .map_or(max_len - 3, |(i, _)| i);
314 format!("{}...", &s[..end])
315}
316
317#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
328 fn test_truncate_short_string_unchanged() {
329 assert_eq!(truncate("hello", 10, TruncateStrategy::End), "hello");
330 }
331
332 #[test]
333 fn test_truncate_end() {
334 assert_eq!(
335 truncate("hello world", 8, TruncateStrategy::End),
336 "hello w\u{2026}"
337 );
338 }
339
340 #[test]
341 fn test_truncate_start() {
342 assert_eq!(
343 truncate("hello world", 8, TruncateStrategy::Start),
344 "\u{2026}o world"
345 );
346 }
347
348 #[test]
349 fn test_truncate_middle() {
350 assert_eq!(
351 truncate("hello world", 8, TruncateStrategy::Middle),
352 "hel\u{2026}orld"
353 );
354 }
355
356 #[test]
357 fn test_truncate_zero_width() {
358 assert_eq!(truncate("anything", 0, TruncateStrategy::End), "");
359 }
360
361 #[test]
362 fn test_truncate_width_one() {
363 assert_eq!(truncate("hello", 1, TruncateStrategy::End), "\u{2026}");
364 }
365
366 #[test]
367 fn test_truncate_path_preserves_filename() {
368 assert_eq!(
369 truncate_path("/home/user/documents/file.txt", 20),
370 "/home/user\u{2026}/file.txt"
371 );
372 }
373
374 #[test]
375 fn test_truncate_path_short_enough() {
376 assert_eq!(truncate_path("/a/b/c.txt", 20), "/a/b/c.txt");
377 }
378
379 #[test]
382 fn test_format_column_left() {
383 assert_eq!(
384 format_column("test", 8, ColumnAlign::Left, TruncateStrategy::End),
385 "test "
386 );
387 }
388
389 #[test]
390 fn test_format_column_right() {
391 assert_eq!(
392 format_column("test", 8, ColumnAlign::Right, TruncateStrategy::End),
393 " test"
394 );
395 }
396
397 #[test]
398 fn test_format_column_center() {
399 assert_eq!(
400 format_column("test", 8, ColumnAlign::Center, TruncateStrategy::End),
401 " test "
402 );
403 }
404
405 #[test]
406 fn test_format_column_truncates() {
407 assert_eq!(
408 format_column(
409 "very long text",
410 8,
411 ColumnAlign::Left,
412 TruncateStrategy::End
413 ),
414 "very lo\u{2026}"
415 );
416 }
417
418 #[test]
419 fn test_format_bytes_column() {
420 assert_eq!(format_bytes_column(1500, 6), " 1.50K");
421 }
422
423 #[test]
424 fn test_format_percent_column() {
425 assert_eq!(format_percent_column(45.3, 7), " 45.3%");
426 }
427
428 #[test]
431 fn test_truncate_str_short_unchanged() {
432 assert_eq!(truncate_str("hello", 10), "hello");
433 }
434
435 #[test]
436 fn test_truncate_str_exact_fit() {
437 assert_eq!(truncate_str("hello", 5), "hello");
438 }
439
440 #[test]
441 fn test_truncate_str_with_ellipsis() {
442 assert_eq!(truncate_str("hello world", 8), "hello...");
443 }
444
445 #[test]
446 fn test_truncate_str_min_len() {
447 assert_eq!(truncate_str("abcdef", 3), "...");
448 }
449
450 #[test]
451 fn test_truncate_str_len_4() {
452 assert_eq!(truncate_str("abcdef", 4), "a...");
453 }
454
455 #[test]
456 fn test_truncate_str_empty() {
457 assert_eq!(truncate_str("", 5), "");
458 }
459
460 #[test]
463 fn test_with_dimensions_trait() {
464 struct TestWidget {
465 width: u32,
466 height: u32,
467 }
468
469 impl WithDimensions for TestWidget {
470 fn set_dimensions(&mut self, width: u32, height: u32) {
471 self.width = width;
472 self.height = height;
473 }
474 }
475
476 let w = TestWidget {
477 width: 0,
478 height: 0,
479 }
480 .dimensions(120, 40);
481 assert_eq!(w.width, 120);
482 assert_eq!(w.height, 40);
483 }
484}