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 mut path_columns, ..
605 }) = metadata
606 {
607 path_columns.sort_unstable();
609 path_columns.dedup();
610
611 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
612 Some(v) => Some(env_to_string(
613 "LS_COLORS",
614 v,
615 input.engine_state,
616 input.stack,
617 )?),
618 None => None,
619 };
620 let ls_colors = get_ls_colors(ls_colors_env_str);
621
622 for column in &path_columns {
623 if let Some(value) = record.get_mut(column) {
624 let span = value.span();
625 if let Value::String { val, .. } = value
626 && let Some(val) = render_path_name(
627 val,
628 &config,
629 &ls_colors,
630 input.cwd.as_deref(),
631 input.cfg.icons,
632 span,
633 )
634 {
635 *value = val;
636 }
637 }
638 }
639 }
640 let opts = create_table_opts(
641 input.engine_state,
642 input.stack,
643 &config,
644 &input.cfg,
645 span,
646 0,
647 );
648 let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
649
650 let result = match result {
651 Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
652 None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
653 };
654
655 let val = Value::string(result, span);
656 let data = val.into_pipeline_data();
657
658 Ok(data)
659}
660
661fn make_record_abbreviation(mut record: Record, limit: usize, span: Span) -> Record {
662 if record.len() <= limit * 2 + 1 {
663 return record;
664 }
665
666 let prev_len = record.len();
668 let mut record_iter = record.into_iter();
669 record = Record::with_capacity(limit * 2 + 1);
670 record.extend(record_iter.by_ref().take(limit));
671 record.push(String::from("..."), Value::string("...", span));
672 record.extend(record_iter.skip(prev_len - 2 * limit));
673 record
674}
675
676fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
677 if signals.interrupted() {
678 "".into()
679 } else {
680 format!("Couldn't fit table into {term_width} columns!")
683 }
684}
685
686fn build_table_kv(
687 record: Record,
688 table_view: TableView,
689 opts: TableOpts<'_>,
690 span: Span,
691) -> StringResult {
692 match table_view {
693 TableView::General => JustTable::kv_table(record, opts),
694 TableView::Expanded {
695 limit,
696 flatten,
697 flatten_separator,
698 } => {
699 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
700 ExpandedTable::new(limit, flatten, sep).build_map(&record, opts)
701 }
702 TableView::Collapsed => {
703 let value = Value::record(record, span);
704 CollapsedTable::build(value, opts)
705 }
706 }
707}
708
709fn build_table_batch(
710 mut vals: Vec<Value>,
711 view: TableView,
712 opts: TableOpts<'_>,
713 span: Span,
714) -> StringResult {
715 for val in &mut vals {
718 let span = val.span();
719
720 if let Value::Custom { val: custom, .. } = val {
721 *val = custom
722 .to_base_value(span)
723 .or_else(|err| Result::<_, ShellError>::Ok(Value::error(err, span)))
724 .expect("error converting custom value to base value")
725 }
726 }
727
728 match view {
729 TableView::General => JustTable::table(vals, opts),
730 TableView::Expanded {
731 limit,
732 flatten,
733 flatten_separator,
734 } => {
735 let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
736 ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
737 }
738 TableView::Collapsed => {
739 let value = Value::list(vals, span);
740 CollapsedTable::build(value, opts)
741 }
742 }
743}
744
745fn handle_row_stream(
746 input: CmdInput<'_>,
747 stream: ListStream,
748 metadata: Option<PipelineMetadata>,
749) -> ShellResult<PipelineData> {
750 let cfg = input.get_config();
751
752 let stream = if let Some(metadata) = metadata {
753 let stream = if let PipelineMetadata {
754 data_source: DataSource::HtmlThemes,
755 ..
756 } = &metadata
757 {
758 stream.map(|mut value| {
759 if let Value::Record { val: record, .. } = &mut value {
760 for (rec_col, rec_val) in record.to_mut().iter_mut() {
761 if rec_col != "name" {
763 continue;
764 }
765 let span = rec_val.span();
769 if let Value::String { val, .. } = rec_val {
770 let s = match color_from_hex(val) {
771 Ok(c) => match c {
772 Some(c) => c.normal(),
774 None => nu_ansi_term::Style::default(),
775 },
776 Err(_) => nu_ansi_term::Style::default(),
777 };
778 *rec_val = Value::string(
779 s.paint(&*val).to_string(),
781 span,
782 );
783 }
784 }
785 }
786 value
787 })
788 } else {
789 stream
790 };
791
792 let PipelineMetadata {
793 mut path_columns, ..
794 } = metadata;
795
796 path_columns.sort_unstable();
798 path_columns.dedup();
799
800 let config = cfg.clone();
801 let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
802 Some(v) => Some(env_to_string(
803 "LS_COLORS",
804 v,
805 input.engine_state,
806 input.stack,
807 )?),
808 None => None,
809 };
810 let ls_colors = get_ls_colors(ls_colors_env_str);
811
812 stream.map(move |mut value| {
813 if let Value::Record { val: record, .. } = &mut value {
814 for column in &path_columns {
815 if let Some(value) = record.to_mut().get_mut(column) {
816 let span = value.span();
817 if let Value::String { val, .. } = value
818 && let Some(val) = render_path_name(
819 val,
820 &config,
821 &ls_colors,
822 input.cwd.as_deref(),
823 input.cfg.icons,
824 span,
825 )
826 {
827 *value = val;
828 }
829 }
830 }
831 }
832 value
833 })
834 } else {
835 stream
836 };
837
838 let paginator = PagingTableCreator::new(
839 input.call.head,
840 stream,
841 input.engine_state.clone(),
844 input.stack.clone(),
845 input.cfg,
846 cfg,
847 );
848 let stream = ByteStream::from_result_iter(
849 paginator,
850 input.call.head,
851 Signals::empty(),
852 ByteStreamType::String,
853 );
854 Ok(PipelineData::byte_stream(stream, None))
855}
856
857fn make_clickable_link(
858 full_path: String,
859 link_name: Option<&str>,
860 show_clickable_links: bool,
861) -> String {
862 #[cfg(any(
865 unix,
866 windows,
867 target_os = "redox",
868 target_os = "wasi",
869 target_os = "hermit"
870 ))]
871 if show_clickable_links {
872 format!(
873 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
874 match Url::from_file_path(full_path.clone()) {
875 Ok(url) => url.to_string(),
876 Err(_) => full_path.clone(),
877 },
878 link_name.unwrap_or(full_path.as_str())
879 )
880 } else {
881 match link_name {
882 Some(link_name) => link_name.to_string(),
883 None => full_path,
884 }
885 }
886
887 #[cfg(not(any(
888 unix,
889 windows,
890 target_os = "redox",
891 target_os = "wasi",
892 target_os = "hermit"
893 )))]
894 match link_name {
895 Some(link_name) => link_name.to_string(),
896 None => full_path,
897 }
898}
899
900struct PagingTableCreator {
901 head: Span,
902 stream: ValueIterator,
903 engine_state: EngineState,
904 stack: Stack,
905 elements_displayed: usize,
906 reached_end: bool,
907 table_config: TableConfig,
908 row_offset: usize,
909 config: std::sync::Arc<Config>,
910}
911
912impl PagingTableCreator {
913 fn new(
914 head: Span,
915 stream: ListStream,
916 engine_state: EngineState,
917 stack: Stack,
918 table_config: TableConfig,
919 config: std::sync::Arc<Config>,
920 ) -> Self {
921 PagingTableCreator {
922 head,
923 stream: stream.into_inner(),
924 engine_state,
925 stack,
926 config,
927 table_config,
928 elements_displayed: 0,
929 reached_end: false,
930 row_offset: 0,
931 }
932 }
933
934 fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
935 if batch.is_empty() {
936 return Ok(None);
937 }
938
939 let opts = self.create_table_opts();
940 build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
941 }
942
943 fn create_table_opts(&self) -> TableOpts<'_> {
944 create_table_opts(
945 &self.engine_state,
946 &self.stack,
947 &self.config,
948 &self.table_config,
949 self.head,
950 self.row_offset,
951 )
952 }
953}
954
955impl Iterator for PagingTableCreator {
956 type Item = ShellResult<Vec<u8>>;
957
958 fn next(&mut self) -> Option<Self::Item> {
959 let batch;
960 let end;
961
962 match self.table_config.abbreviation {
963 Some(abbr) => {
964 (batch, _, end) = stream_collect_abbreviated(
965 &mut self.stream,
966 abbr,
967 self.engine_state.signals(),
968 self.head,
969 );
970 }
971 None => {
972 (batch, end) = stream_collect(
974 &mut self.stream,
975 self.config.table.stream_page_size.get() as usize,
976 self.config.table.batch_duration,
977 self.engine_state.signals(),
978 );
979 }
980 }
981
982 let batch_size = batch.len();
983
984 self.elements_displayed += batch_size;
986 self.reached_end = self.reached_end || end;
987
988 if batch.is_empty() {
989 return if self.elements_displayed == 0 && self.reached_end {
992 self.elements_displayed = 1;
995 let result = create_empty_placeholder(
996 "list",
997 self.table_config.width,
998 &self.engine_state,
999 &self.stack,
1000 self.table_config.use_ansi_coloring,
1001 );
1002 let mut bytes = result.into_bytes();
1003 if !bytes.is_empty() {
1005 bytes.push(b'\n');
1006 }
1007 Some(Ok(bytes))
1008 } else {
1009 None
1010 };
1011 }
1012
1013 let table = self.build_table(batch);
1014
1015 self.row_offset += batch_size;
1016
1017 convert_table_to_output(
1018 table,
1019 self.engine_state.signals(),
1020 self.table_config.width,
1021 self.table_config.use_ansi_coloring,
1022 )
1023 }
1024}
1025
1026fn stream_collect(
1027 stream: impl Iterator<Item = Value>,
1028 size: usize,
1029 batch_duration: Duration,
1030 signals: &Signals,
1031) -> (Vec<Value>, bool) {
1032 let start_time = Instant::now();
1033 let mut end = true;
1034
1035 let mut batch = Vec::with_capacity(size);
1036 for (i, item) in stream.enumerate() {
1037 batch.push(item);
1038
1039 if (Instant::now() - start_time) >= batch_duration {
1041 end = false;
1042 break;
1043 }
1044
1045 if i + 1 == size {
1047 end = false;
1048 break;
1049 }
1050
1051 if signals.interrupted() {
1052 break;
1053 }
1054 }
1055
1056 (batch, end)
1057}
1058
1059fn stream_collect_abbreviated(
1060 stream: impl Iterator<Item = Value>,
1061 size: usize,
1062 signals: &Signals,
1063 span: Span,
1064) -> (Vec<Value>, usize, bool) {
1065 let mut end = true;
1066 let mut read = 0;
1067 let mut head = Vec::with_capacity(size);
1068 let mut tail = VecDeque::with_capacity(size);
1069
1070 if size == 0 {
1071 return (vec![], 0, false);
1072 }
1073
1074 for item in stream {
1075 read += 1;
1076
1077 if read <= size {
1078 head.push(item);
1079 } else if tail.len() < size {
1080 tail.push_back(item);
1081 } else {
1082 let _ = tail.pop_front();
1083 tail.push_back(item);
1084 }
1085
1086 if signals.interrupted() {
1087 end = false;
1088 break;
1089 }
1090 }
1091
1092 let have_filled_list = head.len() == size && tail.len() == size;
1093 if have_filled_list {
1094 let dummy = get_abbreviated_dummy(&head, &tail, span);
1095 head.insert(size, dummy)
1096 }
1097
1098 head.extend(tail);
1099
1100 (head, read, end)
1101}
1102
1103fn get_abbreviated_dummy(head: &[Value], tail: &VecDeque<Value>, span: Span) -> Value {
1104 let dummy = || Value::string(String::from("..."), span);
1105 let is_record_list = is_record_list(head.iter()) && is_record_list(tail.iter());
1106
1107 if is_record_list {
1108 Value::record(
1110 head[0]
1111 .as_record()
1112 .expect("ok")
1113 .columns()
1114 .map(|key| (key.clone(), dummy()))
1115 .collect(),
1116 span,
1117 )
1118 } else {
1119 dummy()
1120 }
1121}
1122
1123fn is_record_list<'a>(mut batch: impl ExactSizeIterator<Item = &'a Value>) -> bool {
1124 batch.len() > 0 && batch.all(|value| matches!(value, Value::Record { .. }))
1125}
1126
1127fn render_path_name(
1128 path: &str,
1129 config: &Config,
1130 ls_colors: &LsColors,
1131 cwd: Option<&NuPath>,
1132 icons: bool,
1133 span: Span,
1134) -> Option<Value> {
1135 if !config.ls.use_ls_colors {
1136 return None;
1137 }
1138
1139 let fullpath = match cwd {
1140 Some(cwd) => PathBuf::from(cwd.join(path)),
1141 None => PathBuf::from(path),
1142 };
1143
1144 let stripped_path = nu_utils::strip_ansi_unlikely(path);
1145 let metadata = std::fs::symlink_metadata(fullpath);
1146 let has_metadata = metadata.is_ok();
1147 let style =
1148 ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
1149
1150 let file_icon = icon_for_file(path, &None);
1151 let icon_style = lookup_ansi_color_style(file_icon.color);
1152
1153 let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
1155 let show_clickable_links = config.ls.clickable_links
1157 && !in_ssh_session
1158 && has_metadata
1159 && config.shell_integration.osc8;
1160
1161 let ansi_style = style
1169 .map(Style::to_nu_ansi_term_style)
1170 .unwrap_or(nu_ansi_term::Style {
1171 foreground: Some(nu_ansi_term::Color::Default),
1172 background: Some(nu_ansi_term::Color::Default),
1173 is_bold: false,
1174 is_dimmed: false,
1175 is_italic: false,
1176 is_underline: false,
1177 is_blink: false,
1178 is_reverse: false,
1179 is_hidden: false,
1180 is_strikethrough: false,
1181 prefix_with_reset: false,
1182 });
1183
1184 let full_path = std::path::absolute(stripped_path.as_ref())
1185 .unwrap_or_else(|_| PathBuf::from(stripped_path.as_ref()));
1186
1187 let full_path_link = make_clickable_link(
1188 full_path.display().to_string(),
1189 Some(path),
1190 show_clickable_links,
1191 );
1192
1193 let val = if icons {
1194 format!(
1195 "{} {}",
1196 icon_style.paint(String::from(file_icon.icon)),
1197 ansi_style.paint(full_path_link)
1198 )
1199 } else {
1200 ansi_style.paint(full_path_link).to_string()
1201 };
1202
1203 Some(Value::string(val, span))
1204}
1205
1206fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
1207 if !use_ansi_coloring {
1210 nu_utils::strip_ansi_string_likely(output)
1212 } else {
1213 output
1215 }
1216}
1217
1218fn create_empty_placeholder(
1219 value_type_name: &str,
1220 termwidth: usize,
1221 engine_state: &EngineState,
1222 stack: &Stack,
1223 use_ansi_coloring: bool,
1224) -> String {
1225 let config = stack.get_config(engine_state);
1226 if !config.table.show_empty {
1227 return String::new();
1228 }
1229
1230 let cell = format!("empty {value_type_name}");
1231 let mut table = NuTable::new(1, 1);
1232 table.insert((0, 0), cell);
1233 table.set_data_style(TextStyle::default().dimmed());
1234 let mut out = TableOutput::from_table(table, false, false);
1235
1236 let style_computer = &StyleComputer::from_config(engine_state, stack);
1237 configure_table(&mut out, &config, style_computer, TableMode::default());
1238
1239 if !use_ansi_coloring {
1240 out.table.clear_all_colors();
1241 }
1242
1243 out.table
1244 .draw(termwidth)
1245 .expect("Could not create empty table placeholder")
1246}
1247
1248fn convert_table_to_output(
1249 table: ShellResult<Option<String>>,
1250 signals: &Signals,
1251 term_width: usize,
1252 use_ansi_coloring: bool,
1253) -> Option<ShellResult<Vec<u8>>> {
1254 match table {
1255 Ok(Some(table)) => {
1256 let table = maybe_strip_color(table, use_ansi_coloring);
1257
1258 let mut bytes = table.as_bytes().to_vec();
1259 bytes.push(b'\n'); Some(Ok(bytes))
1262 }
1263 Ok(None) => {
1264 let msg = if signals.interrupted() {
1265 String::from("")
1266 } else {
1267 format!("Couldn't fit table into {term_width} columns!")
1270 };
1271
1272 Some(Ok(msg.as_bytes().to_vec()))
1273 }
1274 Err(err) => Some(Err(err)),
1275 }
1276}
1277
1278const SUPPORTED_TABLE_MODES: &[&str] = &[
1279 "basic",
1280 "compact",
1281 "compact_double",
1282 "default",
1283 "frameless",
1284 "heavy",
1285 "light",
1286 "none",
1287 "reinforced",
1288 "rounded",
1289 "thin",
1290 "with_love",
1291 "psql",
1292 "markdown",
1293 "dots",
1294 "restructured",
1295 "ascii_rounded",
1296 "basic_compact",
1297 "single",
1298 "double",
1299];
1300
1301fn supported_table_modes() -> Vec<Value> {
1302 SUPPORTED_TABLE_MODES
1303 .iter()
1304 .copied()
1305 .map(Value::test_string)
1306 .collect()
1307}
1308
1309fn create_table_opts<'a>(
1310 engine_state: &'a EngineState,
1311 stack: &'a Stack,
1312 cfg: &'a Config,
1313 table_cfg: &'a TableConfig,
1314 span: Span,
1315 offset: usize,
1316) -> TableOpts<'a> {
1317 let comp = StyleComputer::from_config(engine_state, stack);
1318 let signals = engine_state.signals();
1319 let offset = table_cfg.index.unwrap_or(0) + offset;
1320 let index = table_cfg.index.is_none();
1321 let width = table_cfg.width;
1322 let theme = table_cfg.theme;
1323
1324 TableOpts::new(cfg, comp, signals, span, width, theme, offset, index)
1325}
1326
1327fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
1328 #[cfg(feature = "os")]
1329 let cwd = engine_state.cwd(Some(stack)).map(Some)?;
1330
1331 #[cfg(not(feature = "os"))]
1332 let cwd = None;
1333
1334 Ok(cwd)
1335}
1336
1337fn get_table_width(width_param: Option<i64>) -> usize {
1338 if let Some(col) = width_param {
1339 col as usize
1340 } else if let Ok((w, _h)) = terminal_size() {
1341 w as usize
1342 } else {
1343 DEFAULT_TABLE_WIDTH
1344 }
1345}
1346
1347fn get_hex_styles(engine_state: &EngineState, stack: &mut Stack) -> HexStyles {
1348 let comp = StyleComputer::from_config(engine_state, stack);
1349 let null = Value::nothing(Span::unknown());
1350 HexStyles {
1351 null_char: comp.compute("binary_null_char", &null),
1352 printable: comp.compute("binary_printable", &null),
1353 whitespace: comp.compute("binary_whitespace", &null),
1354 ascii_other: comp.compute("binary_ascii_other", &null),
1355 non_ascii: comp.compute("binary_non_ascii", &null),
1356 }
1357}