mod app;
mod clipboard;
#[cfg(feature = "disasm")]
mod disasm;
mod export;
mod formats;
mod mru;
mod rkyv_inspect;
mod sqlite;
mod store;
use anyhow::{anyhow, Result};
use clap::Parser;
use ratatui::DefaultTerminal;
use std::path::PathBuf;
use store::Store;
#[derive(Parser)]
#[command(
name = "zdbview",
version,
about = "Terminal inspector and CRUD editor for rkyv archives and SQLite databases"
)]
struct Cli {
file: Option<PathBuf>,
#[arg(long, conflicts_with = "rkyv")]
sqlite: bool,
#[arg(long)]
rkyv: bool,
#[arg(long, value_name = "FORMAT")]
export: Option<String>,
}
fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(fmt) = &cli.export {
return run_export(&cli, fmt);
}
let mut terminal = ratatui::init();
let res = run(&cli, &mut terminal);
ratatui::restore();
res
}
fn run_export(cli: &Cli, fmt: &str) -> Result<()> {
let fmt = fmt.to_lowercase();
if fmt != "json" && fmt != "csv" {
return Err(anyhow!("--export expects 'json' or 'csv', got '{}'", fmt));
}
let file = cli
.file
.clone()
.ok_or_else(|| anyhow!("--export requires a file argument"))?;
let kind = store::detect(&file, cli.sqlite, cli.rkyv)?;
let (store, _) = store::Store::open(&file, kind)?;
print!("{}", export_store(&store, &fmt)?);
Ok(())
}
fn export_store(store: &Store, fmt: &str) -> Result<String> {
match store {
Store::Sqlite(s) => {
if fmt == "csv" {
let table = s
.tables
.first()
.cloned()
.ok_or_else(|| anyhow!("no tables to export"))?;
let total = s.count(&table)?;
let v = s.rows(&table, total.max(1), 0)?;
Ok(export::rows_to_csv(&v.columns, &v.rows))
} else {
let mut out = String::from("{");
for (i, t) in s.tables.iter().enumerate() {
if i > 0 {
out.push(',');
}
let total = s.count(t)?;
let v = s.rows(t, total.max(1), 0)?;
out.push_str(&export::json_escape(t));
out.push(':');
out.push_str(&export::rows_to_json(&v.columns, &v.rows));
}
out.push('}');
out.push('\n');
Ok(out)
}
}
Store::Rkyv(r) => {
let d = formats::try_decode(&r.bytes)
.ok_or_else(|| anyhow!("unrecognized rkyv archive — nothing to export"))?;
let recs: Vec<export::RecordExport> = d
.records
.iter()
.map(|rec| export::RecordExport {
key: &rec.key,
fields: &rec.fields,
value: &rec.value,
})
.collect();
let mut out = export::records_to_json(&recs);
out.push('\n');
Ok(out)
}
}
}
fn run(cli: &Cli, terminal: &mut DefaultTerminal) -> Result<()> {
let file = match &cli.file {
Some(f) => f.clone(),
None => {
let recent: Vec<mru::Entry> = mru::load()
.into_iter()
.filter(|e| e.path.exists())
.collect();
match app::pick_mru(terminal, &recent)? {
Some(p) => p,
None => return Ok(()), }
}
};
let kind = store::detect(&file, cli.sqlite, cli.rkyv)?;
let (store, actual) = store::Store::open(&file, kind)?;
mru::record(&file, actual);
app::App::new(store).run(terminal)
}