diff --git a/README.md b/README.md index e1edbf1..1af945a 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ and [jiq](https://github.com/fiatjaf/jiq). - [Object Identifier-Index](https://jqlang.github.io/jq/manual/#object-identifier-index) - [Array Index](https://jqlang.github.io/jq/manual/#array-index) - Hint message to evaluate the filter +- Keeps the last successful result visible when a query returns null or fails, + so the view stays stable while you fix the filter ## Installation diff --git a/src/guide.rs b/src/guide.rs index f3f51eb..f34087f 100644 --- a/src/guide.rs +++ b/src/guide.rs @@ -20,6 +20,8 @@ pub enum GuideMessage { NoSuggestionFound(String), JqReturnedNull(String), JqFailed(String), + /// The query is empty, so the full input is shown unfiltered. + NoFilter, } /// Represent an action to be performed on the guide. @@ -66,6 +68,9 @@ fn message_to_state(message: GuideMessage) -> status::State { GuideMessage::JqFailed(e) => { status::State::new(format!("jq failed: `{e}`"), Severity::Error) } + GuideMessage::NoFilter => { + status::State::new("no filter — showing full input", Severity::Success) + } } } diff --git a/src/json_viewer.rs b/src/json_viewer.rs index be1fcf5..c38ee8a 100644 --- a/src/json_viewer.rs +++ b/src/json_viewer.rs @@ -32,6 +32,10 @@ pub enum RenderTrigger { pub struct JsonViewer { state: jsonstream::State, json: Vec, + /// The most recent non-null successful query result. Used to keep the view + /// stable when a later query returns null or fails, instead of resetting to + /// the whole input document. Empty until the first successful result. + last_good: Vec, keybinds: JsonViewerKeybinds, } @@ -89,21 +93,37 @@ impl JsonViewer { area: (u16, u16), input: String, ) -> (Option, Option) { + // An empty (or whitespace-only) query means "no filter": reset to the + // full input document fresh. jaq rejects an empty program as invalid, so + // without this guard an explicit clear (e.g. Ctrl+U) is treated like an + // incomplete filter mid-typing — falling back to the stale last result, + // or on a fresh view silently dumping the whole input with no hint. + if input.trim().is_empty() { + self.last_good = Vec::new(); + self.state.stream = JsonStream::new(self.json.iter()); + return ( + Some(GuideMessage::NoFilter), + Some(self.state.create_graphemes(area.0, area.1)), + ); + } + 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()); + // `all` is vacuously true for an empty result, so a filter that + // produces nothing is treated like null: keep the view stable. + let guide = if ret.iter().all(|val| *val == Value::Null) { + self.show_last_good(); + Some(GuideMessage::JqReturnedNull(input)) } else { - self.state.stream = JsonStream::new(ret.iter()); - } + self.last_good = ret; + self.state.stream = JsonStream::new(self.last_good.iter()); + None + }; (guide, Some(self.state.create_graphemes(area.0, area.1))) } Err(e) => { - self.state.stream = JsonStream::new(self.json.iter()); + self.show_last_good(); ( Some(GuideMessage::JqFailed(e.to_string())), @@ -112,6 +132,16 @@ impl JsonViewer { } } } + + /// Point the view at the most recent successful result, falling back to the + /// original input document if no query has succeeded yet. + fn show_last_good(&mut self) { + self.state.stream = if self.last_good.is_empty() { + JsonStream::new(self.json.iter()) + } else { + JsonStream::new(self.last_good.iter()) + }; + } } /// Initialize the JSON viewer with the given input, configuration, keybinds, and shared context. @@ -158,6 +188,7 @@ pub async fn initialize( Ok(Arc::new(Mutex::new(JsonViewer { json: input_stream, + last_good: Vec::new(), state, keybinds, })))