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 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
- import { platform } from "os";
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 existsSync3 } from "fs";
966
- import { platform as platform3, homedir } from "os";
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 findLibrary2(SDL_TTF_LIB_PATHS);
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 = platform3();
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
- 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
- }
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
- for (const p of this.getSystemFontPaths()) {
1317
- try {
1318
- if (existsSync3(p)) {
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
- for (const p of bundledPaths) {
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
- 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 [];
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
- 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;
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
- for (const fallbackPath of this.getFallbackFontPaths()) {
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
- 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 [];
1359
+ return getPlatformPaths(EMOJI_FONTS);
1410
1360
  }
1411
1361
  /**
1412
1362
  * Find an available emoji font
1413
1363
  */
1414
1364
  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;
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.defaultBgColor = parseBackgroundColor(options.backgroundColor);
1980
- this.bgColor = { ...this.defaultBgColor };
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.sdl.setRenderDrawColor(
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
- 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
- };
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.sdl.setRenderDrawColor(
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.sdl.setRenderDrawColor(
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
- this.defaultBgColor.r,
2289
- this.defaultBgColor.g,
2290
- this.defaultBgColor.b,
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.sdl.setRenderDrawColor(
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
- if (this.window) {
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSdlStreams
4
- } from "./chunk-BOQYTA3S.js";
4
+ } from "./chunk-LP5C65TH.js";
5
5
 
6
6
  // src/cli.tsx
7
7
  import { parseArgs } from "util";
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
@@ -15,7 +15,7 @@ import {
15
15
  isSdl2Available,
16
16
  isSdlAvailable,
17
17
  isSdlTtfAvailable
18
- } from "./chunk-BOQYTA3S.js";
18
+ } from "./chunk-LP5C65TH.js";
19
19
  export {
20
20
  AnsiParser,
21
21
  InputBridge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-sdl",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Render Ink terminal apps in native SDL windows",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",