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 +21 -0
- package/README.md +389 -0
- package/dist/chunk-3NCUBVFY.js +124915 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +412 -0
- package/dist/index.d.ts +714 -0
- package/dist/index.js +26 -0
- package/fonts/Cozette.bdf +93671 -0
- package/native/fenster.dylib +0 -0
- package/native/fenster.h +443 -0
- package/native/fenster_bridge.c +149 -0
- package/package.json +71 -0
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
|