diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 252a13e..227ac4c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
@@ -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 }}
diff --git a/README.md b/README.md
index 572d468..1e1b3c5 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/adapters/tui-input/Cargo.lock b/adapters/tui-input/Cargo.lock
new file mode 100644
index 0000000..1eef598
--- /dev/null
+++ b/adapters/tui-input/Cargo.lock
@@ -0,0 +1,129 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "interaction-check-tui-input"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tui-input",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tui-input"
+version = "0.15.5"
+source = "git+https://github.com/sayanarijit/tui-input?rev=7e615bb87db7a1611f3b00718788950defcbc2ee#7e615bb87db7a1611f3b00718788950defcbc2ee"
+dependencies = [
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/adapters/tui-input/Cargo.toml b/adapters/tui-input/Cargo.toml
new file mode 100644
index 0000000..eb5c3c3
--- /dev/null
+++ b/adapters/tui-input/Cargo.toml
@@ -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"
diff --git a/adapters/tui-input/README.md b/adapters/tui-input/README.md
new file mode 100644
index 0000000..59afc16
--- /dev/null
+++ b/adapters/tui-input/README.md
@@ -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.
diff --git a/adapters/tui-input/src/main.rs b/adapters/tui-input/src/main.rs
new file mode 100644
index 0000000..8062237
--- /dev/null
+++ b/adapters/tui-input/src/main.rs
@@ -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 = 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);
+ }
+}
diff --git a/adapters/tui-textarea/Cargo.lock b/adapters/tui-textarea/Cargo.lock
new file mode 100644
index 0000000..6ae1f0a
--- /dev/null
+++ b/adapters/tui-textarea/Cargo.lock
@@ -0,0 +1,391 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "compact_str"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "darling"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "either"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
+
+[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indoc"
+version = "2.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "instability"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20"
+dependencies = [
+ "darling",
+ "indoc",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "interaction-check-tui-textarea"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tui-textarea",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "lru"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ratatui"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "compact_str",
+ "indoc",
+ "instability",
+ "itertools",
+ "lru",
+ "paste",
+ "strum",
+ "unicode-segmentation",
+ "unicode-truncate",
+ "unicode-width 0.2.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tui-textarea"
+version = "0.7.0"
+source = "git+https://github.com/PerchAnton/tui-textarea?rev=1a0276461f31a57a74187d22f9d6513a9c539c37#1a0276461f31a57a74187d22f9d6513a9c539c37"
+dependencies = [
+ "ratatui",
+ "unicode-width 0.2.0",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-truncate"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
+dependencies = [
+ "itertools",
+ "unicode-segmentation",
+ "unicode-width 0.1.14",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/adapters/tui-textarea/Cargo.toml b/adapters/tui-textarea/Cargo.toml
new file mode 100644
index 0000000..25ed107
--- /dev/null
+++ b/adapters/tui-textarea/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "interaction-check-tui-textarea"
+version = "0.1.0"
+edition = "2024"
+publish = false
+license = "MIT OR Apache-2.0"
+
+[dependencies]
+tui-textarea = { git = "https://github.com/PerchAnton/tui-textarea", rev = "1a0276461f31a57a74187d22f9d6513a9c539c37", default-features = false, features = ["no-backend"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
diff --git a/adapters/tui-textarea/README.md b/adapters/tui-textarea/README.md
new file mode 100644
index 0000000..09b11fc
--- /dev/null
+++ b/adapters/tui-textarea/README.md
@@ -0,0 +1,18 @@
+# tui-textarea external adapter
+
+Experimental adapter maintained by PerchAnton; upstream adoption is not claimed.
+Source: [PerchAnton/tui-textarea](https://github.com/PerchAnton/tui-textarea/tree/1a0276461f31a57a74187d22f9d6513a9c539c37), pinned in Cargo.toml and Cargo.lock.
+
+TextArea receives native Input events, selection and deletion operations. Cursor is [row, codepoint column]. This tests the proposed fix in [upstream PR #126](https://github.com/rhysd/tui-textarea/pull/126), not upstream main: all three traces fail on upstream 4d18622eeac13b309e0ff6a55a46ac6706da68cf and pass on the pinned fix. The host bug is reported in issue #121. No renderer, terminal or clipboard is exercised.
+
+With Rust 1.99.1 and Python 3.10+, from the repository root:
+
+```sh
+cargo build --locked --manifest-path adapters/tui-textarea/Cargo.toml
+cargo clippy --locked --manifest-path adapters/tui-textarea/Cargo.toml --all-targets -- -D warnings
+python3 interaction_check.py scenarios/tui-textarea/*.json -- adapters/tui-textarea/target/debug/interaction-check-tui-textarea
+```
+
+Use `.exe` on Windows. Three scenarios / 14 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.
diff --git a/adapters/tui-textarea/src/main.rs b/adapters/tui-textarea/src/main.rs
new file mode 100644
index 0000000..e2d6c1e
--- /dev/null
+++ b/adapters/tui-textarea/src/main.rs
@@ -0,0 +1,77 @@
+use serde::Deserialize;
+use serde_json::json;
+use tui_textarea::{Input, Key, TextArea};
+#[path = "../../widget_protocol.rs"]
+mod protocol;
+
+#[derive(Deserialize)]
+#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
+enum Event {
+ New { lines: Vec },
+ Text { text: String },
+ Key { key: String },
+ SelectAll,
+ Delete { chars: Option },
+}
+
+fn main() {
+ let mut model: Option