nu_protocol/config/
table.rs

1use super::{config_update_string_enum, prelude::*};
2use crate as nu_protocol;
3
4#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
5pub enum TableMode {
6    Basic,
7    Thin,
8    Light,
9    Compact,
10    WithLove,
11    CompactDouble,
12    #[default]
13    Rounded,
14    Reinforced,
15    Heavy,
16    None,
17    Psql,
18    Markdown,
19    Dots,
20    Restructured,
21    AsciiRounded,
22    BasicCompact,
23}
24
25impl FromStr for TableMode {
26    type Err = &'static str;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s.to_ascii_lowercase().as_str() {
30            "basic" => Ok(Self::Basic),
31            "thin" => Ok(Self::Thin),
32            "light" => Ok(Self::Light),
33            "compact" => Ok(Self::Compact),
34            "with_love" => Ok(Self::WithLove),
35            "compact_double" => Ok(Self::CompactDouble),
36            "default" => Ok(TableMode::default()),
37            "rounded" => Ok(Self::Rounded),
38            "reinforced" => Ok(Self::Reinforced),
39            "heavy" => Ok(Self::Heavy),
40            "none" => Ok(Self::None),
41            "psql" => Ok(Self::Psql),
42            "markdown" => Ok(Self::Markdown),
43            "dots" => Ok(Self::Dots),
44            "restructured" => Ok(Self::Restructured),
45            "ascii_rounded" => Ok(Self::AsciiRounded),
46            "basic_compact" => Ok(Self::BasicCompact),
47            _ => Err("'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', or 'basic_compact'"),
48        }
49    }
50}
51
52impl UpdateFromValue for TableMode {
53    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
54        config_update_string_enum(self, value, path, errors)
55    }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub enum FooterMode {
60    /// Never show the footer
61    Never,
62    /// Always show the footer
63    Always,
64    /// Only show the footer if there are more than RowCount rows
65    RowCount(u64),
66    /// Calculate the screen height and row count, if screen height is larger than row count, don't show footer
67    Auto,
68}
69
70impl FromStr for FooterMode {
71    type Err = &'static str;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        match s.to_ascii_lowercase().as_str() {
75            "always" => Ok(FooterMode::Always),
76            "never" => Ok(FooterMode::Never),
77            "auto" => Ok(FooterMode::Auto),
78            _ => Err("'never', 'always', 'auto', or int"),
79        }
80    }
81}
82
83impl UpdateFromValue for FooterMode {
84    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
85        match value {
86            Value::String { val, .. } => match val.parse() {
87                Ok(val) => *self = val,
88                Err(err) => errors.invalid_value(path, err.to_string(), value),
89            },
90            &Value::Int { val, .. } => {
91                if val >= 0 {
92                    *self = Self::RowCount(val as u64);
93                } else {
94                    errors.invalid_value(path, "a non-negative integer", value);
95                }
96            }
97            _ => errors.type_mismatch(
98                path,
99                Type::custom("'never', 'always', 'auto', or int"),
100                value,
101            ),
102        }
103    }
104}
105
106impl IntoValue for FooterMode {
107    fn into_value(self, span: Span) -> Value {
108        match self {
109            FooterMode::Always => "always".into_value(span),
110            FooterMode::Never => "never".into_value(span),
111            FooterMode::Auto => "auto".into_value(span),
112            FooterMode::RowCount(c) => (c as i64).into_value(span),
113        }
114    }
115}
116
117#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
118pub enum TableIndexMode {
119    /// Always show indexes
120    Always,
121    /// Never show indexes
122    Never,
123    /// Show indexes when a table has "index" column
124    Auto,
125}
126
127impl FromStr for TableIndexMode {
128    type Err = &'static str;
129
130    fn from_str(s: &str) -> Result<Self, Self::Err> {
131        match s.to_ascii_lowercase().as_str() {
132            "always" => Ok(TableIndexMode::Always),
133            "never" => Ok(TableIndexMode::Never),
134            "auto" => Ok(TableIndexMode::Auto),
135            _ => Err("'never', 'always' or 'auto'"),
136        }
137    }
138}
139
140impl UpdateFromValue for TableIndexMode {
141    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
142        config_update_string_enum(self, value, path, errors)
143    }
144}
145
146/// A Table view configuration, for a situation where
147/// we need to limit cell width in order to adjust for a terminal size.
148#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
149pub enum TrimStrategy {
150    /// Wrapping strategy.
151    ///
152    /// It it's similar to original nu_table, strategy.
153    Wrap {
154        /// A flag which indicates whether is it necessary to try
155        /// to keep word boundaries.
156        try_to_keep_words: bool,
157    },
158    /// Truncating strategy, where we just cut the string.
159    /// And append the suffix if applicable.
160    Truncate {
161        /// Suffix which can be appended to a truncated string after being cut.
162        ///
163        /// It will be applied only when there's enough room for it.
164        /// For example in case where a cell width must be 12 chars, but
165        /// the suffix takes 13 chars it won't be used.
166        suffix: Option<String>,
167    },
168}
169
170impl TrimStrategy {
171    pub fn wrap(dont_split_words: bool) -> Self {
172        Self::Wrap {
173            try_to_keep_words: dont_split_words,
174        }
175    }
176
177    pub fn truncate(suffix: Option<String>) -> Self {
178        Self::Truncate { suffix }
179    }
180}
181
182impl Default for TrimStrategy {
183    fn default() -> Self {
184        Self::Wrap {
185            try_to_keep_words: true,
186        }
187    }
188}
189
190impl IntoValue for TrimStrategy {
191    fn into_value(self, span: Span) -> Value {
192        match self {
193            TrimStrategy::Wrap { try_to_keep_words } => {
194                record! {
195                    "methodology" => "wrapping".into_value(span),
196                    "wrapping_try_keep_words" => try_to_keep_words.into_value(span),
197                }
198            }
199            TrimStrategy::Truncate { suffix } => {
200                record! {
201                    "methodology" => "truncating".into_value(span),
202                    "truncating_suffix" => suffix.into_value(span),
203                }
204            }
205        }
206        .into_value(span)
207    }
208}
209
210impl UpdateFromValue for TrimStrategy {
211    fn update<'a>(
212        &mut self,
213        value: &'a Value,
214        path: &mut ConfigPath<'a>,
215        errors: &mut ConfigErrors,
216    ) {
217        let Value::Record { val: record, .. } = value else {
218            errors.type_mismatch(path, Type::record(), value);
219            return;
220        };
221
222        let Some(methodology) = record.get("methodology") else {
223            errors.missing_column(path, "methodology", value.span());
224            return;
225        };
226
227        match methodology.as_str() {
228            Ok("wrapping") => {
229                let mut try_to_keep_words = if let &mut Self::Wrap { try_to_keep_words } = self {
230                    try_to_keep_words
231                } else {
232                    false
233                };
234                for (col, val) in record.iter() {
235                    let path = &mut path.push(col);
236                    match col.as_str() {
237                        "wrapping_try_keep_words" => try_to_keep_words.update(val, path, errors),
238                        "methodology" | "truncating_suffix" => (),
239                        _ => errors.unknown_option(path, val),
240                    }
241                }
242                *self = Self::Wrap { try_to_keep_words };
243            }
244            Ok("truncating") => {
245                let mut suffix = if let Self::Truncate { suffix } = self {
246                    suffix.take()
247                } else {
248                    None
249                };
250                for (col, val) in record.iter() {
251                    let path = &mut path.push(col);
252                    match col.as_str() {
253                        "truncating_suffix" => match val {
254                            Value::Nothing { .. } => suffix = None,
255                            Value::String { val, .. } => suffix = Some(val.clone()),
256                            _ => errors.type_mismatch(path, Type::String, val),
257                        },
258                        "methodology" | "wrapping_try_keep_words" => (),
259                        _ => errors.unknown_option(path, val),
260                    }
261                }
262                *self = Self::Truncate { suffix };
263            }
264            Ok(_) => errors.invalid_value(
265                &path.push("methodology"),
266                "'wrapping' or 'truncating'",
267                methodology,
268            ),
269            Err(_) => errors.type_mismatch(&path.push("methodology"), Type::String, methodology),
270        }
271    }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
275pub struct TableIndent {
276    pub left: usize,
277    pub right: usize,
278}
279
280impl TableIndent {
281    pub fn new(left: usize, right: usize) -> Self {
282        Self { left, right }
283    }
284}
285
286impl IntoValue for TableIndent {
287    fn into_value(self, span: Span) -> Value {
288        record! {
289            "left" => (self.left as i64).into_value(span),
290            "right" => (self.right as i64).into_value(span),
291        }
292        .into_value(span)
293    }
294}
295
296impl Default for TableIndent {
297    fn default() -> Self {
298        Self { left: 1, right: 1 }
299    }
300}
301
302impl UpdateFromValue for TableIndent {
303    fn update<'a>(
304        &mut self,
305        value: &'a Value,
306        path: &mut ConfigPath<'a>,
307        errors: &mut ConfigErrors,
308    ) {
309        match value {
310            &Value::Int { val, .. } => {
311                if let Ok(val) = val.try_into() {
312                    self.left = val;
313                    self.right = val;
314                } else {
315                    errors.invalid_value(path, "a non-negative integer", value);
316                }
317            }
318            Value::Record { val: record, .. } => {
319                for (col, val) in record.iter() {
320                    let path = &mut path.push(col);
321                    match col.as_str() {
322                        "left" => self.left.update(val, path, errors),
323                        "right" => self.right.update(val, path, errors),
324                        _ => errors.unknown_option(path, val),
325                    }
326                }
327            }
328            _ => errors.type_mismatch(path, Type::custom("int or record"), value),
329        }
330    }
331}
332
333#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
334pub struct TableConfig {
335    pub mode: TableMode,
336    pub index_mode: TableIndexMode,
337    pub show_empty: bool,
338    pub padding: TableIndent,
339    pub trim: TrimStrategy,
340    pub header_on_separator: bool,
341    pub abbreviated_row_count: Option<usize>,
342    pub footer_inheritance: bool,
343}
344
345impl IntoValue for TableConfig {
346    fn into_value(self, span: Span) -> Value {
347        let abbv_count = self
348            .abbreviated_row_count
349            .map(|t| t as i64)
350            .into_value(span);
351
352        record! {
353            "mode" => self.mode.into_value(span),
354            "index_mode" => self.index_mode.into_value(span),
355            "show_empty" => self.show_empty.into_value(span),
356            "padding" => self.padding.into_value(span),
357            "trim" => self.trim.into_value(span),
358            "header_on_separator" => self.header_on_separator.into_value(span),
359            "abbreviated_row_count" => abbv_count,
360            "footer_inheritance" => self.footer_inheritance.into_value(span),
361        }
362        .into_value(span)
363    }
364}
365
366impl Default for TableConfig {
367    fn default() -> Self {
368        Self {
369            mode: TableMode::Rounded,
370            index_mode: TableIndexMode::Always,
371            show_empty: true,
372            trim: TrimStrategy::default(),
373            header_on_separator: false,
374            padding: TableIndent::default(),
375            abbreviated_row_count: None,
376            footer_inheritance: false,
377        }
378    }
379}
380
381impl UpdateFromValue for TableConfig {
382    fn update<'a>(
383        &mut self,
384        value: &'a Value,
385        path: &mut ConfigPath<'a>,
386        errors: &mut ConfigErrors,
387    ) {
388        let Value::Record { val: record, .. } = value else {
389            errors.type_mismatch(path, Type::record(), value);
390            return;
391        };
392
393        for (col, val) in record.iter() {
394            let path = &mut path.push(col);
395            match col.as_str() {
396                "mode" => self.mode.update(val, path, errors),
397                "index_mode" => self.index_mode.update(val, path, errors),
398                "show_empty" => self.show_empty.update(val, path, errors),
399                "trim" => self.trim.update(val, path, errors),
400                "header_on_separator" => self.header_on_separator.update(val, path, errors),
401                "padding" => self.padding.update(val, path, errors),
402                "abbreviated_row_count" => match val {
403                    Value::Nothing { .. } => self.abbreviated_row_count = None,
404                    &Value::Int { val: count, .. } => {
405                        if let Ok(count) = count.try_into() {
406                            self.abbreviated_row_count = Some(count);
407                        } else {
408                            errors.invalid_value(path, "a non-negative integer", val);
409                        }
410                    }
411                    _ => errors.type_mismatch(path, Type::custom("int or nothing"), val),
412                },
413                "footer_inheritance" => self.footer_inheritance.update(val, path, errors),
414                _ => errors.unknown_option(path, val),
415            }
416        }
417    }
418}