mod csv;
mod find;
mod input;
mod ui;
#[allow(dead_code)]
mod util;
mod view;
use crate::input::{Control, InputHandler};
use crate::ui::{CsvTable, CsvTableState, FinderState};
extern crate csv as sushi_csv;
use anyhow::{Context, Result, bail};
use clap::Parser;
use std::convert::TryInto;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::sync::Arc;
use std::usize;
use tempfile::NamedTempFile;
use termion::{raw::IntoRawMode, screen::AlternateScreen};
use tui::backend::TermionBackend;
use tui::Terminal;
fn get_offsets_to_make_visible(
found_record: find::FoundRecord,
rows_view: &view::RowsView,
csv_table_state: &CsvTableState,
) -> (Option<u64>, Option<u64>) {
let new_rows_offset;
if rows_view.in_view(found_record.row_index() as u64) {
new_rows_offset = None;
} else {
new_rows_offset = Some(found_record.row_index() as u64);
}
let new_cols_offset;
let cols_offset = csv_table_state.cols_offset;
let last_rendered_col = cols_offset.saturating_add(csv_table_state.num_cols_rendered);
let column_index = found_record.first_column() as u64;
if column_index >= cols_offset && column_index < last_rendered_col {
new_cols_offset = None;
} else {
new_cols_offset = Some(column_index)
}
(new_rows_offset, new_cols_offset)
}
fn scroll_to_found_record(
found_record: find::FoundRecord,
rows_view: &mut view::RowsView,
csv_table_state: &mut CsvTableState,
) {
let (new_rows_offset, new_cols_offset) =
get_offsets_to_make_visible(found_record.clone(), rows_view, csv_table_state);
if let Some(rows_offset) = new_rows_offset {
rows_view.set_rows_from(rows_offset).unwrap();
csv_table_state.set_rows_offset(rows_offset);
}
if let Some(cols_offset) = new_cols_offset {
csv_table_state.set_cols_offset(cols_offset);
}
}
struct SeekableFile {
filename: String,
inner_file: Option<NamedTempFile>,
}
impl SeekableFile {
fn new(filename: &str) -> Result<SeekableFile> {
let mut f = File::open(filename).context(format!("Failed to open file: {}", filename))?;
let mut inner_file = NamedTempFile::new()?;
let inner_file_res;
if f.seek(SeekFrom::Start(0)).is_err() {
let mut buffer: Vec<u8> = vec![];
f.read_to_end(&mut buffer)?;
inner_file.write(&buffer)?;
inner_file_res = Some(inner_file);
} else {
inner_file_res = None;
}
Ok(SeekableFile {
filename: filename.to_string(),
inner_file: inner_file_res,
})
}
fn filename(&self) -> &str {
if let Some(f) = &self.inner_file {
f.path().to_str().unwrap()
} else {
self.filename.as_str()
}
}
}
#[derive(Parser, Debug)]
struct Args {
filename: String,
#[clap(short, long)]
delimiter: Option<String>,
#[clap(long)]
debug: bool,
}
fn parse_delimiter(args: &Args) -> Result<Option<u8>> {
if let Some(s) = &args.delimiter {
let mut chars = s.chars();
let c = chars.next().context("Delimiter should not be empty")?;
if !c.is_ascii() {
bail!("Delimiter should be within the ASCII range: {} is too fancy", c);
}
if chars.next().is_some() {
bail!("Delimiter should be exactly one character, got {}", s);
}
Ok(Some(c.try_into()?))
} else {
Ok(None)
}
}
fn run_csvlens() -> Result<()> {
let args = Args::parse();
let show_stats = args.debug;
let delimiter = parse_delimiter(&args)?;
let file = SeekableFile::new(args.filename.as_str())?;
let filename = file.filename();
let num_rows_not_visible = 5;
let num_rows = 50 - num_rows_not_visible;
let mut config = csv::CsvConfig::new(filename);
if let Some(d) = delimiter {
config.builder.delimiter(d);
}
let shared_config = Arc::new(config);
let csvlens_reader = csv::CsvLensReader::new(shared_config.clone())
.context(format!("Failed to open file: {}", filename))?;
let mut rows_view = view::RowsView::new(csvlens_reader, num_rows)?;
let headers = rows_view.headers().clone();
let stdout = io::stdout().into_raw_mode().unwrap();
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
let mut input_handler = InputHandler::new();
let mut csv_table_state = CsvTableState::new(filename.to_string(), headers.len());
let mut finder: Option<find::Finder> = None;
let mut first_found_scrolled = false;
loop {
terminal
.draw(|f| {
let size = f.size();
let frame_size_adjusted_num_rows =
size.height.saturating_sub(num_rows_not_visible as u16) as u64;
rows_view
.set_num_rows(frame_size_adjusted_num_rows)
.unwrap();
let rows = rows_view.rows();
let csv_table = CsvTable::new(&headers, rows);
f.render_stateful_widget(csv_table, size, &mut csv_table_state);
})
.unwrap();
let control = input_handler.next();
rows_view.handle_control(&control)?;
match control {
Control::Quit => {
break;
}
Control::ScrollTo(_) => {
csv_table_state.reset_buffer();
}
Control::ScrollLeft => {
let new_cols_offset = csv_table_state.cols_offset.saturating_sub(1);
csv_table_state.set_cols_offset(new_cols_offset);
}
Control::ScrollRight => {
if csv_table_state.has_more_cols_to_show() {
let new_cols_offset = csv_table_state.cols_offset.saturating_add(1);
csv_table_state.set_cols_offset(new_cols_offset);
}
}
Control::ScrollToNextFound if !rows_view.is_filter() => {
if let Some(fdr) = finder.as_mut() {
if let Some(found_record) = fdr.next() {
scroll_to_found_record(found_record, &mut rows_view, &mut csv_table_state);
}
}
}
Control::ScrollToPrevFound if !rows_view.is_filter() => {
if let Some(fdr) = finder.as_mut() {
if let Some(found_record) = fdr.prev() {
scroll_to_found_record(found_record, &mut rows_view, &mut csv_table_state);
}
}
}
Control::Find(s) => {
finder = Some(find::Finder::new(shared_config.clone(), s.as_str()).unwrap());
first_found_scrolled = false;
rows_view.reset_filter().unwrap();
csv_table_state.reset_buffer();
}
Control::Filter(s) => {
finder = Some(find::Finder::new(shared_config.clone(), s.as_str()).unwrap());
csv_table_state.reset_buffer();
rows_view.set_rows_from(0).unwrap();
rows_view.set_filter(finder.as_ref().unwrap()).unwrap();
}
Control::BufferContent(buf) => {
csv_table_state.set_buffer(input_handler.mode(), buf.as_str());
}
Control::BufferReset => {
csv_table_state.reset_buffer();
if finder.is_some() {
finder = None;
csv_table_state.finder_state = FinderState::FinderInactive;
rows_view.reset_filter().unwrap();
}
}
_ => {}
}
if let Some(fdr) = finder.as_mut() {
if !rows_view.is_filter() {
if !first_found_scrolled && fdr.count() > 0 {
fdr.set_row_hint(0);
if let Some(found_record) = fdr.next() {
scroll_to_found_record(found_record, &mut rows_view, &mut csv_table_state);
}
first_found_scrolled = true;
}
if let Some(cursor_row_index) = fdr.cursor_row_index() {
if !rows_view.in_view(cursor_row_index as u64) {
fdr.reset_cursor();
}
}
fdr.set_row_hint(rows_view.rows_from() as usize);
} else {
rows_view.set_filter(fdr).unwrap();
}
}
if let Some(elapsed) = rows_view.elapsed() {
if show_stats {
csv_table_state.elapsed = Some(elapsed as f64 / 1000.0);
}
}
csv_table_state.set_rows_offset(rows_view.rows_from());
csv_table_state.selected = rows_view.selected();
if let Some(n) = rows_view.get_total_line_numbers() {
csv_table_state.set_total_line_number(n);
} else if let Some(n) = rows_view.get_total_line_numbers_approx() {
csv_table_state.set_total_line_number(n);
}
if let Some(f) = &finder {
csv_table_state.finder_state = FinderState::from_finder(f, &rows_view);
}
}
Ok(())
}
fn main() {
if let Err(e) = run_csvlens() {
println!("{}", e.to_string());
std::process::exit(1);
}
}