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