use clap::{Parser, ValueEnum};
use csvmd::error::Result;
use csvmd::{csv_to_markdown_streaming, Config, HeaderAlignment};
use std::fs::File;
use std::io::{self, IsTerminal, Read};
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::thread;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, ValueEnum)]
enum ClapAlignment {
Left,
Center,
Centre,
Right,
}
impl From<ClapAlignment> for HeaderAlignment {
fn from(clap_align: ClapAlignment) -> Self {
match clap_align {
ClapAlignment::Left => HeaderAlignment::Left,
ClapAlignment::Center | ClapAlignment::Centre => HeaderAlignment::Center,
ClapAlignment::Right => HeaderAlignment::Right,
}
}
}
#[derive(Parser)]
#[command(name = "csvmd")]
#[command(about = "Convert a CSV to a Markdown table, outputted to stdout")]
#[command(version)]
struct Args {
file: Option<PathBuf>,
#[arg(short, long, default_value = ",")]
delimiter: char,
#[arg(long)]
no_headers: bool,
#[arg(long)]
stream: bool,
#[arg(long, default_value = "left")]
align: ClapAlignment,
}
struct InteractiveStdin {
buffer: Vec<u8>,
position: usize,
initialized: bool,
}
impl InteractiveStdin {
fn new() -> Self {
Self {
buffer: Vec::new(),
position: 0,
initialized: false,
}
}
fn initialize_if_needed(&mut self) -> io::Result<()> {
if self.initialized {
return Ok(());
}
self.initialized = true;
if !std::io::stdin().is_terminal() {
io::stdin().read_to_end(&mut self.buffer)?;
return Ok(());
}
let (tx, rx) = mpsc::channel();
let tx_for_thread = tx.clone();
thread::spawn(move || {
let mut stdin_buffer = Vec::new();
match io::stdin().read_to_end(&mut stdin_buffer) {
Ok(_) => {
let _ = tx_for_thread.send(Ok(stdin_buffer));
}
Err(e) => {
let _ = tx_for_thread.send(Err(e));
}
}
});
thread::sleep(Duration::from_secs(2));
match rx.try_recv() {
Ok(Ok(data)) => {
self.buffer = data;
return Ok(());
}
Ok(Err(e)) => {
return Err(e);
}
Err(TryRecvError::Empty) => {
self.show_spinner_and_wait(rx)?;
}
Err(TryRecvError::Disconnected) => {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Input thread disconnected",
));
}
}
Ok(())
}
fn show_spinner_and_wait(
&mut self,
rx: Receiver<std::result::Result<Vec<u8>, io::Error>>,
) -> io::Result<()> {
let _start_time = Instant::now();
let mut message_shown = false;
loop {
if !message_shown {
eprint!("Waiting for input via stdin... (To read from a file, use `csvmd path/to/file.csv`.)");
message_shown = true;
}
match rx.try_recv() {
Ok(Ok(data)) => {
eprint!("\r{}\r", " ".repeat(85));
self.buffer = data;
return Ok(());
}
Ok(Err(e)) => {
eprint!("\r{}\r", " ".repeat(85));
return Err(e);
}
Err(TryRecvError::Empty) => {
thread::sleep(Duration::from_millis(100));
}
Err(TryRecvError::Disconnected) => {
eprint!("\r{}\r", " ".repeat(85));
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Input thread disconnected",
));
}
}
}
}
}
impl Read for InteractiveStdin {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.initialize_if_needed()?;
let remaining = self.buffer.len() - self.position;
if remaining == 0 {
return Ok(0); }
let to_copy = buf.len().min(remaining);
buf[..to_copy].copy_from_slice(&self.buffer[self.position..self.position + to_copy]);
self.position += to_copy;
Ok(to_copy)
}
}
fn main() -> Result<()> {
let args = Args::parse();
let config = Config {
has_headers: !args.no_headers,
flexible: true,
delimiter: args.delimiter as u8,
header_alignment: args.align.into(),
};
if args.stream {
match args.file {
Some(path) => {
let file = File::open(path)?;
csvmd::csv_to_markdown_streaming_seekable(file, io::stdout(), config)?;
}
None => {
let input: Box<dyn Read> = Box::new(InteractiveStdin::new());
csv_to_markdown_streaming(input, io::stdout(), config)?;
}
}
} else {
let input: Box<dyn Read> = match args.file {
Some(path) => Box::new(File::open(path)?),
None => Box::new(InteractiveStdin::new()),
};
let output = csvmd::csv_to_markdown(input, config)?;
print!("{}", output);
}
Ok(())
}