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