1use super::text::{Token, WidthMode, string_width, tokenize};
22
23#[inline]
24fn bs(b: &[u8]) -> &str {
25 unsafe { std::str::from_utf8_unchecked(b) }
27}
28
29pub fn truncate(s: &str, length: usize, tail: &str) -> String {
33 truncate_mode(s, length, tail, WidthMode::default(), false)
34}
35
36pub fn truncate_mode(
40 s: &str,
41 length: usize,
42 tail: &str,
43 mode: WidthMode,
44 eaw_wide: bool,
45) -> String {
46 if length == 0 {
47 return String::new();
48 }
49 if string_width(s.as_bytes(), mode, eaw_wide) <= length {
50 return s.to_string();
51 }
52 let tail_w = string_width(tail.as_bytes(), mode, eaw_wide);
53 let budget = length.saturating_sub(tail_w);
54
55 let mut out = String::new();
56 let mut used = 0usize;
57 let mut tail_inserted = false;
58 for tok in tokenize(s.as_bytes(), mode, eaw_wide) {
59 match tok {
60 Token::Escape(esc) => out.push_str(bs(esc)),
61 Token::Control(b) => {
62 if !tail_inserted {
63 out.push(b as char);
64 }
65 }
66 Token::Text { text, width } => {
67 let w = width as usize;
68 if used + w > budget {
69 if !tail_inserted {
70 out.push_str(tail);
71 tail_inserted = true;
72 }
73 continue;
75 }
76 out.push_str(bs(text));
77 used += w;
78 }
79 }
80 }
81 if !tail_inserted {
82 out.push_str(tail);
83 }
84 out
85}
86
87pub fn truncate_left(s: &str, length: usize, prefix: &str) -> String {
91 truncate_left_mode(s, length, prefix, WidthMode::default(), false)
92}
93
94pub fn truncate_left_mode(
98 s: &str,
99 length: usize,
100 prefix: &str,
101 mode: WidthMode,
102 eaw_wide: bool,
103) -> String {
104 let total = string_width(s.as_bytes(), mode, eaw_wide);
105 if total <= length {
106 return s.to_string();
107 }
108 let prefix_w = string_width(prefix.as_bytes(), mode, eaw_wide);
109 let drop = total.saturating_sub(length.saturating_sub(prefix_w));
110
111 let mut head_escapes = String::new();
112 let mut out = String::new();
113 let mut dropped = 0usize;
114 let mut dropping = true;
115 for tok in tokenize(s.as_bytes(), mode, eaw_wide) {
116 match tok {
117 Token::Escape(esc) => {
118 if dropping {
119 head_escapes.push_str(bs(esc));
120 } else {
121 out.push_str(bs(esc));
122 }
123 }
124 Token::Control(b) => {
125 if !dropping {
126 out.push(b as char);
127 }
128 }
129 Token::Text { text, width } => {
130 let w = width as usize;
131 if dropping {
132 dropped += w;
133 if dropped >= drop {
134 dropping = false;
135 }
136 } else {
137 out.push_str(bs(text));
138 }
139 }
140 }
141 }
142 let mut result = String::new();
143 result.push_str(&head_escapes);
145 result.push_str(prefix);
146 result.push_str(&out);
147 result
148}
149
150pub fn cut(s: &str, left: usize, right: usize) -> String {
154 cut_mode(s, left, right, WidthMode::default(), false)
155}
156
157pub fn cut_mode(s: &str, left: usize, right: usize, mode: WidthMode, eaw_wide: bool) -> String {
161 if left == 0 && right == 0 {
162 return s.to_string();
163 }
164 let total = string_width(s.as_bytes(), mode, eaw_wide);
165 if left >= total {
166 return String::new();
167 }
168 let keep_end = total.saturating_sub(right);
169 if keep_end <= left {
170 return String::new();
171 }
172 let target = keep_end - left;
173
174 let mut out = String::new();
175 let mut col = 0usize;
176 let mut emitted = 0usize;
177 for tok in tokenize(s.as_bytes(), mode, eaw_wide) {
178 match tok {
179 Token::Escape(esc) => out.push_str(bs(esc)),
180 Token::Control(b) => {
181 if col >= left && emitted < target {
182 out.push(b as char);
183 }
184 }
185 Token::Text { text, width } => {
186 let w = width as usize;
187 if col >= left && emitted + w <= target {
188 out.push_str(bs(text));
189 emitted += w;
190 }
191 col += w;
192 if emitted >= target {
193 }
195 }
196 }
197 }
198 out
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn truncate_basic() {
207 assert_eq!(truncate("hello world", 5, ""), "hello");
208 }
209
210 #[test]
211 fn truncate_with_tail() {
212 assert_eq!(truncate("hello world", 8, "..."), "hello...");
213 }
214
215 #[test]
216 fn truncate_no_op() {
217 assert_eq!(truncate("hi", 5, "..."), "hi");
218 }
219
220 #[test]
221 fn truncate_preserves_ansi() {
222 let s = "\x1b[31mhello world\x1b[m";
223 let got = truncate(s, 5, "");
224 assert_eq!(got, "\x1b[31mhello\x1b[m");
225 }
226
227 #[test]
228 fn truncate_wide_chars() {
229 assert_eq!(truncate("中文测试", 4, ""), "中文");
230 assert_eq!(truncate("中文", 3, ""), "中");
232 }
233
234 #[test]
235 fn truncate_zero_length() {
236 assert_eq!(truncate("hello", 0, ""), "");
237 assert_eq!(truncate("hello", 0, "..."), "");
238 }
239
240 #[test]
241 fn truncate_left_basic() {
242 assert_eq!(truncate_left("hello world", 5, ""), "world");
243 }
244
245 #[test]
246 fn truncate_left_with_prefix() {
247 assert_eq!(truncate_left("hello world", 8, "..."), "...world");
248 }
249
250 #[test]
251 fn cut_basic() {
252 assert_eq!(cut("hello world", 2, 2), "llo wor");
253 }
254
255 #[test]
256 fn cut_zero_zero_is_noop() {
257 assert_eq!(cut("hello", 0, 0), "hello");
258 }
259
260 #[test]
261 fn cut_too_wide() {
262 assert_eq!(cut("hi", 10, 0), "");
263 }
264
265 #[test]
266 fn truncate_preserves_osc_link() {
267 let s = "\x1b]8;;https://example.com\x1b\\link text\x1b]8;;\x1b\\";
268 let got = truncate(s, 4, "");
269 assert_eq!(got, "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\");
270 }
271}