termwright 0.2.0

Playwright-like automation framework for terminal TUI applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# Termwright

A Playwright-like automation framework for terminal TUI applications.

Termwright enables AI agents and integration tests to interact with and observe terminal user interfaces by wrapping applications in a pseudo-terminal (PTY).

## Features

- **PTY Wrapping**: Spawn and control any terminal application
- **Screen Reading**: Access text, colors, cursor position, and cell attributes
- **Wait Conditions**: Wait for text, regex patterns, screen stability, or process exit
- **Input Simulation**: Send keystrokes, special keys, and control sequences
- **Multiple Output Formats**: Plain text, JSON (for AI agents), and PNG screenshots
- **Box Detection**: Detect UI boundaries using box-drawing characters
- **Framework Agnostic**: Works with any TUI framework (ratatui, crossterm, ncurses, etc.)

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
termwright = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

Or install the CLI:

```bash
cargo install termwright
```

## Quick Start

### Library Usage

```rust
use termwright::prelude::*;

#[tokio::main]
async fn main() -> Result<()> {
    // Spawn a terminal application
    let term = Terminal::builder()
        .size(80, 24)
        .spawn("vim", &["test.txt"])
        .await?;

    // Wait for the application to be ready
    term.expect("VIM")
        .timeout(Duration::from_secs(5))
        .await?;

    // Send input
    term.send_key(Key::Char('i')).await?;
    term.type_str("Hello, world!").await?;
    term.send_key(Key::Escape).await?;

    // Query screen state
    let screen = term.screen().await;
    assert!(screen.contains("Hello, world!"));

    // Get structured output for AI agents
    println!("{}", screen.to_json()?);

    // Take a screenshot
    term.screenshot().await.save("vim.png")?;

    // Quit the application
    term.type_str(":q!").await?;
    term.enter().await?;
    term.wait_exit().await?;

    Ok(())
}
```

### CLI Usage

Capture terminal output as text:

```bash
termwright run -- ls -la
```

Take a screenshot of a TUI application:

```bash
termwright screenshot --wait-for "VIM" -o vim.png -- vim test.txt
```

Get JSON output for AI processing:

```bash
termwright run --format json -- htop
```

### Daemon Usage

The `daemon` subcommand runs a long-lived terminal session and exposes a local Unix socket for automation. This is useful when you want to keep an app running and interact with it incrementally (similar to how Playwright keeps a browser process alive).

Start a daemon (foreground; blocks until you close it):

```bash
termwright daemon -- vim test.txt
# prints a socket path like:
# /tmp/termwright-12345.sock
```

Start a daemon in the background (returns immediately):

```bash
SOCK=$(termwright daemon --background -- vim test.txt)
echo "$SOCK"
```

Stop a daemon (sends a `close` request):

```bash
printf '{"id":1,"method":"close","params":null}\n' | nc -U "$SOCK"
```

## Shell Scripting Quick Start

The daemon mode makes termwright ideal for shell-based E2E testing of TUI applications. Here's how to get started:

### Prerequisites

```bash
# Install termwright
cargo install termwright

# Install helper tools
brew install socat jq  # macOS
# or: sudo apt-get install socat jq  # Ubuntu/Debian
```

### Basic Pattern

```bash
#!/bin/bash
set -euo pipefail

# 1. Start the daemon with your TUI app
SOCK="/tmp/my-test-$$.sock"
termwright daemon --socket "$SOCK" --cols 80 --rows 24 -- ./my-tui-app &

# Wait for socket
while [ ! -S "$SOCK" ]; do sleep 0.1; done

# 2. Helper function to send commands
tw() {
    echo "$1" | socat - UNIX-CONNECT:"$SOCK"
}

# 3. Wait for app to be ready
tw '{"id":1,"method":"wait_for_text","params":{"text":"Welcome","timeout_ms":5000}}'

# 4. Interact with the app
tw '{"id":2,"method":"press","params":{"key":"Enter"}}'
tw '{"id":3,"method":"type","params":{"text":"hello world"}}'
tw '{"id":4,"method":"hotkey","params":{"ctrl":true,"ch":"s"}}'  # Ctrl+S

# 5. Read screen content
SCREEN=$(tw '{"id":5,"method":"screen","params":{"format":"text"}}' | jq -r '.result')
echo "$SCREEN"

# 6. Take a screenshot
RESULT=$(tw '{"id":6,"method":"screenshot","params":{}}')
echo "$RESULT" | jq -r '.result.png_base64' | base64 -d > screenshot.png

# 7. Clean up
tw '{"id":99,"method":"close","params":null}'
```

### Available Daemon Commands

