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 nu_utils::time::Instant;
11use url::Url;
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, HexStyles};
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 hex_styles: HexStyles,
248}
249
250#[derive(Debug, Clone)]
251enum TableView {
252 General,
253 Collapsed,
254 Expanded {
255 limit: Option<usize>,
256 flatten: bool,
257 flatten_separator: Option<String>,
258 },
259}
260
261struct CLIArgs {
262 width: Option<i64>,
263 abbreviation: Option<usize>,
264 theme: TableMode,
265 expand: bool,
266 expand_limit: Option<usize>,
267 expand_flatten: bool,
268 expand_flatten_separator: Option<String>,
269 collapse: bool,
270 index: Option<usize>,
271 use_ansi_coloring: bool,
272 icons: bool,
273}
274
275fn parse_table_config(
276 call: &Call,
277 state: &EngineState,
278 stack: &mut Stack,
279) -> ShellResult<TableConfig> {
280 let args @ CLIArgs {
281 abbreviation,
282 theme,
283 index,
284 use_ansi_coloring,
285 icons,
286 ..
287 } = get_cli_args(call, state, stack)?;
288
289 let table_view = get_table_view(&args);
290 let term_width = get_table_width(args.width);
291 let hex_styles = get_hex_styles(state, stack);
292
293 let cfg = TableConfig {
294 view: table_view,
295 width: term_width,
296 theme,
297 abbreviation,
298 index,
299 use_ansi_coloring,
300 icons,
301 hex_styles,
302 };
303
304 Ok(cfg)
305}
306
307fn get_table_view(args: &CLIArgs) -> TableView {
308 match (args.expand, args.collapse) {
309 (false, false) => TableView::General,
310 (_, true) => TableView::Collapsed,
311 (true, _) => TableView::Expanded {
312 limit: args.expand_limit,
313 flatten: args.expand_flatten,
314 flatten_separator: args.expand_flatten_separator.clone(),
315 },
316 }
317}
318
319fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
320 let width: Option<i64> = call.get_flag(state, stack, "width")?;
321 let expand: bool = call.has_flag(state, stack, "expand")?;
322 let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
323 let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
324 let expand_flatten_separator: Option<String> =
325 call.get_flag(state, stack, "flatten-separator")?;
326 let collapse: bool = call.has_flag(state, stack, "collapse")?;
327 let abbreviation: Option<usize> = call
328 .get_flag(state, stack, "abbreviated")?
329 .or_else(|| stack.get_config(state).table.abbreviated_row_count);
330 let theme =
331 get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
332 let index = get_index_flag(call, state, stack)?;
333 let icons = call.has_flag(state, stack, "icons")?;
334
335 let use_ansi_coloring = stack.get_config(state).use_ansi_coloring.get(state);
336
337 Ok(CLIArgs {
338 theme,
339 abbreviation,
340 collapse,
341 expand,
342 expand_limit,
343 expand_flatten,
344 expand_flatten_separator,
345 width,
346 index,
347 use_ansi_coloring,
348 icons,
349 })
350}
351
352fn get_index_flag(
353 call: &Call,
354 state: &EngineState,
355 stack: &mut Stack,
356) -> ShellResult<Option<usize>> {
357 let index: Option<Value> = call.get_flag(state, stack, "index")?;
358 let value = match index {
359 Some(value) => value,
360 None => return Ok(Some(0)),
361 };
362 let span = value.span();
363
364 match value {
365 Value::Bool { val, .. } => {
366 if val {
367 Ok(Some(0))
368 } else {
369 Ok(None)
370 }
371 }
372 Value::Int { val, .. } => {
373 if val < 0 {
374 Err(ShellError::UnsupportedInput {
375 msg: String::from("got a negative integer"),
376 input: val.to_string(),
377 msg_span: call.span(),
378 input_span: span,
379 })
380 } else {
381 Ok(Some(val as usize))
382 }
383 }
384 Value::Nothing { .. } => Ok(Some(0)),
385 _ => Err(ShellError::CantConvert {
386 to_type: String::from("index"),
387 from_type: String::new(),
388 span: call.span(),
389 help: Some(String::from("supported values: [bool, int, nothing]")),
390 }),
391 }
392}
393
394fn get_theme_flag(
395 call: &Call,
396 state: &EngineState,
397 stack: &mut Stack,
398) -> ShellResult<Option<TableMode>> {
399 call.get_flag(state, stack, "theme")?
400 .map(|theme: String| {
401 TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
402 to_type: String::from("theme"),
403 from_type: String::from("string"),
404 span: call.span(),
405 help: Some(format!("{err}, but found '{theme}'.")),
406 })
407 })
408 .transpose()
409}
410
411struct CmdInput<'a> {
412 engine_state: &'a EngineState,
413 stack: &'a mut Stack,
414 call: &'a Call<'a>,
415 data: PipelineData,
416 cfg: TableConfig,
417 cwd: Option<NuPathBuf>,
418}
419
420impl<'a> CmdInput<'a> {
421 fn parse(
422 engine_state: &'a EngineState,
423 stack: &'a mut Stack,
424 call: &'a Call<'a>,
425 data: PipelineData,
426 ) -> ShellResult<Self> {
427 let cfg = parse_table_config(call, engine_state, stack)?;
428 let cwd = get_cwd(engine_state, stack)?;
429
430 Ok(Self {
431 engine_state,
432 stack,
433 call,
434 data,
435 cfg,
436 cwd,
437 })
438 }
439
440 fn get_config(&self) -> std::sync::Arc<Config> {
441 self.stack.get_config(self.engine_state)
442 }
443}
444
445fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
446 let span = input.data.span().unwrap_or(input.call.head);
447 match input.data {
448 PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
450 PipelineData::byte_stream(pretty_hex_stream(stream, input.cfg, input.call.head), None),
451 ),
452 PipelineData::ByteStream(..) => Ok(input.data),
453 PipelineData::Value(Value::Binary { val, .. }, ..) => {
454 let signals = input.engine_state.signals().clone();
455 let stream = ByteStream::read_binary(val, input.call.head, signals);
456 Ok(PipelineData::byte_stream(
457 pretty_hex_stream(stream, input.cfg, input.call.head),
458 None,
459 ))
460 }
461 PipelineData::Value(Value::List { vals, .. }, metadata) => {
463 let signals = input.engine_state.signals().clone();
464 let stream = ListStream::new(vals.into_iter(), span, signals);
465 input.data = PipelineData::empty();
466
467 handle_row_stream(input, stream, metadata)
468 }
469 PipelineData::ListStream(stream, metadata) => {
470 input.data = PipelineData::empty();
471 handle_row_stream(input, stream, metadata)
472 }
473 PipelineData::Value(Value::Record { val, .. }, metadata) => {
474 input.data = PipelineData::empty();
475 handle_record(input, val.into_owned(), metadata)
476 }
477 PipelineData::Value(Value::Error { error, .. }, ..) => {
478 Err(*error)
481 }
482 PipelineData::Value(Value::Custom { val, .. }, metadata) => {
483 let base_pipeline = val
484 .to_base_value(span)?
485 .into_pipeline_data_with_metadata(metadata);
486 Table.run(input.engine_state, input.stack, input.call, base_pipeline)
487 }
488 PipelineData::Value(Value::Range { val, .. }, metadata) => {
489 let signals = input.engine_state.signals().clone();
490 let stream =
491 ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
492 input.data = PipelineData::empty();
493 handle_row_stream(input, stream, metadata)
494 }
495 x => Ok(x),
496 }
497}
498
499fn pretty_hex_stream(stream: ByteStream, table_cfg: TableConfig, span: Span) -> ByteStream {
500 let mut cfg = HexConfig {
501 title: true,
503 length: stream.known_size().and_then(|sz| sz.try_into().ok()),
505 styles: table_cfg.hex_styles,
506 ..HexConfig::default()
507 };
508
509 debug_assert!(cfg.width > 0, "the default hex config width was zero");
511
512 let mut read_buf = Vec::with_capacity(cfg.width);
513
514 let mut reader = if let Some(reader) = stream.reader() {
515 reader
516 } else {
517 return ByteStream::read_string("".into(), span, Signals::empty());
519 };
520
521 ByteStream::from_fn(
522 span,
523 Signals::empty(),
524 ByteStreamType::String,
525 move |buffer| {
526 let mut write_buf = std::mem::take(buffer);
528 write_buf.clear();
529 let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
531
532 if cfg.title {
534 nu_pretty_hex::write_title(&mut write_buf, cfg, table_cfg.use_ansi_coloring)
535 .expect("format error");
536 cfg.title = false;
537
538 *buffer = write_buf.into_bytes();
540
541 Ok(true)
542 } else {
543 read_buf.clear();
545 (&mut reader)
546 .take(cfg.width as u64)
547 .read_to_end(&mut read_buf)
548 .map_err(|err| match ShellErrorBridge::try_from(err) {
549 Ok(ShellErrorBridge(err)) => err,
550 Err(err) => IoError::new(err, span, None).into(),
551 })?;
552
553 if !read_buf.is_empty() {
554 nu_pretty_hex::hex_write(
555 &mut write_buf,
556 &read_buf,
557 cfg,
558 Some(table_cfg.use_ansi_coloring),
559 )
560 .expect("format error");
561 write_buf.push('\n');
562
563 cfg.address_offset += read_buf.len();
565
566 *buffer = write_buf.into_bytes();
568
569 Ok(true)
570 } else {
571 Ok(false)
572 }
573 }
574 },
575 )
576}
577
578fn handle_record(
579 input: CmdInput,
580 mut record: Record,
581 metadata: Option<PipelineMetadata>,
582) -> ShellResult<PipelineData> {
583 let span = input.data.span().unwrap_or(input.call.head);
584
585 if record.is_empty() {
586 let value = create_empty_placeholder(
587 "record",
588 input.cfg.width,
589 input.engine_state,
590 input.stack,
591 input.cfg.use_ansi_coloring,
592 );
593 let value = Value::string(value, span);
594 return Ok(value.into_pipeline_data());
595 };
596
597 if let Some(limit) = input.cfg.abbreviation {
598 record = make_record_abbreviation(record, limit, span);
599 }
600
601 let config = input.get_config();
602
603 if let Some(PipelineMetadata {
604 data_source,
605 mut path_columns,
606 ..
607 }) = metadata
608 {
609 #[allow(deprecated)]
610 if data_source == DataSource::Ls {
611 path_columns.push(String::from("name"));
612 }
613 path_columns.sort_unstable();
615 path_columns.dedup();
616
617 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
618 Some(v) => Some(env_to_string(
619 "LS_COLORS",
620 v,
621 input.engine_state,
622 input.stack,
623 )?),
624 None => None,
625 };
626 let ls_colors = get_ls_colors(ls_colors_env_str);
627
628 for column in &path_columns {
629 if let Some(value) = record.get_mut(column) {
630 let span = value.span();
631 if let Value::String { val, .. } = value
632 && let Some(val) = render_path_name(
633 val,
634 &config,
635 &ls_colors,
636 input.cwd.as_deref(),
637 input.cfg.icons,
638 span,
639 )
640 {
641 *value = val;
642 }
643 }
644 }
645 }
646 let opts = create_table_opts(
647 input.engine_state,
648 input.stack,
649 &config,
650 &input.cfg,
651 span,
652 0,
653 );
654 let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
655
656 let result = match result {
657 Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
658 None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
659 };
660
661 let val = Value::string(result, span);
662 let data = val.into_pipeline_data();
663
664 Ok(data)
665}
666
667fn make_record_abbreviation(mut record: Record, limit: usize, span: Span) -> Record {
668 if record.len() <= limit * 2 + 1 {
669 return record;
670 }
671
672 let prev_len = record.len();
674 let mut record_iter = record.into_iter();
675 record = Record::with_capacity(limit * 2 + 1);
676 record.extend(record_iter.by_ref().take(limit));
677 record.push(String::from("..."), Value::string("...", span));
678 record.extend(record_iter.skip(prev_len - 2 * limit));
679 record
680}
681
682fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
683 if signals.interrupted() {
684 "".into()
685 } else {
686 format!("Couldn't fit table into {term_width} columns!")
689 }
690}
691
692fn build_table_kv(
693 record: Record,
694 table_view: TableView,
695 opts: TableOpts<'_>,
696 span: Span,
697) -> StringResult {
698 match table_view {
699 TableView::General => JustTable::kv_table(record, opts),
700 TableView::Expanded {
701 limit,
702 flatten,
703 flatten_separator,
704 } => {
705 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
706 ExpandedTable::new(limit, flatten, sep).build_map(&record, opts)
707 }
708 TableView::Collapsed => {
709 let value = Value::record(record, span);
710 CollapsedTable::build(value, opts)
711 }
712 }
713}
714
715fn build_table_batch(
716 mut vals: Vec<Value>,
717 view: TableView,
718 opts: TableOpts<'_>,
719 span: Span,
720) -> StringResult {
721 for val in &mut vals {
724 let span = val.span();
725
726 if let Value::Custom { val: custom, .. } = val {
727 *val = custom
728 .to_base_value(span)
729 .or_else(|err| Result::<_, ShellError>::Ok(Value::error(err, span)))
730 .expect("error converting custom value to base value")
731 }
732 }
733
734 match view {
735 TableView::General => JustTable::table(vals, opts),
736 TableView::Expanded {
737 limit,
738 flatten,
739 flatten_separator,
740 } => {
741 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
742 ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
743 }
744 TableView::Collapsed => {
745 let value = Value::list(vals, span);
746 CollapsedTable::build(value, opts)
747 }
748 }
749}
750
751fn handle_row_stream(
752 input: CmdInput<'_>,
753 stream: ListStream,
754 metadata: Option<PipelineMetadata>,
755) -> ShellResult<PipelineData> {
756 let cfg = input.get_config();
757
758 let stream = if let Some(metadata) = metadata {
759 let stream = if let PipelineMetadata {
760 data_source: DataSource::HtmlThemes,
761 ..
762 } = &metadata
763 {
764 stream.map(|mut value| {
765 if let Value::Record { val: record, .. } = &mut value {
766 for (rec_col, rec_val) in record.to_mut().iter_mut() {
767 if rec_col != "name" {
769 continue;
770 }
771 let span = rec_val.span();
775 if let Value::String { val, .. } = rec_val {
776 let s = match color_from_hex(val) {
777 Ok(c) => match c {
778 Some(c) => c.normal(),
780 None => nu_ansi_term::Style::default(),
781 },
782 Err(_) => nu_ansi_term::Style::default(),
783 };
784 *rec_val = Value::string(
785 s.paint(&*val).to_string(),
787 span,
788 );
789 }
790 }
791 }
792 value
793 })
794 } else {
795 stream
796 };
797
798 let PipelineMetadata {
799 data_source,
800 mut path_columns,
801 ..
802 } = metadata;
803
804 #[allow(deprecated)]
805 if data_source == DataSource::Ls {
806 path_columns.push(String::from("name"));
807 }
808 path_columns.sort_unstable();
810 path_columns.dedup();
811
812 let config = cfg.clone();
813 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
814 Some(v) => Some(env_to_string(
815 "LS_COLORS",
816 v,
817 input.engine_state,
818 input.stack,
819 )?),
820 None => None,
821 };
822 let ls_colors = get_ls_colors(ls_colors_env_str);
823
824 stream.map(move |mut value| {
825 if let Value::Record { val: record, .. } = &mut value {
826 for column in &path_columns {
827 if let Some(value) = record.to_mut().get_mut(column) {
828 let span = value.span();
829 if let Value::String { val, .. } = value
830 && let Some(val) = render_path_name(
831 val,
832 &config,
833 &ls_colors,
834 input.cwd.as_deref(),
835 input.cfg.icons,
836 span,
837 )
838 {
839 *value = val;
840 }
841 }
842 }
843 }
844 value
845 })
846 } else {
847 stream
848 };
849
850 let paginator = PagingTableCreator::new(
851 input.call.head,
852 stream,
853 input.engine_state.clone(),
856 input.stack.clone(),
857 input.cfg,
858 cfg,
859 );
860 let stream = ByteStream::from_result_iter(
861 paginator,
862 input.call.head,
863 Signals::empty(),
864 ByteStreamType::String,
865 );
866 Ok(PipelineData::byte_stream(stream, None))
867}
868
869fn make_clickable_link(
870 full_path: String,
871 link_name: Option<&str>,
872 show_clickable_links: bool,
873) -> String {
874 #[cfg(any(
877 unix,
878 windows,
879 target_os = "redox",
880 target_os = "wasi",
881 target_os = "hermit"
882 ))]
883 if show_clickable_links {
884 format!(
885 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
886 match Url::from_file_path(full_path.clone()) {
887 Ok(url) => url.to_string(),
888 Err(_) => full_path.clone(),
889 },
890 link_name.unwrap_or(full_path.as_str())
891 )
892 } else {
893 match link_name {
894 Some(link_name) => link_name.to_string(),
895 None => full_path,
896 }
897 }
898
899 #[cfg(not(any(
900 unix,
901 windows,
902 target_os = "redox",
903 target_os = "wasi",
904 target_os = "hermit"
905 )))]
906 match link_name {
907 Some(link_name) => link_name.to_string(),
908 None => full_path,
909 }
910}
911
912struct PagingTableCreator {
913 head: Span,
914 stream: ValueIterator,
915 engine_state: EngineState,
916 stack: Stack,
917 elements_displayed: usize,
918 reached_end: bool,
919 table_config: TableConfig,
920 row_offset: usize,
921 config: std::sync::Arc<Config>,
922}
923
924impl PagingTableCreator {
925 fn new(
926 head: Span,
927 stream: ListStream,
928 engine_state: EngineState,
929 stack: Stack,
930 table_config: TableConfig,
931 config: std::sync::Arc<Config>,
932 ) -> Self {
933 PagingTableCreator {
934 head,
935 stream: stream.into_inner(),
936 engine_state,
937 stack,
938 config,
939 table_config,
940 elements_displayed: 0,
941 reached_end: false,
942 row_offset: 0,
943 }
944 }
945
946 fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
947 if batch.is_empty() {
948 return Ok(None);
949 }
950
951 let opts = self.create_table_opts();
952 build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
953 }
954
955 fn create_table_opts(&self) -> TableOpts<'_> {
956 create_table_opts(
957 &self.engine_state,
958 &self.stack,
959 &self.config,
960 &self.table_config,
961 self.head,
962 self.row_offset,
963 )
964 }
965}
966
967impl Iterator for PagingTableCreator {
968 type Item = ShellResult<Vec<u8>>;
969
970 fn next(&mut self) -> Option<Self::Item> {
971 let batch;
972 let end;
973
974 match self.table_config.abbreviation {
975 Some(abbr) => {
976 (batch, _, end) = stream_collect_abbreviated(
977 &mut self.stream,
978 abbr,
979 self.engine_state.signals(),
980 self.head,
981 );
982 }
983 None => {
984 (batch, end) = stream_collect(
986 &mut self.stream,
987 self.config.table.stream_page_size.get() as usize,
988 self.config.table.batch_duration,
989 self.engine_state.signals(),
990 );
991 }
992 }
993
994 let batch_size = batch.len();
995
996 self.elements_displayed += batch_size;
998 self.reached_end = self.reached_end || end;
999
1000 if batch.is_empty() {
1001 return if self.elements_displayed == 0 && self.reached_end {
1004 self.elements_displayed = 1;
1007 let result = create_empty_placeholder(
1008 "list",
1009 self.table_config.width,
1010 &self.engine_state,
1011 &self.stack,
1012 self.table_config.use_ansi_coloring,
1013 );
1014 let mut bytes = result.into_bytes();
1015 if !bytes.is_empty() {
1017 bytes.push(b'\n');
1018 }
1019 Some(Ok(bytes))
1020 } else {
1021 None
1022 };
1023 }
1024
1025 let table = self.build_table(batch);
1026
1027 self.row_offset += batch_size;
1028
1029 convert_table_to_output(
1030 table,
1031 self.engine_state.signals(),
1032 self.table_config.width,
1033 self.table_config.use_ansi_coloring,
1034 )
1035 }
1036}
1037
1038fn stream_collect(
1039 stream: impl Iterator<Item = Value>,
1040 size: usize,
1041 batch_duration: Duration,
1042 signals: &Signals,
1043) -> (Vec<Value>, bool) {
1044 let start_time = Instant::now();
1045 let mut end = true;
1046
1047 let mut batch = Vec::with_capacity(size);
1048 for (i, item) in stream.enumerate() {
1049 batch.push(item);
1050
1051 if (Instant::now() - start_time) >= batch_duration {
1053 end = false;
1054 break;
1055 }
1056
1057 if i + 1 == size {
1059 end = false;
1060 break;
1061 }
1062
1063 if signals.interrupted() {
1064 break;
1065 }
1066 }
1067
1068 (batch, end)
1069}
1070
1071fn stream_collect_abbreviated(
1072 stream: impl Iterator<Item = Value>,
1073 size: usize,
1074 signals: &Signals,
1075 span: Span,
1076) -> (Vec<Value>, usize, bool) {
1077 let mut end = true;
1078 let mut read = 0;
1079 let mut head = Vec::with_capacity(size);
1080 let mut tail = VecDeque::with_capacity(size);
1081
1082 if size == 0 {
1083 return (vec![], 0, false);
1084 }
1085
1086 for item in stream {
1087 read += 1;
1088
1089 if read <= size {
1090 head.push(item);
1091 } else if tail.len() < size {
1092 tail.push_back(item);
1093 } else {
1094 let _ = tail.pop_front();
1095 tail.push_back(item);
1096 }
1097
1098 if signals.interrupted() {
1099 end = false;
1100 break;
1101 }
1102 }
1103
1104 let have_filled_list = head.len() == size && tail.len() == size;
1105 if have_filled_list {
1106 let dummy = get_abbreviated_dummy(&head, &tail, span);
1107 head.insert(size, dummy)
1108 }
1109
1110 head.extend(tail);
1111
1112 (head, read, end)
1113}
1114
1115fn get_abbreviated_dummy(head: &[Value], tail: &VecDeque<Value>, span: Span) -> Value {
1116 let dummy = || Value::string(String::from("..."), span);
1117 let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1118
1119 if is_record_list {
1120 Value::record(
1122 head[0]
1123 .as_record()
1124 .expect("ok")
1125 .columns()
1126 .map(|key| (key.clone(), dummy()))
1127 .collect(),
1128 span,
1129 )
1130 } else {
1131 dummy()
1132 }
1133}
1134
1135fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1136 batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1137}
1138
1139fn render_path_name(
1140 path: &str,
1141 config: &Config,
1142 ls_colors: &LsColors,
1143 cwd: Option<&NuPath>,
1144 icons: bool,
1145 span: Span,
1146) -> Option<Value> {
1147 if !config.ls.use_ls_colors {
1148 return None;
1149 }
1150
1151 let fullpath = match cwd {
1152 Some(cwd) => PathBuf::from(cwd.join(path)),
1153 None => PathBuf::from(path),
1154 };
1155
1156 let stripped_path = nu_utils::strip_ansi_unlikely(path);
1157 let metadata = std::fs::symlink_metadata(fullpath);
1158 let has_metadata = metadata.is_ok();
1159 let style =
1160 ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1161
1162 let file_icon = icon_for_file(path, &None);
1163 let icon_style = lookup_ansi_color_style(file_icon.color);
1164
1165 let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1167 let show_clickable_links = config.ls.clickable_links
1169 && !in_ssh_session
1170 && has_metadata
1171 && config.shell_integration.osc8;
1172
1173 let ansi_style = style
1181 .map(Style::to_nu_ansi_term_style)
1182 .unwrap_or(nu_ansi_term::Style {
1183 foreground: Some(nu_ansi_term::Color::Default),
1184 background: Some(nu_ansi_term::Color::Default),
1185 is_bold: false,
1186 is_dimmed: false,
1187 is_italic: false,
1188 is_underline: false,
1189 is_blink: false,
1190 is_reverse: false,
1191 is_hidden: false,
1192 is_strikethrough: false,
1193 prefix_with_reset: false,
1194 });
1195
1196 let full_path = std::path::absolute(stripped_path.as_ref())
1197 .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1198
1199 let full_path_link = make_clickable_link(
1200 full_path.display().to_string(),
1201 Some(path),
1202 show_clickable_links,
1203 );
1204
1205 let val = if icons {
1206 format!(
1207 "{} {}",
1208 icon_style.paint(String::from(file_icon.icon)),
1209 ansi_style.paint(full_path_link)
1210 )
1211 } else {
1212 ansi_style.paint(full_path_link).to_string()
1213 };
1214
1215 Some(Value::string(val, span))
1216}
1217
1218fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1219 if !use_ansi_coloring {
1222 nu_utils::strip_ansi_string_likely(output)
1224 } else {
1225 output
1227 }
1228}
1229
1230fn create_empty_placeholder(
1231 value_type_name: &str,
1232 termwidth: usize,
1233 engine_state: &EngineState,
1234 stack: &Stack,
1235 use_ansi_coloring: bool,
1236) -> String {
1237 let config = stack.get_config(engine_state);
1238 if !config.table.show_empty {
1239 return String::new();
1240 }
1241
1242 let cell = format!("empty {value_type_name}");
1243 let mut table = NuTable::new(1, 1);
1244 table.insert((0, 0), cell);
1245 table.set_data_style(TextStyle::default().dimmed());
1246 let mut out = TableOutput::from_table(table, false, false);
1247
1248 let style_computer = &StyleComputer::from_config(engine_state, stack);
1249 configure_table(&mut out, &config, style_computer, TableMode::default());
1250
1251 if !use_ansi_coloring {
1252 out.table.clear_all_colors();
1253 }
1254
1255 out.table
1256 .draw(termwidth)
1257 .expect("Could not create empty table placeholder")
1258}
1259
1260fn convert_table_to_output(
1261 table: ShellResult<Option<String>>,
1262 signals: &Signals,
1263 term_width: usize,
1264 use_ansi_coloring: bool,
1265) -> Option<ShellResult<Vec<u8>>> {
1266 match table {
1267 Ok(Some(table)) => {
1268 let table = maybe_strip_color(table, use_ansi_coloring);
1269
1270 let mut bytes = table.as_bytes().to_vec();
1271 bytes.push(b'\n'); Some(Ok(bytes))
1274 }
1275 Ok(None) => {
1276 let msg = if signals.interrupted() {
1277 String::from("")
1278 } else {
1279 format!("Couldn't fit table into {term_width} columns!")
1282 };
1283
1284 Some(Ok(msg.as_bytes().to_vec()))
1285 }
1286 Err(err) => Some(Err(err)),
1287 }
1288}
1289
1290const SUPPORTED_TABLE_MODES: &[&str] = &[
1291 "basic",
1292 "compact",
1293 "compact_double",
1294 "default",
1295 "frameless",
1296 "heavy",
1297 "light",
1298 "none",
1299 "reinforced",
1300 "rounded",
1301 "thin",
1302 "with_love",
1303 "psql",
1304 "markdown",
1305 "dots",
1306 "restructured",
1307 "ascii_rounded",
1308 "basic_compact",
1309 "single",
1310 "double",
1311];
1312
1313fn supported_table_modes() -> Vec<Value> {
1314 SUPPORTED_TABLE_MODES
1315 .iter()
1316 .copied()
1317 .map(Value::test_string)
1318 .collect()
1319}
1320
1321fn create_table_opts<'a>(
1322 engine_state: &'a EngineState,
1323 stack: &'a Stack,
1324 cfg: &'a Config,
1325 table_cfg: &'a TableConfig,
1326 span: Span,
1327 offset: usize,
1328) -> TableOpts<'a> {
1329 let comp = StyleComputer::from_config(engine_state, stack);
1330 let signals = engine_state.signals();
1331 let offset = table_cfg.index.unwrap_or(0) + offset;
1332 let index = table_cfg.index.is_none();
1333 let width = table_cfg.width;
1334 let theme = table_cfg.theme;
1335
1336 TableOpts::new(cfg, comp, signals, span, width, theme, offset, index)
1337}
1338
1339fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1340 #[cfg(feature = "os")]
1341 let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1342
1343 #[cfg(not(feature = "os"))]
1344 let cwd = None;
1345
1346 Ok(cwd)
1347}
1348
1349fn get_table_width(width_param: Option<i64>) -> usize {
1350 if let Some(col) = width_param {
1351 col as usize
1352 } else if let Ok((w, _h)) = terminal_size() {
1353 w as usize
1354 } else {
1355 DEFAULT_TABLE_WIDTH
1356 }
1357}
1358
1359fn get_hex_styles(engine_state: &EngineState, stack: &mut Stack) -> HexStyles {
1360 let comp = StyleComputer::from_config(engine_state, stack);
1361 let null = Value::nothing(Span::unknown());
1362 HexStyles {
1363 null_char: comp.compute("binary_null_char", &null),
1364 printable: comp.compute("binary_printable", &null),
1365 whitespace: comp.compute("binary_whitespace", &null),
1366 ascii_other: comp.compute("binary_ascii_other", &null),
1367 non_ascii: comp.compute("binary_non_ascii", &null),
1368 }
1369}