mod display;
mod eval;
mod help;
mod partial_eval;
mod viewport;
use std::sync::Arc;
use std::time::{Duration, Instant};
use arboard::Clipboard;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use simd_json::OwnedValue as Value;
use unicode_width::UnicodeWidthStr;
use crate::error::FilterError;
use crate::filter;
use crate::loader::Loader;
use crate::text_buffer::TextBuffer;
use crate::ui::{self, RenderOptions};
use crate::worker::{EvalRequest, Worker};
use display::DisplayOptions;
use eval::EvalTracker;
use help::HelpState;
use viewport::Viewport;
const DEBOUNCE_DURATION: Duration = Duration::from_millis(250);
pub enum AppAction {
Continue,
Quit,
}
pub enum LoadingState {
Loading,
Ready,
Failed(String),
}
pub struct App {
input_values: Option<Arc<Vec<Value>>>,
loading_state: LoadingState,
loader: Option<Loader>,
filter_buffer: TextBuffer,
filter_error: Option<FilterError>,
pending_filter_update: Option<Instant>,
partial_eval_mode: bool,
output_values: Vec<Value>,
total_lines: usize,
editing_filter: bool,
eval_paused: bool,
worker: Worker,
help: HelpState,
eval: EvalTracker,
viewport: Viewport,
display: DisplayOptions,
}
impl App {
pub fn new(input_values: Vec<Value>) -> Self {
let input_values = Arc::new(input_values);
let worker = Worker::spawn();
let filter_buffer = TextBuffer::new(".");
let filter_result = filter::evaluate_all(filter_buffer.text(), &input_values);
let (output_values, filter_error) = match filter_result {
Ok(values) => (values, None),
Err(e) => (vec![], Some(e)),
};
let options = RenderOptions::default();
let total_lines = ui::count_total_lines(&output_values, &options);
Self {
input_values: Some(input_values),
loading_state: LoadingState::Ready,
loader: None,
filter_buffer,
filter_error,
pending_filter_update: None,
partial_eval_mode: false,
output_values,
total_lines,
editing_filter: false,
eval_paused: false,
worker,
help: HelpState::new(),
eval: EvalTracker::new(),
viewport: Viewport::new(),
display: DisplayOptions::new(),
}
}
pub fn new_loading(loader: Loader) -> Self {
Self {
input_values: None,
loading_state: LoadingState::Loading,
loader: Some(loader),
filter_buffer: TextBuffer::new("."),
filter_error: None,
pending_filter_update: None,
partial_eval_mode: false,
output_values: vec![],
total_lines: 0,
editing_filter: false,
eval_paused: false,
worker: Worker::spawn(),
help: HelpState::new(),
eval: EvalTracker::new(),
viewport: Viewport::new(),
display: DisplayOptions::new(),
}
}
pub fn input_count(&self) -> usize {
self.input_values.as_ref().map(|v| v.len()).unwrap_or(0)
}
pub fn loading_state(&self) -> &LoadingState {
&self.loading_state
}
pub fn render_options(&self) -> RenderOptions {
self.display.render_options()
}
pub fn output_values(&self) -> &[Value] {
&self.output_values
}
fn recompute_total_lines(&mut self) {
let options = self.render_options();
self.total_lines = ui::count_total_lines(&self.output_values, &options);
}
fn mark_filter_dirty(&mut self) {
self.pending_filter_update = Some(Instant::now());
}
fn request_eval(&mut self) {
let Some(ref input_values) = self.input_values else {
return;
};
let id = self.eval.next_id();
let filter_text = if self.partial_eval_mode {
let byte_pos = self.filter_buffer.cursor_byte_pos();
let result =
partial_eval::extract_partial_eval_text(self.filter_buffer.text(), byte_pos);
result.eval_text
} else {
self.filter_buffer.text().to_string()
};
self.worker.send(EvalRequest {
id,
filter_text,
inputs: Arc::clone(input_values),
slurp_mode: self.display.slurp,
});
self.eval.start(id);
self.pending_filter_update = None;
}
pub fn tick(&mut self) {
let load_result = self.loader.as_ref().and_then(|l| l.try_recv());
if let Some(result) = load_result {
self.loader = None;
match result.result {
Ok(values) => {
self.input_values = Some(Arc::new(values));
self.loading_state = LoadingState::Ready;
self.request_eval(); }
Err(e) => {
self.loading_state = LoadingState::Failed(e);
}
}
}
while let Some(result) = self.worker.try_recv() {
if self.eval.complete(result.id) {
match result.result {
Ok(values) => {
self.output_values = values;
self.filter_error = None;
self.recompute_total_lines();
self.viewport.reset_scroll();
}
Err(e) => {
self.filter_error = Some(e);
}
}
}
}
if let Some(changed_at) = self.pending_filter_update
&& changed_at.elapsed() >= DEBOUNCE_DURATION
{
self.pending_filter_update = None;
let filter_text = if self.partial_eval_mode {
let byte_pos = self.filter_buffer.cursor_byte_pos();
let result =
partial_eval::extract_partial_eval_text(self.filter_buffer.text(), byte_pos);
result.eval_text
} else {
self.filter_buffer.text().to_string()
};
match filter::parse_check(&filter_text) {
Ok(()) => {
self.filter_error = None;
if !self.eval_paused {
self.request_eval();
}
}
Err(e) => {
self.filter_error = Some(FilterError::Parse(e));
}
}
}
}
pub fn scroll_offset(&self) -> usize {
self.viewport.scroll_offset
}
pub fn total_lines(&self) -> usize {
self.total_lines
}
pub fn horizontal_offset(&self) -> usize {
self.viewport.horizontal_offset
}
pub fn show_help(&self) -> bool {
self.help.visible
}
pub fn help_scroll(&self) -> usize {
self.help.scroll
}
pub fn clamp_help_scroll(&mut self, max_scroll: usize) {
self.help.clamp_scroll(max_scroll);
}
pub fn wrap_lines(&self) -> bool {
self.display.wrap_lines
}
pub fn editing_filter(&self) -> bool {
self.editing_filter
}
pub fn partial_eval_mode(&self) -> bool {
self.partial_eval_mode
}
pub fn computed_partial_eval_ranges(&self) -> Option<Vec<(usize, usize)>> {
if self.partial_eval_mode {
let byte_pos = self.filter_buffer.cursor_byte_pos();
let result =
partial_eval::extract_partial_eval_text(self.filter_buffer.text(), byte_pos);
Some(result.highlight_ranges)
} else {
None
}
}
pub fn raw_output(&self) -> bool {
self.display.raw_output
}
pub fn slurp_mode(&self) -> bool {
self.display.slurp
}
pub fn compact_output(&self) -> bool {
self.display.compact
}
pub fn is_evaluating(&self) -> bool {
self.eval.is_evaluating()
}
pub fn eval_paused(&self) -> bool {
self.eval_paused
}
fn cancel_eval(&mut self) {
self.eval.cancel();
self.pending_filter_update = None;
}
pub fn filter_text(&self) -> &str {
self.filter_buffer.text()
}
fn copy_filter_to_clipboard(&self) {
if let Ok(mut clipboard) = Clipboard::new() {
let mut flags = String::new();
if self.display.slurp {
flags.push('s');
}
if self.display.raw_output {
flags.push('r');
}
if self.display.compact {
flags.push('c');
}
let command = if flags.is_empty() {
format!("jq '{}'", self.filter_buffer.text())
} else {
format!("jq -{} '{}'", flags, self.filter_buffer.text())
};
let _ = clipboard.set_text(command);
}
}
pub fn filter_cursor_display_col(&self) -> usize {
let byte_pos = self.filter_buffer.cursor_byte_pos();
self.filter_buffer.text()[..byte_pos].width()
}
pub fn filter_error(&self) -> Option<&FilterError> {
self.filter_error.as_ref()
}
pub fn set_viewport(&mut self, width: usize, height: usize) {
self.viewport.set_size(width, height);
self.viewport.clamp_scroll(self.total_lines);
}
pub fn handle_key(&mut self, key: KeyEvent) -> AppAction {
if key.code == KeyCode::Char('h') && key.modifiers.contains(KeyModifiers::CONTROL) {
self.help.toggle(self.editing_filter);
return AppAction::Continue;
}
if self.help.visible {
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
self.help.scroll_down(1);
}
KeyCode::Char('k') | KeyCode::Up => {
self.help.scroll_up(1);
}
KeyCode::PageDown => {
self.help.scroll_down(10);
}
KeyCode::PageUp => {
self.help.scroll_up(10);
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.help.scroll_down(10);
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.help.scroll_up(10);
}
KeyCode::Char('g') => {
self.help.scroll = 0;
}
KeyCode::Char('G') => {
self.help.scroll = usize::MAX;
}
_ => {
self.help.visible = false;
}
}
return AppAction::Continue;
}
if self.editing_filter {
return self.handle_filter_key(key);
}
self.handle_navigation_key(key)
}
pub fn help_for_filter_mode(&self) -> bool {
self.help.for_filter_mode
}
fn handle_navigation_key(&mut self, key: KeyEvent) -> AppAction {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => AppAction::Quit,
KeyCode::Char('i') | KeyCode::Char('/') => {
self.editing_filter = true;
AppAction::Continue
}
KeyCode::Char('j') | KeyCode::Down => {
self.viewport.scroll_down(1, self.total_lines);
AppAction::Continue
}
KeyCode::Char('k') | KeyCode::Up => {
self.viewport.scroll_up(1);
AppAction::Continue
}
KeyCode::Char('h') | KeyCode::Left => {
if !self.display.wrap_lines {
self.viewport.scroll_left(4);
}
AppAction::Continue
}
KeyCode::Char('l') | KeyCode::Right => {
if !self.display.wrap_lines {
self.viewport.scroll_right(4);
}
AppAction::Continue
}
KeyCode::Char('g') => {
self.viewport.scroll_to_top();
AppAction::Continue
}
KeyCode::Char('G') => {
self.viewport.scroll_to_bottom(self.total_lines);
AppAction::Continue
}
KeyCode::Char('0') => {
self.viewport.horizontal_offset = 0;
AppAction::Continue
}
KeyCode::Char('w') => {
self.display.wrap_lines = !self.display.wrap_lines;
if self.display.wrap_lines {
self.viewport.horizontal_offset = 0;
}
AppAction::Continue
}
KeyCode::Char('r') => {
self.display.raw_output = !self.display.raw_output;
self.recompute_total_lines();
AppAction::Continue
}
KeyCode::Char('s') => {
self.display.slurp = !self.display.slurp;
self.request_eval();
AppAction::Continue
}
KeyCode::Char('o') => {
self.display.compact = !self.display.compact;
self.recompute_total_lines();
AppAction::Continue
}
KeyCode::Char('y') => {
self.copy_filter_to_clipboard();
AppAction::Continue
}
KeyCode::PageDown => {
self.viewport
.scroll_down(self.viewport.height.saturating_sub(1), self.total_lines);
AppAction::Continue
}
KeyCode::PageUp => {
self.viewport
.scroll_up(self.viewport.height.saturating_sub(1));
AppAction::Continue
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.viewport
.scroll_down(self.viewport.height / 2, self.total_lines);
AppAction::Continue
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.viewport.scroll_up(self.viewport.height / 2);
AppAction::Continue
}
_ => AppAction::Continue,
}
}
fn handle_filter_key(&mut self, key: KeyEvent) -> AppAction {
match key.code {
KeyCode::Esc => {
self.exit_filter_edit();
}
KeyCode::Enter => {
if self.eval_paused {
self.request_eval();
}
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if self.eval.started_at.is_some() {
self.cancel_eval();
self.eval_paused = true;
}
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.exit_filter_edit();
}
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.eval_paused = !self.eval_paused;
if !self.eval_paused && self.pending_filter_update.is_some() {
self.request_eval();
}
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.display.slurp = !self.display.slurp;
self.request_eval();
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.display.raw_output = !self.display.raw_output;
self.recompute_total_lines();
}
KeyCode::Char('o') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.display.compact = !self.display.compact;
self.recompute_total_lines();
}
KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.copy_filter_to_clipboard();
}
KeyCode::Backspace => {
if self.filter_buffer.backspace() {
self.mark_filter_dirty();
}
}
KeyCode::Delete => {
if self.filter_buffer.delete() {
self.mark_filter_dirty();
}
}
KeyCode::Left => {
self.filter_buffer.move_left();
if self.partial_eval_mode {
self.mark_filter_dirty();
}
}
KeyCode::Right => {
self.filter_buffer.move_right();
if self.partial_eval_mode {
self.mark_filter_dirty();
}
}
KeyCode::Tab => {
self.partial_eval_mode = !self.partial_eval_mode;
self.mark_filter_dirty();
}
KeyCode::Home => {
self.filter_buffer.move_to_start();
}
KeyCode::End => {
self.filter_buffer.move_to_end();
}
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.filter_buffer.move_to_start();
}
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.filter_buffer.move_to_end();
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.filter_buffer = TextBuffer::new(".");
self.mark_filter_dirty();
}
KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if self.filter_buffer.delete_word_back() {
self.mark_filter_dirty();
}
}
KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => {
self.filter_buffer.jump_word_back();
if self.partial_eval_mode {
self.mark_filter_dirty();
}
}
KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => {
self.filter_buffer.jump_word_forward();
if self.partial_eval_mode {
self.mark_filter_dirty();
}
}
KeyCode::Char(c) => {
self.filter_buffer.insert(c);
self.mark_filter_dirty();
}
_ => {}
}
AppAction::Continue
}
fn exit_filter_edit(&mut self) {
if self.pending_filter_update.is_some() {
self.request_eval();
}
self.editing_filter = false;
}
}