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