use std::collections::HashMap; use nu_ansi_term::Style; use nu_protocol::{Config, FooterMode, TrimStrategy}; use tabled::{ builder::Builder, formatting_settings::AlignmentStrategy, object::{Cell, Columns, Rows, Segment}, papergrid, style::Color, Alignment, AlignmentHorizontal, Modify, ModifyObject, TableOption, Width, }; use crate::{table_theme::TableTheme, width_control::maybe_truncate_columns, StyledString}; /// Table represent a table view. #[derive(Debug)] pub struct Table { headers: Option>, data: Vec>, theme: TableTheme, } #[derive(Debug)] pub struct Alignments { data: AlignmentHorizontal, index: AlignmentHorizontal, header: AlignmentHorizontal, } impl Default for Alignments { fn default() -> Self { Self { data: AlignmentHorizontal::Center, index: AlignmentHorizontal::Right, header: AlignmentHorizontal::Center, } } } impl Table { /// Creates a [Table] instance. /// /// If `headers.is_empty` then no headers will be rendered. pub fn new( headers: Vec, data: Vec>, theme: TableTheme, ) -> Table { let headers = if headers.is_empty() { None } else { Some(headers) }; Table { headers, data, theme, } } /// Draws a trable on a String. /// /// It returns None in case where table cannot be fit to a terminal width. pub fn draw_table( &self, config: &Config, color_hm: &HashMap, alignments: Alignments, termwidth: usize, ) -> Option { draw_table(self, config, color_hm, alignments, termwidth) } } fn draw_table( table: &Table, config: &Config, color_hm: &HashMap, alignments: Alignments, termwidth: usize, ) -> Option { let mut headers = colorize_headers(table.headers.as_deref()); let mut data = colorize_data(&table.data, table.headers.as_ref().map_or(0, |h| h.len())); let count_columns = table_fix_lengths(headers.as_mut(), &mut data); let is_empty = maybe_truncate_columns(&mut headers, &mut data, count_columns, termwidth); if is_empty { return None; } let table_data = &table.data; let theme = &table.theme; let with_header = headers.is_some(); let with_footer = with_header && need_footer(config, data.len() as u64); let with_index = !config.disable_table_indexes; let table = build_table(data, headers, with_footer); let table = load_theme(table, color_hm, theme, with_footer, with_header); let table = align_table( table, alignments, with_index, with_header, with_footer, table_data, ); let table = table_trim_columns(table, termwidth, &config.trim_strategy); let table = print_table(table, config); if table_width(&table) > termwidth { None } else { Some(table) } } fn print_table(table: tabled::Table, config: &Config) -> String { let output = table.to_string(); // the atty is for when people do ls from vim, there should be no coloring there if !config.use_ansi_coloring || !atty::is(atty::Stream::Stdout) { // Draw the table without ansi colors match strip_ansi_escapes::strip(&output) { Ok(bytes) => String::from_utf8_lossy(&bytes).to_string(), Err(_) => output, // we did our best; so return at least something } } else { // Draw the table with ansi colors output } } fn table_width(table: &str) -> usize { table.lines().next().map_or(0, papergrid::string_width) } fn colorize_data(table_data: &[Vec], count_columns: usize) -> Vec> { let mut data = vec![Vec::with_capacity(count_columns); table_data.len()]; for (row, row_data) in table_data.iter().enumerate() { for cell in row_data { let colored_text = cell .style .color_style .as_ref() .map(|color| color.paint(&cell.contents).to_string()) .unwrap_or_else(|| cell.contents.clone()); data[row].push(colored_text) } } data } fn colorize_headers(headers: Option<&[StyledString]>) -> Option> { headers.map(|table_headers| { let mut headers = Vec::with_capacity(table_headers.len()); for cell in table_headers { let colored_text = cell .style .color_style .as_ref() .map(|color| color.paint(&cell.contents).to_string()) .unwrap_or_else(|| cell.contents.clone()); headers.push(colored_text) } headers }) } fn build_table( data: Vec>, headers: Option>, need_footer: bool, ) -> tabled::Table { let mut builder = Builder::from(data); if let Some(headers) = headers { builder.set_columns(headers.clone()); if need_footer { builder.add_record(headers); } } builder.build() } fn align_table( mut table: tabled::Table, alignments: Alignments, with_index: bool, with_header: bool, with_footer: bool, data: &[Vec], ) -> tabled::Table { table = table.with( Modify::new(Segment::all()) .with(Alignment::Horizontal(alignments.data)) .with(AlignmentStrategy::PerLine), ); if with_header { let alignment = Alignment::Horizontal(alignments.header); if with_footer { table = table.with(Modify::new(Rows::last()).with(alignment.clone())); } table = table.with(Modify::new(Rows::first()).with(alignment)); } if with_index { table = table.with(Modify::new(Columns::first()).with(Alignment::Horizontal(alignments.index))); } table = override_alignments(table, data, with_header, with_index, alignments); table } fn override_alignments( mut table: tabled::Table, data: &[Vec], header_present: bool, index_present: bool, alignments: Alignments, ) -> tabled::Table { let offset = if header_present { 1 } else { 0 }; for (row, rows) in data.iter().enumerate() { for (col, s) in rows.iter().enumerate() { if index_present && col == 0 && s.style.alignment == alignments.index { continue; } if s.style.alignment == alignments.data { continue; } table = table.with( Cell(row + offset, col) .modify() .with(Alignment::Horizontal(s.style.alignment)), ); } } table } fn load_theme( mut table: tabled::Table, color_hm: &HashMap, theme: &TableTheme, with_footer: bool, with_header: bool, ) -> tabled::Table { let mut theme = theme.theme.clone(); if !with_header { theme.set_lines(HashMap::default()); } table = table.with(theme); if let Some(color) = color_hm.get("separator") { let color = color.paint(" ").to_string(); if let Ok(color) = Color::try_from(color) { table = table.with(color); } } if with_footer { table = table.with(FooterStyle).with( Modify::new(Rows::last()) .with(Alignment::center()) .with(AlignmentStrategy::PerCell), ); } table } fn need_footer(config: &Config, count_records: u64) -> bool { matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit) || matches!(config.footer_mode, FooterMode::Always) } struct FooterStyle; impl TableOption for FooterStyle { fn change(&mut self, grid: &mut papergrid::Grid) { if grid.count_columns() == 0 || grid.count_rows() == 0 { return; } if let Some(line) = grid.clone().get_split_line(1) { grid.set_split_line(grid.count_rows() - 1, line.clone()); } } } fn table_trim_columns( table: tabled::Table, termwidth: usize, trim_strategy: &TrimStrategy, ) -> tabled::Table { table.with(&TrimStrategyModifier { termwidth, trim_strategy, }) } pub struct TrimStrategyModifier<'a> { termwidth: usize, trim_strategy: &'a TrimStrategy, } impl tabled::TableOption for &TrimStrategyModifier<'_> { fn change(&mut self, grid: &mut papergrid::Grid) { match self.trim_strategy { TrimStrategy::Wrap { try_to_keep_words } => { let mut w = Width::wrap(self.termwidth).priority::(); if *try_to_keep_words { w = w.keep_words(); } w.change(grid) } TrimStrategy::Truncate { suffix } => { let mut w = Width::truncate(self.termwidth).priority::(); if let Some(suffix) = suffix { w = w.suffix(suffix).suffix_try_color(true); } w.change(grid); } }; } } fn table_fix_lengths(headers: Option<&mut Vec>, data: &mut [Vec]) -> usize { let length = table_find_max_length(headers.as_deref(), data); if let Some(headers) = headers { headers.extend(std::iter::repeat(String::default()).take(length - headers.len())); } for row in data { row.extend(std::iter::repeat(String::default()).take(length - row.len())); } length } fn table_find_max_length(headers: Option<&Vec>, data: &[Vec]) -> usize { let mut length = headers.map_or(0, |h| h.len()); for row in data { length = std::cmp::max(length, row.len()); } length }