| Method | Params | Description |
|--------|--------|-------------|
| `handshake` | `null` | Get daemon info (pid, version) |
| `screen` | `{"format":"text"\|"json"}` | Get current screen content |
| `screenshot` | `{}` | Get PNG screenshot as base64 |
| `press` | `{"key":"Enter"}` | Press a key (Enter, Escape, Tab, Up, Down, etc.) |
| `type` | `{"text":"..."}` | Type text |
| `hotkey` | `{"ctrl":true,"ch":"c"}` | Send Ctrl/Alt combinations |
| `wait_for_text` | `{"text":"...","timeout_ms":5000}` | Wait for text to appear |
| `wait_for_idle` | `{"idle_ms":500,"timeout_ms":5000}` | Wait for screen to stabilize |
| `status` | `null` | Check if process is still running |
| `close` | `null` | Terminate the daemon and child process |

### Reusable Test Library

For multiple tests, create a shared library (e.g., `lib.sh`):

```bash
#!/bin/bash
# lib.sh - Shared test helpers

REQUEST_ID=1

next_id() {
    local id=$REQUEST_ID
    REQUEST_ID=$((REQUEST_ID + 1))
    echo $id
}

# Send command with auto-incrementing ID
tw_auto() {
    local sock="$1"
    local method="$2"
    local params="${3:-null}"
    local id=$(next_id)
    echo "{\"id\":$id,\"method\":\"$method\",\"params\":$params}" | \
        socat - UNIX-CONNECT:"$sock"
}

# Convenience wrappers
get_screen() { tw_auto "$1" "screen" '{"format":"text"}' | jq -r '.result'; }
press()      { tw_auto "$1" "press" "{\"key\":\"$2\"}" > /dev/null; }
type_text()  { tw_auto "$1" "type" "{\"text\":\"$2\"}" > /dev/null; }
ctrl()       { tw_auto "$1" "hotkey" "{\"ctrl\":true,\"ch\":\"$2\"}" > /dev/null; }
wait_idle()  { tw_auto "$1" "wait_for_idle" "{\"idle_ms\":${2:-500},\"timeout_ms\":${3:-5000}}"; }

# Assert screen contains text
assert_contains() {
    local sock="$1" expected="$2" desc="${3:-contains '$2'}"
    if get_screen "$sock" | grep -q "$expected"; then
        echo "PASS: $desc"
        return 0
    else
        echo "FAIL: $desc"
        return 1
    fi
}
```

### Example Test Script

```bash
#!/bin/bash
# test_my_app.sh
source "$(dirname "$0")/lib.sh"

SOCK="/tmp/test-$$.sock"
termwright daemon --socket "$SOCK" -- ./my-app &
while [ ! -S "$SOCK" ]; do sleep 0.1; done

# Wait for app to initialize
wait_idle "$SOCK" > /dev/null

# Run tests
assert_contains "$SOCK" "Main Menu" "App shows main menu"

press "$SOCK" "Enter"
wait_idle "$SOCK" > /dev/null
assert_contains "$SOCK" "Settings" "Enter opens settings"

ctrl "$SOCK" "q"
echo "All tests passed!"
```

### Key Names Reference

Common key names for the `press` command:

- Navigation: `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`
- Actions: `Enter`, `Escape`, `Tab`, `Backspace`, `Delete`, `Insert`
- Function keys: `F1` through `F12`
- Characters: Any single character like `a`, `1`, `?`

## CLI Reference

### `termwright fonts`

List available font families on the system (helpful for selecting a monospace font for screenshots).

```
termwright fonts
```

### `termwright run`

Run a command and capture its output.

```
termwright run [OPTIONS] -- <COMMAND> [ARGS]...

Options:
  --cols <COLS>          Terminal width [default: 80]
  --rows <ROWS>          Terminal height [default: 24]
  --wait-for <TEXT>      Wait for this text to appear before capturing
  --delay <MS>           Delay in milliseconds before capturing [default: 500]
  --format <FORMAT>      Output format: text, json, json-compact [default: text]
  --timeout <SECS>       Timeout for wait conditions [default: 30]
```

### `termwright screenshot`

Take a PNG screenshot of a terminal application.

```
termwright screenshot [OPTIONS] -- <COMMAND> [ARGS]...

Options:
  --cols <COLS>          Terminal width [default: 80]
  --rows <ROWS>          Terminal height [default: 24]
  --wait-for <TEXT>      Wait for this text to appear before capturing
  --delay <MS>           Delay in milliseconds before capturing [default: 500]
  -o, --output <PATH>    Output file path (defaults to stdout)
  --font <NAME>          Font name for rendering
  --font-size <SIZE>     Font size in pixels [default: 14]
  --timeout <SECS>       Timeout for wait conditions [default: 30]
```

### `termwright run-steps`

Run a YAML or JSON steps file for end-to-end testing.

