Skip to content

Commit 8266593

Browse files
committed
Add ptywright wait revisions and mode-aware special keys
Fix waitFor lost-wakeups with a revision counter, and encode SpecialKey presses from Ghostty's live terminal modes without changing press() raw-string behavior.
1 parent 74a8920 commit 8266593

10 files changed

Lines changed: 390 additions & 6 deletions

File tree

‎packages/ptywright/README.md‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ Writes text followed by `Enter`.
304304

305305
##### `session.press(key)`
306306

307-
Writes a key sequence, typically one of the exported key constants.
307+
Writes a raw key sequence. The exported `Key*` constants are those raw strings and stay pass-through.
308308

309309
```ts
310310
import { KeyArrowDown, KeyEnter } from "@onkernel/ptywright";
@@ -313,6 +313,16 @@ session.press(KeyArrowDown);
313313
session.press(KeyEnter);
314314
```
315315

316+
##### `session.pressKey(key)`
317+
318+
Encodes a `SpecialKey` from the live terminal modes (for example DECCKM application cursor keys) and writes those bytes.
319+
320+
```ts
321+
import { SpecialArrowUp } from "@onkernel/ptywright";
322+
323+
session.pressKey(SpecialArrowUp);
324+
```
325+
316326
#### Lifecycle and snapshots
317327

318328
##### `session.resize(cols, rows)`
@@ -438,6 +448,8 @@ The package exports common terminal key sequences as strings:
438448
- `KeyArrowLeft`
439449
- `KeyArrowRight`
440450

451+
Those `Key*` values are raw bytes for `press()` / `send()`. For mode-aware arrows and the other specials, use `SpecialArrowUp` and `session.pressKey(...)`.
452+
441453
You can also pass any raw sequence directly to `session.send(...)`.
442454

443455
## Examples

‎packages/ptywright/native/src/addon.cc‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,40 @@ Napi::Value Snapshot(const Napi::CallbackInfo &info) {
175175
return output;
176176
}
177177

