1use std::{collections::VecDeque, io::Read, path::PathBuf, str::FromStr};
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 STREAM_PAGE_SIZE: usize = 1000;
29const DEFAULT_TABLE_WIDTH: usize = 80;
30
31#[derive(Clone)]
32pub struct Table;
33
34impl Command for Table {
36 fn name(&self) -> &str {
37 "table"
38 }
39
40 fn description(&self) -> &str {
41 "Render the table."
42 }
43
44 fn extra_description(&self) -> &str {
45 "If the table contains a column called 'index', this column is used as the table index instead of the usual continuous index."
46 }
47
48 fn search_terms(&self) -> Vec<&str> {
49 vec!["display", "render"]
50 }
51
52 fn signature(&self) -> Signature {
53 Signature::build("table")
54 .input_output_types(vec![(Type::Any, Type::Any)])
55 .named(
57 "theme",
58 SyntaxShape::String,
59 "set a table mode/theme",
60 Some('t'),
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!("{}, but found '{}'.", err, 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::ByteStream(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::ByteStream(
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 if let Some(val) =
699 render_path_name(val, &config, &ls_colors, input.cwd.clone(), span)
700 {
701 *value = val;
702 }
703 }
704 }
705 }
706 value
707 })
708 }
709 Some(PipelineMetadata {
711 data_source: DataSource::HtmlThemes,
712 ..
713 }) => {
714 stream.map(|mut value| {
715 if let Value::Record { val: record, .. } = &mut value {
716 for (rec_col, rec_val) in record.to_mut().iter_mut() {
717 if rec_col != "name" {
719 continue;
720 }
721 let span = rec_val.span();
725 if let Value::String { val, .. } = rec_val {
726 let s = match color_from_hex(val) {
727 Ok(c) => match c {
728 Some(c) => c.normal(),
730 None => nu_ansi_term::Style::default(),
731 },
732 Err(_) => nu_ansi_term::Style::default(),
733 };
734 *rec_val = Value::string(
735 s.paint(&*val).to_string(),
737 span,
738 );
739 }
740 }
741 }
742 value
743 })
744 }
745 _ => stream,
746 };
747
748 let paginator = PagingTableCreator::new(
749 input.call.head,
750 stream,
751 input.engine_state.clone(),
754 input.stack.clone(),
755 input.cfg,
756 cfg,
757 );
758 let stream = ByteStream::from_result_iter(
759 paginator,
760 input.call.head,
761 Signals::empty(),
762 ByteStreamType::String,
763 );
764 Ok(PipelineData::ByteStream(stream, None))
765}
766
767fn make_clickable_link(
768 full_path: String,
769 link_name: Option<&str>,
770 show_clickable_links: bool,
771) -> String {
772 #[cfg(any(
775 unix,
776 windows,
777 target_os = "redox",
778 target_os = "wasi",
779 target_os = "hermit"
780 ))]
781 if show_clickable_links {
782 format!(
783 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
784 match Url::from_file_path(full_path.clone()) {
785 Ok(url) => url.to_string(),
786 Err(_) => full_path.clone(),
787 },
788 link_name.unwrap_or(full_path.as_str())
789 )
790 } else {
791 match link_name {
792 Some(link_name) => link_name.to_string(),
793 None => full_path,
794 }
795 }
796
797 #[cfg(not(any(
798 unix,
799 windows,
800 target_os = "redox",
801 target_os = "wasi",
802 target_os = "hermit"
803 )))]
804 match link_name {
805 Some(link_name) => link_name.to_string(),
806 None => full_path,
807 }
808}
809
810struct PagingTableCreator {
811 head: Span,
812 stream: ValueIterator,
813 engine_state: EngineState,
814 stack: Stack,
815 elements_displayed: usize,
816 reached_end: bool,
817 table_config: TableConfig,
818 row_offset: usize,
819 config: std::sync::Arc<Config>,
820}
821
822impl PagingTableCreator {
823 fn new(
824 head: Span,
825 stream: ListStream,
826 engine_state: EngineState,
827 stack: Stack,
828 table_config: TableConfig,
829 config: std::sync::Arc<Config>,
830 ) -> Self {
831 PagingTableCreator {
832 head,
833 stream: stream.into_inner(),
834 engine_state,
835 stack,
836 config,
837 table_config,
838 elements_displayed: 0,
839 reached_end: false,
840 row_offset: 0,
841 }
842 }
843
844 fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
845 if batch.is_empty() {
846 return Ok(None);
847 }
848
849 let opts = self.create_table_opts();
850 build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
851 }
852
853 fn create_table_opts(&self) -> TableOpts<'_> {
854 create_table_opts(
855 &self.engine_state,
856 &self.stack,
857 &self.config,
858 &self.table_config,
859 self.head,
860 self.row_offset,
861 )
862 }
863}
864
865impl Iterator for PagingTableCreator {
866 type Item = ShellResult<Vec<u8>>;
867
868 fn next(&mut self) -> Option<Self::Item> {
869 let batch;
870 let end;
871
872 match self.table_config.abbreviation {
873 Some(abbr) => {
874 (batch, _, end) =
875 stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals());
876 }
877 None => {
878 (batch, end) = stream_collect(
880 &mut self.stream,
881 STREAM_PAGE_SIZE,
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 signals: &Signals,
935) -> (Vec<Value>, bool) {
936 let start_time = Instant::now();
937 let mut end = true;
938
939 let mut batch = Vec::with_capacity(size);
940 for (i, item) in stream.enumerate() {
941 batch.push(item);
942
943 if (Instant::now() - start_time).as_secs() >= 1 {
945 end = false;
946 break;
947 }
948
949 if i + 1 == size {
950 end = false;
951 break;
952 }
953
954 if signals.interrupted() {
955 break;
956 }
957 }
958
959 (batch, end)
960}
961
962fn stream_collect_abbriviated(
963 stream: impl Iterator<Item = Value>,
964 size: usize,
965 signals: &Signals,
966) -> (Vec<Value>, usize, bool) {
967 let mut end = true;
968 let mut read = 0;
969 let mut head = Vec::with_capacity(size);
970 let mut tail = VecDeque::with_capacity(size);
971
972 if size == 0 {
973 return (vec![], 0, false);
974 }
975
976 for item in stream {
977 read += 1;
978
979 if read <= size {
980 head.push(item);
981 } else if tail.len() < size {
982 tail.push_back(item);
983 } else {
984 let _ = tail.pop_front();
985 tail.push_back(item);
986 }
987
988 if signals.interrupted() {
989 end = false;
990 break;
991 }
992 }
993
994 let have_filled_list = head.len() == size && tail.len() == size;
995 if have_filled_list {
996 let dummy = get_abbriviated_dummy(&head, &tail);
997 head.insert(size, dummy)
998 }
999
1000 head.extend(tail);
1001
1002 (head, read, end)
1003}
1004
1005fn get_abbriviated_dummy(head: &[Value], tail: &VecDeque<Value>) -> Value {
1006 let dummy = || Value::string(String::from("..."), Span::unknown());
1007 let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1008
1009 if is_record_list {
1010 Value::record(
1012 head[0]
1013 .as_record()
1014 .expect("ok")
1015 .columns()
1016 .map(|key| (key.clone(), dummy()))
1017 .collect(),
1018 Span::unknown(),
1019 )
1020 } else {
1021 dummy()
1022 }
1023}
1024
1025fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1026 batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1027}
1028
1029fn render_path_name(
1030 path: &str,
1031 config: &Config,
1032 ls_colors: &LsColors,
1033 cwd: Option<NuPathBuf>,
1034 span: Span,
1035) -> Option<Value> {
1036 if !config.ls.use_ls_colors {
1037 return None;
1038 }
1039
1040 let fullpath = match cwd {
1041 Some(cwd) => PathBuf::from(cwd.join(path)),
1042 None => PathBuf::from(path),
1043 };
1044
1045 let stripped_path = nu_utils::strip_ansi_unlikely(path);
1046 let metadata = std::fs::symlink_metadata(fullpath);
1047 let has_metadata = metadata.is_ok();
1048 let style =
1049 ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1050
1051 let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1053 let show_clickable_links = config.ls.clickable_links
1055 && !in_ssh_session
1056 && has_metadata
1057 && config.shell_integration.osc8;
1058
1059 let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default();
1060
1061 let full_path = PathBuf::from(stripped_path.as_ref())
1062 .canonicalize()
1063 .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1064
1065 let full_path_link = make_clickable_link(
1066 full_path.display().to_string(),
1067 Some(path),
1068 show_clickable_links,
1069 );
1070
1071 let val = ansi_style.paint(full_path_link).to_string();
1072 Some(Value::string(val, span))
1073}
1074
1075fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1076 if !use_ansi_coloring {
1079 nu_utils::strip_ansi_string_likely(output)
1081 } else {
1082 output
1084 }
1085}
1086
1087fn create_empty_placeholder(
1088 value_type_name: &str,
1089 termwidth: usize,
1090 engine_state: &EngineState,
1091 stack: &Stack,
1092 use_ansi_coloring: bool,
1093) -> String {
1094 let config = stack.get_config(engine_state);
1095 if !config.table.show_empty {
1096 return String::new();
1097 }
1098
1099 let cell = format!("empty {}", value_type_name);
1100 let mut table = NuTable::new(1, 1);
1101 table.insert((0, 0), cell);
1102 table.set_data_style(TextStyle::default().dimmed());
1103 let mut out = TableOutput::from_table(table, false, false);
1104
1105 let style_computer = &StyleComputer::from_config(engine_state, stack);
1106 configure_table(&mut out, &config, style_computer, TableMode::default());
1107
1108 if !use_ansi_coloring {
1109 out.table.clear_all_colors();
1110 }
1111
1112 out.table
1113 .draw(termwidth)
1114 .expect("Could not create empty table placeholder")
1115}
1116
1117fn convert_table_to_output(
1118 table: ShellResult<Option<String>>,
1119 signals: &Signals,
1120 term_width: usize,
1121 use_ansi_coloring: bool,
1122) -> Option<ShellResult<Vec<u8>>> {
1123 match table {
1124 Ok(Some(table)) => {
1125 let table = maybe_strip_color(table, use_ansi_coloring);
1126
1127 let mut bytes = table.as_bytes().to_vec();
1128 bytes.push(b'\n'); Some(Ok(bytes))
1131 }
1132 Ok(None) => {
1133 let msg = if signals.interrupted() {
1134 String::from("")
1135 } else {
1136 format!("Couldn't fit table into {} columns!", term_width)
1139 };
1140
1141 Some(Ok(msg.as_bytes().to_vec()))
1142 }
1143 Err(err) => Some(Err(err)),
1144 }
1145}
1146
1147fn supported_table_modes() -> Vec<Value> {
1148 vec![
1149 Value::test_string("basic"),
1150 Value::test_string("compact"),
1151 Value::test_string("compact_double"),
1152 Value::test_string("default"),
1153 Value::test_string("heavy"),
1154 Value::test_string("light"),
1155 Value::test_string("none"),
1156 Value::test_string("reinforced"),
1157 Value::test_string("rounded"),
1158 Value::test_string("thin"),
1159 Value::test_string("with_love"),
1160 Value::test_string("psql"),
1161 Value::test_string("markdown"),
1162 Value::test_string("dots"),
1163 Value::test_string("restructured"),
1164 Value::test_string("ascii_rounded"),
1165 Value::test_string("basic_compact"),
1166 Value::test_string("single"),
1167 ]
1168}
1169
1170fn create_table_opts<'a>(
1171 engine_state: &'a EngineState,
1172 stack: &'a Stack,
1173 cfg: &'a Config,
1174 table_cfg: &'a TableConfig,
1175 span: Span,
1176 offset: usize,
1177) -> TableOpts<'a> {
1178 let comp = StyleComputer::from_config(engine_state, stack);
1179 let signals = engine_state.signals();
1180 let offset = table_cfg.index.unwrap_or(0) + offset;
1181 let index = table_cfg.index.is_none();
1182 let width = table_cfg.width;
1183 let theme = table_cfg.theme;
1184
1185 TableOpts::new(cfg, comp, signals, span, width, theme, offset, index)
1186}
1187
1188fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1189 #[cfg(feature = "os")]
1190 let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1191
1192 #[cfg(not(feature = "os"))]
1193 let cwd = None;
1194
1195 Ok(cwd)
1196}
1197
1198fn get_table_width(width_param: Option<i64>) -> usize {
1199 if let Some(col) = width_param {
1200 col as usize
1201 } else if let Ok((w, _h)) = terminal_size() {
1202 w as usize
1203 } else {
1204 DEFAULT_TABLE_WIDTH
1205 }
1206}