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