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