use std::{cell::RefCell, rc::Rc};
use anyhow::Result;
use gag::Gag;
use promkit::{
crossterm::{
event::Event,
style::{Attribute, Attributes, Color, ContentStyle},
},
json::{self, JsonNode, JsonPathSegment, JsonStream},
keymap::KeymapManager,
listbox,
serde_json::{self, Deserializer},
snapshot::Snapshot,
style::StyleBuilder,
suggest::Suggest,
text, text_editor, Prompt, PromptSignal, Renderer,
};
mod keymap;
mod render;
mod trie;
use trie::QueryTrie;
pub struct Jnv {
input_json_stream: Vec<serde_json::Value>,
expand_depth: Option<usize>,
no_hint: bool,
query_editor_renderer: text_editor::Renderer,
hint_message_renderer: text::Renderer,
suggest: Suggest,
suggest_renderer: listbox::Renderer,
json_renderer: json::Renderer,
keymap: KeymapManager<self::render::Renderer>,
}
impl Jnv {
pub fn try_new(
input_json_str: String,
expand_depth: Option<usize>,
no_hint: bool,
edit_mode: text_editor::Mode,
indent: usize,
suggestion_list_length: usize,
) -> Result<Self> {
let stream = deserialize_json(&input_json_str)?;
let all_kinds = JsonStream::new(stream.clone(), None).flatten_kinds();
let suggestions = all_kinds
.iter()
.filter_map(|kind| kind.path())
.map(|segments| {
if segments.is_empty() {
".".to_string()
} else {
segments
.iter()
.map(|segment| match segment {
JsonPathSegment::Key(key) => {
if key.contains('.') || key.contains('-') || key.contains('@') {
format!(".\"{}\"", key)
} else {
format!(".{}", key)
}
}
JsonPathSegment::Index(index) => format!("[{}]", index),
})
.collect::<String>()
}
});
Ok(Self {
input_json_stream: stream.clone(),
expand_depth,
no_hint,
query_editor_renderer: text_editor::Renderer {
texteditor: Default::default(),
history: Default::default(),
prefix: String::from("❯❯ "),
mask: Default::default(),
prefix_style: StyleBuilder::new().fgc(Color::Blue).build(),
active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(),
inactive_char_style: StyleBuilder::new().build(),
edit_mode,
lines: Default::default(),
},
hint_message_renderer: text::Renderer {
text: Default::default(),
style: StyleBuilder::new()
.fgc(Color::Green)
.attrs(Attributes::from(Attribute::Bold))
.build(),
},
suggest: Suggest::from_iter(suggestions),
suggest_renderer: listbox::Renderer {
listbox: listbox::Listbox::from_iter(Vec::<String>::new()),
cursor: String::from("❯ "),
active_item_style: StyleBuilder::new()
.fgc(Color::Grey)
.bgc(Color::Yellow)
.build(),
inactive_item_style: StyleBuilder::new().fgc(Color::Grey).build(),
lines: Some(suggestion_list_length),
},
keymap: KeymapManager::new("default", self::keymap::default)
.register("on_suggest", self::keymap::on_suggest),
json_renderer: json::Renderer {
stream: JsonStream::new(stream, expand_depth),
theme: json::Theme {
curly_brackets_style: StyleBuilder::new()
.attrs(Attributes::from(Attribute::Bold))
.build(),
square_brackets_style: StyleBuilder::new()
.attrs(Attributes::from(Attribute::Bold))
.build(),
key_style: StyleBuilder::new().fgc(Color::Cyan).build(),
string_value_style: StyleBuilder::new().fgc(Color::Green).build(),
number_value_style: StyleBuilder::new().build(),
boolean_value_style: StyleBuilder::new().build(),
null_value_style: StyleBuilder::new().fgc(Color::Grey).build(),
active_item_attribute: Attribute::Bold,
inactive_item_attribute: Attribute::Dim,
lines: Default::default(),
indent,
},
},
})
}
fn update_hint_message(
&self,
renderer: &mut self::render::Renderer,
text: String,
style: ContentStyle,
) {
if !self.no_hint {
renderer
.hint_message_snapshot
.after_mut()
.replace(text::Renderer { text, style })
}
}
fn evaluate(
&self,
event: &Event,
renderer: &mut Box<dyn Renderer + 'static>,
trie: RefCell<QueryTrie>,
) -> promkit::Result<PromptSignal> {
let renderer = self::render::Renderer::cast_mut(renderer.as_mut())?;
let signal = match renderer.keymap.get() {
Some(f) => f(event, renderer),
None => Ok(PromptSignal::Quit),
}?;
let completed = renderer
.query_editor_snapshot
.after()
.texteditor
.text_without_cursor()
.to_string();
if completed
!= renderer
.query_editor_snapshot
.borrow_before()
.texteditor
.text_without_cursor()
.to_string()
{
renderer.hint_message_snapshot.reset_after_to_init();
let ignore_err = Gag::stderr().unwrap();
let mut flatten_ret = Vec::<String>::new();
for v in &self.input_json_stream {
let inner_ret: Vec<String> = match j9::run(&completed, &v.to_string()) {
Ok(ret) => ret,
Err(_e) => {
self.update_hint_message(
renderer,
format!("Failed to execute jq query '{}'", &completed),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
renderer.json_snapshot.after_mut().stream =
JsonStream::new(searched.clone(), self.expand_depth);
}
return Ok(signal);
}
};
flatten_ret.extend(inner_ret);
}
drop(ignore_err);
if flatten_ret.is_empty() {
self.update_hint_message(
renderer,
format!(
"JSON query ('{}') was executed, but no results were returned.",
&completed
),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
renderer.json_snapshot.after_mut().stream =
JsonStream::new(searched.clone(), self.expand_depth);
}
} else {
match deserialize_json(&flatten_ret.join("\n")) {
Ok(jsonl) => {
let stream = JsonStream::new(jsonl.clone(), self.expand_depth);
let is_null = stream
.roots()
.iter()
.all(|node| node == &JsonNode::Leaf(serde_json::Value::Null));
if is_null {
self.update_hint_message(
renderer,
format!("JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'", &completed),
StyleBuilder::new()
.fgc(Color::Yellow)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
renderer.json_snapshot.after_mut().stream =
JsonStream::new(searched.clone(), self.expand_depth);
}
} else {
trie.borrow_mut().insert(&completed, jsonl);
renderer.json_snapshot.after_mut().stream = stream;
}
}
Err(e) => {
self.update_hint_message(
renderer,
format!("Failed to parse query result for viewing: {}", e),
StyleBuilder::new()
.fgc(Color::Red)
.attrs(Attributes::from(Attribute::Bold))
.build(),
);
if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
renderer.json_snapshot.after_mut().stream =
JsonStream::new(searched.clone(), self.expand_depth);
}
}
}
}
}
Ok(signal)
}
pub fn prompt(self) -> Result<Prompt<String>> {
let rc_self = Rc::new(RefCell::new(self));
let rc_self_clone = rc_self.clone();
let keymap_clone = rc_self_clone.borrow().keymap.clone();
let query_editor_renderer_clone = rc_self_clone.borrow().query_editor_renderer.clone();
let hint_message_renderer_clone = rc_self_clone.borrow().hint_message_renderer.clone();
let suggest_clone = rc_self_clone.borrow().suggest.clone();
let suggest_renderer_clone = rc_self_clone.borrow().suggest_renderer.clone();
let json_renderer_clone = rc_self_clone.borrow().json_renderer.clone();
Ok(Prompt::try_new(
Box::new(self::render::Renderer {
keymap: keymap_clone,
query_editor_snapshot: Snapshot::<text_editor::Renderer>::new(
query_editor_renderer_clone,
),
hint_message_snapshot: Snapshot::<text::Renderer>::new(hint_message_renderer_clone),
suggest: suggest_clone,
suggest_snapshot: Snapshot::<listbox::Renderer>::new(suggest_renderer_clone),
json_snapshot: Snapshot::<json::Renderer>::new(json_renderer_clone),
}),
Box::new(
move |event: &Event,
renderer: &mut Box<dyn Renderer + 'static>|
-> promkit::Result<PromptSignal> {
let trie = RefCell::new(QueryTrie::default());
rc_self_clone.borrow().evaluate(event, renderer, trie)
},
),
|renderer: &(dyn Renderer + '_)| -> promkit::Result<String> {
Ok(self::render::Renderer::cast(renderer)?
.query_editor_snapshot
.after()
.texteditor
.text_without_cursor()
.to_string())
},
)?)
}
}
fn deserialize_json(json_str: &str) -> Result<Vec<serde_json::Value>> {
Deserializer::from_str(json_str)
.into_iter::<serde_json::Value>()
.map(|res| res.map_err(anyhow::Error::new))
.collect::<Result<Vec<serde_json::Value>>>()
}