@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.
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/CMakeLists.txt CHANGED
@@ -16,21 +16,270 @@ execute_process(
16
16
  string(REPLACE "\"" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
17
17
 
18
18
  set(GHOSTTY_GIT_TAG "d4a38c8661453026bd1b1bb022ba2c37b347adce" CACHE STRING "Ghostty git revision for libghostty-vt")
19
+ # The pin itself, independent of the overridable cache entry above.
20
+ set(ADHDEV_GHOSTTY_PINNED_REVISION "d4a38c8661453026bd1b1bb022ba2c37b347adce")
19
21
 
20
22
  if(DEFINED ENV{ZIG_EXECUTABLE} AND NOT DEFINED ZIG_EXECUTABLE)
21
23
  set(ZIG_EXECUTABLE "$ENV{ZIG_EXECUTABLE}" CACHE FILEPATH "Path to zig executable")
22
24
  endif()
23
25
 
26
+ # --- Build provenance ---------------------------------------------------------
27
+ # A release binary must be built from exactly the pinned ghostty revision, in a
28
+ # clean FetchContent-managed clone, with the pinned zig, and its libghostty-vt
29
+ # must be built by THIS build from fresh, private zig caches (see "Runtime
30
+ # freshness" below). Each violation
31
+ # is a problem code; any problem fails the configure step unless the explicit
32
+ # developer override ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD=1 (cache variable or
33
+ # environment) is set. The override ALWAYS makes the build non-release, even
34
+ # with nothing else wrong: the binary reports releaseEligible=false and the
35
+ # prebuilt release gates refuse it. Only problem codes are embedded, never paths.
36
+ #
37
+ # toolchain.json is the single zig pin: the native workflow installs that
38
+ # version and the release gate compares against it. It is a binding digest
39
+ # input, so changing the pin invalidates every existing prebuilt.
40
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/toolchain.json" ADHDEV_TOOLCHAIN_JSON)
41
+ string(JSON ADHDEV_PINNED_ZIG_VERSION GET "${ADHDEV_TOOLCHAIN_JSON}" zig)
42
+
43
+ set(ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD OFF CACHE BOOL "Allow a non-release build from an unverified ghostty source or toolchain")
44
+ if("$ENV{ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD}" STREQUAL "1")
45
+ set(ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD ON)
46
+ endif()
47
+ set(ADHDEV_PROVENANCE_PROBLEMS "")
48
+ if(ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD)
49
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "unverified-dev-build")
50
+ endif()
51
+ if(NOT GHOSTTY_GIT_TAG STREQUAL ADHDEV_GHOSTTY_PINNED_REVISION)
52
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "ghostty-tag-override")
53
+ endif()
54
+ # A substituted checkout is not under this build's control (it may be shared,
55
+ # reused or written to while building). Release builds use the managed clone.
56
+ if(FETCHCONTENT_SOURCE_DIR_GHOSTTY)
57
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "ghostty-source-override")
58
+ endif()
59
+
60
+ # User-supplied extra zig flags change the library ghostty builds.
61
+ if(NOT "${GHOSTTY_ZIG_BUILD_FLAGS}" STREQUAL "")
62
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "extra-zig-build-flags")
63
+ endif()
64
+
65
+ # Resolve zig and record the version that actually builds libghostty-vt. The whole output, with
66
+ # only surrounding whitespace/CRLF trimmed, must equal the pin exactly.
67
+ find_program(ZIG_EXECUTABLE zig REQUIRED)
68
+ execute_process(
69
+ COMMAND "${ZIG_EXECUTABLE}" version
70
+ OUTPUT_VARIABLE zig_version_output
71
+ RESULT_VARIABLE zig_version_result
72
+ ERROR_QUIET
73
+ )
74
+ string(STRIP "${zig_version_output}" zig_version_output)
75
+ if(zig_version_result EQUAL 0 AND zig_version_output STREQUAL ADHDEV_PINNED_ZIG_VERSION)
76
+ set(ADHDEV_ZIG_VERSION "${ADHDEV_PINNED_ZIG_VERSION}")
77
+ else()
78
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "zig-version-mismatch")
79
+ # Recorded for diagnostics only, and only if it is a plain version token.
80
+ if(zig_version_output MATCHES "^[0-9A-Za-z.+-]+$")
81
+ set(ADHDEV_ZIG_VERSION "${zig_version_output}")
82
+ else()
83
+ set(ADHDEV_ZIG_VERSION "unrecognized")
84
+ endif()
85
+ endif()
86
+
87
+ # Only the SOURCE is taken from the clone. Upstream's CMakeLists.txt is not
88
+ # added (SOURCE_SUBDIR points at a directory without one): it builds through an
89
+ # OUTPUT-only custom command with the ambient zig caches and installs into the
90
+ # git-ignored zig-out/ of the source tree, none of which this build can vouch
91
+ # for. libghostty-vt is built by adhdev_ghostty_runtime below instead.
24
92
  FetchContent_Declare(ghostty
25
93
  GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
26
94
  GIT_TAG ${GHOSTTY_GIT_TAG}
95
+ SOURCE_SUBDIR adhdev-no-upstream-cmake
27
96
  )
28
97
  FetchContent_MakeAvailable(ghostty)
29
98
 
