tabled/features/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
3use std::{borrow::Cow, marker::PhantomData};
4
5use papergrid::{
6    records::{empty::EmptyRecords, Records, RecordsMut},
7    util::cut_str,
8    width::{CfgWidthFunction, WidthFunc},
9    Entity, GridConfig,
10};
11
12use crate::{
13    peaker::{Peaker, PriorityNone},
14    width::{count_borders, get_table_widths, get_table_widths_with_total, Measurment},
15    CellOption, Table, TableOption, Width,
16};
17
18/// Truncate cut the string to a given width if its length exceeds it.
19/// Otherwise keeps the content of a cell untouched.
20///
21/// The function is color aware if a `color` feature is on.
22///
23/// Be aware that it doesn't consider padding.
24/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0.
25///    
26/// ## Example
27///
28/// ```
29/// use tabled::{object::Segment, Width, Modify, Table};
30///
31/// let table = Table::new(&["Hello World!"])
32///     .with(Modify::new(Segment::all()).with(Width::truncate(3)));
33/// ```
34///
35/// [`Padding`]: crate::Padding
36#[derive(Debug)]
37pub struct Truncate<'a, W = usize, P = PriorityNone> {
38    width: W,
39    suffix: Option<TruncateSuffix<'a>>,
40    _priority: PhantomData<P>,
41}
42
43#[derive(Debug)]
44struct TruncateSuffix<'a> {
45    text: Cow<'a, str>,
46    limit: SuffixLimit,
47    #[cfg(feature = "color")]
48    try_color: bool,
49}
50
51impl Default for TruncateSuffix<'_> {
52    fn default() -> Self {
53        Self {
54            text: Cow::default(),
55            limit: SuffixLimit::Cut,
56            #[cfg(feature = "color")]
57            try_color: false,
58        }
59    }
60}
61
62/// A suffix limit settings.
63#[derive(Debug, Clone, Copy)]
64pub enum SuffixLimit {
65    /// Cut the suffix.
66    Cut,
67    /// Don't show the suffix.
68    Ignore,
69    /// Use a string with n chars instead.
70    Replace(char),
71}
72
73impl<W> Truncate<'static, W>
74where
75    W: Measurment<Width>,
76{
77    /// Creates a [`Truncate`] object
78    pub fn new(width: W) -> Truncate<'static, W> {
79        Self {
80            width,
81            suffix: None,
82            _priority: PhantomData::default(),
83        }
84    }
85}
86
87impl<'a, W, P> Truncate<'a, W, P> {
88    /// Sets a suffix which will be appended to a resultant string.
89    ///
90    /// The suffix is used in 3 circamstances:
91    ///     1. If original string is *bigger* than the suffix.
92    ///        We cut more of the original string and append the suffix.
93    ///     2. If suffix is bigger than the original string.
94    ///        We cut the suffix to fit in the width by default.
95    ///        But you can peak the behaviour by using [`Truncate::suffix_limit`]
96    pub fn suffix<S: Into<Cow<'a, str>>>(self, suffix: S) -> Truncate<'a, W, P> {
97        let mut suff = self.suffix.unwrap_or_default();
98        suff.text = suffix.into();
99
100        Truncate {
101            width: self.width,
102            suffix: Some(suff),
103            _priority: PhantomData::default(),
104        }
105    }
106
107    /// Sets a suffix limit, which is used when the suffix is too big to be used.
108    pub fn suffix_limit(self, limit: SuffixLimit) -> Truncate<'a, W, P> {
109        let mut suff = self.suffix.unwrap_or_default();
110        suff.limit = limit;
111
112        Truncate {
113            width: self.width,
114            suffix: Some(suff),
115            _priority: PhantomData::default(),
116        }
117    }
118
119    #[cfg(feature = "color")]
120    /// Sets a optional logic to try to colorize a suffix.
121    pub fn suffix_try_color(self, color: bool) -> Truncate<'a, W, P> {
122        let mut suff = self.suffix.unwrap_or_default();
123        suff.try_color = color;
124
125        Truncate {
126            width: self.width,
127            suffix: Some(suff),
128            _priority: PhantomData::default(),
129        }
130    }
131}
132
133impl<'a, W, P> Truncate<'a, W, P> {
134    /// Priority defines the logic by which a truncate will be applied when is done for the whole table.
135    ///
136    /// - [`PriorityNone`] which cuts the columns one after another.
137    /// - [`PriorityMax`] cuts the biggest columns first.
138    /// - [`PriorityMin`] cuts the lowest columns first.
139    ///
140    /// [`PriorityMax`]: crate::peaker::PriorityMax
141    /// [`PriorityMin`]: crate::peaker::PriorityMin
142    pub fn priority<PP: Peaker>(self) -> Truncate<'a, W, PP> {
143        Truncate {
144            width: self.width,
145            suffix: self.suffix,
146            _priority: PhantomData::default(),
147        }
148    }
149}
150
151impl<W, P, R> CellOption<R> for Truncate<'_, W, P>
152where
153    W: Measurment<Width>,
154    R: Records + RecordsMut<String>,
155{
156    fn change_cell(&mut self, table: &mut Table<R>, entity: Entity) {
157        let width_ctrl = CfgWidthFunction::from_cfg(table.get_config());
158        let set_width = self.width.measure(table.get_records(), table.get_config());
159
160        let mut width = set_width;
161        let suffix = match self.suffix.as_ref() {
162            Some(suffix) => {
163                let suffix_length = width_ctrl.width(&suffix.text);
164                if width > suffix_length {
165                    width -= suffix_length;
166                    Cow::Borrowed(suffix.text.as_ref())
167                } else {
168                    match suffix.limit {
169                        SuffixLimit::Ignore => Cow::Borrowed(""),
170                        SuffixLimit::Cut => {
171                            width = 0;
172                            cut_str(&suffix.text, set_width)
173                        }
174                        SuffixLimit::Replace(c) => {
175                            width = 0;
176                            Cow::Owned(std::iter::repeat(c).take(set_width).collect())
177                        }
178                    }
179                }
180            }
181            None => Cow::Borrowed(""),
182        };
183
184        let (count_rows, count_cols) = table.shape();
185        for pos in entity.iter(count_rows, count_cols) {
186            let cell_width = table.get_records().get_width(pos, &width_ctrl);
187            if set_width >= cell_width {
188                continue;
189            }
190
191            let suffix_color_try_keeping;
192            #[cfg(not(feature = "color"))]
193            {
194                suffix_color_try_keeping = false;
195            }
196            #[cfg(feature = "color")]
197            {
198                suffix_color_try_keeping = self.suffix.as_ref().map_or(false, |s| s.try_color);
199            }
200
201            let records = table.get_records();
202            let text = records.get_text(pos);
203            // todo: Think about it.
204            //       We could eliminate this allcation if we would be allowed to cut '\t' with unknown characters.
205            //       Currently we don't do that.
206            let text = papergrid::util::replace_tab(text, table.get_config().get_tab_width());
207            let text = truncate_text(&text, width, set_width, &suffix, suffix_color_try_keeping)
208                .into_owned();
209
210            let records = table.get_records_mut();
211            records.set(pos, text, &width_ctrl);
212        }
213
214        table.destroy_width_cache();
215    }
216}
217
218impl<W, P, R> TableOption<R> for Truncate<'_, W, P>
219where
220    W: Measurment<Width>,
221    P: Peaker,
222    R: Records + RecordsMut<String>,
223{
224    fn change(&mut self, table: &mut Table<R>) {
225        if table.is_empty() {
226            return;
227        }
228
229        let width = self.width.measure(table.get_records(), table.get_config());
230        let (widths, total_width) =
231            get_table_widths_with_total(table.get_records(), table.get_config());
232        if total_width <= width {
233            return;
234        }
235
236        let suffix = self.suffix.as_ref().map(|s| TruncateSuffix {
237            limit: s.limit,
238            text: Cow::Borrowed(&s.text),
239            #[cfg(feature = "color")]
240            try_color: s.try_color,
241        });
242
243        truncate_total_width(table, widths, total_width, width, suffix, P::create());
244    }
245}
246
247fn truncate_text<'a>(
248    content: &'a str,
249    width: usize,
250    original_width: usize,
251    suffix: &'a str,
252    _suffix_color_try_keeping: bool,
253) -> Cow<'a, str> {
254    if width == 0 {
255        if original_width == 0 {
256            Cow::Borrowed("")
257        } else {
258            Cow::Borrowed(suffix)
259        }
260    } else {
261        let content = cut_str(content, width);
262
263        if suffix.is_empty() {
264            content
265        } else {
266            #[cfg(feature = "color")]
267            {
268                if _suffix_color_try_keeping {
269                    if let Some(clr) = ansi_str::get_blocks(&content).last() {
270                        if clr.has_ansi() {
271                            Cow::Owned(format!("{}{}{}{}", content, clr.start(), suffix, clr.end()))
272                        } else {
273                            let mut content = content.into_owned();
274                            content.push_str(suffix);
275                            Cow::Owned(content)
276                        }
277                    } else {
278                        let mut content = content.into_owned();
279                        content.push_str(suffix);
280                        Cow::Owned(content)
281                    }
282                } else {
283                    let mut content = content.into_owned();
284                    content.push_str(suffix);
285                    Cow::Owned(content)
286                }
287            }
288
289            #[cfg(not(feature = "color"))]
290            {
291                let mut content = content.into_owned();
292                content.push_str(suffix);
293                Cow::Owned(content)
294            }
295        }
296    }
297}
298
299pub(crate) fn get_decrease_cell_list(
300    cfg: &GridConfig,
301    widths: &[usize],
302    min_widths: &[usize],
303    (count_rows, count_cols): (usize, usize),
304) -> Vec<((usize, usize), usize)> {
305    let mut points = Vec::new();
306    (0..count_cols).for_each(|col| {
307        (0..count_rows)
308            .filter(|&row| cfg.is_cell_visible((row, col), (count_rows, count_cols)))
309            .for_each(|row| {
310                let (width, width_min) =
311                    match cfg.get_column_span((row, col), (count_rows, count_cols)) {
312                        Some(span) => {
313                            let width = (col..col + span).map(|i| widths[i]).sum::<usize>();
314                            let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>();
315                            let count_borders = count_borders(cfg, col, col + span, count_cols);
316                            (width + count_borders, min_width + count_borders)
317                        }
318                        None => (widths[col], min_widths[col]),
319                    };
320
321                if width >= width_min {
322                    let padding = cfg.get_padding((row, col).into());
323                    let width = width.saturating_sub(padding.left.size + padding.right.size);
324
325                    points.push(((row, col), width));
326                }
327            });
328    });
329
330    points
331}
332
333pub(crate) fn decrease_widths<F>(
334    widths: &mut [usize],
335    min_widths: &[usize],
336    total_width: usize,
337    mut width: usize,
338    mut peeaker: F,
339) where
340    F: Peaker,
341{
342    let mut empty_list = 0;
343    for col in 0..widths.len() {
344        if widths[col] == 0 || widths[col] <= min_widths[col] {
345            empty_list += 1;
346        }
347    }
348
349    while width != total_width {
350        if empty_list == widths.len() {
351            break;
352        }
353
354        let col = match peeaker.peak(min_widths, widths) {
355            Some(col) => col,
356            None => break,
357        };
358
359        if widths[col] == 0 || widths[col] <= min_widths[col] {
360            continue;
361        }
362
363        widths[col] -= 1;
364
365        if widths[col] == 0 || widths[col] <= min_widths[col] {
366            empty_list += 1;
367        }
368
369        width += 1;
370    }
371}
372
373fn truncate_total_width<P, R>(
374    table: &mut Table<R>,
375    mut widths: Vec<usize>,
376    widths_total: usize,
377    width: usize,
378    suffix: Option<TruncateSuffix<'_>>,
379    priority: P,
380) where
381    P: Peaker,
382    R: Records + RecordsMut<String>,
383{
384    let (count_rows, count_cols) = table.shape();
385    let cfg = table.get_config();
386    let min_widths = get_table_widths(EmptyRecords::new(count_rows, count_cols), cfg);
387
388    decrease_widths(&mut widths, &min_widths, widths_total, width, priority);
389
390    let points = get_decrease_cell_list(cfg, &widths, &min_widths, (count_rows, count_cols));
391
392    let mut truncate = Truncate::new(0);
393    truncate.suffix = suffix;
394    for ((row, col), width) in points {
395        truncate.width = width;
396        truncate.change_cell(table, (row, col).into());
397    }
398
399    table.destroy_width_cache();
400    table.destroy_height_cache();
401    table.cache_width(widths);
402}
403
404#[cfg(feature = "color")]
405#[cfg(test)]
406mod tests {
407    use owo_colors::{colors::Yellow, OwoColorize};
408    use papergrid::util::cut_str;
409
410    #[test]
411    fn test_color_strip() {
412        let s = "Collored string"
413            .fg::<Yellow>()
414            .on_truecolor(12, 200, 100)
415            .blink()
416            .to_string();
417        assert_eq!(
418            cut_str(&s, 1),
419            "\u{1b}[5m\u{1b}[48;2;12;200;100m\u{1b}[33mC\u{1b}[25m\u{1b}[39m\u{1b}[49m"
420        )
421    }
422}