Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,38 @@ 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.

## [Unreleased]
## [0.14.0] - 2026-07-28

### Added

- Added configurable `TerminalSession` management for raw mode, alternate screen, cursor visibility, and mouse capture
- Added a prefix-search widget for composing filtered selection interfaces
- Added mouse-click support for text input, selection lists, checkboxes, trees, JSON, and YAML
- Added multiline text editing with continuation indentation and block submission, together with a REPL example
- Added a CSV table widget for navigating large files vertically and horizontally by display cell
- Added stable line numbers and viewport-aware projection to the JSON and YAML widgets
- Expanded terminal rendering regression coverage with [termharness](https://github.com/ynqa/termharness), running applications in a pseudo-terminal, replaying keyboard input and window resizing, and comparing the resulting screen with expected output
- Added renderer-managed widget layout and screen-to-widget hit testing

### Changed

- Shifted from framework-owned prompt presets to application-owned composition using reusable widget states and the optional `Prompt` runtime
- Reorganized Cargo features into runtime, terminal lifecycle, capabilities, and individual widgets
- Changed widgets to produce `CreatedGraphemes` with layout hints and logical cursor positions
- Improved renderer layout performance by extracting a terminal-independent layout engine and reducing layout cloning
- Improved structured JSON and YAML loading by parsing directly into rows and bounding projection to the visible viewport
- Updated `promkit-core` to `v0.5.0` and `promkit-widgets` to `v0.7.0`

### Removed

- Removed the built-in prompt presets and the generic widget cursor abstraction

### Fixed

- Preserved preceding terminal output and cleared stale rows correctly across resize and redraw cycles
- Preserved overwide graphemes during wrapping
- Stabilized JSON and YAML viewports, line numbers, root navigation, and keyed-container toggling
- Corrected CSV mouse scrolling behavior on macOS

## [0.13.0] - 2026-07-24

Expand Down
16 changes: 13 additions & 3 deletions Concept.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ The resulting development loop is:
4. Application-specific orchestration remains in the application, while
examples document useful compositions.

The renderer defines three vertical sizing policies. `HeightPolicy::OrderedContent`
uses the wrapped content height, optionally capped by `max_height`, and allocates
available rows in widget order. `HeightPolicy::FairFill` shares the height
remaining after ordered-content items equally with other fair-fill items and
pads content to preserve the allocated area. `HeightPolicy::FairContent` joins
the same initial fair allocation but stops at its content height without
redistributing unused rows. Keeping these policies explicit avoids making
individual widgets reproduce container-level allocation policy.

For this reason, the `promkit` crate no longer owns preset implementations.
It provides an optional `Prompt` lifecycle runtime, capabilities, and a widget
facade for applications that find them useful, but it is not intended to replace
Expand Down Expand Up @@ -145,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.14.0", features = [
promkit = { version = "0.15.0", features = [
"runtime",
"validate",
"prefixsearch",
Expand All @@ -171,7 +180,8 @@ that push grapheme changes directly to a shared renderer.
## Quality Strategy for Rendering Behavior

Ensuring consistent rendering behavior across terminal environments is a key focus.
The [readline terminal scenarios](./tests/readline/tests/scenarios) use
The [terminal scenarios](./tests) use
[`termharness`](https://github.com/ynqa/termharness) to verify wrapping, resizing,
cursor movement, and viewport behavior against recorded screen expectations.
cursor movement, viewport behavior, and height allocation against recorded
screen expectations.
This keeps terminal behavior predictable while the rendering internals evolve.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Put the package in your `Cargo.toml`.

```toml
[dependencies]
promkit = { version = "0.14.0", features = ["runtime", "texteditor"] }
promkit = { version = "0.15.0", features = ["runtime", "texteditor"] }
```

## Features
Expand All @@ -30,7 +30,7 @@ promkit = { version = "0.14.0", features = ["runtime", "texteditor"] }
resizing, and screen-to-widget hit testing.
- **Reusable widget states** — [`promkit-widgets`](./promkit-widgets/) provides
text editing and display, list and checkbox selection, prefix search,
spinners and status output, trees, JSON and YAML documents, and CSV tables.
spinners, trees, JSON and YAML documents, and CSV tables.
- **Efficient large-content projection** — JSON, YAML, and table widgets can
project only the visible terminal viewport instead of rebuilding all content
on every redraw.
Expand Down
13 changes: 7 additions & 6 deletions examples/readline/src/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ fn click(column: u16, row: u16, ctx: &mut Readline) {
}

fn select_suggestion(index: usize, ctx: &mut Readline) {
if ctx.suggestions.prefix_search.move_to(index) {
if ctx.suggestions.result.move_to(index) {
ctx.focus = Focus::Suggestion;
}
}

fn apply_suggestion(ctx: &mut Readline) {
let Some(suggestion) = ctx.suggestions.prefix_search.get() else {
let Some(suggestion) = ctx.suggestions.result.get() else {
dismiss_suggestions(ctx);
return;
};
Expand All @@ -89,7 +89,7 @@ fn apply_suggestion(ctx: &mut Readline) {
}

fn dismiss_suggestions(ctx: &mut Readline) {
ctx.suggestions.prefix_search.clear();
ctx.suggestions.result.clear();
ctx.focus = Focus::Readline;
}

Expand Down Expand Up @@ -131,7 +131,8 @@ pub async fn readline(event: &Event, ctx: &mut Readline) -> anyhow::Result<Signa
state: KeyEventState::NONE,
}) => {
let text = ctx.readline.texteditor.text_without_cursor().to_string();
if ctx.suggestions.prefix_search.search(text) {
ctx.suggestions.result = ctx.prefix_search.query(text);
if !ctx.suggestions.result.is_empty() {
ctx.focus = Focus::Suggestion;
} else {
dismiss_suggestions(ctx);
Expand Down Expand Up @@ -279,15 +280,15 @@ pub async fn suggestion(event: &Event, ctx: &mut Readline) -> anyhow::Result<Sig
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => {
ctx.suggestions.prefix_search.forward();
ctx.suggestions.result.forward();
}
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => {
ctx.suggestions.prefix_search.backward();
ctx.suggestions.result.backward();
}
_ => {
let before = ctx.readline.texteditor.text_without_cursor();
Expand Down
5 changes: 4 additions & 1 deletion examples/readline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub struct Readline {
pub focus: Focus,
pub title: text::State,
pub readline: text_editor::State,
pub prefix_search: PrefixSearch,
pub suggestions: prefix_search::State,
pub validator: Option<ValidatorManager<str>>,
pub error_message: text::State,
Expand Down Expand Up @@ -88,8 +89,9 @@ impl Default for Readline {
lines: Default::default(),
},
},
prefix_search: PrefixSearch::default(),
suggestions: prefix_search::State {
prefix_search: PrefixSearch::default(),
result: Default::default(),
config: prefix_search::Config {
cursor: String::from("❯ "),
active_item_style: Some(ContentStyle {
Expand All @@ -114,6 +116,7 @@ impl Default for Readline {
..Default::default()
}),
lines: None,
..Default::default()
},
},
}
Expand Down
3 changes: 1 addition & 2 deletions examples/readline/src/readline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ use readline::Readline;
async fn main() -> anyhow::Result<()> {
let mut prompt = Readline::default();
prompt.title.text = Text::from("Hi!");
prompt.suggestions.prefix_search =
PrefixSearch::from_iter(["apple", "applet", "application", "banana"]);
prompt.prefix_search = PrefixSearch::from_iter(["apple", "applet", "application", "banana"]);
prompt.validator = Some(ValidatorManager::new(
|text| text.len() > 10,
|text| format!("Length must be over 10 but got {}", text.len()),
Expand Down
2 changes: 1 addition & 1 deletion promkit-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "promkit-core"
version = "0.5.0"
version = "0.6.0"
edition = "2024"
authors = ["ynqa <un.pensiero.vano@gmail.com>"]
description = "Core library for promkit"
Expand Down
1 change: 1 addition & 0 deletions promkit-core/benches/renderer_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ fn created_fixture(
layout: WidgetLayout {
max_height: None,
width_mode,
..Default::default()
},
cursor: Some(ContentPosition {
row: cursor_row,
Expand Down
4 changes: 2 additions & 2 deletions promkit-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ pub mod terminal;
pub mod widget;

pub use widget::{
ContentPosition, CreatedGraphemes, ScreenPosition, ViewportChange, VisualPosition, Widget,
WidgetLayout, WidgetPosition, WidgetViewport, WidthMode,
ContentPosition, CreatedGraphemes, HeightPolicy, ScreenPosition, ViewportChange,
VisualPosition, Widget, WidgetLayout, WidgetPosition, WidgetViewport, WidthMode,
};
10 changes: 7 additions & 3 deletions promkit-core/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
//! include its cursor, and delegates the resulting visible rows to the terminal.
//!
//! Empty items and items with `max_height == Some(0)` do not occupy space.
//! Remaining items are allocated in key order while reserving at least one row
//! for every later non-empty item. A terminal that cannot provide one row per
//! non-empty item produces an error.
//! Ordered-content items are allocated in key order while reserving at least one
//! row for every non-empty item. The remaining height is shared equally between
//! items using [`crate::HeightPolicy::FairContent`] or
//! [`crate::HeightPolicy::FairFill`]. Fair-content items stop at their content
//! height without redistributing their unused share, while fair-fill items pad
//! their content to preserve the allocated area. A terminal that cannot provide
//! one row per non-empty item produces an error.
//!
//! A successful render saves a layout snapshot. [`Renderer::hit_test`] and
//! [`Renderer::screen_position`] always use that snapshot, so event handling maps
Expand Down
Loading
Loading