1use 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,
19 TABLE_WIDTH_PRIORITY_COLUMNS_METADATA_KEY, TableMode, 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
37impl 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 .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_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 #[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 width_priority_columns: Vec<String>,
249}
250
251#[derive(Debug, Clone)]
252enum TableView {
253 General,
254 Collapsed,
255 Expanded {
256 limit: Option<usize>,
257 flatten: bool,
258 flatten_separator: Option<String>,
259 },
260}
261
262struct CLIArgs {
263 width: Option<i64>,
264 abbreviation: Option<usize>,
265 theme: TableMode,
266 expand: bool,
267 expand_limit: Option<usize>,
268 expand_flatten: bool,
269 expand_flatten_separator: Option<String>,
270 collapse: bool,
271 index: Option<usize>,
272 use_ansi_coloring: bool,
273 icons: bool,
274}
275
276fn parse_table_config(
277 call: &Call,
278 state: &EngineState,
279 stack: &mut Stack,
280) -> ShellResult<TableConfig> {
281 let args @ CLIArgs {
282 abbreviation,
283 theme,
284 index,
285 use_ansi_coloring,
286 icons,
287 ..
288 } = get_cli_args(call, state, stack)?;
289
290 let table_view = get_table_view(&args);
291 let term_width = get_table_width(args.width);
292 let hex_styles = get_hex_styles(state, stack);
293
294 let cfg = TableConfig {
295 view: table_view,
296 width: term_width,
297 theme,
298 abbreviation,
299 index,
300 use_ansi_coloring,
301 icons,
302 hex_styles,
303 width_priority_columns: vec![],
304 };
305
306 Ok(cfg)
307}
308
309fn get_table_view(args: &CLIArgs) -> TableView {
310 match (args.expand, args.collapse) {
311 (false, false) => TableView::General,
312 (_, true) => TableView::Collapsed,
313 (true, _) => TableView::Expanded {
314 limit: args.expand_limit,
315 flatten: args.expand_flatten,
316 flatten_separator: args.expand_flatten_separator.clone(),
317 },
318 }
319}
320
321fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
322 let width: Option<i64> = call.get_flag(state, stack, "width")?;
323 let expand: bool = call.has_flag(state, stack, "expand")?;
324 let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
325 let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
326 let expand_flatten_separator: Option<String> =
327 call.get_flag(state, stack, "flatten-separator")?;
328 let collapse: bool = call.has_flag(state, stack, "collapse")?;
329 let abbreviation: Option<usize> = call
330 .get_flag(state, stack, "abbreviated")?
331 .or_else(|| stack.get_config(state).table.abbreviated_row_count);
332 let theme =
333 get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
334 let index = get_index_flag(call, state, stack)?;
335 let icons = call.has_flag(state, stack, "icons")?;
336
337 let use_ansi_coloring = stack.get_config(state).use_ansi_coloring.get(state);
338
339 Ok(CLIArgs {
340 theme,
341 abbreviation,
342 collapse,
343 expand,
344 expand_limit,
345 expand_flatten,
346 expand_flatten_separator,
347 width,
348 index,
349 use_ansi_coloring,
350 icons,
351 })
352}
353
354fn get_index_flag(
355 call: &Call,
356 state: &EngineState,
357 stack: &mut Stack,
358) -> ShellResult<Option<usize>> {
359 let index: Option<Value> = call.get_flag(state, stack, "index")?;
360 let value = match index {
361 Some(value) => value,
362 None => return Ok(Some(0)),
363 };
364 let span = value.span();
365
366 match value {
367 Value::Bool { val, .. } => {
368 if val {
369 Ok(Some(0))
370 } else {
371 Ok(None)
372 }
373 }
374 Value::Int { val, .. } => {
375 if val < 0 {
376 Err(ShellError::UnsupportedInput {
377 msg: String::from("got a negative integer"),
378 input: val.to_string(),
379 msg_span: call.span(),
380 input_span: span,
381 })
382 } else {
383 Ok(Some(val as usize))
384 }
385 }
386 Value::Nothing { .. } => Ok(Some(0)),
387 _ => Err(ShellError::CantConvert {
388 to_type: String::from("index"),
389 from_type: String::new(),
390 span: call.span(),
391 help: Some(String::from("supported values: [bool, int, nothing]")),
392 }),
393 }
394}
395
396fn get_theme_flag(
397 call: &Call,
398 state: &EngineState,
399 stack: &mut Stack,
400) -> ShellResult<Option<TableMode>> {
401 call.get_flag(state, stack, "theme")?
402 .map(|theme: String| {
403 TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
404 to_type: String::from("theme"),
405 from_type: String::from("string"),
406 span: call.span(),
407 help: Some(format!("{err}, but found '{theme}'.")),
408 })
409 })
410 .transpose()
411}
412
413struct CmdInput<'a> {
414 engine_state: &'a EngineState,
415 stack: &'a mut Stack,
416 call: &'a Call<'a>,
417 data: PipelineData,
418 cfg: TableConfig,
419 cwd: Option<NuPathBuf>,
420}
421
422impl<'a> CmdInput<'a> {
423 fn parse(
424 engine_state: &'a EngineState,
425 stack: &'a mut Stack,
426 call: &'a Call<'a>,
427 data: PipelineData,
428 ) -> ShellResult<Self> {
429 let cfg = parse_table_config(call, engine_state, stack)?;
430 let cwd = get_cwd(engine_state, stack)?;
431
432 Ok(Self {
433 engine_state,
434 stack,
435 call,
436 data,
437 cfg,
438 cwd,
439 })
440 }
441
442 fn get_config(&self) -> std::sync::Arc<Config> {
443 self.stack.get_config(self.engine_state)
444 }
445}
446
447fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
448 let span = input.data.span().unwrap_or(input.call.head);
449 match input.data {
450 PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
452 PipelineData::byte_stream(pretty_hex_stream(stream, input.cfg, input.call.head), None),
453 ),
454 PipelineData::ByteStream(..) => Ok(input.data),
455 PipelineData::Value(Value::Binary { val, .. }, ..) => {
456 let signals = input.engine_state.signals().clone();
457 let stream = ByteStream::read_binary(val, input.call.head, signals);
458 Ok(PipelineData::byte_stream(
459 pretty_hex_stream(stream, input.cfg, input.call.head),
460 None,
461 ))
462 }
463 PipelineData::Value(Value::List { vals, .. }, metadata) => {
465 let signals = input.engine_state.signals().clone();
466 let stream = ListStream::new(vals.into_iter(), span, signals);
467 input.data = PipelineData::empty();
468
469 handle_row_stream(input, stream, metadata)
470 }
471 PipelineData::ListStream(stream, metadata) => {
472 input.data = PipelineData::empty();
473 handle_row_stream(input, stream, metadata)
474 }
475 PipelineData::Value(Value::Record { val, .. }, metadata) => {
476 input.data = PipelineData::empty();
477 handle_record(input, val.into_owned(), metadata)
478 }
479 PipelineData::Value(Value::Error { error, .. }, ..) => {
480 Err(*error)
483 }
484 PipelineData::Value(Value::Custom { val, .. }, metadata) => {
485 let base_pipeline = val
486 .to_base_value(span)?
487 .into_pipeline_data_with_metadata(metadata);
488 Table.run(input.engine_state, input.stack, input.call, base_pipeline)
489 }
490 PipelineData::Value(Value::Range { val, .. }, metadata) => {
491 let signals = input.engine_state.signals().clone();
492 let stream =
493 ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
494 input.data = PipelineData::empty();
495 handle_row_stream(input, stream, metadata)
496 }
497 x => Ok(x),
498 }
499}
500
501fn pretty_hex_stream(stream: ByteStream, table_cfg: TableConfig, span: Span) -> ByteStream {
502 let mut cfg = HexConfig {
503 title: true,
505 length: stream.known_size().and_then(|sz| sz.try_into().ok()),
507 styles: table_cfg.hex_styles,
508 ..HexConfig::default()
509 };
510
511 debug_assert!(cfg.width > 0, "the default hex config width was zero");
513
514 let mut read_buf = Vec::with_capacity(cfg.width);
515
516 let mut reader = if let Some(reader) = stream.reader() {
517 reader
518 } else {
519 return ByteStream::read_string("".into(), span, Signals::empty());
521 };
522
523 ByteStream::from_fn(
524 span,
525 Signals::empty(),
526 ByteStreamType::String,
527 move |buffer| {
528 let mut write_buf = std::mem::take(buffer);
530 write_buf.clear();
531 let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
533
534 if cfg.title {
536 nu_pretty_hex::write_title(&mut write_buf, cfg, table_cfg.use_ansi_coloring)
537 .expect("format error");
538 cfg.title = false;
539
540 *buffer = write_buf.into_bytes();
542
543 Ok(true)
544 } else {
545 read_buf.clear();
547 (&mut reader)
548 .take(cfg.width as u64)
549 .read_to_end(&mut read_buf)
550 .map_err(|err| match ShellErrorBridge::try_from(err) {
551 Ok(ShellErrorBridge(err)) => err,
552 Err(err) => IoError::new(err, span, None).into(),
553 })?;
554
555 if !read_buf.is_empty() {
556 nu_pretty_hex::hex_write(
557 &mut write_buf,
558 &read_buf,
559 cfg,
560 Some(table_cfg.use_ansi_coloring),
561 )
562 .expect("format error");
563 write_buf.push('\n');
564
565 cfg.address_offset += read_buf.len();
567
568 *buffer = write_buf.into_bytes();
570
571 Ok(true)
572 } else {
573 Ok(false)
574 }
575 }
576 },
577 )
578}
579
580fn handle_record(
581 mut input: CmdInput,
582 mut record: Record,
583 metadata: Option<PipelineMetadata>,
584) -> ShellResult<PipelineData> {
585 let span = input.data.span().unwrap_or(input.call.head);
586
587 if record.is_empty() {
588 let value = create_empty_placeholder(
589 "record",
590 input.cfg.width,
591 input.engine_state,
592 input.stack,
593 input.cfg.use_ansi_coloring,
594 );
595 let value = Value::string(value, span);
596 return Ok(value.into_pipeline_data());
597 };
598
599 if let Some(limit) = input.cfg.abbreviation {
600 record = make_record_abbreviation(record, limit, span);
601 }
602
603 input.cfg.width_priority_columns = get_width_priority_columns(metadata.as_ref());
604
605 let config = input.get_config();
606
607 if let Some(PipelineMetadata {
608 mut path_columns, ..
609 }) = metadata
610 {
611 path_columns.sort_unstable();
613 path_columns.dedup();
614
615 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
616 Some(v) => Some(env_to_string(
617 "LS_COLORS",
618 v,
619 input.engine_state,
620 input.stack,
621 )?),
622 None => None,
623 };
624 let ls_colors = get_ls_colors(ls_colors_env_str);
625
626 for column in &path_columns {
627 if let Some(value) = record.get_mut(column) {
628 let span = value.span();
629 if let Value::String { val, .. } = value
630 && let Some(val) = render_path_name(
631 val,
632 &config,
633 &ls_colors,
634 input.cwd.as_deref(),
635 input.cfg.icons,
636 span,
637 )
638 {
639 *value = val;
640 }
641 }
642 }
643 }
644 let opts = create_table_opts(
645 input.engine_state,
646 input.stack,
647 &config,
648 &input.cfg,
649 span,
650 0,
651 );
652 let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
653
654 let result = match result {
655 Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
656 None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
657 };
658
659 let val = Value::string(result, span);
660 let data = val.into_pipeline_data();
661
662 Ok(data)
663}
664
665fn make_record_abbreviation(mut record: Record, limit: usize, span: Span) -> Record {
666 if record.len() <= limit * 2 + 1 {
667 return record;
668 }
669
670 let prev_len = record.len();
672 let mut record_iter = record.into_iter();
673 record = Record::with_capacity(limit * 2 + 1);
674 record.extend(record_iter.by_ref().take(limit));
675 record.push(String::from("..."), Value::string("...", span));
676 record.extend(record_iter.skip(prev_len - 2 * limit));
677 record
678}
679
680fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
681 if signals.interrupted() {
682 "".into()
683 } else {
684 format!("Couldn't fit table into {term_width} columns!")
687 }
688}
689
690fn build_table_kv(
691 record: Record,
692 table_view: TableView,
693 opts: TableOpts<'_>,
694 span: Span,
695) -> StringResult {
696 match table_view {
697 TableView::General => JustTable::kv_table(record, opts),
698 TableView::Expanded {
699 limit,
700 flatten,
701 flatten_separator,
702 } => {
703 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
704 ExpandedTable::new(limit, flatten, sep).build_map(&record, opts)
705 }
706 TableView::Collapsed => {
707 let value = Value::record(record, span);
708 CollapsedTable::build(value, opts)
709 }
710 }
711}
712
713fn build_table_batch(
714 mut vals: Vec<Value>,
715 view: TableView,
716 opts: TableOpts<'_>,
717 span: Span,
718) -> StringResult {
719 for val in &mut vals {
722 let span = val.span();
723
724 if let Value::Custom { val: custom, .. } = val {
725 *val = custom
726 .to_base_value(span)
727 .or_else(|err| Result::<_, ShellError>::Ok(Value::error(err, span)))
728 .expect("error converting custom value to base value")
729 }
730 }
731
732 match view {
733 TableView::General => JustTable::table(vals, opts),
734 TableView::Expanded {
735 limit,
736 flatten,
737 flatten_separator,
738 } => {
739 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
740 ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
741 }
742 TableView::Collapsed => {
743 let value = Value::list(vals, span);
744 CollapsedTable::build(value, opts)
745 }
746 }
747}
748
749fn handle_row_stream(
750 mut input: CmdInput<'_>,
751 stream: ListStream,
752 metadata: Option<PipelineMetadata>,
753) -> ShellResult<PipelineData> {
754 input.cfg.width_priority_columns = get_width_priority_columns(metadata.as_ref());
755
756 let cfg = input.get_config();
757
758 let stream = if let Some(metadata) = metadata {
759 let stream = if let PipelineMetadata {
760 data_source: DataSource::HtmlThemes,
761 ..
762 } = &metadata
763 {
764 stream.map(|mut value| {
765 if let Value::Record { val: record, .. } = &mut value {
766 for (rec_col, rec_val) in record.to_mut().iter_mut() {
767 if rec_col != "name" {
769 continue;
770 }
771 let span = rec_val.span();
775 if let Value::String { val, .. } = rec_val {
776 let s = match color_from_hex(val) {
777 Ok(c) => match c {
778 Some(c) => c.normal(),
780 None => nu_ansi_term::Style::default(),
781 },
782 Err(_) => nu_ansi_term::Style::default(),
783 };
784 *rec_val = Value::string(
785 s.paint(&*val).to_string(),
787 span,
788 );
789 }
790 }
791 }
792 value
793 })
794 } else {
795 stream
796 };
797
798 let PipelineMetadata {
799 mut path_columns, ..
800 } = metadata;
801
802 path_columns.sort_unstable();
804 path_columns.dedup();
805
806 let config = cfg.clone();
807 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
808 Some(v) => Some(env_to_string(
809 "LS_COLORS",
810 v,
811 input.engine_state,
812 input.stack,
813 )?),
814 None => None,
815 };
816 let ls_colors = get_ls_colors(ls_colors_env_str);
817
818 stream.map(move |mut value| {
819 if let Value::Record { val: record, .. } = &mut value {
820 for column in &path_columns {
821 if let Some(value) = record.to_mut().get_mut(column) {
822 let span = value.span();
823 if let Value::String { val, .. } = value
824 && let Some(val) = render_path_name(
825 val,
826 &config,
827 &ls_colors,
828 input.cwd.as_deref(),
829 input.cfg.icons,
830 span,
831 )
832 {
833 *value = val;
834 }
835 }
836 }
837 }
838 value
839 })
840 } else {
841 stream
842 };
843
844 let paginator = PagingTableCreator::new(
845 input.call.head,
846 stream,
847 input.engine_state.clone(),
850 input.stack.clone(),
851 input.cfg,
852 cfg,
853 );
854 let stream = ByteStream::from_result_iter(
855 paginator,
856 input.call.head,
857 Signals::empty(),
858 ByteStreamType::String,
859 );
860 Ok(PipelineData::byte_stream(stream, None))
861}
862
863fn make_clickable_link(
864 full_path: String,
865 link_name: Option<&str>,
866 show_clickable_links: bool,
867) -> String {
868 #[cfg(any(
871 unix,
872 windows,
873 target_os = "redox",
874 target_os = "wasi",
875 target_os = "hermit"
876 ))]
877 if show_clickable_links {
878 format!(
879 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
880 match Url::from_file_path(full_path.clone()) {
881 Ok(url) => url.to_string(),
882 Err(_) => full_path.clone(),
883 },
884 link_name.unwrap_or(full_path.as_str())
885 )
886 } else {
887 match link_name {
888 Some(link_name) => link_name.to_string(),
889 None => full_path,
890 }
891 }
892
893 #[cfg(not(any(
894 unix,
895 windows,
896 target_os = "redox",
897 target_os = "wasi",
898 target_os = "hermit"
899 )))]
900 match link_name {
901 Some(link_name) => link_name.to_string(),
902 None => full_path,
903 }
904}
905
906struct PagingTableCreator {
907 head: Span,
908 stream: ValueIterator,
909 engine_state: EngineState,
910 stack: Stack,
911 elements_displayed: usize,
912 reached_end: bool,
913 table_config: TableConfig,
914 row_offset: usize,
915 config: std::sync::Arc<Config>,
916}
917
918impl PagingTableCreator {
919 fn new(
920 head: Span,
921 stream: ListStream,
922 engine_state: EngineState,
923 stack: Stack,
924 table_config: TableConfig,
925 config: std::sync::Arc<Config>,
926 ) -> Self {
927 PagingTableCreator {
928 head,
929 stream: stream.into_inner(),
930 engine_state,
931 stack,
932 config,
933 table_config,
934 elements_displayed: 0,
935 reached_end: false,
936 row_offset: 0,
937 }
938 }
939
940 fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
941 if batch.is_empty() {
942 return Ok(None);
943 }
944
945 let opts = self.create_table_opts();
946 build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
947 }
948
949 fn create_table_opts(&self) -> TableOpts<'_> {
950 create_table_opts(
951 &self.engine_state,
952 &self.stack,
953 &self.config,
954 &self.table_config,
955 self.head,
956 self.row_offset,
957 )
958 }
959}
960
961impl Iterator for PagingTableCreator {
962 type Item = ShellResult<Vec<u8>>;
963
964 fn next(&mut self) -> Option<Self::Item> {
965 let batch;
966 let end;
967
968 match self.table_config.abbreviation {
969 Some(abbr) => {
970 (batch, _, end) = stream_collect_abbreviated(
971 &mut self.stream,
972 abbr,
973 self.engine_state.signals(),
974 self.head,
975 );
976 }
977 None => {
978 (batch, end) = stream_collect(
980 &mut self.stream,
981 self.config.table.stream_page_size.get() as usize,
982 self.config.table.batch_duration,
983 self.engine_state.signals(),
984 );
985 }
986 }
987
988 let batch_size = batch.len();
989
990 self.elements_displayed += batch_size;
992 self.reached_end = self.reached_end || end;
993
994 if batch.is_empty() {
995 return if self.elements_displayed == 0 && self.reached_end {
998 self.elements_displayed = 1;
1001 let result = create_empty_placeholder(
1002 "list",
1003 self.table_config.width,
1004 &self.engine_state,
1005 &self.stack,
1006 self.table_config.use_ansi_coloring,
1007 );
1008 let mut bytes = result.into_bytes();
1009 if !bytes.is_empty() {
1011 bytes.push(b'\n');
1012 }
1013 Some(Ok(bytes))
1014 } else {
1015 None
1016 };
1017 }
1018
1019 let table = self.build_table(batch);
1020
1021 self.row_offset += batch_size;
1022
1023 convert_table_to_output(
1024 table,
1025 self.engine_state.signals(),
1026 self.table_config.width,
1027 self.table_config.use_ansi_coloring,
1028 )
1029 }
1030}
1031
1032fn stream_collect(
1033 stream: impl Iterator<Item = Value>,
1034 size: usize,
1035 batch_duration: Duration,
1036 signals: &Signals,
1037) -> (Vec<Value>, bool) {
1038 let start_time = Instant::now();
1039 let mut end = true;
1040
1041 let mut batch = Vec::with_capacity(size);
1042 for (i, item) in stream.enumerate() {
1043 batch.push(item);
1044
1045 if (Instant::now() - start_time) >= batch_duration {
1047 end = false;
1048 break;
1049 }
1050
1051 if i + 1 == size {
1053 end = false;
1054 break;
1055 }
1056
1057 if signals.interrupted() {
1058 break;
1059 }
1060 }
1061
1062 (batch, end)
1063}
1064
1065fn stream_collect_abbreviated(
1066 stream: impl Iterator<Item = Value>,
1067 size: usize,
1068 signals: &Signals,
1069 span: Span,
1070) -> (Vec<Value>, usize, bool) {
1071 let mut end = true;
1072 let mut read = 0;
1073 let mut head = Vec::with_capacity(size);
1074 let mut tail = VecDeque::with_capacity(size);
1075
1076 if size == 0 {
1077 return (vec![], 0, false);
1078 }
1079
1080 for item in stream {
1081 read += 1;
1082
1083 if read <= size {
1084 head.push(item);
1085 } else if tail.len() < size {
1086 tail.push_back(item);
1087 } else {
1088 let _ = tail.pop_front();
1089 tail.push_back(item);
1090 }
1091
1092 if signals.interrupted() {
1093 end = false;
1094 break;
1095 }
1096 }
1097
1098 let have_filled_list = head.len() == size && tail.len() == size;
1099 if have_filled_list {
1100 let dummy = get_abbreviated_dummy(&head, &tail, span);
1101 head.insert(size, dummy)
1102 }
1103
1104 head.extend(tail);
1105
1106 (head, read, end)
1107}
1108
1109fn get_abbreviated_dummy(head: &[Value], tail: &VecDeque<Value>, span: Span) -> Value {
1110 let dummy = || Value::string(String::from("..."), span);
1111 let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1112
1113 if is_record_list {
1114 Value::record(
1116 head[0]
1117 .as_record()
1118 .expect("ok")
1119 .columns()
1120 .map(|key| (key.clone(), dummy()))
1121 .collect(),
1122 span,
1123 )
1124 } else {
1125 dummy()
1126 }
1127}
1128
1129fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1130 batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1131}
1132
1133fn render_path_name(
1134 path: &str,
1135 config: &Config,
1136 ls_colors: &LsColors,
1137 cwd: Option<&NuPath>,
1138 icons: bool,
1139 span: Span,
1140) -> Option<Value> {
1141 if !config.ls.use_ls_colors {
1142 return None;
1143 }
1144
1145 let fullpath = match cwd {
1146 Some(cwd) => PathBuf::from(cwd.join(path)),
1147 None => PathBuf::from(path),
1148 };
1149
1150 let stripped_path = nu_utils::strip_ansi_unlikely(path);
1151 let metadata = std::fs::symlink_metadata(fullpath);
1152 let has_metadata = metadata.is_ok();
1153 let style =
1154 ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1155
1156 let file_icon = icon_for_file(path, &None);
1157 let icon_style = lookup_ansi_color_style(file_icon.color);
1158
1159 let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1161 let show_clickable_links = config.ls.clickable_links
1163 && !in_ssh_session
1164 && has_metadata
1165 && config.shell_integration.osc8;
1166
1167 let ansi_style = style
1175 .map(Style::to_nu_ansi_term_style)
1176 .unwrap_or(nu_ansi_term::Style {
1177 foreground: Some(nu_ansi_term::Color::Default),
1178 background: Some(nu_ansi_term::Color::Default),
1179 is_bold: false,
1180 is_dimmed: false,
1181 is_italic: false,
1182 is_underline: false,
1183 is_blink: false,
1184 is_reverse: false,
1185 is_hidden: false,
1186 is_strikethrough: false,
1187 prefix_with_reset: false,
1188 });
1189
1190 let full_path = std::path::absolute(stripped_path.as_ref())
1191 .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1192
1193 let full_path_link = make_clickable_link(
1194 full_path.display().to_string(),
1195 Some(path),
1196 show_clickable_links,
1197 );
1198
1199 let val = if icons {
1200 format!(
1201 "{} {}",
1202 icon_style.paint(String::from(file_icon.icon)),
1203 ansi_style.paint(full_path_link)
1204 )
1205 } else {
1206 ansi_style.paint(full_path_link).to_string()
1207 };
1208
1209 Some(Value::string(val, span))
1210}
1211
1212fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1213 if !use_ansi_coloring {
1216 nu_utils::strip_ansi_string_likely(output)
1218 } else {
1219 output
1221 }
1222}
1223
1224fn create_empty_placeholder(
1225 value_type_name: &str,
1226 termwidth: usize,
1227 engine_state: &EngineState,
1228 stack: &Stack,
1229 use_ansi_coloring: bool,
1230) -> String {
1231 let config = stack.get_config(engine_state);
1232 if !config.table.show_empty {
1233 return String::new();
1234 }
1235
1236 let cell = format!("empty {value_type_name}");
1237 let mut table = NuTable::new(1, 1);
1238 table.insert((0, 0), cell);
1239 table.set_data_style(TextStyle::default().dimmed());
1240 let mut out = TableOutput::from_table(table, false, false);
1241
1242 let style_computer = &StyleComputer::from_config(engine_state, stack);
1243 configure_table(&mut out, &config, style_computer, TableMode::default());
1244
1245 if !use_ansi_coloring {
1246 out.table.clear_all_colors();
1247 }
1248
1249 out.table
1250 .draw(termwidth)
1251 .expect("Could not create empty table placeholder")
1252}
1253
1254fn convert_table_to_output(
1255 table: ShellResult<Option<String>>,
1256 signals: &Signals,
1257 term_width: usize,
1258 use_ansi_coloring: bool,
1259) -> Option<ShellResult<Vec<u8>>> {
1260 match table {
1261 Ok(Some(table)) => {
1262 let table = maybe_strip_color(table, use_ansi_coloring);
1263
1264 let mut bytes = table.as_bytes().to_vec();
1265 bytes.push(b'\n'); Some(Ok(bytes))
1268 }
1269 Ok(None) => {
1270 let msg = if signals.interrupted() {
1271 String::from("")
1272 } else {
1273 format!("Couldn't fit table into {term_width} columns!")
1276 };
1277
1278 Some(Ok(msg.as_bytes().to_vec()))
1279 }
1280 Err(err) => Some(Err(err)),
1281 }
1282}
1283
1284const SUPPORTED_TABLE_MODES: &[&str] = &[
1285 "basic",
1286 "compact",
1287 "compact_double",
1288 "default",
1289 "frameless",
1290 "heavy",
1291 "light",
1292 "none",
1293 "reinforced",
1294 "rounded",
1295 "thin",
1296 "with_love",
1297 "psql",
1298 "markdown",
1299 "dots",
1300 "restructured",
1301 "ascii_rounded",
1302 "basic_compact",
1303 "single",
1304 "double",
1305];
1306
1307fn supported_table_modes() -> Vec<Value> {
1308 SUPPORTED_TABLE_MODES
1309 .iter()
1310 .copied()
1311 .map(Value::test_string)
1312 .collect()
1313}
1314
1315fn create_table_opts<'a>(
1316 engine_state: &'a EngineState,
1317 stack: &'a Stack,
1318 cfg: &'a Config,
1319 table_cfg: &'a TableConfig,
1320 span: Span,
1321 offset: usize,
1322) -> TableOpts<'a> {
1323 let comp = StyleComputer::from_config(engine_state, stack);
1324 let signals = engine_state.signals();
1325 let offset = table_cfg.index.unwrap_or(0) + offset;
1326 let index = table_cfg.index.is_none();
1327 let width = table_cfg.width;
1328 let theme = table_cfg.theme;
1329
1330 TableOpts::new(
1331 cfg,
1332 comp,
1333 signals,
1334 span,
1335 width,
1336 theme,
1337 offset,
1338 index,
1339 table_cfg.width_priority_columns.clone(),
1340 )
1341}
1342
1343fn get_width_priority_columns(metadata: Option<&PipelineMetadata>) -> Vec<String> {
1347 let mut width_priority_columns = Vec::new();
1348
1349 let Some(metadata) = metadata else {
1350 return width_priority_columns;
1351 };
1352
1353 let Some(value) = metadata
1354 .custom
1355 .get(TABLE_WIDTH_PRIORITY_COLUMNS_METADATA_KEY)
1356 else {
1357 return width_priority_columns;
1358 };
1359
1360 let Ok(values) = value.as_list() else {
1361 return width_priority_columns;
1362 };
1363
1364 for value in values {
1365 if let Ok(column_name) = value.as_str()
1366 && !column_name.is_empty()
1367 && !width_priority_columns
1368 .iter()
1369 .any(|column| column == column_name)
1370 {
1371 width_priority_columns.push(column_name.to_string());
1372 }
1373 }
1374
1375 width_priority_columns
1376}
1377
1378fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1379 #[cfg(feature = "os")]
1380 let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1381
1382 #[cfg(not(feature = "os"))]
1383 let cwd = None;
1384
1385 Ok(cwd)
1386}
1387
1388fn get_table_width(width_param: Option<i64>) -> usize {
1389 if let Some(col) = width_param {
1390 col as usize
1391 } else if let Ok((w, _h)) = terminal_size() {
1392 w as usize
1393 } else {
1394 DEFAULT_TABLE_WIDTH
1395 }
1396}
1397
1398fn get_hex_styles(engine_state: &EngineState, stack: &mut Stack) -> HexStyles {
1399 let comp = StyleComputer::from_config(engine_state, stack);
1400 let null = Value::nothing(Span::unknown());
1401 HexStyles {
1402 null_char: comp.compute("binary_null_char", &null),
1403 printable: comp.compute("binary_printable", &null),
1404 whitespace: comp.compute("binary_whitespace", &null),
1405 ascii_other: comp.compute("binary_ascii_other", &null),
1406 non_ascii: comp.compute("binary_non_ascii", &null),
1407 }
1408}