Skip to main content

shannon_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,
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}
248
249impl TableConfig {
250    fn new(
251        view: TableView,
252        width: usize,
253        theme: TableMode,
254        abbreviation: Option<usize>,
255        index: Option<usize>,
256        use_ansi_coloring: bool,
257        icons: bool,
258    ) -> Self {
259        Self {
260            view,
261            width,
262            theme,
263            abbreviation,
264            index,
265            use_ansi_coloring,
266            icons,
267        }
268    }
269}
270
271#[derive(Debug, Clone)]
272enum TableView {
273    General,
274    Collapsed,
275    Expanded {
276        limit: Option<usize>,
277        flatten: bool,
278        flatten_separator: Option<String>,
279    },
280}
281
282struct CLIArgs {
283    width: Option<i64>,
284    abbrivation: Option<usize>,
285    theme: TableMode,
286    expand: bool,
287    expand_limit: Option<usize>,
288    expand_flatten: bool,
289    expand_flatten_separator: Option<String>,
290    collapse: bool,
291    index: Option<usize>,
292    use_ansi_coloring: bool,
293    icons: bool,
294}
295
296fn parse_table_config(
297    call: &Call,
298    state: &EngineState,
299    stack: &mut Stack,
300) -> ShellResult<TableConfig> {
301    let args = get_cli_args(call, state, stack)?;
302    let table_view = get_table_view(&args);
303    let term_width = get_table_width(args.width);
304
305    let cfg = TableConfig::new(
306        table_view,
307        term_width,
308        args.theme,
309        args.abbrivation,
310        args.index,
311        args.use_ansi_coloring,
312        args.icons,
313    );
314
315    Ok(cfg)
316}
317
318fn get_table_view(args: &CLIArgs) -> TableView {
319    match (args.expand, args.collapse) {
320        (false, false) => TableView::General,
321        (_, true) => TableView::Collapsed,
322        (true, _) => TableView::Expanded {
323            limit: args.expand_limit,
324            flatten: args.expand_flatten,
325            flatten_separator: args.expand_flatten_separator.clone(),
326        },
327    }
328}
329
330fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
331    let width: Option<i64> = call.get_flag(state, stack, "width")?;
332    let expand: bool = call.has_flag(state, stack, "expand")?;
333    let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
334    let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
335    let expand_flatten_separator: Option<String> =
336        call.get_flag(state, stack, "flatten-separator")?;
337    let collapse: bool = call.has_flag(state, stack, "collapse")?;
338    let abbrivation: Option<usize> = call
339        .get_flag(state, stack, "abbreviated")?
340        .or_else(|| stack.get_config(state).table.abbreviated_row_count);
341    let theme =
342        get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
343    let index = get_index_flag(call, state, stack)?;
344    let icons = call.has_flag(state, stack, "icons")?;
345
346    let use_ansi_coloring = stack.get_config(state).use_ansi_coloring.get(state);
347
348    Ok(CLIArgs {
349        theme,
350        abbrivation,
351        collapse,
352        expand,
353        expand_limit,
354        expand_flatten,
355        expand_flatten_separator,
356        width,
357        index,
358        use_ansi_coloring,
359        icons,
360    })
361}
362
363fn get_index_flag(
364    call: &Call,
365    state: &EngineState,
366    stack: &mut Stack,
367) -> ShellResult<Option<usize>> {
368    let index: Option<Value> = call.get_flag(state, stack, "index")?;
369    let value = match index {
370        Some(value) => value,
371        None => return Ok(Some(0)),
372    };
373    let span = value.span();
374
375    match value {
376        Value::Bool { val, .. } => {
377            if val {
378                Ok(Some(0))
379            } else {
380                Ok(None)
381            }
382        }
383        Value::Int { val, .. } => {
384            if val < 0 {
385                Err(ShellError::UnsupportedInput {
386                    msg: String::from("got a negative integer"),
387                    input: val.to_string(),
388                    msg_span: call.span(),
389                    input_span: span,
390                })
391            } else {
392                Ok(Some(val as usize))
393            }
394        }
395        Value::Nothing { .. } => Ok(Some(0)),
396        _ => Err(ShellError::CantConvert {
397            to_type: String::from("index"),
398            from_type: String::new(),
399            span: call.span(),
400            help: Some(String::from("supported values: [bool, int, nothing]")),
401        }),
402    }
403}
404
405fn get_theme_flag(
406    call: &Call,
407    state: &EngineState,
408    stack: &mut Stack,
409) -> ShellResult<Option<TableMode>> {
410    call.get_flag(state, stack, "theme")?
411        .map(|theme: String| {
412            TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
413                to_type: String::from("theme"),
414                from_type: String::from("string"),
415                span: call.span(),
416                help: Some(format!("{err}, but found '{theme}'.")),
417            })
418        })
419        .transpose()
420}
421
422struct CmdInput<'a> {
423    engine_state: &'a EngineState,
424    stack: &'a mut Stack,
425    call: &'a Call<'a>,
426    data: PipelineData,
427    cfg: TableConfig,
428    cwd: Option<NuPathBuf>,
429}
430
431impl<'a> CmdInput<'a> {
432    fn parse(
433        engine_state: &'a EngineState,
434        stack: &'a mut Stack,
435        call: &'a Call<'a>,
436        data: PipelineData,
437    ) -> ShellResult<Self> {
438        let cfg = parse_table_config(call, engine_state, stack)?;
439        let cwd = get_cwd(engine_state, stack)?;
440
441        Ok(Self {
442            engine_state,
443            stack,
444            call,
445            data,
446            cfg,
447            cwd,
448        })
449    }
450
451    fn get_config(&self) -> std::sync::Arc<Config> {
452        self.stack.get_config(self.engine_state)
453    }
454}
455
456fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
457    let span = input.data.span().unwrap_or(input.call.head);
458    match input.data {
459        // Binary streams should behave as if they really are `binary` data, and printed as hex
460        PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
461            PipelineData::byte_stream(pretty_hex_stream(stream, input.call.head), None),
462        ),
463        PipelineData::ByteStream(..) => Ok(input.data),
464        PipelineData::Value(Value::Binary { val, .. }, ..) => {
465            let signals = input.engine_state.signals().clone();
466            let stream = ByteStream::read_binary(val, input.call.head, signals);
467            Ok(PipelineData::byte_stream(
468                pretty_hex_stream(stream, input.call.head),
469                None,
470            ))
471        }
472        // None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack.
473        PipelineData::Value(Value::List { vals, .. }, metadata) => {
474            let signals = input.engine_state.signals().clone();
475            let stream = ListStream::new(vals.into_iter(), span, signals);
476            input.data = PipelineData::empty();
477
478            handle_row_stream(input, stream, metadata)
479        }
480        PipelineData::ListStream(stream, metadata) => {
481            input.data = PipelineData::empty();
482            handle_row_stream(input, stream, metadata)
483        }
484        PipelineData::Value(Value::Record { val, .. }, metadata) => {
485            input.data = PipelineData::empty();
486            handle_record(input, val.into_owned(), metadata)
487        }
488        PipelineData::Value(Value::Error { error, .. }, ..) => {
489            // Propagate this error outward, so that it goes to stderr
490            // instead of stdout.
491            Err(*error)
492        }
493        PipelineData::Value(Value::Custom { val, .. }, ..) => {
494            let base_pipeline = val.to_base_value(span)?.into_pipeline_data();
495            Table.run(input.engine_state, input.stack, input.call, base_pipeline)
496        }
497        PipelineData::Value(Value::Range { val, .. }, metadata) => {
498            let signals = input.engine_state.signals().clone();
499            let stream =
500                ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
501            input.data = PipelineData::empty();
502            handle_row_stream(input, stream, metadata)
503        }
504        x => Ok(x),
505    }
506}
507
508fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream {
509    let mut cfg = HexConfig {
510        // We are going to render the title manually first
511        title: true,
512        // If building on 32-bit, the stream size might be bigger than a usize
513        length: stream.known_size().and_then(|sz| sz.try_into().ok()),
514        ..HexConfig::default()
515    };
516
517    // This won't really work for us
518    debug_assert!(cfg.width > 0, "the default hex config width was zero");
519
520    let mut read_buf = Vec::with_capacity(cfg.width);
521
522    let mut reader = if let Some(reader) = stream.reader() {
523        reader
524    } else {
525        // No stream to read from
526        return ByteStream::read_string("".into(), span, Signals::empty());
527    };
528
529    ByteStream::from_fn(
530        span,
531        Signals::empty(),
532        ByteStreamType::String,
533        move |buffer| {
534            // Turn the buffer into a String we can write to
535            let mut write_buf = std::mem::take(buffer);
536            write_buf.clear();
537            // SAFETY: we just truncated it empty
538            let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
539
540            // Write the title at the beginning
541            if cfg.title {
542                nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error");
543                cfg.title = false;
544
545                // Put the write_buf back into buffer
546                *buffer = write_buf.into_bytes();
547
548                Ok(true)
549            } else {
550                // Read up to `cfg.width` bytes
551                read_buf.clear();
552                (&mut reader)
553                    .take(cfg.width as u64)
554                    .read_to_end(&mut read_buf)
555                    .map_err(|err| match ShellErrorBridge::try_from(err) {
556                        Ok(ShellErrorBridge(err)) => err,
557                        Err(err) => IoError::new(err, span, None).into(),
558                    })?;
559
560                if !read_buf.is_empty() {
561                    nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true))
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    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);
601    }
602
603    let config = input.get_config();
604
605    if let Some(PipelineMetadata {
606        data_source,
607        mut path_columns,
608        ..
609    }) = metadata
610    {
611        #[allow(deprecated)]
612        if data_source == DataSource::Ls {
613            path_columns.push(String::from("name"));
614        }
615        // Remove duplicates
616        path_columns.sort_unstable();
617        path_columns.dedup();
618
619        let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
620            Some(v) => Some(env_to_string(
621                "LS_COLORS",
622                v,
623                input.engine_state,
624                input.stack,
625            )?),
626            None => None,
627        };
628        let ls_colors = get_ls_colors(ls_colors_env_str);
629
630        for column in &path_columns {
631            if let Some(value) = record.get_mut(column) {
632                let span = value.span();
633                if let Value::String { val, .. } = value
634                    && let Some(val) = render_path_name(
635                        val,
636                        &config,
637                        &ls_colors,
638                        input.cwd.as_deref(),
639                        input.cfg.icons,
640                        span,
641                    )
642                {
643                    *value = val;
644                }
645            }
646        }
647    }
648    let opts = create_table_opts(
649        input.engine_state,
650        input.stack,
651        &config,
652        &input.cfg,
653        span,
654        0,
655    );
656    let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
657
658    let result = match result {
659        Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
660        None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
661    };
662
663    let val = Value::string(result, span);
664    let data = val.into_pipeline_data();
665
666    Ok(data)
667}
668
669fn make_record_abbreviation(mut record: Record, limit: usize) -> Record {
670    if record.len() <= limit * 2 + 1 {
671        return record;
672    }
673
674    // TODO: see if the following table builders would be happy with a simple iterator
675    let prev_len = record.len();
676    let mut record_iter = record.into_iter();
677    record = Record::with_capacity(limit * 2 + 1);
678    record.extend(record_iter.by_ref().take(limit));
679    record.push(String::from("..."), Value::string("...", Span::unknown()));
680    record.extend(record_iter.skip(prev_len - 2 * limit));
681    record
682}
683
684fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
685    if signals.interrupted() {
686        "".into()
687    } else {
688        // assume this failed because the table was too wide
689        // TODO: more robust error classification
690        format!("Couldn't fit table into {term_width} columns!")
691    }
692}
693
694fn build_table_kv(
695    record: Record,
696    table_view: TableView,
697    opts: TableOpts<'_>,
698    span: Span,
699) -> StringResult {
700    match table_view {
701        TableView::General => JustTable::kv_table(record, opts),
702        TableView::Expanded {
703            limit,
704            flatten,
705            flatten_separator,
706        } => {
707            let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
708            ExpandedTable::new(limit, flatten, sep).build_map(&record, opts)
709        }
710        TableView::Collapsed => {
711            let value = Value::record(record, span);
712            CollapsedTable::build(value, opts)
713        }
714    }
715}
716
717fn build_table_batch(
718    mut vals: Vec<Value>,
719    view: TableView,
720    opts: TableOpts<'_>,
721    span: Span,
722) -> StringResult {
723    // convert each custom value to its base value so it can be properly
724    // displayed in a table
725    for val in &mut vals {
726        let span = val.span();
727
728        if let Value::Custom { val: custom, .. } = val {
729            *val = custom
730                .to_base_value(span)
731                .or_else(|err| Result::<_, ShellError>::Ok(Value::error(err, span)))
732                .expect("error converting custom value to base value")
733        }
734    }
735
736    match view {
737        TableView::General => JustTable::table(vals, opts),
738        TableView::Expanded {
739            limit,
740            flatten,
741            flatten_separator,
742        } => {
743            let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
744            ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
745        }
746        TableView::Collapsed => {
747            let value = Value::list(vals, span);
748            CollapsedTable::build(value, opts)
749        }
750    }
751}
752
753fn handle_row_stream(
754    input: CmdInput<'_>,
755    stream: ListStream,
756    metadata: Option<PipelineMetadata>,
757) -> ShellResult<PipelineData> {
758    let cfg = input.get_config();
759
760    let stream = if let Some(metadata) = metadata {
761        let stream = if let PipelineMetadata {
762            data_source: DataSource::HtmlThemes,
763            ..
764        } = &metadata
765        {
766            stream.map(|mut value| {
767                if let Value::Record { val: record, .. } = &mut value {
768                    for (rec_col, rec_val) in record.to_mut().iter_mut() {
769                        // Every column in the HTML theme table except 'name' is colored
770                        if rec_col != "name" {
771                            continue;
772                        }
773                        // Simple routine to grab the hex code, convert to a style,
774                        // then place it in a new Value::String.
775
776                        let span = rec_val.span();
777                        if let Value::String { val, .. } = rec_val {
778                            let s = match color_from_hex(val) {
779                                Ok(c) => match c {
780                                    // .normal() just sets the text foreground color.
781                                    Some(c) => c.normal(),
782                                    None => nu_ansi_term::Style::default(),
783                                },
784                                Err(_) => nu_ansi_term::Style::default(),
785                            };
786                            *rec_val = Value::string(
787                                // Apply the style (ANSI codes) to the string
788                                s.paint(&*val).to_string(),
789                                span,
790                            );
791                        }
792                    }
793                }
794                value
795            })
796        } else {
797            stream
798        };
799
800        let PipelineMetadata {
801            data_source,
802            mut path_columns,
803            ..
804        } = metadata;
805
806        #[allow(deprecated)]
807        if data_source == DataSource::Ls {
808            path_columns.push(String::from("name"));
809        }
810        // Remove duplicates
811        path_columns.sort_unstable();
812        path_columns.dedup();
813
814        let config = cfg.clone();
815        let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
816            Some(v) => Some(env_to_string(
817                "LS_COLORS",
818                v,
819                input.engine_state,
820                input.stack,
821            )?),
822            None => None,
823        };
824        let ls_colors = get_ls_colors(ls_colors_env_str);
825
826        stream.map(move |mut value| {
827            if let Value::Record { val: record, .. } = &mut value {
828                for column in &path_columns {
829                    if let Some(value) = record.to_mut().get_mut(column) {
830                        let span = value.span();
831                        if let Value::String { val, .. } = value
832                            && let Some(val) = render_path_name(
833                                val,
834                                &config,
835                                &ls_colors,
836                                input.cwd.as_deref(),
837                                input.cfg.icons,
838                                span,
839                            )
840                        {
841                            *value = val;
842                        }
843                    }
844                }
845            }
846            value
847        })
848    } else {
849        stream
850    };
851
852    let paginator = PagingTableCreator::new(
853        input.call.head,
854        stream,
855        // These are passed in as a way to have PagingTable create StyleComputers
856        // for the values it outputs. Because engine_state is passed in, config doesn't need to.
857        input.engine_state.clone(),
858        input.stack.clone(),
859        input.cfg,
860        cfg,
861    );
862    let stream = ByteStream::from_result_iter(
863        paginator,
864        input.call.head,
865        Signals::empty(),
866        ByteStreamType::String,
867    );
868    Ok(PipelineData::byte_stream(stream, None))
869}
870
871fn make_clickable_link(
872    full_path: String,
873    link_name: Option<&str>,
874    show_clickable_links: bool,
875) -> String {
876    // uri's based on this https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
877
878    #[cfg(any(
879        unix,
880        windows,
881        target_os = "redox",
882        target_os = "wasi",
883        target_os = "hermit"
884    ))]
885    if show_clickable_links {
886        format!(
887            "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
888            match Url::from_file_path(full_path.clone()) {
889                Ok(url) => url.to_string(),
890                Err(_) => full_path.clone(),
891            },
892            link_name.unwrap_or(full_path.as_str())
893        )
894    } else {
895        match link_name {
896            Some(link_name) => link_name.to_string(),
897            None => full_path,
898        }
899    }
900
901    #[cfg(not(any(
902        unix,
903        windows,
904        target_os = "redox",
905        target_os = "wasi",
906        target_os = "hermit"
907    )))]
908    match link_name {
909        Some(link_name) => link_name.to_string(),
910        None => full_path,
911    }
912}
913
914struct PagingTableCreator {
915    head: Span,
916    stream: ValueIterator,
917    engine_state: EngineState,
918    stack: Stack,
919    elements_displayed: usize,
920    reached_end: bool,
921    table_config: TableConfig,
922    row_offset: usize,
923    config: std::sync::Arc<Config>,
924}
925
926impl PagingTableCreator {
927    fn new(
928        head: Span,
929        stream: ListStream,
930        engine_state: EngineState,
931        stack: Stack,
932        table_config: TableConfig,
933        config: std::sync::Arc<Config>,
934    ) -> Self {
935        PagingTableCreator {
936            head,
937            stream: stream.into_inner(),
938            engine_state,
939            stack,
940            config,
941            table_config,
942            elements_displayed: 0,
943            reached_end: false,
944            row_offset: 0,
945        }
946    }
947
948    fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
949        if batch.is_empty() {
950            return Ok(None);
951        }
952
953        let opts = self.create_table_opts();
954        build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
955    }
956
957    fn create_table_opts(&self) -> TableOpts<'_> {
958        create_table_opts(
959            &self.engine_state,
960            &self.stack,
961            &self.config,
962            &self.table_config,
963            self.head,
964            self.row_offset,
965        )
966    }
967}
968
969impl Iterator for PagingTableCreator {
970    type Item = ShellResult<Vec<u8>>;
971
972    fn next(&mut self) -> Option<Self::Item> {
973        let batch;
974        let end;
975
976        match self.table_config.abbreviation {
977            Some(abbr) => {
978                (batch, _, end) =
979                    stream_collect_abbreviated(&mut self.stream, abbr, self.engine_state.signals());
980            }
981            None => {
982                // Pull from stream until time runs out or we have enough items
983                (batch, end) = stream_collect(
984                    &mut self.stream,
985                    self.config.table.stream_page_size.get() as usize,
986                    self.config.table.batch_duration,
987                    self.engine_state.signals(),
988                );
989            }
990        }
991
992        let batch_size = batch.len();
993
994        // Count how much elements were displayed and if end of stream was reached
995        self.elements_displayed += batch_size;
996        self.reached_end = self.reached_end || end;
997
998        if batch.is_empty() {
999            // If this iterator has not displayed a single entry and reached its end (no more elements
1000            // or interrupted by ctrl+c) display as "empty list"
1001            return if self.elements_displayed == 0 && self.reached_end {
1002                // Increase elements_displayed by one so on next iteration next branch of this
1003                // if else triggers and terminates stream
1004                self.elements_displayed = 1;
1005                let result = create_empty_placeholder(
1006                    "list",
1007                    self.table_config.width,
1008                    &self.engine_state,
1009                    &self.stack,
1010                    self.table_config.use_ansi_coloring,
1011                );
1012                let mut bytes = result.into_bytes();
1013                // Add extra newline if show_empty is enabled
1014                if !bytes.is_empty() {
1015                    bytes.push(b'\n');
1016                }
1017                Some(Ok(bytes))
1018            } else {
1019                None
1020            };
1021        }
1022
1023        let table = self.build_table(batch);
1024
1025        self.row_offset += batch_size;
1026
1027        convert_table_to_output(
1028            table,
1029            self.engine_state.signals(),
1030            self.table_config.width,
1031            self.table_config.use_ansi_coloring,
1032        )
1033    }
1034}
1035
1036fn stream_collect(
1037    stream: impl Iterator<Item = Value>,
1038    size: usize,
1039    batch_duration: Duration,
1040    signals: &Signals,
1041) -> (Vec<Value>, bool) {
1042    let start_time = Instant::now();
1043    let mut end = true;
1044
1045    let mut batch = Vec::with_capacity(size);
1046    for (i, item) in stream.enumerate() {
1047        batch.push(item);
1048
1049        // We buffer until `$env.config.table.batch_duration`, then we send out what we have so far
1050        if (Instant::now() - start_time) >= batch_duration {
1051            end = false;
1052            break;
1053        }
1054
1055        // Or until we reached `$env.config.table.stream_page_size`.
1056        if i + 1 == size {
1057            end = false;
1058            break;
1059        }
1060
1061        if signals.interrupted() {
1062            break;
1063        }
1064    }
1065
1066    (batch, end)
1067}
1068
1069fn stream_collect_abbreviated(
1070    stream: impl Iterator<Item = Value>,
1071    size: usize,
1072    signals: &Signals,
1073) -> (Vec<Value>, usize, bool) {
1074    let mut end = true;
1075    let mut read = 0;
1076    let mut head = Vec::with_capacity(size);
1077    let mut tail = VecDeque::with_capacity(size);
1078
1079    if size == 0 {
1080        return (vec![], 0, false);
1081    }
1082
1083    for item in stream {
1084        read += 1;
1085
1086        if read <= size {
1087            head.push(item);
1088        } else if tail.len() < size {
1089            tail.push_back(item);
1090        } else {
1091            let _ = tail.pop_front();
1092            tail.push_back(item);
1093        }
1094
1095        if signals.interrupted() {
1096            end = false;
1097            break;
1098        }
1099    }
1100
1101    let have_filled_list = head.len() == size && tail.len() == size;
1102    if have_filled_list {
1103        let dummy = get_abbreviated_dummy(&head, &tail);
1104        head.insert(size, dummy)
1105    }
1106
1107    head.extend(tail);
1108
1109    (head, read, end)
1110}
1111
1112fn get_abbreviated_dummy(head: &[Value], tail: &VecDeque<Value>) -> Value {
1113    let dummy = || Value::string(String::from("..."), Span::unknown());
1114    let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1115
1116    if is_record_list {
1117        // in case it's a record list we set a default text to each column instead of a single value.
1118        Value::record(
1119            head[0]
1120                .as_record()
1121                .expect("ok")
1122                .columns()
1123                .map(|key| (key.clone(), dummy()))
1124                .collect(),
1125            Span::unknown(),
1126        )
1127    } else {
1128        dummy()
1129    }
1130}
1131
1132fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1133    batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1134}
1135
1136fn render_path_name(
1137    path: &str,
1138    config: &Config,
1139    ls_colors: &LsColors,
1140    cwd: Option<&NuPath>,
1141    icons: bool,
1142    span: Span,
1143) -> Option<Value> {
1144    if !config.ls.use_ls_colors {
1145        return None;
1146    }
1147
1148    let fullpath = match cwd {
1149        Some(cwd) => PathBuf::from(cwd.join(path)),
1150        None => PathBuf::from(path),
1151    };
1152
1153    let stripped_path = nu_utils::strip_ansi_unlikely(path);
1154    let metadata = std::fs::symlink_metadata(fullpath);
1155    let has_metadata = metadata.is_ok();
1156    let style =
1157        ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1158
1159    let file_icon = icon_for_file(path, &None);
1160    let icon_style = lookup_ansi_color_style(file_icon.color);
1161
1162    // clickable links don't work in remote SSH sessions
1163    let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1164    //TODO: Deprecated show_clickable_links_in_ls in favor of shell_integration_osc8
1165    let show_clickable_links = config.ls.clickable_links
1166        && !in_ssh_session
1167        && has_metadata
1168        && config.shell_integration.osc8;
1169
1170    // If there is no style at all set it to use 'default' foreground and background
1171    // colors. This prevents it being colored in tabled as string colors.
1172    // To test this:
1173    //   $env.LS_COLORS = 'fi=0'
1174    //   $env.config.color_config.string = 'red'
1175    // if a regular file without an extension is the color 'default' then it's working
1176    // if a regular file without an extension is the color 'red' then it's not working
1177    let ansi_style = style
1178        .map(Style::to_nu_ansi_term_style)
1179        .unwrap_or(nu_ansi_term::Style {
1180            foreground: Some(nu_ansi_term::Color::Default),
1181            background: Some(nu_ansi_term::Color::Default),
1182            is_bold: false,
1183            is_dimmed: false,
1184            is_italic: false,
1185            is_underline: false,
1186            is_blink: false,
1187            is_reverse: false,
1188            is_hidden: false,
1189            is_strikethrough: false,
1190            prefix_with_reset: false,
1191        });
1192
1193    let full_path = std::path::absolute(stripped_path.as_ref())
1194        .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1195
1196    let full_path_link = make_clickable_link(
1197        full_path.display().to_string(),
1198        Some(path),
1199        show_clickable_links,
1200    );
1201
1202    let val = if icons {
1203        format!(
1204            "{}  {}",
1205            icon_style.paint(String::from(file_icon.icon)),
1206            ansi_style.paint(full_path_link)
1207        )
1208    } else {
1209        ansi_style.paint(full_path_link).to_string()
1210    };
1211
1212    Some(Value::string(val, span))
1213}
1214
1215fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1216    // only use `use_ansi_coloring` here, it already includes `std::io::stdout().is_terminal()`
1217    // when set to "auto"
1218    if !use_ansi_coloring {
1219        // Draw the table without ansi colors
1220        nu_utils::strip_ansi_string_likely(output)
1221    } else {
1222        // Draw the table with ansi colors
1223        output
1224    }
1225}
1226
1227fn create_empty_placeholder(
1228    value_type_name: &str,
1229    termwidth: usize,
1230    engine_state: &EngineState,
1231    stack: &Stack,
1232    use_ansi_coloring: bool,
1233) -> String {
1234    let config = stack.get_config(engine_state);
1235    if !config.table.show_empty {
1236        return String::new();
1237    }
1238
1239    let cell = format!("empty {value_type_name}");
1240    let mut table = NuTable::new(1, 1);
1241    table.insert((0, 0), cell);
1242    table.set_data_style(TextStyle::default().dimmed());
1243    let mut out = TableOutput::from_table(table, false, false);
1244
1245    let style_computer = &StyleComputer::from_config(engine_state, stack);
1246    configure_table(&mut out, &config, style_computer, TableMode::default());
1247
1248    if !use_ansi_coloring {
1249        out.table.clear_all_colors();
1250    }
1251
1252    out.table
1253        .draw(termwidth)
1254        .expect("Could not create empty table placeholder")
1255}
1256
1257fn convert_table_to_output(
1258    table: ShellResult<Option<String>>,
1259    signals: &Signals,
1260    term_width: usize,
1261    use_ansi_coloring: bool,
1262) -> Option<ShellResult<Vec<u8>>> {
1263    match table {
1264        Ok(Some(table)) => {
1265            let table = maybe_strip_color(table, use_ansi_coloring);
1266
1267            let mut bytes = table.as_bytes().to_vec();
1268            bytes.push(b'\n'); // nu-table tables don't come with a newline on the end
1269
1270            Some(Ok(bytes))
1271        }
1272        Ok(None) => {
1273            let msg = if signals.interrupted() {
1274                String::from("")
1275            } else {
1276                // assume this failed because the table was too wide
1277                // TODO: more robust error classification
1278                format!("Couldn't fit table into {term_width} columns!")
1279            };
1280
1281            Some(Ok(msg.as_bytes().to_vec()))
1282        }
1283        Err(err) => Some(Err(err)),
1284    }
1285}
1286
1287const SUPPORTED_TABLE_MODES: &[&str] = &[
1288    "basic",
1289    "compact",
1290    "compact_double",
1291    "default",
1292    "heavy",
1293    "light",
1294    "none",
1295    "reinforced",
1296    "rounded",
1297    "thin",
1298    "with_love",
1299    "psql",
1300    "markdown",
1301    "dots",
1302    "restructured",
1303    "ascii_rounded",
1304    "basic_compact",
1305    "single",
1306    "double",
1307];
1308
1309fn supported_table_modes() -> Vec<Value> {
1310    SUPPORTED_TABLE_MODES
1311        .iter()
1312        .copied()
1313        .map(Value::test_string)
1314        .collect()
1315}
1316
1317fn create_table_opts<'a>(
1318    engine_state: &'a EngineState,
1319    stack: &'a Stack,
1320    cfg: &'a Config,
1321    table_cfg: &'a TableConfig,
1322    span: Span,
1323    offset: usize,
1324) -> TableOpts<'a> {
1325    let comp = StyleComputer::from_config(engine_state, stack);
1326    let signals = engine_state.signals();
1327    let offset = table_cfg.index.unwrap_or(0) + offset;
1328    let index = table_cfg.index.is_none();
1329    let width = table_cfg.width;
1330    let theme = table_cfg.theme;
1331
1332    TableOpts::new(cfg, comp, signals, span, width, theme, offset, index)
1333}
1334
1335fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1336    #[cfg(feature = "os")]
1337    let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1338
1339    #[cfg(not(feature = "os"))]
1340    let cwd = None;
1341
1342    Ok(cwd)
1343}
1344
1345fn get_table_width(width_param: Option<i64>) -> usize {
1346    if let Some(col) = width_param {
1347        col as usize
1348    } else if let Ok((w, _h)) = terminal_size() {
1349        w as usize
1350    } else {
1351        DEFAULT_TABLE_WIDTH
1352    }
1353}