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