```
termwright run-steps [OPTIONS] <FILE>

Options:
  --connect <PATH>       Connect to an existing daemon socket instead of spawning
  --trace                Write a trace.json file in the artifacts directory
```

### `termwright exec`

Execute a single daemon request and print the response.

```
termwright exec --socket <PATH> --method <NAME> [--params <JSON>]
```

### `termwright hub`

Start or stop multiple daemon sessions for parallel agents.

```
termwright hub start --count <N> [--cols <COLS>] [--rows <ROWS>] [--output <FILE>] -- <COMMAND> [ARGS]...
termwright hub stop --socket <PATH>... [--input <FILE>]
```

### `termwright daemon`

Run a single TUI session and expose it over a Unix socket.

```
termwright daemon [OPTIONS] -- <COMMAND> [ARGS]...

Options:
  --cols <COLS>          Terminal width [default: 80]
  --rows <ROWS>          Terminal height [default: 24]
  --socket <PATH>        Unix socket path (defaults to a temp path)
  --background           Start daemon in the background
```

The command prints the socket path to stdout.

## Daemon User Guide

### Connecting from Rust

```rust
use std::time::Duration;
use termwright::prelude::*;

#[tokio::main]
async fn main() -> Result<()> {
    let client = DaemonClient::connect_unix("/tmp/termwright-12345.sock").await?;

    // Sanity check: talk to the server
    let info = client.handshake().await?;
    println!("daemon pid={}", info.pid);

    // Wait and read screen
    client.wait_for_text("VIM", Some(Duration::from_secs(5))).await?;
    println!("{}", client.screen_text().await?);

    // Keyboard
    client.press("Escape").await?;
    client.hotkey_ctrl('r').await?;
    client.r#type(":q!\n").await?;

    // Mouse (row/col are 0-based cell coordinates)
    client.mouse_move(10, 10).await?;
    client.mouse_click(10, 10, MouseButton::Left).await?;

    // Shut down daemon + child process
    client.close().await?;
    Ok(())
}
```

### Notes / Caveats

- The daemon is local-only: it listens on a Unix socket you control.
- Mouse events are best-effort: many TUIs ignore mouse input unless they explicitly enable mouse reporting.
- Coordinate system for `mouse_move`/`mouse_click` is `row`/`col` in terminal cells (0-based).

## API Overview

### Terminal

The main entry point for controlling terminal applications:

```rust
let term = Terminal::builder()
    .size(80, 24)
    .spawn("vim", &["file.txt"])
    .await?;

// Input
term.type_str("hello").await?;
term.send_key(Key::Enter).await?;
term.enter().await?;  // Shorthand for Enter key

// Screen access
let screen = term.screen().await;

// Wait conditions
term.expect("Ready").timeout(Duration::from_secs(5)).await?;
term.wait_exit().await?;

// Screenshots
term.screenshot().await.save("output.png")?;
```

### Screen

Query the terminal screen state:

```rust
let screen = term.screen().await;

// Text access
let text = screen.text();
let line = screen.line(0);
assert!(screen.contains("hello"));

// Cell-level access
let cell = screen.cell(0, 0);
println!("Char: {}, FG: {:?}, BG: {:?}", cell.char, cell.fg, cell.bg);

// Cursor position
let cursor = screen.cursor();
println!("Cursor at row={}, col={}", cursor.row, cursor.col);

// Region extraction
let region = screen.region(0..10, 0..5);

// Pattern matching
if let Some(pos) = screen.find_text("error") {
    println!("Found at row={}, col={}", pos.row, pos.col);
}

// Box detection (UI boundaries)
let boxes = screen.detect_boxes();

// Output formats
println!("{}", screen.to_json()?);        // Pretty JSON
println!("{}", screen.to_json_compact()?); // Compact JSON
```

### Keys

Available key types for input:

```rust
Key::Char('a')      // Regular characters
Key::Enter          // Enter/Return
Key::Tab            // Tab
Key::Escape         // Escape
Key::Backspace      // Backspace
Key::Up, Key::Down, Key::Left, Key::Right  // Arrow keys
Key::Home, Key::End
Key::PageUp, Key::PageDown
Key::Insert, Key::Delete
Key::F(1)..Key::F(12)  // Function keys
Key::Ctrl('c')      // Ctrl combinations
Key::Alt('x')       // Alt combinations
```

## Requirements

- Rust 1.85.0 or later (Edition 2024)
- macOS or Linux (Windows not supported)
- For screenshots: A monospace font (uses system fonts via font-kit)

## Use Cases

- **AI Agents**: Enable LLMs to observe and interact with terminal UIs via JSON output
- **Integration Testing**: Automated testing of TUI applications
- **Documentation**: Generate screenshots for documentation
- **Accessibility**: Extract text content from visual terminal applications

## License

MIT License - see [LICENSE](LICENSE) for details.