ink-native 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -286,8 +286,9 @@ Event emitter for window lifecycle and input events.
286
286
 
287
287
  #### Events
288
288
 
289
+ - `keydown` -- Emitted when a key is pressed (with `NativeKeyboardEvent` payload)
290
+ - `keyup` -- Emitted when a key is released (with `NativeKeyboardEvent` payload)
289
291
  - `close` -- Emitted when the window is closed
290
- - `key` -- Emitted on keyboard events
291
292
  - `resize` -- Emitted when the window is resized (with `{ columns, rows }`)
292
293
  - `sigint` -- Emitted on Ctrl+C (if a listener is registered; otherwise sends SIGINT to the process)
293
294
 
@@ -303,9 +304,59 @@ Event emitter for window lifecycle and input events.
303
304
  - `resume()` -- Resume the Ink event loop
304
305
  - `isPaused()` -- Check if the event loop is paused
305
306
 
306
- ## Keyboard Support
307
+ ## Keyboard Events
307
308
 
308
- The following keys are mapped to terminal sequences:
309
+ The `window` emits `keydown` and `keyup` events with a `NativeKeyboardEvent` payload:
310
+
311
+ ```typescript
312
+ import { createStreams, type NativeKeyboardEvent } from "ink-native";
313
+
314
+ const { window } = createStreams({ title: "My Game" });
315
+
316
+ window.on("keydown", (event: NativeKeyboardEvent) => {
317
+ console.log(event.key); // "a", "A", "Enter", "ArrowUp", "Shift"
318
+ console.log(event.code); // "KeyA", "Enter", "ArrowUp", "ShiftLeft"
319
+ console.log(event.ctrlKey); // true if Ctrl is held
320
+ console.log(event.type); // "keydown"
321
+ });
322
+
323
+ window.on("keyup", (event: NativeKeyboardEvent) => {
324
+ console.log(event.key, "released");
325
+ });
326
+ ```
327
+
328
+ #### `NativeKeyboardEvent`
329
+
330
+ | Property | Type | Description |
331
+ | ---------- | ------------------------ | ------------------------------------------------------- |
332
+ | `key` | `string` | The key value: `"a"`, `"A"`, `"Enter"`, `"Shift"` |
333
+ | `code` | `string` | Physical key code: `"KeyA"`, `"Enter"`, `"ShiftLeft"` |
334
+ | `ctrlKey` | `boolean` | Whether Ctrl is held |
335
+ | `shiftKey` | `boolean` | Whether Shift is held |
336
+ | `altKey` | `boolean` | Whether Alt is held |
337
+ | `metaKey` | `boolean` | Whether Meta/Command is held |
338
+ | `repeat` | `false` | Always `false` (fenster only reports transitions) |
339
+ | `type` | `"keydown" \| "keyup"` | Whether the key was pressed or released |
340
+
341
+ Modifier keys fire their own events with left/right distinction — `event.code` will be `"ShiftLeft"` or `"ShiftRight"`, while `event.key` gives the generic name `"Shift"`.
342
+
343
+ #### `isNativeKeyboardEvent(value)`
344
+
345
+ Type guard to check if a value is a `NativeKeyboardEvent`:
346
+
347
+ ```typescript
348
+ import { isNativeKeyboardEvent } from "ink-native";
349
+
350
+ window.on("keydown", (event) => {
351
+ if (isNativeKeyboardEvent(event)) {
352
+ // event is typed as NativeKeyboardEvent
353
+ }
354
+ });
355
+ ```
356
+
357
+ ### Terminal Sequences
358
+
359
+ In addition to `keydown`/`keyup` events, key presses are also mapped to terminal escape sequences and pushed to `stdin` for Ink's built-in key handling:
309
360
 
310
361
  - Arrow keys (Up, Down, Left, Right)
311
362
  - Enter, Escape, Backspace, Tab, Delete
