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 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 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 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,
117 Always,
119 RowCount(u64),
121 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,
176 Never,
178 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204pub enum TrimStrategy {
205 Wrap {
209 try_to_keep_words: bool,
212 },
213 Truncate {
216 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}