tabled/settings/width/
truncate.rs

1//! This module contains [`Truncate`] structure, used to decrease width of a [`Table`]s or a cell on a [`Table`] by truncating the width.
2//!
3//! [`Table`]: crate::Table
4
5use 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/// Truncate cut the string to a given width if its length exceeds it.
28/// Otherwise keeps the content of a cell untouched.
29///
30/// The function is color aware if a `color` feature is on.
31///
32/// Be aware that it doesn't consider padding.
33/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0.
34///    
35/// ## Example
36///
37/// ```
38/// use tabled::{Table, settings::{object::Segment, Width, Modify}};
39///
40/// let table = Table::new(&["Hello World!"])
41///     .with(Modify::new(Segment::all()).with(Width::truncate(3)));
42/// ```
43///
44/// [`Padding`]: crate::settings::Padding
45#[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/// A suffix limit settings.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
81pub enum SuffixLimit {
82    /// Cut the suffix.
83    Cut,
84    /// Don't show the suffix.
85    Ignore,
86    /// Use a string with n chars instead.
87    Replace(char),
88}
89
90impl<W> Truncate<'static, W>
91where
92    W: Measurement<Width>,
93{
94    /// Creates a [`Truncate`] object
95    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    /// Sets a suffix which will be appended to a resultant string.
107    ///
108    /// The suffix is used in 3 circumstances:
109    ///     1. If original string is *bigger* than the suffix.
110    ///        We cut more of the original string and append the suffix.
111    ///     2. If suffix is bigger than the original string.
112    ///        We cut the suffix to fit in the width by default.
113    ///        But you can peak the behaviour by using [`Truncate::suffix_limit`]
114    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    /// Sets a suffix limit, which is used when the suffix is too big to be used.
127    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    /// Use trancate logic per line, not as a string as a whole.
140    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    /// Sets a optional logic to try to colorize a suffix.
151    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    /// Priority defines the logic by which a truncate will be applied when is done for the whole table.
166    ///
167    /// - [`PriorityNone`] which cuts the columns one after another.
168    /// - [`PriorityMax`] cuts the biggest columns first.
169    /// - [`PriorityMin`] cuts the lowest columns first.
170    ///
171    /// [`PriorityMax`]: crate::settings::peaker::PriorityMax
172    /// [`PriorityMin`]: crate::settings::peaker::PriorityMin
173    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    /// Truncate a given string
185    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(); // TODO: Can be optimized -- we must know the proper height already since we did wrap
267    }
268
269    fn hint_change(&self) -> Option<Entity> {
270        // NOTE: We properly set widths and heights so nothign got need reastimation
271        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}