use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span, Text};
use std::fmt::Write as _;
use std::io::Cursor;
use crate::integrations::{delimiter_from_path_for_viewer, detect_delimiter_byte};
use crate::render::viewers::pretty_tables;
use crate::themes;
pub const VIEWER_TABLE_MAX_COLUMNS: usize = 30;
const CSV_TOTAL_ROWS_META_PREFIX: &str = "__UBLX_CSV_TOTAL_ROWS__=";
#[must_use]
pub fn should_render_as_table(rows: &[Vec<String>]) -> bool {
!rows.is_empty() && rows.iter().map(Vec::len).max().unwrap_or(0) <= VIEWER_TABLE_MAX_COLUMNS
}
fn strip_total_rows_meta_row(rows: &mut Vec<Vec<String>>) {
if rows
.last()
.is_some_and(|r| r.len() == 1 && r[0].trim().starts_with(CSV_TOTAL_ROWS_META_PREFIX))
{
rows.pop();
}
}
#[must_use]
pub fn total_rows_hint_from_raw(raw: &str) -> Option<usize> {
let last = raw.lines().last()?.trim();
let n = last.strip_prefix(CSV_TOTAL_ROWS_META_PREFIX)?;
n.parse::<usize>().ok()
}
fn truncate_for_width(s: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
let char_count = s.chars().count();
if char_count <= width {
return s.to_string();
}
if width <= 3 {
return s.chars().take(width).collect();
}
let keep = width - 3;
format!("{}...", s.chars().take(keep).collect::<String>())
}
fn visible_cols_and_widths(rows: &[Vec<String>], content_width: u16) -> (usize, Vec<usize>, usize) {
let total_cols = rows.iter().map(Vec::len).max().unwrap_or(0);
if total_cols == 0 {
return (0, Vec::new(), 0);
}
let viewport = (content_width as usize).max(1);
let sample_rows = rows.iter().take(256);
let mut max_lens = vec![1usize; total_cols];
for row in sample_rows {
for (j, cell) in row.iter().enumerate() {
let c = pretty_tables::collapse_viewer_cell_whitespace(cell);
max_lens[j] = max_lens[j].max(c.chars().count());
}
}
let mut widths = Vec::new();
let mut used = 0usize;
for max_len in max_lens {
let w = max_len.clamp(3, 24);
let sep = usize::from(!widths.is_empty()) * 3;
if !widths.is_empty() && used + sep + w > viewport {
break;
}
used += sep + w;
widths.push(w);
}
if widths.is_empty() {
widths.push(viewport.clamp(1, 24));
}
let visible = widths.len().min(total_cols);
(visible, widths, total_cols)
}
fn row_to_structured_line(row: &[String], widths: &[usize]) -> String {
widths
.iter()
.enumerate()
.map(|(j, width)| {
let raw = row.get(j).map_or("", String::as_str);
let collapsed = pretty_tables::collapse_viewer_cell_whitespace(raw);
let clipped = truncate_for_width(&collapsed, *width);
format!("{clipped:<width$}")
})
.collect::<Vec<_>>()
.join(" | ")
}
fn structured_cell_text(row: &[String], col_idx: usize, width: usize) -> String {
let raw = row.get(col_idx).map_or("", String::as_str);
let collapsed = pretty_tables::collapse_viewer_cell_whitespace(raw);
let clipped = truncate_for_width(&collapsed, width);
format!("{clipped:<width$}")
}
fn row_to_structured_spans(row: &[String], widths: &[usize]) -> Vec<Span<'static>> {
let palette = themes::current();
let base = Style::default().fg(palette.text);
let alt = Style::default().fg(palette.hint);
let mut spans: Vec<Span<'static>> = Vec::new();
for (j, width) in widths.iter().enumerate() {
let padded = structured_cell_text(row, j, *width);
let style = if j % 2 == 0 { base } else { alt };
spans.push(Span::styled(padded, style));
if j + 1 < widths.len() {
spans.push(Span::styled(" | ".to_string(), base));
}
}
spans
}
fn wide_structured_banner(
shown_rows: usize,
total_rows: usize,
total_cols: usize,
visible_cols: usize,
hidden_rows: usize,
hidden_cols: usize,
) -> String {
format!(
"[wide delimited view: truncated from {total_rows} rows and {total_cols} columns; rows {shown_rows} shown, {hidden_rows} hidden; columns {visible_cols} shown, {hidden_cols} hidden]"
)
}
struct WideViewMetrics {
visible_cols: usize,
widths: Vec<usize>,
total_cols: usize,
shown_rows: usize,
total_rows: usize,
hidden_rows: usize,
hidden_cols: usize,
}
fn wide_view_metrics(
rows: &[Vec<String>],
content_width: u16,
total_rows_hint: Option<usize>,
) -> Option<WideViewMetrics> {
if rows.is_empty() {
return None;
}
let (visible_cols, widths, total_cols) = visible_cols_and_widths(rows, content_width);
if visible_cols == 0 {
return None;
}
let shown_rows = rows.len();
let total_rows = total_rows_hint.unwrap_or(shown_rows).max(shown_rows);
Some(WideViewMetrics {
visible_cols,
hidden_cols: total_cols.saturating_sub(visible_cols),
widths,
total_cols,
shown_rows,
total_rows,
hidden_rows: total_rows.saturating_sub(shown_rows),
})
}
#[must_use]
pub fn wide_structured_string(
rows: &[Vec<String>],
content_width: u16,
total_rows_hint: Option<usize>,
) -> String {
let Some(m) = wide_view_metrics(rows, content_width, total_rows_hint) else {
return String::new();
};
let mut out = String::new();
let _ = write!(
out,
"{}",
wide_structured_banner(
m.shown_rows,
m.total_rows,
m.total_cols,
m.visible_cols,
m.hidden_rows,
m.hidden_cols
)
);
out.push('\n');
for row in rows {
out.push_str(&row_to_structured_line(row, &m.widths));
out.push('\n');
}
if out.ends_with('\n') {
out.pop();
}
out
}
#[must_use]
pub fn wide_structured_text(
rows: &[Vec<String>],
content_width: u16,
total_rows_hint: Option<usize>,
) -> Text<'static> {
let Some(m) = wide_view_metrics(rows, content_width, total_rows_hint) else {
return Text::default();
};
let palette = themes::current();
let banner = wide_structured_banner(
m.shown_rows,
m.total_rows,
m.total_cols,
m.visible_cols,
m.hidden_rows,
m.hidden_cols,
);
let mut lines: Vec<Line<'static>> = Vec::with_capacity(rows.len().saturating_add(1));
lines.push(Line::from(vec![Span::styled(
banner,
Style::default()
.fg(palette.text)
.add_modifier(Modifier::ITALIC),
)]));
lines.extend(
rows.iter()
.map(|row| Line::from(row_to_structured_spans(row, &m.widths))),
);
Text::from(lines)
}
#[must_use]
pub fn wide_structured_line_count(rows: &[Vec<String>], content_width: u16) -> usize {
wide_structured_string(rows, content_width, None)
.lines()
.count()
}
fn parse_with_delimiter(raw: &str, delim: u8) -> Result<Vec<Vec<String>>, ::csv::Error> {
let mut rows = Vec::new();
let mut rdr = ::csv::ReaderBuilder::new()
.has_headers(false)
.delimiter(delim)
.flexible(true)
.from_reader(Cursor::new(raw));
for result in rdr.records() {
let record = result?;
rows.push(record.iter().map(String::from).collect());
}
Ok(rows)
}
pub fn parse_csv(raw: &str, path_hint: Option<&str>) -> Result<Vec<Vec<String>>, ::csv::Error> {
let hint = path_hint.unwrap_or("");
let delim = delimiter_from_path_for_viewer(hint).unwrap_or_else(|| detect_delimiter_byte(raw));
let mut rows = parse_with_delimiter(raw, delim)?;
strip_total_rows_meta_row(&mut rows);
Ok(rows)
}
#[must_use]
pub fn table_string_with_max_cell(
rows: &[Vec<String>],
content_width: u16,
max_cell_chars: usize,
) -> String {
pretty_tables::table_string_with_max_cell(rows, content_width, max_cell_chars)
}
#[must_use]
pub fn table_string_rows_only(
rows: &[Vec<String>],
content_width: u16,
max_cell_chars: usize,
) -> String {
pretty_tables::table_string_rows_only(rows, content_width, max_cell_chars)
}
#[must_use]
pub fn table_string(rows: &[Vec<String>], content_width: u16) -> String {
if rows.is_empty() {
return String::new();
}
let mut rows = rows.to_vec();
let header = rows.remove(0);
pretty_tables::table_string_header_body_smart_wrap(&header, &rows, content_width)
}
#[must_use]
pub fn table_to_text(rows: &[Vec<String>], content_width: u16) -> Text<'static> {
table_string_to_text(&table_string(rows, content_width))
}
#[must_use]
pub fn table_string_to_text(table_str: &str) -> Text<'static> {
pretty_tables::table_string_to_text(table_str)
}
#[must_use]
pub fn table_line_count(rows: &[Vec<String>], content_width: u16) -> usize {
table_string(rows, content_width).lines().count()
}
#[must_use]
pub fn table_string_and_line_count(rows: &[Vec<String>], content_width: u16) -> (String, usize) {
let s = table_string(rows, content_width);
let count = s.lines().count();
(s, count)
}