tabled/display/
expanded_display.rs

1//! This module contains an [`ExpandedDisplay`] structure which is useful in cases where
2//! a structure has a lot of fields.
3//!
4#![cfg_attr(feature = "derive", doc = "```")]
5#![cfg_attr(not(feature = "derive"), doc = "```ignore")]
6//! use tabled::{Tabled, display::ExpandedDisplay};
7//!
8//! #[derive(Tabled)]
9//! struct Language {
10//!     name: &'static str,
11//!     designed_by: &'static str,
12//!     invented_year: usize,
13//! }
14//!
15//! let languages = vec![
16//!     Language{
17//!         name: "C",
18//!         designed_by: "Dennis Ritchie",
19//!         invented_year: 1972
20//!     },
21//!     Language{
22//!         name: "Rust",
23//!         designed_by: "Graydon Hoare",
24//!         invented_year: 2010
25//!     },
26//!     Language{
27//!         name: "Go",
28//!         designed_by: "Rob Pike",
29//!         invented_year: 2009
30//!     },
31//! ];
32//!
33//! let table = ExpandedDisplay::new(languages).to_string();
34//!
35//! let expected = "-[ RECORD 0 ]-+---------------\n\
36//!                 name          | C\n\
37//!                 designed_by   | Dennis Ritchie\n\
38//!                 invented_year | 1972\n\
39//!                 -[ RECORD 1 ]-+---------------\n\
40//!                 name          | Rust\n\
41//!                 designed_by   | Graydon Hoare\n\
42//!                 invented_year | 2010\n\
43//!                 -[ RECORD 2 ]-+---------------\n\
44//!                 name          | Go\n\
45//!                 designed_by   | Rob Pike\n\
46//!                 invented_year | 2009";
47//!
48//! assert_eq!(table, expected);
49//! ```
50
51use papergrid::util::{cut_str_basic, string_width};
52
53use crate::Tabled;
54
55/// `ExpandedDisplay` display data in a 'expanded display mode' from postgresql.
56/// It may be useful for a large data sets with a lot of fields.
57///
58/// See 'Examples' in <https://www.postgresql.org/docs/current/app-psql.html.>.
59///
60/// It escapes strings to resolve a multi-line ones.
61/// Because of that ANSI sequences will be not be rendered too so colores will not be showed.
62///
63/// ```
64/// use tabled::{display::ExpandedDisplay};
65///
66/// let data = vec!["Hello", "2021"];
67/// let table = ExpandedDisplay::new(&data);
68///
69/// assert_eq!(
70///     table.to_string(),
71///     concat!(
72///         "-[ RECORD 0 ]-\n",
73///         "&str | Hello\n",
74///         "-[ RECORD 1 ]-\n",
75///         "&str | 2021",
76///     )
77/// );
78/// ```
79#[derive(Debug, Clone)]
80pub struct ExpandedDisplay {
81    fields: Vec<String>,
82    records: Vec<Vec<String>>,
83}
84
85impl ExpandedDisplay {
86    /// Creates a new instance of `ExpandedDisplay`
87    pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self
88    where
89        T: Tabled,
90    {
91        let data = iter
92            .into_iter()
93            .map(|i| {
94                i.fields()
95                    .into_iter()
96                    .map(|s| s.escape_debug().to_string())
97                    .collect()
98            })
99            .collect();
100        let header = T::headers()
101            .into_iter()
102            .map(|s| s.escape_debug().to_string())
103            .collect();
104
105        Self {
106            records: data,
107            fields: header,
108        }
109    }
110
111    /// Truncates table to a set width value for a table.
112    /// It returns a success inticator, where `false` means it's not possible to set the table width,
113    /// because of the given arguments.
114    ///
115    /// It tries to not affect fields, but if there's no enough space all records will be deleted and fields will be cut.
116    ///
117    /// The minimum width is 14.
118    pub fn truncate(&mut self, max: usize, suffix: &str) -> bool {
119        // -[ RECORD 0 ]-
120        let teplate_width = self.records.len().to_string().len() + 13;
121        let min_width = teplate_width;
122        if max < min_width {
123            return false;
124        }
125
126        let suffix_width = string_width(suffix);
127        if max < suffix_width {
128            return false;
129        }
130
131        let max = max - suffix_width;
132
133        let fields_max_width = self
134            .fields
135            .iter()
136            .map(|s| string_width(s))
137            .max()
138            .unwrap_or_default();
139
140        // 3 is a space for ' | '
141        let fields_affected = max < fields_max_width + 3;
142        if fields_affected {
143            if max < 3 {
144                return false;
145            }
146
147            let max = max - 3;
148
149            if max < suffix_width {
150                return false;
151            }
152
153            let max = max - suffix_width;
154
155            truncate_fields(&mut self.fields, max, suffix);
156            truncate_records(&mut self.records, 0, suffix);
157        } else {
158            let max = max - fields_max_width - 3 - suffix_width;
159            truncate_records(&mut self.records, max, suffix);
160        }
161
162        true
163    }
164}
165
166impl std::fmt::Display for ExpandedDisplay {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        if self.records.is_empty() {
169            return Ok(());
170        }
171
172        // It's possible that field|header can be a multiline string so
173        // we escape it and trim \" chars.
174        let fields = self.fields.iter().collect::<Vec<_>>();
175
176        let max_field_width = fields
177            .iter()
178            .map(|s| string_width(s))
179            .max()
180            .unwrap_or_default();
181
182        let max_values_length = self
183            .records
184            .iter()
185            .map(|record| record.iter().map(|s| string_width(s)).max())
186            .max()
187            .unwrap_or_default()
188            .unwrap_or_default();
189
190        for (i, records) in self.records.iter().enumerate() {
191            write_header_template(f, i, max_field_width, max_values_length)?;
192
193            for (value, field) in records.iter().zip(fields.iter()) {
194                writeln!(f)?;
195                write_record(f, field, value, max_field_width)?;
196            }
197
198            let is_last_record = i + 1 == self.records.len();
199            if !is_last_record {
200                writeln!(f)?;
201            }
202        }
203
204        Ok(())
205    }
206}
207
208fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) {
209    for fields in records {
210        truncate_fields(fields, max_width, suffix);
211    }
212}
213
214fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) {
215    for text in records {
216        truncate(text, max_width, suffix);
217    }
218}
219
220fn write_header_template(
221    f: &mut std::fmt::Formatter<'_>,
222    index: usize,
223    max_field_width: usize,
224    max_values_length: usize,
225) -> std::fmt::Result {
226    let mut template = format!("-[ RECORD {} ]-", index);
227    let default_template_length = template.len();
228
229    // 3 - is responsible for ' | ' formatting
230    let max_line_width = std::cmp::max(
231        max_field_width + 3 + max_values_length,
232        default_template_length,
233    );
234    let rest_to_print = max_line_width - default_template_length;
235    if rest_to_print > 0 {
236        // + 1 is a space after field name and we get a next pos so its +2
237        if max_field_width + 2 > default_template_length {
238            let part1 = (max_field_width + 1) - default_template_length;
239            let part2 = rest_to_print - part1 - 1;
240
241            template.extend(
242                std::iter::repeat('-')
243                    .take(part1)
244                    .chain(std::iter::once('+'))
245                    .chain(std::iter::repeat('-').take(part2)),
246            );
247        } else {
248            template.extend(std::iter::repeat('-').take(rest_to_print));
249        }
250    }
251
252    write!(f, "{}", template)?;
253
254    Ok(())
255}
256
257fn write_record(
258    f: &mut std::fmt::Formatter<'_>,
259    field: &str,
260    value: &str,
261    max_field_width: usize,
262) -> std::fmt::Result {
263    write!(f, "{:width$} | {}", field, value, width = max_field_width)
264}
265
266fn truncate(text: &mut String, max: usize, suffix: &str) {
267    let original_len = text.len();
268
269    if max == 0 || text.is_empty() {
270        *text = String::new();
271    } else {
272        *text = cut_str_basic(text, max).into_owned();
273    }
274
275    let cut_was_done = text.len() < original_len;
276    if !suffix.is_empty() && cut_was_done {
277        text.push_str(suffix);
278    }
279}