use std::time::{Duration, Instant};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::text::Line;
use serde_json::Value;
use unicode_width::UnicodeWidthStr;
use crate::error::FilterError;
use crate::filter;
use crate::text_buffer::TextBuffer;
use crate::ui;
const DEBOUNCE_DURATION: Duration = Duration::from_millis(250);
pub enum AppAction {
Continue,
Quit,
}
pub struct App<'a> {
input_values: Vec<Value>,
filter_buffer: TextBuffer,
filter_result: Result<Vec<Value>, FilterError>,
pending_filter_update: Option<Instant>,
partial_eval_mode: bool,
lines: Vec<Line<'a>>,
scroll_offset: usize,
horizontal_offset: usize,
viewport_height: usize,
viewport_width: usize,
show_help: bool,
wrap_lines: bool,
editing_filter: bool,
raw_output: bool,
slurp_mode: bool,
compact_output: bool,
}
impl<'a> App<'a> {
pub fn new(input_values: Vec<Value>) -> Self {
let filter_buffer = TextBuffer::new(".");
let filter_result = filter::evaluate_all(filter_buffer.text(), &input_values);
let lines = match &filter_result {
Ok(values) => {
let mut lines = Vec::new();
for value in values {
lines.extend(ui::json_to_lines(value));
}
lines
}
Err(_) => Vec::new(),
};
Self {
input_values,
filter_buffer,
filter_result,
pending_filter_update: None,
partial_eval_mode: false,
lines,
scroll_offset: 0,
horizontal_offset: 0,
viewport_height: 0,
viewport_width: 0,
show_help: false,
wrap_lines: false,
editing_filter: false,
raw_output: false,
slurp_mode: false,
compact_output: false,
}
}
pub fn input_count(&self) -> usize {
self.input_values.len()
}
fn values_to_lines(&self, values: &[Value]) -> Vec<Line<'static>> {
let options = ui::RenderOptions {
raw_output: self.raw_output,
compact: self.compact_output,
};
let mut lines = Vec::new();
for value in values {
lines.extend(ui::json_to_lines_with_options(value, &options));
}
lines
}
fn rebuild_lines(&mut self) {
if let Ok(ref values) = self.filter_result {
self.lines = self.values_to_lines(values);
}
}
fn mark_filter_dirty(&mut self) {
self.pending_filter_update = Some(Instant::now());
}
fn apply_filter(&mut self) {
let filter_to_eval = 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()
};
let inputs_to_filter = if self.slurp_mode {
vec![Value::Array(self.input_values.clone())]
} else {
self.input_values.clone()
};
let result = filter::evaluate_all(filter_to_eval, &inputs_to_filter);
if let Ok(ref values) = result {
self.lines = self.values_to_lines(values);
self.scroll_offset = 0;
self.horizontal_offset = 0;
}
self.filter_result = result;
self.pending_filter_update = None;
}
pub fn tick(&mut self) {
if let Some(changed_at) = self.pending_filter_update
&& changed_at.elapsed() >= DEBOUNCE_DURATION
{
self.apply_filter();
}
}
pub fn lines(&self) -> &[Line<'a>] {
&self.lines
}
pub fn scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn total_lines(&self) -> usize {
self.lines.len()
}
pub fn horizontal_offset(&self) -> usize {
self.horizontal_offset
}
pub fn show_help(&self) -> bool {
self.show_help
}
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 filter_text(&self) -> &str {
self.filter_buffer.text()
}
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_result.as_ref().err()
}
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 self.show_help {
self.show_help = false;
return self.handle_key_after_help(key);
}
if self.editing_filter {
return self.handle_filter_key(key);
}
self.handle_navigation_key(key)
}
fn handle_key_after_help(&mut self, key: KeyEvent) -> AppAction {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => AppAction::Continue,
_ => self.handle_navigation_key(key),
}
}
fn handle_navigation_key(&mut self, key: KeyEvent) -> AppAction {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => AppAction::Quit,
KeyCode::Char('?') => {
self.show_help = true;
AppAction::Continue
}
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.rebuild_lines();
AppAction::Continue
}
KeyCode::Char('s') => {
self.slurp_mode = !self.slurp_mode;
self.apply_filter();
AppAction::Continue
}
KeyCode::Char('c') => {
self.compact_output = !self.compact_output;
self.rebuild_lines();
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 | KeyCode::Enter => {
self.exit_filter_edit();
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.exit_filter_edit();
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.exit_filter_edit();
}
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.apply_filter();
}
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.lines.len() > self.viewport_height {
self.scroll_offset = self.lines.len() - self.viewport_height;
}
}
fn clamp_scroll(&mut self) {
let max_scroll = self.lines.len().saturating_sub(self.viewport_height);
self.scroll_offset = self.scroll_offset.min(max_scroll);
}
}