178+
Napi::Value EncodeSpecialKey(const Napi::CallbackInfo &info) {
179+
Napi::Env env = info.Env();
180+
TerminalState *state = GetState(info);
181+
if (!EnsureOpen(env, state)) {
182+
return env.Undefined();
183+
}
184+
if (info.Length() < 1 || !info[0].IsString()) {
185+
Napi::TypeError::New(env, "encodeSpecialKey expects a key name").ThrowAsJavaScriptException();
186+
return env.Undefined();
187+
}
188+
189+
std::string name = info[0].As<Napi::String>().Utf8Value();
190+
uint8_t *bytes = nullptr;
191+
size_t len = 0;
192+
int result = ptywright_ghostty_terminal_encode_special_key(
193+
state->terminal,
194+
name.c_str(),
195+
&bytes,
196+
&len);
197+
if (result != 0) {
198+
ThrowGhosttyError(env, "ptywright_ghostty_terminal_encode_special_key", result);
199+
ptywright_ghostty_free_bytes(bytes);
200+
return env.Undefined();
201+
}
202+
if (bytes == nullptr || len == 0) {
203+
ptywright_ghostty_free_bytes(bytes);
204+
return env.Undefined();
205+
}
206+
207+
Napi::Buffer<uint8_t> output = Napi::Buffer<uint8_t>::Copy(env, bytes, len);
208+
ptywright_ghostty_free_bytes(bytes);
209+
return output;
210+
}
211+
178212
Napi::Value Dispose(const Napi::CallbackInfo &info) {
179213
TerminalState *state = GetState(info);
180214
if (state && state->terminal) {
@@ -211,6 +245,7 @@ Napi::Value CreateTerminal(const Napi::CallbackInfo &info) {
211245
object.Set("feed", Napi::Function::New(env, Feed, "feed", state));
212246
object.Set("resize", Napi::Function::New(env, Resize, "resize", state));
213247
object.Set("snapshot", Napi::Function::New(env, Snapshot, "snapshot", state));
248+
object.Set("encodeSpecialKey", Napi::Function::New(env, EncodeSpecialKey, "encodeSpecialKey", state));
214249
object.Set("dispose", Napi::Function::New(env, Dispose, "dispose", state));
215250
return object;
216251
}

‎packages/ptywright/native/src/ghostty_bridge.c‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum {
1717

1818
struct PtywrightGhosttyTerminal {
1919
GhosttyTerminal handle;
20+
GhosttyKeyEncoder encoder;
2021
uint8_t *reply_buf;
2122
size_t reply_len;
2223
size_t reply_cap;
@@ -27,6 +28,44 @@ struct PtywrightGhosttyTerminal {
2728
int reply_error;
2829
};
2930

31+
static int ptywright_ghostty_special_key(const char *name, GhosttyKey *out_key,
32+
GhosttyMods *out_mods) {
33+
if (name == NULL || out_key == NULL || out_mods == NULL) {
34+
return GHOSTTY_INVALID_VALUE;
35+
}
36+
37+
*out_mods = 0;
38+
if (strcmp(name, "arrow_up") == 0) {
39+
*out_key = GHOSTTY_KEY_ARROW_UP;
40+
} else if (strcmp(name, "arrow_down") == 0) {
41+
*out_key = GHOSTTY_KEY_ARROW_DOWN;
42+
} else if (strcmp(name, "arrow_left") == 0) {
43+
*out_key = GHOSTTY_KEY_ARROW_LEFT;
44+
} else if (strcmp(name, "arrow_right") == 0) {
45+
*out_key = GHOSTTY_KEY_ARROW_RIGHT;
46+
} else if (strcmp(name, "home") == 0) {
47+
*out_key = GHOSTTY_KEY_HOME;
48+
} else if (strcmp(name, "end") == 0) {
49+
*out_key = GHOSTTY_KEY_END;
50+
} else if (strcmp(name, "page_up") == 0) {
51+
*out_key = GHOSTTY_KEY_PAGE_UP;
52+
} else if (strcmp(name, "page_down") == 0) {
53+
*out_key = GHOSTTY_KEY_PAGE_DOWN;
54+
} else if (strcmp(name, "insert") == 0) {
55+
*out_key = GHOSTTY_KEY_INSERT;
56+
} else if (strcmp(name, "delete") == 0) {
57+
*out_key = GHOSTTY_KEY_DELETE;
58+
} else if (strcmp(name, "escape") == 0) {
59+
*out_key = GHOSTTY_KEY_ESCAPE;
60+
} else if (strcmp(name, "backtab") == 0) {
61+
*out_key = GHOSTTY_KEY_TAB;
62+
*out_mods = GHOSTTY_MODS_SHIFT;
63+
} else {
64+
return GHOSTTY_INVALID_VALUE;
65+
}
66+
return GHOSTTY_SUCCESS;
67+
}
68+
3069
static int ptywright_ghostty_reserve_buffer(uint8_t **buf,
3170
size_t *cap,
3271
size_t needed) {
@@ -218,6 +257,13 @@ int ptywright_ghostty_terminal_create(uint16_t cols, uint16_t rows, size_t scrol
218257
return result;
219258
}
220259

260+
result = ghostty_key_encoder_new(NULL, &terminal->encoder);
261+
if (result != GHOSTTY_SUCCESS) {
262+
ghostty_terminal_free(terminal->handle);
263+
free(terminal);
264+
return result;
265+
}
266+
221267
*out_terminal = terminal;
222268
return GHOSTTY_SUCCESS;
223269
}
@@ -263,6 +309,78 @@ int ptywright_ghostty_terminal_resize(PtywrightGhosttyTerminal *terminal,
263309
return ghostty_terminal_resize(terminal->handle, cols, rows, kCellWidthPx, kCellHeightPx);
264310
}
265311

312+
int ptywright_ghostty_terminal_encode_special_key(PtywrightGhosttyTerminal *terminal,
313+
const char *name,
314+
uint8_t **out_bytes,
315+
size_t *out_len) {
316+
if (terminal == NULL || terminal->handle == NULL || terminal->encoder == NULL ||
317+
name == NULL || out_bytes == NULL || out_len == NULL) {
318+
return GHOSTTY_INVALID_VALUE;
319+
}
320+
321+
*out_bytes = NULL;
322+
*out_len = 0;
323+
324+
GhosttyKey key = GHOSTTY_KEY_UNIDENTIFIED;
325+
GhosttyMods mods = 0;
326+
int mapped = ptywright_ghostty_special_key(name, &key, &mods);
327+
if (mapped != GHOSTTY_SUCCESS) {
328+
return mapped;
329+
}
330+
331+
GhosttyKeyEvent event = NULL;
332+
GhosttyResult result = ghostty_key_event_new(NULL, &event);
333+
if (result != GHOSTTY_SUCCESS) {
334+
return result;
335+
}
336+
337+
ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS);
338+
ghostty_key_event_set_key(event, key);
339+
ghostty_key_event_set_mods(event, mods);
340+
ghostty_key_encoder_setopt_from_terminal(terminal->encoder, terminal->handle);
341+
342+
char stack[128];
343+
size_t written = 0;
344+
result = ghostty_key_encoder_encode(terminal->encoder, event, stack, sizeof(stack), &written);
345+
if (result == GHOSTTY_OUT_OF_SPACE) {
346+
char *dynamic = malloc(written > 0 ? written : 1);
347+
if (dynamic == NULL) {
348+
ghostty_key_event_free(event);
349+
return GHOSTTY_OUT_OF_MEMORY;
350+
}
351+
result = ghostty_key_encoder_encode(terminal->encoder, event, dynamic, written, &written);
352+
if (result != GHOSTTY_SUCCESS) {
353+
free(dynamic);
354+
ghostty_key_event_free(event);
355+
return result;
356+
}
357+
*out_bytes = (uint8_t *)dynamic;
358+
*out_len = written;
359+
ghostty_key_event_free(event);
360+
return GHOSTTY_SUCCESS;
361+
}
362+
if (result != GHOSTTY_SUCCESS) {
363+
ghostty_key_event_free(event);
364+
return result;
365+
}
366+
367+
if (written == 0) {
368+
ghostty_key_event_free(event);
369+
return GHOSTTY_SUCCESS;
370+
}
371+
372+
uint8_t *copy = malloc(written);
373+
if (copy == NULL) {
374+
ghostty_key_event_free(event);
375+
return GHOSTTY_OUT_OF_MEMORY;
376+
}
377+
memcpy(copy, stack, written);
378+
*out_bytes = copy;
379+
*out_len = written;
380+
ghostty_key_event_free(event);
381+
return GHOSTTY_SUCCESS;
382+
}
383+
266384
int ptywright_ghostty_terminal_snapshot(PtywrightGhosttyTerminal *terminal,
267385
int trim, int unwrap,
268386
PtywrightGhosttySnapshot *out_snapshot) {
@@ -416,6 +534,10 @@ void ptywright_ghostty_terminal_destroy(PtywrightGhosttyTerminal *terminal) {
416534
return;
417535
}
418536

537+
if (terminal->encoder != NULL) {
538+
ghostty_key_encoder_free(terminal->encoder);
539+
terminal->encoder = NULL;
540+
}
419541
if (terminal->handle != NULL) {
420542
ghostty_terminal_free(terminal->handle);
421543
terminal->handle = NULL;

‎packages/ptywright/native/src/ghostty_bridge.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ int ptywright_ghostty_terminal_feed(PtywrightGhosttyTerminal *terminal,
3636
int ptywright_ghostty_terminal_resize(PtywrightGhosttyTerminal *terminal,
3737
uint16_t cols, uint16_t rows);
3838

39+
int ptywright_ghostty_terminal_encode_special_key(PtywrightGhosttyTerminal *terminal,
40+
const char *name,
41+
uint8_t **out_bytes,
42+
size_t *out_len);
43+
3944
int ptywright_ghostty_terminal_snapshot(PtywrightGhosttyTerminal *terminal,
4045
int trim, int unwrap,
4146
PtywrightGhosttySnapshot *out_snapshot);

‎packages/ptywright/src/keys.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,41 @@ export const KeyArrowUp: Key = "\x1b[A";
1717
export const KeyArrowDown: Key = "\x1b[B";
1818
export const KeyArrowLeft: Key = "\x1b[D";
1919
export const KeyArrowRight: Key = "\x1b[C";
20+
21+
export const SPECIAL_KEY_KIND = "special" as const;
22+
23+
export type SpecialKeyName =
24+
| "arrow_up"
25+
| "arrow_down"
26+
| "arrow_left"
27+
| "arrow_right"
28+
| "home"
29+
| "end"
30+
| "page_up"
31+
| "page_down"
32+
| "insert"
33+
| "delete"
34+
| "escape"
35+
| "backtab";
36+
37+
export interface SpecialKey {
38+
readonly kind: typeof SPECIAL_KEY_KIND;
39+
readonly name: SpecialKeyName;
40+
}
41+
42+
export function specialKey(name: SpecialKeyName): SpecialKey {
43+
return { kind: SPECIAL_KEY_KIND, name };
44+
}
45+
46+
export const SpecialArrowUp = specialKey("arrow_up");
47+
export const SpecialArrowDown = specialKey("arrow_down");
48+
export const SpecialArrowLeft = specialKey("arrow_left");
49+
export const SpecialArrowRight = specialKey("arrow_right");
50+
export const SpecialHome = specialKey("home");
51+
export const SpecialEnd = specialKey("end");
52+
export const SpecialPageUp = specialKey("page_up");
53+
export const SpecialPageDown = specialKey("page_down");
54+
export const SpecialInsert = specialKey("insert");
55+
export const SpecialDelete = specialKey("delete");
56+
export const SpecialEscape = specialKey("escape");
57+
export const SpecialBacktab = specialKey("backtab");

‎packages/ptywright/src/native-loader.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface NativeTerminalHandle {
2626
feed(data: string | Uint8Array): Uint8Array | undefined;
2727
resize(cols: number, rows: number): void;
2828
snapshot(options?: { trim?: boolean; unwrap?: boolean }): NativeSnapshot;
29+
encodeSpecialKey(name: string): Uint8Array | undefined;
2930
dispose(): void;
3031
}
3132

0 commit comments

Comments
 (0)