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