use std::{
fs::File,
io::{self, Read, Write},
path::PathBuf,
};
use anyhow::anyhow;
use clap::Parser;
use config::Config;
use promkit_widgets::{
listbox::{self, Listbox},
text_editor::{self, TextEditor},
};
mod editor;
use editor::Editor;
mod config;
mod json;
use json::JsonStreamProvider;
mod stdout_redirect;
use stdout_redirect::StdoutRedirect;
mod processor;
use processor::{
init::ViewInitializer, monitor::ContextMonitor, Context, Processor, ViewProvider, Visualizer,
};
mod prompt;
mod search;
use search::{IncrementalSearcher, SearchProvider};
use crate::config::DEFAULT_CONFIG;
#[derive(Parser)]
#[command(
name = "jnv",
version,
help_template = "
{about}
Usage: {usage}
Examples:
- Read from a file:
{bin} data.json
- Read from standard input:
cat data.json | {bin}
Arguments:
{positionals}
Options:
{options}
"
)]
pub struct Args {
pub input: Option<PathBuf>,
#[arg(short = 'c', long = "config", help = "Path to the configuration file.")]
pub config_file: Option<PathBuf>,
#[arg(
long = "default-filter",
help = "Default jq filter to apply to the input data",
long_help = "
Sets the default jq filter to apply to the input data.
The filter is applied when the interface is first loaded.
"
)]
default_filter: Option<String>,
#[arg(
long = "write-to-stdout",
help = "Write the current JSON result to stdout when exiting"
)]
write_to_stdout: bool,
}
fn parse_input(args: &Args) -> anyhow::Result<String> {
let mut ret = String::new();
match &args.input {
None => {
io::stdin().read_to_string(&mut ret)?;
}
Some(path) => {
if path == &PathBuf::from("-") {
io::stdin().read_to_string(&mut ret)?;
} else {
File::open(path)?.read_to_string(&mut ret)?;
}
}
}
Ok(ret)
}
fn ensure_file_exists(path: &PathBuf) -> anyhow::Result<()> {
if path.exists() {
return Ok(());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| anyhow!("Failed to create directory: {e}"))?;
}
std::fs::File::create(path)?.write_all(DEFAULT_CONFIG.as_bytes())?;
Ok(())
}
fn determine_config_file(config_path: Option<PathBuf>) -> anyhow::Result<PathBuf> {
if let Some(path) = config_path {
ensure_file_exists(&path)?;
return Ok(path);
}
let default_path = dirs::config_dir()
.ok_or_else(|| anyhow!("Failed to determine the configuration directory"))?
.join("jnv")
.join("config.toml");
ensure_file_exists(&default_path)?;
Ok(default_path)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let input = parse_input(&args)?;
let config = determine_config_file(args.config_file)
.and_then(|config_file| {
std::fs::read_to_string(&config_file)
.map_err(|e| anyhow!("Failed to read configuration file: {e}"))
})
.and_then(|content| Config::load_from(&content))
.unwrap_or_else(|_e| {
Config::load_from(DEFAULT_CONFIG).expect("Failed to load default configuration")
});
let listbox_state = listbox::State {
listbox: Listbox::default(),
config: config.completion.listbox.clone(),
};
let searcher =
IncrementalSearcher::new(listbox_state, config.completion.search_result_chunk_size);
let text_editor_state = text_editor::State {
texteditor: if let Some(filter) = args.default_filter {
TextEditor::new(filter)
} else {
Default::default()
},
history: Default::default(),
config: config.editor.on_focus.clone(),
};
let provider =
&mut JsonStreamProvider::new(config.json.stream.clone(), config.json.max_streams);
let item = Box::leak(input.into_boxed_str());
let loading_suggestions_task =
searcher.spawn_load_task(provider, item, config.completion.search_load_chunk_size);
let editor = Editor::new(
text_editor_state,
searcher,
config.editor.on_focus,
config.editor.on_defocus,
config.keybinds.on_editor.clone(),
);
let mut stdout_redirect = StdoutRedirect::try_new_for_tui(args.write_to_stdout)?;
let maybe_output = prompt::run(
item,
config.reactivity_control,
provider,
editor,
loading_suggestions_task,
config.no_hint,
config.keybinds,
args.write_to_stdout,
)
.await;
stdout_redirect.restore()?;
let maybe_output = maybe_output?;
if let Some(output) = maybe_output {
let mut stdout = io::stdout();
stdout.write_all(output.as_bytes())?;
if !output.ends_with('\n') {
stdout.write_all(b"\n")?;
}
}
Ok(())
}