99
+ # The DECLARED tag proves nothing: FETCHCONTENT_SOURCE_DIR_GHOSTTY can point at
100
+ # any checkout. Verify the source that is actually built.
101
+ find_package(Git QUIET)
102
+ find_program(ADHDEV_NODE_EXECUTABLE node REQUIRED)
103
+ set(ADHDEV_GHOSTTY_SOURCE_REVISION "")
104
+ set(ADHDEV_GHOSTTY_SOURCE_CLEAN 0)
105
+ if(GIT_EXECUTABLE)
106
+ execute_process(
107
+ COMMAND "${GIT_EXECUTABLE}" -C "${ghostty_SOURCE_DIR}" rev-parse HEAD
108
+ OUTPUT_VARIABLE ghostty_head
109
+ OUTPUT_STRIP_TRAILING_WHITESPACE
110
+ RESULT_VARIABLE ghostty_head_result
111
+ ERROR_QUIET
112
+ )
113
+ execute_process(
114
+ COMMAND "${GIT_EXECUTABLE}" -C "${ghostty_SOURCE_DIR}" status --porcelain
115
+ OUTPUT_VARIABLE ghostty_status
116
+ OUTPUT_STRIP_TRAILING_WHITESPACE
117
+ RESULT_VARIABLE ghostty_status_result
118
+ ERROR_QUIET
119
+ )
120
+ if(ghostty_head_result EQUAL 0 AND ghostty_head MATCHES "^[0-9a-f]+$")
121
+ set(ADHDEV_GHOSTTY_SOURCE_REVISION "${ghostty_head}")
122
+ endif()
123
+ if(ghostty_status_result EQUAL 0 AND ghostty_status STREQUAL "")
124
+ set(ADHDEV_GHOSTTY_SOURCE_CLEAN 1)
125
+ endif()
126
+ endif()
127
+ if(ADHDEV_GHOSTTY_SOURCE_REVISION STREQUAL "")
128
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "ghostty-source-not-a-git-checkout")
129
+ elseif(NOT ADHDEV_GHOSTTY_SOURCE_REVISION STREQUAL ADHDEV_GHOSTTY_PINNED_REVISION)
130
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "ghostty-head-mismatch")
131
+ endif()
132
+ if(NOT ADHDEV_GHOSTTY_SOURCE_CLEAN)
133
+ list(APPEND ADHDEV_PROVENANCE_PROBLEMS "ghostty-source-dirty")
134
+ endif()
135
+
136
+ set(ADHDEV_VERIFY_GHOSTTY_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/adhdev-verify-ghostty-source.cmake")
137
+ file(WRITE "${ADHDEV_VERIFY_GHOSTTY_SCRIPT}" [==[
138
+ # Generated by ghostty-vt-node/CMakeLists.txt: re-verify at build time that the
139
+ # ghostty source is still the pinned revision with a clean tree.
140
+ execute_process(COMMAND "${GIT}" -C "${SRC}" rev-parse HEAD
141
+ OUTPUT_VARIABLE head OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE head_result ERROR_QUIET)
142
+ execute_process(COMMAND "${GIT}" -C "${SRC}" status --porcelain
143
+ OUTPUT_VARIABLE status OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE status_result ERROR_QUIET)
144
+ if(NOT head_result EQUAL 0 OR NOT head STREQUAL PIN)
145
+ message(FATAL_ERROR "ghostty-vt-node: ghostty source HEAD is '${head}', not the pin ${PIN}; reconfigure")
146
+ endif()
147
+ if(NOT status_result EQUAL 0 OR NOT status STREQUAL "")
148
+ message(FATAL_ERROR "ghostty-vt-node: ghostty source tree changed since configure; reconfigure from a clean clone")
149
+ endif()
150
+ ]==])
151
+
152
+ if(ADHDEV_PROVENANCE_PROBLEMS)
153
+ if(NOT ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD)
154
+ message(FATAL_ERROR
155
+ "ghostty-vt-node build provenance check failed: ${ADHDEV_PROVENANCE_PROBLEMS}\n"
156
+ " ghostty pin ${ADHDEV_GHOSTTY_PINNED_REVISION}, resolved '${ADHDEV_GHOSTTY_SOURCE_REVISION}' (clean=${ADHDEV_GHOSTTY_SOURCE_CLEAN})\n"
157
+ " zig pin ${ADHDEV_PINNED_ZIG_VERSION} (toolchain.json), found '${ADHDEV_ZIG_VERSION}'\n"
158
+ "Fix the toolchain/source, or set ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD=1 for a local NON-RELEASE build "
159
+ "(the prebuilt release gates refuse it).")
160
+ endif()
161
+ message(WARNING "ghostty-vt-node: NON-RELEASE build (${ADHDEV_PROVENANCE_PROBLEMS})")
162
+ set(ADHDEV_GHOSTTY_RELEASE_ELIGIBLE 0)
163
+ else()
164
+ set(ADHDEV_GHOSTTY_RELEASE_ELIGIBLE 1)
165
+ endif()
166
+ string(REPLACE ";" "," ADHDEV_PROVENANCE_PROBLEMS_TEXT "${ADHDEV_PROVENANCE_PROBLEMS}")
167
+ message(STATUS "ghostty-vt-node provenance: ghostty ${ADHDEV_GHOSTTY_SOURCE_REVISION} clean=${ADHDEV_GHOSTTY_SOURCE_CLEAN} zig ${ADHDEV_ZIG_VERSION} release=${ADHDEV_GHOSTTY_RELEASE_ELIGIBLE}")
168
+
169
+ # --- Runtime freshness --------------------------------------------------------
170
+ # HEAD/status/zig checks describe the INPUTS; the runtime must also be built
171
+ # from them and nothing else. Everything zig reads or writes besides the
172
+ # verified source and the pinned zig lives in this build tree:
173
+ # ghostty-zig/global-cache zig's global cache, incl. fetched packages (p/)
174
+ # ghostty-zig/local-cache zig's local cache (compiled artifacts)
175
+ # ghostty-zig/prefix the install prefix the addon links and ships
176
+ # They are passed as --global-cache-dir / --cache-dir / --prefix, which take
177
+ # precedence over ZIG_GLOBAL_CACHE_DIR / ZIG_LOCAL_CACHE_DIR, so ambient caches
178
+ # are never read: overriding them (rather than refusing a build with them set)
179
+ # keeps CI and developer environments working while removing their influence.
180
+ # Zig accepts an existing package directory in a cache without re-hashing it,
181
+ # and reuses cache hits as-is, so for a RELEASE build all three are deleted at
182
+ # every configure: dependencies are fetched afresh (zig verifies each package
183
+ # hash on fetch) and everything is compiled from scratch. A non-release
184
+ # (ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD) build keeps them between configures.
185
+ #
186
+ # adhdev_ghostty_runtime runs `zig build` on EVERY build, before the addon links
187
+ # and copies the runtime, so a library left in the prefix is re-installed from
188
+ # this build's cache; for a release build it re-verifies the pinned, clean
189
+ # source before and after.
190
+ #
191
+ # LIMITATION: these checks are check-then-use, not atomic. A release build
192
+ # assumes exclusive ownership of the build tree, the ghostty clone and the zig
193
+ # installation for the whole configure + build: a concurrent writer could
194
+ # change an input between a check and its use. Build release prebuilts on a
195
+ # machine and in directories no other process writes to.
196
+ set(ADHDEV_GHOSTTY_ZIG_DIR "${CMAKE_CURRENT_BINARY_DIR}/ghostty-zig")
197
+ set(ADHDEV_ZIG_GLOBAL_CACHE "${ADHDEV_GHOSTTY_ZIG_DIR}/global-cache")
198
+ set(ADHDEV_ZIG_LOCAL_CACHE "${ADHDEV_GHOSTTY_ZIG_DIR}/local-cache")
199
+ set(ADHDEV_GHOSTTY_PREFIX "${ADHDEV_GHOSTTY_ZIG_DIR}/prefix")
200
+ if(ADHDEV_GHOSTTY_RELEASE_ELIGIBLE)
201
+ # Not file(REMOVE_RECURSE): on Windows it stops part-way on names Win32
202
+ # normalizes away (zig extracts macOS `._.` files into the package cache),
203
+ # silently keeping cached packages. scripts/clean-dir.cjs removes them, and a
204
+ # clean that leaves anything behind fails the configure; a release build never
205
+ # proceeds on a partially emptied cache.
206
+ execute_process(
207
+ COMMAND "${ADHDEV_NODE_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/clean-dir.cjs" "${ADHDEV_GHOSTTY_ZIG_DIR}"
208
+ RESULT_VARIABLE adhdev_clean_result
209
+ OUTPUT_VARIABLE adhdev_clean_output
210
+ ERROR_VARIABLE adhdev_clean_output
211
+ )
212
+ if(NOT adhdev_clean_result EQUAL 0 OR EXISTS "${ADHDEV_GHOSTTY_ZIG_DIR}")
213
+ message(FATAL_ERROR
214
+ "ghostty-vt-node: could not empty the private zig caches in ${ADHDEV_GHOSTTY_ZIG_DIR} "
215
+ "(exit ${adhdev_clean_result}): ${adhdev_clean_output}\n"
216
+ "A release build must start from empty caches. Remove the directory by hand, "
217
+ "and make sure no other process is using it.")
218
+ endif()
219
+ endif()
220
+ file(MAKE_DIRECTORY "${ADHDEV_ZIG_GLOBAL_CACHE}" "${ADHDEV_ZIG_LOCAL_CACHE}" "${ADHDEV_GHOSTTY_PREFIX}/include")
221
+ if(DEFINED ENV{ZIG_GLOBAL_CACHE_DIR} OR DEFINED ENV{ZIG_LOCAL_CACHE_DIR})
222
+ message(STATUS "ghostty-vt-node: ZIG_GLOBAL_CACHE_DIR / ZIG_LOCAL_CACHE_DIR are ignored; zig uses the private caches in ${ADHDEV_GHOSTTY_ZIG_DIR}")
223
+ endif()
224
+
225
+ # Upstream's zig flags: build types with optimization map to ReleaseFast.
226
+ set(ADHDEV_GHOSTTY_ZIG_FLAGS -Demit-lib-vt ${GHOSTTY_ZIG_BUILD_FLAGS})
227
+ string(TOUPPER "${CMAKE_BUILD_TYPE}" adhdev_build_type)
228
+ if(adhdev_build_type STREQUAL "RELEASE" OR adhdev_build_type STREQUAL "MINSIZEREL" OR adhdev_build_type STREQUAL "RELWITHDEBINFO")
229
+ list(APPEND ADHDEV_GHOSTTY_ZIG_FLAGS "-Doptimize=ReleaseFast")
230
+ endif()
231
+
232
+ # The library names upstream's CMake wrapper uses at the pinned revision.
233
+ if(APPLE)
234
+ set(GHOSTTY_VT_SONAME "libghostty-vt.0.dylib")
235
+ set(GHOSTTY_VT_SHARED_LIBRARY "${ADHDEV_GHOSTTY_PREFIX}/lib/libghostty-vt.0.1.0.dylib")
236
+ elseif(WIN32)
237
+ set(GHOSTTY_VT_SHARED_LIBRARY "${ADHDEV_GHOSTTY_PREFIX}/bin/ghostty-vt.dll")
238
+ set(GHOSTTY_VT_IMPLIB "${ADHDEV_GHOSTTY_PREFIX}/lib/ghostty-vt.lib")
239
+ else()
240
+ set(GHOSTTY_VT_SONAME "libghostty-vt.so.0")
241
+ set(GHOSTTY_VT_SHARED_LIBRARY "${ADHDEV_GHOSTTY_PREFIX}/lib/libghostty-vt.so.0.1.0")
242
+ endif()
243
+
244
+ if(ADHDEV_GHOSTTY_RELEASE_ELIGIBLE)
245
+ set(ADHDEV_VERIFY_GHOSTTY_COMMAND
246
+ "${CMAKE_COMMAND}" "-DGIT=${GIT_EXECUTABLE}" "-DSRC=${ghostty_SOURCE_DIR}"
247
+ "-DPIN=${ADHDEV_GHOSTTY_PINNED_REVISION}" -P "${ADHDEV_VERIFY_GHOSTTY_SCRIPT}")
248
+ else()
249
+ set(ADHDEV_VERIFY_GHOSTTY_COMMAND "${CMAKE_COMMAND}" -E echo "ghostty-vt-node: non-release build, source not re-verified")
250
+ endif()
251
+ add_custom_target(adhdev_ghostty_runtime
252
+ COMMAND ${ADHDEV_VERIFY_GHOSTTY_COMMAND}
253
+ COMMAND "${ZIG_EXECUTABLE}" build ${ADHDEV_GHOSTTY_ZIG_FLAGS}
254
+ --prefix "${ADHDEV_GHOSTTY_PREFIX}"
255
+ --cache-dir "${ADHDEV_ZIG_LOCAL_CACHE}"
256
+ --global-cache-dir "${ADHDEV_ZIG_GLOBAL_CACHE}"
257
+ COMMAND ${ADHDEV_VERIFY_GHOSTTY_COMMAND}
258
+ BYPRODUCTS "${GHOSTTY_VT_SHARED_LIBRARY}" ${GHOSTTY_VT_IMPLIB}
259
+ WORKING_DIRECTORY "${ghostty_SOURCE_DIR}"
260
+ COMMENT "Building libghostty-vt with zig (always; private caches and prefix)"
261
+ VERBATIM
262
+ )
263
+
264
+ add_library(ghostty-vt SHARED IMPORTED GLOBAL)
265
+ set_target_properties(ghostty-vt PROPERTIES
266
+ IMPORTED_LOCATION "${GHOSTTY_VT_SHARED_LIBRARY}"
267
+ INTERFACE_INCLUDE_DIRECTORIES "${ADHDEV_GHOSTTY_PREFIX}/include"
268
+ )
269
+ if(APPLE)
270
+ set_target_properties(ghostty-vt PROPERTIES IMPORTED_SONAME "@rpath/${GHOSTTY_VT_SONAME}")
271
+ elseif(WIN32)
272
+ set_target_properties(ghostty-vt PROPERTIES IMPORTED_IMPLIB "${GHOSTTY_VT_IMPLIB}")
273
+ else()
274
+ set_target_properties(ghostty-vt PROPERTIES IMPORTED_SONAME "${GHOSTTY_VT_SONAME}")
275
+ endif()
276
+ add_dependencies(ghostty-vt adhdev_ghostty_runtime)
277
+
30
278
  add_library(${PROJECT_NAME} SHARED
31
279
  src/addon.cc
32
280
  src/ghostty_bridge.c
33
281
  )
282
+ add_dependencies(${PROJECT_NAME} adhdev_ghostty_runtime)
34
283
 
35
284
  set(GHOSTTY_VT_RUNTIME_ALIAS "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}")
36
285
 
@@ -39,9 +288,44 @@ target_include_directories(${PROJECT_NAME} PRIVATE
39
288
  ${NODE_ADDON_API_DIR}
40
289
  )
41
290
 
291
+ # Binding source digest, embedded so a compiled binary can be matched against
292
+ # the sources it was built from. scripts/binding-digest.mjs implements the same
293
+ # algorithm: per file "<path>:<sha256 of LF-normalized content>\n", in this
294
+ # order, then sha256 of the concatenation. Reconfigure whenever an input
295
+ # changes so an incremental compile never embeds a stale digest.
296
+ set(ADHDEV_BINDING_DIGEST_INPUTS
297
+ CMakeLists.txt
298
+ toolchain.json
299
+ src/addon.cc
300
+ src/ghostty_bridge.c
301
+ src/ghostty_bridge.h
302
+ scripts/clean-dir.cjs
303
+ )
304
+ set(ADHDEV_BINDING_DIGEST_MANIFEST "")
305
+ foreach(input IN LISTS ADHDEV_BINDING_DIGEST_INPUTS)
306
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/${input}" input_content)
307
+ string(REPLACE "\r\n" "\n" input_content "${input_content}")
308
+ string(SHA256 input_hash "${input_content}")
309
+ string(APPEND ADHDEV_BINDING_DIGEST_MANIFEST "${input}:${input_hash}\n")
310
+ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${input}")
311
+ endforeach()
312
+ string(SHA256 ADHDEV_BINDING_SOURCE_DIGEST "${ADHDEV_BINDING_DIGEST_MANIFEST}")
313
+ message(STATUS "ghostty-vt-node binding source digest: ${ADHDEV_BINDING_SOURCE_DIGEST}")
314
+
42
315
  target_compile_definitions(${PROJECT_NAME} PRIVATE
43
316
  NAPI_DISABLE_CPP_EXCEPTIONS
317
+ NAPI_VERSION=8
44
318
  GHOSTTY_STATIC
319
+ ADHDEV_GHOSTTY_SOURCE_DIGEST="${ADHDEV_BINDING_SOURCE_DIGEST}"
320
+ ADHDEV_GHOSTTY_GIT_TAG="${GHOSTTY_GIT_TAG}"
321
+ ADHDEV_GHOSTTY_RELEASE_ELIGIBLE=${ADHDEV_GHOSTTY_RELEASE_ELIGIBLE}
322
+ ADHDEV_GHOSTTY_PROVENANCE_PROBLEMS="${ADHDEV_PROVENANCE_PROBLEMS_TEXT}"
323
+ ADHDEV_GHOSTTY_SOURCE_REVISION="${ADHDEV_GHOSTTY_SOURCE_REVISION}"
324
+ ADHDEV_GHOSTTY_SOURCE_CLEAN=${ADHDEV_GHOSTTY_SOURCE_CLEAN}
325
+ ADHDEV_GHOSTTY_ZIG_VERSION="${ADHDEV_ZIG_VERSION}"
326
+ ADHDEV_GHOSTTY_C_COMPILER="${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}"
327
+ ADHDEV_GHOSTTY_CXX_COMPILER="${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}"
328
+ ADHDEV_GHOSTTY_BUILD_TYPE="${CMAKE_BUILD_TYPE}"
45
329
  )
46
330
 
47
331
  target_link_libraries(${PROJECT_NAME} PRIVATE
package/README.md CHANGED
@@ -52,28 +52,170 @@ Unable to load @adhdev/ghostty-vt-node native binding for running triplet
52
52
  linux-x64-node137, win32-x64-node137]. ...
53
53
  ```
54
54
 
55
+ ## Viewport / cursor / cell reads (API level 2)
56
+
57
+ Each terminal handle also exposes an exact-read API. All rows and the cursor are
58
+ in an explicit coordinate space; nothing is derived from formatter strings.
59
+
60
+ - `getViewport({ includeRows: 'none' | 'cursor' | 'all', cells })` — one native
61
+ snapshot: `cursorRow`/`cursorCol` (ACTIVE space), `activeScreen`
62
+ (`'primary'`/`'alternate'`), `scrollbar` (`total`/`offset`/`len`), the derived
63
+ `scrollbackRows` / `activeOriginRow` / `viewportOriginRow` /
64
+ `viewportScrollbackOffset` / `cursorViewportRow`, a per-terminal `revision`,
65
+ and the requested ACTIVE rows. Metadata and rows come from the same call.
66
+ - `readRow(row, { space: 'active' | 'viewport', cells, expectedRevision })` — one
67
+ row with `wrapped` / `wrapContinuation`; with `cells`, exactly `cols` cells of
68
+ `{ col, text, width, wideKind }` (`text` is the full stored grapheme).
69
+ - `scrollViewport('top' | 'bottom' | delta)` — moves the VIEWPORT only; ACTIVE
70
+ rows and `cursorRow` do not change.
71
+
72
+ **ACTIVE vs VIEWPORT.** ACTIVE is the rows x cols area the cursor moves in.
73
+ VIEWPORT is what is visible and differs only while scrolled into history.
74
+ `formatPlainText()` output is SCREEN-origin (history + active, trailing blank
75
+ rows dropped): the cursor's row is at index `activeOriginRow + cursorRow` there,
76
+ not at `cursorRow`. The legacy `formatPlainText` / `formatVT` /
77
+ `getCursorPosition` outputs are unchanged (see `test/fixtures/legacy-golden.json`).
78
+
79
+ **Feature detection.** A binary built before API level 2 still loads and serves
80
+ the legacy methods but has no `getViewport`/`readRow`/`scrollViewport`. Check
81
+ `require('@adhdev/ghostty-vt-node').capabilities.viewport` (or `typeof
82
+ handle.getViewport === 'function'`) and treat exact reads as unavailable —
83
+ never fall back to guessing from formatter output. `bindingInfo` reports the
84
+ loaded binary's `apiLevel`, embedded `sourceDigest` and `ghosttyRevision`.
85
+
86
+ Every method on a terminal handle owns a share of the native state, so a method
87
+ taken off the handle (`const get = t.getViewport`) keeps working after the
88
+ handle is garbage-collected; the terminal is freed when the last of them is.
89
+ After `dispose()` every method throws `ERR_GHOSTTY_DISPOSED`.
90
+
55
91
  ## Producing / refreshing a prebuilt
56
92
 
57
93
  Build on the **target platform/arch** (cross-builds are not wired here) with the
58
- toolchain present (`cmake`, a C/C++ compiler, and `zig` for `libghostty`; set
59
- `ZIG_EXECUTABLE` if zig is not on `PATH`):
94
+ toolchain present (`cmake`, `git`, a C/C++ compiler, and the zig version pinned
95
+ in `toolchain.json` for `libghostty`; set `ZIG_EXECUTABLE` if zig is not on
96
+ `PATH`).
97
+
98
+ The configure step verifies the build's provenance and fails on any problem:
99
+ ghostty must come from the FetchContent-managed clone (a
100
+ `FETCHCONTENT_SOURCE_DIR_GHOSTTY` substitute or a `GHOSTTY_GIT_TAG` override is
101
+ refused) at the pinned revision with a clean tree; `zig version` must print
102
+ exactly the `toolchain.json` version (only surrounding whitespace is trimmed);
103
+ and `GHOSTTY_ZIG_BUILD_FLAGS` must be empty. Upstream's CMake wrapper is not
104
+ used: the `adhdev_ghostty_runtime` target runs `zig build` itself on every
105
+ build, with a private global cache, local cache and install prefix inside the
106
+ build tree (`build/ghostty-zig/`; passed as `--global-cache-dir`, `--cache-dir`
107
+ and `--prefix`, so `ZIG_GLOBAL_CACHE_DIR` / `ZIG_LOCAL_CACHE_DIR` are ignored),
108
+ and re-verifies the pinned, clean source before and after. For a release build
109
+ configure empties those caches first (with `scripts/clean-dir.cjs`, because
110
+ CMake's own removal stops on the `._.` files zig extracts on Windows; if
111
+ anything survives, configure fails), so every dependency is fetched afresh
112
+ (zig verifies its package hash on fetch) and everything is compiled from
113
+ scratch: a stale `zig-out/`, a reused build directory or a poisoned shared zig
114
+ cache cannot reach the shipped runtime. What this does NOT cover: the zig
115
+ installation itself (only its version is checked) and concurrent writers.
116
+ The checks are check-then-use, so a release build needs exclusive ownership of
117
+ its build tree, the ghostty clone and the zig installation for the whole
118
+ configure + build; build release prebuilts where no other process writes to
119
+ them. For local experiments only, `ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD=1` turns
120
+ a failure into a warning, and ALWAYS makes the build non-release: the binary
121
+ reports `releaseEligible: false` with its problem codes (at least
122
+ `unverified-dev-build`), and every release gate refuses it (`non-release`). The
123
+ addon also records the zig version, C/C++ compiler id and version, and build
124
+ type (`getBindingInfo()`, copied into the manifest).
125
+ `scripts/probe-build-provenance.mjs <dir>` exercises all of this against a
126
+ configured build in `<dir>/build`: stale runtime, poisoned ambient and private
127
+ zig caches (including a control showing the poison breaks a build that keeps
128
+ them), reconfigure, source edit, dev override, zig output parsing and source
129
+ override. It rebuilds twice from empty caches, so it needs network access and
130
+ about 1 GiB of free disk. Two fresh builds differ only in per-build debug
131
+ metadata (cache object paths, mtimes, image id, signature), so it compares
132
+ runtimes from different builds by their loaded sections.
60
133
 
61
134
  ```bash
62
- # Build for the running platform-arch and emit it into prebuilt/<triplet>/
63
- ADHDEV_GHOSTTY_VT_EMIT_PREBUILT=1 npm run build -w oss/packages/ghostty-vt-node
135
+ # Compile from source and emit into the committed prebuilt for this
136
+ # platform-arch (prebuilt/<platform>-<arch>-node137/, whatever Node version runs
137
+ # the build), stamping prebuilt/<triplet>/manifest.json from the built binary:
138
+ ADHDEV_GHOSTTY_VT_FORCE_COMPILE=1 ADHDEV_GHOSTTY_VT_EMIT_PREBUILT=1 npm run build -w oss/packages/ghostty-vt-node
139
+
140
+ # Or, after any build: copy build/Release into the committed triplet + stamp
141
+ node oss/packages/ghostty-vt-node/scripts/prebuilt-manifest.mjs emit
64
142
 
65
- # Then keep the node137-named dir (per ABI-portability) and commit it:
66
- # git add oss/packages/ghostty-vt-node/prebuilt/<platform>-<arch>-node137/
143
+ # Verify exactly that directory, then the whole set
144
+ node oss/packages/ghostty-vt-node/scripts/verify-prebuilt.mjs oss/packages/ghostty-vt-node/prebuilt/<triplet>
145
+ node oss/packages/ghostty-vt-node/scripts/check-prebuilts.mjs
67
146
  ```
68
147
 
69
- Because the output is ABI-portable, you only need to build **once per
70
- platform/arch**; rename/keep the directory as the `node137` triplet and the
71
- runtime fallback covers all other Node versions on that platform.
148
+ With `ADHDEV_GHOSTTY_VT_FORCE_COMPILE=1` the build is a clean `cmake-js
149
+ rebuild` (fresh configure and clone); `build/` is removed with
150
+ `scripts/clean-dir.cjs` first, and the build stops if that fails. Without it, `npm run build` keeps an
151
+ existing local build or copies a usable prebuilt, or compiles incrementally
152
+ (`--plan` prints which it would do).
153
+
154
+ Binaries embed absolute build paths (the private zig caches live in the build
155
+ tree), so a prebuilt you commit must be built outside any home directory, from
156
+ a fresh clone of the commit being stamped. Copy the package to a neutral
157
+ directory and point `HOME` there for cmake-js' Node headers (shown for
158
+ macOS/Linux; on Windows use e.g. `C:\gvn` and `C:\gvn-home` for
159
+ `HOME`/`USERPROFILE`, with zig installed outside the user profile). The zig
160
+ caches need no setup: the build always uses fresh private ones. Do not set
161
+ `FETCHCONTENT_SOURCE_DIR_GHOSTTY`, `GHOSTTY_GIT_TAG`, `GHOSTTY_ZIG_BUILD_FLAGS`
162
+ or `ADHDEV_GHOSTTY_UNVERIFIED_DEV_BUILD`, and let nothing else write to these
163
+ directories during the build. On Windows, remove a previous `C:\gvn` with
164
+ `node oss\packages\ghostty-vt-node\scripts\clean-dir.cjs C:\gvn`: `rmdir /s`
165
+ and CMake cannot delete the `._.` files an earlier build's zig cache holds.
166
+
167
+ ```bash
168
+ rm -rf /tmp/gvn /tmp/gvn-home && mkdir -p /tmp/gvn/node_modules /tmp/gvn-home
169
+ rsync -a --exclude node_modules --exclude prebuilt --exclude build oss/packages/ghostty-vt-node/ /tmp/gvn/
170
+ cp -R oss/node_modules/node-addon-api /tmp/gvn/node_modules/ # or the package-local copy
171
+ (cd /tmp/gvn && HOME=/tmp/gvn-home node "$OLDPWD/oss/node_modules/cmake-js/bin/cmake-js" rebuild)
172
+ node oss/packages/ghostty-vt-node/scripts/probe-build-provenance.mjs /tmp/gvn # optional, expect 13/13
173
+ node oss/packages/ghostty-vt-node/scripts/prebuilt-manifest.mjs emit /tmp/gvn/build/Release
174
+ node oss/packages/ghostty-vt-node/scripts/check-prebuilts.mjs
175
+ ```
72
176
 
73
177
  - macOS arm64 and Linux x64 can be produced on the corresponding native runner.
74
178
  - Windows x64 must be built on a Windows runner (MSVC; see the `if(MSVC)` block in
75
179
  `CMakeLists.txt` for the required dynamic-CRT link flags).
76
180
 
181
+ ### Release-safety gate (`scripts/check-prebuilts.mjs`)
182
+
183
+ CMake embeds a **binding source digest** (sha256 over `CMakeLists.txt`,
184
+ `toolchain.json` and `src/*`, LF-normalized; `scripts/binding-digest.mjs`
185
+ reproduces it) and the build provenance into the addon. Each shipped prebuilt
186
+ carries a `manifest.json` (schema 2) stamped from the binary itself plus file
187
+ hashes. The gate fails unless:
188
+
189
+ - (a) all three shipped triplets carry a manifest matching the current digest,
190
+ API level, ghostty pin and zig pin, with matching file hashes, stamped as a
191
+ release build from the pinned, clean ghostty checkout. A triplet built from
192
+ older sources, another zig, or an older manifest schema reports
193
+ **pending-rebuild**: after any bridge/addon/toolchain change the tree is not
194
+ shippable until every platform is rebuilt and re-stamped. Without loading
195
+ anything (release CI runs on Linux), the manifest's platform/arch must match
196
+ the directory, the platform's runtime set must be complete (darwin: addon +
197
+ `libghostty-vt.dylib` + `libghostty-vt.X.Y.Z.dylib`; linux: addon +
198
+ `libghostty-vt.so`, `.so.0`, `.so.0.X.Y`; win32: addon + `ghostty-vt.dll`),
199
+ and every file must be a thin Mach-O arm64 / ELF x86-64 (SONAME
200
+ `libghostty-vt.so.0`, NEEDed by the addon) / PE32+ x64 DLL
201
+ (`scripts/native-format.mjs`).
202
+ - (b) the host platform's committed prebuilt, loaded by path, embeds the
203
+ current digest and release provenance, and its viewport API behaves.
204
+ - (c) the native CI build compiles from source (`ADHDEV_GHOSTTY_VT_FORCE_COMPILE`)
205
+ as a clean `cmake-js rebuild` with the zig read from `toolchain.json` and no
206
+ restored zig cache (`setup-zig` `use-cache: false`, no `ZIG_*_CACHE_DIR`),
207
+ verifies + stamps the exact staged
208
+ artifact, and the release job checks every prebuilt it publishes
209
+ (`--all-present`).
210
+ - (d) no shipped native file (`.node`/`.dylib`/`.so*`/`.dll`) embeds a
211
+ home-directory path (`/Users/<name>/`, `/home/<name>/`, `X:\Users\<name>\`),
212
+ scanned as raw bytes in ASCII and UTF-16LE (`scripts/host-path-scan.mjs`; CI
213
+ accounts `runner`/`runneradmin` are allowed). Build prebuilts from a neutral
214
+ directory with a neutral zig cache (`ZIG_GLOBAL_CACHE_DIR`). Extra literals
215
+ (hostnames) can be denied from an untracked file named by
216
+ `ADHDEV_GHOSTTY_HOST_PATH_DENYLIST` or `.host-path-denylist` in this package.
217
+ `verify-prebuilt.mjs` runs the same scan on the directory it verifies.
218
+
77
219
  ## Skipping the native build
78
220
 
79
221
  Set `ADHDEV_SKIP_GHOSTTY_VT_BUILD=1` to skip compilation when no prebuilt matches
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ // Pure candidate-ordering logic for the native binding loader (index.js).
3
+ // No native loading happens here so the selection rules are unit-testable with
4
+ // fake directory listings.
5
+ //
6
+ // Selection rule for prebuilt triplet directories of the host platform-arch:
7
+ // 1. highest manifest `apiLevel` first (a directory without a readable
8
+ // manifest counts as level 0 — never preferred over a manifest-bearing
9
+ // one, which is how a stale pre-API-2 binary loses to the current one even
10
+ // when its Node ABI number matches the running Node exactly),
11
+ // 2. then the exact running ABI (`...-node<abi>`),
12
+ // 3. then the higher ABI number, then name, for determinism.
13
+ // Foreign platform/arch directories (test-isolation fallback) come last.
14
+
15
+ function parseAbi(name) {
16
+ const match = /-node(\d+)$/.exec(name);
17
+ return match ? Number(match[1]) : -1;
18
+ }
19
+
20
+ function orderPrebuiltTriplets({ names, platform, arch, abi, readApiLevel }) {
21
+ const prefix = `${platform}-${arch}-`;
22
+ const exact = `${platform}-${arch}-node${abi}`;
23
+ const level = (name) => {
24
+ const value = readApiLevel(name);
25
+ return Number.isFinite(value) ? value : 0;
26
+ };
27
+ const rank = (list) =>
28
+ list
29
+ .map((name) => ({ name, level: level(name), exact: name === exact ? 1 : 0, abi: parseAbi(name) }))
30
+ .sort((a, b) => b.level - a.level || b.exact - a.exact || b.abi - a.abi || a.name.localeCompare(b.name));
31
+ const same = names.filter((name) => name.startsWith(prefix));
32
+ const other = names.filter((name) => !name.startsWith(prefix));
33
+ return {
34
+ samePlatform: rank(same).map((entry) => entry.name),
35
+ bestApiLevel: same.length ? Math.max(...same.map(level)) : 0,
36
+ otherPlatform: other.slice().sort(),
37
+ };
38
+ }
39
+
40
+ module.exports = { orderPrebuiltTriplets };
package/index.d.ts CHANGED
@@ -8,6 +8,97 @@ export type GhosttyPlainTextFormatOptions = {
8
8
  trim?: boolean;
9
9
  };
10
10
 
11
+ /**
12
+ * Coordinate space of a row index.
13
+ * - `active`: the rows x cols area the cursor moves in. `cursorRow` is in this
14
+ * space, and it does not move when the user scrolls through history.
15
+ * - `viewport`: the visible area. Equal to `active` unless the viewport is
16
+ * scrolled into history (`viewportScrollbackOffset > 0`).
17
+ */
18
+ export type GhosttyCoordinateSpace = 'active' | 'viewport';
19
+
20
+ export type GhosttyCellWideKind = 'narrow' | 'wide' | 'spacer-tail' | 'spacer-head';
21
+
22
+ export type GhosttyCell = {
23
+ /** Physical cell column, 0-based. */
24
+ col: number;
25
+ /** The cell's full grapheme cluster as stored natively; '' for empty cells and spacers. */
26
+ text: string;
27
+ /** 2 for a wide lead cell, 0 for spacers, 1 otherwise. */
28
+ width: 0 | 1 | 2;
29
+ wideKind: GhosttyCellWideKind;
30
+ };
31
+
32
+ export type GhosttyRow = {
33
+ /** Row index, 0-based, in `coordinateSpace`. */
34
+ row: number;
35
+ coordinateSpace: GhosttyCoordinateSpace;
36
+ /**
37
+ * Row text. Empty cells before content read as U+0020; the trailing run of
38
+ * content-less cells is omitted; spacer cells contribute nothing. For
39
+ * readability only — `cells` is the authority for columns.
40
+ */
41
+ text: string;
42
+ /** This row soft-wraps into the next physical row. */
43
+ wrapped: boolean;
44
+ /** This row continues a soft-wrap from the previous physical row. */
45
+ wrapContinuation: boolean;
46
+ /** Exactly `cols` entries, present when requested. */
47
+ cells?: GhosttyCell[];
48
+ };
49
+
50
+ export type GhosttyViewportOptions = {
51
+ /** Rows to include: none (default), the cursor's row, or all ACTIVE rows. */
52
+ includeRows?: 'none' | 'cursor' | 'all';
53
+ cells?: boolean;
54
+ };
55
+
56
+ export type GhosttyScrollbar = {
57
+ /** Rows in the selected screen: retained history + active area. */
58
+ total: number;
59
+ /** Row of `total` at which the visible viewport starts. */
60
+ offset: number;
61
+ /** Visible rows. */
62
+ len: number;
63
+ };
64
+
65
+ /** One native snapshot; every field describes the same terminal state. */
66
+ export type GhosttyViewport = {
67
+ /** Per-terminal mutation counter (write/resize/scrollViewport). */
68
+ revision: number;
69
+ /** Space of `cursorRow` and of every entry in `lines`. Always 'active'. */
70
+ coordinateSpace: 'active';
71
+ rows: number;
72
+ cols: number;
73
+ cursorRow: number;
74
+ cursorCol: number;
75
+ cursorPendingWrap: boolean;
76
+ /** DEC mode 25; says nothing about whether the cursor is in the viewport. */
77
+ cursorVisible: boolean;
78
+ activeScreen: 'primary' | 'alternate';
79
+ scrollbar: GhosttyScrollbar;
80
+ /** Retained history rows of the selected screen (scrollbar.total - scrollbar.len). */
81
+ scrollbackRows: number;
82
+ /** Screen row at which the ACTIVE area starts. */
83
+ activeOriginRow: number;
84
+ /** Screen row at which the VIEWPORT starts (scrollbar.offset). */
85
+ viewportOriginRow: number;
86
+ /** How many rows the viewport is scrolled up from the active area. */
87
+ viewportScrollbackOffset: number;
88
+ /** Cursor row in VIEWPORT space, or null when the cursor is scrolled out of view. */
89
+ cursorViewportRow: number | null;
90
+ /** ACTIVE rows: all of them, or just the cursor's row, per `includeRows`. */
91
+ lines?: GhosttyRow[];
92
+ };
93
+
94
+ export type GhosttyReadRowOptions = {
95
+ cells?: boolean;
96
+ /** Defaults to 'active'. */
97
+ space?: GhosttyCoordinateSpace;
98
+ /** Throw ERR_GHOSTTY_STALE_REVISION unless the terminal is still at this revision. */
99
+ expectedRevision?: number;
100
+ };
101
+
11
102
  export interface GhosttyTerminalHandle {
12
103
  write(data: string | Uint8Array): void;
13
104
  resize(cols: number, rows: number): void;
@@ -15,6 +106,22 @@ export interface GhosttyTerminalHandle {
15
106
  formatVT(): string;
16
107
  getCursorPosition(): { col: number; row: number };
17
108
  dispose(): void;
109
+ // API level 2. Absent on older binaries: check `capabilities.viewport`.
110
+ getViewport?(options?: GhosttyViewportOptions): GhosttyViewport;
111
+ readRow?(row: number, options?: GhosttyReadRowOptions): GhosttyRow & { revision: number };
112
+ scrollViewport?(target: 'top' | 'bottom' | number): void;
18
113
  }
19
114
 
115
+ export type GhosttyBindingInfo = {
116
+ /** 1 for binaries that predate getBindingInfo. */
117
+ apiLevel: number;
118
+ /** Binding source digest embedded at compile time (scripts/binding-digest.mjs). */
119
+ sourceDigest: string | null;
120
+ ghosttyRevision: string | null;
121
+ napiVersion: number | null;
122
+ };
123
+
20
124
  export function createTerminal(options: GhosttyTerminalOptions): GhosttyTerminalHandle;
125
+
126
+ export const bindingInfo: GhosttyBindingInfo;
127
+ export const capabilities: Readonly<{ viewport: boolean }>;