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::{color_from_hex, StyleComputer, TextStyle};
12use nu_engine::{command_prelude::*, env_to_string};
13use nu_path::form::Absolute;
14use nu_pretty_hex::HexConfig;
15use nu_protocol::{
16 shell_error::io::IoError, ByteStream, Config, DataSource, ListStream, PipelineMetadata,
17 Signals, TableMode, ValueIterator,
18};
19use nu_table::{
20 common::configure_table, CollapsedTable, ExpandedTable, JustTable, NuRecordsValue, NuTable,
21 StringResult, TableOpts, TableOutput,
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] [2 [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] [2 [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] [2 [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] [2 [4 4]]] | table -i true"#,
195 result: None,
196 },
197 Example {
198 description:
199 "Set the starting number of the #/index column to 100 for a single run",
200 example: r#"[[a b]; [1 2] [2 [4 4]]] | table -i 100"#,
201 result: None,
202 },
203 Example {
204 description: "Force hiding of the #/index column for a single run",
205 example: r#"[[a b]; [1 2] [2 [4 4]]] | table -i false"#,
206 result: None,
207 },
208 ]
209 }
210}
211
212#[derive(Debug, Clone)]
213struct TableConfig {
214 view: TableView,
215 width: usize,
216 theme: TableMode,
217 abbreviation: Option<usize>,
218 index: Option<usize>,
219 use_ansi_coloring: bool,
220}
221
222impl TableConfig {
223 fn new(
224 view: TableView,
225 width: usize,
226 theme: TableMode,
227 abbreviation: Option<usize>,
228 index: Option<usize>,
229 use_ansi_coloring: bool,
230 ) -> Self {
231 Self {
232 view,
233 width,
234 theme,
235 abbreviation,
236 index,
237 use_ansi_coloring,
238 }
239 }
240}
241
242#[derive(Debug, Clone)]
243enum TableView {
244 General,
245 Collapsed,
246 Expanded {
247 limit: Option<usize>,
248 flatten: bool,
249 flatten_separator: Option<String>,
250 },
251}
252
253struct CLIArgs {
254 width: Option<i64>,
255 abbrivation: Option<usize>,
256 theme: TableMode,
257 expand: bool,
258 expand_limit: Option<usize>,
259 expand_flatten: bool,
260 expand_flatten_separator: Option<String>,
261 collapse: bool,
262 index: Option<usize>,
263 use_ansi_coloring: bool,
264}
265
266fn parse_table_config(
267 call: &Call,
268 state: &EngineState,
269 stack: &mut Stack,
270) -> ShellResult<TableConfig> {
271 let args = get_cli_args(call, state, stack)?;
272 let table_view = get_table_view(&args);
273 let term_width = get_table_width(args.width);
274
275 let cfg = TableConfig::new(
276 table_view,
277 term_width,
278 args.theme,
279 args.abbrivation,
280 args.index,
281 args.use_ansi_coloring,
282 );
283
284 Ok(cfg)
285}
286
287fn get_table_view(args: &CLIArgs) -> TableView {
288 match (args.expand, args.collapse) {
289 (false, false) => TableView::General,
290 (_, true) => TableView::Collapsed,
291 (true, _) => TableView::Expanded {
292 limit: args.expand_limit,
293 flatten: args.expand_flatten,
294 flatten_separator: args.expand_flatten_separator.clone(),
295 },
296 }
297}
298
299fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
300 let width: Option<i64> = call.get_flag(state, stack, "width")?;
301 let expand: bool = call.has_flag(state, stack, "expand")?;
302 let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
303 let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
304 let expand_flatten_separator: Option<String> =
305 call.get_flag(state, stack, "flatten-separator")?;
306 let collapse: bool = call.has_flag(state, stack, "collapse")?;
307 let abbrivation: Option<usize> = call
308 .get_flag(state, stack, "abbreviated")?
309 .or_else(|| stack.get_config(state).table.abbreviated_row_count);
310 let theme =
311 get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
312 let index = get_index_flag(call, state, stack)?;
313
314 let use_ansi_coloring = stack.get_config(state).use_ansi_coloring.get(state);
315
316 Ok(CLIArgs {
317 theme,
318 abbrivation,
319 collapse,
320 expand,
321 expand_limit,
322 expand_flatten,
323 expand_flatten_separator,
324 width,
325 index,
326 use_ansi_coloring,
327 })
328}
329
330fn get_index_flag(
331 call: &Call,
332 state: &EngineState,
333 stack: &mut Stack,
334) -> ShellResult<Option<usize>> {
335 let index: Option<Value> = call.get_flag(state, stack, "index")?;
336 let value = match index {
337 Some(value) => value,
338 None => return Ok(Some(0)),
339 };
340 let span = value.span();
341
342 match value {
343 Value::Bool { val, .. } => {
344 if val {
345 Ok(Some(0))
346 } else {
347 Ok(None)
348 }
349 }
350 Value::Int { val, .. } => {
351 if val < 0 {
352 Err(ShellError::UnsupportedInput {
353 msg: String::from("got a negative integer"),
354 input: val.to_string(),
355 msg_span: call.span(),
356 input_span: span,
357 })
358 } else {
359 Ok(Some(val as usize))
360 }
361 }
362 Value::Nothing { .. } => Ok(Some(0)),
363 _ => Err(ShellError::CantConvert {
364 to_type: String::from("index"),
365 from_type: String::new(),
366 span: call.span(),
367 help: Some(String::from("supported values: [bool, int, nothing]")),
368 }),
369 }
370}
371
372fn get_theme_flag(
373 call: &Call,
374 state: &EngineState,
375 stack: &mut Stack,
376) -> ShellResult<Option<TableMode>> {
377 call.get_flag(state, stack, "theme")?
378 .map(|theme: String| {
379 TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
380 to_type: String::from("theme"),
381 from_type: String::from("string"),
382 span: call.span(),
383 help: Some(format!("{}, but found '{}'.", err, theme)),
384 })
385 })
386 .transpose()
387}
388
389struct CmdInput<'a> {
390 engine_state: &'a EngineState,
391 stack: &'a mut Stack,
392 call: &'a Call<'a>,
393 data: PipelineData,
394 cfg: TableConfig,
395 cwd: Option<NuPathBuf>,
396}
397
398impl<'a> CmdInput<'a> {
399 fn parse(
400 engine_state: &'a EngineState,
401 stack: &'a mut Stack,
402 call: &'a Call<'a>,
403 data: PipelineData,
404 ) -> ShellResult<Self> {
405 let cfg = parse_table_config(call, engine_state, stack)?;
406 let cwd = get_cwd(engine_state, stack)?;
407
408 Ok(Self {
409 engine_state,
410 stack,
411 call,
412 data,
413 cfg,
414 cwd,
415 })
416 }
417
418 fn get_config(&self) -> std::sync::Arc<Config> {
419 self.stack.get_config(self.engine_state)
420 }
421}
422
423fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
424 let span = input.data.span().unwrap_or(input.call.head);
425 match input.data {
426 PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
428 PipelineData::ByteStream(pretty_hex_stream(stream, input.call.head), None),
429 ),
430 PipelineData::ByteStream(..) => Ok(input.data),
431 PipelineData::Value(Value::Binary { val, .. }, ..) => {
432 let signals = input.engine_state.signals().clone();
433 let stream = ByteStream::read_binary(val, input.call.head, signals);
434 Ok(PipelineData::ByteStream(
435 pretty_hex_stream(stream, input.call.head),
436 None,
437 ))
438 }
439 PipelineData::Value(Value::List { vals, .. }, metadata) => {
441 let signals = input.engine_state.signals().clone();
442 let stream = ListStream::new(vals.into_iter(), span, signals);
443 input.data = PipelineData::Empty;
444
445 handle_row_stream(input, stream, metadata)
446 }
447 PipelineData::ListStream(stream, metadata) => {
448 input.data = PipelineData::Empty;
449 handle_row_stream(input, stream, metadata)
450 }
451 PipelineData::Value(Value::Record { val, .. }, ..) => {
452 input.data = PipelineData::Empty;
453 handle_record(input, val.into_owned())
454 }
455 PipelineData::Value(Value::Error { error, .. }, ..) => {
456 Err(*error)
459 }
460 PipelineData::Value(Value::Custom { val, .. }, ..) => {
461 let base_pipeline = val.to_base_value(span)?.into_pipeline_data();
462 Table.run(input.engine_state, input.stack, input.call, base_pipeline)
463 }
464 PipelineData::Value(Value::Range { val, .. }, metadata) => {
465 let signals = input.engine_state.signals().clone();
466 let stream =
467 ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
468 input.data = PipelineData::Empty;
469 handle_row_stream(input, stream, metadata)
470 }
471 x => Ok(x),
472 }
473}
474
475fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream {
476 let mut cfg = HexConfig {
477 title: true,
479 length: stream.known_size().and_then(|sz| sz.try_into().ok()),
481 ..HexConfig::default()
482 };
483
484 debug_assert!(cfg.width > 0, "the default hex config width was zero");
486
487 let mut read_buf = Vec::with_capacity(cfg.width);
488
489 let mut reader = if let Some(reader) = stream.reader() {
490 reader
491 } else {
492 return ByteStream::read_string("".into(), span, Signals::empty());
494 };
495
496 ByteStream::from_fn(
497 span,
498 Signals::empty(),
499 ByteStreamType::String,
500 move |buffer| {
501 let mut write_buf = std::mem::take(buffer);
503 write_buf.clear();
504 let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
506
507 if cfg.title {
509 nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error");
510 cfg.title = false;
511
512 *buffer = write_buf.into_bytes();
514
515 Ok(true)
516 } else {
517 read_buf.clear();
519 (&mut reader)
520 .take(cfg.width as u64)
521 .read_to_end(&mut read_buf)
522 .map_err(|err| IoError::new(err.kind(), span, None))?;
523
524 if !read_buf.is_empty() {
525 nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true))
526 .expect("format error");
527 write_buf.push('\n');
528
529 cfg.address_offset += read_buf.len();
531
532 *buffer = write_buf.into_bytes();
534
535 Ok(true)
536 } else {
537 Ok(false)
538 }
539 }
540 },
541 )
542}
543
544fn handle_record(input: CmdInput, mut record: Record) -> ShellResult<PipelineData> {
545 let span = input.data.span().unwrap_or(input.call.head);
546
547 if record.is_empty() {
548 let value =
549 create_empty_placeholder("record", input.cfg.width, input.engine_state, input.stack);
550 let value = Value::string(value, span);
551 return Ok(value.into_pipeline_data());
552 };
553
554 if let Some(limit) = input.cfg.abbreviation {
555 record = make_record_abbreviation(record, limit);
556 }
557
558 let config = input.get_config();
559 let opts = create_table_opts(
560 input.engine_state,
561 input.stack,
562 &config,
563 &input.cfg,
564 span,
565 0,
566 );
567 let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
568
569 let result = match result {
570 Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
571 None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
572 };
573
574 let val = Value::string(result, span);
575 let data = val.into_pipeline_data();
576
577 Ok(data)
578}
579
580fn make_record_abbreviation(mut record: Record, limit: usize) -> Record {
581 if record.len() <= limit * 2 + 1 {
582 return record;
583 }
584
585 let prev_len = record.len();
587 let mut record_iter = record.into_iter();
588 record = Record::with_capacity(limit * 2 + 1);
589 record.extend(record_iter.by_ref().take(limit));
590 record.push(String::from("..."), Value::string("...", Span::unknown()));
591 record.extend(record_iter.skip(prev_len - 2 * limit));
592 record
593}
594
595fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
596 if signals.interrupted() {
597 "".into()
598 } else {
599 format!("Couldn't fit table into {term_width} columns!")
602 }
603}
604
605fn build_table_kv(
606 record: Record,
607 table_view: TableView,
608 opts: TableOpts<'_>,
609 span: Span,
610) -> StringResult {
611 match table_view {
612 TableView::General => JustTable::kv_table(&record, opts),
613 TableView::Expanded {
614 limit,
615 flatten,
616 flatten_separator,
617 } => {
618 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
619 ExpandedTable::new(limit, flatten, sep).build_map(&record, opts)
620 }
621 TableView::Collapsed => {
622 let value = Value::record(record, span);
623 CollapsedTable::build(value, opts)
624 }
625 }
626}
627
628fn build_table_batch(
629 mut vals: Vec<Value>,
630 view: TableView,
631 opts: TableOpts<'_>,
632 span: Span,
633) -> StringResult {
634 for val in &mut vals {
637 let span = val.span();
638
639 if let Value::Custom { val: custom, .. } = val {
640 *val = custom
641 .to_base_value(span)
642 .or_else(|err| Result::<_, ShellError>::Ok(Value::error(err, span)))
643 .expect("error converting custom value to base value")
644 }
645 }
646
647 match view {
648 TableView::General => JustTable::table(&vals, opts),
649 TableView::Expanded {
650 limit,
651 flatten,
652 flatten_separator,
653 } => {
654 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
655 ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
656 }
657 TableView::Collapsed => {
658 let value = Value::list(vals, span);
659 CollapsedTable::build(value, opts)
660 }
661 }
662}
663
664fn handle_row_stream(
665 input: CmdInput<'_>,
666 stream: ListStream,
667 metadata: Option<PipelineMetadata>,
668) -> ShellResult<PipelineData> {
669 let cfg = input.get_config();
670 let stream = match metadata.as_ref() {
671 Some(PipelineMetadata {
673 data_source: DataSource::Ls,
674 ..
675 }) => {
676 let config = cfg.clone();
677 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
678 Some(v) => Some(env_to_string(
679 "LS_COLORS",
680 v,
681 input.engine_state,
682 input.stack,
683 )?),
684 None => None,
685 };
686 let ls_colors = get_ls_colors(ls_colors_env_str);
687
688 stream.map(move |mut value| {
689 if let Value::Record { val: record, .. } = &mut value {
690 if let Some(value) = record.to_mut().get_mut("name") {
692 let span = value.span();
693 if let Value::String { val, .. } = value {
694 if let Some(val) =
695 render_path_name(val, &config, &ls_colors, input.cwd.clone(), span)
696 {
697 *value = val;
698 }
699 }
700 }
701 }
702 value
703 })
704 }
705 Some(PipelineMetadata {
707 data_source: DataSource::HtmlThemes,
708 ..
709 }) => {
710 stream.map(|mut value| {
711 if let Value::Record { val: record, .. } = &mut value {
712 for (rec_col, rec_val) in record.to_mut().iter_mut() {
713 if rec_col != "name" {
715 continue;
716 }
717 let span = rec_val.span();
721 if let Value::String { val, .. } = rec_val {
722 let s = match color_from_hex(val) {
723 Ok(c) => match c {
724 Some(c) => c.normal(),
726 None => nu_ansi_term::Style::default(),
727 },
728 Err(_) => nu_ansi_term::Style::default(),
729 };
730 *rec_val = Value::string(
731 s.paint(&*val).to_string(),
733 span,
734 );
735 }
736 }
737 }
738 value
739 })
740 }
741 _ => stream,
742 };
743
744 let paginator = PagingTableCreator::new(
745 input.call.head,
746 stream,
747 input.engine_state.clone(),
750 input.stack.clone(),
751 input.cfg,
752 cfg,
753 );
754 let stream = ByteStream::from_result_iter(
755 paginator,
756 input.call.head,
757 Signals::empty(),
758 ByteStreamType::String,
759 );
760 Ok(PipelineData::ByteStream(stream, None))
761}
762
763fn make_clickable_link(
764 full_path: String,
765 link_name: Option<&str>,
766 show_clickable_links: bool,
767) -> String {
768 #[cfg(any(
771 unix,
772 windows,
773 target_os = "redox",
774 target_os = "wasi",
775 target_os = "hermit"
776 ))]
777 if show_clickable_links {
778 format!(
779 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
780 match Url::from_file_path(full_path.clone()) {
781 Ok(url) => url.to_string(),
782 Err(_) => full_path.clone(),
783 },
784 link_name.unwrap_or(full_path.as_str())
785 )
786 } else {
787 match link_name {
788 Some(link_name) => link_name.to_string(),
789 None => full_path,
790 }
791 }
792
793 #[cfg(not(any(
794 unix,
795 windows,
796 target_os = "redox",
797 target_os = "wasi",
798 target_os = "hermit"
799 )))]
800 match link_name {
801 Some(link_name) => link_name.to_string(),
802 None => full_path,
803 }
804}
805
806struct PagingTableCreator {
807 head: Span,
808 stream: ValueIterator,
809 engine_state: EngineState,
810 stack: Stack,
811 elements_displayed: usize,
812 reached_end: bool,
813 table_config: TableConfig,
814 row_offset: usize,
815 config: std::sync::Arc<Config>,
816}
817
818impl PagingTableCreator {
819 fn new(
820 head: Span,
821 stream: ListStream,
822 engine_state: EngineState,
823 stack: Stack,
824 table_config: TableConfig,
825 config: std::sync::Arc<Config>,
826 ) -> Self {
827 PagingTableCreator {
828 head,
829 stream: stream.into_inner(),
830 engine_state,
831 stack,
832 config,
833 table_config,
834 elements_displayed: 0,
835 reached_end: false,
836 row_offset: 0,
837 }
838 }
839
840 fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
841 if batch.is_empty() {
842 return Ok(None);
843 }
844
845 let opts = self.create_table_opts();
846 build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
847 }
848
849 fn create_table_opts(&self) -> TableOpts<'_> {
850 create_table_opts(
851 &self.engine_state,
852 &self.stack,
853 &self.config,
854 &self.table_config,
855 self.head,
856 self.row_offset,
857 )
858 }
859}
860
861impl Iterator for PagingTableCreator {
862 type Item = ShellResult<Vec<u8>>;
863
864 fn next(&mut self) -> Option<Self::Item> {
865 let batch;
866 let end;
867
868 match self.table_config.abbreviation {
869 Some(abbr) => {
870 (batch, _, end) =
871 stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals());
872 }
873 None => {
874 (batch, end) = stream_collect(
876 &mut self.stream,
877 STREAM_PAGE_SIZE,
878 self.engine_state.signals(),
879 );
880 }
881 }
882
883 let batch_size = batch.len();
884
885 self.elements_displayed += batch_size;
887 self.reached_end = self.reached_end || end;
888
889 if batch.is_empty() {
890 return if self.elements_displayed == 0 && self.reached_end {
893 self.elements_displayed = 1;
896 let result = create_empty_placeholder(
897 "list",
898 self.table_config.width,
899 &self.engine_state,
900 &self.stack,
901 );
902 let mut bytes = result.into_bytes();
903 if !bytes.is_empty() {
905 bytes.push(b'\n');
906 }
907 Some(Ok(bytes))
908 } else {
909 None
910 };
911 }
912
913 let table = self.build_table(batch);
914
915 self.row_offset += batch_size;
916
917 convert_table_to_output(
918 table,
919 self.engine_state.signals(),
920 self.table_config.width,
921 self.table_config.use_ansi_coloring,
922 )
923 }
924}
925
926fn stream_collect(
927 stream: impl Iterator<Item = Value>,
928 size: usize,
929 signals: &Signals,
930) -> (Vec<Value>, bool) {
931 let start_time = Instant::now();
932 let mut end = true;
933
934 let mut batch = Vec::with_capacity(size);
935 for (i, item) in stream.enumerate() {
936 batch.push(item);
937
938 if (Instant::now() - start_time).as_secs() >= 1 {
940 end = false;
941 break;
942 }
943
944 if i + 1 == size {
945 end = false;
946 break;
947 }
948
949 if signals.interrupted() {
950 break;
951 }
952 }
953
954 (batch, end)
955}
956
957fn stream_collect_abbriviated(
958 stream: impl Iterator<Item = Value>,
959 size: usize,
960 signals: &Signals,
961) -> (Vec<Value>, usize, bool) {
962 let mut end = true;
963 let mut read = 0;
964 let mut head = Vec::with_capacity(size);
965 let mut tail = VecDeque::with_capacity(size);
966
967 if size == 0 {
968 return (vec![], 0, false);
969 }
970
971 for item in stream {
972 read += 1;
973
974 if read <= size {
975 head.push(item);
976 } else if tail.len() < size {
977 tail.push_back(item);
978 } else {
979 let _ = tail.pop_front();
980 tail.push_back(item);
981 }
982
983 if signals.interrupted() {
984 end = false;
985 break;
986 }
987 }
988
989 let have_filled_list = head.len() == size && tail.len() == size;
990 if have_filled_list {
991 let dummy = get_abbriviated_dummy(&head, &tail);
992 head.insert(size, dummy)
993 }
994
995 head.extend(tail);
996
997 (head, read, end)
998}
999
1000fn get_abbriviated_dummy(head: &[Value], tail: &VecDeque<Value>) -> Value {
1001 let dummy = || Value::string(String::from("..."), Span::unknown());
1002 let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1003
1004 if is_record_list {
1005 Value::record(
1007 head[0]
1008 .as_record()
1009 .expect("ok")
1010 .columns()
1011 .map(|key| (key.clone(), dummy()))
1012 .collect(),
1013 Span::unknown(),
1014 )
1015 } else {
1016 dummy()
1017 }
1018}
1019
1020fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1021 batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1022}
1023
1024fn render_path_name(
1025 path: &str,
1026 config: &Config,
1027 ls_colors: &LsColors,
1028 cwd: Option<NuPathBuf>,
1029 span: Span,
1030) -> Option<Value> {
1031 if !config.ls.use_ls_colors {
1032 return None;
1033 }
1034
1035 let fullpath = match cwd {
1036 Some(cwd) => PathBuf::from(cwd.join(path)),
1037 None => PathBuf::from(path),
1038 };
1039
1040 let stripped_path = nu_utils::strip_ansi_unlikely(path);
1041 let metadata = std::fs::symlink_metadata(fullpath);
1042 let has_metadata = metadata.is_ok();
1043 let style =
1044 ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1045
1046 let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1048 let show_clickable_links = config.ls.clickable_links
1050 && !in_ssh_session
1051 && has_metadata
1052 && config.shell_integration.osc8;
1053
1054 let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default();
1055
1056 let full_path = PathBuf::from(stripped_path.as_ref())
1057 .canonicalize()
1058 .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1059
1060 let full_path_link = make_clickable_link(
1061 full_path.display().to_string(),
1062 Some(path),
1063 show_clickable_links,
1064 );
1065
1066 let val = ansi_style.paint(full_path_link).to_string();
1067 Some(Value::string(val, span))
1068}
1069
1070fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1071 if !use_ansi_coloring {
1074 nu_utils::strip_ansi_string_likely(output)
1076 } else {
1077 output
1079 }
1080}
1081
1082fn create_empty_placeholder(
1083 value_type_name: &str,
1084 termwidth: usize,
1085 engine_state: &EngineState,
1086 stack: &Stack,
1087) -> String {
1088 let config = stack.get_config(engine_state);
1089 if !config.table.show_empty {
1090 return String::new();
1091 }
1092
1093 let cell = NuRecordsValue::new(format!("empty {}", value_type_name));
1094 let data = vec![vec![cell]];
1095 let mut table = NuTable::from(data);
1096 table.set_data_style(TextStyle::default().dimmed());
1097 let mut out = TableOutput::from_table(table, false, false);
1098
1099 let style_computer = &StyleComputer::from_config(engine_state, stack);
1100 configure_table(&mut out, &config, style_computer, TableMode::default());
1101
1102 out.table
1103 .draw(termwidth)
1104 .expect("Could not create empty table placeholder")
1105}
1106
1107fn convert_table_to_output(
1108 table: ShellResult<Option<String>>,
1109 signals: &Signals,
1110 term_width: usize,
1111 use_ansi_coloring: bool,
1112) -> Option<ShellResult<Vec<u8>>> {
1113 match table {
1114 Ok(Some(table)) => {
1115 let table = maybe_strip_color(table, use_ansi_coloring);
1116
1117 let mut bytes = table.as_bytes().to_vec();
1118 bytes.push(b'\n'); Some(Ok(bytes))
1121 }
1122 Ok(None) => {
1123 let msg = if signals.interrupted() {
1124 String::from("")
1125 } else {
1126 format!("Couldn't fit table into {} columns!", term_width)
1129 };
1130
1131 Some(Ok(msg.as_bytes().to_vec()))
1132 }
1133 Err(err) => Some(Err(err)),
1134 }
1135}
1136
1137fn supported_table_modes() -> Vec<Value> {
1138 vec![
1139 Value::test_string("basic"),
1140 Value::test_string("compact"),
1141 Value::test_string("compact_double"),
1142 Value::test_string("default"),
1143 Value::test_string("heavy"),
1144 Value::test_string("light"),
1145 Value::test_string("none"),
1146 Value::test_string("reinforced"),
1147 Value::test_string("rounded"),
1148 Value::test_string("thin"),
1149 Value::test_string("with_love"),
1150 Value::test_string("psql"),
1151 Value::test_string("markdown"),
1152 Value::test_string("dots"),
1153 Value::test_string("restructured"),
1154 Value::test_string("ascii_rounded"),
1155 Value::test_string("basic_compact"),
1156 ]
1157}
1158
1159fn create_table_opts<'a>(
1160 engine_state: &'a EngineState,
1161 stack: &'a Stack,
1162 cfg: &'a Config,
1163 table_cfg: &'a TableConfig,
1164 span: Span,
1165 offset: usize,
1166) -> TableOpts<'a> {
1167 let comp = StyleComputer::from_config(engine_state, stack);
1168 let signals = engine_state.signals();
1169 let offset = table_cfg.index.unwrap_or(0) + offset;
1170 let index = table_cfg.index.is_none();
1171 let width = table_cfg.width;
1172 let theme = table_cfg.theme;
1173
1174 TableOpts::new(cfg, comp, signals, span, width, theme, offset, index)
1175}
1176
1177fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1178 #[cfg(feature = "os")]
1179 let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1180
1181 #[cfg(not(feature = "os"))]
1182 let cwd = None;
1183
1184 Ok(cwd)
1185}
1186
1187fn get_table_width(width_param: Option<i64>) -> usize {
1188 if let Some(col) = width_param {
1189 col as usize
1190 } else if let Ok((w, _h)) = terminal_size() {
1191 w as usize
1192 } else {
1193 DEFAULT_TABLE_WIDTH
1194 }
1195}