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,
72 Always,
74 RowCount(u64),
76 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,
131 Never,
133 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
159pub enum TrimStrategy {
160 Wrap {
164 try_to_keep_words: bool,
167 },
168 Truncate {
171 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}