ink-native 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Derek Petersen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,389 @@
1
+ # ink-native
2
+
3
+ Render [Ink](https://github.com/vadimdemedes/ink) TUI applications in native windows instead of the terminal. Build graphical applications using React/Ink's declarative paradigm with zero system dependencies.
4
+
5
+ ## Why ink-native?
6
+
7
+ For plain text TUIs, a GPU-accelerated terminal like [Ghostty](https://ghostty.org/) or [Kitty](https://sw.kovidgoyal.net/kitty/) works great. So why render to a native window instead?
8
+
9
+ **The problem appears when you need high-framerate graphics alongside your Ink UI** like an emulator, game, or video player with a React-based menu system.
10
+
11
+ Even GPU-accelerated terminals struggle when using image protocols (like the [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/)) because they require:
12
+
13
+ ```
14
+ Raw pixels → base64 encode (+33% size) → escape sequences →
15
+ PTY syscalls → terminal parses sequences → base64 decode → GPU upload → render
16
+ ```
17
+
18
+ At 60fps for an 800x600 frame, that's ~110 MB/s of base64-encoded data through the PTY. Even the fastest terminals can't keep up.
19
+
20
+ **Direct framebuffer rendering bypasses all of this:**
21
+
22
+ ```
23
+ Raw pixels → memcpy to framebuffer → render
24
+ ```
25
+
26
+ No encoding, no PTY, no parsing, no process boundary - just a memory copy.
27
+
28
+ **ink-native lets you combine both**: render game/emulator frames directly to the framebuffer for performance, while reusing your existing Ink components for menus and UI in the same window. And since everything is bundled (native library + bitmap font), there are zero system dependencies to install.
29
+
30
+ ## Features
31
+
32
+ - Zero system dependencies - no external libraries to install
33
+ - Full ANSI color support (16, 256, and 24-bit true color)
34
+ - Keyboard input with modifier keys (Ctrl, Shift, Alt)
35
+ - Window resizing with automatic terminal dimension updates
36
+ - HiDPI/Retina display support
37
+ - Embedded [Cozette](https://github.com/slavfox/Cozette) bitmap font with 6,000+ glyphs
38
+ - Cross-platform (macOS, Linux, Windows)
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install ink-native
44
+ # or
45
+ pnpm add ink-native
46
+ ```
47
+
48
+ No system dependencies required. The native window library and bitmap font are bundled with the package.
49
+
50
+ ## Demo
51
+
52
+ Run the built-in demo to see ink-native in action:
53
+
54
+ ```bash
55
+ npx ink-native
56
+ # or
57
+ pnpm dlx ink-native
58
+ ```
59
+
60
+ The demo showcases text styles, colors, box layouts, and dynamic updates. Use `--help` to see available options:
61
+
62
+ ```bash
63
+ npx ink-native --help
64
+ ```
65
+
66
+ **Example commands:**
67
+
68
+ ```bash
69
+ # Custom window size
70
+ npx ink-native --width 1024 --height 768
71
+
72
+ # Dark background
73
+ npx ink-native --background "#1a1a2e"
74
+
75
+ # Custom frame rate
76
+ npx ink-native --frame-rate 30
77
+ ```
78
+
79
+ | Flag | Description |
80
+ | -------------- | ----------------------------------------- |
81
+ | `--title` | Window title |
82
+ | `--width` | Window width in pixels (default: 800) |
83
+ | `--height` | Window height in pixels (default: 600) |
84
+ | `--background` | Background color as hex (e.g., "#1a1a2e") |
85
+ | `--frame-rate` | Force frame rate instead of default 60fps |
86
+ | `-h`, `--help` | Show help message |
87
+
88
+ ## Usage
89
+
90
+ ```tsx
91
+ import React, { useState, useEffect } from "react";
92
+ import { render, Text, Box } from "ink";
93
+ import { createStreams } from "ink-native";
94
+
95
+ const App = () => {
96
+ const [count, setCount] = useState(0);
97
+
98
+ useEffect(() => {
99
+ const timer = setInterval(() => {
100
+ setCount((c) => c + 1);
101
+ }, 1_000);
102
+ return () => clearInterval(timer);
103
+ }, []);
104
+
105
+ return (
106
+ <Box flexDirection="column" padding={1}>
107
+ <Text color="green" bold>
108
+ Hello from ink-native!
109
+ </Text>
110
+ <Text>
111
+ Counter: <Text color="cyan">{count}</Text>
112
+ </Text>
113
+ </Box>
114
+ );
115
+ };
116
+
117
+ const { stdin, stdout, window } = createStreams({
118
+ title: "My App",
119
+ width: 800,
120
+ height: 600,
121
+ });
122
+
123
+ render(<App />, { stdin, stdout });
124
+
125
+ window.on("close", () => process.exit(0));
126
+ ```
127
+
128
+ ## Direct Framebuffer Access
129
+
130
+ For high-framerate graphics (emulators, games, video players), you can write pixels directly to the framebuffer while pausing the Ink event loop:
131
+
132
+ ```tsx
133
+ import { createStreams, packColor } from "ink-native";
134
+ import { render, Text, Box } from "ink";
135
+
136
+ const App = () => (
137
+ <Box>
138
+ <Text>Game UI overlay</Text>
139
+ </Box>
140
+ );
141
+
142
+ const { stdin, stdout, window, renderer } = createStreams({
143
+ title: "My Game",
144
+ width: 800,
145
+ height: 600,
146
+ });
147
+
148
+ render(<App />, { stdin, stdout });
149
+
150
+ // Pause Ink's event loop to take over rendering
151
+ window.pause();
152
+
153
+ const fb = renderer.getFramebuffer();
154
+
155
+ // Game loop — you control timing and rendering
156
+ const gameLoop = setInterval(() => {
157
+ // Write pixels directly (0xAARRGGBB format)
158
+ for (let y = 100; y < 200; y++) {
159
+ for (let x = 100; x < 200; x++) {
160
+ fb.pixels[y * fb.width + x] = packColor(255, 0, 0); // red square
161
+ }
162
+ }
163
+
164
+ // Present the framebuffer and poll window events
165
+ renderer.present();
166
+ const { keyEvents, mod } = renderer.processEventsAndPresent();
167
+
168
+ // Handle input
169
+ for (const event of keyEvents) {
170
+ const seq = renderer.keyEventToSequence(event, mod);
171
+ if (seq === "q") {
172
+ clearInterval(gameLoop);
173
+ window.resume(); // hand control back to Ink
174
+ }
175
+ }
176
+
177
+ if (renderer.shouldClose()) {
178
+ clearInterval(gameLoop);
179
+ window.close();
180
+ process.exit(0);
181
+ }
182
+ }, 16); // ~60fps
183
+ ```
184
+
185
+ ### Switching Between Ink UI and Custom Rendering
186
+
187
+ For applications that need to switch between Ink UI (e.g., menus) and custom rendering (e.g., an emulator or game), use `pause()` and `resume()` to hand off control:
188
+
189
+ ```typescript
190
+ import { render, Text, Box } from "ink";
191
+ import { createStreams, packColor } from "ink-native";
192
+
193
+ const { stdin, stdout, window, renderer } = createStreams({
194
+ title: "My Emulator",
195
+ width: 800,
196
+ height: 600,
197
+ });
198
+
199
+ // Phase 1: Render menu UI with Ink
200
+ const MenuApp = () => (
201
+ <Box flexDirection="column" padding={1}>
202
+ <Text color="green" bold>My Emulator</Text>
203
+ <Text>Press Enter to start</Text>
204
+ </Box>
205
+ );
206
+
207
+ const { unmount } = render(<MenuApp />, { stdin, stdout });
208
+
209
+ // Phase 2: When ready, pause Ink and take over rendering
210
+ const startEmulator = () => {
211
+ window.pause();
212
+
213
+ const fb = renderer.getFramebuffer();
214
+
215
+ const emuLoop = setInterval(() => {
216
+ // Write emulator frame directly to the framebuffer
217
+ renderEmulatorFrame(fb.pixels, fb.width, fb.height);
218
+
219
+ // Present and poll events
220
+ renderer.present();
221
+ const { keyEvents, mod } = renderer.processEventsAndPresent();
222
+
223
+ for (const event of keyEvents) {
224
+ const seq = renderer.keyEventToSequence(event, mod);
225
+ if (seq === "\x1b") {
226
+ // Escape pressed — return to menu
227
+ clearInterval(emuLoop);
228
+ renderer.clear();
229
+ window.resume(); // hand control back to Ink
230
+ return;
231
+ }
232
+ }
233
+
234
+ if (renderer.shouldClose()) {
235
+ clearInterval(emuLoop);
236
+ window.close();
237
+ process.exit(0);
238
+ }
239
+ }, 16);
240
+ };
241
+ ```
242
+
243
+ The framebuffer is shared — Ink renders to it when active, and you write pixels directly when paused. Calling `resume()` hands control back to Ink seamlessly.
244
+
245
+ ### API Summary
246
+
247
+ | Export | Description |
248
+ | --------------------------- | ------------------------------------------------------- |
249
+ | `packColor(r, g, b)` | Pack RGB values into `0xAARRGGBB` pixel format |
250
+ | `renderer.getFramebuffer()` | Get `{ pixels, width, height }` — the live pixel buffer |
251
+ | `window.pause()` | Stop Ink's event loop for manual rendering |
252
+ | `window.resume()` | Restart Ink's event loop |
253
+ | `window.isPaused()` | Check if the event loop is paused |
254
+
255
+ ## API
256
+
257
+ ### `createStreams(options?)`
258
+
259
+ Creates stdin/stdout streams and a window for use with Ink.
260
+
261
+ #### Options (`StreamsOptions`)
262
+
263
+ | Option | Type | Default | Description |
264
+ | ----------------- | ------------------------------------ | -------------- | ----------------------------------------------------- |
265
+ | `title` | `string` | `"ink-native"` | Window title |
266
+ | `width` | `number` | `800` | Window width in pixels |
267
+ | `height` | `number` | `600` | Window height in pixels |
268
+ | `backgroundColor` | `[number, number, number] \| string` | `[0, 0, 0]` | Background color as RGB tuple or hex string "#RRGGBB" |
269
+ | `frameRate` | `number` | `60` | Target frame rate |
270
+ | `scaleFactor` | `number \| null` | `null` | Override HiDPI scale factor (null = auto-detect) |
271
+
272
+ #### Returns (`Streams`)
273
+
274
+ ```typescript
275
+ {
276
+ stdin: InputStream; // Readable stream for keyboard input
277
+ stdout: OutputStream; // Writable stream for ANSI output
278
+ window: Window; // Window wrapper with events
279
+ renderer: UiRenderer; // UI renderer (for advanced use)
280
+ }
281
+ ```
282
+
283
+ ### `Window`
284
+
285
+ Event emitter for window lifecycle and input events.
286
+
287
+ #### Events
288
+
289
+ - `close` -- Emitted when the window is closed
290
+ - `key` -- Emitted on keyboard events
291
+ - `resize` -- Emitted when the window is resized (with `{ columns, rows }`)
292
+ - `sigint` -- Emitted on Ctrl+C (if a listener is registered; otherwise sends SIGINT to the process)
293
+
294
+ #### Methods
295
+
296
+ - `getDimensions()` -- Returns `{ columns, rows }` for terminal size
297
+ - `getFrameRate()` -- Returns the current frame rate
298
+ - `getOutputStream()` -- Returns the output stream
299
+ - `clear()` -- Clear the screen
300
+ - `close()` -- Close the window
301
+ - `isClosed()` -- Check if the window is closed
302
+ - `pause()` -- Pause the Ink event loop for manual rendering
303
+ - `resume()` -- Resume the Ink event loop
304
+ - `isPaused()` -- Check if the event loop is paused
305
+
306
+ ## Keyboard Support
307
+
308
+ The following keys are mapped to terminal sequences:
309
+
310
+ - Arrow keys (Up, Down, Left, Right)
311
+ - Enter, Escape, Backspace, Tab, Delete
312
+ - Home, End, Page Up, Page Down
313
+ - Function keys (F1-F12)
314
+ - Ctrl+A through Ctrl+Z
315
+ - Shift for uppercase letters
316
+ - Alt + letter sends `\x1b` + letter
317
+
318
+ ## Low-Level Components
319
+
320
+ For advanced use cases, ink-native exports its internal components:
321
+
322
+ ```typescript
323
+ import {
324
+ // Main API
325
+ createStreams,
326
+ Window,
327
+ InputStream,
328
+ OutputStream,
329
+
330
+ // Renderer
331
+ UiRenderer,
332
+ packColor,
333
+ type UiRendererOptions,
334
+ type Framebuffer,
335
+ type ProcessEventsResult,
336
+
337
+ // Font
338
+ BitmapFontRenderer,
339
+
340
+ // ANSI parsing
341
+ AnsiParser,
342
+ type Color,
343
+ type DrawCommand,
344
+
345
+ // Fenster FFI bindings
346
+ getFenster,
347
+ Fenster,
348
+ isFensterAvailable,
349
+ type FensterPointer,
350
+ type FensterKeyEvent,
351
+
352
+ // Types
353
+ type StreamsOptions,
354
+ type Streams,
355
+ } from "ink-native";
356
+ ```
357
+
358
+ ### `isFensterAvailable()`
359
+
360
+ Check if the native fenster library can be loaded on the current platform. Useful for graceful fallback to terminal rendering:
361
+
362
+ ```typescript
363
+ import { isFensterAvailable, createStreams } from "ink-native";
364
+ import { render } from "ink";
365
+
366
+ if (isFensterAvailable()) {
367
+ const { stdin, stdout, window } = createStreams({ title: "My App" });
368
+ render(<App />, { stdin, stdout });
369
+ window.on("close", () => process.exit(0));
370
+ } else {
371
+ render(<App />);
372
+ }
373
+ ```
374
+
375
+ ### `AnsiParser`
376
+
377
+ Parses ANSI escape sequences into structured draw commands. Supports cursor positioning, 16/256/24-bit colors, text styles (bold, dim, reverse), screen/line clearing, and alt screen buffer.
378
+
379
+ ### `BitmapFontRenderer`
380
+
381
+ Renders text by blitting embedded Cozette bitmap font glyphs into a `Uint32Array` framebuffer. Supports 6,000+ glyphs including ASCII, Latin-1, box drawing, block elements, braille patterns, and more.
382
+
383
+ ### `getFenster()` / `Fenster`
384
+
385
+ Low-level FFI bindings to the [fenster](https://github.com/zserge/fenster) native library via koffi. Provides direct access to window creation, framebuffer manipulation, and event polling.
386
+
387
+ ## License
388
+
389
+ MIT