@adhdev/ghostty-vt-node 1.0.78 → 1.0.82-rc.38

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.
Files changed (54) hide show
  1. package/CMakeLists.txt +284 -0
  2. package/README.md +151 -9
  3. package/binding-candidates.js +40 -0
  4. package/index.d.ts +107 -0
  5. package/index.js +92 -48
  6. package/package.json +6 -2
  7. package/prebuilt/darwin-arm64-node137/ghostty_vt_node.node +0 -0
  8. package/prebuilt/darwin-arm64-node137/libghostty-vt.0.1.0.dylib +0 -0
  9. package/prebuilt/darwin-arm64-node137/libghostty-vt.dylib +0 -0
  10. package/prebuilt/darwin-arm64-node137/manifest.json +34 -0
  11. package/prebuilt/linux-x64-node137/ghostty_vt_node.node +0 -0
  12. package/prebuilt/linux-x64-node137/libghostty-vt.so +0 -0
  13. package/prebuilt/linux-x64-node137/libghostty-vt.so.0 +0 -0
  14. package/prebuilt/linux-x64-node137/libghostty-vt.so.0.1.0 +0 -0
  15. package/prebuilt/linux-x64-node137/manifest.json +38 -0
  16. package/prebuilt/win32-x64-node137/ghostty-vt.dll +0 -0
  17. package/prebuilt/win32-x64-node137/ghostty_vt_node.node +0 -0
  18. package/prebuilt/win32-x64-node137/manifest.json +30 -0
  19. package/scripts/binding-digest.mjs +60 -0
  20. package/scripts/build-or-use-prebuilt.mjs +60 -31
  21. package/scripts/check-prebuilts.mjs +113 -0
  22. package/scripts/clean-dir.cjs +32 -0
  23. package/scripts/host-path-scan.mjs +115 -0
  24. package/scripts/native-format.mjs +219 -0
  25. package/scripts/prebuilt-manifest.mjs +231 -0
  26. package/scripts/probe-build-provenance.mjs +288 -0
  27. package/scripts/verify-prebuilt.mjs +115 -0
  28. package/src/addon.cc +403 -23
  29. package/src/ghostty_bridge.c +344 -0
  30. package/src/ghostty_bridge.h +96 -0
  31. package/toolchain.json +3 -0
  32. package/prebuilt/darwin-arm64-node115/ghostty_vt_node.node +0 -0
  33. package/prebuilt/darwin-arm64-node115/libghostty-vt.0.1.0.dylib +0 -0
  34. package/prebuilt/darwin-arm64-node115/libghostty-vt.dylib +0 -0
  35. package/prebuilt/darwin-arm64-node127/ghostty_vt_node.node +0 -0
  36. package/prebuilt/darwin-arm64-node127/libghostty-vt.0.1.0.dylib +0 -0
  37. package/prebuilt/darwin-arm64-node127/libghostty-vt.dylib +0 -0
  38. package/prebuilt/darwin-arm64-node141/ghostty_vt_node.node +0 -0
  39. package/prebuilt/darwin-arm64-node141/libghostty-vt.0.1.0.dylib +0 -0
  40. package/prebuilt/darwin-arm64-node141/libghostty-vt.dylib +0 -0
  41. package/prebuilt/linux-x64-node115/ghostty_vt_node.node +0 -0
  42. package/prebuilt/linux-x64-node115/libghostty-vt.so +0 -0
  43. package/prebuilt/linux-x64-node115/libghostty-vt.so.0 +0 -0
  44. package/prebuilt/linux-x64-node115/libghostty-vt.so.0.1.0 +0 -0
  45. package/prebuilt/linux-x64-node127/ghostty_vt_node.node +0 -0
  46. package/prebuilt/linux-x64-node127/libghostty-vt.so +0 -0
  47. package/prebuilt/linux-x64-node127/libghostty-vt.so.0 +0 -0
  48. package/prebuilt/linux-x64-node127/libghostty-vt.so.0.1.0 +0 -0
  49. package/prebuilt/linux-x64-node141/ghostty_vt_node.node +0 -0
  50. package/prebuilt/linux-x64-node141/libghostty-vt.so +0 -0
  51. package/prebuilt/linux-x64-node141/libghostty-vt.so.0 +0 -0
  52. package/prebuilt/linux-x64-node141/libghostty-vt.so.0.1.0 +0 -0
  53. package/prebuilt/win32-x64-node127/ghostty-vt.dll +0 -0
  54. package/prebuilt/win32-x64-node127/ghostty_vt_node.node +0 -0
