use std::cell::RefCell;
use anyhow::Result;
use arboard::Clipboard;
use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
use promkit::{
crossterm::{
event::Event,
style::{Attribute, Attributes, Color, ContentStyle},
},
jsonstream::{self, JsonStream},
jsonz::{self, format::RowFormatter, Value},
listbox,
pane::Pane,
serde_json,
snapshot::Snapshot,
style::StyleBuilder,
suggest::Suggest,
switch::ActiveKeySwitcher,
text, text_editor, PaneFactory, Prompt, PromptSignal,
};
use crate::trie::FilterTrie;
mod keymap;
fn run_jaq(
query: &str,
json_stream: &[serde_json::Value],
) -> anyhow::Result<Vec<serde_json::Value>> {
let mut ret = Vec::<serde_json::Value>::new();
for input in json_stream {
let mut ctx = ParseCtx::new(Vec::new());
ctx.insert_natives(jaq_core::core());
ctx.insert_defs(jaq_std::std());
let (f, errs) = jaq_parse::parse(query, jaq_parse::main());
if !errs.is_empty() {
let error_message = errs
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow::anyhow!(error_message));
}
let f = ctx.compile(f.unwrap());
let inputs = RcIter::new(core::iter::empty());
let mut out = f.run((Ctx::new([], &inputs), Val::from(input.clone())));
while let Some(Ok(val)) = out.next() {
ret.push(val.into());
}
}
Ok(ret)
}
pub struct JsonTheme {
pub curly_brackets_style: ContentStyle,
pub square_brackets_style: ContentStyle,
pub key_style: ContentStyle,
pub string_value_style: ContentStyle,
pub number_value_style: ContentStyle,
pub boolean_value_style: ContentStyle,
pub null_value_style: ContentStyle,
pub active_item_attribute: Attribute,
pub inactive_item_attribute: Attribute,
pub lines: Option<usize>,
pub indent: usize,
}
pub struct Jnv {
input_stream: Vec<serde_json::Value>,
keymap: RefCell<ActiveKeySwitcher<keymap::Keymap>>,
filter_editor: Snapshot<text_editor::State>,
hint_message: Snapshot<text::State>,
suggestions: listbox::State,
json: jsonstream::State,
trie: FilterTrie,
suggest: Suggest,
no_hint: bool,
}
impl Jnv {
#[allow(clippy::too_many_arguments)]
pub fn try_new(
input_stream: Vec<serde_json::Value>,
filter_editor: text_editor::State,
hint_message: text::State,
suggestions: listbox::State,
json_theme: JsonTheme,
no_hint: bool,
) -> Result<Prompt<Self>> {
let mut trie = FilterTrie::default();
trie.insert(".", input_stream.clone());
let stream = JsonStream::new(&input_stream);
let suggest = Suggest::from_iter(jsonz::get_all_paths(&input_stream));
Ok(Prompt {
renderer: Self {
keymap: RefCell::new(
ActiveKeySwitcher::new("default", self::keymap::default as keymap::Keymap)
.register("on_suggest", self::keymap::on_suggest),
),
filter_editor: Snapshot::<text_editor::State>::new(filter_editor),
hint_message: Snapshot::<text::State>::new(hint_message),
suggestions,
json: jsonstream::State {
stream,
formatter: RowFormatter {
curly_brackets_style: json_theme.curly_brackets_style,
square_brackets_style: json_theme.square_brackets_style,
key_style: json_theme.key_style,
string_value_style: json_theme.string_value_style,
number_value_style: json_theme.number_value_style,
boolean_value_style: json_theme.boolean_value_style,
null_value_style: json_theme.null_value_style,
active_item_attribute: json_theme.active_item_attribute,
inactive_item_attribute: json_theme.inactive_item_attribute,
indent: json_theme.indent,
},
lines: json_theme.lines,
},
trie,
suggest,
no_hint,
input_stream,
},
})
}
fn update_hint_message(&mut self, text: String, style: ContentStyle) {
if !self.no_hint {
self.hint_message
.after_mut()
.replace(text::State { text, style })
}
}
fn store_to_clipboard(&mut self, content: &str, hint: &str) {
match Clipboard::new() {
Ok(mut clipboard) => match clipboard.set_text(content) {
Ok(_) => {
self.update_hint_message(
hint.to_string(),
StyleBuilder::new().fgc(Color::Grey).build(),
);
}
Err(e) => {
self.update_hint_message(
format!("Failed to copy to clipboard: {}", e),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
}
},
Err(e) => {
self.update_hint_message(
format!("Failed to setup clipboard: {}", e),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
}
}
}
fn store_content_to_clipboard(&mut self) {
self.store_to_clipboard(
&self.json.formatter.format_raw_json(self.json.stream.rows()),
"Copied selected content to clipboard!",
);
}
fn store_query_to_clipboard(&mut self) {
self.store_to_clipboard(
&self
.filter_editor
.after()
.texteditor
.text_without_cursor()
.to_string(),
"Copied jq query to clipboard!",
);
}
}
impl promkit::Finalizer for Jnv {
type Return = String;
fn finalize(&mut self) -> anyhow::Result<Self::Return> {
Ok(self
.filter_editor
.after()
.texteditor
.text_without_cursor()
.to_string())
}
}
impl promkit::Renderer for Jnv {
fn create_panes(&self, width: u16, height: u16) -> Vec<Pane> {
vec![
self.filter_editor.create_pane(width, height),
self.hint_message.create_pane(width, height),
self.suggestions.create_pane(width, height),
self.json.create_pane(width, height),
]
}
fn evaluate(&mut self, event: &Event) -> anyhow::Result<PromptSignal> {
let keymap = *self.keymap.borrow_mut().get();
let signal = keymap(event, self);
let filter = self
.filter_editor
.after()
.texteditor
.text_without_cursor()
.to_string();
if filter
!= self
.filter_editor
.borrow_before()
.texteditor
.text_without_cursor()
.to_string()
{
self.hint_message.reset_after_to_init();
match self.trie.exact_search(&filter) {
Some(jsonl) => {
self.json.stream = JsonStream::new(jsonl);
self.update_hint_message(
format!(
"JSON query ('{}') was already executed. Result was retrieved from cache.",
&filter
),
StyleBuilder::new()
.fgc(Color::DarkGrey)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
}
None => {
match run_jaq(&filter, &self.input_stream) {
Ok(ret) => {
if ret.is_empty() {
self.update_hint_message(
format!(
"JSON query ('{}') was executed, but no results were returned.",
&filter
),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = self.trie.prefix_search(&filter) {
self.json.stream = JsonStream::new(searched);
}
} else {
let stream = JsonStream::new(ret.iter());
let is_null =
stream.rows().iter().all(|node| node.v == Value::Null);
if is_null {
self.update_hint_message(
format!("JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'", &filter),
StyleBuilder::new()
.fgc(Color::Yellow)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = self.trie.prefix_search(&filter) {
self.json.stream = JsonStream::new(searched);
}
} else {
self.trie.insert(&filter, ret);
self.json.stream = stream;
}
}
}
Err(_) => {
self.update_hint_message(
format!("Failed to execute jq query '{}'", &filter),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = self.trie.prefix_search(&filter) {
self.json.stream = JsonStream::new(searched);
}
return signal;
}
}
}
}
}
signal
}
}