diff --git a/README.md b/README.md index e1edbf1..de2f6c9 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 +- Result summary showing the current result's type and length + (e.g. `object · 3 keys`) in the status line ## Installation diff --git a/src/guide.rs b/src/guide.rs index f3f51eb..0f67153 100644 --- a/src/guide.rs +++ b/src/guide.rs @@ -20,6 +20,8 @@ pub enum GuideMessage { NoSuggestionFound(String), JqReturnedNull(String), JqFailed(String), + /// Type/length summary of the current jq result (e.g. `object · 3 keys`). + ResultSummary(String), } /// 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::ResultSummary(summary) => { + status::State::new(format!("result: {summary}"), Severity::Success) + } } } diff --git a/src/json.rs b/src/json.rs index f12a377..afdb585 100644 --- a/src/json.rs +++ b/src/json.rs @@ -70,3 +70,75 @@ pub fn run_jaq( Ok(ret) } + +/// Summarize a jq result stream as a short `type · length` string, mirroring +/// what `| type` and `| length` would report. Used for a status-line hint so +/// users don't have to append those filters manually. +/// +/// A single value is described by its type and (where meaningful) its length; +/// a multi-value stream is reported as a count. +pub fn summarize(values: &[serde_json::Value]) -> String { + match values { + [] => "empty (0 results)".to_string(), + [value] => summarize_value(value), + many => format!("stream · {} values", many.len()), + } +} + +fn summarize_value(value: &serde_json::Value) -> String { + match value { + Value::Object(map) => format!("object · {} {}", map.len(), pluralize(map.len(), "key")), + Value::Array(items) => { + format!("array · {} {}", items.len(), pluralize(items.len(), "item")) + } + Value::String(s) => { + let count = s.chars().count(); + format!("string · {} {}", count, pluralize(count, "char")) + } + Value::Number(_) => "number".to_string(), + Value::Bool(_) => "boolean".to_string(), + Value::Null => "null".to_string(), + } +} + +fn pluralize(count: usize, noun: &str) -> String { + if count == 1 { + noun.to_string() + } else { + format!("{noun}s") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use promkit_widgets::serde_json::json; + + #[test] + fn summarize_object_array_string() { + assert_eq!(summarize(&[json!({"a": 1, "b": 2})]), "object · 2 keys"); + assert_eq!(summarize(&[json!({"a": 1})]), "object · 1 key"); + assert_eq!(summarize(&[json!([1, 2, 3])]), "array · 3 items"); + assert_eq!(summarize(&[json!([1])]), "array · 1 item"); + assert_eq!(summarize(&[json!("hello")]), "string · 5 chars"); + } + + #[test] + fn summarize_scalars() { + assert_eq!(summarize(&[json!(42)]), "number"); + assert_eq!(summarize(&[json!(true)]), "boolean"); + assert_eq!(summarize(&[json!(null)]), "null"); + } + + #[test] + fn summarize_stream_and_empty() { + assert_eq!(summarize(&[json!(1), json!(2)]), "stream · 2 values"); + assert_eq!(summarize(&[]), "empty (0 results)"); + } + + #[test] + fn summarize_string_counts_unicode_scalar_values() { + // "café" is 4 chars even though 'é' is multi-byte. + assert_eq!(summarize(&[json!("café")]), "string · 4 chars"); + } +} diff --git a/src/json_viewer.rs b/src/json_viewer.rs index be1fcf5..9d20d84 100644 --- a/src/json_viewer.rs +++ b/src/json_viewer.rs @@ -91,14 +91,15 @@ impl JsonViewer { ) -> (Option, Option) { 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)); - + // `all` is vacuously true for an empty result, so this also + // covers the "filter produced nothing" case. + let guide = if ret.iter().all(|val| *val == Value::Null) { self.state.stream = JsonStream::new(self.json.iter()); + Some(GuideMessage::JqReturnedNull(input)) } else { self.state.stream = JsonStream::new(ret.iter()); - } + Some(GuideMessage::ResultSummary(json::summarize(&ret))) + }; (guide, Some(self.state.create_graphemes(area.0, area.1))) }