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};
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,
scroll_offset: usize,
horizontal_offset: usize,
viewport_height: usize,
viewport_width: usize,
show_help: bool,
help_for_filter_mode: bool,
help_scroll: usize,
wrap_lines: bool,
editing_filter: bool,
raw_output: bool,
slurp_mode: bool,
compact_output: bool,
eval_paused: bool,
worker: Worker,
eval_request_id: u64,
pending_eval_id: Option<u64>,
eval_started_at: Option<Instant>,
}
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,
scroll_offset: 0,
horizontal_offset: 0,
viewport_height: 0,
viewport_width: 0,
show_help: false,
help_for_filter_mode: false,
help_scroll: 0,
wrap_lines: false,
editing_filter: false,
raw_output: false,
slurp_mode: false,
compact_output: false,
eval_paused: false,
worker,
eval_request_id: 0,
pending_eval_id: None,
eval_started_at: None,
}
}
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,
scroll_offset: 0,
horizontal_offset: 0,
viewport_height: 0,
viewport_width: 0,
show_help: false,
help_for_filter_mode: false,
help_scroll: 0,
wrap_lines: false,
editing_filter: false,
raw_output: false,
slurp_mode: false,
compact_output: false,
eval_paused: false,
worker: Worker::spawn(),
eval_request_id: 0,
pending_eval_id: None,
eval_started_at: None,
}
}
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 {
RenderOptions {
raw_output: self.raw_output,
compact: self.compact_output,
}
}
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;
};
self.eval_request_id += 1;
let filter_text = if self.partial_eval_mode {
let byte_pos = self.filter_buffer.cursor_byte_pos();
self.filter_buffer.text()[..byte_pos].to_string()
} else {
self.filter_buffer.text().to_string()
};
self.worker.send(EvalRequest {
id: self.eval_request_id,
filter_text,
inputs: Arc::clone(input_values),
slurp_mode: self.slurp_mode,
});
self.pending_eval_id = Some(self.eval_request_id);
self.eval_started_at = Some(Instant::now());
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 Some(result.id) == self.pending_eval_id {
self.pending_eval_id = None;
self.eval_started_at = None;
match result.result {
Ok(values) => {
self.output_values = values;
self.filter_error = None;
self.recompute_total_lines();
self.scroll_offset = 0;
self.horizontal_offset = 0;
}
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();
&self.filter_buffer.text()[..byte_pos]
} else {
self.filter_buffer.text()
};
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.scroll_offset
}
pub fn total_lines(&self) -> usize {
self.total_lines
}
pub fn horizontal_offset(&self) -> usize {
self.horizontal_offset
}
pub fn show_help(&self) -> bool {
self.show_help
}
pub fn help_scroll(&self) -> usize {
self.help_scroll
}
pub fn clamp_help_scroll(&mut self, max_scroll: usize) {
self.help_scroll = self.help_scroll.min(max_scroll);
}
pub fn wrap_lines(&self) -> bool {
self.wrap_lines
}
pub fn editing_filter(&self) -> bool {
self.editing_filter
}
pub fn partial_eval_mode(&self) -> bool {
self.partial_eval_mode
}
pub fn raw_output(&self) -> bool {
self.raw_output
}
pub fn slurp_mode(&self) -> bool {
self.slurp_mode
}
pub fn compact_output(&self) -> bool {
self.compact_output
}
pub fn is_evaluating(&self) -> bool {
const EVAL_INDICATOR_DELAY: Duration = Duration::from_millis(100);
self.eval_started_at
.is_some_and(|started| started.elapsed() >= EVAL_INDICATOR_DELAY)
}
pub fn eval_paused(&self) -> bool {
self.eval_paused
}
fn cancel_eval(&mut self) {
self.pending_eval_id = None;
self.eval_started_at = None;
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.slurp_mode {
flags.push('s');
}
if self.raw_output {
flags.push('r');
}
if self.compact_output {
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_width = width;
self.viewport_height = height;
self.clamp_scroll();
}
pub fn handle_key(&mut self, key: KeyEvent) -> AppAction {
if key.code == KeyCode::Char('h') && key.modifiers.contains(KeyModifiers::CONTROL) {
if self.show_help {
self.show_help = false;
} else {
self.show_help = true;
self.help_scroll = 0;
self.help_for_filter_mode = self.editing_filter;
}
return AppAction::Continue;
}
if self.show_help {
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
self.help_scroll = self.help_scroll.saturating_add(1);
}
KeyCode::Char('k') | KeyCode::Up => {
self.help_scroll = self.help_scroll.saturating_sub(1);
}
KeyCode::PageDown => {
self.help_scroll = self.help_scroll.saturating_add(10);
}
KeyCode::PageUp => {
self.help_scroll = self.help_scroll.saturating_sub(10);
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.help_scroll = self.help_scroll.saturating_add(10);
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.help_scroll = self.help_scroll.saturating_sub(10);
}
KeyCode::Char('g') => {
self.help_scroll = 0;
}
KeyCode::Char('G') => {
self.help_scroll = usize::MAX;
}
_ => {
self.show_help = 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.scroll_down(1);
AppAction::Continue
}
KeyCode::Char('k') | KeyCode::Up => {
self.scroll_up(1);
AppAction::Continue
}
KeyCode::Char('h') | KeyCode::Left => {
if !self.wrap_lines {
self.scroll_left(4);
}
AppAction::Continue
}
KeyCode::Char('l') | KeyCode::Right => {
if !self.wrap_lines {
self.scroll_right(4);
}
AppAction::Continue
}
KeyCode::Char('g') => {
self.scroll_to_top();
AppAction::Continue
}
KeyCode::Char('G') => {
self.scroll_to_bottom();
AppAction::Continue
}
KeyCode::Char('0') => {
self.horizontal_offset = 0;
AppAction::Continue
}
KeyCode::Char('w') => {
self.wrap_lines = !self.wrap_lines;
if self.wrap_lines {
self.horizontal_offset = 0;
}
AppAction::Continue
}
KeyCode::Char('r') => {
self.raw_output = !self.raw_output;
self.recompute_total_lines();
AppAction::Continue
}
KeyCode::Char('s') => {
self.slurp_mode = !self.slurp_mode;
self.request_eval();
AppAction::Continue
}
KeyCode::Char('o') => {
self.compact_output = !self.compact_output;
self.recompute_total_lines();
AppAction::Continue
}
KeyCode::Char('y') => {
self.copy_filter_to_clipboard();
AppAction::Continue
}
KeyCode::PageDown => {
self.scroll_down(self.viewport_height.saturating_sub(1));
AppAction::Continue
}
KeyCode::PageUp => {
self.scroll_up(self.viewport_height.saturating_sub(1));
AppAction::Continue
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.scroll_down(self.viewport_height / 2);
AppAction::Continue
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.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.slurp_mode = !self.slurp_mode;
self.request_eval();
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.raw_output = !self.raw_output;
self.recompute_total_lines();
}
KeyCode::Char('o') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.compact_output = !self.compact_output;
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;
}
fn scroll_down(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_add(lines);
self.clamp_scroll();
}
fn scroll_up(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
}
fn scroll_left(&mut self, cols: usize) {
self.horizontal_offset = self.horizontal_offset.saturating_sub(cols);
}
fn scroll_right(&mut self, cols: usize) {
self.horizontal_offset = self.horizontal_offset.saturating_add(cols);
}
fn scroll_to_top(&mut self) {
self.scroll_offset = 0;
}
fn scroll_to_bottom(&mut self) {
if self.total_lines > self.viewport_height {
self.scroll_offset = self.total_lines - self.viewport_height;
}
}
fn clamp_scroll(&mut self) {
let max_scroll = self.total_lines.saturating_sub(self.viewport_height);
self.scroll_offset = self.scroll_offset.min(max_scroll);
}
}