From ba58a2cadee58c7dac3efb107220be5bcb69e984 Mon Sep 17 00:00:00 2001 From: ynqa Date: Sat, 12 Sep 2026 14:00:12 +0900 Subject: [PATCH 1/3] Add JSON and YAML child counts and bump crate versions --- README.md | 2 +- examples/json/src/json.rs | 5 + examples/yaml/src/yaml.rs | 5 + promkit-widgets/Cargo.toml | 2 +- promkit-widgets/README.md | 41 ++++- promkit-widgets/benches/structured/json.rs | 15 ++ promkit-widgets/benches/structured/yaml.rs | 15 ++ promkit-widgets/src/structured/json/config.rs | 118 ++++++++++++- .../src/structured/json/deserializer.rs | 16 +- .../src/structured/json/document.rs | 66 +++++++ promkit-widgets/src/structured/json/jsonz.rs | 19 +- promkit-widgets/src/structured/mod.rs | 14 +- promkit-widgets/src/structured/yaml/config.rs | 166 +++++++++++++++++- .../src/structured/yaml/deserializer.rs | 10 +- .../src/structured/yaml/document.rs | 102 +++++++++++ promkit-widgets/src/structured/yaml/yamlz.rs | 19 +- promkit-widgets/tests/jsonz/create_rows.rs | 11 ++ promkit-widgets/tests/jsonz/extract.rs | 4 + .../tests/jsonz/multi_documents.rs | 9 + promkit-widgets/tests/jsonz/toggle.rs | 27 ++- .../tests/yamlz/multi_documents.rs | 12 ++ promkit/Cargo.toml | 4 +- 22 files changed, 650 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 83a108d2..f243f59f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Put the package in your `Cargo.toml`. ```toml [dependencies] -promkit = { version = "0.16.0", features = ["runtime", "texteditor"] } +promkit = { version = "0.17.0", features = ["runtime", "texteditor"] } ``` ## Features diff --git a/examples/json/src/json.rs b/examples/json/src/json.rs index 3b512c43..f3c33826 100644 --- a/examples/json/src/json.rs +++ b/examples/json/src/json.rs @@ -104,6 +104,11 @@ impl JsonViewer { overflow_mode: OverflowMode::Wrap, lines: None, show_line_numbers: true, + show_child_count: true, + child_count_style: ContentStyle { + foreground_color: Some(Color::DarkGrey), + ..Default::default() + }, }, }, } diff --git a/examples/yaml/src/yaml.rs b/examples/yaml/src/yaml.rs index 71ebb642..e8aa1d98 100644 --- a/examples/yaml/src/yaml.rs +++ b/examples/yaml/src/yaml.rs @@ -108,6 +108,11 @@ impl YamlViewer { overflow_mode: OverflowMode::Wrap, lines: None, show_line_numbers: true, + show_child_count: true, + child_count_style: ContentStyle { + foreground_color: Some(Color::DarkGrey), + ..Default::default() + }, }, }, } diff --git a/promkit-widgets/Cargo.toml b/promkit-widgets/Cargo.toml index 2c2fe87c..7d170828 100644 --- a/promkit-widgets/Cargo.toml +++ b/promkit-widgets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "promkit-widgets" -version = "0.9.0" +version = "0.10.0" edition = "2024" authors = ["ynqa "] description = "Widgets for promkit" diff --git a/promkit-widgets/README.md b/promkit-widgets/README.md index 19ccc213..388d36d4 100644 --- a/promkit-widgets/README.md +++ b/promkit-widgets/README.md @@ -14,14 +14,14 @@ using the `promkit` runtime can enable them through the main crate: ```toml [dependencies] -promkit = { version = "0.16.0", features = ["runtime", "texteditor"] } +promkit = { version = "0.17.0", features = ["runtime", "texteditor"] } ``` The widget states can also be used directly: ```toml [dependencies] -promkit-widgets = { version = "0.9", features = ["json", "yaml"] } +promkit-widgets = { version = "0.10", features = ["json", "yaml"] } ``` `promkit` re-exports this crate as `promkit::widgets`, while @@ -44,6 +44,43 @@ promkit-widgets = { version = "0.9", features = ["json", "yaml"] } | `serde` | Serde support for widget configuration | | `all` | All features above | +## JSON and YAML child counts + +JSON and YAML documents retain the number of immediate array/sequence elements +or object/mapping entries, independently of folding. Query a container with +`Document::child_count(row_index)`, using a zero-based index into `Document::rows()`. +Empty containers return `Some(0)`; closing rows return the opening container's +count. Scalars, YAML document separators, and invalid indices return `None`. + +Enable annotations through the widget configuration: + +```rust +use promkit_widgets::json::{Config, Document, State}; + +let mut state = State { + document: Document::from_str(r#"{"items": [1, 2, 3]}"#)?, + config: Config { + show_child_count: true, + ..Default::default() + }, +}; +assert_eq!(state.document.child_count(1), Some(3)); +state.document.toggle_at(1); +// The items row now displays: "items": […] (3 items) +``` + +The same APIs are available under `promkit_widgets::yaml`. Annotations appear +beside collapsed or empty containers, for example `{…} (2 keys)` and `[] (0 items)`. +`show_child_count` defaults to `false`; `child_count_style` controls annotation +styling. Changing these settings does not fold or expand containers. Existing +`toggle()` / `toggle_at()` operations control folding. Pretty-printed data exports +do not include annotations. + +When constructing `ContainerNode::Open` directly, supply its new `child_count` +field. Existing exhaustive `Open` patterns must include the field or `..`. +For `Config` struct literals, use `..Default::default()` or supply the new fields; +stored Serde configurations without these fields keep the default display. + ## Responsibilities Widgets manage state and project it into renderable content. They intentionally diff --git a/promkit-widgets/benches/structured/json.rs b/promkit-widgets/benches/structured/json.rs index 59c0e67a..1a2e7d74 100644 --- a/promkit-widgets/benches/structured/json.rs +++ b/promkit-widgets/benches/structured/json.rs @@ -98,6 +98,21 @@ fn benchmark_fixture(c: &mut Criterion, path: &Path) { }); }); + state.document.set_nodes_visibility(true); + for show_child_count in [false, true] { + state.config.show_child_count = show_child_count; + let label = if show_child_count { + "counts_visible" + } else { + "counts_hidden" + }; + group.bench_function(format!("collapsed_viewport_projection/{label}"), |b| { + b.iter(|| { + black_box(state.create_graphemes_in_viewport(VIEWPORT_WIDTH, VIEWPORT_HEIGHT)) + }); + }); + } + group.finish(); } diff --git a/promkit-widgets/benches/structured/yaml.rs b/promkit-widgets/benches/structured/yaml.rs index 24d82f7d..b6a2ec72 100644 --- a/promkit-widgets/benches/structured/yaml.rs +++ b/promkit-widgets/benches/structured/yaml.rs @@ -98,6 +98,21 @@ fn benchmark_fixture(c: &mut Criterion, path: &Path) { }); }); + state.document.set_nodes_visibility(true); + for show_child_count in [false, true] { + state.config.show_child_count = show_child_count; + let label = if show_child_count { + "counts_visible" + } else { + "counts_hidden" + }; + group.bench_function(format!("collapsed_viewport_projection/{label}"), |b| { + b.iter(|| { + black_box(state.create_graphemes_in_viewport(VIEWPORT_WIDTH, VIEWPORT_HEIGHT)) + }); + }); + } + group.finish(); } diff --git a/promkit-widgets/src/structured/json/config.rs b/promkit-widgets/src/structured/json/config.rs index b8691510..9c0560ef 100644 --- a/promkit-widgets/src/structured/json/config.rs +++ b/promkit-widgets/src/structured/json/config.rs @@ -92,6 +92,15 @@ pub struct Config { pub lines: Option, /// Whether to display stable one-based line numbers to the left of the content. pub show_line_numbers: bool, + /// Display immediate child counts next to collapsed and empty containers. + /// Disabled by default; changing this does not change folding state. + pub show_child_count: bool, + /// Style for child count annotations, such as ` (3 items)` or ` (2 keys)`. + #[cfg_attr( + feature = "serde", + serde(with = "termcfg::crossterm_config::content_style_serde") + )] + pub child_count_style: ContentStyle, } impl Default for Config { @@ -110,6 +119,8 @@ impl Default for Config { overflow_mode: OverflowMode::default(), lines: Default::default(), show_line_numbers: false, + show_child_count: false, + child_count_style: Default::default(), } } } @@ -174,8 +185,19 @@ impl Config { parts.push( StyledGraphemes::from(typ.empty_str()).apply_style(bracket_style), ); + if self.show_child_count { + parts.push( + StyledGraphemes::from(typ.child_count_label(0)) + .apply_style(self.child_count_style), + ); + } } - ContainerNode::Open { typ, collapsed, .. } => { + ContainerNode::Open { + typ, + collapsed, + child_count, + .. + } => { let bracket_style = match typ { ContainerType::Object => self.curly_brackets_style, ContainerType::Array => self.square_brackets_style, @@ -185,6 +207,12 @@ impl Config { StyledGraphemes::from(typ.collapsed_preview()) .apply_style(bracket_style), ); + if self.show_child_count { + parts.push( + StyledGraphemes::from(typ.child_count_label(*child_count)) + .apply_style(self.child_count_style), + ); + } } else { parts.push( StyledGraphemes::from(typ.open_str()).apply_style(bracket_style), @@ -264,6 +292,83 @@ mod tests { mod config { use super::*; + mod render_content_rows { + use super::*; + use crate::structured::json::Document; + use promkit_core::crossterm::style::Color; + + #[test] + fn annotates_collapsed_and_empty_containers_without_changing_state() { + let config = Config { + show_child_count: true, + ..Default::default() + }; + for (input, expected) in [ + ("[]", "[] (0 items)"), + ("{}", "{} (0 keys)"), + ("[1]", "[…] (1 item)"), + ("[1, [2, 3]]", "[…] (2 items)"), + (r#"{"x": []}"#, "{…} (1 key)"), + (r#"{"a": [], "b": {}}"#, "{…} (2 keys)"), + ] { + let mut document = Document::from_str(input).unwrap(); + document.set_nodes_visibility(true); + let before = document.rows().to_vec(); + let visible = document.visible_rows(); + assert_eq!( + config.render_content_rows(&visible, 0)[0].to_string(), + expected + ); + assert_eq!( + Config::default().render_content_rows(&visible, 0)[0].to_string(), + expected.split(" (").next().unwrap() + ); + assert_eq!(document.rows(), before); + } + let document = Document::from_str("[1, 2]").unwrap(); + let rows = document.visible_rows(); + assert_eq!( + config.render_content_rows(&rows, 0), + Config::default().render_content_rows(&rows, 0) + ); + } + + #[test] + fn styles_annotations_and_respects_width_limits() { + let mut document = Document::from_str("[1, 2]").unwrap(); + document.set_nodes_visibility(true); + let rows = document.visible_rows(); + let style = ContentStyle { + foreground_color: Some(Color::Cyan), + ..Default::default() + }; + let mut config = Config { + show_child_count: true, + child_count_style: style, + ..Default::default() + }; + let expected: StyledGraphemes = [ + StyledGraphemes::from("[…]"), + StyledGraphemes::from(" (2 items)").apply_style(style), + ] + .into_iter() + .collect(); + assert_eq!( + config.render_content_rows(&rows, 0), + [expected.apply_attribute(config.active_item_attribute)] + ); + let truncated = config.render_terminal_rows(&rows, 7); + assert_eq!(truncated[0].to_string(), "[…] (2…"); + config.overflow_mode = OverflowMode::Wrap; + let wrapped = config.render_terminal_rows(&rows, 7); + assert!(wrapped.iter().all(|line| line.widths() <= 7)); + assert_eq!( + wrapped.iter().map(ToString::to_string).collect::(), + "[…] (2 items)" + ); + } + } + mod render_terminal_rows { use super::*; @@ -336,6 +441,8 @@ mod tests { obj.remove("overflow_mode"); obj.remove("lines"); obj.remove("show_line_numbers"); + obj.remove("show_child_count"); + obj.remove("child_count_style"); let formatter: Config = serde_json::from_value(value).unwrap(); @@ -345,6 +452,8 @@ mod tests { assert_eq!(formatter.overflow_mode, OverflowMode::Truncate); assert_eq!(formatter.lines, None); assert!(!formatter.show_line_numbers); + assert!(!formatter.show_child_count); + assert_eq!(formatter.child_count_style, ContentStyle::default()); } #[test] @@ -353,6 +462,8 @@ mod tests { indent = 4 lines = 7 show_line_numbers = true + show_child_count = true + child_count_style = "fg=cyan" curly_brackets_style = "attr=bold" square_brackets_style = "attr=bold" key_style = "fg=cyan" @@ -370,6 +481,11 @@ mod tests { assert_eq!(formatter.indent, 4); assert_eq!(formatter.lines, Some(7)); assert!(formatter.show_line_numbers); + assert!(formatter.show_child_count); + assert_eq!( + formatter.child_count_style.foreground_color, + Some(Color::Cyan) + ); assert_eq!( formatter.curly_brackets_style.attributes, Attributes::from(Attribute::Bold), diff --git a/promkit-widgets/src/structured/json/deserializer.rs b/promkit-widgets/src/structured/json/deserializer.rs index 0f36f106..0c5beb64 100644 --- a/promkit-widgets/src/structured/json/deserializer.rs +++ b/promkit-widgets/src/structured/json/deserializer.rs @@ -126,10 +126,11 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Array, collapsed: false, close_index: 0, + child_count: 0, }), }); - let mut is_empty = true; + let mut child_count = 0; while sequence .next_element_seed(RowsSeed { rows: &mut *rows, @@ -138,10 +139,10 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { })? .is_some() { - is_empty = false; + child_count += 1; } - if is_empty { + if child_count == 0 { rows[open_index].node = JsonNode::Container(ContainerNode::Empty { typ: ContainerType::Array, }); @@ -162,6 +163,7 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Array, collapsed: false, close_index, + child_count, }); Ok(open_index) } @@ -179,20 +181,21 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Object, collapsed: false, close_index: 0, + child_count: 0, }), }); - let mut is_empty = true; + let mut child_count = 0; while let Some(key) = mapping.next_key::()? { mapping.next_value_seed(RowsSeed { rows: &mut *rows, depth: depth + 1, key: Some(key), })?; - is_empty = false; + child_count += 1; } - if is_empty { + if child_count == 0 { rows[open_index].node = JsonNode::Container(ContainerNode::Empty { typ: ContainerType::Object, }); @@ -213,6 +216,7 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Object, collapsed: false, close_index, + child_count, }); Ok(open_index) } diff --git a/promkit-widgets/src/structured/json/document.rs b/promkit-widgets/src/structured/json/document.rs index c72cb307..8526f9ca 100644 --- a/promkit-widgets/src/structured/json/document.rs +++ b/promkit-widgets/src/structured/json/document.rs @@ -60,6 +60,26 @@ impl Document { &self.rows } + /// Returns the number of immediate children of the container at a zero-based + /// index in [`Self::rows`], independently of its folding state. + /// + /// Empty containers return `Some(0)`. Closing rows return their opening + /// container's count. Scalars and out-of-bounds indices return `None`. + pub fn child_count(&self, row_index: usize) -> Option { + let container = match &self.rows.get(row_index)?.node { + JsonNode::Container(container) => container, + _ => return None, + }; + match container { + ContainerNode::Empty { .. } => Some(0), + ContainerNode::Open { child_count, .. } => Some(*child_count), + ContainerNode::Close { open_index, .. } => match &self.rows.get(*open_index)?.node { + JsonNode::Container(ContainerNode::Open { child_count, .. }) => Some(*child_count), + _ => None, + }, + } + } + /// Extract rows from the current cursor position. pub fn extract_rows_from_current(&self, n: usize) -> Vec { self.rows.extract(self.position, n) @@ -385,6 +405,52 @@ mod tests { Document::new(values.iter()) } + mod child_count { + use super::*; + + #[test] + fn counts_immediate_children_across_construction_and_folding() { + let input = r#"{"items":[1,{"x":[]}],"empty":{},"scalar":true} [null]"#; + let expected = [ + Some(3), + Some(2), + None, + Some(1), + Some(0), + Some(1), + Some(2), + Some(0), + None, + Some(3), + Some(1), + None, + Some(1), + ]; + for mut document in [ + via_value(input), + Document::from_str(input).unwrap(), + Document::from_reader(Cursor::new(input)).unwrap(), + ] { + let counts = |document: &Document| { + (0..document.rows().len()) + .map(|i| document.child_count(i)) + .collect::>() + }; + assert_eq!(counts(&document), expected); + assert_eq!(document.child_count(usize::MAX), None); + // Toggle from opening and closing rows, including nested containers. + for index in [1, 6, 3, 5, 0, 9] { + document.toggle_at(index); + assert_eq!(counts(&document), expected); + } + for collapsed in [true, false] { + document.set_nodes_visibility(collapsed); + assert_eq!(counts(&document), expected); + } + } + } + } + mod from_str { use super::*; diff --git a/promkit-widgets/src/structured/json/jsonz.rs b/promkit-widgets/src/structured/json/jsonz.rs index 30f14b25..bc60092b 100644 --- a/promkit-widgets/src/structured/json/jsonz.rs +++ b/promkit-widgets/src/structured/json/jsonz.rs @@ -154,15 +154,18 @@ impl RowOperation for Vec { typ, collapsed, close_index, + child_count, }) => { let new_collapsed = !collapsed; let close_idx = *close_index; + let child_count = *child_count; let typ_clone = typ.clone(); self[current].node = JsonNode::Container(ContainerNode::Open { typ: typ_clone.clone(), collapsed: new_collapsed, close_index: close_idx, + child_count, }); self[close_idx].node = JsonNode::Container(ContainerNode::Close { @@ -180,6 +183,11 @@ impl RowOperation for Vec { }) => { let new_collapsed = !collapsed; let open_idx = *open_index; + let JsonNode::Container(ContainerNode::Open { child_count, .. }) = + self[open_idx].node + else { + unreachable!("paired container must be open"); + }; let typ_clone = typ.clone(); self[current].node = JsonNode::Container(ContainerNode::Close { @@ -192,6 +200,7 @@ impl RowOperation for Vec { typ: typ_clone, collapsed: new_collapsed, close_index: current, + child_count, }); if new_collapsed { open_idx } else { current } @@ -203,13 +212,17 @@ impl RowOperation for Vec { fn set_rows_visibility(&mut self, collapsed: bool) { self.par_iter_mut().for_each(|row| { if let JsonNode::Container(ContainerNode::Open { - typ, close_index, .. + typ, + close_index, + child_count, + .. }) = &row.node { row.node = JsonNode::Container(ContainerNode::Open { typ: typ.clone(), collapsed, close_index: *close_index, + child_count: *child_count, }); } else if let JsonNode::Container(ContainerNode::Close { typ, open_index, .. @@ -311,6 +324,7 @@ fn process_value( typ: ContainerType::Array, collapsed: false, close_index: 0, + child_count: arr.len(), }), }); @@ -333,6 +347,7 @@ fn process_value( typ: ContainerType::Array, collapsed: false, close_index, + child_count: arr.len(), }); open_index @@ -358,6 +373,7 @@ fn process_value( typ: ContainerType::Object, collapsed: false, close_index: 0, + child_count: obj.len(), }), }); @@ -380,6 +396,7 @@ fn process_value( typ: ContainerType::Object, collapsed: false, close_index, + child_count: obj.len(), }); open_index diff --git a/promkit-widgets/src/structured/mod.rs b/promkit-widgets/src/structured/mod.rs index d7dfe883..4d4802b6 100644 --- a/promkit-widgets/src/structured/mod.rs +++ b/promkit-widgets/src/structured/mod.rs @@ -70,6 +70,16 @@ impl ContainerType { } } + #[cfg(any(feature = "json", feature = "yaml"))] + fn child_count_label(&self, count: usize) -> String { + let unit = match self { + Self::Object => "key", + Self::Array => "item", + }; + let plural = if count == 1 { "" } else { "s" }; + format!(" ({count} {unit}{plural})") + } + /// Collapsed preview string of the container. pub fn collapsed_preview(&self) -> &'static str { match self { @@ -86,11 +96,13 @@ pub enum ContainerNode { /// An empty container (e.g., `{}` or `[]`). Empty { typ: ContainerType }, /// An opening container (e.g., `{` or `[`), with information - /// about whether it's collapsed and its corresponding closing index. + /// about whether it's collapsed, its corresponding closing index, and child count. Open { typ: ContainerType, collapsed: bool, close_index: usize, + /// Number of immediate array elements or object entries. + child_count: usize, }, /// A closing container (e.g., `}` or `]`), with information /// about whether it's collapsed and its corresponding opening index. diff --git a/promkit-widgets/src/structured/yaml/config.rs b/promkit-widgets/src/structured/yaml/config.rs index b4221f46..69c3c459 100644 --- a/promkit-widgets/src/structured/yaml/config.rs +++ b/promkit-widgets/src/structured/yaml/config.rs @@ -103,6 +103,15 @@ pub struct Config { pub lines: Option, /// Whether to display stable one-based line numbers to the left of the content. pub show_line_numbers: bool, + /// Display immediate child counts next to collapsed and empty containers. + /// Disabled by default; changing this does not change folding state. + pub show_child_count: bool, + /// Style for child count annotations, such as ` (3 items)` or ` (2 keys)`. + #[cfg_attr( + feature = "serde", + serde(with = "termcfg::crossterm_config::content_style_serde") + )] + pub child_count_style: ContentStyle, } impl Default for Config { @@ -122,6 +131,8 @@ impl Default for Config { overflow_mode: OverflowMode::default(), lines: None, show_line_numbers: false, + show_child_count: false, + child_count_style: Default::default(), } } } @@ -156,15 +167,42 @@ impl Config { ContainerType::Object => self.map_style, ContainerType::Array => self.sequence_style, }; - StyledGraphemes::from(typ.empty_str()).apply_style(style) + self.with_child_count( + StyledGraphemes::from(typ.empty_str()).apply_style(style), + typ, + 0, + ) } - fn render_collapsed_marker(&self, typ: &ContainerType) -> StyledGraphemes { + fn render_collapsed_marker(&self, typ: &ContainerType, child_count: usize) -> StyledGraphemes { let style = match typ { ContainerType::Object => self.map_style, ContainerType::Array => self.sequence_style, }; - StyledGraphemes::from(typ.collapsed_preview()).apply_style(style) + self.with_child_count( + StyledGraphemes::from(typ.collapsed_preview()).apply_style(style), + typ, + child_count, + ) + } + + fn with_child_count( + &self, + marker: StyledGraphemes, + typ: &ContainerType, + count: usize, + ) -> StyledGraphemes { + if self.show_child_count { + [ + marker, + StyledGraphemes::from(typ.child_count_label(count)) + .apply_style(self.child_count_style), + ] + .into_iter() + .collect() + } else { + marker + } } fn render_node(&self, node: &YamlNode) -> Option { @@ -193,8 +231,9 @@ impl Config { ContainerNode::Open { typ, collapsed: true, + child_count, .. - } => Some(self.render_collapsed_marker(typ)), + } => Some(self.render_collapsed_marker(typ, *child_count)), ContainerNode::Open { collapsed: false, .. } => None, @@ -319,6 +358,124 @@ mod tests { mod config { use super::*; + mod render_content_rows { + use super::*; + use crate::structured::yaml::Document; + use promkit_core::crossterm::style::Color; + + #[test] + fn annotates_collapsed_and_empty_containers_without_changing_state() { + let config = Config { + show_child_count: true, + ..Default::default() + }; + for (input, expected) in [ + ("[]", "[] (0 items)"), + ("{}", "{} (0 keys)"), + ("[1]", "[…] (1 item)"), + ("[1, [2, 3]]", "[…] (2 items)"), + (r#"{"x": []}"#, "{…} (1 key)"), + (r#"{"a": [], "b": {}}"#, "{…} (2 keys)"), + ] { + let mut document = Document::from_str(input).unwrap(); + document.set_nodes_visibility(true); + let before = document.rows().to_vec(); + let visible = document.visible_rows(); + assert_eq!( + config.render_content_rows(&visible, 0)[0].to_string(), + expected + ); + assert_eq!( + Config::default().render_content_rows(&visible, 0)[0].to_string(), + expected.split(" (").next().unwrap() + ); + assert_eq!(document.rows(), before); + } + let document = Document::from_str("[1, 2]").unwrap(); + let rows = document.visible_rows(); + assert_eq!( + config.render_content_rows(&rows, 0), + Config::default().render_content_rows(&rows, 0) + ); + } + + #[test] + fn annotates_tagged_containers_on_sequence_mapping_lines() { + let mut document = + Document::from_str("- items: !Items [1, 2]\n empty: !Empty {}\n").unwrap(); + document.toggle_at(2); + let config = Config { + show_child_count: true, + ..Default::default() + }; + let lines = config + .render_content_rows(&document.visible_rows(), 0) + .iter() + .map(ToString::to_string) + .collect::>(); + assert_eq!( + lines, + [ + "- items: !Items […] (2 items)", + " empty: !Empty {} (0 keys)" + ] + ); + } + + #[test] + fn styles_annotations_and_respects_width_limits() { + let mut document = Document::from_str("[1, 2]").unwrap(); + document.set_nodes_visibility(true); + let rows = document.visible_rows(); + let style = ContentStyle { + foreground_color: Some(Color::Cyan), + ..Default::default() + }; + let mut config = Config { + show_child_count: true, + child_count_style: style, + ..Default::default() + }; + let expected: StyledGraphemes = [ + StyledGraphemes::from("[…]"), + StyledGraphemes::from(" (2 items)").apply_style(style), + ] + .into_iter() + .collect(); + assert_eq!( + config.render_content_rows(&rows, 0), + [expected.apply_attribute(config.active_item_attribute)] + ); + let truncated = config.render_terminal_rows(&rows, 7); + assert_eq!(truncated[0].to_string(), "[…] (2…"); + config.overflow_mode = OverflowMode::Wrap; + let wrapped = config.render_terminal_rows(&rows, 7); + assert!(wrapped.iter().all(|line| line.widths() <= 7)); + assert_eq!( + wrapped.iter().map(ToString::to_string).collect::(), + "[…] (2 items)" + ); + } + } + + #[cfg(feature = "serde")] + mod deserialize { + use super::*; + + #[test] + fn defaults_and_round_trips_count_settings() { + let config: Config = toml::from_str("indent = 2").unwrap(); + assert!(!config.show_child_count); + assert_eq!(config.child_count_style, ContentStyle::default()); + let config: Config = + toml::from_str("show_child_count = true\nchild_count_style = \"fg=cyan\"") + .unwrap(); + let restored: Config = toml::from_str(&toml::to_string(&config).unwrap()).unwrap(); + assert!(restored.show_child_count); + assert_eq!(restored.child_count_style, config.child_count_style); + } + } + mod render_terminal_rows { use super::*; use crate::structured::ContainerNode; @@ -334,6 +491,7 @@ mod tests { typ: ContainerType::Object, collapsed: false, close_index: 3, + child_count: 2, }), }, Row { diff --git a/promkit-widgets/src/structured/yaml/deserializer.rs b/promkit-widgets/src/structured/yaml/deserializer.rs index ba1d1b98..4dfc8a82 100644 --- a/promkit-widgets/src/structured/yaml/deserializer.rs +++ b/promkit-widgets/src/structured/yaml/deserializer.rs @@ -155,11 +155,12 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Array, collapsed: false, close_index: 0, + child_count: 0, }), }); path_key_kinds.push(path_key_kind); - let mut is_empty = true; + let mut child_count = 0; while sequence .next_element_seed(RowsSeed { rows: &mut *rows, @@ -171,10 +172,10 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { })? .is_some() { - is_empty = false; + child_count += 1; } - if is_empty { + if child_count == 0 { rows[open_index].node = YamlNode::Container(ContainerNode::Empty { typ: ContainerType::Array, }); @@ -197,6 +198,7 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Array, collapsed: false, close_index, + child_count, }); Ok(open_index) } @@ -222,6 +224,7 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Object, collapsed: false, close_index: 0, + child_count: 0, }), }); path_key_kinds.push(path_key_kind); @@ -266,6 +269,7 @@ impl<'de> Visitor<'de> for RowsVisitor<'_> { typ: ContainerType::Object, collapsed: false, close_index, + child_count: keys.len(), }); Ok(open_index) } diff --git a/promkit-widgets/src/structured/yaml/document.rs b/promkit-widgets/src/structured/yaml/document.rs index 45090d30..cb329476 100644 --- a/promkit-widgets/src/structured/yaml/document.rs +++ b/promkit-widgets/src/structured/yaml/document.rs @@ -93,6 +93,26 @@ impl Document { &self.rows } + /// Returns the number of immediate children of the container at a zero-based + /// index in [`Self::rows`], independently of its folding state. + /// + /// Empty containers return `Some(0)`. Closing rows return their opening + /// container's count. Scalars, document separators, and out-of-bounds indices + /// return `None`. YAML tags do not affect the count. + pub fn child_count(&self, row_index: usize) -> Option { + let container = TagAwareContainer::get(&self.rows.get(row_index)?.node)?; + match container { + ContainerNode::Empty { .. } => Some(0), + ContainerNode::Open { child_count, .. } => Some(*child_count), + ContainerNode::Close { open_index, .. } => { + match TagAwareContainer::get(&self.rows.get(*open_index)?.node)? { + ContainerNode::Open { child_count, .. } => Some(*child_count), + _ => None, + } + } + } + } + /// Extract rows from the current cursor position. pub fn extract_rows_from_current(&self, n: usize) -> Vec { self.rows.extract(self.position, n) @@ -509,6 +529,88 @@ second: [1, 2] Document::new(values.iter()) } + mod child_count { + use super::*; + + #[test] + fn counts_immediate_children_across_construction_and_folding() { + let input = "items: [1, {x: []}]\nempty: {}\nscalar: true\n---\n[null]"; + let expected = [ + Some(3), + Some(2), + None, + Some(1), + Some(0), + Some(1), + Some(2), + Some(0), + None, + Some(3), + None, + Some(1), + None, + Some(1), + ]; + for mut document in [ + via_value(input), + Document::from_str(input).unwrap(), + Document::from_reader(Cursor::new(input)).unwrap(), + ] { + let counts = |document: &Document| { + (0..document.rows().len()) + .map(|i| document.child_count(i)) + .collect::>() + }; + assert_eq!(counts(&document), expected); + assert_eq!(document.child_count(usize::MAX), None); + // Toggle from opening and closing rows, including nested containers. + for index in [1, 6, 3, 5, 0, 9] { + document.toggle_at(index); + assert_eq!(counts(&document), expected); + } + for collapsed in [true, false] { + document.set_nodes_visibility(collapsed); + assert_eq!(counts(&document), expected); + } + } + } + + #[test] + fn counts_tagged_containers_and_non_string_mapping_keys() { + let input = + "!Root {seq: !Items [!Map {a: [], b: 1}, {}], empty: !Empty [], scalar: !Scalar x}"; + for document in [ + via_value(input), + Document::from_str(input).unwrap(), + Document::from_reader(Cursor::new(input)).unwrap(), + ] { + let counts = (0..document.rows().len()) + .map(|i| document.child_count(i)) + .collect::>(); + assert_eq!( + counts, + [ + Some(3), + Some(2), + Some(2), + Some(0), + None, + Some(2), + Some(0), + Some(2), + Some(0), + None, + Some(3) + ] + ); + } + let input = "{1: a, true: b, null: c, ? [a, b] : d}"; + for document in [via_value(input), Document::from_str(input).unwrap()] { + assert_eq!(document.child_count(0), Some(4)); + } + } + } + mod from_str { use super::*; diff --git a/promkit-widgets/src/structured/yaml/yamlz.rs b/promkit-widgets/src/structured/yaml/yamlz.rs index 0c82c9cc..500094b5 100644 --- a/promkit-widgets/src/structured/yaml/yamlz.rs +++ b/promkit-widgets/src/structured/yaml/yamlz.rs @@ -241,6 +241,7 @@ impl RowOperation for Vec { typ, collapsed, close_index, + child_count, }) => { let new_collapsed = !collapsed; @@ -250,6 +251,7 @@ impl RowOperation for Vec { typ: typ.clone(), collapsed: new_collapsed, close_index, + child_count, }, ) .expect("container open node must be present"); @@ -278,6 +280,12 @@ impl RowOperation for Vec { open_index, }) => { let new_collapsed = !collapsed; + let Some(ContainerNode::Open { child_count, .. }) = + TagAwareContainer::get(&self[open_index].node) + else { + unreachable!("paired container must be open"); + }; + let child_count = *child_count; self[target].node = TagAwareContainer::replace( &self[target].node, @@ -295,6 +303,7 @@ impl RowOperation for Vec { typ, collapsed: new_collapsed, close_index: target, + child_count, }, ) .expect("container open node must be present"); @@ -310,7 +319,10 @@ impl RowOperation for Vec { let container = TagAwareContainer::get(&row.node).cloned(); match container { Some(ContainerNode::Open { - typ, close_index, .. + typ, + close_index, + child_count, + .. }) => { row.node = TagAwareContainer::replace( &row.node, @@ -318,6 +330,7 @@ impl RowOperation for Vec { typ, collapsed, close_index, + child_count, }, ) .expect("container open node must be present"); @@ -548,6 +561,7 @@ fn process_value( typ: ContainerType::Array, collapsed: false, close_index: 0, + child_count: seq.len(), }), }, path_key_kind, @@ -585,6 +599,7 @@ fn process_value( typ: ContainerType::Array, collapsed: false, close_index, + child_count: seq.len(), }); open_index @@ -617,6 +632,7 @@ fn process_value( typ: ContainerType::Object, collapsed: false, close_index: 0, + child_count: map.len(), }), }, path_key_kind, @@ -656,6 +672,7 @@ fn process_value( typ: ContainerType::Object, collapsed: false, close_index, + child_count: map.len(), }); open_index diff --git a/promkit-widgets/tests/jsonz/create_rows.rs b/promkit-widgets/tests/jsonz/create_rows.rs index e371abf6..1fb5ce0a 100644 --- a/promkit-widgets/tests/jsonz/create_rows.rs +++ b/promkit-widgets/tests/jsonz/create_rows.rs @@ -68,6 +68,7 @@ fn creates_rows_for_a_nested_object() { typ: ContainerType::Object, collapsed: false, close_index: 6, + child_count: 1, }), } ); @@ -81,6 +82,7 @@ fn creates_rows_for_a_nested_object() { typ: ContainerType::Object, collapsed: false, close_index: 5, + child_count: 1, }), } ); @@ -94,6 +96,7 @@ fn creates_rows_for_a_nested_object() { typ: ContainerType::Object, collapsed: false, close_index: 4, + child_count: 1, }), } ); @@ -175,6 +178,7 @@ fn creates_rows_for_a_nested_array() { typ: ContainerType::Array, collapsed: false, close_index: 8, + child_count: 1, }), } ); @@ -188,6 +192,7 @@ fn creates_rows_for_a_nested_array() { typ: ContainerType::Array, collapsed: false, close_index: 7, + child_count: 1, }), } ); @@ -201,6 +206,7 @@ fn creates_rows_for_a_nested_array() { typ: ContainerType::Array, collapsed: false, close_index: 6, + child_count: 3, }), } ); @@ -290,6 +296,7 @@ fn creates_rows_for_mixed_containers() { typ: ContainerType::Object, collapsed: false, close_index: 14, + child_count: 2, }), } ); @@ -303,6 +310,7 @@ fn creates_rows_for_mixed_containers() { typ: ContainerType::Array, collapsed: false, close_index: 10, + child_count: 2, }), } ); @@ -316,6 +324,7 @@ fn creates_rows_for_mixed_containers() { typ: ContainerType::Object, collapsed: false, close_index: 4, + child_count: 1, }), } ); @@ -351,6 +360,7 @@ fn creates_rows_for_mixed_containers() { typ: ContainerType::Array, collapsed: false, close_index: 9, + child_count: 3, }), } ); @@ -401,6 +411,7 @@ fn creates_rows_for_mixed_containers() { typ: ContainerType::Object, collapsed: false, close_index: 13, + child_count: 1, }), } ); diff --git a/promkit-widgets/tests/jsonz/extract.rs b/promkit-widgets/tests/jsonz/extract.rs index 9f955eb1..22641d4e 100644 --- a/promkit-widgets/tests/jsonz/extract.rs +++ b/promkit-widgets/tests/jsonz/extract.rs @@ -28,6 +28,7 @@ fn extracts_visible_rows() { typ: ContainerType::Object, collapsed: false, close_index: 4, + child_count: 3, }), } ); @@ -94,6 +95,7 @@ fn extracts_a_collapsed_open_container() { typ: ContainerType::Object, collapsed: true, close_index: 4, + child_count: 2, }), } ); @@ -150,6 +152,7 @@ fn extracts_a_nested_structure() { typ: ContainerType::Object, collapsed: false, close_index: 4, + child_count: 1, }), } ); @@ -261,6 +264,7 @@ fn extracts_complex_nested_collapsed_containers() { typ: ContainerType::Array, collapsed: false, close_index: 10, + child_count: 1, }), } ); diff --git a/promkit-widgets/tests/jsonz/multi_documents.rs b/promkit-widgets/tests/jsonz/multi_documents.rs index 4b17d7de..c77784c9 100644 --- a/promkit-widgets/tests/jsonz/multi_documents.rs +++ b/promkit-widgets/tests/jsonz/multi_documents.rs @@ -35,6 +35,7 @@ fn creates_rows_for_multiple_documents() { typ: ContainerType::Object, collapsed: false, close_index: 3, + child_count: 2, }), } ); @@ -76,6 +77,7 @@ fn creates_rows_for_multiple_documents() { typ: ContainerType::Object, collapsed: false, close_index: 7, + child_count: 2, }), } ); @@ -115,6 +117,7 @@ fn creates_rows_for_multiple_documents() { typ: ContainerType::Object, collapsed: true, close_index: 3, + child_count: 2, }) ); assert_eq!( @@ -169,6 +172,7 @@ fn creates_rows_for_mixed_documents() { typ: ContainerType::Object, collapsed: false, close_index: 6, + child_count: 1, }), } ); @@ -181,6 +185,7 @@ fn creates_rows_for_mixed_documents() { typ: ContainerType::Array, collapsed: false, close_index: 5, + child_count: 3, }), } ); @@ -206,6 +211,7 @@ fn creates_rows_for_mixed_documents() { typ: ContainerType::Array, collapsed: false, close_index: 14, + child_count: 2, }), } ); @@ -219,6 +225,7 @@ fn creates_rows_for_mixed_documents() { typ: ContainerType::Object, collapsed: false, close_index: 10, + child_count: 1, }), } ); @@ -234,6 +241,7 @@ fn creates_rows_for_mixed_documents() { typ: ContainerType::Object, collapsed: false, close_index: 10, + child_count: 1, }), } ); @@ -265,6 +273,7 @@ fn creates_rows_for_mixed_documents() { typ: ContainerType::Array, collapsed: true, close_index: 14, + child_count: 2, }) ); } diff --git a/promkit-widgets/tests/jsonz/toggle.rs b/promkit-widgets/tests/jsonz/toggle.rs index 5d80cb7c..e7162c02 100644 --- a/promkit-widgets/tests/jsonz/toggle.rs +++ b/promkit-widgets/tests/jsonz/toggle.rs @@ -29,7 +29,8 @@ fn collapses_open_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Object, collapsed: true, - close_index: 3 + close_index: 3, + child_count: 1, }) ); assert_eq!( @@ -48,7 +49,8 @@ fn collapses_open_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Array, collapsed: true, - close_index: 8 + close_index: 8, + child_count: 3, }) ); assert_eq!( @@ -67,7 +69,8 @@ fn collapses_open_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Object, collapsed: true, - close_index: 9 + close_index: 9, + child_count: 2, }) ); assert_eq!( @@ -85,7 +88,8 @@ fn collapses_open_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Object, collapsed: false, - close_index: 9 + close_index: 9, + child_count: 2, }) ); assert_eq!( @@ -103,7 +107,8 @@ fn collapses_open_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Array, collapsed: false, - close_index: 8 + close_index: 8, + child_count: 3, }) ); assert_eq!( @@ -121,7 +126,8 @@ fn collapses_open_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Object, collapsed: false, - close_index: 3 + close_index: 3, + child_count: 1, }) ); assert_eq!( @@ -161,7 +167,8 @@ fn expands_collapsed_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Object, collapsed: true, - close_index: 3 + close_index: 3, + child_count: 1, }) ); assert_eq!( @@ -180,7 +187,8 @@ fn expands_collapsed_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Array, collapsed: true, - close_index: 8 + close_index: 8, + child_count: 3, }) ); assert_eq!( @@ -199,7 +207,8 @@ fn expands_collapsed_containers() { JsonNode::Container(ContainerNode::Open { typ: ContainerType::Object, collapsed: true, - close_index: 9 + close_index: 9, + child_count: 2, }) ); assert_eq!( diff --git a/promkit-widgets/tests/yamlz/multi_documents.rs b/promkit-widgets/tests/yamlz/multi_documents.rs index f3784db0..cece8f6f 100644 --- a/promkit-widgets/tests/yamlz/multi_documents.rs +++ b/promkit-widgets/tests/yamlz/multi_documents.rs @@ -69,6 +69,7 @@ complex_keys: typ: ContainerType::Object, collapsed: false, close_index: 45, + child_count: 16, }), is_sequence_item: false, }, @@ -146,6 +147,7 @@ complex_keys: typ: ContainerType::Array, collapsed: false, close_index: 15, + child_count: 3, }), is_sequence_item: false, }, @@ -184,6 +186,7 @@ complex_keys: typ: ContainerType::Object, collapsed: false, close_index: 18, + child_count: 1, }), is_sequence_item: false, }, @@ -210,6 +213,7 @@ complex_keys: typ: ContainerType::Array, collapsed: false, close_index: 29, + child_count: 2, }), is_sequence_item: false, }, @@ -220,6 +224,7 @@ complex_keys: typ: ContainerType::Object, collapsed: false, close_index: 23, + child_count: 2, }), is_sequence_item: true, }, @@ -252,6 +257,7 @@ complex_keys: typ: ContainerType::Array, collapsed: false, close_index: 28, + child_count: 3, }), is_sequence_item: true, }, @@ -302,6 +308,7 @@ complex_keys: typ: ContainerType::Object, collapsed: false, close_index: 33, + child_count: 2, })), }, is_sequence_item: false, @@ -337,6 +344,7 @@ complex_keys: typ: ContainerType::Array, collapsed: false, close_index: 37, + child_count: 2, })), }, is_sequence_item: false, @@ -370,6 +378,7 @@ complex_keys: typ: ContainerType::Object, collapsed: false, close_index: 44, + child_count: 5, }), is_sequence_item: false, }, @@ -436,6 +445,7 @@ complex_keys: typ: ContainerType::Array, collapsed: false, close_index: 58, + child_count: 5, }), is_sequence_item: false, }, @@ -455,6 +465,7 @@ complex_keys: typ: ContainerType::Object, collapsed: false, close_index: 51, + child_count: 1, }), is_sequence_item: true, }, @@ -481,6 +492,7 @@ complex_keys: typ: ContainerType::Array, collapsed: false, close_index: 55, + child_count: 2, }), is_sequence_item: true, }, diff --git a/promkit/Cargo.toml b/promkit/Cargo.toml index 475cc40a..4ebcc129 100644 --- a/promkit/Cargo.toml +++ b/promkit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "promkit" -version = "0.16.0" +version = "0.17.0" authors = ["ynqa "] edition = "2021" description = "A toolkit for building your own interactive command-line tools" @@ -67,7 +67,7 @@ bitflags = { workspace = true, optional = true } crossterm = { workspace = true, optional = true } futures = { workspace = true, optional = true } promkit-core = { path = "../promkit-core", version = "=0.6.1", optional = true } -promkit-widgets = { path = "../promkit-widgets", version = "=0.9.0", default-features = false, optional = true } +promkit-widgets = { path = "../promkit-widgets", version = "=0.10.0", default-features = false, optional = true } tokio = { workspace = true, optional = true } [package.metadata.docs.rs] From 8a6248f0bad6a682ed874ad7e492b35f153699cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 14 Sep 2026 07:16:34 +0000 Subject: [PATCH 2/3] docs: align Concept.md version example Co-authored-by: ynqa <6745370+ynqa@users.noreply.github.com> --- Concept.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Concept.md b/Concept.md index 3be8a391..21acf6e7 100644 --- a/Concept.md +++ b/Concept.md @@ -154,7 +154,7 @@ Applications select the capabilities and widgets they need through Cargo features. The runtime is independent from the widget set: ```toml -promkit = { version = "0.16.0", features = [ +promkit = { version = "0.17.0", features = [ "runtime", "validate", "prefixsearch", From 433bc54dc9966309ef395d7f977675d81e6b46ca Mon Sep 17 00:00:00 2001 From: ynqa Date: Mon, 14 Sep 2026 16:26:54 +0900 Subject: [PATCH 3/3] docs: document changes from v0.15.0 to v0.17.0 --- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d657be0f..2615b1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > Patch versions are not included due to maintenance cost considerations. > More detailed version tracking may be described as the project matures. +## [0.17.0] - Unreleased + +### Added + +- Added `Document::child_count(row_index)` to JSON and YAML for querying immediate container sizes independently of folding; empty containers return `Some(0)`, closing rows return their opening container's count, and non-container rows return `None` +- Added optional child-count annotations for collapsed and empty JSON and YAML containers, such as `[…] (3 items)` and `{…} (2 keys)`, controlled by `show_child_count` and `child_count_style` + +### Changed + +- Added `child_count` to `ContainerNode::Open`; direct constructors and exhaustive patterns must account for the new field +- Extended JSON and YAML `Config` with child-count settings, disabled by default; existing serialized configurations retain the previous display, and pretty-printed exports omit annotations +- Updated `promkit-widgets` to `v0.10.0` + +## [0.16.0] - 2026-09-07 + +### Added + +- Added `Document::row_index_for_path` and `Document::move_to_path` to JSON and YAML for navigating jq-style paths, including object keys, array indices, quoted string keys, and YAML non-string scalar keys +- Added zero-based document indexes to path navigation for JSON Lines and YAML document streams; moving to a path expands folded ancestors while preserving unrelated folding state +- Added `Document::selected_path()` to retrieve the selected row's document index and jq-style path + +### Changed + +- Improved selected-path lookup with compact per-row parent and array-index metadata +- Updated workspace dependencies, `promkit-core` to `v0.6.1`, and `promkit-widgets` to `v0.9.0` + +## [0.15.0] - 2026-08-16 + +### Added + +- Added `WidgetLayout::height_policy` with `HeightPolicy::OrderedContent`, `FairContent`, and `FairFill` for content-based allocation, equal-share limits, and padded fill areas +- Added configurable text overflow through `text::config::OverflowMode`, supporting wrapping by default or truncation with an ellipsis +- Added terminal regression scenarios for renderer height allocation + +### Changed + +- Replaced the mutating `PrefixSearch::search` API with `PrefixSearch::query`, which returns an independently selectable `PrefixSearchResult` snapshot +- Changed prefix-search widget state from `State::prefix_search` to `State::result`, moving candidate selection and navigation to the result snapshot +- Extracted renderer height allocation into a dedicated module; fair-content panes stay within their equal share without redistributing unused rows +- Updated `promkit-core` to `v0.6.0` and `promkit-widgets` to `v0.8.0` + +### Removed + +- Removed the status widget and its `status` Cargo feature from `promkit` and `promkit-widgets` + ## [0.14.0] - 2026-07-28 ### Added @@ -158,7 +203,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 For versions prior to 0.10.0, please refer to the git history or GitHub releases. -[Unreleased]: https://github.com/ynqa/promkit/compare/v0.13.0...HEAD +[Unreleased]: https://github.com/ynqa/promkit/compare/v0.16.0...HEAD +[0.17.0]: https://github.com/ynqa/promkit/compare/v0.16.0...HEAD +[0.16.0]: https://github.com/ynqa/promkit/releases/tag/v0.16.0 +[0.15.0]: https://github.com/ynqa/promkit/releases/tag/v0.15.0 +[0.14.0]: https://github.com/ynqa/promkit/releases/tag/v0.14.0 [0.13.0]: https://github.com/ynqa/promkit/releases/tag/v0.13.0 [0.12.0]: https://github.com/ynqa/promkit/releases/tag/v0.12.0 [0.11.0]: https://github.com/ynqa/promkit/releases/tag/v0.11.0