Skip to main content

nu_protocol/config/
table.rs

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