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