Skip to content
Closed
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
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-latest]
adapter: [bubbles, huh]
adapter: [bubbles, huh, tview]
defaults:
run:
shell: bash
Expand Down Expand Up @@ -70,13 +70,17 @@ jobs:
widgets-rust:
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
adapter: [cursive, tui-input, tui-textarea]
env:
CARGO_PROFILE_DEV_DEBUG: '0'
CARGO_INCREMENTAL: '0'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- run: rustup toolchain install 1.99.1 --profile minimal --component rustfmt,clippy
- run: cargo +1.99.1 fmt --check --manifest-path adapters/cursive/Cargo.toml
- run: cargo +1.99.1 build --locked --manifest-path adapters/cursive/Cargo.toml
- run: cargo +1.99.1 clippy --locked --manifest-path adapters/cursive/Cargo.toml --all-targets -- -D warnings
- run: python3 interaction_check.py scenarios/cursive/*.json -- adapters/cursive/target/debug/interaction-check-cursive
- run: cargo +1.99.1 fmt --check --manifest-path adapters/${{ matrix.adapter }}/Cargo.toml
- run: cargo +1.99.1 build --locked --manifest-path adapters/${{ matrix.adapter }}/Cargo.toml
- run: cargo +1.99.1 clippy --locked --manifest-path adapters/${{ matrix.adapter }}/Cargo.toml --all-targets -- -D warnings
- run: python3 interaction_check.py scenarios/${{ matrix.adapter }}/*.json -- adapters/${{ matrix.adapter }}/target/debug/interaction-check-${{ matrix.adapter }}
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ See [reproduction instructions](https://github.com/PerchAnton/pdfcraft/blob/535d
| [Bubbles](adapters/bubbles/README.md) | Real textinput update/layout, Unicode rune cursor, focus and resize | 3 / 20 steps |
| [Huh](adapters/huh/README.md) | Real two-field form, validation, Tab/Shift+Tab, submit and abort | 3 / 19 steps |
| [Cursive](adapters/cursive/README.md) | Real EditView, UTF-8 byte cursor, graphemes, layout and callbacks | 3 / 17 steps |
| [tview](adapters/tview/README.md) | Real InputField callbacks and tcell SimulationScreen | 3 / 15 steps |
| [tui-input](adapters/tui-input/README.md) | Native requests, grapheme editing, codepoint and display-column positions | 3 / 16 steps |
| [tui-textarea](adapters/tui-textarea/README.md) | Native selection/deletion/Undo/Redo; proposed #121 fix | 3 / 14 steps |

These are external prototypes, not accepted upstream integrations. Preliminary discussions
These are external prototypes, not accepted upstream integrations. The tui-textarea adapter
pins our proposed [fix #126](https://github.com/rhysd/tui-textarea/pull/126); all three
traces fail on upstream main and pass on the patch. It is not a claim that upstream main is fixed. Preliminary discussions
are [Bubbles #1076](https://github.com/charmbracelet/bubbles/discussions/1076) and
[Huh #838](https://github.com/charmbracelet/huh/discussions/838). Each adapter has its own
pinned dependencies, documented boundary and replay instructions. No runtime integration
Expand Down
129 changes: 129 additions & 0 deletions adapters/tui-input/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions adapters/tui-input/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "interaction-check-tui-input"
version = "0.1.0"
edition = "2024"
publish = false
license = "MIT OR Apache-2.0"

[dependencies]
tui-input = { git = "https://github.com/sayanarijit/tui-input", rev = "7e615bb87db7a1611f3b00718788950defcbc2ee", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
18 changes: 18 additions & 0 deletions adapters/tui-input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# tui-input external adapter

Experimental adapter maintained by PerchAnton; upstream adoption is not claimed.
Source: [sayanarijit/tui-input](https://github.com/sayanarijit/tui-input/tree/7e615bb87db7a1611f3b00718788950defcbc2ee), pinned in Cargo.toml and Cargo.lock.

Input::handle receives native requests. Cursor positions count Unicode codepoints, movements/deletions use graphemes, and visual_cursor/visual_scroll report display columns. No terminal backend is initialized.

With Rust 1.99.1 and Python 3.10+, from the repository root:

```sh
cargo build --locked --manifest-path adapters/tui-input/Cargo.toml
cargo clippy --locked --manifest-path adapters/tui-input/Cargo.toml --all-targets -- -D warnings
python3 interaction_check.py scenarios/tui-input/*.json -- adapters/tui-input/target/debug/interaction-check-tui-input
```

Use `.exe` on Windows. Three scenarios / 16 steps; every scenario starts a fresh adapter. Expected values remain in the runner, never sent to the adapter. Input records are bounded to 64 KiB. The adapter translates events and observes state; it does not reimplement the widget.

The upstream crate is MIT licensed. This adapter is MIT OR Apache-2.0. No upstream source is copied into the adapter.
74 changes: 74 additions & 0 deletions adapters/tui-input/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use serde::Deserialize;
use serde_json::json;
use tui_input::{Input, InputRequest};
#[path = "../../widget_protocol.rs"]
mod protocol;

#[derive(Deserialize)]
#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
enum Event {
New { text: String, width: usize },
Text { text: String },
Key { key: String },
Resize { width: usize },
}

fn main() {
let mut model: Option<Input> = None;
let mut width = 1;
let result = protocol::serve(|event| {
match event {
Event::New { text, width: w } => {
if model.is_some() {
return Err("new is only allowed once".into());
}
if !(1..=1000).contains(&w) {
return Err("width must be 1..1000".into());
}
model = Some(Input::new(text));
width = w;
}
event => {
let input = model.as_mut().ok_or("first event must be new")?;
match event {
Event::Text { text } => {
for c in text.chars() {
input.handle(InputRequest::InsertChar(c));
}
}
Event::Key { key } => {
let req = match key.as_str() {
"left" => InputRequest::GoToPrevChar,
"right" => InputRequest::GoToNextChar,
"home" => InputRequest::GoToStart,
"end" => InputRequest::GoToEnd,
"backspace" => InputRequest::DeletePrevChar,
"delete" => InputRequest::DeleteNextChar,
"ctrl+w" => InputRequest::DeletePrevWord,
"ctrl+k" => InputRequest::DeleteTillEnd,
"ctrl+y" => InputRequest::Yank,
_ => return Err(format!("unsupported key {key}")),
};
input.handle(req);
}
Event::Resize { width: w } => {
if !(1..=1000).contains(&w) {
return Err("width must be 1..1000".into());
}
width = w;
}
Event::New { .. } => return Err("unexpected new".into()),
}
}
}
let input = model.as_ref().ok_or("model unavailable")?;
Ok(
json!({"value":input.value(),"position_codepoints":input.cursor(),
"visual_cursor":input.visual_cursor(),"scroll_columns":input.visual_scroll(width),"width":width}),
)
});
if let Err(err) = result {
eprintln!("{err}");
std::process::exit(1);
}
}
Loading
Loading