nu_command/viewers/
table.rs

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