use ratatui::layout::Constraint;
use ratatui::text::Line;
use ratatui::widgets::{Cell, Row, Table};
use rayon::prelude::*;
use crate::config::PARALLEL;
use crate::layout::style;
use crate::modules::viewer_search;
use crate::ui::UI_STRINGS;
use crate::utils::truncate_middle;
use super::{
format,
sections::{ContentsSection, KvSection, SingleColumnListSection},
};
const COLUMN_SPACING: usize = 1;
const KEY_WIDTH_FALLBACK: usize = 4;
const KEY_WIDTH_MIN: usize = 35;
const VALUE_WIDTH_MIN: usize = 10;
const SIZE_OPTIMIZATION_COLUMN_THRESHOLD: usize = 3;
pub struct KvFindSync<'a> {
pub line_starts: &'a [usize],
pub ranges: &'a [(usize, usize)],
pub current: usize,
pub first_data_line_idx: usize,
pub row_skip: usize,
}
#[derive(Clone, Copy)]
pub struct TableFindRenderCtx<'a> {
pub needle: Option<&'a str>,
pub current_line_idx: Option<usize>,
pub first_data_line_idx: usize,
pub metadata_mode: bool,
}
#[derive(Clone, Copy)]
pub struct TableWindow {
pub row_offset: usize,
pub start: usize,
pub end: usize,
}
#[inline]
fn table_with_chrome(t: Table<'static>) -> Table<'static> {
t.column_spacing(COLUMN_SPACING as u16)
.style(style::text_style())
}
#[inline]
fn cell_for_str(
s: &str,
find_needle: Option<&str>,
current_match_row: bool,
metadata_mode: bool,
) -> Cell<'static> {
if viewer_search::option_needle_nonempty(find_needle) {
if current_match_row && metadata_mode {
Cell::from(viewer_search::highlight_cell_line_with_style(
s,
find_needle.unwrap(),
style::viewer_find_match_current_metadata_contrast(),
true,
))
} else if metadata_mode {
Cell::from(viewer_search::highlight_cell_line_ascii_insensitive(
s,
find_needle.unwrap(),
))
} else {
Cell::from(viewer_search::highlight_cell_line(s, find_needle.unwrap()))
}
} else {
Cell::from(Line::from(s.to_string()))
}
}
#[must_use]
pub fn balanced_column_widths(
natural: &[usize],
available_width: usize,
spacing: usize,
) -> Vec<u16> {
let n = natural.len().max(1);
let gaps = (n - 1) * spacing;
let available = available_width.saturating_sub(gaps);
if available == 0 {
return natural.iter().map(|_| 1u16).collect();
}
let total: usize = natural.iter().sum();
if total == 0 {
let w = (available / n).min(u16::MAX as usize) as u16;
return (0..natural.len()).map(|_| w.max(1)).collect();
}
let mut widths: Vec<u16> = natural
.iter()
.map(|&nat| {
let w = (nat * available) / total;
(w.min(u16::MAX as usize).max(1)) as u16
})
.collect();
let mut remainder = available.saturating_sub(widths.iter().map(|&w| w as usize).sum::<usize>());
for w in &mut widths {
if remainder == 0 {
break;
}
*w = (*w as usize + 1).min(u16::MAX as usize) as u16;
remainder -= 1;
}
widths
}
#[must_use]
pub fn entry_cell(
obj: &serde_json::Map<String, serde_json::Value>,
key: &str,
max_array_inline: usize,
) -> String {
obj.get(key).map_or_else(
|| "—".to_string(),
|v| format::format_value(v, key, max_array_inline),
)
}
#[must_use]
pub fn section_to_table(
section: &KvSection,
row_offset: usize,
find_needle: Option<&str>,
find_kv: Option<&KvFindSync<'_>>,
metadata_mode: bool,
table_width_chars: u16,
) -> Table<'static> {
let header = Row::new(vec![
UI_STRINGS.tables.header_key,
UI_STRINGS.tables.header_value,
])
.style(style::table_header_style())
.bottom_margin(0);
let key_w = section
.rows
.iter()
.map(|(k, _)| k.chars().count())
.max()
.unwrap_or(KEY_WIDTH_FALLBACK)
.min(KEY_WIDTH_MIN) as u16;
let value_max_chars = (table_width_chars as usize)
.saturating_sub(key_w as usize)
.saturating_sub(COLUMN_SPACING)
.max(8);
let truncate_value = find_kv.is_none() && !viewer_search::option_needle_nonempty(find_needle);
let data_rows: Vec<Row> = section
.rows
.iter()
.enumerate()
.map(|(i, (k, v))| {
let v_display = if truncate_value && v.chars().count() > value_max_chars {
truncate_middle(v, value_max_chars)
} else {
v.clone()
};
let (key_cell, value_cell) = if let Some(f) = find_kv {
let li = f.first_data_line_idx + f.row_skip + i;
let key_off = f.line_starts.get(li).copied().unwrap_or(0);
let value_off = key_off.saturating_add(k.len()).saturating_add(1);
let base_style = style::text_style();
let match_style = style::viewer_find_match_table_cell();
let current_style = if metadata_mode {
style::viewer_find_match_current_metadata_contrast()
} else {
style::viewer_find_match_current_table_cell()
};
let key_cell = Cell::from(viewer_search::highlight_line_with_find_styles(
k.as_str(),
key_off,
f.ranges,
f.current,
base_style,
match_style,
current_style,
));
let value_cell = Cell::from(viewer_search::highlight_line_with_find_styles(
v.as_str(),
value_off,
f.ranges,
f.current,
base_style,
match_style,
current_style,
));
(key_cell, value_cell)
} else {
let key_cell = cell_for_str(k.as_str(), find_needle, false, false);
let value_cell = if viewer_search::option_needle_nonempty(find_needle) {
cell_for_str(v_display.as_str(), find_needle, false, false)
} else {
match format::value_cell_style(v_display.as_str()) {
Some(st) => Cell::from(Line::from(v_display).style(st)),
None => Cell::from(Line::from(v_display)),
}
};
(key_cell, value_cell)
};
Row::new(vec![key_cell, value_cell]).style(style::table_row_style(row_offset + i))
})
.collect();
table_with_chrome(
Table::new(
data_rows,
[
Constraint::Length(key_w),
Constraint::Min(VALUE_WIDTH_MIN as u16),
],
)
.header(header),
)
}
fn contents_row(
obj: &serde_json::Map<String, serde_json::Value>,
column_keys: &[String],
column_widths: &[u16],
max_array_inline: usize,
) -> Vec<String> {
column_keys
.iter()
.enumerate()
.map(|(j, k)| {
let cell = entry_cell(obj, k, max_array_inline);
let max_chars = column_widths.get(j).copied().unwrap_or(0) as usize;
let len = cell.chars().count();
if max_chars > 0 && len > max_chars {
truncate_middle(&cell, max_chars)
} else {
cell
}
})
.collect()
}
fn accumulate_natural_widths_from_entries<'a>(
natural: &mut [usize],
entries: impl Iterator<Item = &'a serde_json::Value>,
keys: &[String],
max_array_inline: usize,
) {
for v in entries {
let Some(obj) = v.as_object() else {
continue;
};
for (j, k) in keys.iter().enumerate() {
let len = entry_cell(obj, k, max_array_inline).chars().count();
if let Some(nat) = natural.get_mut(j) {
*nat = (*nat).max(len);
}
}
}
}
fn merge_max_natural_widths(acc: &mut [usize], chunk: &[usize]) {
for (j, &cn) in chunk.iter().enumerate() {
if let Some(nat_j) = acc.get_mut(j) {
*nat_j = (*nat_j).max(cn);
}
}
}
#[must_use]
pub fn contents_natural_widths(
section: &ContentsSection,
start: usize,
end: usize,
max_array_inline: usize,
) -> Vec<usize> {
let keys = §ion.column_keys;
let cols = §ion.columns;
if keys.is_empty() {
return vec![];
}
let header_natural: Vec<usize> = cols.iter().map(|s| s.chars().count()).collect();
let entries_window = end.saturating_sub(start);
if entries_window < PARALLEL.contents_natural_widths {
let mut natural = header_natural;
accumulate_natural_widths_from_entries(
&mut natural,
section.entries.iter().skip(start).take(entries_window),
keys,
max_array_inline,
);
natural
} else {
let slice = §ion.entries[start..end];
let chunk_size = (entries_window / 4).max(1);
let chunk_naturals: Vec<Vec<usize>> = slice
.par_chunks(chunk_size)
.map(|chunk| {
let mut nat = header_natural.clone();
accumulate_natural_widths_from_entries(
&mut nat,
chunk.iter(),
keys,
max_array_inline,
);
nat
})
.collect();
let mut natural = header_natural;
for chunk_nat in chunk_naturals {
merge_max_natural_widths(&mut natural, &chunk_nat);
}
natural
}
}
fn contents_header_widths(section: &ContentsSection) -> Vec<u16> {
section
.columns
.iter()
.map(|s| s.chars().count().min(u16::MAX as usize) as u16)
.collect()
}
#[must_use]
pub fn contents_to_table_window(
section: &ContentsSection,
window: TableWindow,
table_width: u16,
max_array_inline: usize,
find: TableFindRenderCtx<'_>,
) -> Table<'static> {
let natural = contents_natural_widths(section, window.start, window.end, max_array_inline);
let header_widths = contents_header_widths(section);
let ncols = section.column_keys.len();
let use_size_optimization = ncols > SIZE_OPTIMIZATION_COLUMN_THRESHOLD;
let mut column_widths = if natural.is_empty() {
let available =
(table_width as usize).saturating_sub((ncols.saturating_sub(1)) * COLUMN_SPACING);
let w = (available / ncols.max(1)).min(u16::MAX as usize) as u16;
(0..ncols).map(|_| w.max(1)).collect::<Vec<u16>>()
} else if use_size_optimization {
balanced_column_widths(&natural, table_width as usize, COLUMN_SPACING)
} else {
let gaps = (ncols.saturating_sub(1)) * COLUMN_SPACING;
let natural_with_header: Vec<usize> = natural
.iter()
.zip(header_widths.iter())
.map(|(n, &hw)| (*n).max(hw as usize))
.collect();
let total_compact = natural_with_header.iter().sum::<usize>() + gaps;
if total_compact <= table_width as usize {
natural_with_header
.into_iter()
.map(|w| w.min(u16::MAX as usize) as u16)
.collect()
} else {
balanced_column_widths(&natural_with_header, table_width as usize, COLUMN_SPACING)
}
};
for (j, &min_w) in header_widths.iter().enumerate() {
if let Some(w) = column_widths.get_mut(j) {
*w = (*w).max(min_w);
}
}
let constraints: Vec<Constraint> = column_widths
.iter()
.map(|&w| Constraint::Length(w))
.collect();
let header = Row::new(
section
.columns
.iter()
.map(|s| cell_for_str(s.as_str(), find.needle, false, false))
.collect::<Vec<_>>(),
)
.style(style::table_header_style())
.bottom_margin(0);
let data_rows: Vec<Row> = section
.entries
.iter()
.enumerate()
.skip(window.start)
.take(window.end.saturating_sub(window.start))
.filter_map(|(_i, v)| v.as_object())
.enumerate()
.map(|(idx, obj)| {
let global_row_idx = window.start + idx;
let row_is_current = find
.current_line_idx
.is_some_and(|li| li == find.first_data_line_idx.saturating_add(global_row_idx));
let row_strs =
contents_row(obj, §ion.column_keys, &column_widths, max_array_inline);
Row::new(
row_strs
.into_iter()
.map(|c| cell_for_str(&c, find.needle, row_is_current, find.metadata_mode))
.collect::<Vec<_>>(),
)
.style(style::table_row_style(
window.row_offset + window.start + idx,
))
})
.collect();
table_with_chrome(Table::new(data_rows, constraints).header(header))
}
#[must_use]
pub fn single_column_list_to_table(
section: &SingleColumnListSection,
window: TableWindow,
find: TableFindRenderCtx<'_>,
) -> Table<'static> {
let data_rows: Vec<Row> = section
.values
.iter()
.skip(window.start)
.take(window.end.saturating_sub(window.start))
.enumerate()
.map(|(idx, s)| {
let global_row_idx = window.start + idx;
let row_is_current = find
.current_line_idx
.is_some_and(|li| li == find.first_data_line_idx.saturating_add(global_row_idx));
Row::new(vec![cell_for_str(
s.as_str(),
find.needle,
row_is_current,
find.metadata_mode,
)])
.style(style::table_row_style(
window.row_offset + window.start + idx,
))
})
.collect();
table_with_chrome(Table::new(data_rows, [Constraint::Min(0)]))
}