use std::io::{self, IsTerminal, Read, Write};
use std::path::PathBuf;
use std::time::Duration;
use std::{env, fs, panic, process};
use anyhow::{Context, Result};
use crossterm::{
ExecutableCommand,
event::{self, Event},
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::prelude::*;
const POLL_TIMEOUT: Duration = Duration::from_millis(50);
mod app;
mod error;
mod filter;
mod loader;
mod text_buffer;
mod ui;
mod worker;
use app::{App, AppAction};
use loader::Loader;
fn main() {
if let Err(e) = run() {
eprintln!("Error: {e:#}");
process::exit(1);
}
}
fn run() -> Result<()> {
let args: Vec<String> = env::args().skip(1).collect();
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("jarq {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let stdin_is_pipe = !io::stdin().is_terminal();
match args.len() {
0 => {
if stdin_is_pipe {
run_interactive_async(Loader::spawn_stdin())
} else {
eprintln!("Usage: jarq [FILTER] [FILE]");
eprintln!(" cat file.json | jarq [FILTER]");
eprintln!();
eprintln!("If FILTER is provided, runs non-interactively.");
eprintln!("Otherwise, opens an interactive TUI.");
Ok(())
}
}
1 => {
if stdin_is_pipe {
let json_input = read_stdin()?;
let values = parse_json(&json_input)?;
run_noninteractive(&args[0], &values)
} else {
run_interactive_async(Loader::spawn_file(PathBuf::from(&args[0])))
}
}
_ => {
let json_input = fs::read_to_string(&args[1])
.with_context(|| format!("Failed to read file: {}", &args[1]))?;
let values = parse_json(&json_input)?;
run_noninteractive(&args[0], &values)
}
}
}
fn run_noninteractive(filter_text: &str, values: &[serde_json::Value]) -> Result<()> {
let results =
filter::evaluate_all(filter_text, values).map_err(|e| anyhow::anyhow!("{}", e))?;
let stdout = io::stdout();
let mut handle = stdout.lock();
for value in results {
writeln!(handle, "{}", serde_json::to_string_pretty(&value)?)?;
}
Ok(())
}
fn run_interactive_async(loader: Loader) -> Result<()> {
let mut app = App::new_loading(loader);
run_tui_loop(&mut app)
}
#[allow(dead_code)]
fn run_interactive(values: Vec<serde_json::Value>) -> Result<()> {
let mut app = App::new(values);
run_tui_loop(&mut app)
}
fn run_tui_loop(app: &mut App) -> Result<()> {
let mut stdout = io::stdout();
enable_raw_mode().context("Failed to enable raw mode")?;
stdout
.execute(EnterAlternateScreen)
.context("Failed to enter alternate screen")?;
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = disable_raw_mode();
let _ = io::stdout().execute(LeaveAlternateScreen);
original_hook(panic_info);
}));
let mut terminal =
Terminal::new(CrosstermBackend::new(stdout)).context("Failed to create terminal")?;
loop {
terminal.draw(|frame| ui::render(frame, app))?;
if event::poll(POLL_TIMEOUT).context("Failed to poll event")? {
match event::read().context("Failed to read event")? {
Event::Key(key) => match app.handle_key(key) {
AppAction::Quit => break,
AppAction::Continue => {}
},
Event::Resize(_, _) => {}
_ => {}
}
}
app.tick();
}
disable_raw_mode().context("Failed to disable raw mode")?;
io::stdout()
.execute(LeaveAlternateScreen)
.context("Failed to leave alternate screen")?;
Ok(())
}
fn parse_json(input: &str) -> Result<Vec<serde_json::Value>> {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(input) {
return Ok(vec![value]);
}
let mut values = Vec::new();
for (line_num, line) in input.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let value = serde_json::from_str(trimmed)
.with_context(|| format!("Invalid JSON on line {}", line_num + 1))?;
values.push(value);
}
if values.is_empty() {
anyhow::bail!("No valid JSON found in input");
}
Ok(values)
}
fn read_stdin() -> Result<String> {
let mut input = String::new();
io::stdin()
.read_to_string(&mut input)
.context("Failed to read from stdin")?;
if input.is_empty() {
anyhow::bail!("Empty input is not valid JSON");
}
Ok(input)
}