use super::types::Delimiter;
use super::types::SortOrder;
use crate::style::Color;
use crate::utils::natural_cmp;
use crate::widget::theme::{DARK_GRAY, PLACEHOLDER_FG};
use crate::widget::traits::WidgetProps;
#[derive(Clone, Debug)]
pub struct CsvViewer {
pub data: Vec<Vec<String>>,
pub has_header: bool,
pub column_widths: Vec<u16>,
pub selected_row: usize,
pub selected_col: usize,
pub scroll_row: usize,
pub scroll_col: usize,
pub sort_column: Option<usize>,
pub sort_order: SortOrder,
pub sorted_indices: Vec<usize>,
pub search_query: String,
pub search_matches: Vec<(usize, usize)>,
pub current_match: usize,
pub show_row_numbers: bool,
pub show_separators: bool,
pub delimiter: Delimiter,
pub header_fg: Option<Color>,
pub header_bg: Option<Color>,
pub selected_fg: Option<Color>,
pub selected_bg: Option<Color>,
pub match_fg: Option<Color>,
pub match_bg: Option<Color>,
pub separator_fg: Option<Color>,
pub row_number_fg: Option<Color>,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub props: WidgetProps,
}
impl Default for CsvViewer {
fn default() -> Self {
Self::new()
}
}
impl CsvViewer {
pub fn new() -> Self {
Self {
data: Vec::new(),
has_header: true,
column_widths: Vec::new(),
selected_row: 0,
selected_col: 0,
scroll_row: 0,
scroll_col: 0,
sort_column: None,
sort_order: SortOrder::None,
sorted_indices: Vec::new(),
search_query: String::new(),
search_matches: Vec::new(),
current_match: 0,
show_row_numbers: true,
show_separators: true,
delimiter: Delimiter::Auto,
header_fg: Some(Color::WHITE),
header_bg: Some(Color::rgb(60, 60, 80)),
selected_fg: Some(Color::WHITE),
selected_bg: Some(Color::BLUE),
match_fg: Some(Color::BLACK),
match_bg: Some(Color::YELLOW),
separator_fg: Some(DARK_GRAY),
row_number_fg: Some(PLACEHOLDER_FG),
fg: None,
bg: None,
props: WidgetProps::new(),
}
}
pub fn from_content(content: &str) -> Self {
let mut viewer = Self::new();
viewer.parse(content);
viewer
}
pub fn parse(&mut self, content: &str) {
let delimiter = self.detect_delimiter(content);
self.data = self.parse_csv(content, delimiter);
self.calculate_column_widths();
self.reset_sort();
}
pub fn data(mut self, data: Vec<Vec<String>>) -> Self {
self.data = data;
self.calculate_column_widths();
self.reset_sort();
self
}
pub fn has_header(mut self, has_header: bool) -> Self {
self.has_header = has_header;
self
}
pub fn delimiter(mut self, delimiter: Delimiter) -> Self {
self.delimiter = delimiter;
self
}
pub fn show_row_numbers(mut self, show: bool) -> Self {
self.show_row_numbers = show;
self
}
pub fn show_separators(mut self, show: bool) -> Self {
self.show_separators = show;
self
}
pub fn header_style(mut self, fg: Color, bg: Color) -> Self {
self.header_fg = Some(fg);
self.header_bg = Some(bg);
self
}
pub fn selected_style(mut self, fg: Color, bg: Color) -> Self {
self.selected_fg = Some(fg);
self.selected_bg = Some(bg);
self
}
pub fn match_style(mut self, fg: Color, bg: Color) -> Self {
self.match_fg = Some(fg);
self.match_bg = Some(bg);
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn row_count(&self) -> usize {
if self.has_header && !self.data.is_empty() {
self.data.len() - 1
} else {
self.data.len()
}
}
pub fn column_count(&self) -> usize {
self.data.first().map(|r| r.len()).unwrap_or(0)
}
pub fn selected_row(&self) -> usize {
self.selected_row
}
pub fn selected_col(&self) -> usize {
self.selected_col
}
pub fn get_cell(&self, row: usize, col: usize) -> Option<&str> {
let actual_row = if self.has_header { row + 1 } else { row };
self.data
.get(actual_row)
.and_then(|r| r.get(col))
.map(|s| s.as_str())
}
pub fn get_header(&self, col: usize) -> Option<&str> {
if self.has_header {
self.data
.first()
.and_then(|r| r.get(col))
.map(|s| s.as_str())
} else {
None
}
}
pub fn selected_value(&self) -> Option<&str> {
self.get_cell(self.selected_row, self.selected_col)
}
pub fn is_searching(&self) -> bool {
!self.search_query.is_empty()
}
pub fn match_count(&self) -> usize {
self.search_matches.len()
}
pub fn select_down(&mut self) {
let max_row = self.row_count().saturating_sub(1);
self.selected_row = (self.selected_row + 1).min(max_row);
self.ensure_visible();
}
pub fn select_up(&mut self) {
self.selected_row = self.selected_row.saturating_sub(1);
self.ensure_visible();
}
pub fn select_right(&mut self) {
let max_col = self.column_count().saturating_sub(1);
self.selected_col = (self.selected_col + 1).min(max_col);
}
pub fn select_left(&mut self) {
self.selected_col = self.selected_col.saturating_sub(1);
}
pub fn select_first_row(&mut self) {
self.selected_row = 0;
self.ensure_visible();
}
pub fn select_last_row(&mut self) {
self.selected_row = self.row_count().saturating_sub(1);
self.ensure_visible();
}
pub fn page_down(&mut self, page_size: usize) {
let max_row = self.row_count().saturating_sub(1);
self.selected_row = (self.selected_row + page_size).min(max_row);
self.ensure_visible();
}
pub fn page_up(&mut self, page_size: usize) {
self.selected_row = self.selected_row.saturating_sub(page_size);
self.ensure_visible();
}
fn ensure_visible(&mut self) {
}
pub fn sort_by(&mut self, column: usize) {
if self.sort_column == Some(column) {
self.sort_order = match self.sort_order {
SortOrder::None => SortOrder::Ascending,
SortOrder::Ascending => SortOrder::Descending,
SortOrder::Descending => SortOrder::None,
};
} else {
self.sort_column = Some(column);
self.sort_order = SortOrder::Ascending;
}
self.apply_sort();
}
pub fn reset_sort(&mut self) {
self.sort_column = None;
self.sort_order = SortOrder::None;
let start = if self.has_header { 1 } else { 0 };
self.sorted_indices = (start..self.data.len()).collect();
}
fn apply_sort(&mut self) {
let start = if self.has_header { 1 } else { 0 };
self.sorted_indices = (start..self.data.len()).collect();
if let Some(col) = self.sort_column {
match self.sort_order {
SortOrder::None => {}
SortOrder::Ascending => {
self.sorted_indices.sort_by(|&a, &b| {
let val_a = self
.data
.get(a)
.and_then(|r| r.get(col))
.map(|s| s.as_str())
.unwrap_or("");
let val_b = self
.data
.get(b)
.and_then(|r| r.get(col))
.map(|s| s.as_str())
.unwrap_or("");
natural_cmp(val_a, val_b)
});
}
SortOrder::Descending => {
self.sorted_indices.sort_by(|&a, &b| {
let val_a = self
.data
.get(a)
.and_then(|r| r.get(col))
.map(|s| s.as_str())
.unwrap_or("");
let val_b = self
.data
.get(b)
.and_then(|r| r.get(col))
.map(|s| s.as_str())
.unwrap_or("");
natural_cmp(val_b, val_a)
});
}
}
}
}
pub fn search(&mut self, query: &str) {
self.search_query = query.to_lowercase();
self.search_matches.clear();
self.current_match = 0;
if self.search_query.is_empty() {
return;
}
let start = if self.has_header { 1 } else { 0 };
for (row_idx, row) in self.data.iter().enumerate().skip(start) {
for (col_idx, cell) in row.iter().enumerate() {
if cell.to_lowercase().contains(&self.search_query) {
self.search_matches.push((row_idx - start, col_idx));
}
}
}
}
pub fn clear_search(&mut self) {
self.search_query.clear();
self.search_matches.clear();
self.current_match = 0;
}
pub fn next_match(&mut self) {
if !self.search_matches.is_empty() {
self.current_match = (self.current_match + 1) % self.search_matches.len();
let (row, col) = self.search_matches[self.current_match];
self.selected_row = row;
self.selected_col = col;
}
}
pub fn prev_match(&mut self) {
if !self.search_matches.is_empty() {
self.current_match = self
.current_match
.checked_sub(1)
.unwrap_or(self.search_matches.len() - 1);
let (row, col) = self.search_matches[self.current_match];
self.selected_row = row;
self.selected_col = col;
}
}
fn detect_delimiter(&self, content: &str) -> char {
if let Some(c) = self.delimiter.char() {
return c;
}
let first_lines: String = content.lines().take(5).collect::<Vec<_>>().join("\n");
let delimiters = [',', '\t', ';', '|'];
let mut best = ',';
let mut best_count = 0;
for &d in &delimiters {
let count = first_lines.matches(d).count();
if count > best_count {
best_count = count;
best = d;
}
}
best
}
fn parse_csv(&self, content: &str, delimiter: char) -> Vec<Vec<String>> {
let mut result = Vec::new();
let mut current_row = Vec::new();
let mut current_field = String::new();
let mut in_quotes = false;
let mut chars = content.chars().peekable();
while let Some(c) = chars.next() {
if in_quotes {
if c == '"' {
if chars.peek() == Some(&'"') {
current_field.push('"');
chars.next();
} else {
in_quotes = false;
}
} else {
current_field.push(c);
}
} else if c == '"' {
in_quotes = true;
} else if c == delimiter {
current_row.push(current_field.trim().to_string());
current_field = String::new();
} else if c == '\n' {
current_row.push(current_field.trim().to_string());
if !current_row.iter().all(|s| s.is_empty()) {
result.push(current_row);
}
current_row = Vec::new();
current_field = String::new();
} else if c != '\r' {
current_field.push(c);
}
}
if !current_field.is_empty() || !current_row.is_empty() {
current_row.push(current_field.trim().to_string());
if !current_row.iter().all(|s| s.is_empty()) {
result.push(current_row);
}
}
result
}
fn calculate_column_widths(&mut self) {
let col_count = self.column_count();
self.column_widths = vec![0; col_count];
for row in &self.data {
for (col, cell) in row.iter().enumerate() {
if col < self.column_widths.len() {
let width = crate::utils::display_width(cell) as u16;
self.column_widths[col] = self.column_widths[col].max(width);
}
}
}
for w in &mut self.column_widths {
*w = (*w).clamp(3, 40);
}
}
pub fn row_number_width(&self) -> u16 {
if self.show_row_numbers {
let digits = (self.row_count().max(1) as f64).log10().floor() as u16 + 1;
digits.max(2) + 1 } else {
0
}
}
}