Skip to main content

nu_command/viewers/
table.rs

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