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,
70 Always,
72 RowCount(u64),
74 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,
129 Never,
131 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157pub enum TrimStrategy {
158 Wrap {
162 try_to_keep_words: bool,
165 },
166 Truncate {
169 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}