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