use std::sync::Arc;
use promkit_widgets::{
core::{crossterm::event::Event, grapheme::StyledGraphemes, render::SharedRenderer, Widget},
jsonstream::{self, JsonStream},
serde_json::{self, Value},
};
use tokio::{
sync::{mpsc, Mutex},
task::JoinHandle,
};
use crate::{
config::{JsonConfig, JsonViewerKeybinds},
context::{Index, SharedContext, State},
guide::{self, GuideAction, GuideMessage},
json,
};
pub enum RenderTrigger {
UserAction(Event),
QueryChanged { query: String },
AreaResized { query: String },
}
pub struct JsonViewer {
state: jsonstream::State,
json: Vec<serde_json::Value>,
keybinds: JsonViewerKeybinds,
}
pub type SharedJsonViewer = Arc<Mutex<JsonViewer>>;
impl JsonViewer {
pub fn formatted_content(&self) -> String {
self.state.config.format_raw_json(self.state.stream.rows())
}
fn handle_user_event(&mut self, event: &Event) {
match event {
event if self.keybinds.up.contains(event) => {
self.state.stream.up();
}
event if self.keybinds.down.contains(event) => {
self.state.stream.down();
}
event if self.keybinds.move_to_head.contains(event) => {
self.state.stream.head();
}
event if self.keybinds.move_to_tail.contains(event) => {
self.state.stream.tail();
}
event if self.keybinds.toggle.contains(event) => {
self.state.stream.toggle();
}
event if self.keybinds.expand.contains(event) => {
self.state.stream.set_nodes_visibility(false);
}
event if self.keybinds.collapse.contains(event) => {
self.state.stream.set_nodes_visibility(true);
}
_ => (),
}
}
async fn refresh_view_with_query(
&mut self,
area: (u16, u16),
input: String,
) -> (Option<GuideMessage>, Option<StyledGraphemes>) {
match json::run_jaq(&input, &self.json) {
Ok(ret) => {
let mut guide = None;
if ret.iter().all(|val| *val == Value::Null) {
guide = Some(GuideMessage::JqReturnedNull(input));
self.state.stream = JsonStream::new(self.json.iter());
} else {
self.state.stream = JsonStream::new(ret.iter());
}
(guide, Some(self.state.create_graphemes(area.0, area.1)))
}
Err(e) => {
self.state.stream = JsonStream::new(self.json.iter());
(
Some(GuideMessage::JqFailed(e.to_string())),
Some(self.state.create_graphemes(area.0, area.1)),
)
}
}
}
}
pub async fn initialize(
input: &'static str,
config: JsonConfig,
keybinds: JsonViewerKeybinds,
shared_ctx: SharedContext,
shared_renderer: SharedRenderer<Index>,
) -> anyhow::Result<SharedJsonViewer> {
{
let mut ctx = shared_ctx.lock().await;
if let Some(task) = ctx.current_task.take() {
task.abort();
}
ctx.state = State::Loading;
}
let input_stream = json::deserialize(input, config.max_streams)?;
let stream = JsonStream::new(input_stream.iter());
let state = jsonstream::State {
stream,
config: config.stream,
};
{
let mut ctx = shared_ctx.lock().await;
ctx.state = State::Idle;
}
{
let ctx = shared_ctx.lock().await;
let area = ctx.area;
drop(ctx);
let _ = shared_renderer
.update([(Index::JsonViewer, state.create_graphemes(area.0, area.1))])
.render()
.await;
}
Ok(Arc::new(Mutex::new(JsonViewer {
json: input_stream,
state,
keybinds,
})))
}
pub async fn render(
trigger: RenderTrigger,
shared_ctx: SharedContext,
shared_viewer_state: SharedJsonViewer,
shared_renderer: SharedRenderer<Index>,
guide_action_tx: mpsc::Sender<GuideAction>,
) {
match trigger {
RenderTrigger::UserAction(event) => {
handle_user_event(shared_viewer_state, shared_renderer, shared_ctx, event).await;
}
RenderTrigger::QueryChanged { query } => {
handle_query_changed(
shared_viewer_state,
shared_renderer,
shared_ctx,
guide_action_tx,
query,
)
.await;
}
RenderTrigger::AreaResized { query } => {
handle_area_resized(
shared_viewer_state,
shared_renderer,
shared_ctx,
guide_action_tx,
query,
)
.await;
}
}
}
async fn handle_user_event(
shared_viewer_state: SharedJsonViewer,
shared_renderer: SharedRenderer<Index>,
shared_ctx: SharedContext,
event: Event,
) {
let area = {
let ctx = shared_ctx.lock().await;
ctx.area
};
let graphemes = {
let mut viewer = shared_viewer_state.lock().await;
viewer.handle_user_event(&event);
viewer.state.create_graphemes(area.0, area.1)
};
let _ = shared_renderer
.update([(Index::JsonViewer, graphemes)])
.render()
.await;
}
async fn handle_query_changed(
shared_viewer_state: SharedJsonViewer,
shared_renderer: SharedRenderer<Index>,
shared_ctx: SharedContext,
guide_action_tx: mpsc::Sender<GuideAction>,
query: String,
) {
{
let mut ctx = shared_ctx.lock().await;
if let Some(task) = ctx.current_task.take() {
task.abort();
}
}
let task = spawn_query_update_task(
shared_viewer_state.clone(),
shared_ctx.clone(),
guide_action_tx,
shared_renderer,
query,
);
{
let mut ctx = shared_ctx.lock().await;
ctx.current_task = Some(task);
}
}
async fn handle_area_resized(
shared_viewer_state: SharedJsonViewer,
shared_renderer: SharedRenderer<Index>,
shared_ctx: SharedContext,
guide_action_tx: mpsc::Sender<GuideAction>,
query: String,
) {
{
let mut ctx = shared_ctx.lock().await;
if let Some(task) = ctx.current_task.take() {
task.abort();
}
}
let task = spawn_query_update_task(
shared_viewer_state.clone(),
shared_ctx.clone(),
guide_action_tx,
shared_renderer,
query,
);
{
let mut ctx = shared_ctx.lock().await;
ctx.current_task = Some(task);
}
}
fn spawn_query_update_task(
shared_viewer_state: SharedJsonViewer,
shared_ctx: SharedContext,
guide_action_tx: mpsc::Sender<GuideAction>,
shared_renderer: SharedRenderer<Index>,
query: String,
) -> JoinHandle<()> {
tokio::spawn(async move {
{
let mut ctx = shared_ctx.lock().await;
ctx.state = State::Processing;
}
let (maybe_guide, maybe_resp) = {
let ctx = shared_ctx.lock().await;
let area = ctx.area;
drop(ctx);
let mut runtime = shared_viewer_state.lock().await;
runtime.refresh_view_with_query(area, query).await
};
{
let mut ctx = shared_ctx.lock().await;
ctx.state = State::Idle;
}
if let Some(message) = maybe_guide {
let _ = guide_action_tx.send(GuideAction::Show(message)).await;
}
let _ = shared_renderer
.update([(
Index::JsonViewer,
maybe_resp.unwrap_or(StyledGraphemes::default()),
)])
.render()
.await;
})
}
pub enum ViewerAction {
CopyResult,
UserEvent(Event),
QueryChanged(String),
}
pub fn start_viewer_task(
mut action_rx: mpsc::Receiver<ViewerAction>,
shared_ctx: SharedContext,
shared_viewer_state: SharedJsonViewer,
shared_renderer: SharedRenderer<Index>,
guide_action_tx: mpsc::Sender<GuideAction>,
) -> JoinHandle<anyhow::Result<()>> {
tokio::spawn(async move {
loop {
tokio::select! {
Some(action) = action_rx.recv() => {
match action {
ViewerAction::CopyResult => {
let runtime = shared_viewer_state.lock().await;
let message = guide::copy_to_clipboard_message(&runtime.formatted_content());
let _ = guide_action_tx.send(GuideAction::Show(message)).await;
}
ViewerAction::UserEvent(event) => {
render(
RenderTrigger::UserAction(event),
shared_ctx.clone(),
shared_viewer_state.clone(),
shared_renderer.clone(),
guide_action_tx.clone(),
)
.await;
}
ViewerAction::QueryChanged(query) => {
render(
RenderTrigger::QueryChanged { query },
shared_ctx.clone(),
shared_viewer_state.clone(),
shared_renderer.clone(),
guide_action_tx.clone(),
)
.await;
}
}
}
else => break,
}
}
Ok(())
})
}