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,
19 TABLE_WIDTH_PRIORITY_COLUMNS_METADATA_KEY, TableMode, 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 width_priority_columns: Vec<String>,
249}
250
251#[derive(Debug, Clone)]
252enum TableView {
253 General,
254 Collapsed,
255 Expanded {
256 limit: Option<usize>,
257 flatten: bool,
258 flatten_separator: Option<String>,
259 },
260}
261
262struct CLIArgs {
263 width: Option<i64>,
264 abbreviation: Option<usize>,
265 theme: TableMode,
266 expand: bool,
267 expand_limit: Option<usize>,
268 expand_flatten: bool,
269 expand_flatten_separator: Option<String>,
270 collapse: bool,
271 index: Option<usize>,
272 use_ansi_coloring: bool,
273 icons: bool,
274}
275
276fn parse_table_config(
277 call: &Call,
278 state: &EngineState,
279 stack: &mut Stack,
280) -> ShellResult<TableConfig> {
281 let args @ CLIArgs {
282 abbreviation,
283 theme,
284 index,
285 use_ansi_coloring,
286 icons,
287 ..
288 } = get_cli_args(call, state, stack)?;
289
290 let table_view = get_table_view(&args);
291 let term_width = get_table_width(args.width);
292 let hex_styles = get_hex_styles(state, stack);
293
294 let cfg = TableConfig {
295 view: table_view,
296 width: term_width,
297 theme,
298 abbreviation,
299 index,
300 use_ansi_coloring,
301 icons,
302 hex_styles,
303 width_priority_columns: vec![],
304 };
305
306 Ok(cfg)
307}
308
309fn get_table_view(args: &CLIArgs) -> TableView {
310 match (args.expand, args.collapse) {
311 (false, false) => TableView::General,
312 (_, true) => TableView::Collapsed,
313 (true, _) => TableView::Expanded {
314 limit: args.expand_limit,
315 flatten: args.expand_flatten,
316 flatten_separator: args.expand_flatten_separator.clone(),
317 },
318 }
319}
320
321fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
322 let width: Option<i64> = call.get_flag(state, stack, "width")?;
323 let expand: bool = call.has_flag(state, stack, "expand")?;
324 let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
325 let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
326 let expand_flatten_separator: Option<String> =
327 call.get_flag(state, stack, "flatten-separator")?;
328 let collapse: bool = call.has_flag(state, stack, "collapse")?;
329 let abbreviation: Option<usize> = call
330 .get_flag(state, stack, "abbreviated")?
331 .or_else(|| stack.get_config(state).table.abbreviated_row_count);
332 let theme =
333 get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
334 let index = get_index_flag(call, state, stack)?;
335 let icons = call.has_flag(state, stack, "icons")?;
336
337 let use_ansi_coloring = stack.get_config(state).use_ansi_coloring.get(state);
338
339 Ok(CLIArgs {
340 theme,
341 abbreviation,
342 collapse,
343 expand,
344 expand_limit,
345 expand_flatten,
346 expand_flatten_separator,
347 width,
348 index,
349 use_ansi_coloring,
350 icons,
351 })
352}
353
354fn get_index_flag(
355 call: &Call,
356 state: &EngineState,
357 stack: &mut Stack,
358) -> ShellResult<Option<usize>> {
359 let index: Option<Value> = call.get_flag(state, stack, "index")?;
360 let value = match index {
361 Some(value) => value,
362 None => return Ok(Some(0)),
363 };
364 let span = value.span();
365
366 match value {
367 Value::Bool { val, .. } => {
368 if val {
369 Ok(Some(0))
370 } else {
371 Ok(None)
372 }
373 }
374 Value::Int { val, .. } => {
375 if val < 0 {
376 Err(ShellError::UnsupportedInput {
377 msg: String::from("got a negative integer"),
378 input: val.to_string(),
379 msg_span: call.span(),
380 input_span: span,
381 })
382 } else {
383 Ok(Some(val as usize))
384 }
385 }
386 Value::Nothing { .. } => Ok(Some(0)),
387 _ => Err(ShellError::CantConvert {
388 to_type: String::from("index"),
389 from_type: String::new(),
390 span: call.span(),
391 help: Some(String::from("supported values: [bool, int, nothing]")),
392 }),
393 }
394}
395
396fn get_theme_flag(
397 call: &Call,
398 state: &EngineState,
399 stack: &mut Stack,
400) -> ShellResult<Option<TableMode>> {
401 call.get_flag(state, stack, "theme")?
402 .map(|theme: String| {
403 TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
404 to_type: String::from("theme"),
405 from_type: String::from("string"),
406 span: call.span(),
407 help: Some(format!("{err}, but found '{theme}'.")),
408 })
409 })
410 .transpose()
411}
412
413struct CmdInput<'a> {
414 engine_state: &'a EngineState,
415 stack: &'a mut Stack,
416 call: &'a Call<'a>,
417 data: PipelineData,
418 cfg: TableConfig,
419 cwd: Option<NuPathBuf>,
420}
421
422impl<'a> CmdInput<'a> {
423 fn parse(
424 engine_state: &'a EngineState,
425 stack: &'a mut Stack,
426 call: &'a Call<'a>,
427 data: PipelineData,
428 ) -> ShellResult<Self> {
429 let cfg = parse_table_config(call, engine_state, stack)?;
430 let cwd = get_cwd(engine_state, stack)?;
431
432 Ok(Self {
433 engine_state,
434 stack,
435 call,
436 data,
437 cfg,
438 cwd,
439 })
440 }
441
442 fn get_config(&self) -> std::sync::Arc<Config> {
443 self.stack.get_config(self.engine_state)
444 }
445}
446
447fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
448 let span = input.data.span().unwrap_or(input.call.head);
449 match input.data {
450 PipelineData::ByteStream(stream, _) if stream.type_() == ByteStreamType::Binary => Ok(
452 PipelineData::byte_stream(pretty_hex_stream(stream, input.cfg, input.call.head), None),
453 ),
454 PipelineData::ByteStream(..) => Ok(input.data),
455 PipelineData::Value(Value::Binary { val, .. }, ..) => {
456 let signals = input.engine_state.signals().clone();
457 let stream = ByteStream::read_binary(val, input.call.head, signals);
458 Ok(PipelineData::byte_stream(
459 pretty_hex_stream(stream, input.cfg, input.call.head),
460 None,
461 ))
462 }
463 PipelineData::Value(Value::List { vals, .. }, metadata) => {
465 let signals = input.engine_state.signals().clone();
466 let stream = ListStream::new(vals.into_iter(), span, signals);
467 input.data = PipelineData::empty();
468
469 handle_row_stream(input, stream, metadata)
470 }
471 PipelineData::ListStream(stream, metadata) => {
472 input.data = PipelineData::empty();
473 handle_row_stream(input, stream, metadata)
474 }
475 PipelineData::Value(Value::Record { val, .. }, metadata) => {
476 input.data = PipelineData::empty();
477 handle_record(input, val.into_owned(), metadata)
478 }
479 PipelineData::Value(Value::Error { error, .. }, ..) => {
480 Err(*error)
483 }
484 PipelineData::Value(Value::Custom { val, .. }, metadata) => {
485 let base_pipeline = val
490 .to_base_value(span)?
491 .into_pipeline_data_with_metadata(metadata);
492 Table.run(input.engine_state, input.stack, input.call, base_pipeline)
493 }
494 PipelineData::Value(Value::Range { val, .. }, metadata) => {
495 let signals = input.engine_state.signals().clone();
496 let stream =
497 ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
498 input.data = PipelineData::empty();
499 handle_row_stream(input, stream, metadata)
500 }
501 x => Ok(x),
502 }
503}
504
505fn pretty_hex_stream(stream: ByteStream, table_cfg: TableConfig, span: Span) -> ByteStream {
506 let mut cfg = HexConfig {
507 title: true,
509 length: stream.known_size().and_then(|sz| sz.try_into().ok()),
511 styles: table_cfg.hex_styles,
512 ..HexConfig::default()
513 };
514
515 debug_assert!(cfg.width > 0, "the default hex config width was zero");
517
518 let mut read_buf = Vec::with_capacity(cfg.width);
519
520 let mut reader = if let Some(reader) = stream.reader() {
521 reader
522 } else {
523 return ByteStream::read_string("".into(), span, Signals::empty());
525 };
526
527 ByteStream::from_fn(
528 span,
529 Signals::empty(),
530 ByteStreamType::String,
531 move |buffer| {
532 let mut write_buf = std::mem::take(buffer);
534 write_buf.clear();
535 let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) };
537
538 if cfg.title {
540 nu_pretty_hex::write_title(&mut write_buf, cfg, table_cfg.use_ansi_coloring)
541 .expect("format error");
542 cfg.title = false;
543
544 *buffer = write_buf.into_bytes();
546
547 Ok(true)
548 } else {
549 read_buf.clear();
551 (&mut reader)
552 .take(cfg.width as u64)
553 .read_to_end(&mut read_buf)
554 .map_err(|err| match ShellErrorBridge::try_from(err) {
555 Ok(ShellErrorBridge(err)) => err,
556 Err(err) => IoError::new(err, span, None).into(),
557 })?;
558
559 if !read_buf.is_empty() {
560 nu_pretty_hex::hex_write(
561 &mut write_buf,
562 &read_buf,
563 cfg,
564 Some(table_cfg.use_ansi_coloring),
565 )
566 .expect("format error");
567 write_buf.push('\n');
568
569 cfg.address_offset += read_buf.len();
571
572 *buffer = write_buf.into_bytes();
574
575 Ok(true)
576 } else {
577 Ok(false)
578 }
579 }
580 },
581 )
582}
583
584fn handle_record(
585 mut input: CmdInput,
586 mut record: Record,
587 metadata: Option<PipelineMetadata>,
588) -> ShellResult<PipelineData> {
589 let span = input.data.span().unwrap_or(input.call.head);
590
591 if record.is_empty() {
592 let value = create_empty_placeholder(
593 "record",
594 input.cfg.width,
595 input.engine_state,
596 input.stack,
597 input.cfg.use_ansi_coloring,
598 );
599 let value = Value::string(value, span);
600 return Ok(value.into_pipeline_data());
601 };
602
603 if let Some(limit) = input.cfg.abbreviation {
604 record = make_record_abbreviation(record, limit, span);
605 }
606
607 input.cfg.width_priority_columns = get_width_priority_columns(metadata.as_ref());
608
609 let config = input.get_config();
610
611 if let Some(PipelineMetadata {
612 mut path_columns, ..
613 }) = metadata
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, span: Span) -> 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));
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 {
727 let val_span = val.span();
728
729 if let Value::Custom { val: custom, .. } = val {
730 match custom.to_base_value(val_span) {
731 Ok(base @ (Value::Record { .. } | Value::List { .. })) => *val = base,
732 Ok(_) => {}
733 Err(err) => *val = Value::error(err, val_span),
734 }
735 }
736 }
737
738 match view {
739 TableView::General => JustTable::table(vals, opts),
740 TableView::Expanded {
741 limit,
742 flatten,
743 flatten_separator,
744 } => {
745 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
746 ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
747 }
748 TableView::Collapsed => {
749 let value = Value::list(vals, span);
750 CollapsedTable::build(value, opts)
751 }
752 }
753}
754
755fn handle_row_stream(
756 mut input: CmdInput<'_>,
757 stream: ListStream,
758 metadata: Option<PipelineMetadata>,
759) -> ShellResult<PipelineData> {
760 input.cfg.width_priority_columns = get_width_priority_columns(metadata.as_ref());
761
762 let cfg = input.get_config();
763
764 let stream = if let Some(metadata) = metadata {
765 let stream = if let PipelineMetadata {
766 data_source: DataSource::HtmlThemes,
767 ..
768 } = &metadata
769 {
770 stream.map(|mut value| {
771 if let Value::Record { val: record, .. } = &mut value {
772 for (rec_col, rec_val) in record.to_mut().iter_mut() {
773 if rec_col != "name" {
775 continue;
776 }
777 let span = rec_val.span();
781 if let Value::String { val, .. } = rec_val {
782 let s = match color_from_hex(val) {
783 Ok(c) => match c {
784 Some(c) => c.normal(),
786 None => nu_ansi_term::Style::default(),
787 },
788 Err(_) => nu_ansi_term::Style::default(),
789 };
790 *rec_val = Value::string(
791 s.paint(&*val).to_string(),
793 span,
794 );
795 }
796 }
797 }
798 value
799 })
800 } else {
801 stream
802 };
803
804 let PipelineMetadata {
805 mut path_columns, ..
806 } = metadata;
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(
1337 cfg,
1338 comp,
1339 signals,
1340 span,
1341 width,
1342 theme,
1343 offset,
1344 index,
1345 table_cfg.width_priority_columns.clone(),
1346 )
1347}
1348
1349fn get_width_priority_columns(metadata: Option<&PipelineMetadata>) -> Vec<String> {
1353 let mut width_priority_columns = Vec::new();
1354
1355 let Some(metadata) = metadata else {
1356 return width_priority_columns;
1357 };
1358
1359 let Some(value) = metadata
1360 .custom
1361 .get(TABLE_WIDTH_PRIORITY_COLUMNS_METADATA_KEY)
1362 else {
1363 return width_priority_columns;
1364 };
1365
1366 let Ok(values) = value.as_list() else {
1367 return width_priority_columns;
1368 };
1369
1370 for value in values {
1371 if let Ok(column_name) = value.as_str()
1372 && !column_name.is_empty()
1373 && !width_priority_columns
1374 .iter()
1375 .any(|column| column == column_name)
1376 {
1377 width_priority_columns.push(column_name.to_string());
1378 }
1379 }
1380
1381 width_priority_columns
1382}
1383
1384fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1385 #[cfg(feature = "os")]
1386 let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1387
1388 #[cfg(not(feature = "os"))]
1389 let cwd = None;
1390
1391 Ok(cwd)
1392}
1393
1394fn get_table_width(width_param: Option<i64>) -> usize {
1395 if let Some(col) = width_param {
1396 col as usize
1397 } else if let Ok((w, _h)) = terminal_size() {
1398 w as usize
1399 } else {
1400 DEFAULT_TABLE_WIDTH
1401 }
1402}
1403
1404fn get_hex_styles(engine_state: &EngineState, stack: &mut Stack) -> HexStyles {
1405 let comp = StyleComputer::from_config(engine_state, stack);
1406 let null = Value::nothing(Span::unknown());
1407 HexStyles {
1408 null_char: comp.compute("binary_null_char", &null),
1409 printable: comp.compute("binary_printable", &null),
1410 whitespace: comp.compute("binary_whitespace", &null),
1411 ascii_other: comp.compute("binary_ascii_other", &null),
1412 non_ascii: comp.compute("binary_non_ascii", &null),
1413 }
1414}