@adhdev/ghostty-vt-node 1.0.78 → 1.0.82-rc.37
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/CMakeLists.txt +284 -0
- package/README.md +151 -9
- package/binding-candidates.js +40 -0
- package/index.d.ts +107 -0
- package/index.js +92 -48
- package/package.json +6 -2
- package/prebuilt/darwin-arm64-node137/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node137/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node137/libghostty-vt.dylib +0 -0
- package/prebuilt/darwin-arm64-node137/manifest.json +34 -0
- package/prebuilt/linux-x64-node137/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node137/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node137/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node137/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/linux-x64-node137/manifest.json +38 -0
- package/prebuilt/win32-x64-node137/ghostty-vt.dll +0 -0
- package/prebuilt/win32-x64-node137/ghostty_vt_node.node +0 -0
- package/prebuilt/win32-x64-node137/manifest.json +30 -0
- package/scripts/binding-digest.mjs +60 -0
- package/scripts/build-or-use-prebuilt.mjs +60 -31
- package/scripts/check-prebuilts.mjs +113 -0
- package/scripts/clean-dir.cjs +32 -0
- package/scripts/host-path-scan.mjs +115 -0
- package/scripts/native-format.mjs +219 -0
- package/scripts/prebuilt-manifest.mjs +231 -0
- package/scripts/probe-build-provenance.mjs +288 -0
- package/scripts/verify-prebuilt.mjs +115 -0
- package/src/addon.cc +403 -23
- package/src/ghostty_bridge.c +344 -0
- package/src/ghostty_bridge.h +96 -0
- package/toolchain.json +3 -0
- package/prebuilt/darwin-arm64-node115/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node115/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node115/libghostty-vt.dylib +0 -0
- package/prebuilt/darwin-arm64-node127/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node127/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node127/libghostty-vt.dylib +0 -0
- package/prebuilt/darwin-arm64-node141/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node141/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node141/libghostty-vt.dylib +0 -0
- package/prebuilt/linux-x64-node115/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node115/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node115/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node115/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/linux-x64-node127/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node127/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node127/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node127/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/linux-x64-node141/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node141/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node141/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node141/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/win32-x64-node127/ghostty-vt.dll +0 -0
- 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
|
-
|
|
24
|
-
|
|
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
|
|
63
|
+
return holder->get();
|
|
28
64
|
}
|
|
29
65
|
|
|
30
66
|
bool EnsureOpen(Napi::Env env, TerminalBindingState* state) {
|
|
31
|
-
if (!state
|
|
32
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
object.Set("_native",
|
|
199
|
-
object.Set("write",
|
|
200
|
-
object.Set("resize",
|
|
201
|
-
object.Set("formatPlainText",
|
|
202
|
-
object.Set("formatVT",
|
|
203
|
-
object.Set("getCursorPosition",
|
|
204
|
-
object.Set("
|
|
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
|
|