Skip to main content

ctx/
utils.rs

1//! Shared utility functions for ctx.
2//!
3//! This module contains helper functions used across multiple command modules.
4
5/// Truncate a string with ellipsis, respecting UTF-8 char boundaries.
6///
7/// If the string is longer than `max` characters, it will be truncated
8/// to `max - 3` characters followed by "...".
9///
10/// # Examples
11///
12/// ```ignore
13/// assert_eq!(truncate_str("hello world", 8), "hello...");
14/// assert_eq!(truncate_str("short", 10), "short");
15/// ```
16pub fn truncate_str(s: &str, max: usize) -> String {
17    if s.len() <= max {
18        s.to_string()
19    } else {
20        let target = max.saturating_sub(3);
21        let mut end = target;
22        while end > 0 && !s.is_char_boundary(end) {
23            end -= 1;
24        }
25        format!("{}...", &s[..end])
26    }
27}
28
29/// Truncate a path from the beginning, respecting UTF-8 char boundaries.
30///
31/// Unlike `truncate_str`, this keeps the end of the string (which typically
32/// contains the filename) and truncates from the beginning.
33///
34/// # Examples
35///
36/// ```ignore
37/// assert_eq!(truncate_path("/very/long/path/to/file.rs", 15), "...to/file.rs");
38/// assert_eq!(truncate_path("short.rs", 20), "short.rs");
39/// ```
40pub fn truncate_path(s: &str, max: usize) -> String {
41    if s.len() <= max {
42        s.to_string()
43    } else {
44        let target = s.len() - max + 3;
45        let mut start = target;
46        while start < s.len() && !s.is_char_boundary(start) {
47            start += 1;
48        }
49        format!("...{}", &s[start..])
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_truncate_str_ascii() {
59        // No truncation needed
60        assert_eq!(truncate_str("hello", 10), "hello");
61        assert_eq!(truncate_str("hello", 5), "hello");
62
63        // Truncation needed
64        assert_eq!(truncate_str("hello world", 8), "hello...");
65        assert_eq!(truncate_str("abcdefghij", 7), "abcd...");
66    }
67
68    #[test]
69    fn test_truncate_str_unicode() {
70        // Box drawing (─ is 3 bytes)
71        let box_line = "┌────────────────────┐";
72        let result = truncate_str(box_line, 10);
73        assert!(result.ends_with("..."));
74        // Should not panic
75
76        // Emoji (🎉 is 4 bytes)
77        let emoji = "Hello 🎉🎊🎁 World";
78        let result = truncate_str(emoji, 10);
79        assert!(result.ends_with("..."));
80
81        // Chinese (each char is 3 bytes)
82        let chinese = "你好世界测试";
83        let result = truncate_str(chinese, 8);
84        assert!(result.ends_with("..."));
85    }
86
87    #[test]
88    fn test_truncate_path_ascii() {
89        // No truncation needed
90        assert_eq!(truncate_path("src/main.rs", 20), "src/main.rs");
91
92        // Truncation needed - keeps end of path
93        let result = truncate_path("/very/long/path/to/file.rs", 15);
94        assert!(result.starts_with("..."));
95        assert!(result.contains("file.rs"));
96    }
97
98    #[test]
99    fn test_truncate_path_unicode() {
100        // Path with Unicode
101        let path = "/home/用户/项目/文件.rs";
102        let result = truncate_path(path, 15);
103        assert!(result.starts_with("..."));
104        // Should not panic
105
106        // Path with emoji folder names
107        let path = "/home/📁/🎉/file.rs";
108        let result = truncate_path(path, 12);
109        assert!(result.starts_with("..."));
110    }
111
112    #[test]
113    fn test_truncate_edge_cases() {
114        // Very short max
115        assert_eq!(truncate_str("hello", 3), "...");
116        assert_eq!(truncate_str("hi", 3), "hi");
117
118        // Empty string
119        assert_eq!(truncate_str("", 10), "");
120        assert_eq!(truncate_path("", 10), "");
121    }
122}