ink-sdl 0.2.1 → 0.3.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,2822 @@
1
+ // src/SdlWindow/index.ts
2
+ import { EventEmitter } from "events";
3
+ import { pickBy, isDefined } from "remeda";
4
+
5
+ // src/Sdl2/index.ts
6
+ import koffi from "koffi";
7
+ import { platform } from "os";
8
+ import { existsSync } from "fs";
9
+ import { find, last } from "remeda";
10
+
11
+ // src/Sdl2/consts.ts
12
+ var SDL_INIT_VIDEO = 32;
13
+ var SDL_INIT_EVENTS = 16384;
14
+ var SDL_WINDOW_FULLSCREEN = 1;
15
+ var SDL_WINDOW_SHOWN = 4;
16
+ var SDL_WINDOW_BORDERLESS = 16;
17
+ var SDL_WINDOW_RESIZABLE = 32;
18
+ var SDL_WINDOW_ALLOW_HIGHDPI = 8192;
19
+ var SDL_WINDOW_FULLSCREEN_DESKTOP_MODIFIER = 4096;
20
+ var SDL_WINDOW_FULLSCREEN_DESKTOP = SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP_MODIFIER;
21
+ var SDL_WINDOWPOS_CENTERED = 805240832;
22
+ var SDL_RENDERER_ACCELERATED = 2;
23
+ var SDL_RENDERER_PRESENTVSYNC = 4;
24
+ var SDL_RENDERER_SOFTWARE = 1;
25
+ var SDL_TEXTUREACCESS_TARGET = 2;
26
+ var SDL_BLENDMODE_BLEND = 1;
27
+ var SDL_PIXELFORMAT_ARGB8888 = 372645892;
28
+ var SDL_QUIT = 256;
29
+ var SDL_WINDOWEVENT = 512;
30
+ var SDL_KEYDOWN = 768;
31
+ var SDL_KEYUP = 769;
32
+ var SDL_WINDOWEVENT_SIZE_CHANGED = 6;
33
+ var SDL_WINDOWEVENT_FOCUS_LOST = 13;
34
+ var SDL_WINDOWEVENT_CLOSE = 14;
35
+ var SDLK_RETURN = 13;
36
+ var SDLK_ESCAPE = 27;
37
+ var SDLK_SPACE = 32;
38
+ var SDLK_BACKSPACE = 8;
39
+ var SDLK_TAB = 9;
40
+ var SDLK_DELETE = 127;
41
+ var SDLK_RIGHT = 1073741903;
42
+ var SDLK_LEFT = 1073741904;
43
+ var SDLK_DOWN = 1073741905;
44
+ var SDLK_UP = 1073741906;
45
+ var SDLK_HOME = 1073741898;
46
+ var SDLK_END = 1073741901;
47
+ var SDLK_PAGEUP = 1073741899;
48
+ var SDLK_PAGEDOWN = 1073741902;
49
+ var SDLK_F1 = 1073741882;
50
+ var SDLK_F12 = 1073741893;
51
+ var SDLK_LSHIFT = 1073742049;
52
+ var SDLK_RSHIFT = 1073742053;
53
+ var SDLK_LCTRL = 1073742048;
54
+ var SDLK_RCTRL = 1073742052;
55
+ var SDLK_LALT = 1073742050;
56
+ var SDLK_RALT = 1073742054;
57
+ var INT32_BYTES = 4;
58
+ var SDL_RECT_SIZE = 16;
59
+ var SDL_RECT_X_OFFSET = 0;
60
+ var SDL_RECT_Y_OFFSET = 4;
61
+ var SDL_RECT_W_OFFSET = 8;
62
+ var SDL_RECT_H_OFFSET = 12;
63
+ var SDL_EVENT_SIZE = 56;
64
+ var SDL_WINDOW_EVENT_OFFSET = 12;
65
+ var SDL_KEY_STATE_OFFSET = 12;
66
+ var SDL_KEY_REPEAT_OFFSET = 13;
67
+ var SDL_KEYSYM_SYM_OFFSET = 20;
68
+ var ASCII_A_LOWER = 97;
69
+ var ASCII_Z_LOWER = 122;
70
+
71
+ // src/Sdl2/index.ts
72
+ var SDL_LIB_PATHS = {
73
+ darwin: [
74
+ "/opt/homebrew/lib/libSDL2.dylib",
75
+ // Homebrew ARM
76
+ "/usr/local/lib/libSDL2.dylib",
77
+ // Homebrew Intel
78
+ "/opt/local/lib/libSDL2.dylib",
79
+ // MacPorts
80
+ "libSDL2.dylib"
81
+ // System path
82
+ ],
83
+ linux: [
84
+ "/usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0",
85
+ // Debian/Ubuntu x64
86
+ "/usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0",
87
+ // Debian/Ubuntu ARM64
88
+ "/usr/lib64/libSDL2-2.0.so.0",
89
+ // Fedora/RHEL
90
+ "/usr/lib/libSDL2-2.0.so.0",
91
+ // Arch
92
+ "libSDL2-2.0.so.0"
93
+ // System path
94
+ ],
95
+ win32: ["SDL2.dll", "C:\\Windows\\System32\\SDL2.dll"]
96
+ };
97
+ var isSystemPath = (p) => !p.includes("/") && !p.includes("\\");
98
+ var findLibrary = (pathMap) => {
99
+ const plat = platform();
100
+ const paths = pathMap[plat] ?? [];
101
+ const foundPath = find(paths, (p) => isSystemPath(p) || existsSync(p));
102
+ return foundPath ?? last(paths) ?? null;
103
+ };
104
+ var findSDLLibrary = () => {
105
+ return findLibrary(SDL_LIB_PATHS);
106
+ };
107
+ var Sdl2 = class {
108
+ lib;
109
+ initialized = false;
110
+ // Core functions
111
+ _SDL_Init;
112
+ _SDL_Quit;
113
+ _SDL_GetError;
114
+ // Window functions
115
+ _SDL_CreateWindow;
116
+ _SDL_DestroyWindow;
117
+ _SDL_SetWindowTitle;
118
+ _SDL_GetWindowSize;
119
+ _SDL_RaiseWindow;
120
+ _SDL_SetWindowMinimumSize;
121
+ _SDL_SetWindowMaximumSize;
122
+ // Renderer functions
123
+ _SDL_CreateRenderer;
124
+ _SDL_DestroyRenderer;
125
+ _SDL_RenderClear;
126
+ _SDL_RenderPresent;
127
+ _SDL_RenderCopy;
128
+ _SDL_SetRenderDrawColor;
129
+ // Texture functions
130
+ _SDL_CreateTexture;
131
+ _SDL_DestroyTexture;
132
+ _SDL_UpdateTexture;
133
+ _SDL_CreateTextureFromSurface;
134
+ _SDL_SetTextureBlendMode;
135
+ _SDL_SetTextureColorMod;
136
+ // Surface functions
137
+ _SDL_FreeSurface;
138
+ // Additional renderer functions
139
+ _SDL_GetRendererOutputSize;
140
+ _SDL_RenderFillRect;
141
+ _SDL_SetRenderTarget;
142
+ // HiDPI functions
143
+ _SDL_GL_GetDrawableSize;
144
+ // Event functions
145
+ _SDL_PollEvent;
146
+ constructor() {
147
+ const libPath = findSDLLibrary();
148
+ if (!libPath) {
149
+ throw new Error(
150
+ "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"
151
+ );
152
+ }
153
+ try {
154
+ this.lib = koffi.load(libPath);
155
+ this.bindFunctions();
156
+ } catch (err) {
157
+ const message = err instanceof Error ? err.message : String(err);
158
+ throw new Error(
159
+ `Failed to load SDL2 library from ${libPath}: ${message}`
160
+ );
161
+ }
162
+ }
163
+ bindFunctions() {
164
+ this._SDL_Init = this.lib.func("int SDL_Init(uint32_t flags)");
165
+ this._SDL_Quit = this.lib.func("void SDL_Quit()");
166
+ this._SDL_GetError = this.lib.func("const char* SDL_GetError()");
167
+ this._SDL_CreateWindow = this.lib.func(
168
+ "void* SDL_CreateWindow(const char* title, int x, int y, int w, int h, uint32_t flags)"
169
+ );
170
+ this._SDL_DestroyWindow = this.lib.func(
171
+ "void SDL_DestroyWindow(void* window)"
172
+ );
173
+ this._SDL_SetWindowTitle = this.lib.func(
174
+ "void SDL_SetWindowTitle(void* window, const char* title)"
175
+ );
176
+ this._SDL_GetWindowSize = this.lib.func(
177
+ "void SDL_GetWindowSize(void* window, int* w, int* h)"
178
+ );
179
+ this._SDL_RaiseWindow = this.lib.func("void SDL_RaiseWindow(void* window)");
180
+ this._SDL_SetWindowMinimumSize = this.lib.func(
181
+ "void SDL_SetWindowMinimumSize(void* window, int min_w, int min_h)"
182
+ );
183
+ this._SDL_SetWindowMaximumSize = this.lib.func(
184
+ "void SDL_SetWindowMaximumSize(void* window, int max_w, int max_h)"
185
+ );
186
+ this._SDL_CreateRenderer = this.lib.func(
187
+ "void* SDL_CreateRenderer(void* window, int index, uint32_t flags)"
188
+ );
189
+ this._SDL_DestroyRenderer = this.lib.func(
190
+ "void SDL_DestroyRenderer(void* renderer)"
191
+ );
192
+ this._SDL_RenderClear = this.lib.func(
193
+ "int SDL_RenderClear(void* renderer)"
194
+ );
195
+ this._SDL_RenderPresent = this.lib.func(
196
+ "void SDL_RenderPresent(void* renderer)"
197
+ );
198
+ this._SDL_RenderCopy = this.lib.func(
199
+ "int SDL_RenderCopy(void* renderer, void* texture, void* srcrect, void* dstrect)"
200
+ );
201
+ this._SDL_SetRenderDrawColor = this.lib.func(
202
+ "int SDL_SetRenderDrawColor(void* renderer, uint8_t r, uint8_t g, uint8_t b, uint8_t a)"
203
+ );
204
+ this._SDL_CreateTexture = this.lib.func(
205
+ "void* SDL_CreateTexture(void* renderer, uint32_t format, int access, int w, int h)"
206
+ );
207
+ this._SDL_DestroyTexture = this.lib.func(
208
+ "void SDL_DestroyTexture(void* texture)"
209
+ );
210
+ this._SDL_UpdateTexture = this.lib.func(
211
+ "int SDL_UpdateTexture(void* texture, void* rect, const void* pixels, int pitch)"
212
+ );
213
+ this._SDL_CreateTextureFromSurface = this.lib.func(
214
+ "void* SDL_CreateTextureFromSurface(void* renderer, void* surface)"
215
+ );
216
+ this._SDL_SetTextureBlendMode = this.lib.func(
217
+ "int SDL_SetTextureBlendMode(void* texture, int blendMode)"
218
+ );
219
+ this._SDL_SetTextureColorMod = this.lib.func(
220
+ "int SDL_SetTextureColorMod(void* texture, uint8_t r, uint8_t g, uint8_t b)"
221
+ );
222
+ this._SDL_FreeSurface = this.lib.func(
223
+ "void SDL_FreeSurface(void* surface)"
224
+ );
225
+ this._SDL_GetRendererOutputSize = this.lib.func(
226
+ "int SDL_GetRendererOutputSize(void* renderer, int* w, int* h)"
227
+ );
228
+ this._SDL_RenderFillRect = this.lib.func(
229
+ "int SDL_RenderFillRect(void* renderer, void* rect)"
230
+ );
231
+ this._SDL_SetRenderTarget = this.lib.func(
232
+ "int SDL_SetRenderTarget(void* renderer, void* texture)"
233
+ );
234
+ this._SDL_GL_GetDrawableSize = this.lib.func(
235
+ "void SDL_GL_GetDrawableSize(void* window, int* w, int* h)"
236
+ );
237
+ this._SDL_PollEvent = this.lib.func("int SDL_PollEvent(void* event)");
238
+ }
239
+ /**
240
+ * Initialize SDL with the given subsystems
241
+ */
242
+ init(flags = SDL_INIT_VIDEO | SDL_INIT_EVENTS) {
243
+ if (this.initialized) {
244
+ return true;
245
+ }
246
+ const result = this._SDL_Init(flags);
247
+ if (result !== 0) {
248
+ return false;
249
+ }
250
+ this.initialized = true;
251
+ return true;
252
+ }
253
+ /**
254
+ * Shutdown SDL
255
+ */
256
+ quit() {
257
+ if (this.initialized) {
258
+ this._SDL_Quit();
259
+ this.initialized = false;
260
+ }
261
+ }
262
+ /**
263
+ * Get the last SDL error message
264
+ */
265
+ getError() {
266
+ return this._SDL_GetError();
267
+ }
268
+ /**
269
+ * Create a window
270
+ */
271
+ createWindow(title, x, y, width, height, flags) {
272
+ const window = this._SDL_CreateWindow(title, x, y, width, height, flags);
273
+ if (!window) {
274
+ throw new Error(`SDL_CreateWindow failed: ${this._SDL_GetError()}`);
275
+ }
276
+ return window;
277
+ }
278
+ /**
279
+ * Destroy a window
280
+ */
281
+ destroyWindow(window) {
282
+ this._SDL_DestroyWindow(window);
283
+ }
284
+ /**
285
+ * Set window title
286
+ */
287
+ setWindowTitle(window, title) {
288
+ this._SDL_SetWindowTitle(window, title);
289
+ }
290
+ /**
291
+ * Get window size
292
+ */
293
+ getWindowSize(window) {
294
+ const wBuf = Buffer.alloc(INT32_BYTES);
295
+ const hBuf = Buffer.alloc(INT32_BYTES);
296
+ this._SDL_GetWindowSize(window, wBuf, hBuf);
297
+ return {
298
+ width: wBuf.readInt32LE(0),
299
+ height: hBuf.readInt32LE(0)
300
+ };
301
+ }
302
+ /**
303
+ * Raise window to front and give it keyboard focus
304
+ */
305
+ raiseWindow(window) {
306
+ this._SDL_RaiseWindow(window);
307
+ }
308
+ /**
309
+ * Set the minimum size of a window
310
+ */
311
+ setWindowMinimumSize(window, minW, minH) {
312
+ this._SDL_SetWindowMinimumSize(window, minW, minH);
313
+ }
314
+ /**
315
+ * Set the maximum size of a window
316
+ */
317
+ setWindowMaximumSize(window, maxW, maxH) {
318
+ this._SDL_SetWindowMaximumSize(window, maxW, maxH);
319
+ }
320
+ /**
321
+ * Create a renderer for a window
322
+ */
323
+ createRenderer(window, index = -1, flags = SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) {
324
+ const renderer = this._SDL_CreateRenderer(window, index, flags);
325
+ if (!renderer) {
326
+ const softwareRenderer = this._SDL_CreateRenderer(
327
+ window,
328
+ -1,
329
+ SDL_RENDERER_SOFTWARE
330
+ );
331
+ if (!softwareRenderer) {
332
+ throw new Error(`SDL_CreateRenderer failed: ${this._SDL_GetError()}`);
333
+ }
334
+ return softwareRenderer;
335
+ }
336
+ return renderer;
337
+ }
338
+ /**
339
+ * Destroy a renderer
340
+ */
341
+ destroyRenderer(renderer) {
342
+ this._SDL_DestroyRenderer(renderer);
343
+ }
344
+ /**
345
+ * Clear the renderer
346
+ */
347
+ renderClear(renderer) {
348
+ this._SDL_RenderClear(renderer);
349
+ }
350
+ /**
351
+ * Present the renderer (flip buffers)
352
+ */
353
+ renderPresent(renderer) {
354
+ this._SDL_RenderPresent(renderer);
355
+ }
356
+ /**
357
+ * Copy texture to renderer
358
+ */
359
+ renderCopy(renderer, texture, srcRect = null, dstRect = null) {
360
+ this._SDL_RenderCopy(renderer, texture, srcRect, dstRect);
361
+ }
362
+ /**
363
+ * Set render draw color
364
+ */
365
+ setRenderDrawColor(renderer, r, g, b, a = 255) {
366
+ this._SDL_SetRenderDrawColor(renderer, r, g, b, a);
367
+ }
368
+ /**
369
+ * Create a texture
370
+ */
371
+ createTexture(renderer, format, access, width, height) {
372
+ const texture = this._SDL_CreateTexture(
373
+ renderer,
374
+ format,
375
+ access,
376
+ width,
377
+ height
378
+ );
379
+ if (!texture) {
380
+ throw new Error(`SDL_CreateTexture failed: ${this._SDL_GetError()}`);
381
+ }
382
+ return texture;
383
+ }
384
+ /**
385
+ * Destroy a texture
386
+ */
387
+ destroyTexture(texture) {
388
+ this._SDL_DestroyTexture(texture);
389
+ }
390
+ /**
391
+ * Update texture with pixel data
392
+ */
393
+ updateTexture(texture, pixels, pitch) {
394
+ this._SDL_UpdateTexture(texture, null, pixels, pitch);
395
+ }
396
+ /**
397
+ * Create a texture from an SDL surface
398
+ */
399
+ createTextureFromSurface(renderer, surface) {
400
+ const texture = this._SDL_CreateTextureFromSurface(renderer, surface);
401
+ if (!texture) {
402
+ throw new Error(
403
+ `SDL_CreateTextureFromSurface failed: ${this._SDL_GetError()}`
404
+ );
405
+ }
406
+ return texture;
407
+ }
408
+ /**
409
+ * Set texture blend mode
410
+ */
411
+ setTextureBlendMode(texture, blendMode) {
412
+ this._SDL_SetTextureBlendMode(texture, blendMode);
413
+ }
414
+ /**
415
+ * Set texture color modulation (tint)
416
+ */
417
+ setTextureColorMod(texture, r, g, b) {
418
+ this._SDL_SetTextureColorMod(texture, r, g, b);
419
+ }
420
+ /**
421
+ * Free an SDL surface
422
+ */
423
+ freeSurface(surface) {
424
+ this._SDL_FreeSurface(surface);
425
+ }
426
+ /**
427
+ * Get the output size of a renderer (physical pixels)
428
+ */
429
+ getRendererOutputSize(renderer) {
430
+ const wBuf = Buffer.alloc(INT32_BYTES);
431
+ const hBuf = Buffer.alloc(INT32_BYTES);
432
+ const result = this._SDL_GetRendererOutputSize(renderer, wBuf, hBuf);
433
+ if (result !== 0) {
434
+ return { width: 0, height: 0 };
435
+ }
436
+ return {
437
+ width: wBuf.readInt32LE(0),
438
+ height: hBuf.readInt32LE(0)
439
+ };
440
+ }
441
+ /**
442
+ * Fill a rectangle with the current draw color
443
+ */
444
+ renderFillRect(renderer, rect) {
445
+ this._SDL_RenderFillRect(renderer, rect);
446
+ }
447
+ /**
448
+ * Set the render target (null for default window)
449
+ */
450
+ setRenderTarget(renderer, texture) {
451
+ this._SDL_SetRenderTarget(renderer, texture);
452
+ }
453
+ /**
454
+ * Get drawable size (physical pixels) for HiDPI windows
455
+ */
456
+ getDrawableSize(window) {
457
+ const wBuf = Buffer.alloc(INT32_BYTES);
458
+ const hBuf = Buffer.alloc(INT32_BYTES);
459
+ this._SDL_GL_GetDrawableSize(window, wBuf, hBuf);
460
+ return {
461
+ width: wBuf.readInt32LE(0),
462
+ height: hBuf.readInt32LE(0)
463
+ };
464
+ }
465
+ /**
466
+ * Get the scale factor between logical and physical pixels
467
+ */
468
+ getScaleFactor(window) {
469
+ const logical = this.getWindowSize(window);
470
+ const physical = this.getDrawableSize(window);
471
+ if (logical.width === 0) {
472
+ return 1;
473
+ }
474
+ return physical.width / logical.width;
475
+ }
476
+ /**
477
+ * Get the scale factor using renderer output size (more reliable for non-GL windows)
478
+ */
479
+ getScaleFactorFromRenderer(window, renderer) {
480
+ const logical = this.getWindowSize(window);
481
+ const physical = this.getRendererOutputSize(renderer);
482
+ if (logical.width === 0 || physical.width === 0) {
483
+ return 1;
484
+ }
485
+ return physical.width / logical.width;
486
+ }
487
+ /**
488
+ * Poll for pending events
489
+ */
490
+ pollEvent() {
491
+ const eventBuf = Buffer.alloc(SDL_EVENT_SIZE);
492
+ const hasEvent = this._SDL_PollEvent(eventBuf);
493
+ if (!hasEvent) {
494
+ return null;
495
+ }
496
+ const type = eventBuf.readUInt32LE(0);
497
+ if (type === SDL_WINDOWEVENT) {
498
+ const windowEvent = eventBuf.readUInt8(SDL_WINDOW_EVENT_OFFSET);
499
+ return { type, windowEvent };
500
+ }
501
+ if (type === SDL_KEYDOWN || type === SDL_KEYUP) {
502
+ const pressed = eventBuf.readUInt8(SDL_KEY_STATE_OFFSET) === 1;
503
+ const repeat = eventBuf.readUInt8(SDL_KEY_REPEAT_OFFSET) !== 0;
504
+ const keycode = eventBuf.readInt32LE(SDL_KEYSYM_SYM_OFFSET);
505
+ return { type, keycode, pressed, repeat };
506
+ }
507
+ return { type };
508
+ }
509
+ /**
510
+ * Check if SDL is initialized
511
+ */
512
+ isInitialized() {
513
+ return this.initialized;
514
+ }
515
+ };
516
+ var sdlInstance = null;
517
+ var getSdl2 = () => {
518
+ if (!sdlInstance) {
519
+ sdlInstance = new Sdl2();
520
+ }
521
+ return sdlInstance;
522
+ };
523
+ var isSdl2Available = () => {
524
+ try {
525
+ getSdl2();
526
+ return true;
527
+ } catch {
528
+ return false;
529
+ }
530
+ };
531
+ var createSDLRect = (x, y, w, h) => {
532
+ const buf = Buffer.alloc(SDL_RECT_SIZE);
533
+ buf.writeInt32LE(x, SDL_RECT_X_OFFSET);
534
+ buf.writeInt32LE(y, SDL_RECT_Y_OFFSET);
535
+ buf.writeInt32LE(w, SDL_RECT_W_OFFSET);
536
+ buf.writeInt32LE(h, SDL_RECT_H_OFFSET);
537
+ return buf;
538
+ };
539
+
540
+ // src/consts.ts
541
+ var COLOR_CHANNEL_MAX = 255;
542
+ var DEFAULT_FONT_SIZE = 16;
543
+ var SCALE_FACTOR_EPSILON = 0.01;
544
+
545
+ // src/AnsiParser/consts.ts
546
+ var ANSI_STANDARD_COLOR_COUNT = 8;
547
+ var ANSI_TAB_WIDTH = 8;
548
+ var ANSI_ERASE_ENTIRE_SCREEN = 2;
549
+ var ANSI_ERASE_TO_END_AND_BEYOND = 3;
550
+ var ANSI_EXTENDED_COLOR_OFFSET_256 = 2;
551
+ var ANSI_EXTENDED_RGB_MIN_PARAMS = 3;
552
+ var ANSI_EXTENDED_COLOR_OFFSET_RGB = 4;
553
+ var ANSI_RGB_R_OFFSET = 1;
554
+ var ANSI_RGB_G_OFFSET = 2;
555
+ var ANSI_RGB_B_OFFSET = 3;
556
+ var ANSI_CUBE_RED_MULTIPLIER = 36;
557
+ var ANSI_256_COLOR_LEVELS = 6;
558
+ var ANSI_CUBE_STEP = 40;
559
+ var ANSI_CUBE_BASE = 55;
560
+
561
+ // src/AnsiParser/index.ts
562
+ var DEFAULT_FG = { r: 255, g: 255, b: 255 };
563
+ var DEFAULT_BG = { r: 0, g: 0, b: 0 };
564
+ var ANSI_COLORS_NORMAL = [
565
+ { r: 0, g: 0, b: 0 },
566
+ // 0: Black
567
+ { r: 187, g: 0, b: 0 },
568
+ // 1: Red
569
+ { r: 0, g: 187, b: 0 },
570
+ // 2: Green
571
+ { r: 187, g: 187, b: 0 },
572
+ // 3: Yellow
573
+ { r: 0, g: 0, b: 187 },
574
+ // 4: Blue
575
+ { r: 187, g: 0, b: 187 },
576
+ // 5: Magenta
577
+ { r: 0, g: 187, b: 187 },
578
+ // 6: Cyan
579
+ { r: 187, g: 187, b: 187 }
580
+ // 7: White
581
+ ];
582
+ var ANSI_COLORS_BRIGHT = [
583
+ { r: 85, g: 85, b: 85 },
584
+ // 8: Bright Black (Gray)
585
+ { r: 255, g: 85, b: 85 },
586
+ // 9: Bright Red
587
+ { r: 85, g: 255, b: 85 },
588
+ // 10: Bright Green
589
+ { r: 255, g: 255, b: 85 },
590
+ // 11: Bright Yellow
591
+ { r: 85, g: 85, b: 255 },
592
+ // 12: Bright Blue
593
+ { r: 255, g: 85, b: 255 },
594
+ // 13: Bright Magenta
595
+ { r: 85, g: 255, b: 255 },
596
+ // 14: Bright Cyan
597
+ { r: 255, g: 255, b: 255 }
598
+ // 15: Bright White
599
+ ];
600
+ var SGR_RESET = 0;
601
+ var SGR_BOLD = 1;
602
+ var SGR_DIM = 2;
603
+ var SGR_ITALIC = 3;
604
+ var SGR_UNDERLINE = 4;
605
+ var SGR_REVERSE = 7;
606
+ var SGR_STRIKETHROUGH = 9;
607
+ var SGR_NORMAL_INTENSITY = 22;
608
+ var SGR_NO_ITALIC = 23;
609
+ var SGR_NO_UNDERLINE = 24;
610
+ var SGR_NO_REVERSE = 27;
611
+ var SGR_NO_STRIKETHROUGH = 29;
612
+ var SGR_FG_BASE = 30;
613
+ var SGR_FG_END = 37;
614
+ var SGR_FG_DEFAULT = 39;
615
+ var SGR_BG_BASE = 40;
616
+ var SGR_BG_END = 47;
617
+ var SGR_BG_DEFAULT = 49;
618
+ var SGR_FG_BRIGHT_BASE = 90;
619
+ var SGR_FG_BRIGHT_END = 97;
620
+ var SGR_BG_BRIGHT_BASE = 100;
621
+ var SGR_BG_BRIGHT_END = 107;
622
+ var SGR_EXTENDED = 38;
623
+ var SGR_EXTENDED_BG = 48;
624
+ var EXTENDED_256 = 5;
625
+ var EXTENDED_RGB = 2;
626
+ var COLOR_CUBE_START = 16;
627
+ var COLOR_CUBE_END = 231;
628
+ var GRAYSCALE_START = 232;
629
+ var GRAYSCALE_END = 255;
630
+ var GRAYSCALE_STEP = 10;
631
+ var GRAYSCALE_BASE = 8;
632
+ var ansi256ToRgb = (index) => {
633
+ if (index < COLOR_CUBE_START) {
634
+ if (index < ANSI_STANDARD_COLOR_COUNT) {
635
+ return ANSI_COLORS_NORMAL[index];
636
+ }
637
+ return ANSI_COLORS_BRIGHT[index - ANSI_STANDARD_COLOR_COUNT];
638
+ }
639
+ if (index <= COLOR_CUBE_END) {
640
+ const cubeIndex = index - COLOR_CUBE_START;
641
+ const r = Math.floor(cubeIndex / ANSI_CUBE_RED_MULTIPLIER);
642
+ const g = Math.floor(
643
+ cubeIndex % ANSI_CUBE_RED_MULTIPLIER / ANSI_256_COLOR_LEVELS
644
+ );
645
+ const b = cubeIndex % ANSI_256_COLOR_LEVELS;
646
+ return {
647
+ r: r > 0 ? r * ANSI_CUBE_STEP + ANSI_CUBE_BASE : 0,
648
+ g: g > 0 ? g * ANSI_CUBE_STEP + ANSI_CUBE_BASE : 0,
649
+ b: b > 0 ? b * ANSI_CUBE_STEP + ANSI_CUBE_BASE : 0
650
+ };
651
+ }
652
+ if (index <= GRAYSCALE_END) {
653
+ const gray = (index - GRAYSCALE_START) * GRAYSCALE_STEP + GRAYSCALE_BASE;
654
+ return { r: gray, g: gray, b: gray };
655
+ }
656
+ return DEFAULT_FG;
657
+ };
658
+ var AnsiParser = class {
659
+ cursorRow = 1;
660
+ cursorCol = 1;
661
+ fgColor = { ...DEFAULT_FG };
662
+ bgColor = { ...DEFAULT_BG };
663
+ bold = false;
664
+ /**
665
+ * Parse an ANSI string and return draw commands
666
+ */
667
+ parse(input) {
668
+ const commands = [];
669
+ let i = 0;
670
+ let textBuffer = "";
671
+ const flushText = () => {
672
+ if (textBuffer.length > 0) {
673
+ commands.push({
674
+ type: "text",
675
+ text: textBuffer,
676
+ row: this.cursorRow,
677
+ col: this.cursorCol
678
+ });
679
+ this.cursorCol += textBuffer.length;
680
+ textBuffer = "";
681
+ }
682
+ };
683
+ while (i < input.length) {
684
+ const char = input[i];
685
+ if (char === "\x1B" && input[i + 1] === "[") {
686
+ flushText();
687
+ let j = i + 2;
688
+ while (j < input.length && !/[A-Za-z]/.test(input[j])) {
689
+ j++;
690
+ }
691
+ if (j < input.length) {
692
+ const sequence = input.substring(i + 2, j);
693
+ const command = input[j];
694
+ this.processEscapeSequence(sequence, command, commands);
695
+ i = j + 1;
696
+ } else {
697
+ i++;
698
+ }
699
+ continue;
700
+ }
701
+ if (char === "\n") {
702
+ flushText();
703
+ this.cursorRow++;
704
+ this.cursorCol = 1;
705
+ i++;
706
+ continue;
707
+ }
708
+ if (char === "\r") {
709
+ flushText();
710
+ this.cursorCol = 1;
711
+ i++;
712
+ continue;
713
+ }
714
+ if (char === " ") {
715
+ flushText();
716
+ const nextTab = Math.ceil(this.cursorCol / ANSI_TAB_WIDTH) * ANSI_TAB_WIDTH + 1;
717
+ const spaces = nextTab - this.cursorCol;
718
+ textBuffer = " ".repeat(spaces);
719
+ flushText();
720
+ i++;
721
+ continue;
722
+ }
723
+ textBuffer += char;
724
+ i++;
725
+ }
726
+ flushText();
727
+ return commands;
728
+ }
729
+ /**
730
+ * Process an escape sequence and emit draw commands
731
+ */
732
+ processEscapeSequence(params, command, commands) {
733
+ switch (command) {
734
+ case "H":
735
+ // Cursor Position (CUP)
736
+ case "f":
737
+ this.processCursorPosition(params, commands);
738
+ break;
739
+ case "J":
740
+ this.processEraseDisplay(params, commands);
741
+ break;
742
+ case "K":
743
+ this.processEraseLine(params, commands);
744
+ break;
745
+ case "m":
746
+ this.processSGR(params, commands);
747
+ break;
748
+ case "A":
749
+ this.cursorRow = Math.max(1, this.cursorRow - (parseInt(params) || 1));
750
+ break;
751
+ case "B":
752
+ this.cursorRow += parseInt(params) || 1;
753
+ break;
754
+ case "C":
755
+ this.cursorCol += parseInt(params) || 1;
756
+ break;
757
+ case "D":
758
+ this.cursorCol = Math.max(1, this.cursorCol - (parseInt(params) || 1));
759
+ break;
760
+ case "G":
761
+ this.cursorCol = parseInt(params) || 1;
762
+ break;
763
+ case "s":
764
+ // Save Cursor Position
765
+ case "u":
766
+ break;
767
+ default:
768
+ break;
769
+ }
770
+ }
771
+ /**
772
+ * Process cursor position sequence
773
+ */
774
+ processCursorPosition(params, commands) {
775
+ const parts = params.split(";");
776
+ this.cursorRow = parseInt(parts[0] ?? "1") || 1;
777
+ this.cursorCol = parseInt(parts[1] ?? "1") || 1;
778
+ commands.push({
779
+ type: "cursor_move",
780
+ row: this.cursorRow,
781
+ col: this.cursorCol
782
+ });
783
+ }
784
+ /**
785
+ * Process erase display sequence
786
+ */
787
+ processEraseDisplay(params, commands) {
788
+ const mode = parseInt(params) || 0;
789
+ if (mode === ANSI_ERASE_ENTIRE_SCREEN || mode === ANSI_ERASE_TO_END_AND_BEYOND) {
790
+ commands.push({ type: "clear_screen" });
791
+ this.cursorRow = 1;
792
+ this.cursorCol = 1;
793
+ }
794
+ }
795
+ /**
796
+ * Process erase line sequence
797
+ */
798
+ processEraseLine(params, commands) {
799
+ const mode = parseInt(params) || 0;
800
+ commands.push({
801
+ type: "clear_line",
802
+ row: this.cursorRow,
803
+ col: mode === 0 ? this.cursorCol : 1
804
+ });
805
+ }
806
+ /**
807
+ * Process SGR (Select Graphic Rendition) sequence
808
+ */
809
+ processSGR(params, commands) {
810
+ if (params === "" || params === "0") {
811
+ this.resetStyle(commands);
812
+ return;
813
+ }
814
+ const codes = params.split(";").map((s) => parseInt(s) || 0);
815
+ let i = 0;
816
+ while (i < codes.length) {
817
+ const code = codes[i];
818
+ if (code === SGR_RESET) {
819
+ this.resetStyle(commands);
820
+ } else if (code === SGR_BOLD) {
821
+ this.bold = true;
822
+ commands.push({ type: "set_bold", enabled: true });
823
+ } else if (code === SGR_DIM) {
824
+ commands.push({ type: "set_dim", enabled: true });
825
+ } else if (code === SGR_ITALIC) {
826
+ commands.push({ type: "set_italic", enabled: true });
827
+ } else if (code === SGR_UNDERLINE) {
828
+ commands.push({ type: "set_underline", enabled: true });
829
+ } else if (code === SGR_REVERSE) {
830
+ commands.push({ type: "set_reverse", enabled: true });
831
+ } else if (code === SGR_STRIKETHROUGH) {
832
+ commands.push({ type: "set_strikethrough", enabled: true });
833
+ } else if (code === SGR_NORMAL_INTENSITY) {
834
+ this.bold = false;
835
+ commands.push({ type: "set_bold", enabled: false });
836
+ commands.push({ type: "set_dim", enabled: false });
837
+ } else if (code === SGR_NO_ITALIC) {
838
+ commands.push({ type: "set_italic", enabled: false });
839
+ } else if (code === SGR_NO_UNDERLINE) {
840
+ commands.push({ type: "set_underline", enabled: false });
841
+ } else if (code === SGR_NO_REVERSE) {
842
+ commands.push({ type: "set_reverse", enabled: false });
843
+ } else if (code === SGR_NO_STRIKETHROUGH) {
844
+ commands.push({ type: "set_strikethrough", enabled: false });
845
+ } else if (code >= SGR_FG_BASE && code <= SGR_FG_END) {
846
+ const colorIndex = code - SGR_FG_BASE;
847
+ this.fgColor = this.bold ? { ...ANSI_COLORS_BRIGHT[colorIndex] } : { ...ANSI_COLORS_NORMAL[colorIndex] };
848
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
849
+ } else if (code === SGR_FG_DEFAULT) {
850
+ this.fgColor = { ...DEFAULT_FG };
851
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
852
+ } else if (code >= SGR_BG_BASE && code <= SGR_BG_END) {
853
+ const colorIndex = code - SGR_BG_BASE;
854
+ this.bgColor = { ...ANSI_COLORS_NORMAL[colorIndex] };
855
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
856
+ } else if (code === SGR_BG_DEFAULT) {
857
+ this.bgColor = { ...DEFAULT_BG };
858
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
859
+ } else if (code >= SGR_FG_BRIGHT_BASE && code <= SGR_FG_BRIGHT_END) {
860
+ const colorIndex = code - SGR_FG_BRIGHT_BASE;
861
+ this.fgColor = { ...ANSI_COLORS_BRIGHT[colorIndex] };
862
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
863
+ } else if (code >= SGR_BG_BRIGHT_BASE && code <= SGR_BG_BRIGHT_END) {
864
+ const colorIndex = code - SGR_BG_BRIGHT_BASE;
865
+ this.bgColor = { ...ANSI_COLORS_BRIGHT[colorIndex] };
866
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
867
+ } else if (code === SGR_EXTENDED) {
868
+ const result = this.parseExtendedColor(codes, i + 1);
869
+ if (result.color) {
870
+ this.fgColor = result.color;
871
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
872
+ }
873
+ i = result.nextIndex - 1;
874
+ } else if (code === SGR_EXTENDED_BG) {
875
+ const result = this.parseExtendedColor(codes, i + 1);
876
+ if (result.color) {
877
+ this.bgColor = result.color;
878
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
879
+ }
880
+ i = result.nextIndex - 1;
881
+ }
882
+ i++;
883
+ }
884
+ }
885
+ /**
886
+ * Parse extended color (256-color or 24-bit)
887
+ */
888
+ parseExtendedColor(codes, startIndex) {
889
+ if (startIndex >= codes.length) {
890
+ return { color: null, nextIndex: startIndex };
891
+ }
892
+ const mode = codes[startIndex];
893
+ if (mode === EXTENDED_256 && startIndex + ANSI_RGB_R_OFFSET < codes.length) {
894
+ const colorIndex = codes[startIndex + ANSI_RGB_R_OFFSET];
895
+ return {
896
+ color: ansi256ToRgb(colorIndex),
897
+ nextIndex: startIndex + ANSI_EXTENDED_COLOR_OFFSET_256
898
+ };
899
+ }
900
+ if (mode === EXTENDED_RGB && startIndex + ANSI_EXTENDED_RGB_MIN_PARAMS < codes.length) {
901
+ return {
902
+ color: {
903
+ r: Math.min(
904
+ COLOR_CHANNEL_MAX,
905
+ Math.max(0, codes[startIndex + ANSI_RGB_R_OFFSET])
906
+ ),
907
+ g: Math.min(
908
+ COLOR_CHANNEL_MAX,
909
+ Math.max(0, codes[startIndex + ANSI_RGB_G_OFFSET])
910
+ ),
911
+ b: Math.min(
912
+ COLOR_CHANNEL_MAX,
913
+ Math.max(0, codes[startIndex + ANSI_RGB_B_OFFSET])
914
+ )
915
+ },
916
+ nextIndex: startIndex + ANSI_EXTENDED_COLOR_OFFSET_RGB
917
+ };
918
+ }
919
+ return { color: null, nextIndex: startIndex + 1 };
920
+ }
921
+ /**
922
+ * Reset all styles to default
923
+ */
924
+ resetStyle(commands) {
925
+ this.fgColor = { ...DEFAULT_FG };
926
+ this.bgColor = { ...DEFAULT_BG };
927
+ this.bold = false;
928
+ commands.push({ type: "reset_style" });
929
+ commands.push({ type: "set_fg", color: { ...this.fgColor } });
930
+ commands.push({ type: "set_bg", color: { ...this.bgColor } });
931
+ }
932
+ /**
933
+ * Get current cursor position
934
+ */
935
+ getCursor() {
936
+ return { row: this.cursorRow, col: this.cursorCol };
937
+ }
938
+ /**
939
+ * Get current foreground color
940
+ */
941
+ getFgColor() {
942
+ return { ...this.fgColor };
943
+ }
944
+ /**
945
+ * Get current background color
946
+ */
947
+ getBgColor() {
948
+ return { ...this.bgColor };
949
+ }
950
+ /**
951
+ * Reset parser state
952
+ */
953
+ reset() {
954
+ this.cursorRow = 1;
955
+ this.cursorCol = 1;
956
+ this.fgColor = { ...DEFAULT_FG };
957
+ this.bgColor = { ...DEFAULT_BG };
958
+ this.bold = false;
959
+ }
960
+ };
961
+
962
+ // src/TextRenderer/index.ts
963
+ import { resolve, dirname, join } from "path";
964
+ import { fileURLToPath } from "url";
965
+ import { existsSync as existsSync3 } from "fs";
966
+ import { platform as platform3, homedir } from "os";
967
+ import { sortBy, take } from "remeda";
968
+
969
+ // src/SdlTtf/index.ts
970
+ import koffi2 from "koffi";
971
+ import { platform as platform2 } from "os";
972
+ import { existsSync as existsSync2 } from "fs";
973
+ import { find as find2, last as last2 } from "remeda";
974
+ var SDL_TTF_LIB_PATHS = {
975
+ darwin: [
976
+ "/opt/homebrew/lib/libSDL2_ttf.dylib",
977
+ // Homebrew ARM
978
+ "/usr/local/lib/libSDL2_ttf.dylib",
979
+ // Homebrew Intel
980
+ "/opt/local/lib/libSDL2_ttf.dylib",
981
+ // MacPorts
982
+ "libSDL2_ttf.dylib"
983
+ // System path
984
+ ],
985
+ linux: [
986
+ "/usr/lib/x86_64-linux-gnu/libSDL2_ttf-2.0.so.0",
987
+ // Debian/Ubuntu x64
988
+ "/usr/lib/aarch64-linux-gnu/libSDL2_ttf-2.0.so.0",
989
+ // Debian/Ubuntu ARM64
990
+ "/usr/lib64/libSDL2_ttf-2.0.so.0",
991
+ // Fedora/RHEL
992
+ "/usr/lib/libSDL2_ttf-2.0.so.0",
993
+ // Arch
994
+ "libSDL2_ttf-2.0.so.0"
995
+ // System path
996
+ ],
997
+ win32: ["SDL2_ttf.dll", "C:\\Windows\\System32\\SDL2_ttf.dll"]
998
+ };
999
+ var isSystemPath2 = (p) => !p.includes("/") && !p.includes("\\");
1000
+ var findLibrary2 = (pathMap) => {
1001
+ const plat = platform2();
1002
+ const paths = pathMap[plat] ?? [];
1003
+ const foundPath = find2(paths, (p) => isSystemPath2(p) || existsSync2(p));
1004
+ return foundPath ?? last2(paths) ?? null;
1005
+ };
1006
+ var findSDLTtfLibrary = () => {
1007
+ return findLibrary2(SDL_TTF_LIB_PATHS);
1008
+ };
1009
+ var SdlTtf = class {
1010
+ lib;
1011
+ initialized = false;
1012
+ // TTF functions
1013
+ _TTF_Init;
1014
+ _TTF_Quit;
1015
+ _TTF_OpenFont;
1016
+ _TTF_CloseFont;
1017
+ _TTF_RenderUTF8_Blended;
1018
+ _TTF_SizeUTF8;
1019
+ _TTF_SetFontStyle;
1020
+ _TTF_GetFontStyle;
1021
+ _TTF_GlyphIsProvided32;
1022
+ constructor() {
1023
+ const libPath = findSDLTtfLibrary();
1024
+ if (!libPath) {
1025
+ throw new Error(
1026
+ "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"
1027
+ );
1028
+ }
1029
+ try {
1030
+ this.lib = koffi2.load(libPath);
1031
+ this.bindFunctions();
1032
+ } catch (err) {
1033
+ const message = err instanceof Error ? err.message : String(err);
1034
+ throw new Error(
1035
+ `Failed to load SDL2_ttf library from ${libPath}: ${message}`
1036
+ );
1037
+ }
1038
+ }
1039
+ bindFunctions() {
1040
+ this._TTF_Init = this.lib.func("int TTF_Init()");
1041
+ this._TTF_Quit = this.lib.func("void TTF_Quit()");
1042
+ this._TTF_OpenFont = this.lib.func(
1043
+ "void* TTF_OpenFont(const char* file, int ptsize)"
1044
+ );
1045
+ this._TTF_CloseFont = this.lib.func("void TTF_CloseFont(void* font)");
1046
+ koffi2.struct("SDL_Color", {
1047
+ r: "uint8_t",
1048
+ g: "uint8_t",
1049
+ b: "uint8_t",
1050
+ a: "uint8_t"
1051
+ });
1052
+ this._TTF_RenderUTF8_Blended = this.lib.func(
1053
+ "void* TTF_RenderUTF8_Blended(void* font, const char* text, SDL_Color fg)"
1054
+ );
1055
+ this._TTF_SizeUTF8 = this.lib.func(
1056
+ "int TTF_SizeUTF8(void* font, const char* text, int* w, int* h)"
1057
+ );
1058
+ this._TTF_SetFontStyle = this.lib.func(
1059
+ "void TTF_SetFontStyle(void* font, int style)"
1060
+ );
1061
+ this._TTF_GetFontStyle = this.lib.func("int TTF_GetFontStyle(void* font)");
1062
+ this._TTF_GlyphIsProvided32 = this.lib.func(
1063
+ "int TTF_GlyphIsProvided32(void* font, uint32_t ch)"
1064
+ );
1065
+ }
1066
+ /**
1067
+ * Initialize SDL_ttf
1068
+ */
1069
+ init() {
1070
+ if (this.initialized) {
1071
+ return true;
1072
+ }
1073
+ const result = this._TTF_Init();
1074
+ if (result !== 0) {
1075
+ return false;
1076
+ }
1077
+ this.initialized = true;
1078
+ return true;
1079
+ }
1080
+ /**
1081
+ * Shutdown SDL_ttf
1082
+ */
1083
+ quit() {
1084
+ if (this.initialized) {
1085
+ this._TTF_Quit();
1086
+ this.initialized = false;
1087
+ }
1088
+ }
1089
+ /**
1090
+ * Get the last SDL_ttf error message
1091
+ */
1092
+ getError() {
1093
+ return getSdl2().getError();
1094
+ }
1095
+ /**
1096
+ * Open a TrueType font file
1097
+ */
1098
+ openFont(file, ptsize) {
1099
+ const font = this._TTF_OpenFont(file, ptsize);
1100
+ if (!font) {
1101
+ throw new Error(`TTF_OpenFont failed: ${getSdl2().getError()}`);
1102
+ }
1103
+ return font;
1104
+ }
1105
+ /**
1106
+ * Close a font
1107
+ */
1108
+ closeFont(font) {
1109
+ this._TTF_CloseFont(font);
1110
+ }
1111
+ /**
1112
+ * Render UTF-8 text to a surface with blended (anti-aliased) rendering
1113
+ */
1114
+ renderTextBlended(font, text, r, g, b, a = 255) {
1115
+ const color = { r, g, b, a };
1116
+ const surface = this._TTF_RenderUTF8_Blended(font, text, color);
1117
+ if (!surface) {
1118
+ throw new Error(`TTF_RenderUTF8_Blended failed: ${getSdl2().getError()}`);
1119
+ }
1120
+ return surface;
1121
+ }
1122
+ /**
1123
+ * Get the dimensions of rendered text without actually rendering
1124
+ */
1125
+ sizeText(font, text) {
1126
+ const wBuf = Buffer.alloc(INT32_BYTES);
1127
+ const hBuf = Buffer.alloc(INT32_BYTES);
1128
+ const result = this._TTF_SizeUTF8(font, text, wBuf, hBuf);
1129
+ if (result !== 0) {
1130
+ return { width: 0, height: 0 };
1131
+ }
1132
+ return {
1133
+ width: wBuf.readInt32LE(0),
1134
+ height: hBuf.readInt32LE(0)
1135
+ };
1136
+ }
1137
+ /**
1138
+ * Check if SDL_ttf is initialized
1139
+ */
1140
+ isInitialized() {
1141
+ return this.initialized;
1142
+ }
1143
+ /**
1144
+ * Set font style (bold, italic, underline, strikethrough)
1145
+ */
1146
+ setFontStyle(font, style) {
1147
+ this._TTF_SetFontStyle(font, style);
1148
+ }
1149
+ /**
1150
+ * Get current font style
1151
+ */
1152
+ getFontStyle(font) {
1153
+ return this._TTF_GetFontStyle(font);
1154
+ }
1155
+ /**
1156
+ * Check if a font provides a glyph for the given Unicode codepoint
1157
+ */
1158
+ glyphIsProvided(font, codepoint) {
1159
+ return this._TTF_GlyphIsProvided32(font, codepoint) !== 0;
1160
+ }
1161
+ };
1162
+ var TTF_STYLE_NORMAL = 0;
1163
+ var TTF_STYLE_ITALIC = 2;
1164
+ var ttfInstance = null;
1165
+ var getSdlTtf = () => {
1166
+ if (!ttfInstance) {
1167
+ ttfInstance = new SdlTtf();
1168
+ }
1169
+ return ttfInstance;
1170
+ };
1171
+ var isSdlTtfAvailable = () => {
1172
+ try {
1173
+ getSdlTtf();
1174
+ return true;
1175
+ } catch {
1176
+ return false;
1177
+ }
1178
+ };
1179
+
1180
+ // src/TextRenderer/consts.ts
1181
+ var DEFAULT_FONT_FILENAME = "CozetteVector.ttf";
1182
+ var MAX_GLYPH_CACHE_SIZE = 1e3;
1183
+ var GLYPH_CACHE_EVICT_DIVISOR = 4;
1184
+ var PACK_RED_SHIFT = 16;
1185
+ var PACK_GREEN_SHIFT = 8;
1186
+ var FALLBACK_FONTS = {
1187
+ darwin: [
1188
+ "/System/Library/Fonts/Menlo.ttc",
1189
+ "/System/Library/Fonts/Monaco.ttf",
1190
+ "/System/Library/Fonts/Courier.dfont"
1191
+ ],
1192
+ linux: [
1193
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
1194
+ "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
1195
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
1196
+ "/usr/share/fonts/truetype/freefont/FreeMono.ttf"
1197
+ ],
1198
+ win32: [
1199
+ "C:\\Windows\\Fonts\\consola.ttf",
1200
+ "C:\\Windows\\Fonts\\cour.ttf",
1201
+ "C:\\Windows\\Fonts\\lucon.ttf"
1202
+ ]
1203
+ };
1204
+ var EMOJI_FONTS = {
1205
+ darwin: ["/System/Library/Fonts/Apple Color Emoji.ttc"],
1206
+ linux: [
1207
+ "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf",
1208
+ "/usr/share/fonts/noto-emoji/NotoColorEmoji.ttf",
1209
+ "/usr/share/fonts/truetype/twitter-color-emoji/TwitterColorEmoji-SVGinOT.ttf",
1210
+ "/usr/share/fonts/google-noto-emoji/NotoColorEmoji.ttf"
1211
+ ],
1212
+ win32: ["C:\\Windows\\Fonts\\seguiemj.ttf"]
1213
+ };
1214
+ var EMOJI_FONT_SCALE = 0.75;
1215
+
1216
+ // src/TextRenderer/index.ts
1217
+ var TextRenderer = class {
1218
+ sdl = getSdl2();
1219
+ ttf = getSdlTtf();
1220
+ font = null;
1221
+ fallbackFont = null;
1222
+ renderer;
1223
+ baseFontSize;
1224
+ scaleFactor;
1225
+ glyphCache = /* @__PURE__ */ new Map();
1226
+ accessCounter = 0;
1227
+ charWidth = 0;
1228
+ charHeight = 0;
1229
+ currentFontPath = null;
1230
+ constructor(renderer, options = {}) {
1231
+ this.renderer = renderer;
1232
+ this.baseFontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
1233
+ this.scaleFactor = options.scaleFactor ?? 1;
1234
+ if (!this.ttf.isInitialized()) {
1235
+ this.ttf.init();
1236
+ }
1237
+ let fontPath;
1238
+ if (options.fontPath) {
1239
+ fontPath = options.fontPath;
1240
+ } else if (options.fontName) {
1241
+ fontPath = this.findFontByName(options.fontName);
1242
+ } else if (options.systemFont) {
1243
+ fontPath = this.findSystemFont();
1244
+ } else {
1245
+ fontPath = this.findAvailableFont();
1246
+ }
1247
+ this.loadFont(fontPath);
1248
+ this.loadFallbackFont();
1249
+ }
1250
+ /**
1251
+ * Get system font directories for the current platform
1252
+ */
1253
+ getSystemFontDirectories() {
1254
+ const home = homedir();
1255
+ const plat = platform3();
1256
+ if (plat === "darwin") {
1257
+ return [
1258
+ join(home, "Library/Fonts"),
1259
+ "/Library/Fonts",
1260
+ "/System/Library/Fonts",
1261
+ "/System/Library/Fonts/Supplemental"
1262
+ ];
1263
+ }
1264
+ if (plat === "linux") {
1265
+ return [
1266
+ join(home, ".fonts"),
1267
+ join(home, ".local/share/fonts"),
1268
+ "/usr/share/fonts/truetype",
1269
+ "/usr/share/fonts/TTF",
1270
+ "/usr/local/share/fonts"
1271
+ ];
1272
+ }
1273
+ if (plat === "win32") {
1274
+ return ["C:\\Windows\\Fonts"];
1275
+ }
1276
+ return [];
1277
+ }
1278
+ /**
1279
+ * Get system font paths for the default font filename
1280
+ */
1281
+ getSystemFontPaths() {
1282
+ return this.getSystemFontDirectories().map(
1283
+ (dir) => join(dir, DEFAULT_FONT_FILENAME)
1284
+ );
1285
+ }
1286
+ /**
1287
+ * Find a font by name in system font directories
1288
+ *
1289
+ * Searches for common font file extensions (.ttf, .ttc, .otf)
1290
+ */
1291
+ findFontByName(fontName) {
1292
+ const extensions = [".ttf", ".ttc", ".otf", ".TTC", ".TTF", ".OTF"];
1293
+ const directories = this.getSystemFontDirectories();
1294
+ for (const dir of directories) {
1295
+ for (const ext of extensions) {
1296
+ const fontPath = join(dir, `${fontName}${ext}`);
1297
+ try {
1298
+ if (existsSync3(fontPath)) {
1299
+ return fontPath;
1300
+ }
1301
+ } catch {
1302
+ }
1303
+ }
1304
+ }
1305
+ throw new Error(
1306
+ `Font "${fontName}" not found in system font directories.
1307
+ Searched directories:
1308
+ - ${directories.join("\n - ")}
1309
+ Tried extensions: ${extensions.join(", ")}`
1310
+ );
1311
+ }
1312
+ /**
1313
+ * Get the path to the Cozette font (system or bundled)
1314
+ */
1315
+ getDefaultFontPath() {
1316
+ for (const p of this.getSystemFontPaths()) {
1317
+ try {
1318
+ if (existsSync3(p)) {
1319
+ return p;
1320
+ }
1321
+ } catch {
1322
+ }
1323
+ }
1324
+ const currentFilename = fileURLToPath(import.meta.url);
1325
+ const currentDirname = dirname(currentFilename);
1326
+ const bundledPaths = [
1327
+ resolve(currentDirname, "../../fonts", DEFAULT_FONT_FILENAME),
1328
+ // Dev path
1329
+ resolve(currentDirname, "./fonts", DEFAULT_FONT_FILENAME),
1330
+ // Bundled (dist)
1331
+ resolve(currentDirname, "../fonts", DEFAULT_FONT_FILENAME)
1332
+ // Alternate
1333
+ ];
1334
+ for (const p of bundledPaths) {
1335
+ try {
1336
+ if (existsSync3(p)) {
1337
+ return p;
1338
+ }
1339
+ } catch {
1340
+ }
1341
+ }
1342
+ return bundledPaths[0];
1343
+ }
1344
+ /**
1345
+ * Get fallback fonts for the current platform
1346
+ */
1347
+ getFallbackFontPaths() {
1348
+ const plat = platform3();
1349
+ if (plat === "darwin") {
1350
+ return [...FALLBACK_FONTS.darwin];
1351
+ }
1352
+ if (plat === "linux") {
1353
+ return [...FALLBACK_FONTS.linux];
1354
+ }
1355
+ if (plat === "win32") {
1356
+ return [...FALLBACK_FONTS.win32];
1357
+ }
1358
+ return [];
1359
+ }
1360
+ /**
1361
+ * Find an available font, trying default first then fallbacks
1362
+ */
1363
+ findAvailableFont() {
1364
+ const defaultPath = this.getDefaultFontPath();
1365
+ try {
1366
+ if (existsSync3(defaultPath)) {
1367
+ return defaultPath;
1368
+ }
1369
+ } catch {
1370
+ }
1371
+ for (const fallbackPath of this.getFallbackFontPaths()) {
1372
+ try {
1373
+ if (existsSync3(fallbackPath)) {
1374
+ return fallbackPath;
1375
+ }
1376
+ } catch {
1377
+ }
1378
+ }
1379
+ return defaultPath;
1380
+ }
1381
+ /**
1382
+ * Find a system font, skipping the default bundled font
1383
+ */
1384
+ findSystemFont() {
1385
+ for (const fallbackPath of this.getFallbackFontPaths()) {
1386
+ try {
1387
+ if (existsSync3(fallbackPath)) {
1388
+ return fallbackPath;
1389
+ }
1390
+ } catch {
1391
+ }
1392
+ }
1393
+ return this.getDefaultFontPath();
1394
+ }
1395
+ /**
1396
+ * Get emoji font paths for the current platform
1397
+ */
1398
+ getEmojiFontPaths() {
1399
+ const plat = platform3();
1400
+ if (plat === "darwin") {
1401
+ return [...EMOJI_FONTS.darwin];
1402
+ }
1403
+ if (plat === "linux") {
1404
+ return [...EMOJI_FONTS.linux];
1405
+ }
1406
+ if (plat === "win32") {
1407
+ return [...EMOJI_FONTS.win32];
1408
+ }
1409
+ return [];
1410
+ }
1411
+ /**
1412
+ * Find an available emoji font
1413
+ */
1414
+ findEmojiFont() {
1415
+ for (const fontPath of this.getEmojiFontPaths()) {
1416
+ try {
1417
+ if (existsSync3(fontPath)) {
1418
+ return fontPath;
1419
+ }
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ return null;
1424
+ }
1425
+ /**
1426
+ * Load the fallback emoji font if available
1427
+ */
1428
+ loadFallbackFont() {
1429
+ if (this.fallbackFont) {
1430
+ this.ttf.closeFont(this.fallbackFont);
1431
+ this.fallbackFont = null;
1432
+ }
1433
+ const emojiFontPath = this.findEmojiFont();
1434
+ if (!emojiFontPath) {
1435
+ return;
1436
+ }
1437
+ const physicalSize = Math.round(
1438
+ this.baseFontSize * this.scaleFactor * EMOJI_FONT_SCALE
1439
+ );
1440
+ try {
1441
+ this.fallbackFont = this.ttf.openFont(emojiFontPath, physicalSize);
1442
+ } catch {
1443
+ this.fallbackFont = null;
1444
+ }
1445
+ }
1446
+ /**
1447
+ * Load a TTF font at the current scaled size
1448
+ */
1449
+ loadFont(fontPath) {
1450
+ if (this.font) {
1451
+ this.ttf.closeFont(this.font);
1452
+ this.font = null;
1453
+ }
1454
+ this.clearCache();
1455
+ const physicalSize = Math.round(this.baseFontSize * this.scaleFactor);
1456
+ try {
1457
+ this.font = this.ttf.openFont(fontPath, physicalSize);
1458
+ this.currentFontPath = fontPath;
1459
+ } catch (error) {
1460
+ const fallbackPaths = this.getFallbackFontPaths();
1461
+ const triedPaths = [fontPath, ...fallbackPaths].join("\n - ");
1462
+ throw new Error(
1463
+ `Failed to load font: ${fontPath}
1464
+ Tried paths:
1465
+ - ${triedPaths}
1466
+ Original error: ${error instanceof Error ? error.message : String(error)}`
1467
+ );
1468
+ }
1469
+ const dims = this.ttf.sizeText(this.font, "M");
1470
+ this.charWidth = dims.width;
1471
+ this.charHeight = dims.height;
1472
+ }
1473
+ /**
1474
+ * Update scale factor (for HiDPI display changes)
1475
+ */
1476
+ updateScaleFactor(scaleFactor) {
1477
+ if (Math.abs(scaleFactor - this.scaleFactor) < SCALE_FACTOR_EPSILON) {
1478
+ return;
1479
+ }
1480
+ this.scaleFactor = scaleFactor;
1481
+ const fontPath = this.currentFontPath ?? this.findAvailableFont();
1482
+ this.loadFont(fontPath);
1483
+ this.loadFallbackFont();
1484
+ }
1485
+ /**
1486
+ * Get character dimensions
1487
+ */
1488
+ getCharDimensions() {
1489
+ return { width: this.charWidth, height: this.charHeight };
1490
+ }
1491
+ /**
1492
+ * Generate cache key for a glyph
1493
+ */
1494
+ getCacheKey(char, r, g, b, italic) {
1495
+ const packedColor = r << PACK_RED_SHIFT | g << PACK_GREEN_SHIFT | b;
1496
+ return `${char}:${packedColor}:${italic ? "i" : "n"}`;
1497
+ }
1498
+ /**
1499
+ * Get or create a cached glyph texture
1500
+ */
1501
+ getGlyph(char, r, g, b, italic = false) {
1502
+ const key = this.getCacheKey(char, r, g, b, italic);
1503
+ const cached = this.glyphCache.get(key);
1504
+ if (cached) {
1505
+ cached.lastUsed = this.accessCounter++;
1506
+ return cached;
1507
+ }
1508
+ if (!this.font) {
1509
+ return null;
1510
+ }
1511
+ try {
1512
+ const codepoint = char.codePointAt(0) ?? 0;
1513
+ let fontToUse = this.font;
1514
+ if (this.fallbackFont && !this.ttf.glyphIsProvided(this.font, codepoint) && this.ttf.glyphIsProvided(this.fallbackFont, codepoint)) {
1515
+ fontToUse = this.fallbackFont;
1516
+ }
1517
+ if (fontToUse === this.font) {
1518
+ const currentStyle = this.ttf.getFontStyle(this.font);
1519
+ const targetStyle = italic ? TTF_STYLE_ITALIC : TTF_STYLE_NORMAL;
1520
+ if (currentStyle !== targetStyle) {
1521
+ this.ttf.setFontStyle(this.font, targetStyle);
1522
+ }
1523
+ }
1524
+ const surface = this.ttf.renderTextBlended(
1525
+ fontToUse,
1526
+ char,
1527
+ COLOR_CHANNEL_MAX,
1528
+ // White
1529
+ COLOR_CHANNEL_MAX,
1530
+ COLOR_CHANNEL_MAX,
1531
+ COLOR_CHANNEL_MAX
1532
+ );
1533
+ const texture = this.sdl.createTextureFromSurface(this.renderer, surface);
1534
+ this.sdl.setTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
1535
+ this.sdl.setTextureColorMod(texture, r, g, b);
1536
+ const dims = this.ttf.sizeText(fontToUse, char);
1537
+ this.sdl.freeSurface(surface);
1538
+ const glyph = {
1539
+ texture,
1540
+ width: dims.width,
1541
+ height: dims.height,
1542
+ lastUsed: this.accessCounter++
1543
+ };
1544
+ if (this.glyphCache.size >= MAX_GLYPH_CACHE_SIZE) {
1545
+ this.evictOldGlyphs();
1546
+ }
1547
+ this.glyphCache.set(key, glyph);
1548
+ return glyph;
1549
+ } catch {
1550
+ return null;
1551
+ }
1552
+ }
1553
+ /**
1554
+ * Evict least recently used glyphs
1555
+ */
1556
+ evictOldGlyphs() {
1557
+ const entries = sortBy(
1558
+ [...this.glyphCache.entries()],
1559
+ ([, glyph]) => glyph.lastUsed
1560
+ );
1561
+ const removeCount = Math.floor(
1562
+ MAX_GLYPH_CACHE_SIZE / GLYPH_CACHE_EVICT_DIVISOR
1563
+ );
1564
+ for (const [key, glyph] of take(entries, removeCount)) {
1565
+ this.sdl.destroyTexture(glyph.texture);
1566
+ this.glyphCache.delete(key);
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Render a single character at the specified position
1571
+ */
1572
+ renderChar(char, x, y, color, italic = false) {
1573
+ const glyph = this.getGlyph(char, color.r, color.g, color.b, italic);
1574
+ if (!glyph) {
1575
+ return;
1576
+ }
1577
+ let destWidth = glyph.width;
1578
+ let destHeight = glyph.height;
1579
+ let destX = x;
1580
+ const destY = y;
1581
+ if (glyph.height > this.charHeight) {
1582
+ const scale = this.charHeight / glyph.height;
1583
+ destWidth = Math.round(glyph.width * scale);
1584
+ destHeight = this.charHeight;
1585
+ destX = x + Math.round((this.charWidth - destWidth) / 2);
1586
+ }
1587
+ const destRect = createSDLRect(destX, destY, destWidth, destHeight);
1588
+ this.sdl.renderCopy(this.renderer, glyph.texture, null, destRect);
1589
+ }
1590
+ /**
1591
+ * Render a string of text at the specified position
1592
+ */
1593
+ renderText(text, x, y, color, italic = false) {
1594
+ let cursorX = x;
1595
+ for (const char of text) {
1596
+ if (char === " ") {
1597
+ cursorX += this.charWidth;
1598
+ continue;
1599
+ }
1600
+ this.renderChar(char, cursorX, y, color, italic);
1601
+ cursorX += this.charWidth;
1602
+ }
1603
+ }
1604
+ /**
1605
+ * Get text dimensions
1606
+ */
1607
+ measureText(text) {
1608
+ if (!this.font) {
1609
+ return { width: 0, height: 0 };
1610
+ }
1611
+ return this.ttf.sizeText(this.font, text);
1612
+ }
1613
+ /**
1614
+ * Clear the glyph cache
1615
+ */
1616
+ clearCache() {
1617
+ for (const glyph of this.glyphCache.values()) {
1618
+ this.sdl.destroyTexture(glyph.texture);
1619
+ }
1620
+ this.glyphCache.clear();
1621
+ this.accessCounter = 0;
1622
+ }
1623
+ /**
1624
+ * Get cache statistics
1625
+ */
1626
+ getCacheStats() {
1627
+ return {
1628
+ size: this.glyphCache.size,
1629
+ maxSize: MAX_GLYPH_CACHE_SIZE
1630
+ };
1631
+ }
1632
+ /**
1633
+ * Clean up resources
1634
+ */
1635
+ destroy() {
1636
+ this.clearCache();
1637
+ if (this.font) {
1638
+ this.ttf.closeFont(this.font);
1639
+ this.font = null;
1640
+ }
1641
+ if (this.fallbackFont) {
1642
+ this.ttf.closeFont(this.fallbackFont);
1643
+ this.fallbackFont = null;
1644
+ }
1645
+ }
1646
+ };
1647
+
1648
+ // src/InputBridge/index.ts
1649
+ import { isNonNullish } from "remeda";
1650
+
1651
+ // src/InputBridge/consts.ts
1652
+ var ASCII_PRINTABLE_START = 32;
1653
+ var ASCII_PRINTABLE_END = 126;
1654
+ var CTRL_KEY_OFFSET = 96;
1655
+ var ASCII_BRACKET_OPEN = 91;
1656
+ var ASCII_BACKSLASH = 92;
1657
+ var ASCII_BRACKET_CLOSE = 93;
1658
+ var ASCII_CARET = 94;
1659
+ var ASCII_UNDERSCORE = 95;
1660
+ var FUNCTION_KEY_OFFSET_3 = 3;
1661
+ var FUNCTION_KEY_OFFSET_4 = 4;
1662
+ var FUNCTION_KEY_OFFSET_5 = 5;
1663
+ var FUNCTION_KEY_OFFSET_6 = 6;
1664
+ var FUNCTION_KEY_OFFSET_7 = 7;
1665
+ var FUNCTION_KEY_OFFSET_8 = 8;
1666
+ var FUNCTION_KEY_OFFSET_9 = 9;
1667
+ var FUNCTION_KEY_OFFSET_10 = 10;
1668
+
1669
+ // src/InputBridge/index.ts
1670
+ var InputBridge = class {
1671
+ modifiers = {
1672
+ shift: false,
1673
+ ctrl: false,
1674
+ alt: false
1675
+ };
1676
+ /**
1677
+ * Process an SDL key event and return terminal sequence
1678
+ */
1679
+ processKeyEvent(event) {
1680
+ const { keycode, pressed } = event;
1681
+ if (this.isModifierKey(keycode)) {
1682
+ this.updateModifierState(keycode, pressed);
1683
+ return null;
1684
+ }
1685
+ if (!pressed) {
1686
+ return null;
1687
+ }
1688
+ const specialSequence = this.getSpecialKeySequence(keycode);
1689
+ if (isNonNullish(specialSequence)) {
1690
+ return specialSequence;
1691
+ }
1692
+ if (this.modifiers.ctrl) {
1693
+ const ctrlSequence = this.getCtrlSequence(keycode);
1694
+ if (isNonNullish(ctrlSequence)) {
1695
+ return ctrlSequence;
1696
+ }
1697
+ }
1698
+ if (keycode >= ASCII_PRINTABLE_START && keycode <= ASCII_PRINTABLE_END) {
1699
+ let char = String.fromCharCode(keycode);
1700
+ if (this.modifiers.shift && keycode >= ASCII_A_LOWER && keycode <= ASCII_Z_LOWER) {
1701
+ char = char.toUpperCase();
1702
+ }
1703
+ return char;
1704
+ }
1705
+ return null;
1706
+ }
1707
+ /**
1708
+ * Check if a keycode is a modifier key
1709
+ */
1710
+ isModifierKey(keycode) {
1711
+ return keycode === SDLK_LSHIFT || keycode === SDLK_RSHIFT || keycode === SDLK_LCTRL || keycode === SDLK_RCTRL || keycode === SDLK_LALT || keycode === SDLK_RALT;
1712
+ }
1713
+ /**
1714
+ * Update modifier key state
1715
+ */
1716
+ updateModifierState(keycode, pressed) {
1717
+ if (keycode === SDLK_LSHIFT || keycode === SDLK_RSHIFT) {
1718
+ this.modifiers.shift = pressed;
1719
+ } else if (keycode === SDLK_LCTRL || keycode === SDLK_RCTRL) {
1720
+ this.modifiers.ctrl = pressed;
1721
+ } else if (keycode === SDLK_LALT || keycode === SDLK_RALT) {
1722
+ this.modifiers.alt = pressed;
1723
+ }
1724
+ }
1725
+ /**
1726
+ * Get terminal escape sequence for special keys
1727
+ */
1728
+ getSpecialKeySequence(keycode) {
1729
+ switch (keycode) {
1730
+ // Navigation keys
1731
+ case SDLK_UP:
1732
+ return "\x1B[A";
1733
+ case SDLK_DOWN:
1734
+ return "\x1B[B";
1735
+ case SDLK_RIGHT:
1736
+ return "\x1B[C";
1737
+ case SDLK_LEFT:
1738
+ return "\x1B[D";
1739
+ case SDLK_HOME:
1740
+ return "\x1B[H";
1741
+ case SDLK_END:
1742
+ return "\x1B[F";
1743
+ case SDLK_PAGEUP:
1744
+ return "\x1B[5~";
1745
+ case SDLK_PAGEDOWN:
1746
+ return "\x1B[6~";
1747
+ // Control keys
1748
+ case SDLK_RETURN:
1749
+ return "\r";
1750
+ case SDLK_ESCAPE:
1751
+ return "\x1B";
1752
+ case SDLK_BACKSPACE:
1753
+ return "\x7F";
1754
+ case SDLK_TAB:
1755
+ return this.modifiers.shift ? "\x1B[Z" : " ";
1756
+ case SDLK_DELETE:
1757
+ return "\x1B[3~";
1758
+ case SDLK_SPACE:
1759
+ return " ";
1760
+ // Function keys
1761
+ case SDLK_F1:
1762
+ return "\x1BOP";
1763
+ case SDLK_F1 + 1:
1764
+ return "\x1BOQ";
1765
+ case SDLK_F1 + 2:
1766
+ return "\x1BOR";
1767
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_3:
1768
+ return "\x1BOS";
1769
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_4:
1770
+ return "\x1B[15~";
1771
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_5:
1772
+ return "\x1B[17~";
1773
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_6:
1774
+ return "\x1B[18~";
1775
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_7:
1776
+ return "\x1B[19~";
1777
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_8:
1778
+ return "\x1B[20~";
1779
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_9:
1780
+ return "\x1B[21~";
1781
+ case SDLK_F1 + FUNCTION_KEY_OFFSET_10:
1782
+ return "\x1B[23~";
1783
+ case SDLK_F12:
1784
+ return "\x1B[24~";
1785
+ default:
1786
+ return null;
1787
+ }
1788
+ }
1789
+ /**
1790
+ * Get Ctrl+key sequence
1791
+ */
1792
+ getCtrlSequence(keycode) {
1793
+ if (keycode >= ASCII_A_LOWER && keycode <= ASCII_Z_LOWER) {
1794
+ const ctrlCode = keycode - CTRL_KEY_OFFSET;
1795
+ return String.fromCharCode(ctrlCode);
1796
+ }
1797
+ switch (keycode) {
1798
+ case SDLK_SPACE:
1799
+ return "\0";
1800
+ // Ctrl+Space = NUL
1801
+ case ASCII_BRACKET_OPEN:
1802
+ return "\x1B";
1803
+ // Ctrl+[ = Escape
1804
+ case ASCII_BACKSLASH:
1805
+ return "";
1806
+ // Ctrl+\ = FS
1807
+ case ASCII_BRACKET_CLOSE:
1808
+ return "";
1809
+ // Ctrl+] = GS
1810
+ case ASCII_CARET:
1811
+ return "";
1812
+ // Ctrl+^ = RS
1813
+ case ASCII_UNDERSCORE:
1814
+ return "";
1815
+ // Ctrl+_ = US
1816
+ default:
1817
+ return null;
1818
+ }
1819
+ }
1820
+ /**
1821
+ * Convert SDL key event to Ink-style key event
1822
+ */
1823
+ toInkKeyEvent(event) {
1824
+ const sequence = this.processKeyEvent(event);
1825
+ if (!isNonNullish(sequence)) {
1826
+ return null;
1827
+ }
1828
+ let name = "";
1829
+ if (sequence.length === 1 && sequence >= " " && sequence <= "~") {
1830
+ name = sequence;
1831
+ } else {
1832
+ name = this.getKeyName(event.keycode);
1833
+ }
1834
+ return {
1835
+ sequence,
1836
+ name,
1837
+ ctrl: this.modifiers.ctrl,
1838
+ meta: this.modifiers.alt,
1839
+ shift: this.modifiers.shift
1840
+ };
1841
+ }
1842
+ /**
1843
+ * Get human-readable key name
1844
+ */
1845
+ getKeyName(keycode) {
1846
+ switch (keycode) {
1847
+ case SDLK_UP:
1848
+ return "up";
1849
+ case SDLK_DOWN:
1850
+ return "down";
1851
+ case SDLK_LEFT:
1852
+ return "left";
1853
+ case SDLK_RIGHT:
1854
+ return "right";
1855
+ case SDLK_RETURN:
1856
+ return "return";
1857
+ case SDLK_ESCAPE:
1858
+ return "escape";
1859
+ case SDLK_BACKSPACE:
1860
+ return "backspace";
1861
+ case SDLK_TAB:
1862
+ return "tab";
1863
+ case SDLK_DELETE:
1864
+ return "delete";
1865
+ case SDLK_HOME:
1866
+ return "home";
1867
+ case SDLK_END:
1868
+ return "end";
1869
+ case SDLK_PAGEUP:
1870
+ return "pageup";
1871
+ case SDLK_PAGEDOWN:
1872
+ return "pagedown";
1873
+ default:
1874
+ if (keycode >= SDLK_F1 && keycode <= SDLK_F12) {
1875
+ return `f${keycode - SDLK_F1 + 1}`;
1876
+ }
1877
+ return "unknown";
1878
+ }
1879
+ }
1880
+ /**
1881
+ * Reset modifier state
1882
+ */
1883
+ reset() {
1884
+ this.modifiers = {
1885
+ shift: false,
1886
+ ctrl: false,
1887
+ alt: false
1888
+ };
1889
+ }
1890
+ /**
1891
+ * Get current modifier state
1892
+ */
1893
+ getModifiers() {
1894
+ return { ...this.modifiers };
1895
+ }
1896
+ };
1897
+
1898
+ // src/SdlUiRenderer/consts.ts
1899
+ var DEFAULT_WINDOW_WIDTH = 800;
1900
+ var DEFAULT_WINDOW_HEIGHT = 600;
1901
+ var DEFAULT_COLUMNS = 80;
1902
+ var DEFAULT_ROWS = 24;
1903
+ var MIN_COLUMNS = 40;
1904
+ var MIN_ROWS = 10;
1905
+ var BOLD_BRIGHTNESS_MULTIPLIER = 1.3;
1906
+ var DIM_BRIGHTNESS_MULTIPLIER = 0.5;
1907
+ var UNDERLINE_POSITION = 0.9;
1908
+ var STRIKETHROUGH_POSITION = 0.5;
1909
+ var TEXT_DECORATION_THICKNESS = 0.08;
1910
+
1911
+ // src/SdlUiRenderer/index.ts
1912
+ var DEFAULT_BG2 = { r: 0, g: 0, b: 0 };
1913
+ var DEFAULT_FG2 = { r: 255, g: 255, b: 255 };
1914
+ var MIN_BRIGHTNESS = 100;
1915
+ var HEX_COLOR_LENGTH = 6;
1916
+ var HEX_R_END = 2;
1917
+ var HEX_G_END = 4;
1918
+ var parseBackgroundColor = (color) => {
1919
+ if (!color) {
1920
+ return { ...DEFAULT_BG2 };
1921
+ }
1922
+ if (Array.isArray(color)) {
1923
+ return { r: color[0], g: color[1], b: color[2] };
1924
+ }
1925
+ const hex = color.startsWith("#") ? color.slice(1) : color;
1926
+ if (hex.length === HEX_COLOR_LENGTH) {
1927
+ const r = parseInt(hex.slice(0, HEX_R_END), 16);
1928
+ const g = parseInt(hex.slice(HEX_R_END, HEX_G_END), 16);
1929
+ const b = parseInt(hex.slice(HEX_G_END), 16);
1930
+ if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
1931
+ return { r, g, b };
1932
+ }
1933
+ }
1934
+ return { ...DEFAULT_BG2 };
1935
+ };
1936
+ var SdlUiRenderer = class {
1937
+ sdl = getSdl2();
1938
+ window = null;
1939
+ renderer = null;
1940
+ textRenderer = null;
1941
+ renderTarget = null;
1942
+ ansiParser;
1943
+ inputBridge;
1944
+ windowWidth;
1945
+ windowHeight;
1946
+ columns;
1947
+ rows;
1948
+ charWidth = 0;
1949
+ charHeight = 0;
1950
+ fgColor = { ...DEFAULT_FG2 };
1951
+ bgColor = { ...DEFAULT_BG2 };
1952
+ defaultBgColor = { ...DEFAULT_BG2 };
1953
+ bold = false;
1954
+ dim = false;
1955
+ italic = false;
1956
+ underline = false;
1957
+ strikethrough = false;
1958
+ reverse = false;
1959
+ shouldQuit = false;
1960
+ pendingCommands = [];
1961
+ scaleFactor = 1;
1962
+ userScaleFactor = null;
1963
+ constructor(options = {}) {
1964
+ this.windowWidth = options.width ?? DEFAULT_WINDOW_WIDTH;
1965
+ this.windowHeight = options.height ?? DEFAULT_WINDOW_HEIGHT;
1966
+ this.columns = DEFAULT_COLUMNS;
1967
+ this.rows = DEFAULT_ROWS;
1968
+ this.ansiParser = new AnsiParser();
1969
+ this.inputBridge = new InputBridge();
1970
+ this.initSDL(options);
1971
+ }
1972
+ /**
1973
+ * Initialize SDL window and renderer
1974
+ */
1975
+ initSDL(options) {
1976
+ if (!this.sdl.init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
1977
+ throw new Error("Failed to initialize SDL2 for UI rendering");
1978
+ }
1979
+ this.defaultBgColor = parseBackgroundColor(options.backgroundColor);
1980
+ this.bgColor = { ...this.defaultBgColor };
1981
+ let windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI;
1982
+ if (!options.fullscreen) {
1983
+ windowFlags |= SDL_WINDOW_RESIZABLE;
1984
+ }
1985
+ if (options.fullscreen === "desktop") {
1986
+ windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
1987
+ } else if (options.fullscreen === true) {
1988
+ windowFlags |= SDL_WINDOW_FULLSCREEN;
1989
+ }
1990
+ if (options.borderless && !options.fullscreen) {
1991
+ windowFlags |= SDL_WINDOW_BORDERLESS;
1992
+ }
1993
+ this.window = this.sdl.createWindow(
1994
+ options.title ?? "ink-sdl",
1995
+ SDL_WINDOWPOS_CENTERED,
1996
+ SDL_WINDOWPOS_CENTERED,
1997
+ this.windowWidth,
1998
+ this.windowHeight,
1999
+ windowFlags
2000
+ );
2001
+ if (options.minWidth !== void 0 || options.minHeight !== void 0) {
2002
+ const minW = options.minWidth ?? 1;
2003
+ const minH = options.minHeight ?? 1;
2004
+ this.sdl.setWindowMinimumSize(this.window, minW, minH);
2005
+ }
2006
+ const rendererFlags = options.vsync !== false ? SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC : SDL_RENDERER_ACCELERATED;
2007
+ this.renderer = this.sdl.createRenderer(this.window, -1, rendererFlags);
2008
+ this.userScaleFactor = options.scaleFactor === void 0 ? null : options.scaleFactor;
2009
+ if (this.userScaleFactor !== null) {
2010
+ this.scaleFactor = this.userScaleFactor;
2011
+ } else {
2012
+ this.scaleFactor = this.sdl.getScaleFactorFromRenderer(
2013
+ this.window,
2014
+ this.renderer
2015
+ );
2016
+ }
2017
+ this.textRenderer = new TextRenderer(this.renderer, {
2018
+ fontSize: options.fontSize ?? DEFAULT_FONT_SIZE,
2019
+ scaleFactor: this.scaleFactor,
2020
+ ...options.systemFont && { systemFont: true },
2021
+ ...options.fontPath && { fontPath: options.fontPath },
2022
+ ...options.fontName && { fontName: options.fontName }
2023
+ });
2024
+ const charDims = this.textRenderer.getCharDimensions();
2025
+ this.charWidth = charDims.width;
2026
+ this.charHeight = charDims.height;
2027
+ this.updateTerminalDimensions();
2028
+ this.createRenderTarget();
2029
+ this.sdl.setRenderDrawColor(
2030
+ this.renderer,
2031
+ this.defaultBgColor.r,
2032
+ this.defaultBgColor.g,
2033
+ this.defaultBgColor.b,
2034
+ COLOR_CHANNEL_MAX
2035
+ );
2036
+ this.sdl.setRenderTarget(this.renderer, this.renderTarget);
2037
+ this.sdl.renderClear(this.renderer);
2038
+ this.sdl.setRenderTarget(this.renderer, null);
2039
+ this.sdl.renderClear(this.renderer);
2040
+ this.sdl.renderPresent(this.renderer);
2041
+ this.sdl.raiseWindow(this.window);
2042
+ }
2043
+ /**
2044
+ * Update terminal dimensions based on window size
2045
+ */
2046
+ updateTerminalDimensions() {
2047
+ if (!this.window || this.charWidth === 0 || this.charHeight === 0) {
2048
+ return;
2049
+ }
2050
+ const drawable = this.sdl.getDrawableSize(this.window);
2051
+ this.columns = Math.floor(drawable.width / this.charWidth);
2052
+ this.rows = Math.floor(drawable.height / this.charHeight);
2053
+ this.columns = Math.max(this.columns, MIN_COLUMNS);
2054
+ this.rows = Math.max(this.rows, MIN_ROWS);
2055
+ }
2056
+ /**
2057
+ * Get terminal dimensions
2058
+ */
2059
+ getDimensions() {
2060
+ return {
2061
+ columns: this.columns,
2062
+ rows: this.rows
2063
+ };
2064
+ }
2065
+ /**
2066
+ * Create or recreate the render target texture
2067
+ */
2068
+ createRenderTarget() {
2069
+ if (!this.renderer) {
2070
+ return;
2071
+ }
2072
+ if (this.renderTarget) {
2073
+ this.sdl.destroyTexture(this.renderTarget);
2074
+ this.renderTarget = null;
2075
+ }
2076
+ const drawable = this.sdl.getDrawableSize(this.window);
2077
+ this.renderTarget = this.sdl.createTexture(
2078
+ this.renderer,
2079
+ SDL_PIXELFORMAT_ARGB8888,
2080
+ SDL_TEXTUREACCESS_TARGET,
2081
+ drawable.width,
2082
+ drawable.height
2083
+ );
2084
+ this.sdl.setTextureBlendMode(this.renderTarget, SDL_BLENDMODE_BLEND);
2085
+ }
2086
+ /**
2087
+ * Process ANSI output from Ink
2088
+ */
2089
+ processAnsi(output) {
2090
+ const commands = this.ansiParser.parse(output);
2091
+ this.pendingCommands.push(...commands);
2092
+ }
2093
+ /**
2094
+ * Render pending commands and present
2095
+ */
2096
+ present() {
2097
+ if (!this.renderer || !this.textRenderer || !this.renderTarget) {
2098
+ return;
2099
+ }
2100
+ this.sdl.setRenderTarget(this.renderer, this.renderTarget);
2101
+ for (const cmd of this.pendingCommands) {
2102
+ this.executeCommand(cmd);
2103
+ }
2104
+ this.pendingCommands = [];
2105
+ this.sdl.setRenderTarget(this.renderer, null);
2106
+ this.sdl.renderCopy(this.renderer, this.renderTarget, null, null);
2107
+ this.sdl.renderPresent(this.renderer);
2108
+ }
2109
+ /**
2110
+ * Refresh the display by copying the render target to the screen
2111
+ *
2112
+ * Call this periodically to keep the display updated even when no new
2113
+ * content is being rendered. Required for SDL's double-buffering to work
2114
+ * correctly - without continuous presents, the window can go black.
2115
+ */
2116
+ refreshDisplay() {
2117
+ if (!this.renderer || !this.renderTarget) {
2118
+ return;
2119
+ }
2120
+ this.sdl.setRenderTarget(this.renderer, null);
2121
+ this.sdl.renderCopy(this.renderer, this.renderTarget, null, null);
2122
+ this.sdl.renderPresent(this.renderer);
2123
+ }
2124
+ /**
2125
+ * Execute a single draw command
2126
+ */
2127
+ executeCommand(cmd) {
2128
+ switch (cmd.type) {
2129
+ case "text":
2130
+ this.renderText(cmd);
2131
+ break;
2132
+ case "clear_screen":
2133
+ this.sdl.setRenderDrawColor(
2134
+ this.renderer,
2135
+ this.defaultBgColor.r,
2136
+ this.defaultBgColor.g,
2137
+ this.defaultBgColor.b,
2138
+ COLOR_CHANNEL_MAX
2139
+ );
2140
+ this.sdl.renderClear(this.renderer);
2141
+ this.ansiParser.reset();
2142
+ break;
2143
+ case "clear_line":
2144
+ this.clearLine(cmd.row ?? 1, cmd.col ?? 1);
2145
+ break;
2146
+ case "cursor_move":
2147
+ break;
2148
+ case "set_fg":
2149
+ if (cmd.color) {
2150
+ this.fgColor = cmd.color;
2151
+ }
2152
+ break;
2153
+ case "set_bg":
2154
+ if (cmd.color) {
2155
+ this.bgColor = cmd.color;
2156
+ }
2157
+ break;
2158
+ case "reset_style":
2159
+ this.fgColor = { ...DEFAULT_FG2 };
2160
+ this.bgColor = { ...this.defaultBgColor };
2161
+ this.bold = false;
2162
+ this.dim = false;
2163
+ this.italic = false;
2164
+ this.underline = false;
2165
+ this.strikethrough = false;
2166
+ this.reverse = false;
2167
+ break;
2168
+ case "set_bold":
2169
+ this.bold = cmd.enabled ?? false;
2170
+ break;
2171
+ case "set_dim":
2172
+ this.dim = cmd.enabled ?? false;
2173
+ break;
2174
+ case "set_italic":
2175
+ this.italic = cmd.enabled ?? false;
2176
+ break;
2177
+ case "set_underline":
2178
+ this.underline = cmd.enabled ?? false;
2179
+ break;
2180
+ case "set_strikethrough":
2181
+ this.strikethrough = cmd.enabled ?? false;
2182
+ break;
2183
+ case "set_reverse":
2184
+ this.reverse = cmd.enabled ?? false;
2185
+ break;
2186
+ }
2187
+ }
2188
+ /**
2189
+ * Render text at position
2190
+ */
2191
+ renderText(cmd) {
2192
+ if (!cmd.text || !this.renderer || !this.textRenderer) {
2193
+ return;
2194
+ }
2195
+ const text = cmd.text;
2196
+ const row = cmd.row ?? 1;
2197
+ const col = cmd.col ?? 1;
2198
+ const x = (col - 1) * this.charWidth;
2199
+ const y = (row - 1) * this.charHeight;
2200
+ let fg = this.reverse ? this.bgColor : this.fgColor;
2201
+ const bg = this.reverse ? this.fgColor : this.bgColor;
2202
+ if (this.bold) {
2203
+ fg = {
2204
+ r: Math.min(
2205
+ COLOR_CHANNEL_MAX,
2206
+ Math.floor(fg.r * BOLD_BRIGHTNESS_MULTIPLIER)
2207
+ ),
2208
+ g: Math.min(
2209
+ COLOR_CHANNEL_MAX,
2210
+ Math.floor(fg.g * BOLD_BRIGHTNESS_MULTIPLIER)
2211
+ ),
2212
+ b: Math.min(
2213
+ COLOR_CHANNEL_MAX,
2214
+ Math.floor(fg.b * BOLD_BRIGHTNESS_MULTIPLIER)
2215
+ )
2216
+ };
2217
+ }
2218
+ if (this.dim) {
2219
+ fg = {
2220
+ r: Math.floor(fg.r * DIM_BRIGHTNESS_MULTIPLIER),
2221
+ g: Math.floor(fg.g * DIM_BRIGHTNESS_MULTIPLIER),
2222
+ b: Math.floor(fg.b * DIM_BRIGHTNESS_MULTIPLIER)
2223
+ };
2224
+ }
2225
+ const brightness = Math.max(fg.r, fg.g, fg.b);
2226
+ if (brightness < MIN_BRIGHTNESS) {
2227
+ if (brightness === 0) {
2228
+ fg = { r: MIN_BRIGHTNESS, g: MIN_BRIGHTNESS, b: MIN_BRIGHTNESS };
2229
+ } else {
2230
+ const scale = MIN_BRIGHTNESS / brightness;
2231
+ fg = {
2232
+ r: Math.min(COLOR_CHANNEL_MAX, Math.floor(fg.r * scale)),
2233
+ g: Math.min(COLOR_CHANNEL_MAX, Math.floor(fg.g * scale)),
2234
+ b: Math.min(COLOR_CHANNEL_MAX, Math.floor(fg.b * scale))
2235
+ };
2236
+ }
2237
+ }
2238
+ const textWidth = text.length * this.charWidth;
2239
+ const bgRect = createSDLRect(x, y, textWidth, this.charHeight);
2240
+ this.sdl.setRenderDrawColor(
2241
+ this.renderer,
2242
+ bg.r,
2243
+ bg.g,
2244
+ bg.b,
2245
+ COLOR_CHANNEL_MAX
2246
+ );
2247
+ this.sdl.renderFillRect(this.renderer, bgRect);
2248
+ this.textRenderer.renderText(text, x, y, fg, this.italic);
2249
+ if (this.underline || this.strikethrough) {
2250
+ const lineThickness = Math.max(
2251
+ 1,
2252
+ Math.round(this.charHeight * TEXT_DECORATION_THICKNESS)
2253
+ );
2254
+ this.sdl.setRenderDrawColor(
2255
+ this.renderer,
2256
+ fg.r,
2257
+ fg.g,
2258
+ fg.b,
2259
+ COLOR_CHANNEL_MAX
2260
+ );
2261
+ if (this.underline) {
2262
+ const underlineY = y + Math.round(this.charHeight * UNDERLINE_POSITION);
2263
+ const underlineRect = createSDLRect(
2264
+ x,
2265
+ underlineY,
2266
+ textWidth,
2267
+ lineThickness
2268
+ );
2269
+ this.sdl.renderFillRect(this.renderer, underlineRect);
2270
+ }
2271
+ if (this.strikethrough) {
2272
+ const strikeY = y + Math.round(this.charHeight * STRIKETHROUGH_POSITION);
2273
+ const strikeRect = createSDLRect(x, strikeY, textWidth, lineThickness);
2274
+ this.sdl.renderFillRect(this.renderer, strikeRect);
2275
+ }
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Clear the entire screen
2280
+ */
2281
+ clear() {
2282
+ if (!this.renderer || !this.renderTarget) {
2283
+ return;
2284
+ }
2285
+ this.sdl.setRenderTarget(this.renderer, this.renderTarget);
2286
+ this.sdl.setRenderDrawColor(
2287
+ this.renderer,
2288
+ this.defaultBgColor.r,
2289
+ this.defaultBgColor.g,
2290
+ this.defaultBgColor.b,
2291
+ COLOR_CHANNEL_MAX
2292
+ );
2293
+ this.sdl.renderClear(this.renderer);
2294
+ this.sdl.setRenderTarget(this.renderer, null);
2295
+ this.ansiParser.reset();
2296
+ }
2297
+ /**
2298
+ * Clear a line from a specific position
2299
+ */
2300
+ clearLine(row, fromCol) {
2301
+ if (!this.renderer) {
2302
+ return;
2303
+ }
2304
+ const x = (fromCol - 1) * this.charWidth;
2305
+ const y = (row - 1) * this.charHeight;
2306
+ const drawable = this.sdl.getDrawableSize(this.window);
2307
+ const clearWidth = drawable.width - x;
2308
+ const rect = createSDLRect(x, y, clearWidth, this.charHeight);
2309
+ this.sdl.setRenderDrawColor(
2310
+ this.renderer,
2311
+ this.bgColor.r,
2312
+ this.bgColor.g,
2313
+ this.bgColor.b,
2314
+ COLOR_CHANNEL_MAX
2315
+ );
2316
+ this.sdl.renderFillRect(this.renderer, rect);
2317
+ }
2318
+ /**
2319
+ * Process SDL events
2320
+ */
2321
+ processEvents() {
2322
+ const keyEvents = [];
2323
+ let resized = false;
2324
+ let focusLost = false;
2325
+ let event = this.sdl.pollEvent();
2326
+ while (event) {
2327
+ if (event.type === SDL_QUIT) {
2328
+ this.shouldQuit = true;
2329
+ } else if (event.type === SDL_WINDOWEVENT) {
2330
+ if (event.windowEvent === SDL_WINDOWEVENT_CLOSE) {
2331
+ this.shouldQuit = true;
2332
+ } else if (event.windowEvent === SDL_WINDOWEVENT_SIZE_CHANGED) {
2333
+ this.handleResize();
2334
+ resized = true;
2335
+ } else if (event.windowEvent === SDL_WINDOWEVENT_FOCUS_LOST) {
2336
+ focusLost = true;
2337
+ }
2338
+ } else if (event.type === SDL_KEYDOWN || event.type === SDL_KEYUP) {
2339
+ if (event.keycode !== void 0 && event.pressed !== void 0) {
2340
+ keyEvents.push({
2341
+ keycode: event.keycode,
2342
+ pressed: event.pressed,
2343
+ repeat: event.repeat ?? false
2344
+ });
2345
+ }
2346
+ }
2347
+ event = this.sdl.pollEvent();
2348
+ }
2349
+ return { keyEvents, resized, focusLost };
2350
+ }
2351
+ /**
2352
+ * Convert SDL key event to terminal sequence
2353
+ */
2354
+ keyEventToSequence(event) {
2355
+ return this.inputBridge.processKeyEvent(event);
2356
+ }
2357
+ /**
2358
+ * Handle window resize
2359
+ */
2360
+ handleResize() {
2361
+ if (!this.window) {
2362
+ return;
2363
+ }
2364
+ const size = this.sdl.getWindowSize(this.window);
2365
+ this.windowWidth = size.width;
2366
+ this.windowHeight = size.height;
2367
+ if (this.userScaleFactor === null) {
2368
+ const newScale = this.sdl.getScaleFactor(this.window);
2369
+ if (Math.abs(newScale - this.scaleFactor) > SCALE_FACTOR_EPSILON) {
2370
+ this.scaleFactor = newScale;
2371
+ this.textRenderer?.updateScaleFactor(newScale);
2372
+ if (this.textRenderer) {
2373
+ const charDims = this.textRenderer.getCharDimensions();
2374
+ this.charWidth = charDims.width;
2375
+ this.charHeight = charDims.height;
2376
+ }
2377
+ }
2378
+ }
2379
+ this.createRenderTarget();
2380
+ this.updateTerminalDimensions();
2381
+ this.clear();
2382
+ }
2383
+ /**
2384
+ * Check if quit was requested
2385
+ */
2386
+ shouldClose() {
2387
+ return this.shouldQuit;
2388
+ }
2389
+ /**
2390
+ * Get cursor position
2391
+ */
2392
+ getCursorPos() {
2393
+ const cursor = this.ansiParser.getCursor();
2394
+ return { x: cursor.col, y: cursor.row };
2395
+ }
2396
+ /**
2397
+ * Get the SDL window
2398
+ */
2399
+ getWindow() {
2400
+ return this.window;
2401
+ }
2402
+ /**
2403
+ * Get the SDL renderer
2404
+ */
2405
+ getRenderer() {
2406
+ return this.renderer;
2407
+ }
2408
+ /**
2409
+ * Set the scale factor
2410
+ */
2411
+ setScaleFactor(scaleFactor) {
2412
+ this.userScaleFactor = scaleFactor;
2413
+ let newScale;
2414
+ if (scaleFactor !== null) {
2415
+ newScale = scaleFactor;
2416
+ } else if (this.window && this.renderer) {
2417
+ newScale = this.sdl.getScaleFactorFromRenderer(
2418
+ this.window,
2419
+ this.renderer
2420
+ );
2421
+ } else {
2422
+ newScale = 1;
2423
+ }
2424
+ if (Math.abs(newScale - this.scaleFactor) < SCALE_FACTOR_EPSILON) {
2425
+ return;
2426
+ }
2427
+ this.scaleFactor = newScale;
2428
+ if (this.textRenderer) {
2429
+ this.textRenderer.updateScaleFactor(newScale);
2430
+ const charDims = this.textRenderer.getCharDimensions();
2431
+ this.charWidth = charDims.width;
2432
+ this.charHeight = charDims.height;
2433
+ }
2434
+ this.createRenderTarget();
2435
+ this.updateTerminalDimensions();
2436
+ this.clear();
2437
+ }
2438
+ /**
2439
+ * Get current scale factor
2440
+ */
2441
+ getScaleFactor() {
2442
+ return this.scaleFactor;
2443
+ }
2444
+ /**
2445
+ * Clean up resources
2446
+ */
2447
+ destroy() {
2448
+ if (this.textRenderer) {
2449
+ this.textRenderer.destroy();
2450
+ this.textRenderer = null;
2451
+ }
2452
+ if (this.renderTarget) {
2453
+ this.sdl.destroyTexture(this.renderTarget);
2454
+ this.renderTarget = null;
2455
+ }
2456
+ if (this.renderer) {
2457
+ this.sdl.destroyRenderer(this.renderer);
2458
+ this.renderer = null;
2459
+ }
2460
+ if (this.window) {
2461
+ this.sdl.destroyWindow(this.window);
2462
+ this.window = null;
2463
+ }
2464
+ }
2465
+ /**
2466
+ * Reset state for reuse
2467
+ */
2468
+ reset() {
2469
+ this.shouldQuit = false;
2470
+ this.pendingCommands = [];
2471
+ this.fgColor = { ...DEFAULT_FG2 };
2472
+ this.bgColor = { ...this.defaultBgColor };
2473
+ this.bold = false;
2474
+ this.dim = false;
2475
+ this.italic = false;
2476
+ this.underline = false;
2477
+ this.strikethrough = false;
2478
+ this.reverse = false;
2479
+ this.ansiParser.reset();
2480
+ if (this.window) {
2481
+ const size = this.sdl.getWindowSize(this.window);
2482
+ this.windowWidth = size.width;
2483
+ this.windowHeight = size.height;
2484
+ this.updateTerminalDimensions();
2485
+ }
2486
+ }
2487
+ /**
2488
+ * Reset input state (modifier keys)
2489
+ *
2490
+ * Call this when focus is lost to prevent "stuck" modifier keys.
2491
+ */
2492
+ resetInputState() {
2493
+ this.inputBridge.reset();
2494
+ }
2495
+ /**
2496
+ * Get glyph cache statistics
2497
+ *
2498
+ * Useful for profiling and tuning cache size.
2499
+ */
2500
+ getCacheStats() {
2501
+ return this.textRenderer?.getCacheStats() ?? null;
2502
+ }
2503
+ };
2504
+
2505
+ // src/SdlOutputStream/index.ts
2506
+ import { Writable } from "stream";
2507
+ import { isString } from "remeda";
2508
+ var SdlOutputStream = class extends Writable {
2509
+ /** TTY interface property expected by Ink */
2510
+ isTTY = true;
2511
+ uiRenderer;
2512
+ constructor(uiRenderer) {
2513
+ super({
2514
+ decodeStrings: false
2515
+ });
2516
+ this.uiRenderer = uiRenderer;
2517
+ }
2518
+ /**
2519
+ * Get terminal columns
2520
+ */
2521
+ get columns() {
2522
+ const dims = this.uiRenderer.getDimensions();
2523
+ return dims.columns || DEFAULT_COLUMNS;
2524
+ }
2525
+ /**
2526
+ * Get terminal rows
2527
+ */
2528
+ get rows() {
2529
+ const dims = this.uiRenderer.getDimensions();
2530
+ return dims.rows || DEFAULT_ROWS;
2531
+ }
2532
+ /**
2533
+ * Notify Ink of resize
2534
+ */
2535
+ notifyResize() {
2536
+ this.emit("resize");
2537
+ }
2538
+ /**
2539
+ * Implement Writable._write
2540
+ */
2541
+ _write(chunk, _encoding, callback) {
2542
+ try {
2543
+ const text = isString(chunk) ? chunk : chunk.toString("utf8");
2544
+ this.uiRenderer.processAnsi(text);
2545
+ this.uiRenderer.present();
2546
+ callback(null);
2547
+ } catch (error) {
2548
+ callback(error instanceof Error ? error : new Error(String(error)));
2549
+ }
2550
+ }
2551
+ /**
2552
+ * Get the underlying renderer
2553
+ */
2554
+ getRenderer() {
2555
+ return this.uiRenderer;
2556
+ }
2557
+ /**
2558
+ * Clear the screen
2559
+ */
2560
+ clear() {
2561
+ this.uiRenderer.clear();
2562
+ }
2563
+ /**
2564
+ * Write a string directly (bypasses Writable buffering)
2565
+ */
2566
+ writeSync(text) {
2567
+ this.uiRenderer.processAnsi(text);
2568
+ this.uiRenderer.present();
2569
+ }
2570
+ /**
2571
+ * Get cursor position
2572
+ */
2573
+ getCursorPos() {
2574
+ return this.uiRenderer.getCursorPos();
2575
+ }
2576
+ };
2577
+
2578
+ // src/SdlInputStream/index.ts
2579
+ import { Readable } from "stream";
2580
+ var SdlInputStream = class extends Readable {
2581
+ /** TTY interface properties expected by Ink */
2582
+ isTTY = true;
2583
+ isRaw = true;
2584
+ buffer = [];
2585
+ waiting = false;
2586
+ constructor() {
2587
+ super({
2588
+ encoding: "utf8"
2589
+ });
2590
+ }
2591
+ /**
2592
+ * Push a key sequence into the stream
2593
+ */
2594
+ pushKey(sequence) {
2595
+ this.buffer.push(sequence);
2596
+ if (this.waiting) {
2597
+ this.waiting = false;
2598
+ this._read();
2599
+ }
2600
+ }
2601
+ /**
2602
+ * Implement Readable._read
2603
+ */
2604
+ _read() {
2605
+ if (this.buffer.length > 0) {
2606
+ while (this.buffer.length > 0) {
2607
+ const sequence = this.buffer.shift();
2608
+ if (sequence !== void 0) {
2609
+ this.push(sequence);
2610
+ }
2611
+ }
2612
+ } else {
2613
+ this.waiting = true;
2614
+ }
2615
+ }
2616
+ /**
2617
+ * Set raw mode (no-op for SDL, we're always raw)
2618
+ */
2619
+ setRawMode(_mode) {
2620
+ return this;
2621
+ }
2622
+ /**
2623
+ * Check if stream has buffered data
2624
+ */
2625
+ hasData() {
2626
+ return this.buffer.length > 0;
2627
+ }
2628
+ /**
2629
+ * Clear the input buffer
2630
+ */
2631
+ clear() {
2632
+ this.buffer = [];
2633
+ }
2634
+ /**
2635
+ * Close the stream
2636
+ */
2637
+ close() {
2638
+ this.push(null);
2639
+ }
2640
+ /**
2641
+ * Keep the event loop alive (no-op for SDL)
2642
+ */
2643
+ ref() {
2644
+ return this;
2645
+ }
2646
+ /**
2647
+ * Allow event loop to exit (no-op for SDL)
2648
+ */
2649
+ unref() {
2650
+ return this;
2651
+ }
2652
+ };
2653
+
2654
+ // src/SdlWindow/consts.ts
2655
+ var EVENT_LOOP_INTERVAL_MS = 16;
2656
+
2657
+ // src/SdlWindow/index.ts
2658
+ var SdlWindow = class extends EventEmitter {
2659
+ renderer;
2660
+ eventLoopHandle = null;
2661
+ inputStream;
2662
+ outputStream;
2663
+ closed = false;
2664
+ constructor(renderer, inputStream, outputStream) {
2665
+ super();
2666
+ this.renderer = renderer;
2667
+ this.inputStream = inputStream;
2668
+ this.outputStream = outputStream;
2669
+ this.startEventLoop();
2670
+ }
2671
+ /**
2672
+ * Start the SDL event loop
2673
+ */
2674
+ startEventLoop() {
2675
+ this.eventLoopHandle = setInterval(() => {
2676
+ if (this.closed) {
2677
+ return;
2678
+ }
2679
+ const { keyEvents, resized, focusLost } = this.renderer.processEvents();
2680
+ if (resized) {
2681
+ this.outputStream.notifyResize();
2682
+ this.emit("resize", this.renderer.getDimensions());
2683
+ }
2684
+ if (focusLost) {
2685
+ this.renderer.resetInputState();
2686
+ this.emit("blur");
2687
+ }
2688
+ for (const event of keyEvents) {
2689
+ const sequence = this.renderer.keyEventToSequence(event);
2690
+ if (sequence) {
2691
+ if (sequence === "") {
2692
+ if (this.listenerCount("sigint") > 0) {
2693
+ this.emit("sigint");
2694
+ } else {
2695
+ process.kill(process.pid, "SIGINT");
2696
+ }
2697
+ continue;
2698
+ }
2699
+ this.inputStream.pushKey(sequence);
2700
+ this.emit("key", event);
2701
+ }
2702
+ }
2703
+ this.renderer.refreshDisplay();
2704
+ if (this.renderer.shouldClose()) {
2705
+ this.emit("close");
2706
+ this.close();
2707
+ }
2708
+ }, EVENT_LOOP_INTERVAL_MS);
2709
+ }
2710
+ /**
2711
+ * Get terminal dimensions
2712
+ */
2713
+ getDimensions() {
2714
+ return this.renderer.getDimensions();
2715
+ }
2716
+ /**
2717
+ * Set window title
2718
+ */
2719
+ setTitle(title) {
2720
+ const window = this.renderer.getWindow();
2721
+ if (window) {
2722
+ getSdl2().setWindowTitle(window, title);
2723
+ }
2724
+ }
2725
+ /**
2726
+ * Clear the screen
2727
+ */
2728
+ clear() {
2729
+ this.renderer.clear();
2730
+ this.renderer.present();
2731
+ }
2732
+ /**
2733
+ * Close the window
2734
+ */
2735
+ close() {
2736
+ if (this.closed) {
2737
+ return;
2738
+ }
2739
+ this.closed = true;
2740
+ if (this.eventLoopHandle) {
2741
+ clearInterval(this.eventLoopHandle);
2742
+ this.eventLoopHandle = null;
2743
+ }
2744
+ this.inputStream.close();
2745
+ this.renderer.destroy();
2746
+ }
2747
+ /**
2748
+ * Check if window is closed
2749
+ */
2750
+ isClosed() {
2751
+ return this.closed;
2752
+ }
2753
+ /**
2754
+ * Get the output stream
2755
+ */
2756
+ getOutputStream() {
2757
+ return this.outputStream;
2758
+ }
2759
+ /**
2760
+ * Get glyph cache statistics
2761
+ *
2762
+ * Useful for profiling and tuning cache performance.
2763
+ *
2764
+ * @example
2765
+ * ```typescript
2766
+ * const stats = window.getCacheStats();
2767
+ * if (stats) {
2768
+ * console.log(`Cache: ${stats.size}/${stats.maxSize} glyphs`);
2769
+ * }
2770
+ * ```
2771
+ */
2772
+ getCacheStats() {
2773
+ return this.renderer.getCacheStats();
2774
+ }
2775
+ };
2776
+ var createSdlStreams = (options = {}) => {
2777
+ if (process.env["FORCE_COLOR"] === void 0) {
2778
+ process.env["FORCE_COLOR"] = "3";
2779
+ }
2780
+ const rendererOptions = pickBy(options, isDefined);
2781
+ const renderer = new SdlUiRenderer(rendererOptions);
2782
+ const inputStream = new SdlInputStream();
2783
+ const outputStream = new SdlOutputStream(renderer);
2784
+ const window = new SdlWindow(renderer, inputStream, outputStream);
2785
+ return {
2786
+ stdin: inputStream,
2787
+ stdout: outputStream,
2788
+ window,
2789
+ renderer
2790
+ };
2791
+ };
2792
+
2793
+ // src/index.ts
2794
+ if (process.env["FORCE_COLOR"] === void 0) {
2795
+ process.env["FORCE_COLOR"] = "3";
2796
+ }
2797
+ var isSdlAvailable = () => {
2798
+ try {
2799
+ return isSdl2Available() && isSdlTtfAvailable();
2800
+ } catch {
2801
+ return false;
2802
+ }
2803
+ };
2804
+
2805
+ export {
2806
+ Sdl2,
2807
+ getSdl2,
2808
+ isSdl2Available,
2809
+ createSDLRect,
2810
+ AnsiParser,
2811
+ SdlTtf,
2812
+ getSdlTtf,
2813
+ isSdlTtfAvailable,
2814
+ TextRenderer,
2815
+ InputBridge,
2816
+ SdlUiRenderer,
2817
+ SdlOutputStream,
2818
+ SdlInputStream,
2819
+ SdlWindow,
2820
+ createSdlStreams,
2821
+ isSdlAvailable
2822
+ };