Skip to main content

nu_command/viewers/
table.rs

1// todo: (refactoring) limit get_config() usage to 1 call
2//        overall reduce the redundant calls to StyleComputer etc.
3//        the goal is to configure it once...
4
5use std::{collections::VecDeque, io::Read, path::PathBuf, str::FromStr, time::Duration};
6
7use devicons::icon_for_file;
8use lscolors::{LsColors, Style};
9use nu_color_config::lookup_ansi_color_style;
10use nu_utils::time::Instant;
11use url::Url;
12
13use nu_color_config::{StyleComputer, TextStyle, color_from_hex};
14use nu_engine::{command_prelude::*, env_to_string};
15use nu_path::form::Absolute;
16use nu_pretty_hex::{HexConfig, HexStyles};
17use nu_protocol::{
18    ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals,
19    TABLE_WIDTH_PRIORITY_COLUMNS_METADATA_KEY, TableMode, ValueIterator,
20    shell_error::{bridge::ShellErrorBridge, io::IoError},
21};
22use nu_table::{
23    CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult, TableOpts, TableOutput,
24    common::configure_table,
25};
26use nu_utils::{get_ls_colors, terminal_size};
27
28type ShellResult<T> = Result<T, ShellError>;
29type NuPathBuf = nu_path::PathBuf<Absolute>;
30type NuPath = nu_path::Path<Absolute>;
31
32const DEFAULT_TABLE_WIDTH: usize = 80;
33
34#[derive(Clone)]
35pub struct Table;
36
37//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one.
38impl Command for Table {
39    fn name(&self) -> &str {
40        "table"
41    }
42
43    fn description(&self) -> &str {
44        "Render the table."
45    }
46
47    fn extra_description(&self) -> &str {
48        "If the table contains a column called 'index', this column is used as the table index instead of the usual continuous index."
49    }
50
51    fn search_terms(&self) -> Vec<&str> {
52        vec!["display", "render"]
53    }
54
55    fn signature(&self) -> Signature {
56        Signature::build("table")
57            .input_output_types(vec![(Type::Any, Type::Any)])
58            // TODO: make this more precise: what turns into string and what into raw stream
59            .param(
60                Flag::new("theme")
61                    .short('t')
62                    .arg(SyntaxShape::String)
63                    .desc("Set a table mode/theme.")
64                    .completion(Completion::new_list(SUPPORTED_TABLE_MODES)),
65            )
66            .named(
67                "index",
68                SyntaxShape::Any,
69                "Enable (true) or disable (false) the #/index column or set the starting index.",
70                Some('i'),
71            )
72            .named(
73                "width",
74                SyntaxShape::Int,
75                "Number of terminal columns wide (not output columns).",
76                Some('w'),
77            )
78            .switch(
79                "expand",
80                "Expand the table structure in a light mode.",
81                Some('e'),
82            )
83            .named(
84                "expand-deep",
85                SyntaxShape::Int,
86                "An expand limit of recursion which will take place, must be used with --expand.",
87                Some('d'),
88            )
89            .switch("flatten", "Flatten simple arrays.", None)
90            .named(
91                "flatten-separator",
92                SyntaxShape::String,
93                "Sets a separator when 'flatten' is used.",
94                None,
95            )
96            .switch(
97                "collapse",
98                "Expand the table structure in collapse mode.\nBe aware collapse mode currently doesn't support width control.",
99                Some('c'),
100            )
101            .named(
102                "abbreviated",
103                SyntaxShape::Int,
104                "Abbreviate the data in the table by truncating the middle part and only showing amount provided on top and bottom.",
105                Some('a'),
106            )
107            .switch("list", "List available table modes/themes.", Some('l'))
108            .switch("icons", "Add icons to file paths in tables.", Some('o'),
109            )
110            .category(Category::Viewers)
111    }
112
113    fn run(
114        &self,
115        engine_state: &EngineState,
116        stack: &mut Stack,
117        call: &Call,
118        input: PipelineData,
119    ) -> ShellResult<PipelineData> {
120        let list_themes: bool = call.has_flag(engine_state, stack, "list")?;
121        // if list argument is present we just need to return a list of supported table themes
122        if list_themes {
123            let val = Value::list(supported_table_modes(), Span::test_data());
124            return Ok(val.into_pipeline_data());
125        }
126
127        let input = CmdInput::parse(engine_state, stack, call, input)?;
128
129        // reset vt processing, aka ansi because ill behaved externals can break it
130        #[cfg(windows)]
131        {
132            let _ = nu_utils::enable_vt_processing();
133        }
134
135        handle_table_command(input)
136    }
137
138    fn examples(&self) -> Vec<Example<'_>> {
139        vec![
140            Example {
141                description: "List the files in current directory, with indexes starting from 1",
142                example: "ls | table --index 1",
143                result: None,
144            },
145            Example {
146                description: "Render data in table view",
147                example: "[[a b]; [1 2] [3 4]] | table",
148                result: Some(Value::test_list(vec![
149                    Value::test_record(record! {
150                        "a" =>  Value::test_int(1),
151                        "b" =>  Value::test_int(2),
152                    }),
153                    Value::test_record(record! {
154                        "a" =>  Value::test_int(3),
155                        "b" =>  Value::test_int(4),
156                    }),
157                ])),
158            },
159            Example {
160                description: "Render data in table view (expanded)",
161                example: "[[a b]; [1 2] [3 [4 4]]] | table --expand",
162                result: Some(Value::test_list(vec![
163                    Value::test_record(record! {
164                        "a" =>  Value::test_int(1),
165                        "b" =>  Value::test_int(2),
166                    }),
167                    Value::test_record(record! {
168                        "a" =>  Value::test_int(3),
169                        "b" =>  Value::test_list(vec![
170                            Value::test_int(4),
171                            Value::test_int(4),
172                        ])
173                    }),
174                ])),
175            },
176            Example {
177                description: "Render data in table view (collapsed)",
178                example: "[[a b]; [1 2] [3 [4 4]]] | table --collapse",
179                result: Some(Value::test_list(vec![
180                    Value::test_record(record! {
181                        "a" =>  Value::test_int(1),
182                        "b" =>  Value::test_int(2),
183                    }),
184                    Value::test_record(record! {
185                        "a" =>  Value::test_int(3),
186                        "b" =>  Value::test_list(vec![
187                            Value::test_int(4),
188                            Value::test_int(4),
189                        ])
190                    }),
191                ])),
192            },
193            Example {
194                description: "Change the table theme to the specified theme for a single run",
195                example: "[[a b]; [1 2] [3 [4 4]]] | table --theme basic",
196                result: None,
197            },
198            Example {
199                description: "Force showing of the #/index column for a single run",
200                example: "[[a b]; [1 2] [3 [4 4]]] | table -i true",
201                result: None,
202            },
203            Example {
204                description: "Set the starting number of the #/index column to 100 for a single run",
205                example: "[[a b]; [1 2] [3 [4 4]]] | table -i 100",
206                result: None,
207            },
208            Example {
209                description: "Force hiding of the #/index column for a single run",
210                example: "[[a b]; [1 2] [3 [4 4]]] | table -i false",
211                result: None,
212            },
213        ]
214    }
215}
216
217pub(crate) fn render_value_as_plain_table_text(
218    engine_state: &EngineState,
219    stack: &mut Stack,
220    value: Value,
221    span: Span,
222) -> ShellResult<String> {
223    let call = Call::new(span);
224    let input = value.into_pipeline_data();
225    let input = CmdInput::parse(engine_state, stack, &call, input)?;
226    let output = handle_table_command(input)?;
227    let output = output.into_value(span)?;
228    let config = stack.get_config(engine_state);
229
230    let text = match output {
231        Value::String { val, .. } => val,
232        other => other.to_expanded_string("", &config),
233    };
234
235    Ok(nu_utils::strip_ansi_string_likely(text))
236}
237
238#[derive(Debug, Clone)]
239struct TableConfig {
240    view: TableView,
241    width: usize,
242    theme: TableMode,
243    abbreviation: Option<usize>,
244    index: Option<usize>,
245    use_ansi_coloring: bool,
246    icons: bool,
247    hex_styles: HexStyles,
248    width_priority_columns: Vec<String>,
249}
250
251#[derive(Debug, Clone)]
252enum TableView {
253    General,
254    Collapsed,
255    Expanded {
256        limit: Option<usize>,
257        flatten: bool,
258        flatten_separator: Option<String>,
259    },
260}
261
262struct CLIArgs {
263    width: Option<i64>,
264    abbreviation: Option<usize>,
265    theme: TableMode,
266    expand: bool,
267    expand_limit: Option<usize>,
268    expand_flatten: bool,
269    expand_flatten_separator: Option<String>,
270    collapse: bool,
271    index: Option<usize>,
272    use_ansi_coloring: bool,
273    icons: bool,
274}
275
276fn parse_table_config(
277    call: &Call,
278    state: &EngineState,
279    stack: &mut Stack,
280) -> ShellResult<TableConfig> {
281    let args @ CLIArgs {
282        abbreviation,
283        theme,
284        index,
285        use_ansi_coloring,
286        icons,
287        ..
288    } = get_cli_args(call, state, stack)?;
289
290    let table_view = get_table_view(&args);
291    let term_width = get_table_width(args.width);
292    let hex_styles = get_hex_styles(state, stack);
293
294    let cfg = TableConfig {
295        view: table_view,
296        width: term_width,
297        theme,
298        abbreviation,
299        index,
300        use_ansi_coloring,
301        icons,
302        hex_styles,
303        width_priority_columns: vec![],
304    };
305
306    Ok(cfg)
307}
308
309fn get_table_view(args: &CLIArgs) -> TableView {
310    match (args.expand, args.collapse) {
311        (false, false) => TableView::General,
312        (_, true) => TableView::Collapsed,
313        (true, _) => TableView::Expanded {
314            limit: args.expand_limit,
315            flatten: args.expand_flatten,
316            flatten_separator: args.expand_flatten_separator.clone(),
317        },
318    }
319}
320
321fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
322    let width: Option<i64> = call.get_flag(state, stack, "width")?;
323    let expand: bool = call.has_flag(state, stack, "expand")?;
324    let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
325    let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
326    let expand_flatten_separator: Option<String> =
327        call.get_flag(state, stack, "flatten-separator")?;
328    let collapse: bool = call.has_flag(state, stack, "collapse")?;
329    let abbreviation: Option<usize> = call
330        .get_flag(state, stack, "abbreviated")?
331        .or_else(|| stack.get_config(state).table.abbreviated_row_count);
332    let theme =
333        get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
334    let index = get_index_flag(call, state, stack)?;
335    let icons = call.has_flag(state, stack, "icons")?;
336
337    let use_ansi_coloring = stack.get_config(state).use_ansi_coloring.get(state);
338
339    Ok(CLIArgs {
340        theme,
341        abbreviation,
342        collapse,
343        expand,
344        expand_limit,
345        expand_flatten,
346        expand_flatten_separator,
347        width,
348        index,
349        use_ansi_coloring,
350        icons,
351    })
352}
353
354fn get_index_flag(
355    call: &Call,
356    state: &EngineState,
357    stack: &mut Stack,
358) -> ShellResult<Option<usize>> {
359    let index: Option<Value> = call.get_flag(state, stack, "index")?;
360    let value = match index {
361        Some(value) => value,
362        None => return Ok(Some(0)),
363    };
364    let span = value.span();
365
366    match value {
367        Value::Bool { val, .. } => {
368            if val {
369                Ok(Some(0))
370            } else {
371                Ok(None)
372            }
373        }
374        Value::Int { val, .. } => {
375            if val < 0 {
376                Err(ShellError::UnsupportedInput {
377                    msg: String::from("got a negative integer"),
378                    input: val.to_string(),
379                    msg_span: call.span(),
380                    input_span: span,
381                })
382            } else {
383                Ok(Some(val as usize))
384            }
385        }
386        Value::Nothing { .. } => Ok(Some(0)),
387        _ => Err(ShellError::CantConvert {
388            to_type: String::from("index"),
389            from_type: String::new(),
390            span: call.span(),
391            help: Some(String::from("supported values: [bool, int, nothing]")),
392        }),
393    }
394}
395
396fn get_theme_flag(
397    call: &Call,
398    state: &EngineState,
399    stack: &mut Stack,
400) -> ShellResult<Option<TableMode>> {
401    call.get_flag(state, stack, "theme")?
402        .map(|theme: String| {
403            TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
404                to_type: String::from("theme"),
405                from_type: String::from("string"),
406                span: call.span(),
407                help: Some(format!("{err}, but found '{theme}'.")),
408            })
409        })
410        .transpose()
411}
412
413struct CmdInput<'a> {
414    engine_state: &'a EngineState,
415    stack: &'a mut Stack,
416    call: &'a Call<'a>,
417    data: PipelineData,
418    cfg: TableConfig,
419    cwd: Option<NuPathBuf>,
420}
421
422impl<'a> CmdInput<'a> {
423    fn parse(
424        engine_state: &'a EngineState,
425        stack: &'a mut Stack,
426        call: &'a Call<'a>,
427        data: PipelineData,
428    ) -> ShellResult<Self> {
429        let cfg = parse_table_config(call, engine_state, stack)?;
430        let cwd = get_cwd(engine_state, stack)?;
431
432        Ok(Self {
433            engine_state,
434            stack,
435            call,
436            data,
437            cfg,
438            cwd,
439        })
440    }
441
442    fn get_config(&self) -> std::sync::Arc<Config> {
443        self.stack.get_config(self.engine_state)
444    }
445}
446
447fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
448    let span = input.data.span().unwrap_or(input.call.head);
449    match input.data {
450        // Binary streams should behave as if they really are `binary` data, and printed as hex
451        PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
452            PipelineData::byte_stream(pretty_hex_stream(stream, input.cfg, input.call.head), None),
453        ),
454        PipelineData::ByteStream(..) => Ok(input.data),
455        PipelineData::Value(Value::Binary { val, .. }, ..) => {
456            let signals = input.engine_state.signals().clone();
457            let stream = ByteStream::read_binary(val, input.call.head, signals);
458            Ok(PipelineData::byte_stream(
459                pretty_hex_stream(stream, input.cfg, input.call.head),
460                None,
461            ))
462        }
463        // None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack.
464        PipelineData::Value(Value::List { vals, .. }, metadata) => {
465            let signals = input.engine_state.signals().clone();
466            let stream = ListStream::new(vals.into_iter(), span, signals);
467            input.data = PipelineData::empty();
468
469            handle_row_stream(input, stream, metadata)
470        }
471        PipelineData::ListStream(stream, metadata) => {
472            input.data = PipelineData::empty();
473            handle_row_stream(input, stream, metadata)
474        }
475        PipelineData::Value(Value::Record { val, .. }, metadata) => {
476            input.data = PipelineData::empty();
477            handle_record(input, val.into_owned(), metadata)
478        }
479        PipelineData::Value(Value::Error { error, .. }, ..) => {
480            // Propagate this error outward, so that it goes to stderr
481            // instead of stdout.
482            Err(*error)
483        }
484        PipelineData::Value(Value::Custom { val, .. }, metadata) => {
485            let base_pipeline = val
486                .to_base_value(span)?
487                .into_pipeline_data_with_metadata(metadata);
488            Table.run(input.engine_state, input.stack, input.call, base_pipeline)
489        }
490        PipelineData::Value(Value::Range { val, .. }, metadata) => {
491            let signals = input.engine_state.signals().clone();
492            let stream =
493                ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
494            input.data = PipelineData::empty();
495            handle_row_stream(input, stream, metadata)
496        }
497        x => Ok(x),
498    }
499}
500
501fn pretty_hex_stream(stream: ByteStream, table_cfg: TableConfig, span: Span) -> ByteStream {
502    let mut cfg = HexConfig {
503        // We are going to render the title manually first
504        title: true,
505        // If building on 32-bit, the stream size might be bigger than a usize
506        length: stream.known_size().and_then(|sz| sz.try_into().ok()),
507        styles: table_cfg.hex_styles,
508        ..HexConfig::default()
509    };
510
511    // This won't really work for us
512    debug_assert!(cfg.width > 0, "the default hex config width was zero");
513
514    let mut read_buf = Vec::with_capacity(cfg.width);
515
516    let mut reader = if let Some(reader) = stream.reader() {
517        reader
518    } else {
519        // No stream to read from
520        return ByteStream::read_string("".into(), span, Signals::empty());
521    };
522
523    ByteStream::from_fn(
524        span,
525        Signals::empty(),
526        ByteStreamType::String,
527        move |buffer| {
528            // Turn the buffer into a String we can write to
529            let mut write_buf = std::mem::take(buffer);
530            write_buf.clear();
531            // SAFETY: we just truncated it empty
532            let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
533
534            // Write the title at the beginning
535            if cfg.title {
536                nu_pretty_hex::write_title(&mut write_buf, cfg, table_cfg.use_ansi_coloring)
537                    .expect("format error");
538                cfg.title = false;
539
540                // Put the write_buf back into buffer
541                *buffer = write_buf.into_bytes();
542
543                Ok(true)
544            } else {
545                // Read up to `cfg.width` bytes
546                read_buf.clear();
547                (&mut reader)
548                    .take(cfg.width as u64)
549                    .read_to_end(&mut read_buf)
550                    .map_err(|err| match ShellErrorBridge::try_from(err) {
551                        Ok(ShellErrorBridge(err)) => err,
552                        Err(err) => IoError::new(err, span, None).into(),
553                    })?;
554
555                if !read_buf.is_empty() {
556                    nu_pretty_hex::hex_write(
557                        &mut write_buf,
558                        &read_buf,
559                        cfg,
560                        Some(table_cfg.use_ansi_coloring),
561                    )
562                    .expect("format error");
563                    write_buf.push('\n');
564
565                    // Advance the address offset for next time
566                    cfg.address_offset += read_buf.len();
567
568                    // Put the write_buf back into buffer
569                    *buffer = write_buf.into_bytes();
570
571                    Ok(true)
572                } else {
573                    Ok(false)
574                }
575            }
576        },
577    )
578}
579
580fn handle_record(
581    mut input: CmdInput,
582    mut record: Record,
583    metadata: Option<PipelineMetadata>,
584) -> ShellResult<PipelineData> {
585    let span = input.data.span().unwrap_or(input.call.head);
586
587    if record.is_empty() {
588        let value = create_empty_placeholder(
589            "record",
590            input.cfg.width,
591            input.engine_state,
592            input.stack,
593            input.cfg.use_ansi_coloring,
594        );
595        let value = Value::string(value, span);
596        return Ok(value.into_pipeline_data());
597    };
598
599    if let Some(limit) = input.cfg.abbreviation {
600        record = make_record_abbreviation(record, limit, span);
601    }
602
603    input.cfg.width_priority_columns = get_width_priority_columns(metadata.as_ref());
604
605    let config = input.get_config();
606
607    if let Some(PipelineMetadata {
608        mut path_columns, ..
609    }) = metadata
610    {
611        // Remove duplicates
612        path_columns.sort_unstable();
613        path_columns.dedup();
614
615        let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
616            Some(v) => Some(env_to_string(
617                "LS_COLORS",
618                v,
619                input.engine_state,
620                input.stack,
621            )?),
622            None => None,
623        };
624        let ls_colors = get_ls_colors(ls_colors_env_str);
625
626        for column in &path_columns {
627            if let Some(value) = record.get_mut(column) {
628                let span = value.span();
629                if let Value::String { val, .. } = value
630                    && let Some(val) = render_path_name(
631                        val,
632                        &config,
633                        &ls_colors,
634                        input.cwd.as_deref(),
635                        input.cfg.icons,
636                        span,
637                    )
638                {
639                    *value = val;
640                }
641            }
642        }
643    }
644    let opts = create_table_opts(
645        input.engine_state,
646        input.stack,
647        &config,
648        &input.cfg,
649        span,
650        0,
651    );
652    let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
653
654    let result = match result {
655        Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
656        None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
657    };
658
659    let val = Value::string(result, span);
660    let data = val.into_pipeline_data();
661
662    Ok(data)
663}
664
665fn make_record_abbreviation(mut record: Record, limit: usize, span: Span) -> Record {
666    if record.len() <= limit * 2 + 1 {
667        return record;
668    }
669
670    // TODO: see if the following table builders would be happy with a simple iterator
671    let prev_len = record.len();
672    let mut record_iter = record.into_iter();
673    record = Record::with_capacity(limit * 2 + 1);
674    record.extend(record_iter.by_ref().take(limit));
675    record.push(String::from("..."), Value::string("...", span));
676    record.extend(record_iter.skip(prev_len - 2 * limit));
677    record
678}
679
680fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
681    if signals.interrupted() {
682        "".into()
683    } else {
684        // assume this failed because the table was too wide
685        // TODO: more robust error classification
686        format!("Couldn't fit table into {term_width} columns!")
687    }
688}
689
690fn build_table_kv(
691    record: Record,
692    table_view: TableView,
693    opts: TableOpts<'_>,
694    span: Span,
695) -> StringResult {
696    match table_view {
697        TableView::General => JustTable::kv_table(record, opts),
698        TableView::Expanded {
699            limit,
700            flatten,
701            flatten_separator,
702        } => {
703            let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
704            ExpandedTable::new(limit, flatten, sep).build_map(&record, opts)
705        }
706        TableView::Collapsed => {
707            let value = Value::record(record, span);
708            CollapsedTable::build(value, opts)
709        }
710    }
711}
712
713fn build_table_batch(
714    mut vals: Vec<Value>,
715    view: TableView,
716    opts: TableOpts<'_>,
717    span: Span,
718) -> StringResult {
719    // convert each custom value to its base value so it can be properly
720    // displayed in a table
721    for val in &mut vals {
722        let span = val.span();
723
724        if let Value::Custom { val: custom, .. } = val {
725            *val = custom
726                .to_base_value(span)
727                .or_else(|err| Result::<_, ShellError>::Ok(Value::error(err, span)))
728                .expect("error converting custom value to base value")
729        }
730    }
731
732    match view {
733        TableView::General => JustTable::table(vals, opts),
734        TableView::Expanded {
735            limit,
736            flatten,
737            flatten_separator,
738        } => {
739            let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
740            ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
741        }
742        TableView::Collapsed => {
743            let value = Value::list(vals, span);
744            CollapsedTable::build(value, opts)
745        }
746    }
747}
748
749fn handle_row_stream(
750    mut input: CmdInput<'_>,
751    stream: ListStream,
752    metadata: Option<PipelineMetadata>,
753) -> ShellResult<PipelineData> {
754    input.cfg.width_priority_columns = get_width_priority_columns(metadata.as_ref());
755
756    let cfg = input.get_config();
757
758    let stream = if let Some(metadata) = metadata {
759        let stream = if let PipelineMetadata {
760            data_source: DataSource::HtmlThemes,
761            ..
762        } = &metadata
763        {
764            stream.map(|mut value| {
765                if let Value::Record { val: record, .. } = &mut value {
766                    for (rec_col, rec_val) in record.to_mut().iter_mut() {
767                        // Every column in the HTML theme table except 'name' is colored
768                        if rec_col != "name" {
769                            continue;
770                        }
771                        // Simple routine to grab the hex code, convert to a style,
772                        // then place it in a new Value::String.
773
774                        let span = rec_val.span();
775                        if let Value::String { val, .. } = rec_val {
776                            let s = match color_from_hex(val) {
777                                Ok(c) => match c {
778                                    // .normal() just sets the text foreground color.
779                                    Some(c) => c.normal(),
780                                    None => nu_ansi_term::Style::default(),
781                                },
782                                Err(_) => nu_ansi_term::Style::default(),
783                            };
784                            *rec_val = Value::string(
785                                // Apply the style (ANSI codes) to the string
786                                s.paint(&*val).to_string(),
787                                span,
788                            );
789                        }
790                    }
791                }
792                value
793            })
794        } else {
795            stream
796        };
797
798        let PipelineMetadata {
799            mut path_columns, ..
800        } = metadata;
801
802        // Remove duplicates
803        path_columns.sort_unstable();
804        path_columns.dedup();
805
806        let config = cfg.clone();
807        let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
808            Some(v) => Some(env_to_string(
809                "LS_COLORS",
810                v,
811                input.engine_state,
812                input.stack,
813            )?),
814            None => None,
815        };
816        let ls_colors = get_ls_colors(ls_colors_env_str);
817
818        stream.map(move |mut value| {
819            if let Value::Record { val: record, .. } = &mut value {
820                for column in &path_columns {
821                    if let Some(value) = record.to_mut().get_mut(column) {
822                        let span = value.span();
823                        if let Value::String { val, .. } = value
824                            && let Some(val) = render_path_name(
825                                val,
826                                &config,
827                                &ls_colors,
828                                input.cwd.as_deref(),
829                                input.cfg.icons,
830                                span,
831                            )
832                        {
833                            *value = val;
834                        }
835                    }
836                }
837            }
838            value
839        })
840    } else {
841        stream
842    };
843
844    let paginator = PagingTableCreator::new(
845        input.call.head,
846        stream,
847        // These are passed in as a way to have PagingTable create StyleComputers
848        // for the values it outputs. Because engine_state is passed in, config doesn't need to.
849        input.engine_state.clone(),
850        input.stack.clone(),
851        input.cfg,
852        cfg,
853    );
854    let stream = ByteStream::from_result_iter(
855        paginator,
856        input.call.head,
857        Signals::empty(),
858        ByteStreamType::String,
859    );
860    Ok(PipelineData::byte_stream(stream, None))
861}
862
863fn make_clickable_link(
864    full_path: String,
865    link_name: Option<&str>,
866    show_clickable_links: bool,
867) -> String {
868    // uri's based on this https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
869
870    #[cfg(any(
871        unix,
872        windows,
873        target_os = "redox",
874        target_os = "wasi",
875        target_os = "hermit"
876    ))]
877    if show_clickable_links {
878        format!(
879            "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
880            match Url::from_file_path(full_path.clone()) {
881                Ok(url) => url.to_string(),
882                Err(_) => full_path.clone(),
883            },
884            link_name.unwrap_or(full_path.as_str())
885        )
886    } else {
887        match link_name {
888            Some(link_name) => link_name.to_string(),
889            None => full_path,
890        }
891    }
892
893    #[cfg(not(any(
894        unix,
895        windows,
896        target_os = "redox",
897        target_os = "wasi",
898        target_os = "hermit"
899    )))]
900    match link_name {
901        Some(link_name) => link_name.to_string(),
902        None => full_path,
903    }
904}
905
906struct PagingTableCreator {
907    head: Span,
908    stream: ValueIterator,
909    engine_state: EngineState,
910    stack: Stack,
911    elements_displayed: usize,
912    reached_end: bool,
913    table_config: TableConfig,
914    row_offset: usize,
915    config: std::sync::Arc<Config>,
916}
917
918impl PagingTableCreator {
919    fn new(
920        head: Span,
921        stream: ListStream,
922        engine_state: EngineState,
923        stack: Stack,
924        table_config: TableConfig,
925        config: std::sync::Arc<Config>,
926    ) -> Self {
927        PagingTableCreator {
928            head,
929            stream: stream.into_inner(),
930            engine_state,
931            stack,
932            config,
933            table_config,
934            elements_displayed: 0,
935            reached_end: false,
936            row_offset: 0,
937        }
938    }
939
940    fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
941        if batch.is_empty() {
942            return Ok(None);
943        }
944
945        let opts = self.create_table_opts();
946        build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
947    }
948
949    fn create_table_opts(&self) -> TableOpts<'_> {
950        create_table_opts(
951            &self.engine_state,
952            &self.stack,
953            &self.config,
954            &self.table_config,
955            self.head,
956            self.row_offset,
957        )
958    }
959}
960
961impl Iterator for PagingTableCreator {
962    type Item = ShellResult<Vec<u8>>;
963
964    fn next(&mut self) -> Option<Self::Item> {
965        let batch;
966        let end;
967
968        match self.table_config.abbreviation {
969            Some(abbr) => {
970                (batch, _, end) = stream_collect_abbreviated(
971                    &mut self.stream,
972                    abbr,
973                    self.engine_state.signals(),
974                    self.head,
975                );
976            }
977            None => {
978                // Pull from stream until time runs out or we have enough items
979                (batch, end) = stream_collect(
980                    &mut self.stream,
981                    self.config.table.stream_page_size.get() as usize,
982                    self.config.table.batch_duration,
983                    self.engine_state.signals(),
984                );
985            }
986        }
987
988        let batch_size = batch.len();
989
990        // Count how much elements were displayed and if end of stream was reached
991        self.elements_displayed += batch_size;
992        self.reached_end = self.reached_end || end;
993
994        if batch.is_empty() {
995            // If this iterator has not displayed a single entry and reached its end (no more elements
996            // or interrupted by ctrl+c) display as "empty list"
997            return if self.elements_displayed == 0 && self.reached_end {
998                // Increase elements_displayed by one so on next iteration next branch of this
999                // if else triggers and terminates stream
1000                self.elements_displayed = 1;
1001                let result = create_empty_placeholder(
1002                    "list",
1003                    self.table_config.width,
1004                    &self.engine_state,
1005                    &self.stack,
1006                    self.table_config.use_ansi_coloring,
1007                );
1008                let mut bytes = result.into_bytes();
1009                // Add extra newline if show_empty is enabled
1010                if !bytes.is_empty() {
1011                    bytes.push(b'\n');
1012                }
1013                Some(Ok(bytes))
1014            } else {
1015                None
1016            };
1017        }
1018
1019        let table = self.build_table(batch);
1020
1021        self.row_offset += batch_size;
1022
1023        convert_table_to_output(
1024            table,
1025            self.engine_state.signals(),
1026            self.table_config.width,
1027            self.table_config.use_ansi_coloring,
1028        )
1029    }
1030}
1031
1032fn stream_collect(
1033    stream: impl Iterator<Item = Value>,
1034    size: usize,
1035    batch_duration: Duration,
1036    signals: &Signals,
1037) -> (Vec<Value>, bool) {
1038    let start_time = Instant::now();
1039    let mut end = true;
1040
1041    let mut batch = Vec::with_capacity(size);
1042    for (i, item) in stream.enumerate() {
1043        batch.push(item);
1044
1045        // We buffer until `$env.config.table.batch_duration`, then we send out what we have so far
1046        if (Instant::now() - start_time) >= batch_duration {
1047            end = false;
1048            break;
1049        }
1050
1051        // Or until we reached `$env.config.table.stream_page_size`.
1052        if i + 1 == size {
1053            end = false;
1054            break;
1055        }
1056
1057        if signals.interrupted() {
1058            break;
1059        }
1060    }
1061
1062    (batch, end)
1063}
1064
1065fn stream_collect_abbreviated(
1066    stream: impl Iterator<Item = Value>,
1067    size: usize,
1068    signals: &Signals,
1069    span: Span,
1070) -> (Vec<Value>, usize, bool) {
1071    let mut end = true;
1072    let mut read = 0;
1073    let mut head = Vec::with_capacity(size);
1074    let mut tail = VecDeque::with_capacity(size);
1075
1076    if size == 0 {
1077        return (vec![], 0, false);
1078    }
1079
1080    for item in stream {
1081        read += 1;
1082
1083        if read <= size {
1084            head.push(item);
1085        } else if tail.len() < size {
1086            tail.push_back(item);
1087        } else {
1088            let _ = tail.pop_front();
1089            tail.push_back(item);
1090        }
1091
1092        if signals.interrupted() {
1093            end = false;
1094            break;
1095        }
1096    }
1097
1098    let have_filled_list = head.len() == size && tail.len() == size;
1099    if have_filled_list {
1100        let dummy = get_abbreviated_dummy(&head, &tail, span);
1101        head.insert(size, dummy)
1102    }
1103
1104    head.extend(tail);
1105
1106    (head, read, end)
1107}
1108
1109fn get_abbreviated_dummy(head: &[Value], tail: &VecDeque<Value>, span: Span) -> Value {
1110    let dummy = || Value::string(String::from("..."), span);
1111    let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1112
1113    if is_record_list {
1114        // in case it's a record list we set a default text to each column instead of a single value.
1115        Value::record(
1116            head[0]
1117                .as_record()
1118                .expect("ok")
1119                .columns()
1120                .map(|key| (key.clone(), dummy()))
1121                .collect(),
1122            span,
1123        )
1124    } else {
1125        dummy()
1126    }
1127}
1128
1129fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1130    batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1131}
1132
1133fn render_path_name(
1134    path: &str,
1135    config: &Config,
1136    ls_colors: &LsColors,
1137    cwd: Option<&NuPath>,
1138    icons: bool,
1139    span: Span,
1140) -> Option<Value> {
1141    if !config.ls.use_ls_colors {
1142        return None;
1143    }
1144
1145    let fullpath = match cwd {
1146        Some(cwd) => PathBuf::from(cwd.join(path)),
1147        None => PathBuf::from(path),
1148    };
1149
1150    let stripped_path = nu_utils::strip_ansi_unlikely(path);
1151    let metadata = std::fs::symlink_metadata(fullpath);
1152    let has_metadata = metadata.is_ok();
1153    let style =
1154        ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1155
1156    let file_icon = icon_for_file(path, &None);
1157    let icon_style = lookup_ansi_color_style(file_icon.color);
1158
1159    // clickable links don't work in remote SSH sessions
1160    let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1161    //TODO: Deprecated show_clickable_links_in_ls in favor of shell_integration_osc8
1162    let show_clickable_links = config.ls.clickable_links
1163        && !in_ssh_session
1164        && has_metadata
1165        && config.shell_integration.osc8;
1166
1167    // If there is no style at all set it to use 'default' foreground and background
1168    // colors. This prevents it being colored in tabled as string colors.
1169    // To test this:
1170    //   $env.LS_COLORS = 'fi=0'
1171    //   $env.config.color_config.string = 'red'
1172    // if a regular file without an extension is the color 'default' then it's working
1173    // if a regular file without an extension is the color 'red' then it's not working
1174    let ansi_style = style
1175        .map(Style::to_nu_ansi_term_style)
1176        .unwrap_or(nu_ansi_term::Style {
1177            foreground: Some(nu_ansi_term::Color::Default),
1178            background: Some(nu_ansi_term::Color::Default),
1179            is_bold: false,
1180            is_dimmed: false,
1181            is_italic: false,
1182            is_underline: false,
1183            is_blink: false,
1184            is_reverse: false,
1185            is_hidden: false,
1186            is_strikethrough: false,
1187            prefix_with_reset: false,
1188        });
1189
1190    let full_path = std::path::absolute(stripped_path.as_ref())
1191        .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1192
1193    let full_path_link = make_clickable_link(
1194        full_path.display().to_string(),
1195        Some(path),
1196        show_clickable_links,
1197    );
1198
1199    let val = if icons {
1200        format!(
1201            "{}  {}",
1202            icon_style.paint(String::from(file_icon.icon)),
1203            ansi_style.paint(full_path_link)
1204        )
1205    } else {
1206        ansi_style.paint(full_path_link).to_string()
1207    };
1208
1209    Some(Value::string(val, span))
1210}
1211
1212fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1213    // only use `use_ansi_coloring` here, it already includes `std::io::stdout().is_terminal()`
1214    // when set to "auto"
1215    if !use_ansi_coloring {
1216        // Draw the table without ansi colors
1217        nu_utils::strip_ansi_string_likely(output)
1218    } else {
1219        // Draw the table with ansi colors
1220        output
1221    }
1222}
1223
1224fn create_empty_placeholder(
1225    value_type_name: &str,
1226    termwidth: usize,
1227    engine_state: &EngineState,
1228    stack: &Stack,
1229    use_ansi_coloring: bool,
1230) -> String {
1231    let config = stack.get_config(engine_state);
1232    if !config.table.show_empty {
1233        return String::new();
1234    }
1235
1236    let cell = format!("empty {value_type_name}");
1237    let mut table = NuTable::new(1, 1);
1238    table.insert((0, 0), cell);
1239    table.set_data_style(TextStyle::default().dimmed());
1240    let mut out = TableOutput::from_table(table, false, false);
1241
1242    let style_computer = &StyleComputer::from_config(engine_state, stack);
1243    configure_table(&mut out, &config, style_computer, TableMode::default());
1244
1245    if !use_ansi_coloring {
1246        out.table.clear_all_colors();
1247    }
1248
1249    out.table
1250        .draw(termwidth)
1251        .expect("Could not create empty table placeholder")
1252}
1253
1254fn convert_table_to_output(
1255    table: ShellResult<Option<String>>,
1256    signals: &Signals,
1257    term_width: usize,
1258    use_ansi_coloring: bool,
1259) -> Option<ShellResult<Vec<u8>>> {
1260    match table {
1261        Ok(Some(table)) => {
1262            let table = maybe_strip_color(table, use_ansi_coloring);
1263
1264            let mut bytes = table.as_bytes().to_vec();
1265            bytes.push(b'\n'); // nu-table tables don't come with a newline on the end
1266
1267            Some(Ok(bytes))
1268        }
1269        Ok(None) => {
1270            let msg = if signals.interrupted() {
1271                String::from("")
1272            } else {
1273                // assume this failed because the table was too wide
1274                // TODO: more robust error classification
1275                format!("Couldn't fit table into {term_width} columns!")
1276            };
1277
1278            Some(Ok(msg.as_bytes().to_vec()))
1279        }
1280        Err(err) => Some(Err(err)),
1281    }
1282}
1283
1284const SUPPORTED_TABLE_MODES: &[&str] = &[
1285    "basic",
1286    "compact",
1287    "compact_double",
1288    "default",
1289    "frameless",
1290    "heavy",
1291    "light",
1292    "none",
1293    "reinforced",
1294    "rounded",
1295    "thin",
1296    "with_love",
1297    "psql",
1298    "markdown",
1299    "dots",
1300    "restructured",
1301    "ascii_rounded",
1302    "basic_compact",
1303    "single",
1304    "double",
1305];
1306
1307fn supported_table_modes() -> Vec<Value> {
1308    SUPPORTED_TABLE_MODES
1309        .iter()
1310        .copied()
1311        .map(Value::test_string)
1312        .collect()
1313}
1314
1315fn create_table_opts<'a>(
1316    engine_state: &'a EngineState,
1317    stack: &'a Stack,
1318    cfg: &'a Config,
1319    table_cfg: &'a TableConfig,
1320    span: Span,
1321    offset: usize,
1322) -> TableOpts<'a> {
1323    let comp = StyleComputer::from_config(engine_state, stack);
1324    let signals = engine_state.signals();
1325    let offset = table_cfg.index.unwrap_or(0) + offset;
1326    let index = table_cfg.index.is_none();
1327    let width = table_cfg.width;
1328    let theme = table_cfg.theme;
1329
1330    TableOpts::new(
1331        cfg,
1332        comp,
1333        signals,
1334        span,
1335        width,
1336        theme,
1337        offset,
1338        index,
1339        table_cfg.width_priority_columns.clone(),
1340    )
1341}
1342
1343/// Extracts table width-priority column names from pipeline metadata.
1344///
1345/// Invalid, empty, or duplicated entries are ignored.
1346fn get_width_priority_columns(metadata: Option<&PipelineMetadata>) -> Vec<String> {
1347    let mut width_priority_columns = Vec::new();
1348
1349    let Some(metadata) = metadata else {
1350        return width_priority_columns;
1351    };
1352
1353    let Some(value) = metadata
1354        .custom
1355        .get(TABLE_WIDTH_PRIORITY_COLUMNS_METADATA_KEY)
1356    else {
1357        return width_priority_columns;
1358    };
1359
1360    let Ok(values) = value.as_list() else {
1361        return width_priority_columns;
1362    };
1363
1364    for value in values {
1365        if let Ok(column_name) = value.as_str()
1366            && !column_name.is_empty()
1367            && !width_priority_columns
1368                .iter()
1369                .any(|column| column == column_name)
1370        {
1371            width_priority_columns.push(column_name.to_string());
1372        }
1373    }
1374
1375    width_priority_columns
1376}
1377
1378fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1379    #[cfg(feature = "os")]
1380    let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1381
1382    #[cfg(not(feature = "os"))]
1383    let cwd = None;
1384
1385    Ok(cwd)
1386}
1387
1388fn get_table_width(width_param: Option<i64>) -> usize {
1389    if let Some(col) = width_param {
1390        col as usize
1391    } else if let Ok((w, _h)) = terminal_size() {
1392        w as usize
1393    } else {
1394        DEFAULT_TABLE_WIDTH
1395    }
1396}
1397
1398fn get_hex_styles(engine_state: &EngineState, stack: &mut Stack) -> HexStyles {
1399    let comp = StyleComputer::from_config(engine_state, stack);
1400    let null = Value::nothing(Span::unknown());
1401    HexStyles {
1402        null_char: comp.compute("binary_null_char", &null),
1403        printable: comp.compute("binary_printable", &null),
1404        whitespace: comp.compute("binary_whitespace", &null),
1405        ascii_other: comp.compute("binary_ascii_other", &null),
1406        non_ascii: comp.compute("binary_non_ascii", &null),
1407    }
1408}