use std::io::{self, IsTerminal, Read, Write};
use std::path::PathBuf;
use std::time::Duration;
use std::{fs, panic, process};
use clap::{CommandFactory, Parser};
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
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 json;
mod loader;
mod text_buffer;
mod ui;
mod worker;
use app::{App, AppAction};
use loader::Loader;
#[derive(Parser)]
#[command(version, about)]
struct Args {
#[arg(short = 'c')]
compact: bool,
#[arg(short = 'r')]
raw: bool,
#[arg(short = 's')]
slurp: bool,
filter: Option<String>,
file: Option<PathBuf>,
}
fn main() {
if let Err(e) = run() {
eprintln!("Error: {e:#}");
process::exit(1);
}
}
fn run() -> Result<()> {
let args = Args::parse();
let stdin_is_pipe = !io::stdin().is_terminal();
let has_flags = args.compact || args.raw || args.slurp;
match (&args.filter, &args.file) {
(None, None) => {
if stdin_is_pipe {
if has_flags {
let json_input = read_stdin()?;
let values = parse_json(&json_input, args.slurp)?;
run_noninteractive(".", &values, &args)
} else {
run_interactive_async(Loader::spawn_stdin())
}
} else {
Args::command().print_help()?;
Ok(())
}
}
(Some(filter), None) => {
if stdin_is_pipe {
let json_input = read_stdin()?;
let values = parse_json(&json_input, args.slurp)?;
run_noninteractive(filter, &values, &args)
} else {
run_interactive_async(Loader::spawn_file(PathBuf::from(filter)))
}
}
(None, Some(file)) => {
if has_flags {
let json_input = fs::read_to_string(file)
.with_context(|| format!("Failed to read: {:?}", file))?;
let values = parse_json(&json_input, args.slurp)?;
run_noninteractive(".", &values, &args)
} else {
run_interactive_async(Loader::spawn_file(file.clone()))
}
}
(Some(filter), Some(file)) => {
let json_input =
fs::read_to_string(file).with_context(|| format!("Failed to read: {:?}", file))?;
let values = parse_json(&json_input, args.slurp)?;
run_noninteractive(filter, &values, &args)
}
}
}
fn run_noninteractive(
filter_text: &str,
values: &[simd_json::OwnedValue],
args: &Args,
) -> Result<()> {
let results =
filter::evaluate_all(filter_text, values).map_err(|e| anyhow::anyhow!("{}", e))?;
let stdout = io::stdout();
let use_color = stdout.is_terminal() && !args.compact && !args.raw;
let mut handle = stdout.lock();
for value in results {
if args.raw {
if let Some(s) = json::to_string_raw(&value) {
writeln!(handle, "{}", s)?;
} else if args.compact {
writeln!(handle, "{}", json::to_string_compact(&value))?;
} else {
writeln!(handle, "{}", json::to_string_pretty(&value))?;
}
} else if args.compact {
writeln!(handle, "{}", json::to_string_compact(&value))?;
} else if use_color {
writeln!(handle, "{}", json::to_string_pretty_colored(&value))?;
} else {
writeln!(handle, "{}", 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<simd_json::OwnedValue>) -> 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, slurp: bool) -> Result<Vec<simd_json::OwnedValue>> {
let mut input_bytes = input.as_bytes().to_vec();
if let Ok(value) = simd_json::to_owned_value(&mut input_bytes) {
if slurp {
return Ok(vec![simd_json::OwnedValue::Array(Box::new(vec![value]))]);
}
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 mut line_bytes = trimmed.as_bytes().to_vec();
let value = simd_json::to_owned_value(&mut line_bytes)
.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");
}
if slurp {
return Ok(vec![simd_json::OwnedValue::Array(Box::new(values))]);
}
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)
}