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