thndrs_lib/core/utils/
mod.rs1pub mod datetime;
2
3use crate::tools::MAX_LINE_LEN;
4
5use std::path::PathBuf;
6
7use unicode_segmentation::UnicodeSegmentation;
8use unicode_width::UnicodeWidthStr;
9
10pub fn ratio_of(value: u64, ratio: f64) -> u64 {
12 if value == 0 {
13 return 0;
14 }
15 let scaled = (value as f64) * ratio;
16 scaled.floor() as u64
17}
18
19pub fn scope_depth(scope: &str) -> usize {
21 if scope == "." || scope.is_empty() {
22 return 0;
23 }
24 scope.matches('/').count() + 1
25}
26
27pub fn home_dir() -> Option<PathBuf> {
29 std::env::var_os("HOME")
30 .map(PathBuf::from)
31 .or_else(|| std::env::var_os("USERPROFILE").map(PathBuf::from))
32}
33
34pub fn escape_xml(value: &str) -> String {
36 value
37 .replace('&', "&")
38 .replace('<', "<")
39 .replace('>', ">")
40 .replace('"', """)
41 .replace('\'', "'")
42}
43
44pub fn truncate_line(s: &str) -> String {
46 if s.chars().count() <= MAX_LINE_LEN {
47 s.to_string()
48 } else {
49 format!("{}...", s.chars().take(MAX_LINE_LEN).collect::<String>())
50 }
51}
52
53pub fn truncate_ellipsis(s: &str, max_width: usize) -> String {
59 if max_width == 0 {
60 return String::new();
61 }
62 if text_width(s) <= max_width {
63 return s.to_string();
64 }
65 if max_width <= 1 {
66 return "…".to_string();
67 }
68 format!("{}…", take_display_width(s, max_width - 1))
69}
70
71pub fn truncate_ellipsis_start(s: &str, max_width: usize) -> String {
75 if max_width == 0 {
76 return String::new();
77 }
78 if text_width(s) <= max_width {
79 return s.to_string();
80 }
81 if max_width <= 1 {
82 return "…".to_string();
83 }
84
85 format!("…{}", take_display_width_from_end(s, max_width - 1))
86}
87
88pub fn text_width(text: &str) -> usize {
90 UnicodeWidthStr::width(text)
91}
92
93pub fn grapheme_width(grapheme: &str) -> usize {
95 UnicodeWidthStr::width(grapheme)
96}
97
98fn take_display_width(text: &str, max_width: usize) -> String {
99 let mut out = String::new();
100 let mut used = 0usize;
101 for grapheme in text.graphemes(true) {
102 let width = text_width(grapheme);
103 if used + width > max_width {
104 break;
105 }
106 out.push_str(grapheme);
107 used += width;
108 }
109 out
110}
111
112fn take_display_width_from_end(text: &str, max_width: usize) -> String {
113 let mut chunks = Vec::new();
114 let mut used = 0usize;
115 for grapheme in text.graphemes(true).rev() {
116 let width = text_width(grapheme);
117 if used + width > max_width {
118 break;
119 }
120 chunks.push(grapheme);
121 used += width;
122 }
123 chunks.into_iter().rev().collect()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn truncate_line_short_unchanged() {
132 assert_eq!(truncate_line("hello"), "hello");
133 }
134
135 #[test]
136 fn truncate_line_long_truncated() {
137 let c = MAX_LINE_LEN;
138 let long = "x".repeat(c + 100);
139 let result = truncate_line(&long);
140 assert!(result.ends_with("..."));
141 assert!(result.chars().count() <= c + 3);
142 }
143
144 #[test]
145 fn truncate_ellipsis_short_unchanged() {
146 assert_eq!(truncate_ellipsis("hello", 10), "hello");
147 }
148
149 #[test]
150 fn truncate_ellipsis_exact_fit() {
151 assert_eq!(truncate_ellipsis("hello", 5), "hello");
152 }
153
154 #[test]
155 fn truncate_ellipsis_truncates_with_ellipsis() {
156 assert_eq!(truncate_ellipsis("hello world", 8), "hello w…");
157 }
158
159 #[test]
160 fn truncate_ellipsis_uses_display_width() {
161 assert_eq!(truncate_ellipsis("ab中cd", 5), "ab中…");
162 }
163
164 #[test]
165 fn truncate_ellipsis_keeps_grapheme_together() {
166 let family = "👨\u{200d}👩\u{200d}👧";
167 assert_eq!(truncate_ellipsis(&format!("{family}abc"), 4), format!("{family}a…"));
168 }
169
170 #[test]
171 fn truncate_ellipsis_zero_max() {
172 assert_eq!(truncate_ellipsis("hello", 0), "");
173 }
174
175 #[test]
176 fn truncate_ellipsis_one_max() {
177 assert_eq!(truncate_ellipsis("hello", 1), "…");
178 }
179
180 #[test]
181 fn truncate_ellipsis_start_keeps_end() {
182 assert_eq!(truncate_ellipsis_start("/long/path/to/file.rs", 15), "…ath/to/file.rs");
183 }
184
185 #[test]
186 fn truncate_ellipsis_start_uses_display_width() {
187 assert_eq!(truncate_ellipsis_start("ab中cd", 5), "…中cd");
188 }
189
190 #[test]
191 fn truncate_ellipsis_start_short_unchanged() {
192 assert_eq!(truncate_ellipsis_start("short", 10), "short");
193 }
194
195 #[test]
196 fn scope_depth_counts_segments() {
197 assert_eq!(scope_depth("."), 0);
198 assert_eq!(scope_depth("src"), 1);
199 assert_eq!(scope_depth("src/core"), 2);
200 assert_eq!(scope_depth("src/core/context"), 3);
201 }
202}