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