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.
@@ -0,0 +1,714 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ /** RGB color value */
5
+ interface Color {
6
+ r: number;
7
+ g: number;
8
+ b: number;
9
+ }
10
+ /** Types of draw commands */
11
+ type DrawCommandType = "text" | "clear_screen" | "clear_line" | "cursor_move" | "set_fg" | "set_bg" | "reset_style" | "set_bold" | "set_dim" | "set_italic" | "set_underline" | "set_strikethrough" | "set_reverse";
12
+ /** A single draw command from parsed ANSI output */
13
+ interface DrawCommand {
14
+ type: DrawCommandType;
15
+ text?: string;
16
+ row?: number;
17
+ col?: number;
18
+ color?: Color;
19
+ enabled?: boolean;
20
+ }
21
+
22
+ /**
23
+ * ANSI Parser
24
+ *
25
+ * Parses terminal ANSI escape sequences and converts them to draw commands
26
+ * for rendering in a native framebuffer window.
27
+ */
28
+
29
+ /**
30
+ * ANSI sequence parser
31
+ *
32
+ * Parses ANSI escape sequences from terminal output and produces
33
+ * draw commands for framebuffer rendering.
34
+ */
35
+ declare class AnsiParser {
36
+ private cursorRow;
37
+ private cursorCol;
38
+ private fgColor;
39
+ private bgColor;
40
+ private bold;
41
+ /**
42
+ * Parse an ANSI string and return draw commands
43
+ */
44
+ parse(input: string): DrawCommand[];
45
+ /**
46
+ * Process an escape sequence and emit draw commands
47
+ */
48
+ private processEscapeSequence;
49
+ /**
50
+ * Process cursor position sequence
51
+ */
52
+ private processCursorPosition;
53
+ /**
54
+ * Process erase display sequence
55
+ */
56
+ private processEraseDisplay;
57
+ /**
58
+ * Process erase line sequence
59
+ */
60
+ private processEraseLine;
61
+ /**
62
+ * Process SGR (Select Graphic Rendition) sequence
63
+ */
64
+ private processSGR;
65
+ /**
66
+ * Parse extended color (256-color or 24-bit)
67
+ */
68
+ private parseExtendedColor;
69
+ /**
70
+ * Reset all styles to default
71
+ */
72
+ private resetStyle;
73
+ /**
74
+ * Get current cursor position
75
+ */
76
+ getCursor(): {
77
+ row: number;
78
+ col: number;
79
+ };
80
+ /**
81
+ * Get current foreground color
82
+ */
83
+ getFgColor(): Color;
84
+ /**
85
+ * Get current background color
86
+ */
87
+ getBgColor(): Color;
88
+ /**
89
+ * Reset parser state
90
+ */
91
+ reset(): void;
92
+ }
93
+
94
+ /**
95
+ * Bitmap Font Renderer
96
+ *
97
+ * Renders text using embedded Cozette bitmap font data directly into
98
+ * a Uint32Array framebuffer. Pure TypeScript -- no native dependencies,
99
+ * no caching needed (just bit tests and pixel writes).
100
+ */
101
+
102
+ /**
103
+ * Bitmap Font Renderer
104
+ *
105
+ * Blits bitmap font glyphs directly into a Uint32Array pixel buffer.
106
+ */
107
+ declare class BitmapFontRenderer {
108
+ private fallbackGlyph;
109
+ /**
110
+ * Get the fixed character cell dimensions
111
+ */
112
+ getCharDimensions(): {
113
+ width: number;
114
+ height: number;
115
+ };
116
+ /**
117
+ * Check if a glyph is double-width based on its data length
118
+ *
119
+ * Wide glyphs have 2 bytes per row (26 bytes total) vs 1 byte (13 bytes).
120
+ */
121
+ isWideGlyph(codepoint: number): boolean;
122
+ /**
123
+ * Get the pixel width of a glyph
124
+ */
125
+ getGlyphPixelWidth(codepoint: number): number;
126
+ /**
127
+ * Measure the total pixel width of a text string
128
+ */
129
+ measureText(text: string): number;
130
+ /**
131
+ * Render a single character into the framebuffer
132
+ */
133
+ renderChar(buf: Uint32Array, bufWidth: number, x: number, y: number, codepoint: number, fgColor: number, italic?: boolean): void;
134
+ /**
135
+ * Render a string of text into the framebuffer
136
+ *
137
+ * Returns the total pixel width rendered.
138
+ */
139
+ renderText(buf: Uint32Array, bufWidth: number, x: number, y: number, text: string, fgColor: number, italic?: boolean): number;
140
+ /**
141
+ * Render a single character scaled via nearest-neighbor into the framebuffer
142
+ *
143
+ * Maps each destination pixel back to the source glyph bitmap using
144
+ * nearest-neighbor sampling. Preserves the crisp, pixelated look of
145
+ * bitmap fonts on HiDPI displays.
146
+ */
147
+ renderCharScaled(buf: Uint32Array, bufWidth: number, bufHeight: number, x: number, y: number, codepoint: number, fgColor: number, destWidth: number, destHeight: number, italic?: boolean): void;
148
+ /**
149
+ * Render a string of text scaled via nearest-neighbor into the framebuffer
150
+ *
151
+ * Returns the total pixel width rendered.
152
+ */
153
+ renderTextScaled(buf: Uint32Array, bufWidth: number, bufHeight: number, x: number, y: number, text: string, fgColor: number, scale: number, italic?: boolean): number;
154
+ /**
155
+ * Measure the total scaled pixel width of a text string
156
+ */
157
+ measureTextScaled(text: string, scale: number): number;
158
+ }
159
+
160
+ /** Opaque pointer type for fenster bridge handle */
161
+ type FensterPointer = unknown;
162
+ /** Key event from fenster key state diffing */
163
+ interface FensterKeyEvent {
164
+ /** Key index in fenster's keys[256] array (mostly ASCII) */
165
+ keyIndex: number;
166
+ /** Whether the key is pressed (true) or released (false) */
167
+ pressed: boolean;
168
+ }
169
+
170
+ /**
171
+ * Fenster FFI Bindings
172
+ *
173
+ * Provides fenster bindings for framebuffer-based window rendering
174
+ * using koffi for foreign function interface. No system dependencies
175
+ * required beyond the bundled native library.
176
+ */
177
+
178
+ /**
179
+ * Fenster API wrapper class
180
+ *
181
+ * Provides type-safe access to the fenster bridge functions for
182
+ * framebuffer-based window rendering.
183
+ */
184
+ declare class Fenster {
185
+ private lib;
186
+ private _create;
187
+ private _open;
188
+ private _loop;
189
+ private _close;
190
+ private _copyBuf;
191
+ private _getKeys;
192
+ private _getMod;
193
+ private _getSize;
194
+ private _getResized;
195
+ private _getScale;
196
+ private _setScale;
197
+ constructor();
198
+ private bindFunctions;
199
+ /**
200
+ * Create a new fenster window (does not open it yet)
201
+ */
202
+ create(title: string, width: number, height: number): FensterPointer;
203
+ /**
204
+ * Open the fenster window
205
+ */
206
+ open(f: FensterPointer): void;
207
+ /**
208
+ * Process events and present the buffer
209
+ *
210
+ * Returns 0 on success, non-zero if window should close.
211
+ */
212
+ loop(f: FensterPointer): number;
213
+ /**
214
+ * Close the fenster window and free resources
215
+ */
216
+ close(f: FensterPointer): void;
217
+ /**
218
+ * Copy pixel data into fenster's native buffer
219
+ *
220
+ * Accepts a Buffer (e.g. a view over a Uint32Array) and memcpy's it
221
+ * into the native pixel buffer so the next `loop()` call presents it.
222
+ */
223
+ copyBuf(f: FensterPointer, src: Buffer): void;
224
+ /**
225
+ * Get the keys state array
226
+ *
227
+ * 256 entries; each entry is 1 (pressed) or 0 (released).
228
+ */
229
+ getKeys(f: FensterPointer): number[];
230
+ /**
231
+ * Get the current modifier bitmask
232
+ *
233
+ * Bits: ctrl=1, shift=2, alt=4, meta=8
234
+ */
235
+ getMod(f: FensterPointer): number;
236
+ /**
237
+ * Check if the window was resized since the last call
238
+ *
239
+ * Returns the new dimensions if resized, or null if no resize occurred.
240
+ * Clears the resize flag on read.
241
+ */
242
+ getResized(f: FensterPointer): {
243
+ width: number;
244
+ height: number;
245
+ } | null;
246
+ /**
247
+ * Get the backing scale factor (1.0 = normal, 2.0 = Retina)
248
+ */
249
+ getScale(f: FensterPointer): number;
250
+ /**
251
+ * Override the backing scale factor
252
+ *
253
+ * Recomputes physical dimensions from logical size * scale.
254
+ */
255
+ setScale(f: FensterPointer, scale: number): void;
256
+ /**
257
+ * Get the window size
258
+ */
259
+ getSize(f: FensterPointer): {
260
+ width: number;
261
+ height: number;
262
+ };
263
+ }
264
+ /**
265
+ * Get the Fenster API singleton
266
+ */
267
+ declare const getFenster: () => Fenster;
268
+ /**
269
+ * Check if fenster is available without throwing
270
+ */
271
+ declare const isFensterAvailable: () => boolean;
272
+
273
+ /**
274
+ * Input Stream for Ink
275
+ *
276
+ * A Readable stream that provides keyboard input to Ink from native window events.
277
+ * Implements the TTY interface expected by Ink.
278
+ */
279
+
280
+ /**
281
+ * Input Stream
282
+ *
283
+ * Wraps native keyboard events in a Node.js Readable stream that Ink
284
+ * can use as stdin.
285
+ */
286
+ declare class InputStream extends Readable {
287
+ /** TTY interface properties expected by Ink */
288
+ isTTY: boolean;
289
+ isRaw: boolean;
290
+ private buffer;
291
+ private waiting;
292
+ constructor();
293
+ /**
294
+ * Push a key sequence into the stream
295
+ */
296
+ pushKey(sequence: string): void;
297
+ /**
298
+ * Implement Readable._read
299
+ */
300
+ _read(): void;
301
+ /**
302
+ * Set raw mode (no-op, we're always raw)
303
+ */
304
+ setRawMode(_mode: boolean): this;
305
+ /**
306
+ * Check if stream has buffered data
307
+ */
308
+ hasData(): boolean;
309
+ /**
310
+ * Clear the input buffer
311
+ */
312
+ clear(): void;
313
+ /**
314
+ * Close the stream
315
+ */
316
+ close(): void;
317
+ /**
318
+ * Keep the event loop alive (no-op)
319
+ */
320
+ ref(): this;
321
+ /**
322
+ * Allow event loop to exit (no-op)
323
+ */
324
+ unref(): this;
325
+ }
326
+
327
+ /**
328
+ * OutputStream Types
329
+ */
330
+ /** Duck-typed interface for any UI renderer compatible with OutputStream */
331
+ interface UiRendererLike {
332
+ getDimensions(): {
333
+ columns: number;
334
+ rows: number;
335
+ };
336
+ processAnsi(text: string): void;
337
+ present(): void;
338
+ clear(): void;
339
+ getCursorPos(): {
340
+ x: number;
341
+ y: number;
342
+ };
343
+ }
344
+
345
+ /**
346
+ * Output Stream for Ink
347
+ *
348
+ * A Writable stream that intercepts Ink's ANSI output and renders it
349
+ * to a native window.
350
+ */
351
+
352
+ /**
353
+ * Output Stream
354
+ *
355
+ * Wraps a UiRenderer in a Node.js Writable stream that Ink
356
+ * can use as stdout.
357
+ */
358
+ declare class OutputStream extends Writable {
359
+ /** TTY interface property expected by Ink */
360
+ isTTY: boolean;
361
+ private uiRenderer;
362
+ constructor(uiRenderer: UiRendererLike);
363
+ /**
364
+ * Get terminal columns
365
+ */
366
+ get columns(): number;
367
+ /**
368
+ * Get terminal rows
369
+ */
370
+ get rows(): number;
371
+ /**
372
+ * Notify Ink of resize
373
+ */
374
+ notifyResize(): void;
375
+ /**
376
+ * Implement Writable._write
377
+ */
378
+ _write(chunk: Buffer | string, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
379
+ /**
380
+ * Get the underlying renderer
381
+ */
382
+ getRenderer(): UiRendererLike;
383
+ /**
384
+ * Clear the screen
385
+ */
386
+ clear(): void;
387
+ /**
388
+ * Write a string directly (bypasses Writable buffering)
389
+ */
390
+ writeSync(text: string): void;
391
+ /**
392
+ * Get cursor position
393
+ */
394
+ getCursorPos(): {
395
+ x: number;
396
+ y: number;
397
+ };
398
+ }
399
+
400
+ /** Options for creating a UiRenderer */
401
+ interface UiRendererOptions {
402
+ /** Window width in pixels (default 800) */
403
+ width?: number;
404
+ /** Window height in pixels (default 600) */
405
+ height?: number;
406
+ /** Window title (default "ink-native") */
407
+ title?: string;
408
+ /** Background color as RGB tuple [r, g, b] or hex string "#RRGGBB" */
409
+ backgroundColor?: [number, number, number] | string | undefined;
410
+ /** HiDPI scale factor override (number = override, null/undefined = auto-detect) */
411
+ scaleFactor?: number | null;
412
+ }
413
+ /** Direct access to the native framebuffer pixel buffer */
414
+ interface Framebuffer {
415
+ /** Pixel buffer in 0xAARRGGBB format (physical resolution) */
416
+ pixels: Uint32Array;
417
+ /** Physical width in pixels */
418
+ width: number;
419
+ /** Physical height in pixels */
420
+ height: number;
421
+ }
422
+ /** Result from processing native window events */
423
+ interface ProcessEventsResult {
424
+ /** Key events detected by diffing the keys array */
425
+ keyEvents: FensterKeyEvent[];
426
+ /** Current modifier bitmask */
427
+ mod: number;
428
+ /** Whether the window was resized this frame */
429
+ resized: boolean;
430
+ }
431
+
432
+ /**
433
+ * UI Renderer
434
+ *
435
+ * Renders Ink UI to a native framebuffer window by parsing ANSI sequences
436
+ * and drawing bitmap font glyphs directly into a pixel buffer.
437
+ * No GPU, no textures -- pure TypeScript pixel manipulation.
438
+ */
439
+
440
+ /**
441
+ * Pack RGB color into fenster's 0xAARRGGBB pixel format
442
+ */
443
+ declare const packColor: (r: number, g: number, b: number) => number;
444
+ /**
445
+ * UI Renderer
446
+ *
447
+ * Renders Ink UI to a native framebuffer by parsing ANSI sequences
448
+ * and drawing bitmap font glyphs. Uses a JS-side Uint32Array for
449
+ * rendering, then copies to the native buffer on present.
450
+ */
451
+ declare class UiRenderer {
452
+ private fenster;
453
+ private fensterPtr;
454
+ private fontRenderer;
455
+ private ansiParser;
456
+ /** Logical window dimensions (in points) */
457
+ private windowWidth;
458
+ private windowHeight;
459
+ /** Physical framebuffer dimensions (in pixels) */
460
+ private physicalWidth;
461
+ private physicalHeight;
462
+ /** Backing scale factor */
463
+ private scaleFactor;
464
+ /** User-requested scale factor override (null = auto-detect) */
465
+ private userScaleFactor;
466
+ /** Scaled glyph dimensions (in physical pixels) */
467
+ private scaledGlyphWidth;
468
+ private scaledGlyphHeight;
469
+ private columns;
470
+ private rows;
471
+ /** JS-side framebuffer for rendering (at physical resolution) */
472
+ private framebuffer;
473
+ /** Previous key state for diff-based event detection */
474
+ private prevKeys;
475
+ private fgColor;
476
+ private bgColor;
477
+ private defaultBgColor;
478
+ private bold;
479
+ private dim;
480
+ private italic;
481
+ private underline;
482
+ private strikethrough;
483
+ private reverse;
484
+ private shouldQuit;
485
+ private pendingCommands;
486
+ constructor(options?: UiRendererOptions);
487
+ /**
488
+ * Copy JS framebuffer to native fenster buffer
489
+ */
490
+ private copyToNativeBuffer;
491
+ /**
492
+ * Process ANSI output from Ink
493
+ */
494
+ processAnsi(output: string): void;
495
+ /**
496
+ * Render pending commands and prepare for present
497
+ */
498
+ present(): void;
499
+ /**
500
+ * Process fenster events and present the buffer to screen
501
+ *
502
+ * Calls fenster_loop (which presents the buffer + polls events),
503
+ * then diffs the keys array to detect press/release events.
504
+ */
505
+ processEventsAndPresent(): ProcessEventsResult;
506
+ /**
507
+ * Handle window resize by updating dimensions and recreating the framebuffer
508
+ *
509
+ * Receives physical dimensions from getResized() and derives logical
510
+ * dimensions using the current scale factor.
511
+ */
512
+ private handleResize;
513
+ /**
514
+ * Convert a fenster key event to a terminal escape sequence
515
+ */
516
+ keyEventToSequence(event: FensterKeyEvent, mod: number): string | null;
517
+ /**
518
+ * Check if quit was requested
519
+ */
520
+ shouldClose(): boolean;
521
+ /**
522
+ * Reset input state (no-op for fenster, modifier state is polled)
523
+ */
524
+ resetInputState(): void;
525
+ /**
526
+ * Get the display refresh rate (fixed for fenster)
527
+ */
528
+ getDisplayRefreshRate(): number;
529
+ /**
530
+ * Get terminal dimensions
531
+ */
532
+ getDimensions(): {
533
+ columns: number;
534
+ rows: number;
535
+ };
536
+ /**
537
+ * Get direct access to the framebuffer pixel buffer.
538
+ *
539
+ * The returned `pixels` array is the same backing buffer used by the
540
+ * renderer, so writes are immediately visible on the next `present()` call.
541
+ * Pixel format is 0xAARRGGBB — use `packColor(r, g, b)` to create values.
542
+ */
543
+ getFramebuffer(): Framebuffer;
544
+ /**
545
+ * Clear the entire screen
546
+ */
547
+ clear(): void;
548
+ /**
549
+ * Get cursor position
550
+ */
551
+ getCursorPos(): {
552
+ x: number;
553
+ y: number;
554
+ };
555
+ /**
556
+ * Execute a single draw command
557
+ */
558
+ private executeCommand;
559
+ /**
560
+ * Render text at position (using scaled coordinates for physical resolution)
561
+ */
562
+ private renderText;
563
+ /**
564
+ * Fill a rectangle in the framebuffer (physical pixel coordinates)
565
+ */
566
+ private fillRect;
567
+ /**
568
+ * Clear a line from a specific position (scaled coordinates)
569
+ */
570
+ private clearLine;
571
+ /**
572
+ * Clean up resources
573
+ */
574
+ destroy(): void;
575
+ /**
576
+ * Reset state for reuse
577
+ */
578
+ reset(): void;
579
+ }
580
+
581
+ /**
582
+ * Options for creating streams
583
+ */
584
+ interface StreamsOptions {
585
+ /** Window title */
586
+ title?: string;
587
+ /** Window width in pixels */
588
+ width?: number;
589
+ /** Window height in pixels */
590
+ height?: number;
591
+ /** Background color as RGB tuple [r, g, b] or hex string "#RRGGBB" */
592
+ backgroundColor?: [number, number, number] | string | undefined;
593
+ /** Force a specific frame rate instead of default 60fps */
594
+ frameRate?: number | undefined;
595
+ /** HiDPI scale factor override (number = override, null/undefined = auto-detect) */
596
+ scaleFactor?: number | null;
597
+ }
598
+ /**
599
+ * Result of createStreams
600
+ */
601
+ interface Streams {
602
+ /** Readable stream for keyboard input */
603
+ stdin: InputStream;
604
+ /** Writable stream for ANSI output */
605
+ stdout: OutputStream;
606
+ /** Window wrapper with events */
607
+ window: Window;
608
+ /** UI renderer (for advanced use) */
609
+ renderer: UiRenderer;
610
+ }
611
+
612
+ /**
613
+ * Window and Streams for Ink
614
+ *
615
+ * Factory function to create stdin/stdout streams that render to a
616
+ * native framebuffer window.
617
+ */
618
+
619
+ /**
620
+ * Window wrapper that emits events
621
+ *
622
+ * Manages the event loop, key event processing, and stream coordination
623
+ * for the native window backend.
624
+ */
625
+ declare class Window extends EventEmitter {
626
+ private renderer;
627
+ private eventLoopHandle;
628
+ private inputStream;
629
+ private outputStream;
630
+ private closed;
631
+ private paused;
632
+ private currentFrameRate;
633
+ constructor(renderer: UiRenderer, inputStream: InputStream, outputStream: OutputStream, frameRate?: number);
634
+ /**
635
+ * Start the event loop
636
+ */
637
+ private startEventLoop;
638
+ /**
639
+ * Run a single iteration of the event loop
640
+ */
641
+ private runEventLoopIteration;
642
+ /**
643
+ * Get terminal dimensions
644
+ */
645
+ getDimensions(): {
646
+ columns: number;
647
+ rows: number;
648
+ };
649
+ /**
650
+ * Clear the screen
651
+ */
652
+ clear(): void;
653
+ /**
654
+ * Close the window
655
+ */
656
+ close(): void;
657
+ /**
658
+ * Check if window is closed
659
+ */
660
+ isClosed(): boolean;
661
+ /**
662
+ * Pause the Ink event loop so the caller can take over rendering.
663
+ *
664
+ * While paused, call `renderer.processEventsAndPresent()` manually
665
+ * in your own loop to poll events and present the framebuffer.
666
+ */
667
+ pause(): void;
668
+ /**
669
+ * Resume the Ink event loop after pausing.
670
+ */
671
+ resume(): void;
672
+ /**
673
+ * Check if the event loop is paused
674
+ */
675
+ isPaused(): boolean;
676
+ /**
677
+ * Get the output stream
678
+ */
679
+ getOutputStream(): OutputStream;
680
+ /**
681
+ * Get the current frame rate
682
+ */
683
+ getFrameRate(): number;
684
+ }
685
+ /**
686
+ * Create streams for use with Ink
687
+ *
688
+ * Creates stdin/stdout streams backed by a native window.
689
+ *
690
+ * @example
691
+ * ```typescript
692
+ * import { render, Text, Box } from "ink";
693
+ * import { createStreams } from "ink-native";
694
+ *
695
+ * const App = () => (
696
+ * <Box flexDirection="column">
697
+ * <Text color="green">Hello from ink-native!</Text>
698
+ * </Box>
699
+ * );
700
+ *
701
+ * const { stdin, stdout, window } = createStreams({
702
+ * title: "My App",
703
+ * width: 800,
704
+ * height: 600,
705
+ * });
706
+ *
707
+ * render(<App />, { stdin, stdout });
708
+ *
709
+ * window.on("close", () => process.exit(0));
710
+ * ```
711
+ */
712
+ declare const createStreams: (options?: StreamsOptions) => Streams;
713
+
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 };