package/src/addon.cc CHANGED
@@ -1,4 +1,7 @@
1
1
  #include <algorithm>
2
+ #include <cmath>
3
+ #include <cstdint>
4
+ #include <memory>
2
5
  #include <string>
3
6
 
4
7
  #include <napi.h>
@@ -7,10 +10,34 @@
7
10
 
8
11
  namespace {
9
12
 
13
+ // Native state of one terminal handle. Ownership is shared: the handle's
14
+ // `_native` External and EVERY method function hold a std::shared_ptr to it,
15
+ // each released by that JS object's own finalizer. A method taken off the
16
+ // handle (`const get = t.getViewport`) therefore keeps the state alive after
17
+ // the handle is collected, and the terminal is destroyed only when the last
18
+ // holder goes. Finalizers run in any order (including at env teardown) without
19
+ // one of them leaving the others with a dangling pointer.
20
+ //
21
+ // This is preferred over validating `this` as a wrapped receiver: detached
22
+ // methods are ordinary JS (destructuring, passing `t.write` as a callback) and
23
+ // keep working instead of throwing, and no receiver check is needed on the
24
+ // hot path. A disposed terminal throws ERR_GHOSTTY_DISPOSED.
10
25
  struct TerminalBindingState {
11
26
  AdhdevGhosttyTerminal* terminal = nullptr;
27
+
28
+ TerminalBindingState() = default;
29
+ TerminalBindingState(const TerminalBindingState&) = delete;
30
+ TerminalBindingState& operator=(const TerminalBindingState&) = delete;
31
+ ~TerminalBindingState() {
32
+ if (terminal) {
33
+ adhdev_ghostty_terminal_destroy(terminal);
34
+ terminal = nullptr;
35
+ }
36
+ }
12
37
  };
13
38
 
39
+ using SharedState = std::shared_ptr<TerminalBindingState>;
40
+
14
41
  std::string BuildGhosttyErrorMessage(const char* context, int result) {
15
42
  return std::string(context) + ": " + adhdev_ghostty_result_message(result);
16
43
  }
@@ -19,29 +46,42 @@ void ThrowGhosttyError(Napi::Env env, const char* context, int result) {
19
46
  Napi::Error::New(env, BuildGhosttyErrorMessage(context, result)).ThrowAsJavaScriptException();
20
47
  }
21
48
 
49
+ void ThrowCodedError(Napi::Env env, const std::string& message, const char* code, bool range = false) {
50
+ Napi::Error error = range ? Napi::RangeError::New(env, message) : Napi::Error::New(env, message);
51
+ error.Value().Set("code", Napi::String::New(env, code));
52
+ error.ThrowAsJavaScriptException();
53
+ }
54
+
55
+ // info.Data() is the calling function's own SharedState holder, which lives as
56
+ // long as that function object, so the state it points to is always alive here.
22
57
  TerminalBindingState* GetState(const Napi::CallbackInfo& info) {
23
- if (!info.Data()) {
24
- Napi::Error::New(info.Env(), "missing native terminal state").ThrowAsJavaScriptException();
58
+ auto* holder = static_cast<SharedState*>(info.Data());
59
+ if (!holder || !*holder) {
60
+ ThrowCodedError(info.Env(), "missing native terminal state", "ERR_GHOSTTY_NO_STATE");
25
61
  return nullptr;
26
62
  }
27
- return static_cast<TerminalBindingState*>(info.Data());
63
+ return holder->get();
28
64
  }
29
65
 
30
66
  bool EnsureOpen(Napi::Env env, TerminalBindingState* state) {
31
- if (!state || !state->terminal) {
32
- Napi::Error::New(env, "ghostty terminal already disposed").ThrowAsJavaScriptException();
67
+ if (!state) return false; // GetState already threw
68
+ if (!state->terminal) {
69
+ ThrowCodedError(env, "ghostty terminal already disposed", "ERR_GHOSTTY_DISPOSED");
33
70
  return false;
34
71
  }
35
72
  return true;
36
73
  }
37
74
 
38
- void FinalizeState(Napi::Env, TerminalBindingState* state) {
39
- if (!state) return;
40
- if (state->terminal) {
41
- adhdev_ghostty_terminal_destroy(state->terminal);
42
- state->terminal = nullptr;
43
- }
44
- delete state;
75
+ void ReleaseSharedState(Napi::Env, SharedState* holder) {
76
+ delete holder;
77
+ }
78
+
79
+ // A method function holding its own share of the state.
80
+ Napi::Function NewMethod(Napi::Env env, const SharedState& state, const char* name, Napi::Function::Callback callback) {
81
+ auto* holder = new SharedState(state);
82
+ Napi::Function fn = Napi::Function::New(env, callback, name, holder);
83
+ fn.AddFinalizer(ReleaseSharedState, holder);
84
+ return fn;
45
85
  }
46
86
 
47
87
  Napi::Value Write(const Napi::CallbackInfo& info) {
@@ -162,9 +202,279 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
162
202
  return output;
163
203
  }
164
204
 
205
+ constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL;
206
+
207
+ const char* WideKindName(uint8_t wide) {
208
+ switch (wide) {
209
+ case ADHDEV_GHOSTTY_WIDE_WIDE:
210
+ return "wide";
211
+ case ADHDEV_GHOSTTY_WIDE_SPACER_TAIL:
212
+ return "spacer-tail";
213
+ case ADHDEV_GHOSTTY_WIDE_SPACER_HEAD:
214
+ return "spacer-head";
215
+ default:
216
+ return "narrow";
217
+ }
218
+ }
219
+
220
+ int WideWidth(uint8_t wide) {
221
+ switch (wide) {
222
+ case ADHDEV_GHOSTTY_WIDE_WIDE:
223
+ return 2;
224
+ case ADHDEV_GHOSTTY_WIDE_SPACER_TAIL:
225
+ case ADHDEV_GHOSTTY_WIDE_SPACER_HEAD:
226
+ return 0;
227
+ default:
228
+ return 1;
229
+ }
230
+ }
231
+
232
+ Napi::Object RowToObject(Napi::Env env, const AdhdevGhosttyRowData& row, const char* space) {
233
+ Napi::Object output = Napi::Object::New(env);
234
+ output.Set("row", Napi::Number::New(env, row.row));
235
+ output.Set("coordinateSpace", Napi::String::New(env, space));
236
+ output.Set("text", Napi::String::New(env, row.text ? row.text : "", row.text_len));
237
+ output.Set("wrapped", Napi::Boolean::New(env, row.wrapped));
238
+ output.Set("wrapContinuation", Napi::Boolean::New(env, row.wrap_continuation));
239
+ if (row.cells) {
240
+ Napi::Array cells = Napi::Array::New(env, row.cell_count);
241
+ for (size_t i = 0; i < row.cell_count; i++) {
242
+ const AdhdevGhosttyCellInfo& info = row.cells[i];
243
+ Napi::Object cell = Napi::Object::New(env);
244
+ cell.Set("col", Napi::Number::New(env, static_cast<double>(i)));
245
+ cell.Set("text", Napi::String::New(
246
+ env,
247
+ info.text_len ? row.cell_text + info.text_offset : "",
248
+ info.text_len));
249
+ cell.Set("width", Napi::Number::New(env, WideWidth(info.wide)));
250
+ cell.Set("wideKind", Napi::String::New(env, WideKindName(info.wide)));
251
+ cells.Set(static_cast<uint32_t>(i), cell);
252
+ }
253
+ output.Set("cells", cells);
254
+ }
255
+ return output;
256
+ }
257
+
258
+ // Reads an optional boolean option, rejecting non-boolean values instead of
259
+ // coercing them.
260
+ bool ReadBoolOption(Napi::Env env, const Napi::Object& options, const char* name, bool* out) {
261
+ if (!options.Has(name)) return true;
262
+ Napi::Value value = options.Get(name);
263
+ if (value.IsUndefined()) return true;
264
+ if (!value.IsBoolean()) {
265
+ Napi::TypeError::New(env, std::string(name) + " must be a boolean").ThrowAsJavaScriptException();
266
+ return false;
267
+ }
268
+ *out = value.As<Napi::Boolean>().Value();
269
+ return true;
270
+ }
271
+
272
+ bool ReadOptionsObject(Napi::Env env, const Napi::CallbackInfo& info, size_t index, Napi::Object* out) {
273
+ if (info.Length() <= index || info[index].IsUndefined() || info[index].IsNull()) {
274
+ *out = Napi::Object::New(env);
275
+ return true;
276
+ }
277
+ if (!info[index].IsObject()) {
278
+ Napi::TypeError::New(env, "options must be an object").ThrowAsJavaScriptException();
279
+ return false;
280
+ }
281
+ *out = info[index].As<Napi::Object>();
282
+ return true;
283
+ }
284
+
285
+ // Snapshot of the ACTIVE area: metadata, cursor and (optionally) rows all come
286
+ // from one native call, so they describe the same terminal state.
287
+ Napi::Value GetViewport(const Napi::CallbackInfo& info) {
288
+ Napi::Env env = info.Env();
289
+ TerminalBindingState* state = GetState(info);
290
+ if (!EnsureOpen(env, state)) return env.Null();
291
+
292
+ Napi::Object options;
293
+ if (!ReadOptionsObject(env, info, 0, &options)) return env.Null();
294
+ int include_rows = ADHDEV_GHOSTTY_ROWS_NONE;
295
+ if (options.Has("includeRows") && !options.Get("includeRows").IsUndefined()) {
296
+ Napi::Value value = options.Get("includeRows");
297
+ const std::string mode = value.IsString() ? value.As<Napi::String>().Utf8Value() : "";
298
+ if (mode == "none") {
299
+ include_rows = ADHDEV_GHOSTTY_ROWS_NONE;
300
+ } else if (mode == "cursor") {
301
+ include_rows = ADHDEV_GHOSTTY_ROWS_CURSOR;
302
+ } else if (mode == "all") {
303
+ include_rows = ADHDEV_GHOSTTY_ROWS_ALL;
304
+ } else {
305
+ Napi::TypeError::New(env, "includeRows must be 'none', 'cursor' or 'all'").ThrowAsJavaScriptException();
306
+ return env.Null();
307
+ }
308
+ }
309
+ bool cells = false;
310
+ if (!ReadBoolOption(env, options, "cells", &cells)) return env.Null();
311
+
312
+ AdhdevGhosttyViewport* viewport = nullptr;
313
+ const int result = adhdev_ghostty_terminal_get_viewport(
314
+ state->terminal, include_rows, cells ? 1 : 0, &viewport);
315
+ if (result != ADHDEV_GHOSTTY_SUCCESS) {
316
+ ThrowGhosttyError(env, "adhdev_ghostty_terminal_get_viewport", result);
317
+ return env.Null();
318
+ }
319
+
320
+ const uint64_t total = viewport->scrollbar_total;
321
+ const uint64_t offset = viewport->scrollbar_offset;
322
+ const uint64_t len = viewport->scrollbar_len;
323
+ const uint64_t rows = viewport->rows;
324
+ // The derived origins below are only meaningful for a self-consistent
325
+ // scrollbar; report the inconsistency instead of clamping it into a
326
+ // plausible-looking answer.
327
+ if (total > kMaxSafeInteger || viewport->revision > kMaxSafeInteger || total < rows || len != rows ||
328
+ offset > total - len) {
329
+ adhdev_ghostty_viewport_free(viewport);
330
+ ThrowCodedError(env, "ghostty scrollbar state is inconsistent with the terminal size", "ERR_GHOSTTY_SCROLLBAR_STATE");
331
+ return env.Null();
332
+ }
333
+ const uint64_t active_origin = total - rows;
334
+ const uint64_t cursor_screen_row = active_origin + viewport->cursor_row;
335
+
336
+ Napi::Object output = Napi::Object::New(env);
337
+ output.Set("revision", Napi::Number::New(env, static_cast<double>(viewport->revision)));
338
+ output.Set("coordinateSpace", Napi::String::New(env, "active"));
339
+ output.Set("rows", Napi::Number::New(env, viewport->rows));
340
+ output.Set("cols", Napi::Number::New(env, viewport->cols));
341
+ output.Set("cursorRow", Napi::Number::New(env, viewport->cursor_row));
342
+ output.Set("cursorCol", Napi::Number::New(env, viewport->cursor_col));
343
+ output.Set("cursorPendingWrap", Napi::Boolean::New(env, viewport->cursor_pending_wrap));
344
+ output.Set("cursorVisible", Napi::Boolean::New(env, viewport->cursor_visible));
345
+ output.Set("activeScreen", Napi::String::New(env, viewport->active_screen ? "alternate" : "primary"));
346
+ Napi::Object scrollbar = Napi::Object::New(env);
347
+ scrollbar.Set("total", Napi::Number::New(env, static_cast<double>(total)));
348
+ scrollbar.Set("offset", Napi::Number::New(env, static_cast<double>(offset)));
349
+ scrollbar.Set("len", Napi::Number::New(env, static_cast<double>(len)));
350
+ output.Set("scrollbar", scrollbar);
351
+ output.Set("scrollbackRows", Napi::Number::New(env, static_cast<double>(total - len)));
352
+ output.Set("activeOriginRow", Napi::Number::New(env, static_cast<double>(active_origin)));
353
+ output.Set("viewportOriginRow", Napi::Number::New(env, static_cast<double>(offset)));
354
+ output.Set("viewportScrollbackOffset", Napi::Number::New(env, static_cast<double>(active_origin - offset)));
355
+ if (cursor_screen_row >= offset && cursor_screen_row - offset < rows) {
356
+ output.Set("cursorViewportRow", Napi::Number::New(env, static_cast<double>(cursor_screen_row - offset)));
357
+ } else {
358
+ output.Set("cursorViewportRow", env.Null());
359
+ }
360
+ if (viewport->lines) {
361
+ Napi::Array lines = Napi::Array::New(env, viewport->line_count);
362
+ for (size_t i = 0; i < viewport->line_count; i++) {
363
+ lines.Set(static_cast<uint32_t>(i), RowToObject(env, viewport->lines[i], "active"));
364
+ }
365
+ output.Set("lines", lines);
366
+ }
367
+ adhdev_ghostty_viewport_free(viewport);
368
+ return output;
369
+ }
370
+
371
+ Napi::Value ReadRow(const Napi::CallbackInfo& info) {
372
+ Napi::Env env = info.Env();
373
+ TerminalBindingState* state = GetState(info);
374
+ if (!EnsureOpen(env, state)) return env.Null();
375
+
376
+ // Validate before any narrowing cast: a fractional, negative or huge index
377
+ // must not silently become some other row.
378
+ if (info.Length() < 1 || !info[0].IsNumber()) {
379
+ Napi::TypeError::New(env, "readRow expects a row number").ThrowAsJavaScriptException();
380
+ return env.Null();
381
+ }
382
+ const double row_value = info[0].As<Napi::Number>().DoubleValue();
383
+ if (!std::isfinite(row_value) || std::floor(row_value) != row_value || row_value < 0 || row_value > 65535) {
384
+ ThrowCodedError(env, "row must be an integer within the terminal", "ERR_GHOSTTY_ROW_OUT_OF_RANGE", true);
385
+ return env.Null();
386
+ }
387
+
388
+ Napi::Object options;
389
+ if (!ReadOptionsObject(env, info, 1, &options)) return env.Null();
390
+ bool cells = false;
391
+ if (!ReadBoolOption(env, options, "cells", &cells)) return env.Null();
392
+ int space = ADHDEV_GHOSTTY_SPACE_ACTIVE;
393
+ const char* space_name = "active";
394
+ if (options.Has("space") && !options.Get("space").IsUndefined()) {
395
+ Napi::Value value = options.Get("space");
396
+ const std::string name = value.IsString() ? value.As<Napi::String>().Utf8Value() : "";
397
+ if (name == "active") {
398
+ space = ADHDEV_GHOSTTY_SPACE_ACTIVE;
399
+ } else if (name == "viewport") {
400
+ space = ADHDEV_GHOSTTY_SPACE_VIEWPORT;
401
+ space_name = "viewport";
402
+ } else {
403
+ Napi::TypeError::New(env, "space must be 'active' or 'viewport'").ThrowAsJavaScriptException();
404
+ return env.Null();
405
+ }
406
+ }
407
+ bool has_expected = false;
408
+ double expected = 0;
409
+ if (options.Has("expectedRevision") && !options.Get("expectedRevision").IsUndefined()) {
410
+ Napi::Value value = options.Get("expectedRevision");
411
+ if (!value.IsNumber()) {
412
+ Napi::TypeError::New(env, "expectedRevision must be a number").ThrowAsJavaScriptException();
413
+ return env.Null();
414
+ }
415
+ has_expected = true;
416
+ expected = value.As<Napi::Number>().DoubleValue();
417
+ }
418
+
419
+ if (has_expected) {
420
+ const double current = static_cast<double>(adhdev_ghostty_terminal_revision(state->terminal));
421
+ if (current != expected) {
422
+ ThrowCodedError(env, "terminal changed since the expected revision", "ERR_GHOSTTY_STALE_REVISION");
423
+ return env.Null();
424
+ }
425
+ }
426
+
427
+ AdhdevGhosttyRowData row = {};
428
+ uint64_t revision = 0;
429
+ const int result = adhdev_ghostty_terminal_read_row(
430
+ state->terminal, space, static_cast<uint16_t>(row_value), cells ? 1 : 0, &row, &revision);
431
+ if (result == ADHDEV_GHOSTTY_ROW_OUT_OF_RANGE) {
432
+ ThrowCodedError(env, "row must be an integer within the terminal", "ERR_GHOSTTY_ROW_OUT_OF_RANGE", true);
433
+ return env.Null();
434
+ }
435
+ if (result != ADHDEV_GHOSTTY_SUCCESS) {
436
+ ThrowGhosttyError(env, "adhdev_ghostty_terminal_read_row", result);
437
+ return env.Null();
438
+ }
439
+ Napi::Object output = RowToObject(env, row, space_name);
440
+ output.Set("revision", Napi::Number::New(env, static_cast<double>(revision)));
441
+ adhdev_ghostty_row_data_release(&row);
442
+ return output;
443
+ }
444
+
445
+ Napi::Value ScrollViewport(const Napi::CallbackInfo& info) {
446
+ Napi::Env env = info.Env();
447
+ TerminalBindingState* state = GetState(info);
448
+ if (!EnsureOpen(env, state)) return env.Undefined();
449
+
450
+ int behavior = -1;
451
+ int64_t delta = 0;
452
+ if (info.Length() >= 1 && info[0].IsString()) {
453
+ const std::string target = info[0].As<Napi::String>().Utf8Value();
454
+ if (target == "top") behavior = ADHDEV_GHOSTTY_SCROLL_TOP;
455
+ if (target == "bottom") behavior = ADHDEV_GHOSTTY_SCROLL_BOTTOM;
456
+ } else if (info.Length() >= 1 && info[0].IsNumber()) {
457
+ const double value = info[0].As<Napi::Number>().DoubleValue();
458
+ if (std::isfinite(value) && std::floor(value) == value && std::fabs(value) <= 1e9) {
459
+ behavior = ADHDEV_GHOSTTY_SCROLL_DELTA;
460
+ delta = static_cast<int64_t>(value);
461
+ }
462
+ }
463
+ if (behavior < 0) {
464
+ Napi::TypeError::New(env, "scrollViewport expects 'top', 'bottom' or an integer row delta").ThrowAsJavaScriptException();
465
+ return env.Undefined();
466
+ }
467
+ const int result = adhdev_ghostty_terminal_scroll_viewport(state->terminal, behavior, delta);
468
+ if (result != ADHDEV_GHOSTTY_SUCCESS) {
469
+ ThrowGhosttyError(env, "adhdev_ghostty_terminal_scroll_viewport", result);
470
+ }
471
+ return env.Undefined();
472
+ }
473
+
165
474
  Napi::Value Dispose(const Napi::CallbackInfo& info) {
166
475
  TerminalBindingState* state = GetState(info);
167
- if (state && state->terminal) {
476
+ if (!state) return info.Env().Undefined();
477
+ if (state->terminal) {
168
478
  adhdev_ghostty_terminal_destroy(state->terminal);
169
479
  state->terminal = nullptr;
170
480
  }
@@ -185,30 +495,100 @@ Napi::Value CreateTerminal(const Napi::CallbackInfo& info) {
185
495
  ? static_cast<size_t>(std::max<int64_t>(0, options.Get("scrollback").ToNumber().Int64Value()))
186
496
  : 2000;
187
497
 
188
- auto* state = new TerminalBindingState();
498
+ SharedState state = std::make_shared<TerminalBindingState>();
189
499
  const int result = adhdev_ghostty_terminal_create(cols, rows, scrollback, &state->terminal);
190
500
  if (result != ADHDEV_GHOSTTY_SUCCESS) {
191
- delete state;
501
+ state->terminal = nullptr;
192
502
  ThrowGhosttyError(env, "adhdev_ghostty_terminal_create", result);
193
503
  return env.Null();
194
504
  }
195
505
 
196
506
  Napi::Object object = Napi::Object::New(env);
197
- Napi::External<TerminalBindingState> external = Napi::External<TerminalBindingState>::New(env, state, FinalizeState);
198
- object.Set("_native", external);
199
- object.Set("write", Napi::Function::New(env, Write, "write", state));
200
- object.Set("resize", Napi::Function::New(env, Resize, "resize", state));
201
- object.Set("formatPlainText", Napi::Function::New(env, FormatPlainText, "formatPlainText", state));
202
- object.Set("formatVT", Napi::Function::New(env, FormatVT, "formatVT", state));
203
- object.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition, "getCursorPosition", state));
204
- object.Set("dispose", Napi::Function::New(env, Dispose, "dispose", state));
507
+ auto* nativeHolder = new SharedState(state);
508
+ object.Set("_native", Napi::External<SharedState>::New(env, nativeHolder, ReleaseSharedState));
509
+ object.Set("write", NewMethod(env, state, "write", Write));
510
+ object.Set("resize", NewMethod(env, state, "resize", Resize));
511
+ object.Set("formatPlainText", NewMethod(env, state, "formatPlainText", FormatPlainText));
512
+ object.Set("formatVT", NewMethod(env, state, "formatVT", FormatVT));
513
+ object.Set("getCursorPosition", NewMethod(env, state, "getCursorPosition", GetCursorPosition));
514
+ object.Set("getViewport", NewMethod(env, state, "getViewport", GetViewport));
515
+ object.Set("readRow", NewMethod(env, state, "readRow", ReadRow));
516
+ object.Set("scrollViewport", NewMethod(env, state, "scrollViewport", ScrollViewport));
517
+ object.Set("dispose", NewMethod(env, state, "dispose", Dispose));
205
518
  return object;
206
519
  }
207
520
 
521
+ #ifndef ADHDEV_GHOSTTY_SOURCE_DIGEST
522
+ #define ADHDEV_GHOSTTY_SOURCE_DIGEST ""
523
+ #endif
524
+ #ifndef ADHDEV_GHOSTTY_GIT_TAG
525
+ #define ADHDEV_GHOSTTY_GIT_TAG ""
526
+ #endif
527
+ // Build provenance from CMakeLists.txt. A build without it (no CMake
528
+ // definitions) is never release eligible.
529
+ #ifndef ADHDEV_GHOSTTY_RELEASE_ELIGIBLE
530
+ #define ADHDEV_GHOSTTY_RELEASE_ELIGIBLE 0
531
+ #endif
532
+ #ifndef ADHDEV_GHOSTTY_PROVENANCE_PROBLEMS
533
+ #define ADHDEV_GHOSTTY_PROVENANCE_PROBLEMS "no-build-provenance"
534
+ #endif
535
+ #ifndef ADHDEV_GHOSTTY_SOURCE_REVISION
536
+ #define ADHDEV_GHOSTTY_SOURCE_REVISION ""
537
+ #endif
538
+ #ifndef ADHDEV_GHOSTTY_SOURCE_CLEAN
539
+ #define ADHDEV_GHOSTTY_SOURCE_CLEAN 0
540
+ #endif
541
+ #ifndef ADHDEV_GHOSTTY_ZIG_VERSION
542
+ #define ADHDEV_GHOSTTY_ZIG_VERSION ""
543
+ #endif
544
+ #ifndef ADHDEV_GHOSTTY_C_COMPILER
545
+ #define ADHDEV_GHOSTTY_C_COMPILER ""
546
+ #endif
547
+ #ifndef ADHDEV_GHOSTTY_CXX_COMPILER
548
+ #define ADHDEV_GHOSTTY_CXX_COMPILER ""
549
+ #endif
550
+ #ifndef ADHDEV_GHOSTTY_BUILD_TYPE
551
+ #define ADHDEV_GHOSTTY_BUILD_TYPE ""
552
+ #endif
553
+
554
+ // Provenance of this compiled binary. sourceDigest is computed by CMake from
555
+ // the binding sources at build time (scripts/binding-digest.mjs reproduces it),
556
+ // so a prebuilt can be matched against the sources it claims to implement.
557
+ // ghosttyRevision is the declared pin; ghosttySourceRevision/Clean describe the
558
+ // checkout that was actually built, and zigVersion/cCompiler/cxxCompiler the
559
+ // toolchain. releaseEligible is false for an ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD.
560
+ Napi::Value GetBindingInfo(const Napi::CallbackInfo& info) {
561
+ Napi::Env env = info.Env();
562
+ Napi::Object output = Napi::Object::New(env);
563
+ output.Set("apiLevel", Napi::Number::New(env, ADHDEV_GHOSTTY_API_LEVEL));
564
+ output.Set("sourceDigest", Napi::String::New(env, ADHDEV_GHOSTTY_SOURCE_DIGEST));
565
+ output.Set("ghosttyRevision", Napi::String::New(env, ADHDEV_GHOSTTY_GIT_TAG));
566
+ output.Set("napiVersion", Napi::Number::New(env, NAPI_VERSION));
567
+ output.Set("releaseEligible", Napi::Boolean::New(env, ADHDEV_GHOSTTY_RELEASE_ELIGIBLE != 0));
568
+ Napi::Array problems = Napi::Array::New(env);
569
+ std::string problemText = ADHDEV_GHOSTTY_PROVENANCE_PROBLEMS;
570
+ size_t start = 0;
571
+ while (start < problemText.size()) {
572
+ size_t end = problemText.find(',', start);
573
+ if (end == std::string::npos) end = problemText.size();
574
+ if (end > start) problems.Set(problems.Length(), Napi::String::New(env, problemText.substr(start, end - start)));
575
+ start = end + 1;
576
+ }
577
+ output.Set("provenanceProblems", problems);
578
+ output.Set("ghosttySourceRevision", Napi::String::New(env, ADHDEV_GHOSTTY_SOURCE_REVISION));
579
+ output.Set("ghosttySourceClean", Napi::Boolean::New(env, ADHDEV_GHOSTTY_SOURCE_CLEAN != 0));
580
+ output.Set("zigVersion", Napi::String::New(env, ADHDEV_GHOSTTY_ZIG_VERSION));
581
+ output.Set("cCompiler", Napi::String::New(env, ADHDEV_GHOSTTY_C_COMPILER));
582
+ output.Set("cxxCompiler", Napi::String::New(env, ADHDEV_GHOSTTY_CXX_COMPILER));
583
+ output.Set("buildType", Napi::String::New(env, ADHDEV_GHOSTTY_BUILD_TYPE));
584
+ return output;
585
+ }
586
+
208
587
  } // namespace
209
588
 
210
589
  Napi::Object InitAddon(Napi::Env env, Napi::Object exports) {
211
590
  exports.Set("createTerminal", Napi::Function::New(env, CreateTerminal));
591
+ exports.Set("getBindingInfo", Napi::Function::New(env, GetBindingInfo));
212
592
  return exports;
213
593
  }
214
594