use crate::app::{App, WrapMode};
use crate::delimiter::Delimiter;
use crate::errors::CsvlensResult;
#[cfg(feature = "cli")]
use clap::ArgGroup;
#[cfg(feature = "cli")]
use clap::Parser;
#[cfg(feature = "cli")]
use clap::ValueEnum;
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use std::ffi::OsString;
use std::io::LineWriter;
use std::panic;
use std::thread::panicking;
#[cfg(feature = "cli")]
#[derive(Debug, Clone, ValueEnum)]
#[clap(rename_all = "lower")]
pub enum ClapWrapMode {
Chars,
Words,
}
#[cfg(feature = "cli")]
#[derive(Parser, Debug)]
#[command(version)]
#[command(group(ArgGroup::new("wrap_flags").conflicts_with("wrap")))]
#[command(styles = clap_cargo::style::CLAP_STYLING)]
struct Args {
filename: Option<String>,
#[clap(short, long, value_name = "char")]
delimiter: Option<String>,
#[clap(short = 't', long)]
tab_separated: bool,
#[clap(short = 'c', long)]
comma_separated: bool,
#[clap(long)]
no_headers: bool,
#[arg(long, value_name = "regex")]
columns: Option<String>,
#[arg(long, value_name = "regex")]
filter: Option<String>,
#[arg(long, value_name = "regex")]
find: Option<String>,
#[clap(short, long)]
ignore_case: bool,
#[arg(long, value_name = "column_name")]
echo_column: Option<String>,
#[arg(long, alias = "colorful", visible_alias = "colorful")]
color_columns: bool,
#[arg(long, value_name = "prompt")]
prompt: Option<String>,
#[arg(long, value_enum, value_name = "mode")]
pub wrap: Option<ClapWrapMode>,
#[arg(short = 'S', group = "wrap_flags")]
pub wrap_chars: bool,
#[arg(short = 'W', group = "wrap_flags")]
pub wrap_words: bool,
#[clap(long)]
pub auto_reload: bool,
#[clap(long)]
debug: bool,
#[clap(long)]
pub no_streaming_stdin: bool,
}
#[cfg(feature = "cli")]
impl Args {
fn get_wrap_mode(
wrap: Option<ClapWrapMode>,
wrap_chars: bool,
wrap_words: bool,
) -> Option<WrapMode> {
if let Some(mode) = wrap {
return match mode {
ClapWrapMode::Chars => Some(WrapMode::Chars),
ClapWrapMode::Words => Some(WrapMode::Words),
};
} else {
if wrap_chars {
return Some(WrapMode::Chars);
}
if wrap_words {
return Some(WrapMode::Words);
}
}
None
}
}
#[cfg(feature = "cli")]
impl From<Args> for CsvlensOptions {
fn from(args: Args) -> Self {
Self {
filename: args.filename,
delimiter: args.delimiter,
tab_separated: args.tab_separated,
comma_separated: args.comma_separated,
no_headers: args.no_headers,
columns: args.columns,
filter: args.filter,
find: args.find,
ignore_case: args.ignore_case,
echo_column: args.echo_column,
debug: args.debug,
freeze_cols_offset: None,
color_columns: args.color_columns,
prompt: args.prompt,
wrap_mode: Args::get_wrap_mode(args.wrap, args.wrap_chars, args.wrap_words),
auto_reload: args.auto_reload,
no_streaming_stdin: args.no_streaming_stdin,
}
}
}
#[derive(Debug, Default)]
pub struct CsvlensOptions {
pub filename: Option<String>,
pub delimiter: Option<String>,
pub tab_separated: bool,
pub comma_separated: bool,
pub no_headers: bool,
pub columns: Option<String>,
pub filter: Option<String>,
pub find: Option<String>,
pub ignore_case: bool,
pub echo_column: Option<String>,
pub debug: bool,
pub freeze_cols_offset: Option<u64>,
pub color_columns: bool,
pub prompt: Option<String>,
pub wrap_mode: Option<WrapMode>,
pub auto_reload: bool,
pub no_streaming_stdin: bool,
}
struct AppRunner {
app: App,
}
impl AppRunner {
fn new(app: App) -> AppRunner {
let original_panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
disable_raw_mode().unwrap();
execute!(std::io::stderr(), LeaveAlternateScreen).unwrap();
original_panic_hook(info);
}));
AppRunner { app }
}
fn run(&mut self) -> CsvlensResult<Option<String>> {
enable_raw_mode()?;
let mut output = std::io::stderr();
execute!(output, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(LineWriter::new(output));
let mut terminal = Terminal::new(backend)?;
self.app.main_loop(&mut terminal)
}
}
impl Drop for AppRunner {
fn drop(&mut self) {
if !panicking() {
disable_raw_mode().unwrap();
execute!(std::io::stderr(), LeaveAlternateScreen).unwrap();
}
}
}
pub fn run_csvlens_with_options(options: CsvlensOptions) -> CsvlensResult<Option<String>> {
let show_stats = options.debug;
let delimiter = Delimiter::from_arg(
&options.delimiter,
options.tab_separated,
options.comma_separated,
)?;
let app = App::new(
options.filename,
delimiter,
show_stats,
options.echo_column,
options.ignore_case,
options.no_headers,
options.columns,
options.filter,
options.find,
options.freeze_cols_offset,
options.color_columns,
options.prompt,
options.wrap_mode,
options.auto_reload,
options.no_streaming_stdin,
)?;
let mut app_runner = AppRunner::new(app);
app_runner.run()
}
#[cfg(feature = "cli")]
pub fn run_csvlens<I, T>(args: I) -> CsvlensResult<Option<String>>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let mut args_items = vec![OsString::from("csvlens")];
for item in args {
args_items.push(item.into());
}
let args = Args::parse_from(args_items);
run_csvlens_with_options(args.into())
}
#[cfg(not(feature = "cli"))]
pub fn run_csvlens<I, T>(_args: I) -> CsvlensResult<Option<String>>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
eprintln!("Error: CLI is not enabled. Compile with the 'cli' feature to use this binary.");
std::process::exit(1);
}