ink-sdl 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/dist/index.js ADDED
@@ -0,0 +1,2280 @@
1
+ // src/streams/index.ts
2
+ import { EventEmitter } from "events";
3
+
4
+ // src/sdl/index.ts
5
+ import koffi from "koffi";
6
+ import { platform } from "os";
7
+ import { existsSync } from "fs";
8
+
9
+ // src/sdl/consts.ts
10
+ var SDL_INIT_VIDEO = 32;
11
+ var SDL_INIT_EVENTS = 16384;
12
+ var SDL_WINDOW_SHOWN = 4;
13
+ var SDL_WINDOW_RESIZABLE = 32;
14
+ var SDL_WINDOW_ALLOW_HIGHDPI = 8192;
15
+ var SDL_WINDOWPOS_CENTERED = 805240832;
16
+ var SDL_RENDERER_ACCELERATED = 2;
17
+ var SDL_RENDERER_PRESENTVSYNC = 4;
18
+ var SDL_RENDERER_SOFTWARE = 1;
19
+ var SDL_BLENDMODE_BLEND = 1;
20
+ var SDL_QUIT = 256;
21
+ var SDL_WINDOWEVENT = 512;
22
+ var SDL_KEYDOWN = 768;
23
+ var SDL_KEYUP = 769;
24
+ var SDL_WINDOWEVENT_SIZE_CHANGED = 6;
25
+ var SDL_WINDOWEVENT_CLOSE = 14;
26
+ var SDLK_RETURN = 13;
27
+ var SDLK_ESCAPE = 27;
28
+ var SDLK_SPACE = 32;
29
+ var SDLK_BACKSPACE = 8;
30
+ var SDLK_TAB = 9;
31
+ var SDLK_DELETE = 127;
32
+ var SDLK_RIGHT = 1073741903;
33
+ var SDLK_LEFT = 1073741904;
34
+ var SDLK_DOWN = 1073741905;
35
+ var SDLK_UP = 1073741906;
36
+ var SDLK_HOME = 1073741898;
37
+ var SDLK_END = 1073741901;
38
+ var SDLK_PAGEUP = 1073741899;
39
+ var SDLK_PAGEDOWN = 1073741902;
40
+ var SDLK_F1 = 1073741882;
41
+ var SDLK_F12 = 1073741893;
42
+ var SDLK_LSHIFT = 1073742049;
43
+ var SDLK_RSHIFT = 1073742053;
44
+ var SDLK_LCTRL = 1073742048;
45
+ var SDLK_RCTRL = 1073742052;
46
+ var SDLK_LALT = 1073742050;
47
+ var SDLK_RALT = 1073742054;
48
+ var INT32_BYTES = 4;
49
+ var SDL_RECT_SIZE = 16;
50
+ var SDL_RECT_X_OFFSET = 0;
51
+ var SDL_RECT_Y_OFFSET = 4;
52
+ var SDL_RECT_W_OFFSET = 8;
53
+ var SDL_RECT_H_OFFSET = 12;
54
+ var SDL_EVENT_SIZE = 56;
55
+ var SDL_WINDOW_EVENT_OFFSET = 12;
56
+ var SDL_KEY_STATE_OFFSET = 12;
57
+ var SDL_KEY_REPEAT_OFFSET = 13;
58
+ var SDL_KEYSYM_SYM_OFFSET = 20;
59
+ var ASCII_A_LOWER = 97;
60
+ var ASCII_Z_LOWER = 122;
61
+
62
+ // src/sdl/index.ts
63
+ var SDL_LIB_PATHS = {
64
+ darwin: [
65
+ "/opt/homebrew/lib/libSDL2.dylib",
66
+ // Homebrew ARM
67
+ "/usr/local/lib/libSDL2.dylib",
68
+ // Homebrew Intel
69
+ "/opt/local/lib/libSDL2.dylib",
70
+ // MacPorts
71
+ "libSDL2.dylib"
72
+ // System path
73
+ ],
74
+ linux: [
75
+ "/usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0",
76
+ // Debian/Ubuntu x64
77
+ "/usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0",
78
+ // Debian/Ubuntu ARM64
79
+ "/usr/lib64/libSDL2-2.0.so.0",
80
+ // Fedora/RHEL
81
+ "/usr/lib/libSDL2-2.0.so.0",
82
+ // Arch
83
+ "libSDL2-2.0.so.0"
84
+ // System path
85
+ ],
86
+ win32: ["SDL2.dll", "C:\\Windows\\System32\\SDL2.dll"]
87
+ };
88
+ var SDL_TTF_LIB_PATHS = {
89
+ darwin: [
90
+ "/opt/homebrew/lib/libSDL2_ttf.dylib",
91
+ // Homebrew ARM
92
+ "/usr/local/lib/libSDL2_ttf.dylib",
93
+ // Homebrew Intel
94
+ "/opt/local/lib/libSDL2_ttf.dylib",
95
+ // MacPorts
96
+ "libSDL2_ttf.dylib"
97
+ // System path
98
+ ],
99
+ linux: [
100
+ "/usr/lib/x86_64-linux-gnu/libSDL2_ttf-2.0.so.0",
101
+ // Debian/Ubuntu x64
102
+ "/usr/lib/aarch64-linux-gnu/libSDL2_ttf-2.0.so.0",
103
+ // Debian/Ubuntu ARM64
104
+ "/usr/lib64/libSDL2_ttf-2.0.so.0",
105
+ // Fedora/RHEL
106
+ "/usr/lib/libSDL2_ttf-2.0.so.0",
107
+ // Arch
108
+ "libSDL2_ttf-2.0.so.0"
109
+ // System path
110
+ ],
111
+ win32: ["SDL2_ttf.dll", "C:\\Windows\\System32\\SDL2_ttf.dll"]
112
+ };
113
+ var findLibrary = (pathMap) => {
114
+ const plat = platform();
115
+ const paths = pathMap[plat] ?? [];
116
+ for (const path of paths) {
117
+ if (!path.includes("/") && !path.includes("\\")) {
118
+ return path;
119
+ }
120
+ if (existsSync(path)) {
121
+ return path;
122
+ }
123
+ }
124
+ return paths[paths.length - 1] ?? null;
125
+ };
126
+ var findSDLLibrary = () => {
127
+ return findLibrary(SDL_LIB_PATHS);
128
+ };
129
+ var findSDLTtfLibrary = () => {
130
+ return findLibrary(SDL_TTF_LIB_PATHS);
131
+ };
132
+ var SDL2API = class {
133
+ lib;
134
+ initialized = false;
135
+ // Core functions
136
+ _SDL_Init;
137
+ _SDL_Quit;
138
+ _SDL_GetError;
139
+ // Window functions
140
+ _SDL_CreateWindow;
141
+ _SDL_DestroyWindow;
142
+ _SDL_SetWindowTitle;
143
+ _SDL_GetWindowSize;
144
+ _SDL_RaiseWindow;
145
+ // Renderer functions
146
+ _SDL_CreateRenderer;
147
+ _SDL_DestroyRenderer;
148
+ _SDL_RenderClear;
149
+ _SDL_RenderPresent;
150
+ _SDL_RenderCopy;
151
+ _SDL_SetRenderDrawColor;
152
+ // Texture functions
153
+ _SDL_CreateTexture;
154
+ _SDL_DestroyTexture;
155
+ _SDL_UpdateTexture;
156
+ _SDL_CreateTextureFromSurface;
157
+ _SDL_SetTextureBlendMode;
158
+ _SDL_SetTextureColorMod;
159
+ // Surface functions
160
+ _SDL_FreeSurface;
161
+ // Additional renderer functions
162
+ _SDL_GetRendererOutputSize;
163
+ _SDL_RenderFillRect;
164
+ _SDL_SetRenderTarget;
165
+ // HiDPI functions
166
+ _SDL_GL_GetDrawableSize;
167
+ // Event functions
168
+ _SDL_PollEvent;
169
+ constructor() {
170
+ const libPath = findSDLLibrary();
171
+ if (!libPath) {
172
+ throw new Error(
173
+ "SDL2 library not found. Please install SDL2:\n macOS: brew install sdl2\n Linux: apt install libsdl2-2.0-0 (or equivalent)\n Windows: Download SDL2.dll from libsdl.org"
174
+ );
175
+ }
176
+ try {
177
+ this.lib = koffi.load(libPath);
178
+ this.bindFunctions();
179
+ } catch (err) {
180
+ const message = err instanceof Error ? err.message : String(err);
181
+ throw new Error(
182
+ `Failed to load SDL2 library from ${libPath}: ${message}`
183
+ );
184
+ }
185
+ }
186
+ bindFunctions() {
187
+ this._SDL_Init = this.lib.func("int SDL_Init(uint32_t flags)");
188
+ this._SDL_Quit = this.lib.func("void SDL_Quit()");
189
+ this._SDL_GetError = this.lib.func("const char* SDL_GetError()");
190
+ this._SDL_CreateWindow = this.lib.func(
191
+ "void* SDL_CreateWindow(const char* title, int x, int y, int w, int h, uint32_t flags)"
192
+ );
193
+ this._SDL_DestroyWindow = this.lib.func(
194
+ "void SDL_DestroyWindow(void* window)"
195
+ );
196
+ this._SDL_SetWindowTitle = this.lib.func(
197
+ "void SDL_SetWindowTitle(void* window, const char* title)"
198
+ );
199
+ this._SDL_GetWindowSize = this.lib.func(
200
+ "void SDL_GetWindowSize(void* window, int* w, int* h)"
201
+ );
202
+ this._SDL_RaiseWindow = this.lib.func("void SDL_RaiseWindow(void* window)");
203
+ this._SDL_CreateRenderer = this.lib.func(
204
+ "void* SDL_CreateRenderer(void* window, int index, uint32_t flags)"
205
+ );
206
+ this._SDL_DestroyRenderer = this.lib.func(
207
+ "void SDL_DestroyRenderer(void* renderer)"
208
+ );
209
+ this._SDL_RenderClear = this.lib.func(
210
+ "int SDL_RenderClear(void* renderer)"
211
+ );
212
+ this._SDL_RenderPresent = this.lib.func(
213
+ "void SDL_RenderPresent(void* renderer)"
214
+ );
215
+ this._SDL_RenderCopy = this.lib.func(
216
+ "int SDL_RenderCopy(void* renderer, void* texture, void* srcrect, void* dstrect)"
217
+ );
218
+ this._SDL_SetRenderDrawColor = this.lib.func(
219
+ "int SDL_SetRenderDrawColor(void* renderer, uint8_t r, uint8_t g, uint8_t b, uint8_t a)"
220
+ );
221
+ this._SDL_CreateTexture = this.lib.func(
222
+ "void* SDL_CreateTexture(void* renderer, uint32_t format, int access, int w, int h)"
223
+ );
224
+ this._SDL_DestroyTexture = this.lib.func(
225
+ "void SDL_DestroyTexture(void* texture)"
226
+ );
227
+ this._SDL_UpdateTexture = this.lib.func(
228
+ "int SDL_UpdateTexture(void* texture, void* rect, const void* pixels, int pitch)"
229
+ );
230
+ this._SDL_CreateTextureFromSurface = this.lib.func(
231
+ "void* SDL_CreateTextureFromSurface(void* renderer, void* surface)"
232
+ );
233
+ this._SDL_SetTextureBlendMode = this.lib.func(
234
+ "int SDL_SetTextureBlendMode(void* texture, int blendMode)"
235
+ );
236
+ this._SDL_SetTextureColorMod = this.lib.func(
237
+ "int SDL_SetTextureColorMod(void* texture, uint8_t r, uint8_t g, uint8_t b)"
238
+ );
239
+ this._SDL_FreeSurface = this.lib.func(
240
+ "void SDL_FreeSurface(void* surface)"
241
+ );
242
+ this._SDL_GetRendererOutputSize = this.lib.func(
243
+ "int SDL_GetRendererOutputSize(void* renderer, int* w, int* h)"
244
+ );
245
+ this._SDL_RenderFillRect = this.lib.func(
246
+ "int SDL_RenderFillRect(void* renderer, void* rect)"
247
+ );
248
+ this._SDL_SetRenderTarget = this.lib.func(
249
+ "int SDL_SetRenderTarget(void* renderer, void* texture)"
250
+ );
251
+ this._SDL_GL_GetDrawableSize = this.lib.func(
252
+ "void SDL_GL_GetDrawableSize(void* window, int* w, int* h)"
253
+ );
254
+ this._SDL_PollEvent = this.lib.func("int SDL_PollEvent(void* event)");
255
+ }
256
+ /**
257
+ * Initialize SDL with the given subsystems
258
+ */
259
+ init(flags = SDL_INIT_VIDEO | SDL_INIT_EVENTS) {
260
+ if (this.initialized) {
261
+ return true;
262
+ }
263
+ const result = this._SDL_Init(flags);
264
+ if (result !== 0) {
265
+ return false;
266
+ }
267
+ this.initialized = true;
268
+ return true;
269
+ }
270
+ /**
271
+ * Shutdown SDL
272
+ */
273
+ quit() {
274
+ if (this.initialized) {
275
+ this._SDL_Quit();
276
+ this.initialized = false;
277
+ }
278
+ }
279
+ /**
280
+ * Get the last SDL error message
281
+ */
282
+ getError() {
283
+ return this._SDL_GetError();
284
+ }
285
+ /**
286
+ * Create a window
287
+ */
288
+ createWindow(title, x, y, width, height, flags) {
289
+ const window = this._SDL_CreateWindow(title, x, y, width, height, flags);
290
+ if (!window) {
291
+ throw new Error(`SDL_CreateWindow failed: ${this._SDL_GetError()}`);
292
+ }
293
+ return window;
294
+ }
295
+ /**
296
+ * Destroy a window
297
+ */
298
+ destroyWindow(window) {
299
+ this._SDL_DestroyWindow(window);
300
+ }
301
+ /**
302
+ * Set window title
303
+ */
304
+ setWindowTitle(window, title) {
305
+ this._SDL_SetWindowTitle(window, title);
306
+ }
307
+ /**
308
+ * Get window size
309
+ */
310
+ getWindowSize(window) {
311
+ const wBuf = Buffer.alloc(INT32_BYTES);
312
+ const hBuf = Buffer.alloc(INT32_BYTES);
313
+ this._SDL_GetWindowSize(window, wBuf, hBuf);
314
+ return {
315
+ width: wBuf.readInt32LE(0),
316
+ height: hBuf.readInt32LE(0)
317
+ };
318
+ }
319
+ /**
320
+ * Raise window to front and give it keyboard focus
321
+ */
322
+ raiseWindow(window) {
323
+ this._SDL_RaiseWindow(window);
324
+ }
325
+ /**
326
+ * Create a renderer for a window
327
+ */
328
+ createRenderer(window, index = -1, flags = SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) {
329
+ const renderer = this._SDL_CreateRenderer(window, index, flags);
330
+ if (!renderer) {
331
+ const softwareRenderer = this._SDL_CreateRenderer(
332
+ window,
333
+ -1,
334
+ SDL_RENDERER_SOFTWARE
335
+ );
336
+ if (!softwareRenderer) {
337
+ throw new Error(`SDL_CreateRenderer failed: ${this._SDL_GetError()}`);
338
+ }
339
+ return softwareRenderer;
340
+ }
341
+ return renderer;
342
+ }
343
+ /**
344
+ * Destroy a renderer
345
+ */
346
+ destroyRenderer(renderer) {
347
+ this._SDL_DestroyRenderer(renderer);
348
+ }
349
+ /**
350
+ * Clear the renderer
351
+ */
352
+ renderClear(renderer) {
353
+ this._SDL_RenderClear(renderer);
354
+ }
355
+ /**
356
+ * Present the renderer (flip buffers)
357
+ */
358
+ renderPresent(renderer) {
359
+ this._SDL_RenderPresent(renderer);
360
+ }
361
+ /**
362
+ * Copy texture to renderer
363
+ */
364
+ renderCopy(renderer, texture, srcRect = null, dstRect = null) {
365
+ this._SDL_RenderCopy(renderer, texture, srcRect, dstRect);
366
+ }
367
+ /**
368
+ * Set render draw color
369
+ */
370
+ setRenderDrawColor(renderer, r, g, b, a = 255) {
371
+ this._SDL_SetRenderDrawColor(renderer, r, g, b, a);
372
+ }
373
+ /**
374
+ * Create a texture
375
+ */
376
+ createTexture(renderer, format, access, width, height) {
377
+ const texture = this._SDL_CreateTexture(
378
+ renderer,
379
+ format,
380
+ access,
381
+ width,
382
+ height
383
+ );
384
+ if (!texture) {
385
+ throw new Error(`SDL_CreateTexture failed: ${this._SDL_GetError()}`);
386
+ }
387
+ return texture;
388
+ }
389
+ /**
390
+ * Destroy a texture
391
+ */
392
+ destroyTexture(texture) {
393
+ this._SDL_DestroyTexture(texture);
394
+ }
395
+ /**
396
+ * Update texture with pixel data
397
+ */
398
+ updateTexture(texture, pixels, pitch) {
399
+ this._SDL_UpdateTexture(texture, null, pixels, pitch);
400
+ }
401
+ /**
402
+ * Create a texture from an SDL surface
403
+ */
404
+ createTextureFromSurface(renderer, surface) {
405
+ const texture = this._SDL_CreateTextureFromSurface(renderer, surface);
406
+ if (!texture) {
407
+ throw new Error(
408
+ `SDL_CreateTextureFromSurface failed: ${this._SDL_GetError()}`
409
+ );
410
+ }
411
+ return texture;
412
+ }
413
+ /**
414
+ * Set texture blend mode
415
+ */
416
+ setTextureBlendMode(texture, blendMode) {
417
+ this._SDL_SetTextureBlendMode(texture, blendMode);
418
+ }
419
+ /**
420
+ * Set texture color modulation (tint)
421
+ */
422
+ setTextureColorMod(texture, r, g, b) {
423
+ this._SDL_SetTextureColorMod(texture, r, g, b);
424
+ }
425
+ /**
426
+ * Free an SDL surface
427
+ */
428
+ freeSurface(surface) {
429
+ this._SDL_FreeSurface(surface);
430
+ }
431
+ /**
432
+ * Get the output size of a renderer (physical pixels)
433
+ */
434
+ getRendererOutputSize(renderer) {
435
+ const wBuf = Buffer.alloc(INT32_BYTES);
436
+ const hBuf = Buffer.alloc(INT32_BYTES);
437
+ const result = this._SDL_GetRendererOutputSize(renderer, wBuf, hBuf);
438
+ if (result !== 0) {
439
+ return { width: 0, height: 0 };
440
+ }
441
+ return {
442
+ width: wBuf.readInt32LE(0),
443
+ height: hBuf.readInt32LE(0)
444
+ };
445
+ }
446
+ /**
447
+ * Fill a rectangle with the current draw color
448
+ */
449
+ renderFillRect(renderer, rect) {
450
+ this._SDL_RenderFillRect(renderer, rect);
451
+ }
452
+ /**
453
+ * Set the render target (null for default window)
454
+ */
455
+ setRenderTarget(renderer, texture) {
456
+ this._SDL_SetRenderTarget(renderer, texture);
457
+ }
458
+ /**
459
+ * Get drawable size (physical pixels) for HiDPI windows
460
+ */
461
+ getDrawableSize(window) {
462
+ const wBuf = Buffer.alloc(INT32_BYTES);
463
+ const hBuf = Buffer.alloc(INT32_BYTES);
464
+ this._SDL_GL_GetDrawableSize(window, wBuf, hBuf);
465
+ return {
466
+ width: wBuf.readInt32LE(0),
467
+ height: hBuf.readInt32LE(0)
468
+ };
469
+ }
470
+ /**
471
+ * Get the scale factor between logical and physical pixels
472
+ */
473
+ getScaleFactor(window) {
474
+ const logical = this.getWindowSize(window);
475
+ const physical = this.getDrawableSize(window);
476
+ if (logical.width === 0) {
477
+ return 1;
478
+ }
479
+ return physical.width / logical.width;
480
+ }
481
+ /**
482
+ * Get the scale factor using renderer output size (more reliable for non-GL windows)
483
+ */
484
+ getScaleFactorFromRenderer(window, renderer) {
485
+ const logical = this.getWindowSize(window);
486
+ const physical = this.getRendererOutputSize(renderer);
487
+ if (logical.width === 0 || physical.width === 0) {
488
+ return 1;
489
+ }
490
+ return physical.width / logical.width;
491
+ }
492
+ /**
493
+ * Poll for pending events
494
+ */
495
+ pollEvent() {
496
+ const eventBuf = Buffer.alloc(SDL_EVENT_SIZE);
497
+ const hasEvent = this._SDL_PollEvent(eventBuf);
498
+ if (!hasEvent) {
499
+ return null;
500
+ }
501
+ const type = eventBuf.readUInt32LE(0);
502
+ if (type === SDL_WINDOWEVENT) {
503
+ const windowEvent = eventBuf.readUInt8(SDL_WINDOW_EVENT_OFFSET);
504
+ return { type, windowEvent };
505
+ }
506
+ if (type === SDL_KEYDOWN || type === SDL_KEYUP) {
507
+ const pressed = eventBuf.readUInt8(SDL_KEY_STATE_OFFSET) === 1;
508
+ const repeat = eventBuf.readUInt8(SDL_KEY_REPEAT_OFFSET) !== 0;
509
+ const keycode = eventBuf.readInt32LE(SDL_KEYSYM_SYM_OFFSET);
510
+ return { type, keycode, pressed, repeat };
511
+ }
512
+ return { type };
513
+ }
514
+ /**
515
+ * Check if SDL is initialized
516
+ */
517
+ isInitialized() {
518
+ return this.initialized;
519
+ }
520
+ };
521
+ var sdlInstance = null;
522
+ var getSDL2 = () => {
523
+ if (!sdlInstance) {
524
+ sdlInstance = new SDL2API();
525
+ }
526
+ return sdlInstance;
527
+ };
528
+ var isSDL2Available = () => {
529
+ try {
530
+ getSDL2();
531
+ return true;
532
+ } catch {
533
+ return false;
534
+ }
535
+ };
536
+ var createSDLRect = (x, y, w, h) => {
537
+ const buf = Buffer.alloc(SDL_RECT_SIZE);
538
+ buf.writeInt32LE(x, SDL_RECT_X_OFFSET);
539
+ buf.writeInt32LE(y, SDL_RECT_Y_OFFSET);
540
+ buf.writeInt32LE(w, SDL_RECT_W_OFFSET);
541
+ buf.writeInt32LE(h, SDL_RECT_H_OFFSET);
542
+ return buf;
543
+ };
544
+ var SDL_ttfAPI = class {
545
+ lib;
546
+ initialized = false;
547
+ // TTF functions
548
+ _TTF_Init;
549
+ _TTF_Quit;
550
+ _TTF_OpenFont;
551
+ _TTF_CloseFont;
552
+ _TTF_RenderUTF8_Blended;
553
+ _TTF_SizeUTF8;
554
+ constructor() {
555
+ const libPath = findSDLTtfLibrary();
556
+ if (!libPath) {
557
+ throw new Error(
558
+ "SDL2_ttf library not found. Please install SDL2_ttf:\n macOS: brew install sdl2_ttf\n Linux: apt install libsdl2-ttf-2.0-0 (or equivalent)\n Windows: Download SDL2_ttf.dll from libsdl.org"
559
+ );
560
+ }
561
+ try {
562
+ this.lib = koffi.load(libPath);
563
+ this.bindFunctions();
564
+ } catch (err) {
565
+ const message = err instanceof Error ? err.message : String(err);
566
+ throw new Error(
567
+ `Failed to load SDL2_ttf library from ${libPath}: ${message}`
568
+ );
569
+ }
570
+ }
571
+ bindFunctions() {
572
+ this._TTF_Init = this.lib.func("int TTF_Init()");
573
+ this._TTF_Quit = this.lib.func("void TTF_Quit()");
574
+ this._TTF_OpenFont = this.lib.func(
575
+ "void* TTF_OpenFont(const char* file, int ptsize)"
576
+ );
577
+ this._TTF_CloseFont = this.lib.func("void TTF_CloseFont(void* font)");
578
+ koffi.struct("SDL_Color", {
579
+ r: "uint8_t",
580
+ g: "uint8_t",
581
+ b: "uint8_t",
582
+ a: "uint8_t"
583
+ });
584
+ this._TTF_RenderUTF8_Blended = this.lib.func(
585
+ "void* TTF_RenderUTF8_Blended(void* font, const char* text, SDL_Color fg)"
586
+ );
587
+ this._TTF_SizeUTF8 = this.lib.func(
588
+ "int TTF_SizeUTF8(void* font, const char* text, int* w, int* h)"
589
+ );
590
+ }
591
+ /**
592
+ * Initialize SDL_ttf
593
+ */
594
+ init() {
595
+ if (this.initialized) {
596
+ return true;
597
+ }
598
+ const result = this._TTF_Init();
599
+ if (result !== 0) {
600
+ return false;
601
+ }
602
+ this.initialized = true;
603
+ return true;
604
+ }
605
+ /**
606
+ * Shutdown SDL_ttf
607
+ */
608
+ quit() {
609
+ if (this.initialized) {
610
+ this._TTF_Quit();
611
+ this.initialized = false;
612
+ }
613
+ }
614
+ /**
615
+ * Get the last SDL_ttf error message
616
+ */
617
+ getError() {
618
+ return getSDL2().getError();
619
+ }
620
+ /**
621
+ * Open a TrueType font file
622
+ */
623
+ openFont(file, ptsize) {
624
+ const font = this._TTF_OpenFont(file, ptsize);
625
+ if (!font) {
626
+ throw new Error(`TTF_OpenFont failed: ${getSDL2().getError()}`);
627
+ }
628
+ return font;
629
+ }
630
+ /**
631
+ * Close a font
632
+ */
633
+ closeFont(font) {
634
+ this._TTF_CloseFont(font);
635
+ }
636
+ /**
637
+ * Render UTF-8 text to a surface with blended (anti-aliased) rendering
638
+ */
639
+ renderTextBlended(font, text, r, g, b, a = 255) {
640
+ const color = { r, g, b, a };
641
+ const surface = this._TTF_RenderUTF8_Blended(font, text, color);
642
+ if (!surface) {
643
+ throw new Error(`TTF_RenderUTF8_Blended failed: ${getSDL2().getError()}`);
644
+ }
645
+ return surface;
646
+ }
647
+ /**
648
+ * Get the dimensions of rendered text without actually rendering
649
+ */
650
+ sizeText(font, text) {
651
+ const wBuf = Buffer.alloc(INT32_BYTES);
652
+ const hBuf = Buffer.alloc(INT32_BYTES);
653
+ const result = this._TTF_SizeUTF8(font, text, wBuf, hBuf);
654
+ if (result !== 0) {
655
+ return { width: 0, height: 0 };
656
+ }
657
+ return {
658
+ width: wBuf.readInt32LE(0),
659
+ height: hBuf.readInt32LE(0)
660
+ };
661
+ }
662
+ /**
663
+ * Check if SDL_ttf is initialized
664
+ */
665
+ isInitialized() {
666
+ return this.initialized;
667
+ }
668
+ };
669
+ var ttfInstance = null;
670
+ var getSDL_ttf = () => {
671
+ if (!ttfInstance) {
672
+ ttfInstance = new SDL_ttfAPI();
673
+ }
674
+ return ttfInstance;
675
+ };
676
+ var isSDL_ttfAvailable = () => {
677
+ try {
678
+ getSDL_ttf();
679
+ return true;
680
+ } catch {
681
+ return false;
682
+ }
683
+ };
684
+
685
+ // src/renderer/consts.ts
686
+ var DEFAULT_WINDOW_WIDTH = 800;
687
+ var DEFAULT_WINDOW_HEIGHT = 600;
688
+ var DEFAULT_COLUMNS = 80;
689
+ var DEFAULT_ROWS = 24;
690
+ var MIN_COLUMNS = 40;
691
+ var MIN_ROWS = 10;
692
+ var DEFAULT_FONT_SIZE = 13;
693
+ var MAX_GLYPH_CACHE_SIZE = 1e3;
694
+ var GLYPH_CACHE_EVICT_DIVISOR = 4;
695
+ var COLOR_CHANNEL_MAX = 255;
696
+ var ANSI_STANDARD_COLOR_COUNT = 8;
697
+ var BOLD_BRIGHTNESS_MULTIPLIER = 1.3;
698
+ var DIM_BRIGHTNESS_MULTIPLIER = 0.5;
699
+ var SCALE_FACTOR_EPSILON = 0.01;
700
+ var ANSI_TAB_WIDTH = 8;
701
+ var ANSI_ERASE_ENTIRE_SCREEN = 2;
702
+ var ANSI_ERASE_TO_END_AND_BEYOND = 3;
703
+ var ANSI_EXTENDED_COLOR_OFFSET_256 = 2;
704
+ var ANSI_EXTENDED_RGB_MIN_PARAMS = 3;
705
+ var ANSI_EXTENDED_COLOR_OFFSET_RGB = 4;
706
+ var ANSI_RGB_R_OFFSET = 1;
707
+ var ANSI_RGB_G_OFFSET = 2;
708
+ var ANSI_RGB_B_OFFSET = 3;
709
+ var ANSI_CUBE_RED_MULTIPLIER = 36;
710
+ var ANSI_256_COLOR_LEVELS = 6;
711
+ var ANSI_CUBE_STEP = 40;
712
+ var ANSI_CUBE_BASE = 55;
713
+ var PACK_RED_SHIFT = 16;
714
+ var PACK_GREEN_SHIFT = 8;
715
+
716
+ // src/renderer/ansi-parser.ts
717
+ var DEFAULT_FG = { r: 255, g: 255, b: 255 };
718
+ var DEFAULT_BG = { r: 0, g: 0, b: 0 };
719
+ var ANSI_COLORS_NORMAL = [
720
+ { r: 0, g: 0, b: 0 },
721
+ // 0: Black
722
+ { r: 187, g: 0, b: 0 },
723
+ // 1: Red
724
+ { r: 0, g: 187, b: 0 },
725
+ // 2: Green
726
+ { r: 187, g: 187, b: 0 },
727
+ // 3: Yellow
728
+ { r: 0, g: 0, b: 187 },
729
+ // 4: Blue
730
+ { r: 187, g: 0, b: 187 },
731
+ // 5: Magenta
732
+ { r: 0, g: 187, b: 187 },
733
+ // 6: Cyan
734
+ { r: 187, g: 187, b: 187 }
735
+ // 7: White
736
+ ];
737
+ var ANSI_COLORS_BRIGHT = [
738
+ { r: 85, g: 85, b: 85 },
739
+ // 8: Bright Black (Gray)
740
+ { r: 255, g: 85, b: 85 },
741
+ // 9: Bright Red
742
+ { r: 85, g: 255, b: 85 },
743
+ // 10: Bright Green
744
+ { r: 255, g: 255, b: 85 },
745
+ // 11: Bright Yellow
746
+ { r: 85, g: 85, b: 255 },
747
+ // 12: Bright Blue
748
+ { r: 255, g: 85, b: 255 },
749
+ // 13: Bright Magenta
750
+ { r: 85, g: 255, b: 255 },
751
+ // 14: Bright Cyan
752
+ { r: 255, g: 255, b: 255 }
753
+ // 15: Bright White
754
+ ];
755
+ var SGR_RESET = 0;
756
+ var SGR_BOLD = 1;
757
+ var SGR_DIM = 2;
758
+ var SGR_REVERSE = 7;
759
+ var SGR_NORMAL_INTENSITY = 22;
760
+ var SGR_NO_REVERSE = 27;
761
+ var SGR_FG_BASE = 30;
762
+ var SGR_FG_END = 37;
763
+ var SGR_FG_DEFAULT = 39;
764
+ var SGR_BG_BASE = 40;
765
+ var SGR_BG_END = 47;
766
+ var SGR_BG_DEFAULT = 49;
767
+ var SGR_FG_BRIGHT_BASE = 90;
768
+ var SGR_FG_BRIGHT_END = 97;
769
+ var SGR_BG_BRIGHT_BASE = 100;
770
+ var SGR_BG_BRIGHT_END = 107;
771
+ var SGR_EXTENDED = 38;
772
+ var SGR_EXTENDED_BG = 48;
773
+ var EXTENDED_256 = 5;
774
+ var EXTENDED_RGB = 2;
775
+ var COLOR_CUBE_START = 16;
776
+ var COLOR_CUBE_END = 231;
777
+ var GRAYSCALE_START = 232;
778
+ var GRAYSCALE_END = 255;
779
+ var GRAYSCALE_STEP = 10;
780
+ var GRAYSCALE_BASE = 8;
781
+ var ansi256ToRgb = (index) => {
782
+ if (index < COLOR_CUBE_START) {
783
+ if (index < ANSI_STANDARD_COLOR_COUNT) {
784
+ return ANSI_COLORS_NORMAL[index];
785
+ }
786
+ return ANSI_COLORS_BRIGHT[index - ANSI_STANDARD_COLOR_COUNT];
787
+ }
788
+ if (index <= COLOR_CUBE_END) {
789
+ const cubeIndex = index - COLOR_CUBE_START;
790
+ const r = Math.floor(cubeIndex / ANSI_CUBE_RED_MULTIPLIER);
791
+ const g = Math.floor(
792
+ cubeIndex % ANSI_CUBE_RED_MULTIPLIER / ANSI_256_COLOR_LEVELS
793
+ );
794
+ const b = cubeIndex % ANSI_256_COLOR_LEVELS;
795
+ return {
796
+ r: r > 0 ? r * ANSI_CUBE_STEP + ANSI_CUBE_BASE : 0,
797
+ g: g > 0 ? g * ANSI_CUBE_STEP + ANSI_CUBE_BASE : 0,
798
+ b: b > 0 ? b * ANSI_CUBE_STEP + ANSI_CUBE_BASE : 0
799
+ };
800
+ }
801
+ if (index <= GRAYSCALE_END) {
802
+ const gray = (index - GRAYSCALE_START) * GRAYSCALE_STEP + GRAYSCALE_BASE;
803
+ return { r: gray, g: gray, b: gray };
804
+ }
805
+ return DEFAULT_FG;
806
+ };
807
+ var AnsiParser = class {
808
+ cursorRow = 1;
809
+ cursorCol = 1;
810
+ fgColor = { ...DEFAULT_FG };
811
+ bgColor = { ...DEFAULT_BG };
812
+ bold = false;
813
+ /**
814
+ * Parse an ANSI string and return draw commands
815
+ */
816
+ parse(input) {
817
+ const commands = [];
818
+ let i = 0;
819
+ let textBuffer = "";
820
+ const flushText = () => {
821
+ if (textBuffer.length > 0) {
822
+ commands.push({
823
+ type: "text",
824
+ text: textBuffer,
825
+ row: this.cursorRow,
826
+ col: this.cursorCol
827
+ });
828
+ this.cursorCol += textBuffer.length;
829
+ textBuffer = "";
830
+ }
831
+ };
832
+ while (i < input.length) {
833
+ const char = input[i];
834
+ if (char === "\x1B" && input[i + 1] === "[") {
835
+ flushText();
836
+ let j = i + 2;
837
+ while (j < input.length && !/[A-Za-z]/.test(input[j])) {
838
+ j++;
839
+ }
840
+ if (j < input.length) {
841
+ const sequence = input.substring(i + 2, j);
842
+ const command = input[j];
843
+ this.processEscapeSequence(sequence, command, commands);
844
+ i = j + 1;
845
+ } else {
846
+ i++;
847
+ }
848
+ continue;
849
+ }
850
+ if (char === "\n") {
851
+ flushText();
852
+ this.cursorRow++;
853
+ this.cursorCol = 1;
854
+ i++;
855
+ continue;
856
+ }
857
+ if (char === "\r") {
858
+ flushText();
859
+ this.cursorCol = 1;
860
+ i++;
861
+ continue;
862
+ }
863
+ if (char === " ") {
864
+ flushText();
865
+ const nextTab = Math.ceil(this.cursorCol / ANSI_TAB_WIDTH) * ANSI_TAB_WIDTH + 1;
866
+ const spaces = nextTab - this.cursorCol;
867
+ textBuffer = " ".repeat(spaces);
868
+ flushText();
869
+ i++;
870
+ continue;
871
+ }
872
+ textBuffer += char;
873
+ i++;
874
+ }
875
+ flushText();
876
+ return commands;
877
+ }
878
+ /**
879
+ * Process an escape sequence and emit draw commands
880
+ */
881
+ processEscapeSequence(params, command, commands) {
882
+ switch (command) {
883
+ case "H":
884
+ // Cursor Position (CUP)
885
+ case "f":
886
+ this.processCursorPosition(params, commands);
887
+ break;
888
+ case "J":
889
+ this.processEraseDisplay(params, commands);
890
+ break;
891
+ case "K":
892
+ this.processEraseLine(params, commands);
893
+ break;
894
+ case "m":
895
+ this.processSGR(params, commands);
896
+ break;
897
+ case "A":
898
+ this.cursorRow = Math.max(1, this.cursorRow - (parseInt(params) || 1));
899
+ break;
900
+ case "B":
901
+ this.cursorRow += parseInt(params) || 1;
902
+ break;
903
+ case "C":
904
+ this.cursorCol += parseInt(params) || 1;
905
+ break;
906
+ case "D":
907
+ this.cursorCol = Math.max(1, this.cursorCol - (parseInt(params) || 1));
908
+ break;
909
+ case "G":
910
+ this.cursorCol = parseInt(params) || 1;
911
+ break;
912
+ case "s":
913
+ // Save Cursor Position
914
+ case "u":
915
+ break;
916
+ default:
917
+ break;
918
+ }
919
+ }
920
+ /**
921
+ * Process cursor position sequence
922
+ */
923
+ processCursorPosition(params, commands) {
924
+ const parts = params.split(";");
925
+ this.cursorRow = parseInt(parts[0] ?? "1") || 1;
926
+ this.cursorCol = parseInt(parts[1] ?? "1") || 1;
927
+ commands.push({
928
+ type: "cursor_move",
929
+ row: this.cursorRow,
930
+ col: this.cursorCol
931
+ });
932
+ }
933
+ /**
934
+ * Process erase display sequence
935
+ */
936
+ processEraseDisplay(params, commands) {
937
+ const mode = parseInt(params) || 0;
938
+ if (mode === ANSI_ERASE_ENTIRE_SCREEN || mode === ANSI_ERASE_TO_END_AND_BEYOND) {
939
+ commands.push({ type: "clear_screen" });
940
+ this.cursorRow = 1;
941
+ this.cursorCol = 1;
942
+ }
943
+ }
944
+ /**
945
+ * Process erase line sequence
946
+ */
947
+ processEraseLine(params, commands) {
948
+ const mode = parseInt(params) || 0;
949
+ commands.push({
950
+ type: "clear_line",
951
+ row: this.cursorRow,
952
+ col: mode === 0 ? this.cursorCol : 1
953
+ });
954
+ }
955
+ /**
956
+ * Process SGR (Select Graphic Rendition) sequence
957
+ */
958
+ processSGR(params, commands) {
959
+ if (params === "" || params === "0") {
960
+ this.resetStyle(commands);
961
+ return;
962
+ }
963
+ const codes = params.split(";").map((s) => parseInt(s) || 0);
964
+ let i = 0;
965
+ while (i < codes.length) {
966
+ const code = codes[i];
967
+ if (code === SGR_RESET) {
968
+ this.resetStyle(commands);
969
+ } else if (code === SGR_BOLD) {
970
+ this.bold = true;
971
+ commands.push({ type: "set_bold", enabled: true });
972
+ } else if (code === SGR_DIM) {
973
+ commands.push({ type: "set_dim", enabled: true });
974
+ } else if (code === SGR_REVERSE) {
975
+ commands.push({ type: "set_reverse", enabled: true });
976
+ } else if (code === SGR_NORMAL_INTENSITY) {
977
+ this.bold = false;
978
+ commands.push({ type: "set_bold", enabled: false });
979
+ commands.push({ type: "set_dim", enabled: false });
980
+ } else if (code === SGR_NO_REVERSE) {
981
+ commands.push({ type: "set_reverse", enabled: false });
982
+ } else if (code >= SGR_FG_BASE && code <= SGR_FG_END) {
983
+ const colorIndex = code - SGR_FG_BASE;
984
+ this.fgColor = this.bold ? { ...ANSI_COLORS_BRIGHT[colorIndex] } : { ...ANSI_COLORS_NORMAL[colorIndex] };
985
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
986
+ } else if (code === SGR_FG_DEFAULT) {
987
+ this.fgColor = { ...DEFAULT_FG };
988
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
989
+ } else if (code >= SGR_BG_BASE && code <= SGR_BG_END) {
990
+ const colorIndex = code - SGR_BG_BASE;
991
+ this.bgColor = { ...ANSI_COLORS_NORMAL[colorIndex] };
992
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
993
+ } else if (code === SGR_BG_DEFAULT) {
994
+ this.bgColor = { ...DEFAULT_BG };
995
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
996
+ } else if (code >= SGR_FG_BRIGHT_BASE && code <= SGR_FG_BRIGHT_END) {
997
+ const colorIndex = code - SGR_FG_BRIGHT_BASE;
998
+ this.fgColor = { ...ANSI_COLORS_BRIGHT[colorIndex] };
999
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
1000
+ } else if (code >= SGR_BG_BRIGHT_BASE && code <= SGR_BG_BRIGHT_END) {
1001
+ const colorIndex = code - SGR_BG_BRIGHT_BASE;
1002
+ this.bgColor = { ...ANSI_COLORS_BRIGHT[colorIndex] };
1003
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
1004
+ } else if (code === SGR_EXTENDED) {
1005
+ const result = this.parseExtendedColor(codes, i + 1);
1006
+ if (result.color) {
1007
+ this.fgColor = result.color;
1008
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
1009
+ }
1010
+ i = result.nextIndex - 1;
1011
+ } else if (code === SGR_EXTENDED_BG) {
1012
+ const result = this.parseExtendedColor(codes, i + 1);
1013
+ if (result.color) {
1014
+ this.bgColor = result.color;
1015
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
1016
+ }
1017
+ i = result.nextIndex - 1;
1018
+ }
1019
+ i++;
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Parse extended color (256-color or 24-bit)
1024
+ */
1025
+ parseExtendedColor(codes, startIndex) {
1026
+ if (startIndex >= codes.length) {
1027
+ return { color: null, nextIndex: startIndex };
1028
+ }
1029
+ const mode = codes[startIndex];
1030
+ if (mode === EXTENDED_256 && startIndex + ANSI_RGB_R_OFFSET < codes.length) {
1031
+ const colorIndex = codes[startIndex + ANSI_RGB_R_OFFSET];
1032
+ return {
1033
+ color: ansi256ToRgb(colorIndex),
1034
+ nextIndex: startIndex + ANSI_EXTENDED_COLOR_OFFSET_256
1035
+ };
1036
+ }
1037
+ if (mode === EXTENDED_RGB && startIndex + ANSI_EXTENDED_RGB_MIN_PARAMS < codes.length) {
1038
+ return {
1039
+ color: {
1040
+ r: Math.min(
1041
+ COLOR_CHANNEL_MAX,
1042
+ Math.max(0, codes[startIndex + ANSI_RGB_R_OFFSET])
1043
+ ),
1044
+ g: Math.min(
1045
+ COLOR_CHANNEL_MAX,
1046
+ Math.max(0, codes[startIndex + ANSI_RGB_G_OFFSET])
1047
+ ),
1048
+ b: Math.min(
1049
+ COLOR_CHANNEL_MAX,
1050
+ Math.max(0, codes[startIndex + ANSI_RGB_B_OFFSET])
1051
+ )
1052
+ },
1053
+ nextIndex: startIndex + ANSI_EXTENDED_COLOR_OFFSET_RGB
1054
+ };
1055
+ }
1056
+ return { color: null, nextIndex: startIndex + 1 };
1057
+ }
1058
+ /**
1059
+ * Reset all styles to default
1060
+ */
1061
+ resetStyle(commands) {
1062
+ this.fgColor = { ...DEFAULT_FG };
1063
+ this.bgColor = { ...DEFAULT_BG };
1064
+ this.bold = false;
1065
+ commands.push({ type: "reset_style" });
1066
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
1067
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
1068
+ }
1069
+ /**
1070
+ * Get current cursor position
1071
+ */
1072
+ getCursor() {
1073
+ return { row: this.cursorRow, col: this.cursorCol };
1074
+ }
1075
+ /**
1076
+ * Get current foreground color
1077
+ */
1078
+ getFgColor() {
1079
+ return { ...this.fgColor };
1080
+ }
1081
+ /**
1082
+ * Get current background color
1083
+ */
1084
+ getBgColor() {
1085
+ return { ...this.bgColor };
1086
+ }
1087
+ /**
1088
+ * Reset parser state
1089
+ */
1090
+ reset() {
1091
+ this.cursorRow = 1;
1092
+ this.cursorCol = 1;
1093
+ this.fgColor = { ...DEFAULT_FG };
1094
+ this.bgColor = { ...DEFAULT_BG };
1095
+ this.bold = false;
1096
+ }
1097
+ };
1098
+
1099
+ // src/renderer/text-renderer.ts
1100
+ import { resolve, dirname } from "path";
1101
+ import { fileURLToPath } from "url";
1102
+ import { existsSync as existsSync2 } from "fs";
1103
+ var TextRenderer = class {
1104
+ sdl = getSDL2();
1105
+ ttf = getSDL_ttf();
1106
+ font = null;
1107
+ renderer;
1108
+ baseFontSize;
1109
+ scaleFactor;
1110
+ glyphCache = /* @__PURE__ */ new Map();
1111
+ accessCounter = 0;
1112
+ charWidth = 0;
1113
+ charHeight = 0;
1114
+ constructor(renderer, options = {}) {
1115
+ this.renderer = renderer;
1116
+ this.baseFontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
1117
+ this.scaleFactor = options.scaleFactor ?? 1;
1118
+ if (!this.ttf.isInitialized()) {
1119
+ this.ttf.init();
1120
+ }
1121
+ const fontPath = options.fontPath ?? this.getDefaultFontPath();
1122
+ this.loadFont(fontPath);
1123
+ }
1124
+ /**
1125
+ * Get the path to the bundled Cozette font
1126
+ */
1127
+ getDefaultFontPath() {
1128
+ const currentFilename = fileURLToPath(import.meta.url);
1129
+ const currentDirname = dirname(currentFilename);
1130
+ const paths = [
1131
+ resolve(currentDirname, "../fonts/CozetteVector.ttf"),
1132
+ // Dev path
1133
+ resolve(currentDirname, "./fonts/CozetteVector.ttf"),
1134
+ // Bundled (dist)
1135
+ resolve(currentDirname, "../../src/fonts/CozetteVector.ttf")
1136
+ // Alternate
1137
+ ];
1138
+ for (const p of paths) {
1139
+ try {
1140
+ if (existsSync2(p)) {
1141
+ return p;
1142
+ }
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ return paths[0];
1147
+ }
1148
+ /**
1149
+ * Load a TTF font at the current scaled size
1150
+ */
1151
+ loadFont(fontPath) {
1152
+ if (this.font) {
1153
+ this.ttf.closeFont(this.font);
1154
+ this.font = null;
1155
+ }
1156
+ this.clearCache();
1157
+ const physicalSize = Math.round(
1158
+ this.baseFontSize * this.scaleFactor * this.scaleFactor
1159
+ );
1160
+ this.font = this.ttf.openFont(fontPath, physicalSize);
1161
+ const dims = this.ttf.sizeText(this.font, "M");
1162
+ this.charWidth = dims.width;
1163
+ this.charHeight = dims.height;
1164
+ }
1165
+ /**
1166
+ * Update scale factor (for HiDPI display changes)
1167
+ */
1168
+ updateScaleFactor(scaleFactor) {
1169
+ if (Math.abs(scaleFactor - this.scaleFactor) < SCALE_FACTOR_EPSILON) {
1170
+ return;
1171
+ }
1172
+ this.scaleFactor = scaleFactor;
1173
+ const fontPath = this.getDefaultFontPath();
1174
+ this.loadFont(fontPath);
1175
+ }
1176
+ /**
1177
+ * Get character dimensions
1178
+ */
1179
+ getCharDimensions() {
1180
+ return { width: this.charWidth, height: this.charHeight };
1181
+ }
1182
+ /**
1183
+ * Generate cache key for a glyph
1184
+ */
1185
+ getCacheKey(char, r, g, b) {
1186
+ const packedColor = r << PACK_RED_SHIFT | g << PACK_GREEN_SHIFT | b;
1187
+ return `${char}:${packedColor}`;
1188
+ }
1189
+ /**
1190
+ * Get or create a cached glyph texture
1191
+ */
1192
+ getGlyph(char, r, g, b) {
1193
+ const key = this.getCacheKey(char, r, g, b);
1194
+ const cached = this.glyphCache.get(key);
1195
+ if (cached) {
1196
+ cached.lastUsed = this.accessCounter++;
1197
+ return cached;
1198
+ }
1199
+ if (!this.font) {
1200
+ return null;
1201
+ }
1202
+ try {
1203
+ const surface = this.ttf.renderTextBlended(
1204
+ this.font,
1205
+ char,
1206
+ COLOR_CHANNEL_MAX,
1207
+ // White
1208
+ COLOR_CHANNEL_MAX,
1209
+ COLOR_CHANNEL_MAX,
1210
+ COLOR_CHANNEL_MAX
1211
+ );
1212
+ const texture = this.sdl.createTextureFromSurface(this.renderer, surface);
1213
+ this.sdl.setTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
1214
+ this.sdl.setTextureColorMod(texture, r, g, b);
1215
+ const dims = this.ttf.sizeText(this.font, char);
1216
+ this.sdl.freeSurface(surface);
1217
+ const glyph = {
1218
+ texture,
1219
+ width: dims.width,
1220
+ height: dims.height,
1221
+ lastUsed: this.accessCounter++
1222
+ };
1223
+ if (this.glyphCache.size >= MAX_GLYPH_CACHE_SIZE) {
1224
+ this.evictOldGlyphs();
1225
+ }
1226
+ this.glyphCache.set(key, glyph);
1227
+ return glyph;
1228
+ } catch {
1229
+ return null;
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Evict least recently used glyphs
1234
+ */
1235
+ evictOldGlyphs() {
1236
+ const entries = [...this.glyphCache.entries()];
1237
+ entries.sort((a, b) => a[1].lastUsed - b[1].lastUsed);
1238
+ const removeCount = Math.floor(
1239
+ MAX_GLYPH_CACHE_SIZE / GLYPH_CACHE_EVICT_DIVISOR
1240
+ );
1241
+ for (let i = 0; i < removeCount && i < entries.length; i++) {
1242
+ const entry = entries[i];
1243
+ if (entry) {
1244
+ const [key, glyph] = entry;
1245
+ this.sdl.destroyTexture(glyph.texture);
1246
+ this.glyphCache.delete(key);
1247
+ }
1248
+ }
1249
+ }
1250
+ /**
1251
+ * Render a single character at the specified position
1252
+ */
1253
+ renderChar(char, x, y, color) {
1254
+ const glyph = this.getGlyph(char, color.r, color.g, color.b);
1255
+ if (!glyph) {
1256
+ return;
1257
+ }
1258
+ const destRect = createSDLRect(x, y, glyph.width, glyph.height);
1259
+ this.sdl.renderCopy(this.renderer, glyph.texture, null, destRect);
1260
+ }
1261
+ /**
1262
+ * Render a string of text at the specified position
1263
+ */
1264
+ renderText(text, x, y, color) {
1265
+ let cursorX = x;
1266
+ for (const char of text) {
1267
+ if (char === " ") {
1268
+ cursorX += this.charWidth;
1269
+ continue;
1270
+ }
1271
+ this.renderChar(char, cursorX, y, color);
1272
+ cursorX += this.charWidth;
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Get text dimensions
1277
+ */
1278
+ measureText(text) {
1279
+ if (!this.font) {
1280
+ return { width: 0, height: 0 };
1281
+ }
1282
+ return this.ttf.sizeText(this.font, text);
1283
+ }
1284
+ /**
1285
+ * Clear the glyph cache
1286
+ */
1287
+ clearCache() {
1288
+ for (const glyph of this.glyphCache.values()) {
1289
+ this.sdl.destroyTexture(glyph.texture);
1290
+ }
1291
+ this.glyphCache.clear();
1292
+ this.accessCounter = 0;
1293
+ }
1294
+ /**
1295
+ * Get cache statistics
1296
+ */
1297
+ getCacheStats() {
1298
+ return {
1299
+ size: this.glyphCache.size,
1300
+ maxSize: MAX_GLYPH_CACHE_SIZE
1301
+ };
1302
+ }
1303
+ /**
1304
+ * Clean up resources
1305
+ */
1306
+ destroy() {
1307
+ this.clearCache();
1308
+ if (this.font) {
1309
+ this.ttf.closeFont(this.font);
1310
+ this.font = null;
1311
+ }
1312
+ }
1313
+ };
1314
+
1315
+ // src/input/input-bridge.ts
1316
+ var ASCII_PRINTABLE_START = 32;
1317
+ var ASCII_PRINTABLE_END = 126;
1318
+ var CTRL_KEY_OFFSET = 96;
1319
+ var ASCII_BRACKET_OPEN = 91;
1320
+ var ASCII_BACKSLASH = 92;
1321
+ var ASCII_BRACKET_CLOSE = 93;
1322
+ var ASCII_CARET = 94;
1323
+ var ASCII_UNDERSCORE = 95;
1324
+ var FUNCTION_KEY_OFFSET_3 = 3;
1325
+ var FUNCTION_KEY_OFFSET_4 = 4;
1326
+ var FUNCTION_KEY_OFFSET_5 = 5;
1327
+ var FUNCTION_KEY_OFFSET_6 = 6;
1328
+ var FUNCTION_KEY_OFFSET_7 = 7;
1329
+ var FUNCTION_KEY_OFFSET_8 = 8;
1330
+ var FUNCTION_KEY_OFFSET_9 = 9;
1331
+ var FUNCTION_KEY_OFFSET_10 = 10;
1332
+ var InputBridge = class {
1333
+ modifiers = {
1334
+ shift: false,
1335
+ ctrl: false,
1336
+ alt: false
1337
+ };
1338
+ /**
1339
+ * Process an SDL key event and return terminal sequence
1340
+ */
1341
+ processKeyEvent(event) {
1342
+ const { keycode, pressed } = event;
1343
+ if (this.isModifierKey(keycode)) {
1344
+ this.updateModifierState(keycode, pressed);
1345
+ return null;
1346
+ }
1347
+ if (!pressed) {
1348
+ return null;
1349
+ }
1350
+ const specialSequence = this.getSpecialKeySequence(keycode);
1351
+ if (specialSequence !== null) {
1352
+ return specialSequence;
1353
+ }
1354
+ if (this.modifiers.ctrl) {
1355
+ const ctrlSequence = this.getCtrlSequence(keycode);
1356
+ if (ctrlSequence !== null) {
1357
+ return ctrlSequence;
1358
+ }
1359
+ }
1360
+ if (keycode >= ASCII_PRINTABLE_START && keycode <= ASCII_PRINTABLE_END) {
1361
+ let char = String.fromCharCode(keycode);
1362
+ if (this.modifiers.shift && keycode >= ASCII_A_LOWER && keycode <= ASCII_Z_LOWER) {
1363
+ char = char.toUpperCase();
1364
+ }
1365
+ return char;
1366
+ }
1367
+ return null;
1368
+ }
1369
+ /**
1370
+ * Check if a keycode is a modifier key
1371
+ */
1372
+ isModifierKey(keycode) {
1373
+ return keycode === SDLK_LSHIFT || keycode === SDLK_RSHIFT || keycode === SDLK_LCTRL || keycode === SDLK_RCTRL || keycode === SDLK_LALT || keycode === SDLK_RALT;
1374
+ }
1375
+ /**
1376
+ * Update modifier key state
1377
+ */
1378
+ updateModifierState(keycode, pressed) {
1379
+ if (keycode === SDLK_LSHIFT || keycode === SDLK_RSHIFT) {
1380
+ this.modifiers.shift = pressed;
1381
+ } else if (keycode === SDLK_LCTRL || keycode === SDLK_RCTRL) {
1382
+ this.modifiers.ctrl = pressed;
1383
+ } else if (keycode === SDLK_LALT || keycode === SDLK_RALT) {
1384
+ this.modifiers.alt = pressed;
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Get terminal escape sequence for special keys
1389
+ */
1390
+ getSpecialKeySequence(keycode) {
1391
+ switch (keycode) {
1392
+ // Navigation keys
1393
+ case SDLK_UP:
1394
+ return "\x1B[A";
1395
+ case SDLK_DOWN:
1396
+ return "\x1B[B";
1397
+ case SDLK_RIGHT:
1398
+ return "\x1B[C";
1399
+ case SDLK_LEFT:
1400
+ return "\x1B[D";
1401
+ case SDLK_HOME:
1402
+ return "\x1B[H";
1403
+ case SDLK_END:
1404
+ return "\x1B[F";
1405
+ case SDLK_PAGEUP:
1406
+ return "\x1B[5~";
1407
+ case SDLK_PAGEDOWN:
1408
+ return "\x1B[6~";
1409
+ // Control keys
1410
+ case SDLK_RETURN:
1411
+ return "\r";
1412
+ case SDLK_ESCAPE:
1413
+ return "\x1B";
1414
+ case SDLK_BACKSPACE:
1415
+ return "\x7F";
1416
+ case SDLK_TAB:
1417
+ return this.modifiers.shift ? "\x1B[Z" : " ";
1418
+ case SDLK_DELETE:
1419
+ return "\x1B[3~";
1420
+ case SDLK_SPACE:
1421
+ return " ";
1422
+ // Function keys
1423
+ case SDLK_F1:
1424
+ return "\x1BOP";
1425
+ case SDLK_F1 + 1:
1426
+ return "\x1BOQ";
1427
+ case SDLK_F1 + 2:
1428
+ return "\x1BOR";
1429
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_3:
1430
+ return "\x1BOS";
1431
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_4:
1432
+ return "\x1B[15~";
1433
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_5:
1434
+ return "\x1B[17~";
1435
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_6:
1436
+ return "\x1B[18~";
1437
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_7:
1438
+ return "\x1B[19~";
1439
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_8:
1440
+ return "\x1B[20~";
1441
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_9:
1442
+ return "\x1B[21~";
1443
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_10:
1444
+ return "\x1B[23~";
1445
+ case SDLK_F12:
1446
+ return "\x1B[24~";
1447
+ default:
1448
+ return null;
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Get Ctrl+key sequence
1453
+ */
1454
+ getCtrlSequence(keycode) {
1455
+ if (keycode >= ASCII_A_LOWER && keycode <= ASCII_Z_LOWER) {
1456
+ const ctrlCode = keycode - CTRL_KEY_OFFSET;
1457
+ return String.fromCharCode(ctrlCode);
1458
+ }
1459
+ switch (keycode) {
1460
+ case SDLK_SPACE:
1461
+ return "\0";
1462
+ // Ctrl+Space = NUL
1463
+ case ASCII_BRACKET_OPEN:
1464
+ return "\x1B";
1465
+ // Ctrl+[ = Escape
1466
+ case ASCII_BACKSLASH:
1467
+ return "";
1468
+ // Ctrl+\ = FS
1469
+ case ASCII_BRACKET_CLOSE:
1470
+ return "";
1471
+ // Ctrl+] = GS
1472
+ case ASCII_CARET:
1473
+ return "";
1474
+ // Ctrl+^ = RS
1475
+ case ASCII_UNDERSCORE:
1476
+ return "";
1477
+ // Ctrl+_ = US
1478
+ default:
1479
+ return null;
1480
+ }
1481
+ }
1482
+ /**
1483
+ * Convert SDL key event to Ink-style key event
1484
+ */
1485
+ toInkKeyEvent(event) {
1486
+ const sequence = this.processKeyEvent(event);
1487
+ if (sequence === null) {
1488
+ return null;
1489
+ }
1490
+ let name = "";
1491
+ if (sequence.length === 1 && sequence >= " " && sequence <= "~") {
1492
+ name = sequence;
1493
+ } else {
1494
+ name = this.getKeyName(event.keycode);
1495
+ }
1496
+ return {
1497
+ sequence,
1498
+ name,
1499
+ ctrl: this.modifiers.ctrl,
1500
+ meta: this.modifiers.alt,
1501
+ shift: this.modifiers.shift
1502
+ };
1503
+ }
1504
+ /**
1505
+ * Get human-readable key name
1506
+ */
1507
+ getKeyName(keycode) {
1508
+ switch (keycode) {
1509
+ case SDLK_UP:
1510
+ return "up";
1511
+ case SDLK_DOWN:
1512
+ return "down";
1513
+ case SDLK_LEFT:
1514
+ return "left";
1515
+ case SDLK_RIGHT:
1516
+ return "right";
1517
+ case SDLK_RETURN:
1518
+ return "return";
1519
+ case SDLK_ESCAPE:
1520
+ return "escape";
1521
+ case SDLK_BACKSPACE:
1522
+ return "backspace";
1523
+ case SDLK_TAB:
1524
+ return "tab";
1525
+ case SDLK_DELETE:
1526
+ return "delete";
1527
+ case SDLK_HOME:
1528
+ return "home";
1529
+ case SDLK_END:
1530
+ return "end";
1531
+ case SDLK_PAGEUP:
1532
+ return "pageup";
1533
+ case SDLK_PAGEDOWN:
1534
+ return "pagedown";
1535
+ default:
1536
+ if (keycode >= SDLK_F1 && keycode <= SDLK_F12) {
1537
+ return `f${keycode - SDLK_F1 + 1}`;
1538
+ }
1539
+ return "unknown";
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Reset modifier state
1544
+ */
1545
+ reset() {
1546
+ this.modifiers = {
1547
+ shift: false,
1548
+ ctrl: false,
1549
+ alt: false
1550
+ };
1551
+ }
1552
+ /**
1553
+ * Get current modifier state
1554
+ */
1555
+ getModifiers() {
1556
+ return { ...this.modifiers };
1557
+ }
1558
+ };
1559
+
1560
+ // src/renderer/index.ts
1561
+ var DEFAULT_BG2 = { r: 0, g: 0, b: 0 };
1562
+ var DEFAULT_FG2 = { r: 255, g: 255, b: 255 };
1563
+ var MIN_BRIGHTNESS = 100;
1564
+ var SdlUiRenderer = class {
1565
+ sdl = getSDL2();
1566
+ window = null;
1567
+ renderer = null;
1568
+ textRenderer = null;
1569
+ ansiParser;
1570
+ inputBridge;
1571
+ windowWidth;
1572
+ windowHeight;
1573
+ columns;
1574
+ rows;
1575
+ charWidth = 0;
1576
+ charHeight = 0;
1577
+ fgColor = { ...DEFAULT_FG2 };
1578
+ bgColor = { ...DEFAULT_BG2 };
1579
+ bold = false;
1580
+ dim = false;
1581
+ reverse = false;
1582
+ shouldQuit = false;
1583
+ pendingCommands = [];
1584
+ scaleFactor = 1;
1585
+ userScaleFactor = null;
1586
+ constructor(options = {}) {
1587
+ this.windowWidth = options.width ?? DEFAULT_WINDOW_WIDTH;
1588
+ this.windowHeight = options.height ?? DEFAULT_WINDOW_HEIGHT;
1589
+ this.columns = DEFAULT_COLUMNS;
1590
+ this.rows = DEFAULT_ROWS;
1591
+ this.ansiParser = new AnsiParser();
1592
+ this.inputBridge = new InputBridge();
1593
+ this.initSDL(options);
1594
+ }
1595
+ /**
1596
+ * Initialize SDL window and renderer
1597
+ */
1598
+ initSDL(options) {
1599
+ if (!this.sdl.init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
1600
+ throw new Error("Failed to initialize SDL2 for UI rendering");
1601
+ }
1602
+ this.window = this.sdl.createWindow(
1603
+ options.title ?? "ink-sdl",
1604
+ SDL_WINDOWPOS_CENTERED,
1605
+ SDL_WINDOWPOS_CENTERED,
1606
+ this.windowWidth,
1607
+ this.windowHeight,
1608
+ SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI
1609
+ );
1610
+ const rendererFlags = options.vsync !== false ? SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC : SDL_RENDERER_ACCELERATED;
1611
+ this.renderer = this.sdl.createRenderer(this.window, -1, rendererFlags);
1612
+ this.userScaleFactor = options.scaleFactor === void 0 ? null : options.scaleFactor;
1613
+ if (this.userScaleFactor !== null) {
1614
+ this.scaleFactor = this.userScaleFactor;
1615
+ } else {
1616
+ this.scaleFactor = this.sdl.getScaleFactorFromRenderer(
1617
+ this.window,
1618
+ this.renderer
1619
+ );
1620
+ }
1621
+ this.textRenderer = new TextRenderer(this.renderer, {
1622
+ fontSize: options.fontSize ?? DEFAULT_FONT_SIZE,
1623
+ scaleFactor: this.scaleFactor
1624
+ });
1625
+ const charDims = this.textRenderer.getCharDimensions();
1626
+ this.charWidth = charDims.width;
1627
+ this.charHeight = charDims.height;
1628
+ this.updateTerminalDimensions();
1629
+ this.sdl.setRenderDrawColor(
1630
+ this.renderer,
1631
+ DEFAULT_BG2.r,
1632
+ DEFAULT_BG2.g,
1633
+ DEFAULT_BG2.b,
1634
+ COLOR_CHANNEL_MAX
1635
+ );
1636
+ this.sdl.renderClear(this.renderer);
1637
+ this.sdl.renderPresent(this.renderer);
1638
+ this.sdl.raiseWindow(this.window);
1639
+ }
1640
+ /**
1641
+ * Update terminal dimensions based on window size
1642
+ */
1643
+ updateTerminalDimensions() {
1644
+ if (!this.window || this.charWidth === 0 || this.charHeight === 0) {
1645
+ return;
1646
+ }
1647
+ const drawable = this.sdl.getDrawableSize(this.window);
1648
+ this.columns = Math.floor(drawable.width / this.charWidth);
1649
+ this.rows = Math.floor(drawable.height / this.charHeight);
1650
+ this.columns = Math.max(this.columns, MIN_COLUMNS);
1651
+ this.rows = Math.max(this.rows, MIN_ROWS);
1652
+ }
1653
+ /**
1654
+ * Get terminal dimensions
1655
+ */
1656
+ getDimensions() {
1657
+ return {
1658
+ columns: this.columns,
1659
+ rows: this.rows
1660
+ };
1661
+ }
1662
+ /**
1663
+ * Process ANSI output from Ink
1664
+ */
1665
+ processAnsi(output) {
1666
+ const commands = this.ansiParser.parse(output);
1667
+ this.pendingCommands.push(...commands);
1668
+ }
1669
+ /**
1670
+ * Render pending commands and present
1671
+ */
1672
+ present() {
1673
+ if (!this.renderer || !this.textRenderer) {
1674
+ return;
1675
+ }
1676
+ for (const cmd of this.pendingCommands) {
1677
+ this.executeCommand(cmd);
1678
+ }
1679
+ this.pendingCommands = [];
1680
+ this.sdl.renderPresent(this.renderer);
1681
+ }
1682
+ /**
1683
+ * Execute a single draw command
1684
+ */
1685
+ executeCommand(cmd) {
1686
+ switch (cmd.type) {
1687
+ case "text":
1688
+ this.renderText(cmd);
1689
+ break;
1690
+ case "clear_screen":
1691
+ this.clear();
1692
+ break;
1693
+ case "clear_line":
1694
+ this.clearLine(cmd.row ?? 1, cmd.col ?? 1);
1695
+ break;
1696
+ case "cursor_move":
1697
+ break;
1698
+ case "set_fg":
1699
+ if (cmd.color) {
1700
+ this.fgColor = cmd.color;
1701
+ }
1702
+ break;
1703
+ case "set_bg":
1704
+ if (cmd.color) {
1705
+ this.bgColor = cmd.color;
1706
+ }
1707
+ break;
1708
+ case "reset_style":
1709
+ this.fgColor = { ...DEFAULT_FG2 };
1710
+ this.bgColor = { ...DEFAULT_BG2 };
1711
+ this.bold = false;
1712
+ this.dim = false;
1713
+ this.reverse = false;
1714
+ break;
1715
+ case "set_bold":
1716
+ this.bold = cmd.enabled ?? false;
1717
+ break;
1718
+ case "set_dim":
1719
+ this.dim = cmd.enabled ?? false;
1720
+ break;
1721
+ case "set_reverse":
1722
+ this.reverse = cmd.enabled ?? false;
1723
+ break;
1724
+ }
1725
+ }
1726
+ /**
1727
+ * Render text at position
1728
+ */
1729
+ renderText(cmd) {
1730
+ if (!cmd.text || !this.renderer || !this.textRenderer) {
1731
+ return;
1732
+ }
1733
+ const text = cmd.text;
1734
+ const row = cmd.row ?? 1;
1735
+ const col = cmd.col ?? 1;
1736
+ const x = (col - 1) * this.charWidth;
1737
+ const y = (row - 1) * this.charHeight;
1738
+ let fg = this.reverse ? this.bgColor : this.fgColor;
1739
+ const bg = this.reverse ? this.fgColor : this.bgColor;
1740
+ if (this.bold) {
1741
+ fg = {
1742
+ r: Math.min(
1743
+ COLOR_CHANNEL_MAX,
1744
+ Math.floor(fg.r * BOLD_BRIGHTNESS_MULTIPLIER)
1745
+ ),
1746
+ g: Math.min(
1747
+ COLOR_CHANNEL_MAX,
1748
+ Math.floor(fg.g * BOLD_BRIGHTNESS_MULTIPLIER)
1749
+ ),
1750
+ b: Math.min(
1751
+ COLOR_CHANNEL_MAX,
1752
+ Math.floor(fg.b * BOLD_BRIGHTNESS_MULTIPLIER)
1753
+ )
1754
+ };
1755
+ }
1756
+ if (this.dim) {
1757
+ fg = {
1758
+ r: Math.floor(fg.r * DIM_BRIGHTNESS_MULTIPLIER),
1759
+ g: Math.floor(fg.g * DIM_BRIGHTNESS_MULTIPLIER),
1760
+ b: Math.floor(fg.b * DIM_BRIGHTNESS_MULTIPLIER)
1761
+ };
1762
+ }
1763
+ const brightness = Math.max(fg.r, fg.g, fg.b);
1764
+ if (brightness < MIN_BRIGHTNESS) {
1765
+ if (brightness === 0) {
1766
+ fg = { r: MIN_BRIGHTNESS, g: MIN_BRIGHTNESS, b: MIN_BRIGHTNESS };
1767
+ } else {
1768
+ const scale = MIN_BRIGHTNESS / brightness;
1769
+ fg = {
1770
+ r: Math.min(COLOR_CHANNEL_MAX, Math.floor(fg.r * scale)),
1771
+ g: Math.min(COLOR_CHANNEL_MAX, Math.floor(fg.g * scale)),
1772
+ b: Math.min(COLOR_CHANNEL_MAX, Math.floor(fg.b * scale))
1773
+ };
1774
+ }
1775
+ }
1776
+ const bgRect = createSDLRect(
1777
+ x,
1778
+ y,
1779
+ text.length * this.charWidth,
1780
+ this.charHeight
1781
+ );
1782
+ this.sdl.setRenderDrawColor(
1783
+ this.renderer,
1784
+ bg.r,
1785
+ bg.g,
1786
+ bg.b,
1787
+ COLOR_CHANNEL_MAX
1788
+ );
1789
+ this.sdl.renderFillRect(this.renderer, bgRect);
1790
+ this.textRenderer.renderText(text, x, y, fg);
1791
+ }
1792
+ /**
1793
+ * Clear the entire screen
1794
+ */
1795
+ clear() {
1796
+ if (!this.renderer) {
1797
+ return;
1798
+ }
1799
+ this.sdl.setRenderDrawColor(
1800
+ this.renderer,
1801
+ DEFAULT_BG2.r,
1802
+ DEFAULT_BG2.g,
1803
+ DEFAULT_BG2.b,
1804
+ COLOR_CHANNEL_MAX
1805
+ );
1806
+ this.sdl.renderClear(this.renderer);
1807
+ this.ansiParser.reset();
1808
+ }
1809
+ /**
1810
+ * Clear a line from a specific position
1811
+ */
1812
+ clearLine(row, fromCol) {
1813
+ if (!this.renderer) {
1814
+ return;
1815
+ }
1816
+ const x = (fromCol - 1) * this.charWidth;
1817
+ const y = (row - 1) * this.charHeight;
1818
+ const drawable = this.sdl.getDrawableSize(this.window);
1819
+ const clearWidth = drawable.width - x;
1820
+ const rect = createSDLRect(x, y, clearWidth, this.charHeight);
1821
+ this.sdl.setRenderDrawColor(
1822
+ this.renderer,
1823
+ this.bgColor.r,
1824
+ this.bgColor.g,
1825
+ this.bgColor.b,
1826
+ COLOR_CHANNEL_MAX
1827
+ );
1828
+ this.sdl.renderFillRect(this.renderer, rect);
1829
+ }
1830
+ /**
1831
+ * Process SDL events
1832
+ */
1833
+ processEvents() {
1834
+ const keyEvents = [];
1835
+ let event = this.sdl.pollEvent();
1836
+ while (event) {
1837
+ if (event.type === SDL_QUIT) {
1838
+ this.shouldQuit = true;
1839
+ } else if (event.type === SDL_WINDOWEVENT) {
1840
+ if (event.windowEvent === SDL_WINDOWEVENT_CLOSE) {
1841
+ this.shouldQuit = true;
1842
+ } else if (event.windowEvent === SDL_WINDOWEVENT_SIZE_CHANGED) {
1843
+ this.handleResize();
1844
+ }
1845
+ } else if (event.type === SDL_KEYDOWN || event.type === SDL_KEYUP) {
1846
+ if (event.keycode !== void 0 && event.pressed !== void 0) {
1847
+ keyEvents.push({
1848
+ keycode: event.keycode,
1849
+ pressed: event.pressed,
1850
+ repeat: event.repeat ?? false
1851
+ });
1852
+ }
1853
+ }
1854
+ event = this.sdl.pollEvent();
1855
+ }
1856
+ return keyEvents;
1857
+ }
1858
+ /**
1859
+ * Convert SDL key event to terminal sequence
1860
+ */
1861
+ keyEventToSequence(event) {
1862
+ return this.inputBridge.processKeyEvent(event);
1863
+ }
1864
+ /**
1865
+ * Handle window resize
1866
+ */
1867
+ handleResize() {
1868
+ if (!this.window) {
1869
+ return;
1870
+ }
1871
+ const size = this.sdl.getWindowSize(this.window);
1872
+ this.windowWidth = size.width;
1873
+ this.windowHeight = size.height;
1874
+ if (this.userScaleFactor === null) {
1875
+ const newScale = this.sdl.getScaleFactor(this.window);
1876
+ if (Math.abs(newScale - this.scaleFactor) > SCALE_FACTOR_EPSILON) {
1877
+ this.scaleFactor = newScale;
1878
+ this.textRenderer?.updateScaleFactor(newScale);
1879
+ if (this.textRenderer) {
1880
+ const charDims = this.textRenderer.getCharDimensions();
1881
+ this.charWidth = charDims.width;
1882
+ this.charHeight = charDims.height;
1883
+ }
1884
+ }
1885
+ }
1886
+ this.updateTerminalDimensions();
1887
+ this.clear();
1888
+ }
1889
+ /**
1890
+ * Check if quit was requested
1891
+ */
1892
+ shouldClose() {
1893
+ return this.shouldQuit;
1894
+ }
1895
+ /**
1896
+ * Get cursor position
1897
+ */
1898
+ getCursorPos() {
1899
+ const cursor = this.ansiParser.getCursor();
1900
+ return { x: cursor.col, y: cursor.row };
1901
+ }
1902
+ /**
1903
+ * Get the SDL window
1904
+ */
1905
+ getWindow() {
1906
+ return this.window;
1907
+ }
1908
+ /**
1909
+ * Get the SDL renderer
1910
+ */
1911
+ getRenderer() {
1912
+ return this.renderer;
1913
+ }
1914
+ /**
1915
+ * Set the scale factor
1916
+ */
1917
+ setScaleFactor(scaleFactor) {
1918
+ this.userScaleFactor = scaleFactor;
1919
+ let newScale;
1920
+ if (scaleFactor !== null) {
1921
+ newScale = scaleFactor;
1922
+ } else if (this.window && this.renderer) {
1923
+ newScale = this.sdl.getScaleFactorFromRenderer(
1924
+ this.window,
1925
+ this.renderer
1926
+ );
1927
+ } else {
1928
+ newScale = 1;
1929
+ }
1930
+ if (Math.abs(newScale - this.scaleFactor) < SCALE_FACTOR_EPSILON) {
1931
+ return;
1932
+ }
1933
+ this.scaleFactor = newScale;
1934
+ if (this.textRenderer) {
1935
+ this.textRenderer.updateScaleFactor(newScale);
1936
+ const charDims = this.textRenderer.getCharDimensions();
1937
+ this.charWidth = charDims.width;
1938
+ this.charHeight = charDims.height;
1939
+ }
1940
+ this.updateTerminalDimensions();
1941
+ this.clear();
1942
+ }
1943
+ /**
1944
+ * Get current scale factor
1945
+ */
1946
+ getScaleFactor() {
1947
+ return this.scaleFactor;
1948
+ }
1949
+ /**
1950
+ * Clean up resources
1951
+ */
1952
+ destroy() {
1953
+ if (this.textRenderer) {
1954
+ this.textRenderer.destroy();
1955
+ this.textRenderer = null;
1956
+ }
1957
+ if (this.renderer) {
1958
+ this.sdl.destroyRenderer(this.renderer);
1959
+ this.renderer = null;
1960
+ }
1961
+ if (this.window) {
1962
+ this.sdl.destroyWindow(this.window);
1963
+ this.window = null;
1964
+ }
1965
+ }
1966
+ /**
1967
+ * Reset state for reuse
1968
+ */
1969
+ reset() {
1970
+ this.shouldQuit = false;
1971
+ this.pendingCommands = [];
1972
+ this.fgColor = { ...DEFAULT_FG2 };
1973
+ this.bgColor = { ...DEFAULT_BG2 };
1974
+ this.bold = false;
1975
+ this.dim = false;
1976
+ this.reverse = false;
1977
+ this.ansiParser.reset();
1978
+ if (this.window) {
1979
+ const size = this.sdl.getWindowSize(this.window);
1980
+ this.windowWidth = size.width;
1981
+ this.windowHeight = size.height;
1982
+ this.updateTerminalDimensions();
1983
+ }
1984
+ }
1985
+ };
1986
+
1987
+ // src/streams/output-stream.ts
1988
+ import { Writable } from "stream";
1989
+ var DEFAULT_COLUMNS2 = 80;
1990
+ var DEFAULT_ROWS2 = 24;
1991
+ var SdlOutputStream = class extends Writable {
1992
+ /** TTY interface property expected by Ink */
1993
+ isTTY = true;
1994
+ uiRenderer;
1995
+ constructor(uiRenderer) {
1996
+ super({
1997
+ decodeStrings: false
1998
+ });
1999
+ this.uiRenderer = uiRenderer;
2000
+ }
2001
+ /**
2002
+ * Get terminal columns
2003
+ */
2004
+ get columns() {
2005
+ const dims = this.uiRenderer.getDimensions();
2006
+ return dims.columns || DEFAULT_COLUMNS2;
2007
+ }
2008
+ /**
2009
+ * Get terminal rows
2010
+ */
2011
+ get rows() {
2012
+ const dims = this.uiRenderer.getDimensions();
2013
+ return dims.rows || DEFAULT_ROWS2;
2014
+ }
2015
+ /**
2016
+ * Notify Ink of resize
2017
+ */
2018
+ notifyResize() {
2019
+ this.emit("resize");
2020
+ }
2021
+ /**
2022
+ * Implement Writable._write
2023
+ */
2024
+ _write(chunk, _encoding, callback) {
2025
+ try {
2026
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
2027
+ this.uiRenderer.processAnsi(text);
2028
+ this.uiRenderer.present();
2029
+ callback(null);
2030
+ } catch (error) {
2031
+ callback(error instanceof Error ? error : new Error(String(error)));
2032
+ }
2033
+ }
2034
+ /**
2035
+ * Get the underlying renderer
2036
+ */
2037
+ getRenderer() {
2038
+ return this.uiRenderer;
2039
+ }
2040
+ /**
2041
+ * Clear the screen
2042
+ */
2043
+ clear() {
2044
+ this.uiRenderer.clear();
2045
+ }
2046
+ /**
2047
+ * Write a string directly (bypasses Writable buffering)
2048
+ */
2049
+ writeSync(text) {
2050
+ this.uiRenderer.processAnsi(text);
2051
+ this.uiRenderer.present();
2052
+ }
2053
+ /**
2054
+ * Get cursor position
2055
+ */
2056
+ getCursorPos() {
2057
+ return this.uiRenderer.getCursorPos();
2058
+ }
2059
+ };
2060
+
2061
+ // src/streams/input-stream.ts
2062
+ import { Readable } from "stream";
2063
+ var SdlInputStream = class extends Readable {
2064
+ /** TTY interface properties expected by Ink */
2065
+ isTTY = true;
2066
+ isRaw = true;
2067
+ buffer = [];
2068
+ waiting = false;
2069
+ constructor() {
2070
+ super({
2071
+ encoding: "utf8"
2072
+ });
2073
+ }
2074
+ /**
2075
+ * Push a key sequence into the stream
2076
+ */
2077
+ pushKey(sequence) {
2078
+ this.buffer.push(sequence);
2079
+ if (this.waiting) {
2080
+ this.waiting = false;
2081
+ this._read();
2082
+ }
2083
+ }
2084
+ /**
2085
+ * Implement Readable._read
2086
+ */
2087
+ _read() {
2088
+ if (this.buffer.length > 0) {
2089
+ while (this.buffer.length > 0) {
2090
+ const sequence = this.buffer.shift();
2091
+ if (sequence !== void 0) {
2092
+ this.push(sequence);
2093
+ }
2094
+ }
2095
+ } else {
2096
+ this.waiting = true;
2097
+ }
2098
+ }
2099
+ /**
2100
+ * Set raw mode (no-op for SDL, we're always raw)
2101
+ */
2102
+ setRawMode(_mode) {
2103
+ return this;
2104
+ }
2105
+ /**
2106
+ * Check if stream has buffered data
2107
+ */
2108
+ hasData() {
2109
+ return this.buffer.length > 0;
2110
+ }
2111
+ /**
2112
+ * Clear the input buffer
2113
+ */
2114
+ clear() {
2115
+ this.buffer = [];
2116
+ }
2117
+ /**
2118
+ * Close the stream
2119
+ */
2120
+ close() {
2121
+ this.push(null);
2122
+ }
2123
+ /**
2124
+ * Keep the event loop alive (no-op for SDL)
2125
+ */
2126
+ ref() {
2127
+ return this;
2128
+ }
2129
+ /**
2130
+ * Allow event loop to exit (no-op for SDL)
2131
+ */
2132
+ unref() {
2133
+ return this;
2134
+ }
2135
+ };
2136
+
2137
+ // src/streams/index.ts
2138
+ var EVENT_LOOP_INTERVAL_MS = 16;
2139
+ var SdlWindow = class extends EventEmitter {
2140
+ renderer;
2141
+ eventLoopHandle = null;
2142
+ inputStream;
2143
+ outputStream;
2144
+ closed = false;
2145
+ constructor(renderer, inputStream, outputStream) {
2146
+ super();
2147
+ this.renderer = renderer;
2148
+ this.inputStream = inputStream;
2149
+ this.outputStream = outputStream;
2150
+ this.startEventLoop();
2151
+ }
2152
+ /**
2153
+ * Start the SDL event loop
2154
+ */
2155
+ startEventLoop() {
2156
+ this.eventLoopHandle = setInterval(() => {
2157
+ if (this.closed) {
2158
+ return;
2159
+ }
2160
+ const keyEvents = this.renderer.processEvents();
2161
+ for (const event of keyEvents) {
2162
+ const sequence = this.renderer.keyEventToSequence(event);
2163
+ if (sequence) {
2164
+ this.inputStream.pushKey(sequence);
2165
+ this.emit("key", event);
2166
+ }
2167
+ }
2168
+ if (this.renderer.shouldClose()) {
2169
+ this.emit("close");
2170
+ this.close();
2171
+ }
2172
+ }, EVENT_LOOP_INTERVAL_MS);
2173
+ }
2174
+ /**
2175
+ * Get terminal dimensions
2176
+ */
2177
+ getDimensions() {
2178
+ return this.renderer.getDimensions();
2179
+ }
2180
+ /**
2181
+ * Set window title
2182
+ */
2183
+ setTitle(title) {
2184
+ const window = this.renderer.getWindow();
2185
+ if (window) {
2186
+ getSDL2().setWindowTitle(window, title);
2187
+ }
2188
+ }
2189
+ /**
2190
+ * Clear the screen
2191
+ */
2192
+ clear() {
2193
+ this.renderer.clear();
2194
+ this.renderer.present();
2195
+ }
2196
+ /**
2197
+ * Close the window
2198
+ */
2199
+ close() {
2200
+ if (this.closed) {
2201
+ return;
2202
+ }
2203
+ this.closed = true;
2204
+ if (this.eventLoopHandle) {
2205
+ clearInterval(this.eventLoopHandle);
2206
+ this.eventLoopHandle = null;
2207
+ }
2208
+ this.inputStream.close();
2209
+ this.renderer.destroy();
2210
+ }
2211
+ /**
2212
+ * Check if window is closed
2213
+ */
2214
+ isClosed() {
2215
+ return this.closed;
2216
+ }
2217
+ /**
2218
+ * Get the output stream
2219
+ */
2220
+ getOutputStream() {
2221
+ return this.outputStream;
2222
+ }
2223
+ };
2224
+ var createSdlStreams = (options = {}) => {
2225
+ const rendererOptions = {};
2226
+ if (options.title !== void 0) {
2227
+ rendererOptions.title = options.title;
2228
+ }
2229
+ if (options.width !== void 0) {
2230
+ rendererOptions.width = options.width;
2231
+ }
2232
+ if (options.height !== void 0) {
2233
+ rendererOptions.height = options.height;
2234
+ }
2235
+ if (options.vsync !== void 0) {
2236
+ rendererOptions.vsync = options.vsync;
2237
+ }
2238
+ if (options.fontSize !== void 0) {
2239
+ rendererOptions.fontSize = options.fontSize;
2240
+ }
2241
+ if (options.scaleFactor !== void 0) {
2242
+ rendererOptions.scaleFactor = options.scaleFactor;
2243
+ }
2244
+ const renderer = new SdlUiRenderer(rendererOptions);
2245
+ const inputStream = new SdlInputStream();
2246
+ const outputStream = new SdlOutputStream(renderer);
2247
+ const window = new SdlWindow(renderer, inputStream, outputStream);
2248
+ return {
2249
+ stdin: inputStream,
2250
+ stdout: outputStream,
2251
+ window
2252
+ };
2253
+ };
2254
+
2255
+ // src/index.ts
2256
+ var isSdlAvailable = () => {
2257
+ try {
2258
+ return isSDL2Available() && isSDL_ttfAvailable();
2259
+ } catch {
2260
+ return false;
2261
+ }
2262
+ };
2263
+ export {
2264
+ AnsiParser,
2265
+ InputBridge,
2266
+ SDL2API,
2267
+ SDL_ttfAPI,
2268
+ SdlInputStream,
2269
+ SdlOutputStream,
2270
+ SdlUiRenderer,
2271
+ SdlWindow,
2272
+ TextRenderer,
2273
+ createSDLRect,
2274
+ createSdlStreams,
2275
+ getSDL2,
2276
+ getSDL_ttf,
2277
+ isSDL2Available,
2278
+ isSDL_ttfAvailable,
2279
+ isSdlAvailable
2280
+ };