1use std::{borrow::Cow, iter};
6
7use crate::{
8 grid::{
9 config::{ColoredConfig, Entity, Position, SpannedConfig},
10 dimension::{CompleteDimension, Estimate, IterGridDimension},
11 records::{
12 vec_records::Cell, EmptyRecords, ExactRecords, IntoRecords, PeekableRecords, Records,
13 RecordsMut,
14 },
15 util::string::{get_line_width, get_lines},
16 },
17 settings::{
18 measurement::Measurement,
19 peaker::{Peaker, PriorityNone},
20 CellOption, TableOption, Width,
21 },
22};
23
24use super::util::get_table_total_width;
25use crate::util::string::cut_str;
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
46pub struct Truncate<'a, W = usize, P = PriorityNone> {
47 width: W,
48 suffix: Option<TruncateSuffix<'a>>,
49 multiline: bool,
50 priority: P,
51}
52
53#[cfg(feature = "ansi")]
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
55struct TruncateSuffix<'a> {
56 text: Cow<'a, str>,
57 limit: SuffixLimit,
58 try_color: bool,
59}
60
61#[cfg(not(feature = "ansi"))]
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
63struct TruncateSuffix<'a> {
64 text: Cow<'a, str>,
65 limit: SuffixLimit,
66}
67
68impl Default for TruncateSuffix<'_> {
69 fn default() -> Self {
70 Self {
71 text: Cow::default(),
72 limit: SuffixLimit::Cut,
73 #[cfg(feature = "ansi")]
74 try_color: false,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
81pub enum SuffixLimit {
82 Cut,
84 Ignore,
86 Replace(char),
88}
89
90impl<W> Truncate<'static, W>
91where
92 W: Measurement<Width>,
93{
94 pub const fn new(width: W) -> Truncate<'static, W> {
96 Self {
97 width,
98 multiline: false,
99 suffix: None,
100 priority: PriorityNone::new(),
101 }
102 }
103}
104
105impl<'a, W, P> Truncate<'a, W, P> {
106 pub fn suffix<S: Into<Cow<'a, str>>>(self, suffix: S) -> Truncate<'a, W, P> {
115 let mut suff = self.suffix.unwrap_or_default();
116 suff.text = suffix.into();
117
118 Truncate {
119 width: self.width,
120 multiline: self.multiline,
121 priority: self.priority,
122 suffix: Some(suff),
123 }
124 }
125
126 pub fn suffix_limit(self, limit: SuffixLimit) -> Truncate<'a, W, P> {
128 let mut suff = self.suffix.unwrap_or_default();
129 suff.limit = limit;
130
131 Truncate {
132 width: self.width,
133 multiline: self.multiline,
134 priority: self.priority,
135 suffix: Some(suff),
136 }
137 }
138
139 pub fn multiline(self, on: bool) -> Truncate<'a, W, P> {
141 Truncate {
142 width: self.width,
143 multiline: on,
144 suffix: self.suffix,
145 priority: self.priority,
146 }
147 }
148
149 #[cfg(feature = "ansi")]
150 pub fn suffix_try_color(self, color: bool) -> Truncate<'a, W, P> {
152 let mut suff = self.suffix.unwrap_or_default();
153 suff.try_color = color;
154
155 Truncate {
156 width: self.width,
157 multiline: self.multiline,
158 priority: self.priority,
159 suffix: Some(suff),
160 }
161 }
162}
163
164impl<'a, W, P> Truncate<'a, W, P> {
165 pub fn priority<PP: Peaker>(self, priority: PP) -> Truncate<'a, W, PP> {
174 Truncate {
175 width: self.width,
176 multiline: self.multiline,
177 suffix: self.suffix,
178 priority,
179 }
180 }
181}
182
183impl Truncate<'_, (), ()> {
184 pub fn truncate(text: &str, width: usize) -> Cow<'_, str> {
186 truncate_text(text, width, "", false)
187 }
188}
189
190impl<W, P, R> CellOption<R, ColoredConfig> for Truncate<'_, W, P>
191where
192 W: Measurement<Width>,
193 R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
194 for<'a> &'a R: Records,
195 for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef<str>,
196{
197 fn change(self, records: &mut R, cfg: &mut ColoredConfig, entity: Entity) {
198 let available = self.width.measure(&*records, cfg);
199
200 let mut width = available;
201
202 let colorize = need_suffix_color_preservation(&self.suffix);
203 let mut suffix = Cow::Borrowed("");
204 if let Some(x) = self.suffix.as_ref() {
205 let (cut_suffix, rest_width) = make_suffix(x, width);
206 suffix = cut_suffix;
207 width = rest_width;
208 }
209
210 let count_rows = records.count_rows();
211 let count_columns = records.count_columns();
212 let max_pos = Position::new(count_rows, count_columns);
213
214 for pos in entity.iter(count_rows, count_columns) {
215 if !max_pos.has_coverage(pos) {
216 continue;
217 }
218
219 let cell_width = records.get_width(pos);
220 if available >= cell_width {
221 continue;
222 }
223
224 let text = records.get_text(pos);
225 let text =
226 truncate_multiline(text, &suffix, width, available, colorize, self.multiline);
227
228 records.set(pos, text.into_owned());
229 }
230 }
231}
232
233impl<W, P, R> TableOption<R, ColoredConfig, CompleteDimension> for Truncate<'_, W, P>
234where
235 W: Measurement<Width>,
236 P: Peaker,
237 R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
238 for<'a> &'a R: Records,
239 for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: Cell + AsRef<str>,
240{
241 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
242 if records.count_rows() == 0 || records.count_columns() == 0 {
243 return;
244 }
245
246 let width = self.width.measure(&*records, cfg);
247
248 dims.estimate(&*records, cfg);
249 let widths = dims.get_widths().expect("must be present");
250
251 let total = get_table_total_width(widths, cfg);
252 if total <= width {
253 return;
254 }
255
256 let t = Truncate {
257 multiline: self.multiline,
258 priority: self.priority,
259 suffix: self.suffix,
260 width,
261 };
262
263 let widths = truncate_total_width(records, cfg, widths, total, t);
264
265 dims.set_widths(widths);
266 dims.clear_height(); }
268
269 fn hint_change(&self) -> Option<Entity> {
270 None
272 }
273}
274
275fn truncate_multiline<'a>(
276 text: &'a str,
277 suffix: &'a str,
278 width: usize,
279 twidth: usize,
280 suffix_color: bool,
281 multiline: bool,
282) -> Cow<'a, str> {
283 if !multiline {
284 return make_text_truncated(text, suffix, width, twidth, suffix_color);
285 }
286
287 let mut buf = String::new();
288 for (i, line) in get_lines(text).enumerate() {
289 if i != 0 {
290 buf.push('\n');
291 }
292
293 let line = make_text_truncated(&line, suffix, width, twidth, suffix_color);
294 buf.push_str(&line);
295 }
296
297 Cow::Owned(buf)
298}
299
300fn make_text_truncated<'a>(
301 text: &'a str,
302 suffix: &'a str,
303 width: usize,
304 twidth: usize,
305 suffix_color: bool,
306) -> Cow<'a, str> {
307 if width == 0 {
308 if twidth == 0 {
309 Cow::Borrowed("")
310 } else {
311 Cow::Borrowed(suffix)
312 }
313 } else {
314 truncate_text(text, width, suffix, suffix_color)
315 }
316}
317
318fn need_suffix_color_preservation(_suffix: &Option<TruncateSuffix<'_>>) -> bool {
319 #[cfg(not(feature = "ansi"))]
320 {
321 false
322 }
323 #[cfg(feature = "ansi")]
324 {
325 _suffix.as_ref().is_some_and(|s| s.try_color)
326 }
327}
328
329fn make_suffix<'a>(suffix: &'a TruncateSuffix<'_>, width: usize) -> (Cow<'a, str>, usize) {
330 let suffix_length = get_line_width(&suffix.text);
331 if width > suffix_length {
332 return (Cow::Borrowed(suffix.text.as_ref()), width - suffix_length);
333 }
334
335 match suffix.limit {
336 SuffixLimit::Ignore => (Cow::Borrowed(""), width),
337 SuffixLimit::Cut => {
338 let suffix = cut_str(&suffix.text, width);
339 (suffix, 0)
340 }
341 SuffixLimit::Replace(c) => {
342 let suffix = Cow::Owned(iter::repeat_n(c, width).collect());
343 (suffix, 0)
344 }
345 }
346}
347
348fn truncate_total_width<P, R>(
349 records: &mut R,
350 cfg: &mut ColoredConfig,
351 widths: &[usize],
352 total: usize,
353 t: Truncate<'_, usize, P>,
354) -> Vec<usize>
355where
356 P: Peaker,
357 R: Records + PeekableRecords + ExactRecords + RecordsMut<String>,
358 for<'a> &'a R: Records,
359 for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef<str>,
360{
361 let count_rows = records.count_rows();
362 let count_columns = records.count_columns();
363
364 let colorize = need_suffix_color_preservation(&t.suffix);
365 let mut suffix = Cow::Borrowed("");
366
367 let min_widths = IterGridDimension::width(EmptyRecords::new(count_rows, count_columns), cfg);
368
369 let mut widths = widths.to_vec();
370 decrease_widths(&mut widths, &min_widths, total, t.width, t.priority);
371
372 let points = get_decrease_cell_list(cfg, &widths, &min_widths, count_rows, count_columns);
373
374 for (pos, mut width) in points {
375 let text_width = records.get_width(pos);
376 if width >= text_width {
377 continue;
378 }
379
380 let text = records.get_text(pos);
381 if let Some(x) = &t.suffix {
382 let (cut_suffix, rest_width) = make_suffix(x, width);
383 suffix = cut_suffix;
384 width = rest_width;
385 }
386
387 let text = truncate_multiline(text, &suffix, width, text_width, colorize, t.multiline);
388
389 records.set(pos, text.into_owned());
390 }
391
392 widths
393}
394
395fn truncate_text<'a>(
396 text: &'a str,
397 width: usize,
398 suffix: &str,
399 _suffix_color: bool,
400) -> Cow<'a, str> {
401 let content = cut_str(text, width);
402 if suffix.is_empty() {
403 return content;
404 }
405
406 #[cfg(feature = "ansi")]
407 {
408 if _suffix_color {
409 if let Some(block) = ansi_str::get_blocks(text).last() {
410 if block.has_ansi() {
411 let style = block.style();
412 Cow::Owned(format!(
413 "{}{}{}{}",
414 content,
415 style.start(),
416 suffix,
417 style.end()
418 ))
419 } else {
420 let mut content = content.into_owned();
421 content.push_str(suffix);
422 Cow::Owned(content)
423 }
424 } else {
425 let mut content = content.into_owned();
426 content.push_str(suffix);
427 Cow::Owned(content)
428 }
429 } else {
430 let mut content = content.into_owned();
431 content.push_str(suffix);
432 Cow::Owned(content)
433 }
434 }
435
436 #[cfg(not(feature = "ansi"))]
437 {
438 let mut content = content.into_owned();
439 content.push_str(suffix);
440 Cow::Owned(content)
441 }
442}
443
444fn get_decrease_cell_list(
445 cfg: &SpannedConfig,
446 widths: &[usize],
447 min_widths: &[usize],
448 count_rows: usize,
449 count_columns: usize,
450) -> Vec<(Position, usize)> {
451 let mut points = Vec::new();
452 for col in 0..count_columns {
453 for row in 0..count_rows {
454 let pos = Position::new(row, col);
455 if !cfg.is_cell_visible(pos) {
456 continue;
457 }
458
459 let (width, width_min) = match cfg.get_column_span(pos) {
460 Some(span) => {
461 let width = (col..col + span).map(|i| widths[i]).sum::<usize>();
462 let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>();
463 let count_borders = count_borders(cfg, col, col + span, count_columns);
464 (width + count_borders, min_width + count_borders)
465 }
466 None => (widths[col], min_widths[col]),
467 };
468
469 if width >= width_min {
470 let padding = cfg.get_padding(pos);
471 let width = width.saturating_sub(padding.left.size + padding.right.size);
472
473 points.push((pos, width));
474 }
475 }
476 }
477
478 points
479}
480
481fn decrease_widths<F>(
482 widths: &mut [usize],
483 min_widths: &[usize],
484 total_width: usize,
485 mut width: usize,
486 mut peeaker: F,
487) where
488 F: Peaker,
489{
490 let mut empty_list = 0;
491 for col in 0..widths.len() {
492 if widths[col] == 0 || widths[col] <= min_widths[col] {
493 empty_list += 1;
494 }
495 }
496
497 while width != total_width {
498 if empty_list == widths.len() {
499 break;
500 }
501
502 let col = match peeaker.peak(min_widths, widths) {
503 Some(col) => col,
504 None => break,
505 };
506
507 if widths[col] == 0 || widths[col] <= min_widths[col] {
508 continue;
509 }
510
511 widths[col] -= 1;
512
513 if widths[col] == 0 || widths[col] <= min_widths[col] {
514 empty_list += 1;
515 }
516
517 width += 1;
518 }
519}
520
521fn count_borders(cfg: &SpannedConfig, start: usize, end: usize, count_columns: usize) -> usize {
522 (start..end)
523 .skip(1)
524 .filter(|&i| cfg.has_vertical(i, count_columns))
525 .count()
526}