ink-sdl 0.2.1 → 0.3.1
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/README.md +55 -30
- package/dist/chunk-BOQYTA3S.js +2822 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +437 -0
- package/dist/index.d.ts +412 -245
- package/dist/index.js +25 -2269
- package/package.json +6 -2
|
@@ -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
|
+
};
|