@@ -327,6 +378,11 @@ import {
327
378
  InputStream,
328
379
  OutputStream,
329
380
 
381
+ // Keyboard events
382
+ createKeyboardEvent,
383
+ isNativeKeyboardEvent,
384
+ type NativeKeyboardEvent,
385
+
330
386
  // Renderer
331
387
  UiRenderer,
332
388
  packColor,
@@ -123867,13 +123867,66 @@ var FENSTER_KEY_HOME = 2;
123867
123867
  var FENSTER_KEY_PAGEUP = 3;
123868
123868
  var FENSTER_KEY_PAGEDOWN = 4;
123869
123869
  var FENSTER_KEY_END = 5;
123870
+ var FENSTER_KEY_INSERT = 26;
123870
123871
  var FENSTER_KEY_A = 65;
123872
+ var FENSTER_KEY_B = 66;
123873
+ var FENSTER_KEY_C = 67;
123874
+ var FENSTER_KEY_D = 68;
123875
+ var FENSTER_KEY_E = 69;
123876
+ var FENSTER_KEY_F = 70;
123877
+ var FENSTER_KEY_G = 71;
123878
+ var FENSTER_KEY_H = 72;
123879
+ var FENSTER_KEY_I = 73;
123880
+ var FENSTER_KEY_J = 74;
123881
+ var FENSTER_KEY_K = 75;
123882
+ var FENSTER_KEY_L = 76;
123883
+ var FENSTER_KEY_M = 77;
123884
+ var FENSTER_KEY_N = 78;
123885
+ var FENSTER_KEY_O = 79;
123886
+ var FENSTER_KEY_P = 80;
123887
+ var FENSTER_KEY_Q = 81;
123888
+ var FENSTER_KEY_R = 82;
123889
+ var FENSTER_KEY_S = 83;
123890
+ var FENSTER_KEY_T = 84;
123891
+ var FENSTER_KEY_U = 85;
123892
+ var FENSTER_KEY_V = 86;
123893
+ var FENSTER_KEY_W = 87;
123894
+ var FENSTER_KEY_X = 88;
123895
+ var FENSTER_KEY_Y = 89;
123871
123896
  var FENSTER_KEY_Z = 90;
123897
+ var FENSTER_KEY_0 = 48;
123898
+ var FENSTER_KEY_1 = 49;
123899
+ var FENSTER_KEY_2 = 50;
123900
+ var FENSTER_KEY_3 = 51;
123901
+ var FENSTER_KEY_4 = 52;
123902
+ var FENSTER_KEY_5 = 53;
123903
+ var FENSTER_KEY_6 = 54;
123904
+ var FENSTER_KEY_7 = 55;
123905
+ var FENSTER_KEY_8 = 56;
123906
+ var FENSTER_KEY_9 = 57;
123907
+ var FENSTER_KEY_APOSTROPHE = 39;
123908
+ var FENSTER_KEY_COMMA = 44;
123909
+ var FENSTER_KEY_MINUS = 45;
123910
+ var FENSTER_KEY_PERIOD = 46;
123911
+ var FENSTER_KEY_SLASH = 47;
123912
+ var FENSTER_KEY_SEMICOLON = 59;
123913
+ var FENSTER_KEY_EQUAL = 61;
123872
123914
  var FENSTER_KEY_BRACKET_LEFT = 91;
123873
123915
  var FENSTER_KEY_BACKSLASH = 92;
123874
123916
  var FENSTER_KEY_BRACKET_RIGHT = 93;
123917
+ var FENSTER_KEY_GRAVE = 96;
123918
+ var FENSTER_KEY_SHIFT_LEFT = 128;
123919
+ var FENSTER_KEY_SHIFT_RIGHT = 129;
123920
+ var FENSTER_KEY_CONTROL_LEFT = 130;
123921
+ var FENSTER_KEY_CONTROL_RIGHT = 131;
123922
+ var FENSTER_KEY_ALT_LEFT = 132;
123923
+ var FENSTER_KEY_ALT_RIGHT = 133;
123924
+ var FENSTER_KEY_META_LEFT = 134;
123925
+ var FENSTER_KEY_META_RIGHT = 135;
123875
123926
  var FENSTER_MOD_CTRL = 1;
123876
123927
  var FENSTER_MOD_SHIFT = 2;
123928
+ var FENSTER_MOD_ALT = 4;
123929
+ var FENSTER_MOD_META = 8;
123877
123930
  var KEYS_ARRAY_SIZE = 256;
123878
123931
  var INT32_BYTES = 4;
123879
123932
  var FENSTER_LIB_PATHS = {
@@ -123888,6 +123941,10 @@ var findFensterLibrary = () => {
123888
123941
  if (!libName) {
123889
123942
  return null;
123890
123943
  }
123944
+ try {
123945
+ return fileURLToPath(import.meta.resolve(`ink-native/${libName}`));
123946
+ } catch {
123947
+ }
123891
123948
  const currentDir = dirname(fileURLToPath(import.meta.url));
123892
123949
  const projectRoot = resolve(currentDir, "../..");
123893
123950
  return resolve(projectRoot, libName);
@@ -124134,9 +124191,249 @@ var InputStream = class extends Readable {
124134
124191
  }
124135
124192
  };
124136
124193
 
124194
+ // src/KeyboardEvent/consts.ts
124195
+ var FENSTER_TO_CODE = {
124196
+ // Navigation / control
124197
+ [FENSTER_KEY_BACKSPACE]: "Backspace",
124198
+ [FENSTER_KEY_TAB]: "Tab",
124199
+ [FENSTER_KEY_RETURN]: "Enter",
124200
+ [FENSTER_KEY_UP]: "ArrowUp",
124201
+ [FENSTER_KEY_DOWN]: "ArrowDown",
124202
+ [FENSTER_KEY_RIGHT]: "ArrowRight",
124203
+ [FENSTER_KEY_LEFT]: "ArrowLeft",
124204
+ [FENSTER_KEY_ESCAPE]: "Escape",
124205
+ [FENSTER_KEY_SPACE]: "Space",
124206
+ [FENSTER_KEY_DELETE]: "Delete",
124207
+ [FENSTER_KEY_HOME]: "Home",
124208
+ [FENSTER_KEY_PAGEUP]: "PageUp",
124209
+ [FENSTER_KEY_PAGEDOWN]: "PageDown",
124210
+ [FENSTER_KEY_END]: "End",
124211
+ [FENSTER_KEY_INSERT]: "Insert",
124212
+ // Letters (A-Z → KeyA-KeyZ)
124213
+ [FENSTER_KEY_A]: "KeyA",
124214
+ [FENSTER_KEY_B]: "KeyB",
124215
+ [FENSTER_KEY_C]: "KeyC",
124216
+ [FENSTER_KEY_D]: "KeyD",
124217
+ [FENSTER_KEY_E]: "KeyE",
124218
+ [FENSTER_KEY_F]: "KeyF",
124219
+ [FENSTER_KEY_G]: "KeyG",
124220
+ [FENSTER_KEY_H]: "KeyH",
124221
+ [FENSTER_KEY_I]: "KeyI",
124222
+ [FENSTER_KEY_J]: "KeyJ",
124223
+ [FENSTER_KEY_K]: "KeyK",
124224
+ [FENSTER_KEY_L]: "KeyL",
124225
+ [FENSTER_KEY_M]: "KeyM",
124226
+ [FENSTER_KEY_N]: "KeyN",
124227
+ [FENSTER_KEY_O]: "KeyO",
124228
+ [FENSTER_KEY_P]: "KeyP",
124229
+ [FENSTER_KEY_Q]: "KeyQ",
124230
+ [FENSTER_KEY_R]: "KeyR",
124231
+ [FENSTER_KEY_S]: "KeyS",
124232
+ [FENSTER_KEY_T]: "KeyT",
124233
+ [FENSTER_KEY_U]: "KeyU",
124234
+ [FENSTER_KEY_V]: "KeyV",
124235
+ [FENSTER_KEY_W]: "KeyW",
124236
+ [FENSTER_KEY_X]: "KeyX",
124237
+ [FENSTER_KEY_Y]: "KeyY",
124238
+ [FENSTER_KEY_Z]: "KeyZ",
124239
+ // Digits (0-9 → Digit0-Digit9)
124240
+ [FENSTER_KEY_0]: "Digit0",
124241
+ [FENSTER_KEY_1]: "Digit1",
124242
+ [FENSTER_KEY_2]: "Digit2",
124243
+ [FENSTER_KEY_3]: "Digit3",
124244
+ [FENSTER_KEY_4]: "Digit4",
124245
+ [FENSTER_KEY_5]: "Digit5",
124246
+ [FENSTER_KEY_6]: "Digit6",
124247
+ [FENSTER_KEY_7]: "Digit7",
124248
+ [FENSTER_KEY_8]: "Digit8",
124249
+ [FENSTER_KEY_9]: "Digit9",
124250
+ // Symbols
124251
+ [FENSTER_KEY_APOSTROPHE]: "Quote",
124252
+ [FENSTER_KEY_COMMA]: "Comma",
124253
+ [FENSTER_KEY_MINUS]: "Minus",
124254
+ [FENSTER_KEY_PERIOD]: "Period",
124255
+ [FENSTER_KEY_SLASH]: "Slash",
124256
+ [FENSTER_KEY_SEMICOLON]: "Semicolon",
124257
+ [FENSTER_KEY_EQUAL]: "Equal",
124258
+ [FENSTER_KEY_BRACKET_LEFT]: "BracketLeft",
124259
+ [FENSTER_KEY_BACKSLASH]: "Backslash",
124260
+ [FENSTER_KEY_BRACKET_RIGHT]: "BracketRight",
124261
+ [FENSTER_KEY_GRAVE]: "Backquote",
124262
+ // Modifier keys (left/right)
124263
+ [FENSTER_KEY_SHIFT_LEFT]: "ShiftLeft",
124264
+ [FENSTER_KEY_SHIFT_RIGHT]: "ShiftRight",
124265
+ [FENSTER_KEY_CONTROL_LEFT]: "ControlLeft",
124266
+ [FENSTER_KEY_CONTROL_RIGHT]: "ControlRight",
124267
+ [FENSTER_KEY_ALT_LEFT]: "AltLeft",
124268
+ [FENSTER_KEY_ALT_RIGHT]: "AltRight",
124269
+ [FENSTER_KEY_META_LEFT]: "MetaLeft",
124270
+ [FENSTER_KEY_META_RIGHT]: "MetaRight"
124271
+ };
124272
+ var FENSTER_TO_KEY = {
124273
+ // Navigation / control
124274
+ [FENSTER_KEY_BACKSPACE]: "Backspace",
124275
+ [FENSTER_KEY_TAB]: "Tab",
124276
+ [FENSTER_KEY_RETURN]: "Enter",
124277
+ [FENSTER_KEY_UP]: "ArrowUp",
124278
+ [FENSTER_KEY_DOWN]: "ArrowDown",
124279
+ [FENSTER_KEY_RIGHT]: "ArrowRight",
124280
+ [FENSTER_KEY_LEFT]: "ArrowLeft",
124281
+ [FENSTER_KEY_ESCAPE]: "Escape",
124282
+ [FENSTER_KEY_SPACE]: " ",
124283
+ [FENSTER_KEY_DELETE]: "Delete",
124284
+ [FENSTER_KEY_HOME]: "Home",
124285
+ [FENSTER_KEY_PAGEUP]: "PageUp",
124286
+ [FENSTER_KEY_PAGEDOWN]: "PageDown",
124287
+ [FENSTER_KEY_END]: "End",
124288
+ [FENSTER_KEY_INSERT]: "Insert",
124289
+ // Letters (A-Z → a-z lowercase)
124290
+ [FENSTER_KEY_A]: "a",
124291
+ [FENSTER_KEY_B]: "b",
124292
+ [FENSTER_KEY_C]: "c",
124293
+ [FENSTER_KEY_D]: "d",
124294
+ [FENSTER_KEY_E]: "e",
124295
+ [FENSTER_KEY_F]: "f",
124296
+ [FENSTER_KEY_G]: "g",
124297
+ [FENSTER_KEY_H]: "h",
124298
+ [FENSTER_KEY_I]: "i",
124299
+ [FENSTER_KEY_J]: "j",
124300
+ [FENSTER_KEY_K]: "k",
124301
+ [FENSTER_KEY_L]: "l",
124302
+ [FENSTER_KEY_M]: "m",
124303
+ [FENSTER_KEY_N]: "n",
124304
+ [FENSTER_KEY_O]: "o",
124305
+ [FENSTER_KEY_P]: "p",
124306
+ [FENSTER_KEY_Q]: "q",
124307
+ [FENSTER_KEY_R]: "r",
124308
+ [FENSTER_KEY_S]: "s",
124309
+ [FENSTER_KEY_T]: "t",
124310
+ [FENSTER_KEY_U]: "u",
124311
+ [FENSTER_KEY_V]: "v",
124312
+ [FENSTER_KEY_W]: "w",
124313
+ [FENSTER_KEY_X]: "x",
124314
+ [FENSTER_KEY_Y]: "y",
124315
+ [FENSTER_KEY_Z]: "z",
124316
+ // Digits (0-9)
124317
+ [FENSTER_KEY_0]: "0",
124318
+ [FENSTER_KEY_1]: "1",
124319
+ [FENSTER_KEY_2]: "2",
124320
+ [FENSTER_KEY_3]: "3",
124321
+ [FENSTER_KEY_4]: "4",
124322
+ [FENSTER_KEY_5]: "5",
124323
+ [FENSTER_KEY_6]: "6",
124324
+ [FENSTER_KEY_7]: "7",
124325
+ [FENSTER_KEY_8]: "8",
124326
+ [FENSTER_KEY_9]: "9",
124327
+ // Symbols (unshifted)
124328
+ [FENSTER_KEY_APOSTROPHE]: "'",
124329
+ [FENSTER_KEY_COMMA]: ",",
124330
+ [FENSTER_KEY_MINUS]: "-",
124331
+ [FENSTER_KEY_PERIOD]: ".",
124332
+ [FENSTER_KEY_SLASH]: "/",
124333
+ [FENSTER_KEY_SEMICOLON]: ";",
124334
+ [FENSTER_KEY_EQUAL]: "=",
124335
+ [FENSTER_KEY_BRACKET_LEFT]: "[",
124336
+ [FENSTER_KEY_BACKSLASH]: "\\",
124337
+ [FENSTER_KEY_BRACKET_RIGHT]: "]",
124338
+ [FENSTER_KEY_GRAVE]: "`",
124339
+ // Modifier keys (left/right)
124340
+ [FENSTER_KEY_SHIFT_LEFT]: "Shift",
124341
+ [FENSTER_KEY_SHIFT_RIGHT]: "Shift",
124342
+ [FENSTER_KEY_CONTROL_LEFT]: "Control",
124343
+ [FENSTER_KEY_CONTROL_RIGHT]: "Control",
124344
+ [FENSTER_KEY_ALT_LEFT]: "Alt",
124345
+ [FENSTER_KEY_ALT_RIGHT]: "Alt",
124346
+ [FENSTER_KEY_META_LEFT]: "Meta",
124347
+ [FENSTER_KEY_META_RIGHT]: "Meta"
124348
+ };
124349
+ var SHIFTED_KEY = {
124350
+ // Letters (a-z → A-Z)
124351
+ [FENSTER_KEY_A]: "A",
124352
+ [FENSTER_KEY_B]: "B",
124353
+ [FENSTER_KEY_C]: "C",
124354
+ [FENSTER_KEY_D]: "D",
124355
+ [FENSTER_KEY_E]: "E",
124356
+ [FENSTER_KEY_F]: "F",
124357
+ [FENSTER_KEY_G]: "G",
124358
+ [FENSTER_KEY_H]: "H",
124359
+ [FENSTER_KEY_I]: "I",
124360
+ [FENSTER_KEY_J]: "J",
124361
+ [FENSTER_KEY_K]: "K",
124362
+ [FENSTER_KEY_L]: "L",
124363
+ [FENSTER_KEY_M]: "M",
124364
+ [FENSTER_KEY_N]: "N",
124365
+ [FENSTER_KEY_O]: "O",
124366
+ [FENSTER_KEY_P]: "P",
124367
+ [FENSTER_KEY_Q]: "Q",
124368
+ [FENSTER_KEY_R]: "R",
124369
+ [FENSTER_KEY_S]: "S",
124370
+ [FENSTER_KEY_T]: "T",
124371
+ [FENSTER_KEY_U]: "U",
124372
+ [FENSTER_KEY_V]: "V",
124373
+ [FENSTER_KEY_W]: "W",
124374
+ [FENSTER_KEY_X]: "X",
124375
+ [FENSTER_KEY_Y]: "Y",
124376
+ [FENSTER_KEY_Z]: "Z",
124377
+ // Digits (0-9 → shifted symbols)
124378
+ [FENSTER_KEY_0]: ")",
124379
+ [FENSTER_KEY_1]: "!",
124380
+ [FENSTER_KEY_2]: "@",
124381
+ [FENSTER_KEY_3]: "#",
124382
+ [FENSTER_KEY_4]: "$",
124383
+ [FENSTER_KEY_5]: "%",
124384
+ [FENSTER_KEY_6]: "^",
124385
+ [FENSTER_KEY_7]: "&",
124386
+ [FENSTER_KEY_8]: "*",
124387
+ [FENSTER_KEY_9]: "(",
124388
+ // Symbols (shifted)
124389
+ [FENSTER_KEY_APOSTROPHE]: '"',
124390
+ [FENSTER_KEY_COMMA]: "<",
124391
+ [FENSTER_KEY_MINUS]: "_",
124392
+ [FENSTER_KEY_PERIOD]: ">",
124393
+ [FENSTER_KEY_SLASH]: "?",
124394
+ [FENSTER_KEY_SEMICOLON]: ":",
124395
+ [FENSTER_KEY_EQUAL]: "+",
124396
+ [FENSTER_KEY_BRACKET_LEFT]: "{",
124397
+ [FENSTER_KEY_BACKSLASH]: "|",
124398
+ [FENSTER_KEY_BRACKET_RIGHT]: "}",
124399
+ [FENSTER_KEY_GRAVE]: "~"
124400
+ };
124401
+
124402
+ // src/KeyboardEvent/types.ts
124403
+ import { isBoolean, isPlainObject, isString } from "remeda";
124404
+ var isNativeKeyboardEvent = (value) => {
124405
+ if (!isPlainObject(value)) {
124406
+ return false;
124407
+ }
124408
+ return isString(value["key"]) && isString(value["code"]) && isBoolean(value["ctrlKey"]) && isBoolean(value["shiftKey"]) && isBoolean(value["altKey"]) && isBoolean(value["metaKey"]) && value["repeat"] === false && (value["type"] === "keydown" || value["type"] === "keyup");
124409
+ };
124410
+
124411
+ // src/KeyboardEvent/index.ts
124412
+ var createKeyboardEvent = (event, mod) => {
124413
+ const code = FENSTER_TO_CODE[event.keyIndex];
124414
+ if (code === void 0) {
124415
+ return null;
124416
+ }
124417
+ const shiftKey = (mod & FENSTER_MOD_SHIFT) !== 0;
124418
+ const key = (shiftKey ? SHIFTED_KEY[event.keyIndex] : void 0) ?? FENSTER_TO_KEY[event.keyIndex];
124419
+ if (key === void 0) {
124420
+ return null;
124421
+ }
124422
+ return {
124423
+ key,
124424
+ code,
124425
+ ctrlKey: (mod & FENSTER_MOD_CTRL) !== 0,
124426
+ shiftKey,
124427
+ altKey: (mod & FENSTER_MOD_ALT) !== 0,
124428
+ metaKey: (mod & FENSTER_MOD_META) !== 0,
124429
+ repeat: false,
124430
+ type: event.pressed ? "keydown" : "keyup"
124431
+ };
124432
+ };
124433
+
124137
124434
  // src/OutputStream/index.ts
124138
124435
  import { Writable } from "stream";
124139
- import { isString } from "remeda";
124436
+ import { isString as isString2 } from "remeda";
124140
124437
 
124141
124438
  // src/OutputStream/consts.ts
124142
124439
  var DEFAULT_COLUMNS = 80;
@@ -124178,7 +124475,7 @@ var OutputStream = class extends Writable {
124178
124475
  */
124179
124476
  _write(chunk, _encoding, callback) {
124180
124477
  try {
124181
- const text = isString(chunk) ? chunk : chunk.toString("utf8");
124478
+ const text = isString2(chunk) ? chunk : chunk.toString("utf8");
124182
124479
  this.uiRenderer.processAnsi(text);
124183
124480
  this.uiRenderer.present();
124184
124481
  callback(null);
@@ -124233,9 +124530,53 @@ var FENSTER_REFRESH_RATE = 60;
124233
124530
  var ALPHA_MASK = 4278190080;
124234
124531
  var RED_SHIFT = 16;
124235
124532
  var GREEN_SHIFT = 8;
124236
- var CTRL_KEY_OFFSET = 96;
124533
+ var CTRL_KEY_OFFSET = 64;
124237
124534
  var ASCII_PRINTABLE_START = 32;
124238
124535
  var ASCII_PRINTABLE_END = 126;
124536
+ var SHIFTED_SYMBOLS = {
124537
+ 48: ")",
124538
+ // 0 → )
124539
+ 49: "!",
124540
+ // 1 → !
124541
+ 50: "@",
124542
+ // 2 → @
124543
+ 51: "#",
124544
+ // 3 → #
124545
+ 52: "$",
124546
+ // 4 → $
124547
+ 53: "%",
124548
+ // 5 → %
124549
+ 54: "^",
124550
+ // 6 → ^
124551
+ 55: "&",
124552
+ // 7 → &
124553
+ 56: "*",
124554
+ // 8 → *
124555
+ 57: "(",
124556
+ // 9 → (
124557
+ 45: "_",
124558
+ // - → _
124559
+ 61: "+",
124560
+ // = → +
124561
+ 91: "{",
124562
+ // [ → {
124563
+ 93: "}",
124564
+ // ] → }
124565
+ 92: "|",
124566
+ // \ → |
124567
+ 59: ":",
124568
+ // ; → :
124569
+ 39: '"',
124570
+ // ' → "
124571
+ 96: "~",
124572
+ // ` → ~
124573
+ 44: "<",
124574
+ // , → <
124575
+ 46: ">",
124576
+ // . → >
124577
+ 47: "?"
124578
+ // / → ?
124579
+ };
124239
124580
 
124240
124581
  // src/UiRenderer/index.ts
124241
124582
  var packColor = (r, g, b) => (ALPHA_MASK | r << RED_SHIFT | g << GREEN_SHIFT | b) >>> 0;
@@ -124471,6 +124812,8 @@ var UiRenderer = class {
124471
124812
  return shift ? "\x1B[Z" : " ";
124472
124813
  case FENSTER_KEY_DELETE:
124473
124814
  return "\x1B[3~";
124815
+ case FENSTER_KEY_INSERT:
124816
+ return "\x1B[2~";
124474
124817
  case FENSTER_KEY_SPACE:
124475
124818
  return ctrl ? "\0" : " ";
124476
124819
  }
@@ -124489,11 +124832,16 @@ var UiRenderer = class {
124489
124832
  }
124490
124833
  }
124491
124834
  if (key >= ASCII_PRINTABLE_START && key <= ASCII_PRINTABLE_END) {
124492
- let char = String.fromCharCode(key);
124493
- if (shift && key >= FENSTER_KEY_A && key <= FENSTER_KEY_Z) {
124494
- char = char.toUpperCase();
124835
+ if (key >= FENSTER_KEY_A && key <= FENSTER_KEY_Z) {
124836
+ return shift ? String.fromCharCode(key) : String.fromCharCode(key).toLowerCase();
124495
124837
  }
124496
- return char;
124838
+ if (shift) {
124839
+ const shifted = SHIFTED_SYMBOLS[key];
124840
+ if (shifted) {
124841
+ return shifted;
124842
+ }
124843
+ }
124844
+ return String.fromCharCode(key);
124497
124845
  }
124498
124846
  return null;
124499
124847
  }
@@ -124771,18 +125119,28 @@ var Window = class extends EventEmitter {
124771
125119
  }
124772
125120
  const { keyEvents, mod, resized } = this.renderer.processEventsAndPresent();
124773
125121
  for (const event of keyEvents) {
124774
- const sequence = this.renderer.keyEventToSequence(event, mod);
124775
- if (sequence) {
124776
- if (sequence === "") {
124777
- if (this.listenerCount("sigint") > 0) {
124778
- this.emit("sigint");
124779
- } else {
124780
- process.kill(process.pid, "SIGINT");
125122
+ const kbEvent = createKeyboardEvent(event, mod);
125123
+ if (event.pressed) {
125124
+ const sequence = this.renderer.keyEventToSequence(event, mod);
125125
+ if (sequence) {
125126
+ if (sequence === "") {
125127
+ if (this.listenerCount("sigint") > 0) {
125128
+ this.emit("sigint");
125129
+ } else {
125130
+ process.kill(process.pid, "SIGINT");
125131
+ }
125132
+ if (kbEvent) {
125133
+ this.emit("keydown", kbEvent);
125134
+ }
125135
+ continue;
124781
125136
  }
124782
- continue;
125137
+ this.inputStream.pushKey(sequence);
125138
+ }
125139
+ if (kbEvent) {
125140
+ this.emit("keydown", kbEvent);
124783
125141
  }
124784
- this.inputStream.pushKey(sequence);
124785
- this.emit("key", event);
125142
+ } else if (kbEvent) {
125143
+ this.emit("keyup", kbEvent);
124786
125144
  }
124787
125145
  }
124788
125146
  if (resized) {
@@ -124907,6 +125265,8 @@ export {
124907
125265
  getFenster,
124908
125266
  isFensterAvailable,
124909
125267
  InputStream,
125268
+ isNativeKeyboardEvent,
125269
+ createKeyboardEvent,
124910
125270
  OutputStream,
124911
125271
  packColor,
124912
125272
  UiRenderer,
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createStreams
4
- } from "./chunk-3NCUBVFY.js";
4
+ } from "./chunk-5O6OVITA.js";
5
5
 
6
6
  // src/cli.tsx
7
7
  import { parseArgs } from "util";
package/dist/index.d.ts CHANGED
@@ -324,6 +324,36 @@ declare class InputStream extends Readable {
324
324
  unref(): this;
325
325
  }
326
326
 
327
+ /** Keyboard event from the native window */
328
+ interface NativeKeyboardEvent {
329
+ /** Key value: "a", "A", "Enter", "ArrowUp", "!" */
330
+ readonly key: string;
331
+ /** Physical key code: "KeyA", "Digit1", "ArrowUp" */
332
+ readonly code: string;
333
+ readonly ctrlKey: boolean;
334
+ readonly shiftKey: boolean;
335
+ readonly altKey: boolean;
336
+ readonly metaKey: boolean;
337
+ /** Always false — fenster only reports transitions, not held state */
338
+ readonly repeat: false;
339
+ readonly type: "keydown" | "keyup";
340
+ }
341
+ declare const isNativeKeyboardEvent: (value: unknown) => value is NativeKeyboardEvent;
342
+
343
+ /**
344
+ * KeyboardEvent
345
+ *
346
+ * Converts fenster key events into NativeKeyboardEvent objects,
347
+ * providing keydown/keyup events for games and interactive apps.
348
+ */
349
+
350
+ /**
351
+ * Create a NativeKeyboardEvent from a fenster key event and modifier bitmask.
352
+ *
353
+ * Returns null for unmapped key indices.
354
+ */
355
+ declare const createKeyboardEvent: (event: FensterKeyEvent, mod: number) => NativeKeyboardEvent | null;
356
+
327
357
  /**
328
358
  * OutputStream Types
329
359
  */
@@ -711,4 +741,4 @@ declare class Window extends EventEmitter {
711
741
  */
712
742
  declare const createStreams: (options?: StreamsOptions) => Streams;
713
743
 
714
- export { AnsiParser, BitmapFontRenderer, type Color, type DrawCommand, Fenster, type FensterKeyEvent, type FensterPointer, type Framebuffer, InputStream, OutputStream, type ProcessEventsResult, type Streams, type StreamsOptions, UiRenderer, type UiRendererOptions, Window, createStreams, getFenster, isFensterAvailable, packColor };
744
+ export { AnsiParser, BitmapFontRenderer, type Color, type DrawCommand, Fenster, type FensterKeyEvent, type FensterPointer, type Framebuffer, InputStream, type NativeKeyboardEvent, OutputStream, type ProcessEventsResult, type Streams, type StreamsOptions, UiRenderer, type UiRendererOptions, Window, createKeyboardEvent, createStreams, getFenster, isFensterAvailable, isNativeKeyboardEvent, packColor };
package/dist/index.js CHANGED
@@ -6,11 +6,13 @@ import {
6
6
  OutputStream,
7
7
  UiRenderer,
8
8
  Window,
9
+ createKeyboardEvent,
9
10
  createStreams,
10
11
  getFenster,
11
12
  isFensterAvailable,
13
+ isNativeKeyboardEvent,
12
14
  packColor
13
- } from "./chunk-3NCUBVFY.js";
15
+ } from "./chunk-5O6OVITA.js";
14
16
  export {
15
17
  AnsiParser,
16
18
  BitmapFontRenderer,
@@ -19,8 +21,10 @@ export {
19
21
  OutputStream,
20
22
  UiRenderer,
21
23
  Window,
24
+ createKeyboardEvent,
22
25
  createStreams,
23
26
  getFenster,
24
27
  isFensterAvailable,
28
+ isNativeKeyboardEvent,
25
29
  packColor
26
30
  };
Binary file
package/native/fenster.h CHANGED
@@ -172,6 +172,24 @@ FENSTER_API int fenster_loop(struct fenster *f) {
172
172
  f->mod = (mod & 0xc) | ((mod & 1) << 1) | ((mod >> 1) & 1);
173
173
  return 0;
174
174
  }
175
+ case 12: { /* NSEventTypeFlagsChanged — modifier key press/release */
176
+ NSUInteger k = msg(NSUInteger, ev, "keyCode");
177
+ NSUInteger flags = msg(NSUInteger, ev, "modifierFlags");
178
+ NSUInteger mod = flags >> 17;
179
+ f->mod = (mod & 0xc) | ((mod & 1) << 1) | ((mod >> 1) & 1);
180
+ /* Device-dependent flags distinguish left/right modifiers */
181
+ switch (k) {
182
+ case 56: f->keys[128] = !!(flags & 0x00000002); break; /* Left Shift */
183
+ case 60: f->keys[129] = !!(flags & 0x00000004); break; /* Right Shift */
184
+ case 59: f->keys[130] = !!(flags & 0x00000001); break; /* Left Control */
185
+ case 62: f->keys[131] = !!(flags & 0x00002000); break; /* Right Control */
186
+ case 58: f->keys[132] = !!(flags & 0x00000020); break; /* Left Alt */
187
+ case 61: f->keys[133] = !!(flags & 0x00000040); break; /* Right Alt */
188
+ case 55: f->keys[134] = !!(flags & 0x00000008); break; /* Left Meta */
189
+ case 54: f->keys[135] = !!(flags & 0x00000010); break; /* Right Meta */
190
+ }
191
+ return 0;
192
+ }
175
193
  }
176
194
  msg1(void, NSApp, "sendEvent:", id, ev);
177
195
  /* Poll content view frame for resize */
@@ -240,7 +258,17 @@ static LRESULT CALLBACK fenster_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
240
258
  ((GetKeyState(VK_SHIFT) & 0x8000) >> 14) |
241
259
  ((GetKeyState(VK_MENU) & 0x8000) >> 13) |
242
260
  (((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) >> 12);
243
- f->keys[FENSTER_KEYCODES[HIWORD(lParam) & 0x1ff]] = !((lParam >> 31) & 1);
261
+ int pressed = !((lParam >> 31) & 1);
262
+ f->keys[FENSTER_KEYCODES[HIWORD(lParam) & 0x1ff]] = pressed;
263
+ /* Left/right modifier key tracking */
264
+ f->keys[128] = !!(GetKeyState(VK_LSHIFT) & 0x8000);
265
+ f->keys[129] = !!(GetKeyState(VK_RSHIFT) & 0x8000);
266
+ f->keys[130] = !!(GetKeyState(VK_LCONTROL) & 0x8000);
267
+ f->keys[131] = !!(GetKeyState(VK_RCONTROL) & 0x8000);
268
+ f->keys[132] = !!(GetKeyState(VK_LMENU) & 0x8000);
269
+ f->keys[133] = !!(GetKeyState(VK_RMENU) & 0x8000);
270
+ f->keys[134] = !!(GetKeyState(VK_LWIN) & 0x8000);
271
+ f->keys[135] = !!(GetKeyState(VK_RWIN) & 0x8000);
244
272
  } break;
245
273
  case WM_SIZE:
246
274
  f->width = LOWORD(lParam);
@@ -320,7 +348,7 @@ FENSTER_API int fenster_loop(struct fenster *f) {
320
348
  }
321
349
  #else
322
350
  // clang-format off
323
- static int FENSTER_KEYCODES[124] = {XK_BackSpace,8,XK_Delete,127,XK_Down,18,XK_End,5,XK_Escape,27,XK_Home,2,XK_Insert,26,XK_Left,20,XK_Page_Down,4,XK_Page_Up,3,XK_Return,10,XK_Right,19,XK_Tab,9,XK_Up,17,XK_apostrophe,39,XK_backslash,92,XK_bracketleft,91,XK_bracketright,93,XK_comma,44,XK_equal,61,XK_grave,96,XK_minus,45,XK_period,46,XK_semicolon,59,XK_slash,47,XK_space,32,XK_a,65,XK_b,66,XK_c,67,XK_d,68,XK_e,69,XK_f,70,XK_g,71,XK_h,72,XK_i,73,XK_j,74,XK_k,75,XK_l,76,XK_m,77,XK_n,78,XK_o,79,XK_p,80,XK_q,81,XK_r,82,XK_s,83,XK_t,84,XK_u,85,XK_v,86,XK_w,87,XK_x,88,XK_y,89,XK_z,90,XK_0,48,XK_1,49,XK_2,50,XK_3,51,XK_4,52,XK_5,53,XK_6,54,XK_7,55,XK_8,56,XK_9,57};
351
+ static int FENSTER_KEYCODES[140] = {XK_BackSpace,8,XK_Delete,127,XK_Down,18,XK_End,5,XK_Escape,27,XK_Home,2,XK_Insert,26,XK_Left,20,XK_Page_Down,4,XK_Page_Up,3,XK_Return,10,XK_Right,19,XK_Tab,9,XK_Up,17,XK_apostrophe,39,XK_backslash,92,XK_bracketleft,91,XK_bracketright,93,XK_comma,44,XK_equal,61,XK_grave,96,XK_minus,45,XK_period,46,XK_semicolon,59,XK_slash,47,XK_space,32,XK_a,65,XK_b,66,XK_c,67,XK_d,68,XK_e,69,XK_f,70,XK_g,71,XK_h,72,XK_i,73,XK_j,74,XK_k,75,XK_l,76,XK_m,77,XK_n,78,XK_o,79,XK_p,80,XK_q,81,XK_r,82,XK_s,83,XK_t,84,XK_u,85,XK_v,86,XK_w,87,XK_x,88,XK_y,89,XK_z,90,XK_0,48,XK_1,49,XK_2,50,XK_3,51,XK_4,52,XK_5,53,XK_6,54,XK_7,55,XK_8,56,XK_9,57,XK_Shift_L,128,XK_Shift_R,129,XK_Control_L,130,XK_Control_R,131,XK_Alt_L,132,XK_Alt_R,133,XK_Super_L,134,XK_Super_R,135};
324
352
  // clang-format on
325
353
  FENSTER_API int fenster_open(struct fenster *f) {
326
354
  f->dpy = XOpenDisplay(NULL);
@@ -363,7 +391,7 @@ FENSTER_API int fenster_loop(struct fenster *f) {
363
391
  case KeyRelease: {
364
392
  int m = ev.xkey.state;
365
393
  int k = XkbKeycodeToKeysym(f->dpy, ev.xkey.keycode, 0, 0);
366
- for (unsigned int i = 0; i < 124; i += 2) {
394
+ for (unsigned int i = 0; i < 140; i += 2) {
367
395
  if (FENSTER_KEYCODES[i] == k) {
368
396
  f->keys[FENSTER_KEYCODES[i + 1]] = (ev.type == KeyPress);
369
397
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-native",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Render Ink terminal apps in a native window",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,7 +9,8 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
- }
12
+ },
13
+ "./native/*": "./native/*"
13
14
  },
14
15
  "bin": {
15
16
  "ink-native": "./dist/cli.js"