ink-sdl 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/{chunk-BOQYTA3S.js → chunk-LP5C65TH.js} +151 -203
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,6 +168,7 @@ Creates stdin/stdout streams and a window for use with Ink.
|
|
|
168
168
|
| `borderless` | `boolean` | `false` | Remove window decorations (title bar, borders) |
|
|
169
169
|
| `minWidth` | `number` | `undefined` | Minimum window width in pixels |
|
|
170
170
|
| `minHeight` | `number` | `undefined` | Minimum window height in pixels |
|
|
171
|
+
| `existing` | `ExistingSdlResources` | `undefined` | Use existing SDL window/renderer (see Advanced Usage) |
|
|
171
172
|
|
|
172
173
|
#### Returns
|
|
173
174
|
|
|
@@ -213,6 +214,49 @@ if (isSdlAvailable()) {
|
|
|
213
214
|
|
|
214
215
|
## Advanced Usage
|
|
215
216
|
|
|
217
|
+
### Using Existing SDL Window/Renderer
|
|
218
|
+
|
|
219
|
+
For applications that need to share a single SDL window between ink-sdl and custom rendering (e.g., an emulator with a menu UI), you can pass existing SDL resources:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { render, Text, Box } from "ink";
|
|
223
|
+
import { createSdlStreams, getSdl2, type ExistingSdlResources } from "ink-sdl";
|
|
224
|
+
|
|
225
|
+
// Create your own SDL window and renderer
|
|
226
|
+
const sdl = getSdl2();
|
|
227
|
+
sdl.init(0x20 | 0x4000); // SDL_INIT_VIDEO | SDL_INIT_EVENTS
|
|
228
|
+
|
|
229
|
+
const myWindow = sdl.createWindow("My App", 100, 100, 800, 600, 0x4);
|
|
230
|
+
const myRenderer = sdl.createRenderer(myWindow, -1, 0x2);
|
|
231
|
+
|
|
232
|
+
// Use them with ink-sdl
|
|
233
|
+
const streams = createSdlStreams({
|
|
234
|
+
existing: { window: myWindow, renderer: myRenderer },
|
|
235
|
+
fontSize: 16,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
render(<MenuApp />, { stdin: streams.stdin, stdout: streams.stdout });
|
|
239
|
+
|
|
240
|
+
// When done with ink-sdl UI, close() cleans up ink-sdl resources
|
|
241
|
+
// but does NOT destroy your window/renderer
|
|
242
|
+
streams.window.close();
|
|
243
|
+
|
|
244
|
+
// You can now render directly to the same window, or create new streams later
|
|
245
|
+
// When completely done, destroy the window/renderer yourself
|
|
246
|
+
sdl.destroyRenderer(myRenderer);
|
|
247
|
+
sdl.destroyWindow(myWindow);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Ownership rules:**
|
|
251
|
+
|
|
252
|
+
- When `existing` is provided, ink-sdl does NOT own the window/renderer
|
|
253
|
+
- `close()` will NOT destroy the provided window/renderer
|
|
254
|
+
- The caller retains ownership and must destroy them when fully done
|
|
255
|
+
- Window options (`width`, `height`, `title`, `fullscreen`, `borderless`) are ignored
|
|
256
|
+
- Rendering options (`fontSize`, `scaleFactor`, `fontPath`, etc.) still apply
|
|
257
|
+
|
|
258
|
+
### Low-Level Components
|
|
259
|
+
|
|
216
260
|
For more control, you can use the lower-level components directly:
|
|
217
261
|
|
|
218
262
|
```typescript
|
|
@@ -223,6 +267,8 @@ import {
|
|
|
223
267
|
InputBridge,
|
|
224
268
|
getSdl2,
|
|
225
269
|
getSdlTtf,
|
|
270
|
+
type ExistingSdlResources,
|
|
271
|
+
type SDLPointer,
|
|
226
272
|
} from "ink-sdl";
|
|
227
273
|
```
|
|
228
274
|
|
|
@@ -4,9 +4,18 @@ import { pickBy, isDefined } from "remeda";
|
|
|
4
4
|
|
|
5
5
|
// src/Sdl2/index.ts
|
|
6
6
|
import koffi from "koffi";
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
// src/utils/findLibrary/index.ts
|
|
8
9
|
import { existsSync } from "fs";
|
|
10
|
+
import { platform } from "os";
|
|
9
11
|
import { find, last } from "remeda";
|
|
12
|
+
var isSystemPath = (p) => !p.includes("/") && !p.includes("\\");
|
|
13
|
+
var findLibrary = (pathMap) => {
|
|
14
|
+
const plat = platform();
|
|
15
|
+
const paths = pathMap[plat] ?? [];
|
|
16
|
+
const foundPath = find(paths, (p) => isSystemPath(p) || existsSync(p));
|
|
17
|
+
return foundPath ?? last(paths) ?? null;
|
|
18
|
+
};
|
|
10
19
|
|
|
11
20
|
// src/Sdl2/consts.ts
|
|
12
21
|
var SDL_INIT_VIDEO = 32;
|
|
@@ -94,13 +103,6 @@ var SDL_LIB_PATHS = {
|
|
|
94
103
|
],
|
|
95
104
|
win32: ["SDL2.dll", "C:\\Windows\\System32\\SDL2.dll"]
|
|
96
105
|
};
|
|
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
106
|
var findSDLLibrary = () => {
|
|
105
107
|
return findLibrary(SDL_LIB_PATHS);
|
|
106
108
|
};
|
|
@@ -962,15 +964,12 @@ var AnsiParser = class {
|
|
|
962
964
|
// src/TextRenderer/index.ts
|
|
963
965
|
import { resolve, dirname, join } from "path";
|
|
964
966
|
import { fileURLToPath } from "url";
|
|
965
|
-
import { existsSync as
|
|
966
|
-
import { platform as
|
|
967
|
-
import { sortBy, take } from "remeda";
|
|
967
|
+
import { existsSync as existsSync2 } from "fs";
|
|
968
|
+
import { platform as platform2, homedir } from "os";
|
|
969
|
+
import { flatMap, sortBy, take } from "remeda";
|
|
968
970
|
|
|
969
971
|
// src/SdlTtf/index.ts
|
|
970
972
|
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
973
|
var SDL_TTF_LIB_PATHS = {
|
|
975
974
|
darwin: [
|
|
976
975
|
"/opt/homebrew/lib/libSDL2_ttf.dylib",
|
|
@@ -996,15 +995,8 @@ var SDL_TTF_LIB_PATHS = {
|
|
|
996
995
|
],
|
|
997
996
|
win32: ["SDL2_ttf.dll", "C:\\Windows\\System32\\SDL2_ttf.dll"]
|
|
998
997
|
};
|
|
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
998
|
var findSDLTtfLibrary = () => {
|
|
1007
|
-
return
|
|
999
|
+
return findLibrary(SDL_TTF_LIB_PATHS);
|
|
1008
1000
|
};
|
|
1009
1001
|
var SdlTtf = class {
|
|
1010
1002
|
lib;
|
|
@@ -1214,6 +1206,21 @@ var EMOJI_FONTS = {
|
|
|
1214
1206
|
var EMOJI_FONT_SCALE = 0.75;
|
|
1215
1207
|
|
|
1216
1208
|
// src/TextRenderer/index.ts
|
|
1209
|
+
var findFirstExisting = (paths) => {
|
|
1210
|
+
for (const p of paths) {
|
|
1211
|
+
try {
|
|
1212
|
+
if (existsSync2(p)) {
|
|
1213
|
+
return p;
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return null;
|
|
1219
|
+
};
|
|
1220
|
+
var getPlatformPaths = (pathMap) => {
|
|
1221
|
+
const plat = platform2();
|
|
1222
|
+
return [...pathMap[plat] ?? []];
|
|
1223
|
+
};
|
|
1217
1224
|
var TextRenderer = class {
|
|
1218
1225
|
sdl = getSdl2();
|
|
1219
1226
|
ttf = getSdlTtf();
|
|
@@ -1252,7 +1259,7 @@ var TextRenderer = class {
|
|
|
1252
1259
|
*/
|
|
1253
1260
|
getSystemFontDirectories() {
|
|
1254
1261
|
const home = homedir();
|
|
1255
|
-
const plat =
|
|
1262
|
+
const plat = platform2();
|
|
1256
1263
|
if (plat === "darwin") {
|
|
1257
1264
|
return [
|
|
1258
1265
|
join(home, "Library/Fonts"),
|
|
@@ -1291,16 +1298,13 @@ var TextRenderer = class {
|
|
|
1291
1298
|
findFontByName(fontName) {
|
|
1292
1299
|
const extensions = [".ttf", ".ttc", ".otf", ".TTC", ".TTF", ".OTF"];
|
|
1293
1300
|
const directories = this.getSystemFontDirectories();
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
} catch {
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1301
|
+
const paths = flatMap(
|
|
1302
|
+
directories,
|
|
1303
|
+
(dir) => extensions.map((ext) => join(dir, `${fontName}${ext}`))
|
|
1304
|
+
);
|
|
1305
|
+
const fontPath = findFirstExisting(paths);
|
|
1306
|
+
if (fontPath) {
|
|
1307
|
+
return fontPath;
|
|
1304
1308
|
}
|
|
1305
1309
|
throw new Error(
|
|
1306
1310
|
`Font "${fontName}" not found in system font directories.
|
|
@@ -1313,13 +1317,9 @@ Tried extensions: ${extensions.join(", ")}`
|
|
|
1313
1317
|
* Get the path to the Cozette font (system or bundled)
|
|
1314
1318
|
*/
|
|
1315
1319
|
getDefaultFontPath() {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
return p;
|
|
1320
|
-
}
|
|
1321
|
-
} catch {
|
|
1322
|
-
}
|
|
1320
|
+
const systemPath = findFirstExisting(this.getSystemFontPaths());
|
|
1321
|
+
if (systemPath) {
|
|
1322
|
+
return systemPath;
|
|
1323
1323
|
}
|
|
1324
1324
|
const currentFilename = fileURLToPath(import.meta.url);
|
|
1325
1325
|
const currentDirname = dirname(currentFilename);
|
|
@@ -1331,96 +1331,38 @@ Tried extensions: ${extensions.join(", ")}`
|
|
|
1331
1331
|
resolve(currentDirname, "../fonts", DEFAULT_FONT_FILENAME)
|
|
1332
1332
|
// Alternate
|
|
1333
1333
|
];
|
|
1334
|
-
|
|
1335
|
-
try {
|
|
1336
|
-
if (existsSync3(p)) {
|
|
1337
|
-
return p;
|
|
1338
|
-
}
|
|
1339
|
-
} catch {
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
return bundledPaths[0];
|
|
1334
|
+
return findFirstExisting(bundledPaths) ?? bundledPaths[0];
|
|
1343
1335
|
}
|
|
1344
1336
|
/**
|
|
1345
1337
|
* Get fallback fonts for the current platform
|
|
1346
1338
|
*/
|
|
1347
1339
|
getFallbackFontPaths() {
|
|
1348
|
-
|
|
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 [];
|
|
1340
|
+
return getPlatformPaths(FALLBACK_FONTS);
|
|
1359
1341
|
}
|
|
1360
1342
|
/**
|
|
1361
1343
|
* Find an available font, trying default first then fallbacks
|
|
1362
1344
|
*/
|
|
1363
1345
|
findAvailableFont() {
|
|
1364
1346
|
const defaultPath = this.getDefaultFontPath();
|
|
1365
|
-
|
|
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;
|
|
1347
|
+
return findFirstExisting([defaultPath, ...this.getFallbackFontPaths()]) ?? defaultPath;
|
|
1380
1348
|
}
|
|
1381
1349
|
/**
|
|
1382
1350
|
* Find a system font, skipping the default bundled font
|
|
1383
1351
|
*/
|
|
1384
1352
|
findSystemFont() {
|
|
1385
|
-
|
|
1386
|
-
try {
|
|
1387
|
-
if (existsSync3(fallbackPath)) {
|
|
1388
|
-
return fallbackPath;
|
|
1389
|
-
}
|
|
1390
|
-
} catch {
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
return this.getDefaultFontPath();
|
|
1353
|
+
return findFirstExisting(this.getFallbackFontPaths()) ?? this.getDefaultFontPath();
|
|
1394
1354
|
}
|
|
1395
1355
|
/**
|
|
1396
1356
|
* Get emoji font paths for the current platform
|
|
1397
1357
|
*/
|
|
1398
1358
|
getEmojiFontPaths() {
|
|
1399
|
-
|
|
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 [];
|
|
1359
|
+
return getPlatformPaths(EMOJI_FONTS);
|
|
1410
1360
|
}
|
|
1411
1361
|
/**
|
|
1412
1362
|
* Find an available emoji font
|
|
1413
1363
|
*/
|
|
1414
1364
|
findEmojiFont() {
|
|
1415
|
-
|
|
1416
|
-
try {
|
|
1417
|
-
if (existsSync3(fontPath)) {
|
|
1418
|
-
return fontPath;
|
|
1419
|
-
}
|
|
1420
|
-
} catch {
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
return null;
|
|
1365
|
+
return findFirstExisting(this.getEmojiFontPaths());
|
|
1424
1366
|
}
|
|
1425
1367
|
/**
|
|
1426
1368
|
* Load the fallback emoji font if available
|
|
@@ -1912,6 +1854,11 @@ var TEXT_DECORATION_THICKNESS = 0.08;
|
|
|
1912
1854
|
var DEFAULT_BG2 = { r: 0, g: 0, b: 0 };
|
|
1913
1855
|
var DEFAULT_FG2 = { r: 255, g: 255, b: 255 };
|
|
1914
1856
|
var MIN_BRIGHTNESS = 100;
|
|
1857
|
+
var adjustBrightness = (color, multiplier, clamp = true) => ({
|
|
1858
|
+
r: clamp ? Math.min(COLOR_CHANNEL_MAX, Math.floor(color.r * multiplier)) : Math.floor(color.r * multiplier),
|
|
1859
|
+
g: clamp ? Math.min(COLOR_CHANNEL_MAX, Math.floor(color.g * multiplier)) : Math.floor(color.g * multiplier),
|
|
1860
|
+
b: clamp ? Math.min(COLOR_CHANNEL_MAX, Math.floor(color.b * multiplier)) : Math.floor(color.b * multiplier)
|
|
1861
|
+
});
|
|
1915
1862
|
var HEX_COLOR_LENGTH = 6;
|
|
1916
1863
|
var HEX_R_END = 2;
|
|
1917
1864
|
var HEX_G_END = 4;
|
|
@@ -1939,6 +1886,10 @@ var SdlUiRenderer = class {
|
|
|
1939
1886
|
renderer = null;
|
|
1940
1887
|
textRenderer = null;
|
|
1941
1888
|
renderTarget = null;
|
|
1889
|
+
/** Whether we own the window (should destroy it on cleanup) */
|
|
1890
|
+
ownsWindow = true;
|
|
1891
|
+
/** Whether we own the renderer (should destroy it on cleanup) */
|
|
1892
|
+
ownsRenderer = true;
|
|
1942
1893
|
ansiParser;
|
|
1943
1894
|
inputBridge;
|
|
1944
1895
|
windowWidth;
|
|
@@ -1973,11 +1924,69 @@ var SdlUiRenderer = class {
|
|
|
1973
1924
|
* Initialize SDL window and renderer
|
|
1974
1925
|
*/
|
|
1975
1926
|
initSDL(options) {
|
|
1927
|
+
this.defaultBgColor = parseBackgroundColor(options.backgroundColor);
|
|
1928
|
+
this.bgColor = { ...this.defaultBgColor };
|
|
1929
|
+
if (options.existing) {
|
|
1930
|
+
this.initWithExistingResources(options);
|
|
1931
|
+
} else {
|
|
1932
|
+
this.initNewResources(options);
|
|
1933
|
+
}
|
|
1934
|
+
this.userScaleFactor = options.scaleFactor === void 0 ? null : options.scaleFactor;
|
|
1935
|
+
if (this.userScaleFactor !== null) {
|
|
1936
|
+
this.scaleFactor = this.userScaleFactor;
|
|
1937
|
+
} else {
|
|
1938
|
+
this.scaleFactor = this.sdl.getScaleFactorFromRenderer(
|
|
1939
|
+
this.window,
|
|
1940
|
+
this.renderer
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
this.textRenderer = new TextRenderer(this.renderer, {
|
|
1944
|
+
fontSize: options.fontSize ?? DEFAULT_FONT_SIZE,
|
|
1945
|
+
scaleFactor: this.scaleFactor,
|
|
1946
|
+
...options.systemFont && { systemFont: true },
|
|
1947
|
+
...options.fontPath && { fontPath: options.fontPath },
|
|
1948
|
+
...options.fontName && { fontName: options.fontName }
|
|
1949
|
+
});
|
|
1950
|
+
const charDims = this.textRenderer.getCharDimensions();
|
|
1951
|
+
this.charWidth = charDims.width;
|
|
1952
|
+
this.charHeight = charDims.height;
|
|
1953
|
+
this.updateTerminalDimensions();
|
|
1954
|
+
this.createRenderTarget();
|
|
1955
|
+
this.setDrawColor(this.defaultBgColor);
|
|
1956
|
+
this.sdl.setRenderTarget(this.renderer, this.renderTarget);
|
|
1957
|
+
this.sdl.renderClear(this.renderer);
|
|
1958
|
+
this.sdl.setRenderTarget(this.renderer, null);
|
|
1959
|
+
this.sdl.renderClear(this.renderer);
|
|
1960
|
+
this.sdl.renderPresent(this.renderer);
|
|
1961
|
+
if (this.ownsWindow) {
|
|
1962
|
+
this.sdl.raiseWindow(this.window);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Initialize with existing SDL window and renderer
|
|
1967
|
+
*/
|
|
1968
|
+
initWithExistingResources(options) {
|
|
1969
|
+
const existing = options.existing;
|
|
1970
|
+
this.window = existing.window;
|
|
1971
|
+
this.renderer = existing.renderer;
|
|
1972
|
+
this.ownsWindow = false;
|
|
1973
|
+
this.ownsRenderer = false;
|
|
1974
|
+
if (!this.sdl.init(SDL_INIT_EVENTS)) {
|
|
1975
|
+
throw new Error("Failed to initialize SDL2 events");
|
|
1976
|
+
}
|
|
1977
|
+
const size = this.sdl.getWindowSize(this.window);
|
|
1978
|
+
this.windowWidth = size.width;
|
|
1979
|
+
this.windowHeight = size.height;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Initialize by creating new SDL window and renderer
|
|
1983
|
+
*/
|
|
1984
|
+
initNewResources(options) {
|
|
1976
1985
|
if (!this.sdl.init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
|
|
1977
1986
|
throw new Error("Failed to initialize SDL2 for UI rendering");
|
|
1978
1987
|
}
|
|
1979
|
-
this.
|
|
1980
|
-
this.
|
|
1988
|
+
this.ownsWindow = true;
|
|
1989
|
+
this.ownsRenderer = true;
|
|
1981
1990
|
let windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI;
|
|
1982
1991
|
if (!options.fullscreen) {
|
|
1983
1992
|
windowFlags |= SDL_WINDOW_RESIZABLE;
|
|
@@ -2005,40 +2014,6 @@ var SdlUiRenderer = class {
|
|
|
2005
2014
|
}
|
|
2006
2015
|
const rendererFlags = options.vsync !== false ? SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC : SDL_RENDERER_ACCELERATED;
|
|
2007
2016
|
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
2017
|
}
|
|
2043
2018
|
/**
|
|
2044
2019
|
* Update terminal dimensions based on window size
|
|
@@ -2130,13 +2105,7 @@ var SdlUiRenderer = class {
|
|
|
2130
2105
|
this.renderText(cmd);
|
|
2131
2106
|
break;
|
|
2132
2107
|
case "clear_screen":
|
|
2133
|
-
this.
|
|
2134
|
-
this.renderer,
|
|
2135
|
-
this.defaultBgColor.r,
|
|
2136
|
-
this.defaultBgColor.g,
|
|
2137
|
-
this.defaultBgColor.b,
|
|
2138
|
-
COLOR_CHANNEL_MAX
|
|
2139
|
-
);
|
|
2108
|
+
this.setDrawColor(this.defaultBgColor);
|
|
2140
2109
|
this.sdl.renderClear(this.renderer);
|
|
2141
2110
|
this.ansiParser.reset();
|
|
2142
2111
|
break;
|
|
@@ -2200,50 +2169,22 @@ var SdlUiRenderer = class {
|
|
|
2200
2169
|
let fg = this.reverse ? this.bgColor : this.fgColor;
|
|
2201
2170
|
const bg = this.reverse ? this.fgColor : this.bgColor;
|
|
2202
2171
|
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
|
-
};
|
|
2172
|
+
fg = adjustBrightness(fg, BOLD_BRIGHTNESS_MULTIPLIER);
|
|
2217
2173
|
}
|
|
2218
2174
|
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
|
-
};
|
|
2175
|
+
fg = adjustBrightness(fg, DIM_BRIGHTNESS_MULTIPLIER, false);
|
|
2224
2176
|
}
|
|
2225
2177
|
const brightness = Math.max(fg.r, fg.g, fg.b);
|
|
2226
2178
|
if (brightness < MIN_BRIGHTNESS) {
|
|
2227
2179
|
if (brightness === 0) {
|
|
2228
2180
|
fg = { r: MIN_BRIGHTNESS, g: MIN_BRIGHTNESS, b: MIN_BRIGHTNESS };
|
|
2229
2181
|
} else {
|
|
2230
|
-
|
|
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
|
-
};
|
|
2182
|
+
fg = adjustBrightness(fg, MIN_BRIGHTNESS / brightness);
|
|
2236
2183
|
}
|
|
2237
2184
|
}
|
|
2238
2185
|
const textWidth = text.length * this.charWidth;
|
|
2239
2186
|
const bgRect = createSDLRect(x, y, textWidth, this.charHeight);
|
|
2240
|
-
this.
|
|
2241
|
-
this.renderer,
|
|
2242
|
-
bg.r,
|
|
2243
|
-
bg.g,
|
|
2244
|
-
bg.b,
|
|
2245
|
-
COLOR_CHANNEL_MAX
|
|
2246
|
-
);
|
|
2187
|
+
this.setDrawColor(bg);
|
|
2247
2188
|
this.sdl.renderFillRect(this.renderer, bgRect);
|
|
2248
2189
|
this.textRenderer.renderText(text, x, y, fg, this.italic);
|
|
2249
2190
|
if (this.underline || this.strikethrough) {
|
|
@@ -2251,13 +2192,7 @@ var SdlUiRenderer = class {
|
|
|
2251
2192
|
1,
|
|
2252
2193
|
Math.round(this.charHeight * TEXT_DECORATION_THICKNESS)
|
|
2253
2194
|
);
|
|
2254
|
-
this.
|
|
2255
|
-
this.renderer,
|
|
2256
|
-
fg.r,
|
|
2257
|
-
fg.g,
|
|
2258
|
-
fg.b,
|
|
2259
|
-
COLOR_CHANNEL_MAX
|
|
2260
|
-
);
|
|
2195
|
+
this.setDrawColor(fg);
|
|
2261
2196
|
if (this.underline) {
|
|
2262
2197
|
const underlineY = y + Math.round(this.charHeight * UNDERLINE_POSITION);
|
|
2263
2198
|
const underlineRect = createSDLRect(
|
|
@@ -2283,16 +2218,22 @@ var SdlUiRenderer = class {
|
|
|
2283
2218
|
return;
|
|
2284
2219
|
}
|
|
2285
2220
|
this.sdl.setRenderTarget(this.renderer, this.renderTarget);
|
|
2221
|
+
this.setDrawColor(this.defaultBgColor);
|
|
2222
|
+
this.sdl.renderClear(this.renderer);
|
|
2223
|
+
this.sdl.setRenderTarget(this.renderer, null);
|
|
2224
|
+
this.ansiParser.reset();
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Set the SDL render draw color
|
|
2228
|
+
*/
|
|
2229
|
+
setDrawColor(color) {
|
|
2286
2230
|
this.sdl.setRenderDrawColor(
|
|
2287
2231
|
this.renderer,
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2232
|
+
color.r,
|
|
2233
|
+
color.g,
|
|
2234
|
+
color.b,
|
|
2291
2235
|
COLOR_CHANNEL_MAX
|
|
2292
2236
|
);
|
|
2293
|
-
this.sdl.renderClear(this.renderer);
|
|
2294
|
-
this.sdl.setRenderTarget(this.renderer, null);
|
|
2295
|
-
this.ansiParser.reset();
|
|
2296
2237
|
}
|
|
2297
2238
|
/**
|
|
2298
2239
|
* Clear a line from a specific position
|
|
@@ -2306,13 +2247,7 @@ var SdlUiRenderer = class {
|
|
|
2306
2247
|
const drawable = this.sdl.getDrawableSize(this.window);
|
|
2307
2248
|
const clearWidth = drawable.width - x;
|
|
2308
2249
|
const rect = createSDLRect(x, y, clearWidth, this.charHeight);
|
|
2309
|
-
this.
|
|
2310
|
-
this.renderer,
|
|
2311
|
-
this.bgColor.r,
|
|
2312
|
-
this.bgColor.g,
|
|
2313
|
-
this.bgColor.b,
|
|
2314
|
-
COLOR_CHANNEL_MAX
|
|
2315
|
-
);
|
|
2250
|
+
this.setDrawColor(this.bgColor);
|
|
2316
2251
|
this.sdl.renderFillRect(this.renderer, rect);
|
|
2317
2252
|
}
|
|
2318
2253
|
/**
|
|
@@ -2443,6 +2378,11 @@ var SdlUiRenderer = class {
|
|
|
2443
2378
|
}
|
|
2444
2379
|
/**
|
|
2445
2380
|
* Clean up resources
|
|
2381
|
+
*
|
|
2382
|
+
* When using existing window/renderer (via `existing` option), this method
|
|
2383
|
+
* will NOT destroy the window or renderer - only the resources created by
|
|
2384
|
+
* ink-sdl (TextRenderer, render target texture). The caller is responsible
|
|
2385
|
+
* for destroying the window/renderer they provided.
|
|
2446
2386
|
*/
|
|
2447
2387
|
destroy() {
|
|
2448
2388
|
if (this.textRenderer) {
|
|
@@ -2453,14 +2393,22 @@ var SdlUiRenderer = class {
|
|
|
2453
2393
|
this.sdl.destroyTexture(this.renderTarget);
|
|
2454
2394
|
this.renderTarget = null;
|
|
2455
2395
|
}
|
|
2456
|
-
if (this.renderer) {
|
|
2396
|
+
if (this.ownsRenderer && this.renderer) {
|
|
2457
2397
|
this.sdl.destroyRenderer(this.renderer);
|
|
2458
|
-
this.renderer = null;
|
|
2459
2398
|
}
|
|
2460
|
-
|
|
2399
|
+
this.renderer = null;
|
|
2400
|
+
if (this.ownsWindow && this.window) {
|
|
2461
2401
|
this.sdl.destroyWindow(this.window);
|
|
2462
|
-
this.window = null;
|
|
2463
2402
|
}
|
|
2403
|
+
this.window = null;
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Check if this renderer owns the SDL window
|
|
2407
|
+
*
|
|
2408
|
+
* Returns false when using an existing window via the `existing` option.
|
|
2409
|
+
*/
|
|
2410
|
+
ownsResources() {
|
|
2411
|
+
return this.ownsWindow && this.ownsRenderer;
|
|
2464
2412
|
}
|
|
2465
2413
|
/**
|
|
2466
2414
|
* Reset state for reuse
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -220,6 +220,19 @@ declare const createSDLRect: (x: number, y: number, w: number, h: number) => Buf
|
|
|
220
220
|
* from Ink and renders it to an SDL window using text rendering.
|
|
221
221
|
*/
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Existing SDL resources to use instead of creating new ones.
|
|
225
|
+
*
|
|
226
|
+
* When provided, ink-sdl will use these resources instead of creating its own.
|
|
227
|
+
* The caller retains ownership and is responsible for destroying them after
|
|
228
|
+
* ink-sdl is done.
|
|
229
|
+
*/
|
|
230
|
+
interface ExistingSdlResources {
|
|
231
|
+
/** Existing SDL window pointer */
|
|
232
|
+
window: SDLPointer;
|
|
233
|
+
/** Existing SDL renderer pointer */
|
|
234
|
+
renderer: SDLPointer;
|
|
235
|
+
}
|
|
223
236
|
interface SdlUiRendererOptions {
|
|
224
237
|
width?: number;
|
|
225
238
|
height?: number;
|
|
@@ -243,6 +256,19 @@ interface SdlUiRendererOptions {
|
|
|
243
256
|
minWidth?: number | undefined;
|
|
244
257
|
/** Minimum window height in pixels */
|
|
245
258
|
minHeight?: number | undefined;
|
|
259
|
+
/**
|
|
260
|
+
* Use existing SDL window and renderer instead of creating new ones.
|
|
261
|
+
*
|
|
262
|
+
* When provided, ink-sdl will:
|
|
263
|
+
* - Use the existing window/renderer for all rendering
|
|
264
|
+
* - NOT destroy them when destroy() is called (caller retains ownership)
|
|
265
|
+
* - Read dimensions from the existing window
|
|
266
|
+
* - Ignore width/height/title/fullscreen/borderless options (window already exists)
|
|
267
|
+
*
|
|
268
|
+
* This enables sharing a single SDL window between ink-sdl and other renderers,
|
|
269
|
+
* such as an emulator that switches between menu UI and game rendering.
|
|
270
|
+
*/
|
|
271
|
+
existing?: ExistingSdlResources | undefined;
|
|
246
272
|
}
|
|
247
273
|
/** Result from processing SDL events */
|
|
248
274
|
interface ProcessEventsResult {
|
|
@@ -265,6 +291,10 @@ declare class SdlUiRenderer {
|
|
|
265
291
|
private renderer;
|
|
266
292
|
private textRenderer;
|
|
267
293
|
private renderTarget;
|
|
294
|
+
/** Whether we own the window (should destroy it on cleanup) */
|
|
295
|
+
private ownsWindow;
|
|
296
|
+
/** Whether we own the renderer (should destroy it on cleanup) */
|
|
297
|
+
private ownsRenderer;
|
|
268
298
|
private ansiParser;
|
|
269
299
|
private inputBridge;
|
|
270
300
|
private windowWidth;
|
|
@@ -291,6 +321,14 @@ declare class SdlUiRenderer {
|
|
|
291
321
|
* Initialize SDL window and renderer
|
|
292
322
|
*/
|
|
293
323
|
private initSDL;
|
|
324
|
+
/**
|
|
325
|
+
* Initialize with existing SDL window and renderer
|
|
326
|
+
*/
|
|
327
|
+
private initWithExistingResources;
|
|
328
|
+
/**
|
|
329
|
+
* Initialize by creating new SDL window and renderer
|
|
330
|
+
*/
|
|
331
|
+
private initNewResources;
|
|
294
332
|
/**
|
|
295
333
|
* Update terminal dimensions based on window size
|
|
296
334
|
*/
|
|
@@ -334,6 +372,10 @@ declare class SdlUiRenderer {
|
|
|
334
372
|
* Clear the entire screen
|
|
335
373
|
*/
|
|
336
374
|
clear(): void;
|
|
375
|
+
/**
|
|
376
|
+
* Set the SDL render draw color
|
|
377
|
+
*/
|
|
378
|
+
private setDrawColor;
|
|
337
379
|
/**
|
|
338
380
|
* Clear a line from a specific position
|
|
339
381
|
*/
|
|
@@ -379,8 +421,19 @@ declare class SdlUiRenderer {
|
|
|
379
421
|
getScaleFactor(): number;
|
|
380
422
|
/**
|
|
381
423
|
* Clean up resources
|
|
424
|
+
*
|
|
425
|
+
* When using existing window/renderer (via `existing` option), this method
|
|
426
|
+
* will NOT destroy the window or renderer - only the resources created by
|
|
427
|
+
* ink-sdl (TextRenderer, render target texture). The caller is responsible
|
|
428
|
+
* for destroying the window/renderer they provided.
|
|
382
429
|
*/
|
|
383
430
|
destroy(): void;
|
|
431
|
+
/**
|
|
432
|
+
* Check if this renderer owns the SDL window
|
|
433
|
+
*
|
|
434
|
+
* Returns false when using an existing window via the `existing` option.
|
|
435
|
+
*/
|
|
436
|
+
ownsResources(): boolean;
|
|
384
437
|
/**
|
|
385
438
|
* Reset state for reuse
|
|
386
439
|
*/
|
|
@@ -549,6 +602,37 @@ interface SdlStreamsOptions {
|
|
|
549
602
|
minWidth?: number | undefined;
|
|
550
603
|
/** Minimum window height in pixels */
|
|
551
604
|
minHeight?: number | undefined;
|
|
605
|
+
/**
|
|
606
|
+
* Use existing SDL window and renderer instead of creating new ones.
|
|
607
|
+
*
|
|
608
|
+
* When provided, ink-sdl will:
|
|
609
|
+
* - Use the existing window/renderer for all rendering
|
|
610
|
+
* - NOT destroy them when the window is closed (caller retains ownership)
|
|
611
|
+
* - Read dimensions from the existing window
|
|
612
|
+
* - Ignore width/height/title/fullscreen/borderless options
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* ```typescript
|
|
616
|
+
* // Create your own SDL window and renderer
|
|
617
|
+
* const myWindow = SDL_CreateWindow(...);
|
|
618
|
+
* const myRenderer = SDL_CreateRenderer(myWindow, ...);
|
|
619
|
+
*
|
|
620
|
+
* // Use them with ink-sdl
|
|
621
|
+
* const streams = createSdlStreams({
|
|
622
|
+
* existing: { window: myWindow, renderer: myRenderer },
|
|
623
|
+
* fontSize: 16,
|
|
624
|
+
* });
|
|
625
|
+
*
|
|
626
|
+
* // When done with ink-sdl, clean up
|
|
627
|
+
* streams.window.close();
|
|
628
|
+
*
|
|
629
|
+
* // You can now use the window/renderer for other purposes
|
|
630
|
+
* // or destroy them yourself when fully done
|
|
631
|
+
* SDL_DestroyRenderer(myRenderer);
|
|
632
|
+
* SDL_DestroyWindow(myWindow);
|
|
633
|
+
* ```
|
|
634
|
+
*/
|
|
635
|
+
existing?: ExistingSdlResources | undefined;
|
|
552
636
|
}
|
|
553
637
|
/**
|
|
554
638
|
* SDL Window wrapper that emits events
|
|
@@ -1055,4 +1139,4 @@ declare class InputBridge {
|
|
|
1055
1139
|
*/
|
|
1056
1140
|
declare const isSdlAvailable: () => boolean;
|
|
1057
1141
|
|
|
1058
|
-
export { AnsiParser, type Color, type DrawCommand, type InkKeyEvent, InputBridge, type SDLPointer, Sdl2, SdlInputStream, type SdlKeyEvent, SdlOutputStream, type SdlStreams, type SdlStreamsOptions, SdlTtf, SdlUiRenderer, type SdlUiRendererOptions, SdlWindow, TextRenderer, createSDLRect, createSdlStreams, getSdl2, getSdlTtf, isSdl2Available, isSdlAvailable, isSdlTtfAvailable };
|
|
1142
|
+
export { AnsiParser, type Color, type DrawCommand, type ExistingSdlResources, type InkKeyEvent, InputBridge, type SDLPointer, Sdl2, SdlInputStream, type SdlKeyEvent, SdlOutputStream, type SdlStreams, type SdlStreamsOptions, SdlTtf, SdlUiRenderer, type SdlUiRendererOptions, SdlWindow, TextRenderer, createSDLRect, createSdlStreams, getSdl2, getSdlTtf, isSdl2Available, isSdlAvailable, isSdlTtfAvailable };
|
package/dist/index.js
CHANGED