History

feat(mobile): Add terminal powered by libghostty (#2056)

Julius Marminge committed 7 months ago

Commit da6ea881 parent d68661384 files · +10509 −65Browse files

Unified · Split

84 files
  1. apps/mobile/app.config.ts +8 −0
  2. apps/mobile/modules/t3-terminal/README.md +25 −0
  3. apps/mobile/modules/t3-terminal/T3TerminalNative.podspec +21 −0
  4. apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md +15 −0
  5. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/Info.plist +47 −0
  6. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty.h +1187 −0
  7. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt.h +87 −0
  8. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/allocator.h +196 −0
  9. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/color.h +96 −0
  10. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/key.h +80 −0
  11. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/key/encoder.h +221 −0
  12. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/key/event.h +474 −0
  13. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/osc.h +233 −0
  14. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/paste.h +75 −0
  15. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/result.h +22 −0
  16. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/sgr.h +394 −0
  17. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/wasm.h +159 −0
  18. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/module.modulemap +7 −0
  19. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a +0 −0
  20. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty.h +1187 −0
  21. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt.h +87 −0
  22. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/allocator.h +196 −0
  23. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/color.h +96 −0
  24. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/key.h +80 −0
  25. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/key/encoder.h +221 −0
  26. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/key/event.h +474 −0
  27. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/osc.h +233 −0
  28. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/paste.h +75 −0
  29. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/result.h +22 −0
  30. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/sgr.h +394 −0
  31. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/wasm.h +159 −0
  32. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/module.modulemap +7 −0
  33. apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a +0 −0
  34. apps/mobile/modules/t3-terminal/Vendor/libghostty/VERSION +1 −0
  35. apps/mobile/modules/t3-terminal/android/build.gradle +19 −0
  36. apps/mobile/modules/t3-terminal/android/src/main/AndroidManifest.xml +1 −0
  37. apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt +46 −0
  38. apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt +210 −0
  39. apps/mobile/modules/t3-terminal/expo-module.config.json +10 −0
  40. apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift +43 −0
  41. apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift +565 −0
  42. apps/mobile/modules/t3-terminal/package.json +17 −0
  43. apps/mobile/package.json +4 −0
  44. apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +10 −0
  45. apps/mobile/src/app/threads/[environmentId]/[threadId]/terminal.tsx +5 −0
  46. apps/mobile/src/features/threads/ThreadGitControls.tsx +63 −1
  47. apps/mobile/src/features/threads/ThreadRouteScreen.tsx +114 −10
  48. apps/mobile/src/features/threads/ThreadTerminalPanel.tsx +170 −0
  49. apps/mobile/src/features/threads/ThreadTerminalRouteScreen.test.ts +66 −0
  50. apps/mobile/src/features/threads/ThreadTerminalRouteScreen.tsx +734 −0
  51. apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +5 −3
  52. apps/mobile/src/features/threads/git/GitCommitSheet.tsx +4 −2
  53. apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +5 −3
  54. apps/mobile/src/features/threads/review/ReviewSheet.tsx +3 −3
  55. apps/mobile/src/features/threads/terminalLaunchContext.test.ts +61 −0
  56. apps/mobile/src/features/threads/terminalLaunchContext.ts +39 −0
  57. apps/mobile/src/features/threads/terminalMenu.test.ts +128 −0
  58. apps/mobile/src/features/threads/terminalMenu.ts +139 −0
  59. apps/mobile/src/features/threads/terminalPreferences.test.ts +28 −0
  60. apps/mobile/src/features/threads/terminalPreferences.ts +12 −0
  61. apps/mobile/src/features/threads/terminalRouteBootstrap.ts +25 −0
  62. apps/mobile/src/features/threads/use-selected-thread-git-actions.ts +17 −11
  63. apps/mobile/src/features/threads/use-selected-thread-git-state.ts +4 −6
  64. apps/mobile/src/features/threads/use-selected-thread-worktree.ts +24 −0
  65. apps/mobile/src/lib/routes.ts +12 −0
  66. apps/mobile/src/lib/storage.ts +48 −10
  67. apps/mobile/src/native/terminal/NativeTerminalSurface.tsx +206 −0
  68. apps/mobile/src/native/terminal/nativeTerminalModule.test.ts +52 −0
  69. apps/mobile/src/native/terminal/nativeTerminalModule.ts +65 −0
  70. apps/mobile/src/native/terminal/terminalTheme.test.ts +36 −0
  71. apps/mobile/src/native/terminal/terminalTheme.ts +85 −0
  72. apps/mobile/src/state/use-remote-environment-registry.ts +6 −1
  73. apps/mobile/src/state/use-terminal-session.ts +63 −0
  74. apps/server/src/terminal/Layers/Manager.test.ts +41 −0
  75. apps/server/src/terminal/Layers/Manager.ts +24 −7
  76. apps/web/src/components/ThreadTerminalDrawer.tsx +15 −10
  77. apps/web/src/terminalActivity.ts +1 −0
  78. apps/web/src/terminalStateStore.test.ts +2 −0
  79. bun.lock +7 −0
  80. packages/client-runtime/src/index.ts +1 −0
  81. packages/client-runtime/src/terminalSessionState.test.ts +276 −0
  82. packages/client-runtime/src/terminalSessionState.ts +404 −0
  83. packages/contracts/src/terminal.test.ts +11 −0
  84. packages/contracts/src/terminal.ts +6 −0
apps/mobile/app.config.ts+8−0
@@ -114,6 +114,14 @@ const config: ExpoConfig = { · 113 unmodified lines
114114 },
115115 },
116116 ],
117+ [
118+ "expo-build-properties",
119+ {
120+ ios: {
121+ deploymentTarget: "16.1",
122+ },
123+ },
124+ ],
117125 "expo-secure-store",
118126 "expo-router",
119127 "./plugins/withAndroidCleartextTraffic.cjs",
apps/mobile/modules/t3-terminal/README.md+25−0
@@ -0,0 +1,25 @@
1+# T3 Mobile Terminal Native Module
2+
3+This local Expo module owns the native terminal surface for the mobile app.
4+
5+The JavaScript contract is intentionally small:
6+
7+- input from the native surface is emitted as `{ data: string }`
8+- resize from the native surface is emitted as `{ cols: number, rows: number }`
9+- remote PTY output is delivered by the existing `WsRpcClient.terminal` RPC stream
10+
11+The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the same custom-I/O
12+Ghostty fork used by VVTerm. `T3TerminalView` owns a `libghostty` surface and mirrors VVTerm's
13+custom I/O model:
14+
15+1. initialize libghostty once for the process
16+2. create one Ghostty app and surface per native view
17+3. feed remote output into the surface with `ghostty_surface_feed_data`
18+4. send user input back to JS with the write callback
19+5. emit Ghostty's measured terminal size through `onResize`
20+
21+Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the
22+React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a
23+future Android Ghostty build without changing JS.
24+
25+Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`.
apps/mobile/modules/t3-terminal/T3TerminalNative.podspec+21−0
@@ -0,0 +1,21 @@
1+require 'json'
2+
3+package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4+
5+Pod::Spec.new do |s|
6+ s.name = 'T3TerminalNative'
7+ s.version = package['version']
8+ s.summary = 'Native terminal surface for T3 Code mobile.'
9+ s.description = 'Native terminal surface bridge used by the T3 Code React Native app.'
10+ s.homepage = 'https://t3tools.com'
11+ s.license = { :type => 'UNLICENSED' }
12+ s.author = { 'T3 Tools' => 'hello@t3tools.com' }
13+ s.platforms = { :ios => '16.1' }
14+ s.source = { :path => '.' }
15+ s.source_files = 'ios/**/*.{h,m,mm,swift}'
16+ s.vendored_frameworks = 'Vendor/libghostty/GhosttyKit.xcframework'
17+ s.frameworks = 'IOSurface', 'Metal', 'MetalKit', 'QuartzCore', 'UIKit'
18+ s.libraries = 'c++', 'z'
19+ s.swift_version = '5.9'
20+ s.dependency 'ExpoModulesCore'
21+end
apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md+15−0
@@ -0,0 +1,15 @@
1+# Third-Party Notices
2+
3+## Ghostty / libghostty
4+
5+The iOS terminal renderer vendors `GhosttyKit.xcframework`, a libghostty build produced from
6+VVTerm's custom-I/O Ghostty fork.
7+
8+- Upstream project: https://github.com/ghostty-org/ghostty
9+- Vendored source fork: https://github.com/wiedymi/ghostty
10+- Vendored revision: `268a0a9d761fb19673f05d28042488e2002300f2`
11+- Reference integration: https://github.com/vivy-company/vvterm
12+- License: MIT
13+
14+Ghostty's MIT license applies to the vendored framework. Keep this notice in sync when updating
15+`Vendor/libghostty`.
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/Info.plist+47−0
@@ -0,0 +1,47 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+<plist version="1.0">
4+<dict>
5+ <key>AvailableLibraries</key>
6+ <array>
7+ <dict>
8+ <key>BinaryPath</key>
9+ <string>libghostty-fat.a</string>
10+ <key>HeadersPath</key>
11+ <string>Headers</string>
12+ <key>LibraryIdentifier</key>
13+ <string>ios-arm64</string>
14+ <key>LibraryPath</key>
15+ <string>libghostty-fat.a</string>
16+ <key>SupportedArchitectures</key>
17+ <array>
18+ <string>arm64</string>
19+ </array>
20+ <key>SupportedPlatform</key>
21+ <string>ios</string>
22+ </dict>
23+ <dict>
24+ <key>BinaryPath</key>
25+ <string>libghostty-fat.a</string>
26+ <key>HeadersPath</key>
27+ <string>Headers</string>
28+ <key>LibraryIdentifier</key>
29+ <string>ios-arm64-simulator</string>
30+ <key>LibraryPath</key>
31+ <string>libghostty-fat.a</string>
32+ <key>SupportedArchitectures</key>
33+ <array>
34+ <string>arm64</string>
35+ </array>
36+ <key>SupportedPlatform</key>
37+ <string>ios</string>
38+ <key>SupportedPlatformVariant</key>
39+ <string>simulator</string>
40+ </dict>
41+ </array>
42+ <key>CFBundlePackageType</key>
43+ <string>XFWK</string>
44+ <key>XCFrameworkFormatVersion</key>
45+ <string>1.0</string>
46+</dict>
47+</plist>
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty.h+1187−0
@@ -0,0 +1,1187 @@
1+// Ghostty embedding API. The documentation for the embedding API is
2+// only within the Zig source files that define the implementations. This
3+// isn't meant to be a general purpose embedding API (yet) so there hasn't
4+// been documentation or example work beyond that.
5+//
6+// The only consumer of this API is the macOS app, but the API is built to
7+// be more general purpose.
8+#ifndef GHOSTTY_H
9+#define GHOSTTY_H
10+
11+#ifdef __cplusplus
12+extern "C" {
13+#endif
14+
15+#include <stdbool.h>
16+#include <stddef.h>
17+#include <stdint.h>
18+#include <sys/types.h>
19+
20+//-------------------------------------------------------------------
21+// Macros
22+
23+#define GHOSTTY_SUCCESS 0
24+
25+//-------------------------------------------------------------------
26+// Types
27+
28+// Opaque types
29+typedef void* ghostty_app_t;
30+typedef void* ghostty_config_t;
31+typedef void* ghostty_surface_t;
32+typedef void* ghostty_inspector_t;
33+
34+// All the types below are fully defined and must be kept in sync with
35+// their Zig counterparts. Any changes to these types MUST have an associated
36+// Zig change.
37+typedef enum {
38+ GHOSTTY_PLATFORM_INVALID,
39+ GHOSTTY_PLATFORM_MACOS,
40+ GHOSTTY_PLATFORM_IOS,
41+} ghostty_platform_e;
42+
43+// Callback for custom I/O write handler.
44+typedef void (*ghostty_surface_write_fn)(void* userdata,
45+ const uint8_t* data,
46+ size_t len);
47+
48+typedef enum {
49+ GHOSTTY_CLIPBOARD_STANDARD,
50+ GHOSTTY_CLIPBOARD_SELECTION,
51+} ghostty_clipboard_e;
52+
53+typedef struct {
54+ const char *mime;
55+ const char *data;
56+} ghostty_clipboard_content_s;
57+
58+typedef enum {
59+ GHOSTTY_CLIPBOARD_REQUEST_PASTE,
60+ GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
61+ GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
62+} ghostty_clipboard_request_e;
63+
64+typedef enum {
65+ GHOSTTY_MOUSE_RELEASE,
66+ GHOSTTY_MOUSE_PRESS,
67+} ghostty_input_mouse_state_e;
68+
69+typedef enum {
70+ GHOSTTY_MOUSE_UNKNOWN,
71+ GHOSTTY_MOUSE_LEFT,
72+ GHOSTTY_MOUSE_RIGHT,
73+ GHOSTTY_MOUSE_MIDDLE,
74+ GHOSTTY_MOUSE_FOUR,
75+ GHOSTTY_MOUSE_FIVE,
76+ GHOSTTY_MOUSE_SIX,
77+ GHOSTTY_MOUSE_SEVEN,
78+ GHOSTTY_MOUSE_EIGHT,
79+ GHOSTTY_MOUSE_NINE,
80+ GHOSTTY_MOUSE_TEN,
81+ GHOSTTY_MOUSE_ELEVEN,
82+} ghostty_input_mouse_button_e;
83+
84+typedef enum {
85+ GHOSTTY_MOUSE_MOMENTUM_NONE,
86+ GHOSTTY_MOUSE_MOMENTUM_BEGAN,
87+ GHOSTTY_MOUSE_MOMENTUM_STATIONARY,
88+ GHOSTTY_MOUSE_MOMENTUM_CHANGED,
89+ GHOSTTY_MOUSE_MOMENTUM_ENDED,
90+ GHOSTTY_MOUSE_MOMENTUM_CANCELLED,
91+ GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
92+} ghostty_input_mouse_momentum_e;
93+
94+typedef enum {
95+ GHOSTTY_COLOR_SCHEME_LIGHT = 0,
96+ GHOSTTY_COLOR_SCHEME_DARK = 1,
97+} ghostty_color_scheme_e;
98+
99+// This is a packed struct (see src/input/mouse.zig) but the C standard
100+// afaik doesn't let us reliably define packed structs so we build it up
101+// from scratch.
102+typedef int ghostty_input_scroll_mods_t;
103+
104+typedef enum {
105+ GHOSTTY_MODS_NONE = 0,
106+ GHOSTTY_MODS_SHIFT = 1 << 0,
107+ GHOSTTY_MODS_CTRL = 1 << 1,
108+ GHOSTTY_MODS_ALT = 1 << 2,
109+ GHOSTTY_MODS_SUPER = 1 << 3,
110+ GHOSTTY_MODS_CAPS = 1 << 4,
111+ GHOSTTY_MODS_NUM = 1 << 5,
112+ GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6,
113+ GHOSTTY_MODS_CTRL_RIGHT = 1 << 7,
114+ GHOSTTY_MODS_ALT_RIGHT = 1 << 8,
115+ GHOSTTY_MODS_SUPER_RIGHT = 1 << 9,
116+} ghostty_input_mods_e;
117+
118+typedef enum {
119+ GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0,
120+ GHOSTTY_BINDING_FLAGS_ALL = 1 << 1,
121+ GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2,
122+ GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3,
123+} ghostty_binding_flags_e;
124+
125+typedef enum {
126+ GHOSTTY_ACTION_RELEASE,
127+ GHOSTTY_ACTION_PRESS,
128+ GHOSTTY_ACTION_REPEAT,
129+} ghostty_input_action_e;
130+
131+// Based on: https://www.w3.org/TR/uievents-code/
132+typedef enum {
133+ GHOSTTY_KEY_UNIDENTIFIED,
134+
135+ // "Writing System Keys" § 3.1.1
136+ GHOSTTY_KEY_BACKQUOTE,
137+ GHOSTTY_KEY_BACKSLASH,
138+ GHOSTTY_KEY_BRACKET_LEFT,
139+ GHOSTTY_KEY_BRACKET_RIGHT,
140+ GHOSTTY_KEY_COMMA,
141+ GHOSTTY_KEY_DIGIT_0,
142+ GHOSTTY_KEY_DIGIT_1,
143+ GHOSTTY_KEY_DIGIT_2,
144+ GHOSTTY_KEY_DIGIT_3,
145+ GHOSTTY_KEY_DIGIT_4,
146+ GHOSTTY_KEY_DIGIT_5,
147+ GHOSTTY_KEY_DIGIT_6,
148+ GHOSTTY_KEY_DIGIT_7,
149+ GHOSTTY_KEY_DIGIT_8,
150+ GHOSTTY_KEY_DIGIT_9,
151+ GHOSTTY_KEY_EQUAL,
152+ GHOSTTY_KEY_INTL_BACKSLASH,
153+ GHOSTTY_KEY_INTL_RO,
154+ GHOSTTY_KEY_INTL_YEN,
155+ GHOSTTY_KEY_A,
156+ GHOSTTY_KEY_B,
157+ GHOSTTY_KEY_C,
158+ GHOSTTY_KEY_D,
159+ GHOSTTY_KEY_E,
160+ GHOSTTY_KEY_F,
161+ GHOSTTY_KEY_G,
162+ GHOSTTY_KEY_H,
163+ GHOSTTY_KEY_I,
164+ GHOSTTY_KEY_J,
165+ GHOSTTY_KEY_K,
166+ GHOSTTY_KEY_L,
167+ GHOSTTY_KEY_M,
168+ GHOSTTY_KEY_N,
169+ GHOSTTY_KEY_O,
170+ GHOSTTY_KEY_P,
171+ GHOSTTY_KEY_Q,
172+ GHOSTTY_KEY_R,
173+ GHOSTTY_KEY_S,
174+ GHOSTTY_KEY_T,
175+ GHOSTTY_KEY_U,
176+ GHOSTTY_KEY_V,
177+ GHOSTTY_KEY_W,
178+ GHOSTTY_KEY_X,
179+ GHOSTTY_KEY_Y,
180+ GHOSTTY_KEY_Z,
181+ GHOSTTY_KEY_MINUS,
182+ GHOSTTY_KEY_PERIOD,
183+ GHOSTTY_KEY_QUOTE,
184+ GHOSTTY_KEY_SEMICOLON,
185+ GHOSTTY_KEY_SLASH,
186+
187+ // "Functional Keys" § 3.1.2
188+ GHOSTTY_KEY_ALT_LEFT,
189+ GHOSTTY_KEY_ALT_RIGHT,
190+ GHOSTTY_KEY_BACKSPACE,
191+ GHOSTTY_KEY_CAPS_LOCK,
192+ GHOSTTY_KEY_CONTEXT_MENU,
193+ GHOSTTY_KEY_CONTROL_LEFT,
194+ GHOSTTY_KEY_CONTROL_RIGHT,
195+ GHOSTTY_KEY_ENTER,
196+ GHOSTTY_KEY_META_LEFT,
197+ GHOSTTY_KEY_META_RIGHT,
198+ GHOSTTY_KEY_SHIFT_LEFT,
199+ GHOSTTY_KEY_SHIFT_RIGHT,
200+ GHOSTTY_KEY_SPACE,
201+ GHOSTTY_KEY_TAB,
202+ GHOSTTY_KEY_CONVERT,
203+ GHOSTTY_KEY_KANA_MODE,
204+ GHOSTTY_KEY_NON_CONVERT,
205+
206+ // "Control Pad Section" § 3.2
207+ GHOSTTY_KEY_DELETE,
208+ GHOSTTY_KEY_END,
209+ GHOSTTY_KEY_HELP,
210+ GHOSTTY_KEY_HOME,
211+ GHOSTTY_KEY_INSERT,
212+ GHOSTTY_KEY_PAGE_DOWN,
213+ GHOSTTY_KEY_PAGE_UP,
214+
215+ // "Arrow Pad Section" § 3.3
216+ GHOSTTY_KEY_ARROW_DOWN,
217+ GHOSTTY_KEY_ARROW_LEFT,
218+ GHOSTTY_KEY_ARROW_RIGHT,
219+ GHOSTTY_KEY_ARROW_UP,
220+
221+ // "Numpad Section" § 3.4
222+ GHOSTTY_KEY_NUM_LOCK,
223+ GHOSTTY_KEY_NUMPAD_0,
224+ GHOSTTY_KEY_NUMPAD_1,
225+ GHOSTTY_KEY_NUMPAD_2,
226+ GHOSTTY_KEY_NUMPAD_3,
227+ GHOSTTY_KEY_NUMPAD_4,
228+ GHOSTTY_KEY_NUMPAD_5,
229+ GHOSTTY_KEY_NUMPAD_6,
230+ GHOSTTY_KEY_NUMPAD_7,
231+ GHOSTTY_KEY_NUMPAD_8,
232+ GHOSTTY_KEY_NUMPAD_9,
233+ GHOSTTY_KEY_NUMPAD_ADD,
234+ GHOSTTY_KEY_NUMPAD_BACKSPACE,
235+ GHOSTTY_KEY_NUMPAD_CLEAR,
236+ GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
237+ GHOSTTY_KEY_NUMPAD_COMMA,
238+ GHOSTTY_KEY_NUMPAD_DECIMAL,
239+ GHOSTTY_KEY_NUMPAD_DIVIDE,
240+ GHOSTTY_KEY_NUMPAD_ENTER,
241+ GHOSTTY_KEY_NUMPAD_EQUAL,
242+ GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
243+ GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
244+ GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
245+ GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
246+ GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
247+ GHOSTTY_KEY_NUMPAD_MULTIPLY,
248+ GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
249+ GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
250+ GHOSTTY_KEY_NUMPAD_SUBTRACT,
251+ GHOSTTY_KEY_NUMPAD_SEPARATOR,
252+ GHOSTTY_KEY_NUMPAD_UP,
253+ GHOSTTY_KEY_NUMPAD_DOWN,
254+ GHOSTTY_KEY_NUMPAD_RIGHT,
255+ GHOSTTY_KEY_NUMPAD_LEFT,
256+ GHOSTTY_KEY_NUMPAD_BEGIN,
257+ GHOSTTY_KEY_NUMPAD_HOME,
258+ GHOSTTY_KEY_NUMPAD_END,
259+ GHOSTTY_KEY_NUMPAD_INSERT,
260+ GHOSTTY_KEY_NUMPAD_DELETE,
261+ GHOSTTY_KEY_NUMPAD_PAGE_UP,
262+ GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
263+
264+ // "Function Section" § 3.5
265+ GHOSTTY_KEY_ESCAPE,
266+ GHOSTTY_KEY_F1,
267+ GHOSTTY_KEY_F2,
268+ GHOSTTY_KEY_F3,
269+ GHOSTTY_KEY_F4,
270+ GHOSTTY_KEY_F5,
271+ GHOSTTY_KEY_F6,
272+ GHOSTTY_KEY_F7,
273+ GHOSTTY_KEY_F8,
274+ GHOSTTY_KEY_F9,
275+ GHOSTTY_KEY_F10,
276+ GHOSTTY_KEY_F11,
277+ GHOSTTY_KEY_F12,
278+ GHOSTTY_KEY_F13,
279+ GHOSTTY_KEY_F14,
280+ GHOSTTY_KEY_F15,
281+ GHOSTTY_KEY_F16,
282+ GHOSTTY_KEY_F17,
283+ GHOSTTY_KEY_F18,
284+ GHOSTTY_KEY_F19,
285+ GHOSTTY_KEY_F20,
286+ GHOSTTY_KEY_F21,
287+ GHOSTTY_KEY_F22,
288+ GHOSTTY_KEY_F23,
289+ GHOSTTY_KEY_F24,
290+ GHOSTTY_KEY_F25,
291+ GHOSTTY_KEY_FN,
292+ GHOSTTY_KEY_FN_LOCK,
293+ GHOSTTY_KEY_PRINT_SCREEN,
294+ GHOSTTY_KEY_SCROLL_LOCK,
295+ GHOSTTY_KEY_PAUSE,
296+
297+ // "Media Keys" § 3.6
298+ GHOSTTY_KEY_BROWSER_BACK,
299+ GHOSTTY_KEY_BROWSER_FAVORITES,
300+ GHOSTTY_KEY_BROWSER_FORWARD,
301+ GHOSTTY_KEY_BROWSER_HOME,
302+ GHOSTTY_KEY_BROWSER_REFRESH,
303+ GHOSTTY_KEY_BROWSER_SEARCH,
304+ GHOSTTY_KEY_BROWSER_STOP,
305+ GHOSTTY_KEY_EJECT,
306+ GHOSTTY_KEY_LAUNCH_APP_1,
307+ GHOSTTY_KEY_LAUNCH_APP_2,
308+ GHOSTTY_KEY_LAUNCH_MAIL,
309+ GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
310+ GHOSTTY_KEY_MEDIA_SELECT,
311+ GHOSTTY_KEY_MEDIA_STOP,
312+ GHOSTTY_KEY_MEDIA_TRACK_NEXT,
313+ GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
314+ GHOSTTY_KEY_POWER,
315+ GHOSTTY_KEY_SLEEP,
316+ GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
317+ GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
318+ GHOSTTY_KEY_AUDIO_VOLUME_UP,
319+ GHOSTTY_KEY_WAKE_UP,
320+
321+ // "Legacy, Non-standard, and Special Keys" § 3.7
322+ GHOSTTY_KEY_COPY,
323+ GHOSTTY_KEY_CUT,
324+ GHOSTTY_KEY_PASTE,
325+} ghostty_input_key_e;
326+
327+typedef struct {
328+ ghostty_input_action_e action;
329+ ghostty_input_mods_e mods;
330+ ghostty_input_mods_e consumed_mods;
331+ uint32_t keycode;
332+ const char* text;
333+ uint32_t unshifted_codepoint;
334+ bool composing;
335+} ghostty_input_key_s;
336+
337+typedef enum {
338+ GHOSTTY_TRIGGER_PHYSICAL,
339+ GHOSTTY_TRIGGER_UNICODE,
340+ GHOSTTY_TRIGGER_CATCH_ALL,
341+} ghostty_input_trigger_tag_e;
342+
343+typedef union {
344+ ghostty_input_key_e translated;
345+ ghostty_input_key_e physical;
346+ uint32_t unicode;
347+ // catch_all has no payload
348+} ghostty_input_trigger_key_u;
349+
350+typedef struct {
351+ ghostty_input_trigger_tag_e tag;
352+ ghostty_input_trigger_key_u key;
353+ ghostty_input_mods_e mods;
354+} ghostty_input_trigger_s;
355+
356+typedef struct {
357+ const char* action_key;
358+ const char* action;
359+ const char* title;
360+ const char* description;
361+} ghostty_command_s;
362+
363+typedef enum {
364+ GHOSTTY_BUILD_MODE_DEBUG,
365+ GHOSTTY_BUILD_MODE_RELEASE_SAFE,
366+ GHOSTTY_BUILD_MODE_RELEASE_FAST,
367+ GHOSTTY_BUILD_MODE_RELEASE_SMALL,
368+} ghostty_build_mode_e;
369+
370+typedef struct {
371+ ghostty_build_mode_e build_mode;
372+ const char* version;
373+ uintptr_t version_len;
374+} ghostty_info_s;
375+
376+typedef struct {
377+ const char* message;
378+} ghostty_diagnostic_s;
379+
380+typedef struct {
381+ const char* ptr;
382+ uintptr_t len;
383+ bool sentinel;
384+} ghostty_string_s;
385+
386+typedef struct {
387+ double tl_px_x;
388+ double tl_px_y;
389+ uint32_t offset_start;
390+ uint32_t offset_len;
391+ const char* text;
392+ uintptr_t text_len;
393+} ghostty_text_s;
394+
395+typedef enum {
396+ GHOSTTY_POINT_ACTIVE,
397+ GHOSTTY_POINT_VIEWPORT,
398+ GHOSTTY_POINT_SCREEN,
399+ GHOSTTY_POINT_SURFACE,
400+} ghostty_point_tag_e;
401+
402+typedef enum {
403+ GHOSTTY_POINT_COORD_EXACT,
404+ GHOSTTY_POINT_COORD_TOP_LEFT,
405+ GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
406+} ghostty_point_coord_e;
407+
408+typedef struct {
409+ ghostty_point_tag_e tag;
410+ ghostty_point_coord_e coord;
411+ uint32_t x;
412+ uint32_t y;
413+} ghostty_point_s;
414+
415+typedef struct {
416+ ghostty_point_s top_left;
417+ ghostty_point_s bottom_right;
418+ bool rectangle;
419+} ghostty_selection_s;
420+
421+typedef struct {
422+ const char* key;
423+ const char* value;
424+} ghostty_env_var_s;
425+
426+typedef struct {
427+ void* nsview;
428+} ghostty_platform_macos_s;
429+
430+typedef struct {
431+ void* uiview;
432+} ghostty_platform_ios_s;
433+
434+typedef union {
435+ ghostty_platform_macos_s macos;
436+ ghostty_platform_ios_s ios;
437+} ghostty_platform_u;
438+
439+typedef enum {
440+ GHOSTTY_SURFACE_CONTEXT_WINDOW = 0,
441+ GHOSTTY_SURFACE_CONTEXT_TAB = 1,
442+ GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
443+} ghostty_surface_context_e;
444+
445+typedef struct {
446+ ghostty_platform_e platform_tag;
447+ ghostty_platform_u platform;
448+ void* userdata;
449+ double scale_factor;
450+ float font_size;
451+ const char* working_directory;
452+ const char* command;
453+ ghostty_env_var_s* env_vars;
454+ size_t env_var_count;
455+ const char* initial_input;
456+ bool wait_after_command;
457+ ghostty_surface_context_e context;
458+ bool use_custom_io;
459+} ghostty_surface_config_s;
460+
461+typedef struct {
462+ uint16_t columns;
463+ uint16_t rows;
464+ uint32_t width_px;
465+ uint32_t height_px;
466+ uint32_t cell_width_px;
467+ uint32_t cell_height_px;
468+} ghostty_surface_size_s;
469+
470+// Config types
471+
472+// config.Path
473+typedef struct {
474+ const char* path;
475+ bool optional;
476+} ghostty_config_path_s;
477+
478+// config.Color
479+typedef struct {
480+ uint8_t r;
481+ uint8_t g;
482+ uint8_t b;
483+} ghostty_config_color_s;
484+
485+// config.ColorList
486+typedef struct {
487+ const ghostty_config_color_s* colors;
488+ size_t len;
489+} ghostty_config_color_list_s;
490+
491+// config.RepeatableCommand
492+typedef struct {
493+ const ghostty_command_s* commands;
494+ size_t len;
495+} ghostty_config_command_list_s;
496+
497+// config.Palette
498+typedef struct {
499+ ghostty_config_color_s colors[256];
500+} ghostty_config_palette_s;
501+
502+// config.QuickTerminalSize
503+typedef enum {
504+ GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
505+ GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
506+ GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
507+} ghostty_quick_terminal_size_tag_e;
508+
509+typedef union {
510+ float percentage;
511+ uint32_t pixels;
512+} ghostty_quick_terminal_size_value_u;
513+
514+typedef struct {
515+ ghostty_quick_terminal_size_tag_e tag;
516+ ghostty_quick_terminal_size_value_u value;
517+} ghostty_quick_terminal_size_s;
518+
519+typedef struct {
520+ ghostty_quick_terminal_size_s primary;
521+ ghostty_quick_terminal_size_s secondary;
522+} ghostty_config_quick_terminal_size_s;
523+
524+// config.Fullscreen
525+typedef enum {
526+ GHOSTTY_CONFIG_FULLSCREEN_FALSE,
527+ GHOSTTY_CONFIG_FULLSCREEN_TRUE,
528+ GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE,
529+ GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
530+ GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
531+} ghostty_config_fullscreen_e;
532+
533+// apprt.Target.Key
534+typedef enum {
535+ GHOSTTY_TARGET_APP,
536+ GHOSTTY_TARGET_SURFACE,
537+} ghostty_target_tag_e;
538+
539+typedef union {
540+ ghostty_surface_t surface;
541+} ghostty_target_u;
542+
543+typedef struct {
544+ ghostty_target_tag_e tag;
545+ ghostty_target_u target;
546+} ghostty_target_s;
547+
548+// apprt.action.SplitDirection
549+typedef enum {
550+ GHOSTTY_SPLIT_DIRECTION_RIGHT,
551+ GHOSTTY_SPLIT_DIRECTION_DOWN,
552+ GHOSTTY_SPLIT_DIRECTION_LEFT,
553+ GHOSTTY_SPLIT_DIRECTION_UP,
554+} ghostty_action_split_direction_e;
555+
556+// apprt.action.GotoSplit
557+typedef enum {
558+ GHOSTTY_GOTO_SPLIT_PREVIOUS,
559+ GHOSTTY_GOTO_SPLIT_NEXT,
560+ GHOSTTY_GOTO_SPLIT_UP,
561+ GHOSTTY_GOTO_SPLIT_LEFT,
562+ GHOSTTY_GOTO_SPLIT_DOWN,
563+ GHOSTTY_GOTO_SPLIT_RIGHT,
564+} ghostty_action_goto_split_e;
565+
566+// apprt.action.GotoWindow
567+typedef enum {
568+ GHOSTTY_GOTO_WINDOW_PREVIOUS,
569+ GHOSTTY_GOTO_WINDOW_NEXT,
570+} ghostty_action_goto_window_e;
571+
572+// apprt.action.ResizeSplit.Direction
573+typedef enum {
574+ GHOSTTY_RESIZE_SPLIT_UP,
575+ GHOSTTY_RESIZE_SPLIT_DOWN,
576+ GHOSTTY_RESIZE_SPLIT_LEFT,
577+ GHOSTTY_RESIZE_SPLIT_RIGHT,
578+} ghostty_action_resize_split_direction_e;
579+
580+// apprt.action.ResizeSplit
581+typedef struct {
582+ uint16_t amount;
583+ ghostty_action_resize_split_direction_e direction;
584+} ghostty_action_resize_split_s;
585+
586+// apprt.action.MoveTab
587+typedef struct {
588+ ssize_t amount;
589+} ghostty_action_move_tab_s;
590+
591+// apprt.action.GotoTab
592+typedef enum {
593+ GHOSTTY_GOTO_TAB_PREVIOUS = -1,
594+ GHOSTTY_GOTO_TAB_NEXT = -2,
595+ GHOSTTY_GOTO_TAB_LAST = -3,
596+} ghostty_action_goto_tab_e;
597+
598+// apprt.action.Fullscreen
599+typedef enum {
600+ GHOSTTY_FULLSCREEN_NATIVE,
601+ GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE,
602+ GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU,
603+ GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH,
604+} ghostty_action_fullscreen_e;
605+
606+// apprt.action.FloatWindow
607+typedef enum {
608+ GHOSTTY_FLOAT_WINDOW_ON,
609+ GHOSTTY_FLOAT_WINDOW_OFF,
610+ GHOSTTY_FLOAT_WINDOW_TOGGLE,
611+} ghostty_action_float_window_e;
612+
613+// apprt.action.SecureInput
614+typedef enum {
615+ GHOSTTY_SECURE_INPUT_ON,
616+ GHOSTTY_SECURE_INPUT_OFF,
617+ GHOSTTY_SECURE_INPUT_TOGGLE,
618+} ghostty_action_secure_input_e;
619+
620+// apprt.action.Inspector
621+typedef enum {
622+ GHOSTTY_INSPECTOR_TOGGLE,
623+ GHOSTTY_INSPECTOR_SHOW,
624+ GHOSTTY_INSPECTOR_HIDE,
625+} ghostty_action_inspector_e;
626+
627+// apprt.action.QuitTimer
628+typedef enum {
629+ GHOSTTY_QUIT_TIMER_START,
630+ GHOSTTY_QUIT_TIMER_STOP,
631+} ghostty_action_quit_timer_e;
632+
633+// apprt.action.Readonly
634+typedef enum {
635+ GHOSTTY_READONLY_OFF,
636+ GHOSTTY_READONLY_ON,
637+} ghostty_action_readonly_e;
638+
639+// apprt.action.DesktopNotification.C
640+typedef struct {
641+ const char* title;
642+ const char* body;
643+} ghostty_action_desktop_notification_s;
644+
645+// apprt.action.SetTitle.C
646+typedef struct {
647+ const char* title;
648+} ghostty_action_set_title_s;
649+
650+// apprt.action.PromptTitle
651+typedef enum {
652+ GHOSTTY_PROMPT_TITLE_SURFACE,
653+ GHOSTTY_PROMPT_TITLE_TAB,
654+} ghostty_action_prompt_title_e;
655+
656+// apprt.action.Pwd.C
657+typedef struct {
658+ const char* pwd;
659+} ghostty_action_pwd_s;
660+
661+// terminal.MouseShape
662+typedef enum {
663+ GHOSTTY_MOUSE_SHAPE_DEFAULT,
664+ GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
665+ GHOSTTY_MOUSE_SHAPE_HELP,
666+ GHOSTTY_MOUSE_SHAPE_POINTER,
667+ GHOSTTY_MOUSE_SHAPE_PROGRESS,
668+ GHOSTTY_MOUSE_SHAPE_WAIT,
669+ GHOSTTY_MOUSE_SHAPE_CELL,
670+ GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
671+ GHOSTTY_MOUSE_SHAPE_TEXT,
672+ GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
673+ GHOSTTY_MOUSE_SHAPE_ALIAS,
674+ GHOSTTY_MOUSE_SHAPE_COPY,
675+ GHOSTTY_MOUSE_SHAPE_MOVE,
676+ GHOSTTY_MOUSE_SHAPE_NO_DROP,
677+ GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
678+ GHOSTTY_MOUSE_SHAPE_GRAB,
679+ GHOSTTY_MOUSE_SHAPE_GRABBING,
680+ GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
681+ GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
682+ GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
683+ GHOSTTY_MOUSE_SHAPE_N_RESIZE,
684+ GHOSTTY_MOUSE_SHAPE_E_RESIZE,
685+ GHOSTTY_MOUSE_SHAPE_S_RESIZE,
686+ GHOSTTY_MOUSE_SHAPE_W_RESIZE,
687+ GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
688+ GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
689+ GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
690+ GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
691+ GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
692+ GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
693+ GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
694+ GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
695+ GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
696+ GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
697+} ghostty_action_mouse_shape_e;
698+
699+// apprt.action.MouseVisibility
700+typedef enum {
701+ GHOSTTY_MOUSE_VISIBLE,
702+ GHOSTTY_MOUSE_HIDDEN,
703+} ghostty_action_mouse_visibility_e;
704+
705+// apprt.action.MouseOverLink
706+typedef struct {
707+ const char* url;
708+ size_t len;
709+} ghostty_action_mouse_over_link_s;
710+
711+// apprt.action.SizeLimit
712+typedef struct {
713+ uint32_t min_width;
714+ uint32_t min_height;
715+ uint32_t max_width;
716+ uint32_t max_height;
717+} ghostty_action_size_limit_s;
718+
719+// apprt.action.InitialSize
720+typedef struct {
721+ uint32_t width;
722+ uint32_t height;
723+} ghostty_action_initial_size_s;
724+
725+// apprt.action.CellSize
726+typedef struct {
727+ uint32_t width;
728+ uint32_t height;
729+} ghostty_action_cell_size_s;
730+
731+// renderer.Health
732+typedef enum {
733+ GHOSTTY_RENDERER_HEALTH_HEALTHY,
734+ GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
735+} ghostty_action_renderer_health_e;
736+
737+// apprt.action.KeySequence
738+typedef struct {
739+ bool active;
740+ ghostty_input_trigger_s trigger;
741+} ghostty_action_key_sequence_s;
742+
743+// apprt.action.KeyTable.Tag
744+typedef enum {
745+ GHOSTTY_KEY_TABLE_ACTIVATE,
746+ GHOSTTY_KEY_TABLE_DEACTIVATE,
747+ GHOSTTY_KEY_TABLE_DEACTIVATE_ALL,
748+} ghostty_action_key_table_tag_e;
749+
750+// apprt.action.KeyTable.CValue
751+typedef union {
752+ struct {
753+ const char *name;
754+ size_t len;
755+ } activate;
756+} ghostty_action_key_table_u;
757+
758+// apprt.action.KeyTable.C
759+typedef struct {
760+ ghostty_action_key_table_tag_e tag;
761+ ghostty_action_key_table_u value;
762+} ghostty_action_key_table_s;
763+
764+// apprt.action.ColorKind
765+typedef enum {
766+ GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
767+ GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
768+ GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
769+} ghostty_action_color_kind_e;
770+
771+// apprt.action.ColorChange
772+typedef struct {
773+ ghostty_action_color_kind_e kind;
774+ uint8_t r;
775+ uint8_t g;
776+ uint8_t b;
777+} ghostty_action_color_change_s;
778+
779+// apprt.action.ConfigChange
780+typedef struct {
781+ ghostty_config_t config;
782+} ghostty_action_config_change_s;
783+
784+// apprt.action.ReloadConfig
785+typedef struct {
786+ bool soft;
787+} ghostty_action_reload_config_s;
788+
789+// apprt.action.OpenUrlKind
790+typedef enum {
791+ GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
792+ GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
793+ GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
794+} ghostty_action_open_url_kind_e;
795+
796+// apprt.action.OpenUrl.C
797+typedef struct {
798+ ghostty_action_open_url_kind_e kind;
799+ const char* url;
800+ uintptr_t len;
801+} ghostty_action_open_url_s;
802+
803+// apprt.action.CloseTabMode
804+typedef enum {
805+ GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
806+ GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
807+ GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
808+} ghostty_action_close_tab_mode_e;
809+
810+// apprt.surface.Message.ChildExited
811+typedef struct {
812+ uint32_t exit_code;
813+ uint64_t timetime_ms;
814+} ghostty_surface_message_childexited_s;
815+
816+// terminal.osc.Command.ProgressReport.State
817+typedef enum {
818+ GHOSTTY_PROGRESS_STATE_REMOVE,
819+ GHOSTTY_PROGRESS_STATE_SET,
820+ GHOSTTY_PROGRESS_STATE_ERROR,
821+ GHOSTTY_PROGRESS_STATE_INDETERMINATE,
822+ GHOSTTY_PROGRESS_STATE_PAUSE,
823+} ghostty_action_progress_report_state_e;
824+
825+// terminal.osc.Command.ProgressReport.C
826+typedef struct {
827+ ghostty_action_progress_report_state_e state;
828+ // -1 if no progress was reported, otherwise 0-100 indicating percent
829+ // completeness.
830+ int8_t progress;
831+} ghostty_action_progress_report_s;
832+
833+// apprt.action.CommandFinished.C
834+typedef struct {
835+ // -1 if no exit code was reported, otherwise 0-255
836+ int16_t exit_code;
837+ // number of nanoseconds that command was running for
838+ uint64_t duration;
839+} ghostty_action_command_finished_s;
840+
841+// apprt.action.StartSearch.C
842+typedef struct {
843+ const char* needle;
844+} ghostty_action_start_search_s;
845+
846+// apprt.action.SearchTotal
847+typedef struct {
848+ ssize_t total;
849+} ghostty_action_search_total_s;
850+
851+// apprt.action.SearchSelected
852+typedef struct {
853+ ssize_t selected;
854+} ghostty_action_search_selected_s;
855+
856+// terminal.Scrollbar
857+typedef struct {
858+ uint64_t total;
859+ uint64_t offset;
860+ uint64_t len;
861+} ghostty_action_scrollbar_s;
862+
863+// apprt.Action.Key
864+typedef enum {
865+ GHOSTTY_ACTION_QUIT,
866+ GHOSTTY_ACTION_NEW_WINDOW,
867+ GHOSTTY_ACTION_NEW_TAB,
868+ GHOSTTY_ACTION_CLOSE_TAB,
869+ GHOSTTY_ACTION_NEW_SPLIT,
870+ GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
871+ GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
872+ GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
873+ GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
874+ GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
875+ GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
876+ GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
877+ GHOSTTY_ACTION_TOGGLE_VISIBILITY,
878+ GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY,
879+ GHOSTTY_ACTION_MOVE_TAB,
880+ GHOSTTY_ACTION_GOTO_TAB,
881+ GHOSTTY_ACTION_GOTO_SPLIT,
882+ GHOSTTY_ACTION_GOTO_WINDOW,
883+ GHOSTTY_ACTION_RESIZE_SPLIT,
884+ GHOSTTY_ACTION_EQUALIZE_SPLITS,
885+ GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
886+ GHOSTTY_ACTION_PRESENT_TERMINAL,
887+ GHOSTTY_ACTION_SIZE_LIMIT,
888+ GHOSTTY_ACTION_RESET_WINDOW_SIZE,
889+ GHOSTTY_ACTION_INITIAL_SIZE,
890+ GHOSTTY_ACTION_CELL_SIZE,
891+ GHOSTTY_ACTION_SCROLLBAR,
892+ GHOSTTY_ACTION_RENDER,
893+ GHOSTTY_ACTION_INSPECTOR,
894+ GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
895+ GHOSTTY_ACTION_RENDER_INSPECTOR,
896+ GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
897+ GHOSTTY_ACTION_SET_TITLE,
898+ GHOSTTY_ACTION_PROMPT_TITLE,
899+ GHOSTTY_ACTION_PWD,
900+ GHOSTTY_ACTION_MOUSE_SHAPE,
901+ GHOSTTY_ACTION_MOUSE_VISIBILITY,
902+ GHOSTTY_ACTION_MOUSE_OVER_LINK,
903+ GHOSTTY_ACTION_RENDERER_HEALTH,
904+ GHOSTTY_ACTION_OPEN_CONFIG,
905+ GHOSTTY_ACTION_QUIT_TIMER,
906+ GHOSTTY_ACTION_FLOAT_WINDOW,
907+ GHOSTTY_ACTION_SECURE_INPUT,
908+ GHOSTTY_ACTION_KEY_SEQUENCE,
909+ GHOSTTY_ACTION_KEY_TABLE,
910+ GHOSTTY_ACTION_COLOR_CHANGE,
911+ GHOSTTY_ACTION_RELOAD_CONFIG,
912+ GHOSTTY_ACTION_CONFIG_CHANGE,
913+ GHOSTTY_ACTION_CLOSE_WINDOW,
914+ GHOSTTY_ACTION_RING_BELL,
915+ GHOSTTY_ACTION_UNDO,
916+ GHOSTTY_ACTION_REDO,
917+ GHOSTTY_ACTION_CHECK_FOR_UPDATES,
918+ GHOSTTY_ACTION_OPEN_URL,
919+ GHOSTTY_ACTION_SHOW_CHILD_EXITED,
920+ GHOSTTY_ACTION_PROGRESS_REPORT,
921+ GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
922+ GHOSTTY_ACTION_COMMAND_FINISHED,
923+ GHOSTTY_ACTION_START_SEARCH,
924+ GHOSTTY_ACTION_END_SEARCH,
925+ GHOSTTY_ACTION_SEARCH_TOTAL,
926+ GHOSTTY_ACTION_SEARCH_SELECTED,
927+ GHOSTTY_ACTION_READONLY,
928+ GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
929+} ghostty_action_tag_e;
930+
931+typedef union {
932+ ghostty_action_split_direction_e new_split;
933+ ghostty_action_fullscreen_e toggle_fullscreen;
934+ ghostty_action_move_tab_s move_tab;
935+ ghostty_action_goto_tab_e goto_tab;
936+ ghostty_action_goto_split_e goto_split;
937+ ghostty_action_goto_window_e goto_window;
938+ ghostty_action_resize_split_s resize_split;
939+ ghostty_action_size_limit_s size_limit;
940+ ghostty_action_initial_size_s initial_size;
941+ ghostty_action_cell_size_s cell_size;
942+ ghostty_action_scrollbar_s scrollbar;
943+ ghostty_action_inspector_e inspector;
944+ ghostty_action_desktop_notification_s desktop_notification;
945+ ghostty_action_set_title_s set_title;
946+ ghostty_action_prompt_title_e prompt_title;
947+ ghostty_action_pwd_s pwd;
948+ ghostty_action_mouse_shape_e mouse_shape;
949+ ghostty_action_mouse_visibility_e mouse_visibility;
950+ ghostty_action_mouse_over_link_s mouse_over_link;
951+ ghostty_action_renderer_health_e renderer_health;
952+ ghostty_action_quit_timer_e quit_timer;
953+ ghostty_action_float_window_e float_window;
954+ ghostty_action_secure_input_e secure_input;
955+ ghostty_action_key_sequence_s key_sequence;
956+ ghostty_action_key_table_s key_table;
957+ ghostty_action_color_change_s color_change;
958+ ghostty_action_reload_config_s reload_config;
959+ ghostty_action_config_change_s config_change;
960+ ghostty_action_open_url_s open_url;
961+ ghostty_action_close_tab_mode_e close_tab_mode;
962+ ghostty_surface_message_childexited_s child_exited;
963+ ghostty_action_progress_report_s progress_report;
964+ ghostty_action_command_finished_s command_finished;
965+ ghostty_action_start_search_s start_search;
966+ ghostty_action_search_total_s search_total;
967+ ghostty_action_search_selected_s search_selected;
968+ ghostty_action_readonly_e readonly;
969+} ghostty_action_u;
970+
971+typedef struct {
972+ ghostty_action_tag_e tag;
973+ ghostty_action_u action;
974+} ghostty_action_s;
975+
976+typedef void (*ghostty_runtime_wakeup_cb)(void*);
977+typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
978+ ghostty_clipboard_e,
979+ void*);
980+typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
981+ void*,
982+ const char*,
983+ void*,
984+ ghostty_clipboard_request_e);
985+typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
986+ ghostty_clipboard_e,
987+ const ghostty_clipboard_content_s*,
988+ size_t,
989+ bool);
990+typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
991+typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
992+ ghostty_target_s,
993+ ghostty_action_s);
994+
995+typedef struct {
996+ void* userdata;
997+ bool supports_selection_clipboard;
998+ ghostty_runtime_wakeup_cb wakeup_cb;
999+ ghostty_runtime_action_cb action_cb;
1000+ ghostty_runtime_read_clipboard_cb read_clipboard_cb;
1001+ ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
1002+ ghostty_runtime_write_clipboard_cb write_clipboard_cb;
1003+ ghostty_runtime_close_surface_cb close_surface_cb;
1004+} ghostty_runtime_config_s;
1005+
1006+// apprt.ipc.Target.Key
1007+typedef enum {
1008+ GHOSTTY_IPC_TARGET_CLASS,
1009+ GHOSTTY_IPC_TARGET_DETECT,
1010+} ghostty_ipc_target_tag_e;
1011+
1012+typedef union {
1013+ char *klass;
1014+} ghostty_ipc_target_u;
1015+
1016+typedef struct {
1017+ ghostty_ipc_target_tag_e tag;
1018+ ghostty_ipc_target_u target;
1019+} chostty_ipc_target_s;
1020+
1021+// apprt.ipc.Action.NewWindow
1022+typedef struct {
1023+ // This should be a null terminated list of strings.
1024+ const char **arguments;
1025+} ghostty_ipc_action_new_window_s;
1026+
1027+typedef union {
1028+ ghostty_ipc_action_new_window_s new_window;
1029+} ghostty_ipc_action_u;
1030+
1031+// apprt.ipc.Action.Key
1032+typedef enum {
1033+ GHOSTTY_IPC_ACTION_NEW_WINDOW,
1034+} ghostty_ipc_action_tag_e;
1035+
1036+//-------------------------------------------------------------------
1037+// Published API
1038+
1039+int ghostty_init(uintptr_t, char**);
1040+void ghostty_cli_try_action(void);
1041+ghostty_info_s ghostty_info(void);
1042+const char* ghostty_translate(const char*);
1043+void ghostty_string_free(ghostty_string_s);
1044+
1045+ghostty_config_t ghostty_config_new();
1046+void ghostty_config_free(ghostty_config_t);
1047+ghostty_config_t ghostty_config_clone(ghostty_config_t);
1048+void ghostty_config_load_cli_args(ghostty_config_t);
1049+void ghostty_config_load_file(ghostty_config_t, const char*);
1050+void ghostty_config_load_default_files(ghostty_config_t);
1051+void ghostty_config_load_recursive_files(ghostty_config_t);
1052+void ghostty_config_finalize(ghostty_config_t);
1053+bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
1054+ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
1055+ const char*,
1056+ uintptr_t);
1057+uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
1058+ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
1059+ghostty_string_s ghostty_config_open_path(void);
1060+
1061+ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
1062+ ghostty_config_t);
1063+void ghostty_app_free(ghostty_app_t);
1064+void ghostty_app_tick(ghostty_app_t);
1065+void* ghostty_app_userdata(ghostty_app_t);
1066+void ghostty_app_set_focus(ghostty_app_t, bool);
1067+bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
1068+bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
1069+void ghostty_app_keyboard_changed(ghostty_app_t);
1070+void ghostty_app_open_config(ghostty_app_t);
1071+void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
1072+bool ghostty_app_needs_confirm_quit(ghostty_app_t);
1073+bool ghostty_app_has_global_keybinds(ghostty_app_t);
1074+void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
1075+
1076+ghostty_surface_config_s ghostty_surface_config_new();
1077+
1078+ghostty_surface_t ghostty_surface_new(ghostty_app_t,
1079+ const ghostty_surface_config_s*);
1080+void ghostty_surface_free(ghostty_surface_t);
1081+void* ghostty_surface_userdata(ghostty_surface_t);
1082+ghostty_app_t ghostty_surface_app(ghostty_surface_t);
1083+ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
1084+void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
1085+bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
1086+bool ghostty_surface_process_exited(ghostty_surface_t);
1087+void ghostty_surface_refresh(ghostty_surface_t);
1088+void ghostty_surface_draw(ghostty_surface_t);
1089+// Custom I/O API - feed output into the terminal and receive writes.
1090+void ghostty_surface_feed_data(ghostty_surface_t, const uint8_t*, size_t);
1091+void ghostty_surface_set_write_callback(ghostty_surface_t,
1092+ ghostty_surface_write_fn,
1093+ void*);
1094+void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
1095+void ghostty_surface_set_focus(ghostty_surface_t, bool);
1096+void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
1097+void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
1098+ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
1099+void ghostty_surface_set_color_scheme(ghostty_surface_t,
1100+ ghostty_color_scheme_e);
1101+ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
1102+ ghostty_input_mods_e);
1103+bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
1104+bool ghostty_surface_key_is_binding(ghostty_surface_t,
1105+ ghostty_input_key_s,
1106+ ghostty_binding_flags_e*);
1107+void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
1108+void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
1109+bool ghostty_surface_mouse_captured(ghostty_surface_t);
1110+bool ghostty_surface_mouse_button(ghostty_surface_t,
1111+ ghostty_input_mouse_state_e,
1112+ ghostty_input_mouse_button_e,
1113+ ghostty_input_mods_e);
1114+void ghostty_surface_mouse_pos(ghostty_surface_t,
1115+ double,
1116+ double,
1117+ ghostty_input_mods_e);
1118+void ghostty_surface_mouse_scroll(ghostty_surface_t,
1119+ double,
1120+ double,
1121+ ghostty_input_scroll_mods_t);
1122+void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
1123+void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
1124+void ghostty_surface_request_close(ghostty_surface_t);
1125+void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
1126+void ghostty_surface_split_focus(ghostty_surface_t,
1127+ ghostty_action_goto_split_e);
1128+void ghostty_surface_split_resize(ghostty_surface_t,
1129+ ghostty_action_resize_split_direction_e,
1130+ uint16_t);
1131+void ghostty_surface_split_equalize(ghostty_surface_t);
1132+bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
1133+void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
1134+ const char*,
1135+ void*,
1136+ bool);
1137+bool ghostty_surface_has_selection(ghostty_surface_t);
1138+bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
1139+bool ghostty_surface_read_text(ghostty_surface_t,
1140+ ghostty_selection_s,
1141+ ghostty_text_s*);
1142+void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
1143+
1144+#ifdef __APPLE__
1145+void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
1146+void* ghostty_surface_quicklook_font(ghostty_surface_t);
1147+bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
1148+#endif
1149+
1150+ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
1151+void ghostty_inspector_free(ghostty_surface_t);
1152+void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
1153+void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
1154+void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
1155+void ghostty_inspector_mouse_button(ghostty_inspector_t,
1156+ ghostty_input_mouse_state_e,
1157+ ghostty_input_mouse_button_e,
1158+ ghostty_input_mods_e);
1159+void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
1160+void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
1161+ double,
1162+ double,
1163+ ghostty_input_scroll_mods_t);
1164+void ghostty_inspector_key(ghostty_inspector_t,
1165+ ghostty_input_action_e,
1166+ ghostty_input_key_e,
1167+ ghostty_input_mods_e);
1168+void ghostty_inspector_text(ghostty_inspector_t, const char*);
1169+
1170+#ifdef __APPLE__
1171+bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
1172+void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
1173+bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
1174+#endif
1175+
1176+// APIs I'd like to get rid of eventually but are still needed for now.
1177+// Don't use these unless you know what you're doing.
1178+void ghostty_set_window_background_blur(ghostty_app_t, void*);
1179+
1180+// Benchmark API, if available.
1181+bool ghostty_benchmark_cli(const char*, const char*);
1182+
1183+#ifdef __cplusplus
1184+}
1185+#endif
1186+
1187+#endif /* GHOSTTY_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt.h+87−0
@@ -0,0 +1,87 @@
1+/**
2+ * @file vt.h
3+ *
4+ * libghostty-vt - Virtual terminal emulator library
5+ *
6+ * This library provides functionality for parsing and handling terminal
7+ * escape sequences as well as maintaining terminal state such as styles,
8+ * cursor position, screen, scrollback, and more.
9+ *
10+ * WARNING: This is an incomplete, work-in-progress API. It is not yet
11+ * stable and is definitely going to change.
12+ */
13+
14+/**
15+ * @mainpage libghostty-vt - Virtual Terminal Emulator Library
16+ *
17+ * libghostty-vt is a C library which implements a modern terminal emulator,
18+ * extracted from the [Ghostty](https://ghostty.org) terminal emulator.
19+ *
20+ * libghostty-vt contains the logic for handling the core parts of a terminal
21+ * emulator: parsing terminal escape sequences, maintaining terminal state,
22+ * encoding input events, etc. It can handle scrollback, line wrapping,
23+ * reflow on resize, and more.
24+ *
25+ * @warning This library is currently in development and the API is not yet stable.
26+ * Breaking changes are expected in future versions. Use with caution in production code.
27+ *
28+ * @section groups_sec API Reference
29+ *
30+ * The API is organized into the following groups:
31+ * - @ref key "Key Encoding" - Encode key events into terminal sequences
32+ * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
33+ * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
34+ * - @ref paste "Paste Utilities" - Validate paste data safety
35+ * - @ref allocator "Memory Management" - Memory management and custom allocators
36+ * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions
37+ *
38+ * @section examples_sec Examples
39+ *
40+ * Complete working examples:
41+ * - @ref c-vt/src/main.c - OSC parser example
42+ * - @ref c-vt-key-encode/src/main.c - Key encoding example
43+ * - @ref c-vt-paste/src/main.c - Paste safety check example
44+ * - @ref c-vt-sgr/src/main.c - SGR parser example
45+ *
46+ */
47+
48+/** @example c-vt/src/main.c
49+ * This example demonstrates how to use the OSC parser to parse an OSC sequence,
50+ * extract command information, and retrieve command-specific data like window titles.
51+ */
52+
53+/** @example c-vt-key-encode/src/main.c
54+ * This example demonstrates how to use the key encoder to convert key events
55+ * into terminal escape sequences using the Kitty keyboard protocol.
56+ */
57+
58+/** @example c-vt-paste/src/main.c
59+ * This example demonstrates how to use the paste utilities to check if
60+ * paste data is safe before sending it to the terminal.
61+ */
62+
63+/** @example c-vt-sgr/src/main.c
64+ * This example demonstrates how to use the SGR parser to parse terminal
65+ * styling sequences and extract text attributes like colors and underline styles.
66+ */
67+
68+#ifndef GHOSTTY_VT_H
69+#define GHOSTTY_VT_H
70+
71+#ifdef __cplusplus
72+extern "C" {
73+#endif
74+
75+#include <ghostty/vt/result.h>
76+#include <ghostty/vt/allocator.h>
77+#include <ghostty/vt/osc.h>
78+#include <ghostty/vt/sgr.h>
79+#include <ghostty/vt/key.h>
80+#include <ghostty/vt/paste.h>
81+#include <ghostty/vt/wasm.h>
82+
83+#ifdef __cplusplus
84+}
85+#endif
86+
87+#endif /* GHOSTTY_VT_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/allocator.h+196−0
@@ -0,0 +1,196 @@
1+/**
2+ * @file allocator.h
3+ *
4+ * Memory management interface for libghostty-vt.
5+ */
6+
7+#ifndef GHOSTTY_VT_ALLOCATOR_H
8+#define GHOSTTY_VT_ALLOCATOR_H
9+
10+#include <stdbool.h>
11+#include <stddef.h>
12+#include <stdint.h>
13+
14+/** @defgroup allocator Memory Management
15+ *
16+ * libghostty-vt does require memory allocation for various operations,
17+ * but is resilient to allocation failures and will gracefully handle
18+ * out-of-memory situations by returning error codes.
19+ *
20+ * The exact memory management semantics are documented in the relevant
21+ * functions and data structures.
22+ *
23+ * libghostty-vt uses explicit memory allocation via an allocator
24+ * interface provided by GhosttyAllocator. The interface is based on the
25+ * [Zig](https://ziglang.org) allocator interface, since this has been
26+ * shown to be a flexible and powerful interface in practice and enables
27+ * a wide variety of allocation strategies.
28+ *
29+ * **For the common case, you can pass NULL as the allocator for any
30+ * function that accepts one,** and libghostty will use a default allocator.
31+ * The default allocator will be libc malloc/free if libc is linked.
32+ * Otherwise, a custom allocator is used (currently Zig's SMP allocator)
33+ * that doesn't require any external dependencies.
34+ *
35+ * ## Basic Usage
36+ *
37+ * For simple use cases, you can ignore this interface entirely by passing NULL
38+ * as the allocator parameter to functions that accept one. This will use the
39+ * default allocator (typically libc malloc/free, if libc is linked, but
40+ * we provide our own default allocator if libc isn't linked).
41+ *
42+ * To use a custom allocator:
43+ * 1. Implement the GhosttyAllocatorVtable function pointers
44+ * 2. Create a GhosttyAllocator struct with your vtable and context
45+ * 3. Pass the allocator to functions that accept one
46+ *
47+ * @{
48+ */
49+
50+/**
51+ * Function table for custom memory allocator operations.
52+ *
53+ * This vtable defines the interface for a custom memory allocator. All
54+ * function pointers must be valid and non-NULL.
55+ *
56+ * @ingroup allocator
57+ *
58+ * If you're not going to use a custom allocator, you can ignore all of
59+ * this. All functions that take an allocator pointer allow NULL to use a
60+ * default allocator.
61+ *
62+ * The interface is based on the Zig allocator interface. I'll say up front
63+ * that it is easy to look at this interface and think "wow, this is really
64+ * overcomplicated". The reason for this complexity is well thought out by
65+ * the Zig folks, and it enables a diverse set of allocation strategies
66+ * as shown by the Zig ecosystem. As a consolation, please note that many
67+ * of the arguments are only needed for advanced use cases and can be
68+ * safely ignored in simple implementations. For example, if you look at
69+ * the Zig implementation of the libc allocator in `lib/std/heap.zig`
70+ * (search for CAllocator), you'll see it is very simple.
71+ *
72+ * We chose to align with the Zig allocator interface because:
73+ *
74+ * 1. It is a proven interface that serves a wide variety of use cases
75+ * in the real world via the Zig ecosystem. It's shown to work.
76+ *
77+ * 2. Our core implementation itself is Zig, and this lets us very
78+ * cheaply and easily convert between C and Zig allocators.
79+ *
80+ * NOTE(mitchellh): In the future, we can have default implementations of
81+ * resize/remap and allow those to be null.
82+ */
83+typedef struct {
84+ /**
85+ * Return a pointer to `len` bytes with specified `alignment`, or return
86+ * `NULL` indicating the allocation failed.
87+ *
88+ * @param ctx The allocator context
89+ * @param len Number of bytes to allocate
90+ * @param alignment Required alignment for the allocation. Guaranteed to
91+ * be a power of two between 1 and 16 inclusive.
92+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
93+ * @return Pointer to allocated memory, or NULL if allocation failed
94+ */
95+ void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
96+
97+ /**
98+ * Attempt to expand or shrink memory in place.
99+ *
100+ * `memory_len` must equal the length requested from the most recent
101+ * successful call to `alloc`, `resize`, or `remap`. `alignment` must
102+ * equal the same value that was passed as the `alignment` parameter to
103+ * the original `alloc` call.
104+ *
105+ * `new_len` must be greater than zero.
106+ *
107+ * @param ctx The allocator context
108+ * @param memory Pointer to the memory block to resize
109+ * @param memory_len Current size of the memory block
110+ * @param alignment Alignment (must match original allocation)
111+ * @param new_len New requested size
112+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
113+ * @return true if resize was successful in-place, false if relocation would be required
114+ */
115+ bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
116+
117+ /**
118+ * Attempt to expand or shrink memory, allowing relocation.
119+ *
120+ * `memory_len` must equal the length requested from the most recent
121+ * successful call to `alloc`, `resize`, or `remap`. `alignment` must
122+ * equal the same value that was passed as the `alignment` parameter to
123+ * the original `alloc` call.
124+ *
125+ * A non-`NULL` return value indicates the resize was successful. The
126+ * allocation may have same address, or may have been relocated. In either
127+ * case, the allocation now has size of `new_len`. A `NULL` return value
128+ * indicates that the resize would be equivalent to allocating new memory,
129+ * copying the bytes from the old memory, and then freeing the old memory.
130+ * In such case, it is more efficient for the caller to perform the copy.
131+ *
132+ * `new_len` must be greater than zero.
133+ *
134+ * @param ctx The allocator context
135+ * @param memory Pointer to the memory block to remap
136+ * @param memory_len Current size of the memory block
137+ * @param alignment Alignment (must match original allocation)
138+ * @param new_len New requested size
139+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
140+ * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
141+ */
142+ void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
143+
144+ /**
145+ * Free and invalidate a region of memory.
146+ *
147+ * `memory_len` must equal the length requested from the most recent
148+ * successful call to `alloc`, `resize`, or `remap`. `alignment` must
149+ * equal the same value that was passed as the `alignment` parameter to
150+ * the original `alloc` call.
151+ *
152+ * @param ctx The allocator context
153+ * @param memory Pointer to the memory block to free
154+ * @param memory_len Size of the memory block
155+ * @param alignment Alignment (must match original allocation)
156+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
157+ */
158+ void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
159+} GhosttyAllocatorVtable;
160+
161+/**
162+ * Custom memory allocator.
163+ *
164+ * For functions that take an allocator pointer, a NULL pointer indicates
165+ * that the default allocator should be used. The default allocator will
166+ * be libc malloc/free if we're linking to libc. If libc isn't linked,
167+ * a custom allocator is used (currently Zig's SMP allocator).
168+ *
169+ * @ingroup allocator
170+ *
171+ * Usage example:
172+ * @code
173+ * GhosttyAllocator allocator = {
174+ * .vtable = &my_allocator_vtable,
175+ * .ctx = my_allocator_state
176+ * };
177+ * @endcode
178+ */
179+typedef struct GhosttyAllocator {
180+ /**
181+ * Opaque context pointer passed to all vtable functions.
182+ * This allows the allocator implementation to maintain state
183+ * or reference external resources needed for memory management.
184+ */
185+ void *ctx;
186+
187+ /**
188+ * Pointer to the allocator's vtable containing function pointers
189+ * for memory operations (alloc, resize, remap, free).
190+ */
191+ const GhosttyAllocatorVtable *vtable;
192+} GhosttyAllocator;
193+
194+/** @} */
195+
196+#endif /* GHOSTTY_VT_ALLOCATOR_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/color.h+96−0
@@ -0,0 +1,96 @@
1+/**
2+ * @file color.h
3+ *
4+ * Color types and utilities.
5+ */
6+
7+#ifndef GHOSTTY_VT_COLOR_H
8+#define GHOSTTY_VT_COLOR_H
9+
10+#include <stdint.h>
11+
12+#ifdef __cplusplus
13+extern "C" {
14+#endif
15+
16+/**
17+ * RGB color value.
18+ *
19+ * @ingroup sgr
20+ */
21+typedef struct {
22+ uint8_t r; /**< Red component (0-255) */
23+ uint8_t g; /**< Green component (0-255) */
24+ uint8_t b; /**< Blue component (0-255) */
25+} GhosttyColorRgb;
26+
27+/**
28+ * Palette color index (0-255).
29+ *
30+ * @ingroup sgr
31+ */
32+typedef uint8_t GhosttyColorPaletteIndex;
33+
34+/** @addtogroup sgr
35+ * @{
36+ */
37+
38+/** Black color (0) @ingroup sgr */
39+#define GHOSTTY_COLOR_NAMED_BLACK 0
40+/** Red color (1) @ingroup sgr */
41+#define GHOSTTY_COLOR_NAMED_RED 1
42+/** Green color (2) @ingroup sgr */
43+#define GHOSTTY_COLOR_NAMED_GREEN 2
44+/** Yellow color (3) @ingroup sgr */
45+#define GHOSTTY_COLOR_NAMED_YELLOW 3
46+/** Blue color (4) @ingroup sgr */
47+#define GHOSTTY_COLOR_NAMED_BLUE 4
48+/** Magenta color (5) @ingroup sgr */
49+#define GHOSTTY_COLOR_NAMED_MAGENTA 5
50+/** Cyan color (6) @ingroup sgr */
51+#define GHOSTTY_COLOR_NAMED_CYAN 6
52+/** White color (7) @ingroup sgr */
53+#define GHOSTTY_COLOR_NAMED_WHITE 7
54+/** Bright black color (8) @ingroup sgr */
55+#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8
56+/** Bright red color (9) @ingroup sgr */
57+#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9
58+/** Bright green color (10) @ingroup sgr */
59+#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10
60+/** Bright yellow color (11) @ingroup sgr */
61+#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11
62+/** Bright blue color (12) @ingroup sgr */
63+#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12
64+/** Bright magenta color (13) @ingroup sgr */
65+#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13
66+/** Bright cyan color (14) @ingroup sgr */
67+#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14
68+/** Bright white color (15) @ingroup sgr */
69+#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15
70+
71+/** @} */
72+
73+/**
74+ * Get the RGB color components.
75+ *
76+ * This function extracts the individual red, green, and blue components
77+ * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments
78+ * where accessing struct fields directly is difficult.
79+ *
80+ * @param color The RGB color value
81+ * @param r Pointer to store the red component (0-255)
82+ * @param g Pointer to store the green component (0-255)
83+ * @param b Pointer to store the blue component (0-255)
84+ *
85+ * @ingroup sgr
86+ */
87+void ghostty_color_rgb_get(GhosttyColorRgb color,
88+ uint8_t* r,
89+ uint8_t* g,
90+ uint8_t* b);
91+
92+#ifdef __cplusplus
93+}
94+#endif
95+
96+#endif /* GHOSTTY_VT_COLOR_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/key.h+80−0
@@ -0,0 +1,80 @@
1+/**
2+ * @file key.h
3+ *
4+ * Key encoding module - encode key events into terminal escape sequences.
5+ */
6+
7+#ifndef GHOSTTY_VT_KEY_H
8+#define GHOSTTY_VT_KEY_H
9+
10+/** @defgroup key Key Encoding
11+ *
12+ * Utilities for encoding key events into terminal escape sequences,
13+ * supporting both legacy encoding as well as Kitty Keyboard Protocol.
14+ *
15+ * ## Basic Usage
16+ *
17+ * 1. Create an encoder instance with ghostty_key_encoder_new()
18+ * 2. Configure encoder options with ghostty_key_encoder_setopt().
19+ * 3. For each key event:
20+ * - Create a key event with ghostty_key_event_new()
21+ * - Set event properties (action, key, modifiers, etc.)
22+ * - Encode with ghostty_key_encoder_encode()
23+ * - Free the event with ghostty_key_event_free()
24+ * - Note: You can also reuse the same key event multiple times by
25+ * changing its properties.
26+ * 4. Free the encoder with ghostty_key_encoder_free() when done
27+ *
28+ * ## Example
29+ *
30+ * @code{.c}
31+ * #include <assert.h>
32+ * #include <stdio.h>
33+ * #include <ghostty/vt.h>
34+ *
35+ * int main() {
36+ * // Create encoder
37+ * GhosttyKeyEncoder encoder;
38+ * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
39+ * assert(result == GHOSTTY_SUCCESS);
40+ *
41+ * // Enable Kitty keyboard protocol with all features
42+ * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS,
43+ * &(uint8_t){GHOSTTY_KITTY_KEY_ALL});
44+ *
45+ * // Create and configure key event for Ctrl+C press
46+ * GhosttyKeyEvent event;
47+ * result = ghostty_key_event_new(NULL, &event);
48+ * assert(result == GHOSTTY_SUCCESS);
49+ * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS);
50+ * ghostty_key_event_set_key(event, GHOSTTY_KEY_C);
51+ * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL);
52+ *
53+ * // Encode the key event
54+ * char buf[128];
55+ * size_t written = 0;
56+ * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
57+ * assert(result == GHOSTTY_SUCCESS);
58+ *
59+ * // Use the encoded sequence (e.g., write to terminal)
60+ * fwrite(buf, 1, written, stdout);
61+ *
62+ * // Cleanup
63+ * ghostty_key_event_free(event);
64+ * ghostty_key_encoder_free(encoder);
65+ * return 0;
66+ * }
67+ * @endcode
68+ *
69+ * For a complete working example, see example/c-vt-key-encode in the
70+ * repository.
71+ *
72+ * @{
73+ */
74+
75+#include <ghostty/vt/key/event.h>
76+#include <ghostty/vt/key/encoder.h>
77+
78+/** @} */
79+
80+#endif /* GHOSTTY_VT_KEY_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/key/encoder.h+221−0
@@ -0,0 +1,221 @@
1+/**
2+ * @file encoder.h
3+ *
4+ * Key event encoding to terminal escape sequences.
5+ */
6+
7+#ifndef GHOSTTY_VT_KEY_ENCODER_H
8+#define GHOSTTY_VT_KEY_ENCODER_H
9+
10+#include <stddef.h>
11+#include <stdint.h>
12+#include <ghostty/vt/result.h>
13+#include <ghostty/vt/allocator.h>
14+#include <ghostty/vt/key/event.h>
15+
16+/**
17+ * Opaque handle to a key encoder instance.
18+ *
19+ * This handle represents a key encoder that converts key events into terminal
20+ * escape sequences.
21+ *
22+ * @ingroup key
23+ */
24+typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder;
25+
26+/**
27+ * Kitty keyboard protocol flags.
28+ *
29+ * Bitflags representing the various modes of the Kitty keyboard protocol.
30+ * These can be combined using bitwise OR operations. Valid values all
31+ * start with `GHOSTTY_KITTY_KEY_`.
32+ *
33+ * @ingroup key
34+ */
35+typedef uint8_t GhosttyKittyKeyFlags;
36+
37+/** Kitty keyboard protocol disabled (all flags off) */
38+#define GHOSTTY_KITTY_KEY_DISABLED 0
39+
40+/** Disambiguate escape codes */
41+#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0)
42+
43+/** Report key press and release events */
44+#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1)
45+
46+/** Report alternate key codes */
47+#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2)
48+
49+/** Report all key events including those normally handled by the terminal */
50+#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3)
51+
52+/** Report associated text with key events */
53+#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4)
54+
55+/** All Kitty keyboard protocol flags enabled */
56+#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED)
57+
58+/**
59+ * macOS option key behavior.
60+ *
61+ * Determines whether the "option" key on macOS is treated as "alt" or not.
62+ * See the Ghostty `macos-option-as-alt` configuration option for more details.
63+ *
64+ * @ingroup key
65+ */
66+typedef enum {
67+ /** Option key is not treated as alt */
68+ GHOSTTY_OPTION_AS_ALT_FALSE = 0,
69+ /** Option key is treated as alt */
70+ GHOSTTY_OPTION_AS_ALT_TRUE = 1,
71+ /** Only left option key is treated as alt */
72+ GHOSTTY_OPTION_AS_ALT_LEFT = 2,
73+ /** Only right option key is treated as alt */
74+ GHOSTTY_OPTION_AS_ALT_RIGHT = 3,
75+} GhosttyOptionAsAlt;
76+
77+/**
78+ * Key encoder option identifiers.
79+ *
80+ * These values are used with ghostty_key_encoder_setopt() to configure
81+ * the behavior of the key encoder.
82+ *
83+ * @ingroup key
84+ */
85+typedef enum {
86+ /** Terminal DEC mode 1: cursor key application mode (value: bool) */
87+ GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0,
88+
89+ /** Terminal DEC mode 66: keypad key application mode (value: bool) */
90+ GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1,
91+
92+ /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */
93+ GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2,
94+
95+ /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */
96+ GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3,
97+
98+ /** xterm modifyOtherKeys mode 2 (value: bool) */
99+ GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4,
100+
101+ /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */
102+ GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5,
103+
104+ /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */
105+ GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6,
106+} GhosttyKeyEncoderOption;
107+
108+/**
109+ * Create a new key encoder instance.
110+ *
111+ * Creates a new key encoder with default options. The encoder can be configured
112+ * using ghostty_key_encoder_setopt() and must be freed using
113+ * ghostty_key_encoder_free() when no longer needed.
114+ *
115+ * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
116+ * @param encoder Pointer to store the created encoder handle
117+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
118+ *
119+ * @ingroup key
120+ */
121+GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder);
122+
123+/**
124+ * Free a key encoder instance.
125+ *
126+ * Releases all resources associated with the key encoder. After this call,
127+ * the encoder handle becomes invalid and must not be used.
128+ *
129+ * @param encoder The encoder handle to free (may be NULL)
130+ *
131+ * @ingroup key
132+ */
133+void ghostty_key_encoder_free(GhosttyKeyEncoder encoder);
134+
135+/**
136+ * Set an option on the key encoder.
137+ *
138+ * Configures the behavior of the key encoder. Options control various aspects
139+ * of encoding such as terminal modes (cursor key application mode, keypad mode),
140+ * protocol selection (Kitty keyboard protocol flags), and platform-specific
141+ * behaviors (macOS option-as-alt).
142+ *
143+ * A null pointer value does nothing. It does not reset the value to the
144+ * default. The setopt call will do nothing.
145+ *
146+ * @param encoder The encoder handle, must not be NULL
147+ * @param option The option to set
148+ * @param value Pointer to the value to set (type depends on the option)
149+ *
150+ * @ingroup key
151+ */
152+void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value);
153+
154+/**
155+ * Encode a key event into a terminal escape sequence.
156+ *
157+ * Converts a key event into the appropriate terminal escape sequence based on
158+ * the encoder's current options. The sequence is written to the provided buffer.
159+ *
160+ * Not all key events produce output. For example, unmodified modifier keys
161+ * typically don't generate escape sequences. Check the out_len parameter to
162+ * determine if any data was written.
163+ *
164+ * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY
165+ * and out_len will contain the required buffer size. The caller can then
166+ * allocate a larger buffer and call the function again.
167+ *
168+ * @param encoder The encoder handle, must not be NULL
169+ * @param event The key event to encode, must not be NULL
170+ * @param out_buf Buffer to write the encoded sequence to
171+ * @param out_buf_size Size of the output buffer in bytes
172+ * @param out_len Pointer to store the number of bytes written (may be NULL)
173+ * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code
174+ *
175+ * ## Example: Calculate required buffer size
176+ *
177+ * @code{.c}
178+ * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY)
179+ * size_t required = 0;
180+ * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
181+ * assert(result == GHOSTTY_OUT_OF_MEMORY);
182+ *
183+ * // Allocate buffer of required size
184+ * char *buf = malloc(required);
185+ *
186+ * // Encode with properly sized buffer
187+ * size_t written = 0;
188+ * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written);
189+ * assert(result == GHOSTTY_SUCCESS);
190+ *
191+ * // Use the encoded sequence...
192+ *
193+ * free(buf);
194+ * @endcode
195+ *
196+ * ## Example: Direct encoding with static buffer
197+ *
198+ * @code{.c}
199+ * // Most escape sequences are short, so a static buffer often suffices
200+ * char buf[128];
201+ * size_t written = 0;
202+ * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
203+ *
204+ * if (result == GHOSTTY_SUCCESS) {
205+ * // Write the encoded sequence to the terminal
206+ * write(pty_fd, buf, written);
207+ * } else if (result == GHOSTTY_OUT_OF_MEMORY) {
208+ * // Buffer too small, written contains required size
209+ * char *dynamic_buf = malloc(written);
210+ * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written);
211+ * assert(result == GHOSTTY_SUCCESS);
212+ * write(pty_fd, dynamic_buf, written);
213+ * free(dynamic_buf);
214+ * }
215+ * @endcode
216+ *
217+ * @ingroup key
218+ */
219+GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len);
220+
221+#endif /* GHOSTTY_VT_KEY_ENCODER_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/key/event.h+474−0
@@ -0,0 +1,474 @@
1+/**
2+ * @file event.h
3+ *
4+ * Key event representation and manipulation.
5+ */
6+
7+#ifndef GHOSTTY_VT_KEY_EVENT_H
8+#define GHOSTTY_VT_KEY_EVENT_H
9+
10+#include <stdbool.h>
11+#include <stddef.h>
12+#include <stdint.h>
13+#include <ghostty/vt/result.h>
14+#include <ghostty/vt/allocator.h>
15+
16+/**
17+ * Opaque handle to a key event.
18+ *
19+ * This handle represents a keyboard input event containing information about
20+ * the physical key pressed, modifiers, and generated text.
21+ *
22+ * @ingroup key
23+ */
24+typedef struct GhosttyKeyEvent *GhosttyKeyEvent;
25+
26+/**
27+ * Keyboard input event types.
28+ *
29+ * @ingroup key
30+ */
31+typedef enum {
32+ /** Key was released */
33+ GHOSTTY_KEY_ACTION_RELEASE = 0,
34+ /** Key was pressed */
35+ GHOSTTY_KEY_ACTION_PRESS = 1,
36+ /** Key is being repeated (held down) */
37+ GHOSTTY_KEY_ACTION_REPEAT = 2,
38+} GhosttyKeyAction;
39+
40+/**
41+ * Keyboard modifier keys bitmask.
42+ *
43+ * A bitmask representing all keyboard modifiers. This tracks which modifier keys
44+ * are pressed and, where supported by the platform, which side (left or right)
45+ * of each modifier is active.
46+ *
47+ * Use the GHOSTTY_MODS_* constants to test and set individual modifiers.
48+ *
49+ * Modifier side bits are only meaningful when the corresponding modifier bit is set.
50+ * Not all platforms support distinguishing between left and right modifier
51+ * keys and Ghostty is built to expect that some platforms may not provide this
52+ * information.
53+ *
54+ * @ingroup key
55+ */
56+typedef uint16_t GhosttyMods;
57+
58+/** Shift key is pressed */
59+#define GHOSTTY_MODS_SHIFT (1 << 0)
60+/** Control key is pressed */
61+#define GHOSTTY_MODS_CTRL (1 << 1)
62+/** Alt/Option key is pressed */
63+#define GHOSTTY_MODS_ALT (1 << 2)
64+/** Super/Command/Windows key is pressed */
65+#define GHOSTTY_MODS_SUPER (1 << 3)
66+/** Caps Lock is active */
67+#define GHOSTTY_MODS_CAPS_LOCK (1 << 4)
68+/** Num Lock is active */
69+#define GHOSTTY_MODS_NUM_LOCK (1 << 5)
70+
71+/**
72+ * Right shift is pressed (0 = left, 1 = right).
73+ * Only meaningful when GHOSTTY_MODS_SHIFT is set.
74+ */
75+#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6)
76+/**
77+ * Right ctrl is pressed (0 = left, 1 = right).
78+ * Only meaningful when GHOSTTY_MODS_CTRL is set.
79+ */
80+#define GHOSTTY_MODS_CTRL_SIDE (1 << 7)
81+/**
82+ * Right alt is pressed (0 = left, 1 = right).
83+ * Only meaningful when GHOSTTY_MODS_ALT is set.
84+ */
85+#define GHOSTTY_MODS_ALT_SIDE (1 << 8)
86+/**
87+ * Right super is pressed (0 = left, 1 = right).
88+ * Only meaningful when GHOSTTY_MODS_SUPER is set.
89+ */
90+#define GHOSTTY_MODS_SUPER_SIDE (1 << 9)
91+
92+/**
93+ * Physical key codes.
94+ *
95+ * The set of key codes that Ghostty is aware of. These represent physical keys
96+ * on the keyboard and are layout-independent. For example, the "a" key on a US
97+ * keyboard is the same as the "ф" key on a Russian keyboard, but both will
98+ * report the same key_a value.
99+ *
100+ * Layout-dependent strings are provided separately as UTF-8 text and are produced
101+ * by the platform. These values are based on the W3C UI Events KeyboardEvent code
102+ * standard. See: https://www.w3.org/TR/uievents-code
103+ *
104+ * @ingroup key
105+ */
106+typedef enum {
107+ GHOSTTY_KEY_UNIDENTIFIED = 0,
108+
109+ // Writing System Keys (W3C § 3.1.1)
110+ GHOSTTY_KEY_BACKQUOTE,
111+ GHOSTTY_KEY_BACKSLASH,
112+ GHOSTTY_KEY_BRACKET_LEFT,
113+ GHOSTTY_KEY_BRACKET_RIGHT,
114+ GHOSTTY_KEY_COMMA,
115+ GHOSTTY_KEY_DIGIT_0,
116+ GHOSTTY_KEY_DIGIT_1,
117+ GHOSTTY_KEY_DIGIT_2,
118+ GHOSTTY_KEY_DIGIT_3,
119+ GHOSTTY_KEY_DIGIT_4,
120+ GHOSTTY_KEY_DIGIT_5,
121+ GHOSTTY_KEY_DIGIT_6,
122+ GHOSTTY_KEY_DIGIT_7,
123+ GHOSTTY_KEY_DIGIT_8,
124+ GHOSTTY_KEY_DIGIT_9,
125+ GHOSTTY_KEY_EQUAL,
126+ GHOSTTY_KEY_INTL_BACKSLASH,
127+ GHOSTTY_KEY_INTL_RO,
128+ GHOSTTY_KEY_INTL_YEN,
129+ GHOSTTY_KEY_A,
130+ GHOSTTY_KEY_B,
131+ GHOSTTY_KEY_C,
132+ GHOSTTY_KEY_D,
133+ GHOSTTY_KEY_E,
134+ GHOSTTY_KEY_F,
135+ GHOSTTY_KEY_G,
136+ GHOSTTY_KEY_H,
137+ GHOSTTY_KEY_I,
138+ GHOSTTY_KEY_J,
139+ GHOSTTY_KEY_K,
140+ GHOSTTY_KEY_L,
141+ GHOSTTY_KEY_M,
142+ GHOSTTY_KEY_N,
143+ GHOSTTY_KEY_O,
144+ GHOSTTY_KEY_P,
145+ GHOSTTY_KEY_Q,
146+ GHOSTTY_KEY_R,
147+ GHOSTTY_KEY_S,
148+ GHOSTTY_KEY_T,
149+ GHOSTTY_KEY_U,
150+ GHOSTTY_KEY_V,
151+ GHOSTTY_KEY_W,
152+ GHOSTTY_KEY_X,
153+ GHOSTTY_KEY_Y,
154+ GHOSTTY_KEY_Z,
155+ GHOSTTY_KEY_MINUS,
156+ GHOSTTY_KEY_PERIOD,
157+ GHOSTTY_KEY_QUOTE,
158+ GHOSTTY_KEY_SEMICOLON,
159+ GHOSTTY_KEY_SLASH,
160+
161+ // Functional Keys (W3C § 3.1.2)
162+ GHOSTTY_KEY_ALT_LEFT,
163+ GHOSTTY_KEY_ALT_RIGHT,
164+ GHOSTTY_KEY_BACKSPACE,
165+ GHOSTTY_KEY_CAPS_LOCK,
166+ GHOSTTY_KEY_CONTEXT_MENU,
167+ GHOSTTY_KEY_CONTROL_LEFT,
168+ GHOSTTY_KEY_CONTROL_RIGHT,
169+ GHOSTTY_KEY_ENTER,
170+ GHOSTTY_KEY_META_LEFT,
171+ GHOSTTY_KEY_META_RIGHT,
172+ GHOSTTY_KEY_SHIFT_LEFT,
173+ GHOSTTY_KEY_SHIFT_RIGHT,
174+ GHOSTTY_KEY_SPACE,
175+ GHOSTTY_KEY_TAB,
176+ GHOSTTY_KEY_CONVERT,
177+ GHOSTTY_KEY_KANA_MODE,
178+ GHOSTTY_KEY_NON_CONVERT,
179+
180+ // Control Pad Section (W3C § 3.2)
181+ GHOSTTY_KEY_DELETE,
182+ GHOSTTY_KEY_END,
183+ GHOSTTY_KEY_HELP,
184+ GHOSTTY_KEY_HOME,
185+ GHOSTTY_KEY_INSERT,
186+ GHOSTTY_KEY_PAGE_DOWN,
187+ GHOSTTY_KEY_PAGE_UP,
188+
189+ // Arrow Pad Section (W3C § 3.3)
190+ GHOSTTY_KEY_ARROW_DOWN,
191+ GHOSTTY_KEY_ARROW_LEFT,
192+ GHOSTTY_KEY_ARROW_RIGHT,
193+ GHOSTTY_KEY_ARROW_UP,
194+
195+ // Numpad Section (W3C § 3.4)
196+ GHOSTTY_KEY_NUM_LOCK,
197+ GHOSTTY_KEY_NUMPAD_0,
198+ GHOSTTY_KEY_NUMPAD_1,
199+ GHOSTTY_KEY_NUMPAD_2,
200+ GHOSTTY_KEY_NUMPAD_3,
201+ GHOSTTY_KEY_NUMPAD_4,
202+ GHOSTTY_KEY_NUMPAD_5,
203+ GHOSTTY_KEY_NUMPAD_6,
204+ GHOSTTY_KEY_NUMPAD_7,
205+ GHOSTTY_KEY_NUMPAD_8,
206+ GHOSTTY_KEY_NUMPAD_9,
207+ GHOSTTY_KEY_NUMPAD_ADD,
208+ GHOSTTY_KEY_NUMPAD_BACKSPACE,
209+ GHOSTTY_KEY_NUMPAD_CLEAR,
210+ GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
211+ GHOSTTY_KEY_NUMPAD_COMMA,
212+ GHOSTTY_KEY_NUMPAD_DECIMAL,
213+ GHOSTTY_KEY_NUMPAD_DIVIDE,
214+ GHOSTTY_KEY_NUMPAD_ENTER,
215+ GHOSTTY_KEY_NUMPAD_EQUAL,
216+ GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
217+ GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
218+ GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
219+ GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
220+ GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
221+ GHOSTTY_KEY_NUMPAD_MULTIPLY,
222+ GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
223+ GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
224+ GHOSTTY_KEY_NUMPAD_SUBTRACT,
225+ GHOSTTY_KEY_NUMPAD_SEPARATOR,
226+ GHOSTTY_KEY_NUMPAD_UP,
227+ GHOSTTY_KEY_NUMPAD_DOWN,
228+ GHOSTTY_KEY_NUMPAD_RIGHT,
229+ GHOSTTY_KEY_NUMPAD_LEFT,
230+ GHOSTTY_KEY_NUMPAD_BEGIN,
231+ GHOSTTY_KEY_NUMPAD_HOME,
232+ GHOSTTY_KEY_NUMPAD_END,
233+ GHOSTTY_KEY_NUMPAD_INSERT,
234+ GHOSTTY_KEY_NUMPAD_DELETE,
235+ GHOSTTY_KEY_NUMPAD_PAGE_UP,
236+ GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
237+
238+ // Function Section (W3C § 3.5)
239+ GHOSTTY_KEY_ESCAPE,
240+ GHOSTTY_KEY_F1,
241+ GHOSTTY_KEY_F2,
242+ GHOSTTY_KEY_F3,
243+ GHOSTTY_KEY_F4,
244+ GHOSTTY_KEY_F5,
245+ GHOSTTY_KEY_F6,
246+ GHOSTTY_KEY_F7,
247+ GHOSTTY_KEY_F8,
248+ GHOSTTY_KEY_F9,
249+ GHOSTTY_KEY_F10,
250+ GHOSTTY_KEY_F11,
251+ GHOSTTY_KEY_F12,
252+ GHOSTTY_KEY_F13,
253+ GHOSTTY_KEY_F14,
254+ GHOSTTY_KEY_F15,
255+ GHOSTTY_KEY_F16,
256+ GHOSTTY_KEY_F17,
257+ GHOSTTY_KEY_F18,
258+ GHOSTTY_KEY_F19,
259+ GHOSTTY_KEY_F20,
260+ GHOSTTY_KEY_F21,
261+ GHOSTTY_KEY_F22,
262+ GHOSTTY_KEY_F23,
263+ GHOSTTY_KEY_F24,
264+ GHOSTTY_KEY_F25,
265+ GHOSTTY_KEY_FN,
266+ GHOSTTY_KEY_FN_LOCK,
267+ GHOSTTY_KEY_PRINT_SCREEN,
268+ GHOSTTY_KEY_SCROLL_LOCK,
269+ GHOSTTY_KEY_PAUSE,
270+
271+ // Media Keys (W3C § 3.6)
272+ GHOSTTY_KEY_BROWSER_BACK,
273+ GHOSTTY_KEY_BROWSER_FAVORITES,
274+ GHOSTTY_KEY_BROWSER_FORWARD,
275+ GHOSTTY_KEY_BROWSER_HOME,
276+ GHOSTTY_KEY_BROWSER_REFRESH,
277+ GHOSTTY_KEY_BROWSER_SEARCH,
278+ GHOSTTY_KEY_BROWSER_STOP,
279+ GHOSTTY_KEY_EJECT,
280+ GHOSTTY_KEY_LAUNCH_APP_1,
281+ GHOSTTY_KEY_LAUNCH_APP_2,
282+ GHOSTTY_KEY_LAUNCH_MAIL,
283+ GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
284+ GHOSTTY_KEY_MEDIA_SELECT,
285+ GHOSTTY_KEY_MEDIA_STOP,
286+ GHOSTTY_KEY_MEDIA_TRACK_NEXT,
287+ GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
288+ GHOSTTY_KEY_POWER,
289+ GHOSTTY_KEY_SLEEP,
290+ GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
291+ GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
292+ GHOSTTY_KEY_AUDIO_VOLUME_UP,
293+ GHOSTTY_KEY_WAKE_UP,
294+
295+ // Legacy, Non-standard, and Special Keys (W3C § 3.7)
296+ GHOSTTY_KEY_COPY,
297+ GHOSTTY_KEY_CUT,
298+ GHOSTTY_KEY_PASTE,
299+} GhosttyKey;
300+
301+/**
302+ * Create a new key event instance.
303+ *
304+ * Creates a new key event with default values. The event must be freed using
305+ * ghostty_key_event_free() when no longer needed.
306+ *
307+ * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
308+ * @param event Pointer to store the created key event handle
309+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
310+ *
311+ * @ingroup key
312+ */
313+GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event);
314+
315+/**
316+ * Free a key event instance.
317+ *
318+ * Releases all resources associated with the key event. After this call,
319+ * the event handle becomes invalid and must not be used.
320+ *
321+ * @param event The key event handle to free (may be NULL)
322+ *
323+ * @ingroup key
324+ */
325+void ghostty_key_event_free(GhosttyKeyEvent event);
326+
327+/**
328+ * Set the key action (press, release, repeat).
329+ *
330+ * @param event The key event handle, must not be NULL
331+ * @param action The action to set
332+ *
333+ * @ingroup key
334+ */
335+void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action);
336+
337+/**
338+ * Get the key action (press, release, repeat).
339+ *
340+ * @param event The key event handle, must not be NULL
341+ * @return The key action
342+ *
343+ * @ingroup key
344+ */
345+GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event);
346+
347+/**
348+ * Set the physical key code.
349+ *
350+ * @param event The key event handle, must not be NULL
351+ * @param key The physical key code to set
352+ *
353+ * @ingroup key
354+ */
355+void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key);
356+
357+/**
358+ * Get the physical key code.
359+ *
360+ * @param event The key event handle, must not be NULL
361+ * @return The physical key code
362+ *
363+ * @ingroup key
364+ */
365+GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event);
366+
367+/**
368+ * Set the modifier keys bitmask.
369+ *
370+ * @param event The key event handle, must not be NULL
371+ * @param mods The modifier keys bitmask to set
372+ *
373+ * @ingroup key
374+ */
375+void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods);
376+
377+/**
378+ * Get the modifier keys bitmask.
379+ *
380+ * @param event The key event handle, must not be NULL
381+ * @return The modifier keys bitmask
382+ *
383+ * @ingroup key
384+ */
385+GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event);
386+
387+/**
388+ * Set the consumed modifiers bitmask.
389+ *
390+ * @param event The key event handle, must not be NULL
391+ * @param consumed_mods The consumed modifiers bitmask to set
392+ *
393+ * @ingroup key
394+ */
395+void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods);
396+
397+/**
398+ * Get the consumed modifiers bitmask.
399+ *
400+ * @param event The key event handle, must not be NULL
401+ * @return The consumed modifiers bitmask
402+ *
403+ * @ingroup key
404+ */
405+GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event);
406+
407+/**
408+ * Set whether the key event is part of a composition sequence.
409+ *
410+ * @param event The key event handle, must not be NULL
411+ * @param composing Whether the key event is part of a composition sequence
412+ *
413+ * @ingroup key
414+ */
415+void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing);
416+
417+/**
418+ * Get whether the key event is part of a composition sequence.
419+ *
420+ * @param event The key event handle, must not be NULL
421+ * @return Whether the key event is part of a composition sequence
422+ *
423+ * @ingroup key
424+ */
425+bool ghostty_key_event_get_composing(GhosttyKeyEvent event);
426+
427+/**
428+ * Set the UTF-8 text generated by the key event.
429+ *
430+ * The key event does NOT take ownership of the text pointer. The caller
431+ * must ensure the string remains valid for the lifetime needed by the event.
432+ *
433+ * @param event The key event handle, must not be NULL
434+ * @param utf8 The UTF-8 text to set (or NULL for empty)
435+ * @param len Length of the UTF-8 text in bytes
436+ *
437+ * @ingroup key
438+ */
439+void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len);
440+
441+/**
442+ * Get the UTF-8 text generated by the key event.
443+ *
444+ * The returned pointer is valid until the event is freed or the UTF-8 text is modified.
445+ *
446+ * @param event The key event handle, must not be NULL
447+ * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL)
448+ * @return The UTF-8 text (or NULL for empty)
449+ *
450+ * @ingroup key
451+ */
452+const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len);
453+
454+/**
455+ * Set the unshifted Unicode codepoint.
456+ *
457+ * @param event The key event handle, must not be NULL
458+ * @param codepoint The unshifted Unicode codepoint to set
459+ *
460+ * @ingroup key
461+ */
462+void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint);
463+
464+/**
465+ * Get the unshifted Unicode codepoint.
466+ *
467+ * @param event The key event handle, must not be NULL
468+ * @return The unshifted Unicode codepoint
469+ *
470+ * @ingroup key
471+ */
472+uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event);
473+
474+#endif /* GHOSTTY_VT_KEY_EVENT_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/osc.h+233−0
@@ -0,0 +1,233 @@
1+/**
2+ * @file osc.h
3+ *
4+ * OSC (Operating System Command) sequence parser and command handling.
5+ */
6+
7+#ifndef GHOSTTY_VT_OSC_H
8+#define GHOSTTY_VT_OSC_H
9+
10+#include <stdbool.h>
11+#include <stddef.h>
12+#include <stdint.h>
13+#include <ghostty/vt/result.h>
14+#include <ghostty/vt/allocator.h>
15+
16+/**
17+ * Opaque handle to an OSC parser instance.
18+ *
19+ * This handle represents an OSC (Operating System Command) parser that can
20+ * be used to parse the contents of OSC sequences.
21+ *
22+ * @ingroup osc
23+ */
24+typedef struct GhosttyOscParser *GhosttyOscParser;
25+
26+/**
27+ * Opaque handle to a single OSC command.
28+ *
29+ * This handle represents a parsed OSC (Operating System Command) command.
30+ * The command can be queried for its type and associated data.
31+ *
32+ * @ingroup osc
33+ */
34+typedef struct GhosttyOscCommand *GhosttyOscCommand;
35+
36+/** @defgroup osc OSC Parser
37+ *
38+ * OSC (Operating System Command) sequence parser and command handling.
39+ *
40+ * The parser operates in a streaming fashion, processing input byte-by-byte
41+ * to handle OSC sequences that may arrive in fragments across multiple reads.
42+ * This interface makes it easy to integrate into most environments and avoids
43+ * over-allocating buffers.
44+ *
45+ * ## Basic Usage
46+ *
47+ * 1. Create a parser instance with ghostty_osc_new()
48+ * 2. Feed bytes to the parser using ghostty_osc_next()
49+ * 3. Finalize parsing with ghostty_osc_end() to get the command
50+ * 4. Query command type and extract data using ghostty_osc_command_type()
51+ * and ghostty_osc_command_data()
52+ * 5. Free the parser with ghostty_osc_free() when done
53+ *
54+ * @{
55+ */
56+
57+/**
58+ * OSC command types.
59+ *
60+ * @ingroup osc
61+ */
62+typedef enum {
63+ GHOSTTY_OSC_COMMAND_INVALID = 0,
64+ GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
65+ GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
66+ GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3,
67+ GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4,
68+ GHOSTTY_OSC_COMMAND_REPORT_PWD = 5,
69+ GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6,
70+ GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7,
71+ GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8,
72+ GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9,
73+ GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10,
74+ GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11,
75+ GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12,
76+ GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13,
77+ GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14,
78+ GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15,
79+ GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16,
80+ GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17,
81+ GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18,
82+ GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19,
83+ GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
84+ GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
85+ GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
86+} GhosttyOscCommandType;
87+
88+/**
89+ * OSC command data types.
90+ *
91+ * These values specify what type of data to extract from an OSC command
92+ * using `ghostty_osc_command_data`.
93+ *
94+ * @ingroup osc
95+ */
96+typedef enum {
97+ /** Invalid data type. Never results in any data extraction. */
98+ GHOSTTY_OSC_DATA_INVALID = 0,
99+
100+ /**
101+ * Window title string data.
102+ *
103+ * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE
104+ *
105+ * Output type: const char ** (pointer to null-terminated string)
106+ *
107+ * Lifetime: Valid until the next call to any ghostty_osc_* function with
108+ * the same parser instance. Memory is owned by the parser.
109+ */
110+ GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
111+} GhosttyOscCommandData;
112+
113+/**
114+ * Create a new OSC parser instance.
115+ *
116+ * Creates a new OSC (Operating System Command) parser using the provided
117+ * allocator. The parser must be freed using ghostty_vt_osc_free() when
118+ * no longer needed.
119+ *
120+ * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
121+ * @param parser Pointer to store the created parser handle
122+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
123+ *
124+ * @ingroup osc
125+ */
126+GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
127+
128+/**
129+ * Free an OSC parser instance.
130+ *
131+ * Releases all resources associated with the OSC parser. After this call,
132+ * the parser handle becomes invalid and must not be used.
133+ *
134+ * @param parser The parser handle to free (may be NULL)
135+ *
136+ * @ingroup osc
137+ */
138+void ghostty_osc_free(GhosttyOscParser parser);
139+
140+/**
141+ * Reset an OSC parser instance to its initial state.
142+ *
143+ * Resets the parser state, clearing any partially parsed OSC sequences
144+ * and returning the parser to its initial state. This is useful for
145+ * reusing a parser instance or recovering from parse errors.
146+ *
147+ * @param parser The parser handle to reset, must not be null.
148+ *
149+ * @ingroup osc
150+ */
151+void ghostty_osc_reset(GhosttyOscParser parser);
152+
153+/**
154+ * Parse the next byte in an OSC sequence.
155+ *
156+ * Processes a single byte as part of an OSC sequence. The parser maintains
157+ * internal state to track the progress through the sequence. Call this
158+ * function for each byte in the sequence data.
159+ *
160+ * When finished pumping the parser with bytes, call ghostty_osc_end
161+ * to get the final result.
162+ *
163+ * @param parser The parser handle, must not be null.
164+ * @param byte The next byte to parse
165+ *
166+ * @ingroup osc
167+ */
168+void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
169+
170+/**
171+ * Finalize OSC parsing and retrieve the parsed command.
172+ *
173+ * Call this function after feeding all bytes of an OSC sequence to the parser
174+ * using ghostty_osc_next() with the exception of the terminating character
175+ * (ESC or ST). This function finalizes the parsing process and returns the
176+ * parsed OSC command.
177+ *
178+ * The return value is never NULL. Invalid commands will return a command
179+ * with type GHOSTTY_OSC_COMMAND_INVALID.
180+ *
181+ * The terminator parameter specifies the byte that terminated the OSC sequence
182+ * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
183+ * preserved in the parsed command so that responses can use the same terminator
184+ * format for better compatibility with the calling program. For commands that
185+ * do not require a response, this parameter is ignored and the resulting
186+ * command will not retain the terminator information.
187+ *
188+ * The returned command handle is valid until the next call to any
189+ * `ghostty_osc_*` function with the same parser instance with the exception
190+ * of command introspection functions such as `ghostty_osc_command_type`.
191+ *
192+ * @param parser The parser handle, must not be null.
193+ * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
194+ * @return Handle to the parsed OSC command
195+ *
196+ * @ingroup osc
197+ */
198+GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
199+
200+/**
201+ * Get the type of an OSC command.
202+ *
203+ * Returns the type identifier for the given OSC command. This can be used
204+ * to determine what kind of command was parsed and what data might be
205+ * available from it.
206+ *
207+ * @param command The OSC command handle to query (may be NULL)
208+ * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
209+ *
210+ * @ingroup osc
211+ */
212+GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
213+
214+/**
215+ * Extract data from an OSC command.
216+ *
217+ * Extracts typed data from the given OSC command based on the specified
218+ * data type. The output pointer must be of the appropriate type for the
219+ * requested data kind. Valid command types, output types, and memory
220+ * safety information are documented in the `GhosttyOscCommandData` enum.
221+ *
222+ * @param command The OSC command handle to query (may be NULL)
223+ * @param data The type of data to extract
224+ * @param out Pointer to store the extracted data (type depends on data parameter)
225+ * @return true if data extraction was successful, false otherwise
226+ *
227+ * @ingroup osc
228+ */
229+bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out);
230+
231+/** @} */
232+
233+#endif /* GHOSTTY_VT_OSC_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/paste.h+75−0
@@ -0,0 +1,75 @@
1+/**
2+ * @file paste.h
3+ *
4+ * Paste utilities - validate and encode paste data for terminal input.
5+ */
6+
7+#ifndef GHOSTTY_VT_PASTE_H
8+#define GHOSTTY_VT_PASTE_H
9+
10+/** @defgroup paste Paste Utilities
11+ *
12+ * Utilities for validating paste data safety.
13+ *
14+ * ## Basic Usage
15+ *
16+ * Use ghostty_paste_is_safe() to check if paste data contains potentially
17+ * dangerous sequences before sending it to the terminal.
18+ *
19+ * ## Example
20+ *
21+ * @code{.c}
22+ * #include <stdio.h>
23+ * #include <string.h>
24+ * #include <ghostty/vt.h>
25+ *
26+ * int main() {
27+ * const char* safe_data = "hello world";
28+ * const char* unsafe_data = "rm -rf /\n";
29+ *
30+ * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
31+ * printf("Safe to paste\n");
32+ * }
33+ *
34+ * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) {
35+ * printf("Unsafe! Contains newline\n");
36+ * }
37+ *
38+ * return 0;
39+ * }
40+ * @endcode
41+ *
42+ * @{
43+ */
44+
45+#include <stdbool.h>
46+#include <stddef.h>
47+
48+#ifdef __cplusplus
49+extern "C" {
50+#endif
51+
52+/**
53+ * Check if paste data is safe to paste into the terminal.
54+ *
55+ * Data is considered unsafe if it contains:
56+ * - Newlines (`\n`) which can inject commands
57+ * - The bracketed paste end sequence (`\x1b[201~`) which can be used
58+ * to exit bracketed paste mode and inject commands
59+ *
60+ * This check is conservative and considers data unsafe regardless of
61+ * current terminal state.
62+ *
63+ * @param data The paste data to check (must not be NULL)
64+ * @param len The length of the data in bytes
65+ * @return true if the data is safe to paste, false otherwise
66+ */
67+bool ghostty_paste_is_safe(const char* data, size_t len);
68+
69+#ifdef __cplusplus
70+}
71+#endif
72+
73+/** @} */
74+
75+#endif /* GHOSTTY_VT_PASTE_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/result.h+22−0
@@ -0,0 +1,22 @@
1+/**
2+ * @file result.h
3+ *
4+ * Result codes for libghostty-vt operations.
5+ */
6+
7+#ifndef GHOSTTY_VT_RESULT_H
8+#define GHOSTTY_VT_RESULT_H
9+
10+/**
11+ * Result codes for libghostty-vt operations.
12+ */
13+typedef enum {
14+ /** Operation completed successfully */
15+ GHOSTTY_SUCCESS = 0,
16+ /** Operation failed due to failed allocation */
17+ GHOSTTY_OUT_OF_MEMORY = -1,
18+ /** Operation failed due to invalid value */
19+ GHOSTTY_INVALID_VALUE = -2,
20+} GhosttyResult;
21+
22+#endif /* GHOSTTY_VT_RESULT_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/sgr.h+394−0
@@ -0,0 +1,394 @@
1+/**
2+ * @file sgr.h
3+ *
4+ * SGR (Select Graphic Rendition) attribute parsing and handling.
5+ */
6+
7+#ifndef GHOSTTY_VT_SGR_H
8+#define GHOSTTY_VT_SGR_H
9+
10+/** @defgroup sgr SGR Parser
11+ *
12+ * SGR (Select Graphic Rendition) attribute parser.
13+ *
14+ * SGR sequences are the syntax used to set styling attributes such as
15+ * bold, italic, underline, and colors for text in terminal emulators.
16+ * For example, you may be familiar with sequences like `ESC[1;31m`. The
17+ * `1;31` is the SGR attribute list.
18+ *
19+ * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`)
20+ * and returns individual text attributes like bold, italic, colors, etc.
21+ * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed,
22+ * and handles various color formats including 8-color, 16-color, 256-color,
23+ * X11 named colors, and RGB in multiple formats.
24+ *
25+ * ## Basic Usage
26+ *
27+ * 1. Create a parser instance with ghostty_sgr_new()
28+ * 2. Set SGR parameters with ghostty_sgr_set_params()
29+ * 3. Iterate through attributes using ghostty_sgr_next()
30+ * 4. Free the parser with ghostty_sgr_free() when done
31+ *
32+ * ## Example
33+ *
34+ * @code{.c}
35+ * #include <assert.h>
36+ * #include <stdio.h>
37+ * #include <ghostty/vt.h>
38+ *
39+ * int main() {
40+ * // Create parser
41+ * GhosttySgrParser parser;
42+ * GhosttyResult result = ghostty_sgr_new(NULL, &parser);
43+ * assert(result == GHOSTTY_SUCCESS);
44+ *
45+ * // Parse "bold, red foreground" sequence: ESC[1;31m
46+ * uint16_t params[] = {1, 31};
47+ * result = ghostty_sgr_set_params(parser, params, NULL, 2);
48+ * assert(result == GHOSTTY_SUCCESS);
49+ *
50+ * // Iterate through attributes
51+ * GhosttySgrAttribute attr;
52+ * while (ghostty_sgr_next(parser, &attr)) {
53+ * switch (attr.tag) {
54+ * case GHOSTTY_SGR_ATTR_BOLD:
55+ * printf("Bold enabled\n");
56+ * break;
57+ * case GHOSTTY_SGR_ATTR_FG_8:
58+ * printf("Foreground color: %d\n", attr.value.fg_8);
59+ * break;
60+ * default:
61+ * break;
62+ * }
63+ * }
64+ *
65+ * // Cleanup
66+ * ghostty_sgr_free(parser);
67+ * return 0;
68+ * }
69+ * @endcode
70+ *
71+ * @{
72+ */
73+
74+#include <ghostty/vt/allocator.h>
75+#include <ghostty/vt/color.h>
76+#include <ghostty/vt/result.h>
77+#include <stdbool.h>
78+#include <stddef.h>
79+#include <stdint.h>
80+
81+#ifdef __cplusplus
82+extern "C" {
83+#endif
84+
85+/**
86+ * Opaque handle to an SGR parser instance.
87+ *
88+ * This handle represents an SGR (Select Graphic Rendition) parser that can
89+ * be used to parse SGR sequences and extract individual text attributes.
90+ *
91+ * @ingroup sgr
92+ */
93+typedef struct GhosttySgrParser* GhosttySgrParser;
94+
95+/**
96+ * SGR attribute tags.
97+ *
98+ * These values identify the type of an SGR attribute in a tagged union.
99+ * Use the tag to determine which field in the attribute value union to access.
100+ *
101+ * @ingroup sgr
102+ */
103+typedef enum {
104+ GHOSTTY_SGR_ATTR_UNSET = 0,
105+ GHOSTTY_SGR_ATTR_UNKNOWN = 1,
106+ GHOSTTY_SGR_ATTR_BOLD = 2,
107+ GHOSTTY_SGR_ATTR_RESET_BOLD = 3,
108+ GHOSTTY_SGR_ATTR_ITALIC = 4,
109+ GHOSTTY_SGR_ATTR_RESET_ITALIC = 5,
110+ GHOSTTY_SGR_ATTR_FAINT = 6,
111+ GHOSTTY_SGR_ATTR_UNDERLINE = 7,
112+ GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8,
113+ GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9,
114+ GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10,
115+ GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11,
116+ GHOSTTY_SGR_ATTR_OVERLINE = 12,
117+ GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13,
118+ GHOSTTY_SGR_ATTR_BLINK = 14,
119+ GHOSTTY_SGR_ATTR_RESET_BLINK = 15,
120+ GHOSTTY_SGR_ATTR_INVERSE = 16,
121+ GHOSTTY_SGR_ATTR_RESET_INVERSE = 17,
122+ GHOSTTY_SGR_ATTR_INVISIBLE = 18,
123+ GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19,
124+ GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20,
125+ GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21,
126+ GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22,
127+ GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23,
128+ GHOSTTY_SGR_ATTR_BG_8 = 24,
129+ GHOSTTY_SGR_ATTR_FG_8 = 25,
130+ GHOSTTY_SGR_ATTR_RESET_FG = 26,
131+ GHOSTTY_SGR_ATTR_RESET_BG = 27,
132+ GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28,
133+ GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29,
134+ GHOSTTY_SGR_ATTR_BG_256 = 30,
135+ GHOSTTY_SGR_ATTR_FG_256 = 31,
136+} GhosttySgrAttributeTag;
137+
138+/**
139+ * Underline style types.
140+ *
141+ * @ingroup sgr
142+ */
143+typedef enum {
144+ GHOSTTY_SGR_UNDERLINE_NONE = 0,
145+ GHOSTTY_SGR_UNDERLINE_SINGLE = 1,
146+ GHOSTTY_SGR_UNDERLINE_DOUBLE = 2,
147+ GHOSTTY_SGR_UNDERLINE_CURLY = 3,
148+ GHOSTTY_SGR_UNDERLINE_DOTTED = 4,
149+ GHOSTTY_SGR_UNDERLINE_DASHED = 5,
150+} GhosttySgrUnderline;
151+
152+/**
153+ * Unknown SGR attribute data.
154+ *
155+ * Contains the full parameter list and the partial list where parsing
156+ * encountered an unknown or invalid sequence.
157+ *
158+ * @ingroup sgr
159+ */
160+typedef struct {
161+ const uint16_t* full_ptr;
162+ size_t full_len;
163+ const uint16_t* partial_ptr;
164+ size_t partial_len;
165+} GhosttySgrUnknown;
166+
167+/**
168+ * SGR attribute value union.
169+ *
170+ * This union contains all possible attribute values. Use the tag field
171+ * to determine which union member is active. Attributes without associated
172+ * data (like bold, italic) don't use the union value.
173+ *
174+ * @ingroup sgr
175+ */
176+typedef union {
177+ GhosttySgrUnknown unknown;
178+ GhosttySgrUnderline underline;
179+ GhosttyColorRgb underline_color;
180+ GhosttyColorPaletteIndex underline_color_256;
181+ GhosttyColorRgb direct_color_fg;
182+ GhosttyColorRgb direct_color_bg;
183+ GhosttyColorPaletteIndex bg_8;
184+ GhosttyColorPaletteIndex fg_8;
185+ GhosttyColorPaletteIndex bright_bg_8;
186+ GhosttyColorPaletteIndex bright_fg_8;
187+ GhosttyColorPaletteIndex bg_256;
188+ GhosttyColorPaletteIndex fg_256;
189+ uint64_t _padding[8];
190+} GhosttySgrAttributeValue;
191+
192+/**
193+ * SGR attribute (tagged union).
194+ *
195+ * A complete SGR attribute with both its type tag and associated value.
196+ * Always check the tag field to determine which value union member is valid.
197+ *
198+ * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be
199+ * identified by tag alone; the value union is not used for these and
200+ * the memory in the value field is undefined.
201+ *
202+ * @ingroup sgr
203+ */
204+typedef struct {
205+ GhosttySgrAttributeTag tag;
206+ GhosttySgrAttributeValue value;
207+} GhosttySgrAttribute;
208+
209+/**
210+ * Create a new SGR parser instance.
211+ *
212+ * Creates a new SGR (Select Graphic Rendition) parser using the provided
213+ * allocator. The parser must be freed using ghostty_sgr_free() when
214+ * no longer needed.
215+ *
216+ * @param allocator Pointer to the allocator to use for memory management, or
217+ * NULL to use the default allocator
218+ * @param parser Pointer to store the created parser handle
219+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
220+ *
221+ * @ingroup sgr
222+ */
223+GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator,
224+ GhosttySgrParser* parser);
225+
226+/**
227+ * Free an SGR parser instance.
228+ *
229+ * Releases all resources associated with the SGR parser. After this call,
230+ * the parser handle becomes invalid and must not be used. This includes
231+ * any attributes previously returned by ghostty_sgr_next().
232+ *
233+ * @param parser The parser handle to free (may be NULL)
234+ *
235+ * @ingroup sgr
236+ */
237+void ghostty_sgr_free(GhosttySgrParser parser);
238+
239+/**
240+ * Reset an SGR parser instance to the beginning of the parameter list.
241+ *
242+ * Resets the parser's iteration state without clearing the parameters.
243+ * After calling this, ghostty_sgr_next() will start from the beginning
244+ * of the parameter list again.
245+ *
246+ * @param parser The parser handle to reset, must not be NULL
247+ *
248+ * @ingroup sgr
249+ */
250+void ghostty_sgr_reset(GhosttySgrParser parser);
251+
252+/**
253+ * Set SGR parameters for parsing.
254+ *
255+ * Sets the SGR parameter list to parse. Parameters are the numeric values
256+ * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}).
257+ *
258+ * The separators array optionally specifies the separator type for each
259+ * parameter position. Each byte should be either ';' for semicolon or ':'
260+ * for colon. This is needed for certain color formats that use colon
261+ * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator
262+ * values are treated as semicolons. The separators array must have the same
263+ * length as the params array, if it is not NULL.
264+ *
265+ * If separators is NULL, all parameters are assumed to be semicolon-separated.
266+ *
267+ * This function makes an internal copy of the parameter and separator data,
268+ * so the caller can safely free or modify the input arrays after this call.
269+ *
270+ * After calling this function, the parser is automatically reset and ready
271+ * to iterate from the beginning.
272+ *
273+ * @param parser The parser handle, must not be NULL
274+ * @param params Array of SGR parameter values
275+ * @param separators Optional array of separator characters (';' or ':'), or
276+ * NULL
277+ * @param len Number of parameters (and separators if provided)
278+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
279+ *
280+ * @ingroup sgr
281+ */
282+GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser,
283+ const uint16_t* params,
284+ const char* separators,
285+ size_t len);
286+
287+/**
288+ * Get the next SGR attribute.
289+ *
290+ * Parses and returns the next attribute from the parameter list.
291+ * Call this function repeatedly until it returns false to process
292+ * all attributes in the sequence.
293+ *
294+ * @param parser The parser handle, must not be NULL
295+ * @param attr Pointer to store the next attribute
296+ * @return true if an attribute was returned, false if no more attributes
297+ *
298+ * @ingroup sgr
299+ */
300+bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr);
301+
302+/**
303+ * Get the full parameter list from an unknown SGR attribute.
304+ *
305+ * This function retrieves the full parameter list that was provided to the
306+ * parser when an unknown attribute was encountered. Primarily useful in
307+ * WebAssembly environments where accessing struct fields directly is difficult.
308+ *
309+ * @param unknown The unknown attribute data
310+ * @param ptr Pointer to store the pointer to the parameter array (may be NULL)
311+ * @return The length of the full parameter array
312+ *
313+ * @ingroup sgr
314+ */
315+size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown,
316+ const uint16_t** ptr);
317+
318+/**
319+ * Get the partial parameter list from an unknown SGR attribute.
320+ *
321+ * This function retrieves the partial parameter list where parsing stopped
322+ * when an unknown attribute was encountered. Primarily useful in WebAssembly
323+ * environments where accessing struct fields directly is difficult.
324+ *
325+ * @param unknown The unknown attribute data
326+ * @param ptr Pointer to store the pointer to the parameter array (may be NULL)
327+ * @return The length of the partial parameter array
328+ *
329+ * @ingroup sgr
330+ */
331+size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown,
332+ const uint16_t** ptr);
333+
334+/**
335+ * Get the tag from an SGR attribute.
336+ *
337+ * This function extracts the tag that identifies which type of attribute
338+ * this is. Primarily useful in WebAssembly environments where accessing
339+ * struct fields directly is difficult.
340+ *
341+ * @param attr The SGR attribute
342+ * @return The attribute tag
343+ *
344+ * @ingroup sgr
345+ */
346+GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr);
347+
348+/**
349+ * Get the value from an SGR attribute.
350+ *
351+ * This function returns a pointer to the value union from an SGR attribute. Use
352+ * the tag to determine which field of the union is valid. Primarily useful in
353+ * WebAssembly environments where accessing struct fields directly is difficult.
354+ *
355+ * @param attr Pointer to the SGR attribute
356+ * @return Pointer to the attribute value union
357+ *
358+ * @ingroup sgr
359+ */
360+GhosttySgrAttributeValue* ghostty_sgr_attribute_value(
361+ GhosttySgrAttribute* attr);
362+
363+#ifdef __wasm__
364+/**
365+ * Allocate memory for an SGR attribute (WebAssembly only).
366+ *
367+ * This is a convenience function for WebAssembly environments to allocate
368+ * memory for an SGR attribute structure that can be passed to ghostty_sgr_next.
369+ *
370+ * @return Pointer to the allocated attribute structure
371+ *
372+ * @ingroup wasm
373+ */
374+GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void);
375+
376+/**
377+ * Free memory for an SGR attribute (WebAssembly only).
378+ *
379+ * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute.
380+ *
381+ * @param attr Pointer to the attribute structure to free
382+ *
383+ * @ingroup wasm
384+ */
385+void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr);
386+#endif
387+
388+#ifdef __cplusplus
389+}
390+#endif
391+
392+/** @} */
393+
394+#endif /* GHOSTTY_VT_SGR_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/ghostty/vt/wasm.h+159−0
@@ -0,0 +1,159 @@
1+/**
2+ * @file wasm.h
3+ *
4+ * WebAssembly utility functions for libghostty-vt.
5+ */
6+
7+#ifndef GHOSTTY_VT_WASM_H
8+#define GHOSTTY_VT_WASM_H
9+
10+#ifdef __wasm__
11+
12+#include <stddef.h>
13+#include <stdint.h>
14+
15+/** @defgroup wasm WebAssembly Utilities
16+ *
17+ * Convenience functions for allocating various types in WebAssembly builds.
18+ * **These are only available the libghostty-vt wasm module.**
19+ *
20+ * Ghostty relies on pointers to various types for ABI compatibility, and
21+ * creating those pointers in Wasm can be tedious. These functions provide
22+ * a purely additive set of utilities that simplify memory management in
23+ * Wasm environments without changing the core C library API.
24+ *
25+ * @note These functions always use the default allocator. If you need
26+ * custom allocation strategies, you should allocate types manually using
27+ * your custom allocator. This is a very rare use case in the WebAssembly
28+ * world so these are optimized for simplicity.
29+ *
30+ * ## Example Usage
31+ *
32+ * Here's a simple example of using the Wasm utilities with the key encoder:
33+ *
34+ * @code
35+ * const { exports } = wasmInstance;
36+ * const view = new DataView(wasmMemory.buffer);
37+ *
38+ * // Create key encoder
39+ * const encoderPtr = exports.ghostty_wasm_alloc_opaque();
40+ * exports.ghostty_key_encoder_new(null, encoderPtr);
41+ * const encoder = view.getUint32(encoder, true);
42+ *
43+ * // Configure encoder with Kitty protocol flags
44+ * const flagsPtr = exports.ghostty_wasm_alloc_u8();
45+ * view.setUint8(flagsPtr, 0x1F);
46+ * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr);
47+ *
48+ * // Allocate output buffer and size pointer
49+ * const bufferSize = 32;
50+ * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize);
51+ * const writtenPtr = exports.ghostty_wasm_alloc_usize();
52+ *
53+ * // Encode the key event
54+ * exports.ghostty_key_encoder_encode(
55+ * encoder, eventPtr, bufPtr, bufferSize, writtenPtr
56+ * );
57+ *
58+ * // Read encoded output
59+ * const bytesWritten = view.getUint32(writtenPtr, true);
60+ * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten);
61+ * @endcode
62+ *
63+ * @remark The code above is pretty ugly! This is the lowest level interface
64+ * to the libghostty-vt Wasm module. In practice, this should be wrapped
65+ * in a higher-level API that abstracts away all this.
66+ *
67+ * @{
68+ */
69+
70+/**
71+ * Allocate an opaque pointer. This can be used for any opaque pointer
72+ * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc.
73+ *
74+ * @return Pointer to allocated opaque pointer, or NULL if allocation failed
75+ * @ingroup wasm
76+ */
77+void** ghostty_wasm_alloc_opaque(void);
78+
79+/**
80+ * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque().
81+ *
82+ * @param ptr Pointer to free, or NULL (NULL is safely ignored)
83+ * @ingroup wasm
84+ */
85+void ghostty_wasm_free_opaque(void **ptr);
86+
87+/**
88+ * Allocate an array of uint8_t values.
89+ *
90+ * @param len Number of uint8_t elements to allocate
91+ * @return Pointer to allocated array, or NULL if allocation failed
92+ * @ingroup wasm
93+ */
94+uint8_t* ghostty_wasm_alloc_u8_array(size_t len);
95+
96+/**
97+ * Free an array allocated by ghostty_wasm_alloc_u8_array().
98+ *
99+ * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
100+ * @param len Length of the array (must match the length passed to alloc)
101+ * @ingroup wasm
102+ */
103+void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len);
104+
105+/**
106+ * Allocate an array of uint16_t values.
107+ *
108+ * @param len Number of uint16_t elements to allocate
109+ * @return Pointer to allocated array, or NULL if allocation failed
110+ * @ingroup wasm
111+ */
112+uint16_t* ghostty_wasm_alloc_u16_array(size_t len);
113+
114+/**
115+ * Free an array allocated by ghostty_wasm_alloc_u16_array().
116+ *
117+ * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
118+ * @param len Length of the array (must match the length passed to alloc)
119+ * @ingroup wasm
120+ */
121+void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len);
122+
123+/**
124+ * Allocate a single uint8_t value.
125+ *
126+ * @return Pointer to allocated uint8_t, or NULL if allocation failed
127+ * @ingroup wasm
128+ */
129+uint8_t* ghostty_wasm_alloc_u8(void);
130+
131+/**
132+ * Free a uint8_t allocated by ghostty_wasm_alloc_u8().
133+ *
134+ * @param ptr Pointer to free, or NULL (NULL is safely ignored)
135+ * @ingroup wasm
136+ */
137+void ghostty_wasm_free_u8(uint8_t *ptr);
138+
139+/**
140+ * Allocate a single size_t value.
141+ *
142+ * @return Pointer to allocated size_t, or NULL if allocation failed
143+ * @ingroup wasm
144+ */
145+size_t* ghostty_wasm_alloc_usize(void);
146+
147+/**
148+ * Free a size_t allocated by ghostty_wasm_alloc_usize().
149+ *
150+ * @param ptr Pointer to free, or NULL (NULL is safely ignored)
151+ * @ingroup wasm
152+ */
153+void ghostty_wasm_free_usize(size_t *ptr);
154+
155+/** @} */
156+
157+#endif /* __wasm__ */
158+
159+#endif /* GHOSTTY_VT_WASM_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/Headers/module.modulemap+7−0
@@ -0,0 +1,7 @@
1+// This makes Ghostty available to the XCode build for the macOS app.
2+// We append "Kit" to it not to be cute, but because targets have to have
3+// unique names and we use Ghostty for other things.
4+module GhosttyKit {
5+ umbrella header "ghostty.h"
6+ export *
7+}
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a+0−0

Binary file not shown.

apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty.h+1187−0
@@ -0,0 +1,1187 @@
1+// Ghostty embedding API. The documentation for the embedding API is
2+// only within the Zig source files that define the implementations. This
3+// isn't meant to be a general purpose embedding API (yet) so there hasn't
4+// been documentation or example work beyond that.
5+//
6+// The only consumer of this API is the macOS app, but the API is built to
7+// be more general purpose.
8+#ifndef GHOSTTY_H
9+#define GHOSTTY_H
10+
11+#ifdef __cplusplus
12+extern "C" {
13+#endif
14+
15+#include <stdbool.h>
16+#include <stddef.h>
17+#include <stdint.h>
18+#include <sys/types.h>
19+
20+//-------------------------------------------------------------------
21+// Macros
22+
23+#define GHOSTTY_SUCCESS 0
24+
25+//-------------------------------------------------------------------
26+// Types
27+
28+// Opaque types
29+typedef void* ghostty_app_t;
30+typedef void* ghostty_config_t;
31+typedef void* ghostty_surface_t;
32+typedef void* ghostty_inspector_t;
33+
34+// All the types below are fully defined and must be kept in sync with
35+// their Zig counterparts. Any changes to these types MUST have an associated
36+// Zig change.
37+typedef enum {
38+ GHOSTTY_PLATFORM_INVALID,
39+ GHOSTTY_PLATFORM_MACOS,
40+ GHOSTTY_PLATFORM_IOS,
41+} ghostty_platform_e;
42+
43+// Callback for custom I/O write handler.
44+typedef void (*ghostty_surface_write_fn)(void* userdata,
45+ const uint8_t* data,
46+ size_t len);
47+
48+typedef enum {
49+ GHOSTTY_CLIPBOARD_STANDARD,
50+ GHOSTTY_CLIPBOARD_SELECTION,
51+} ghostty_clipboard_e;
52+
53+typedef struct {
54+ const char *mime;
55+ const char *data;
56+} ghostty_clipboard_content_s;
57+
58+typedef enum {
59+ GHOSTTY_CLIPBOARD_REQUEST_PASTE,
60+ GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
61+ GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
62+} ghostty_clipboard_request_e;
63+
64+typedef enum {
65+ GHOSTTY_MOUSE_RELEASE,
66+ GHOSTTY_MOUSE_PRESS,
67+} ghostty_input_mouse_state_e;
68+
69+typedef enum {
70+ GHOSTTY_MOUSE_UNKNOWN,
71+ GHOSTTY_MOUSE_LEFT,
72+ GHOSTTY_MOUSE_RIGHT,
73+ GHOSTTY_MOUSE_MIDDLE,
74+ GHOSTTY_MOUSE_FOUR,
75+ GHOSTTY_MOUSE_FIVE,
76+ GHOSTTY_MOUSE_SIX,
77+ GHOSTTY_MOUSE_SEVEN,
78+ GHOSTTY_MOUSE_EIGHT,
79+ GHOSTTY_MOUSE_NINE,
80+ GHOSTTY_MOUSE_TEN,
81+ GHOSTTY_MOUSE_ELEVEN,
82+} ghostty_input_mouse_button_e;
83+
84+typedef enum {
85+ GHOSTTY_MOUSE_MOMENTUM_NONE,
86+ GHOSTTY_MOUSE_MOMENTUM_BEGAN,
87+ GHOSTTY_MOUSE_MOMENTUM_STATIONARY,
88+ GHOSTTY_MOUSE_MOMENTUM_CHANGED,
89+ GHOSTTY_MOUSE_MOMENTUM_ENDED,
90+ GHOSTTY_MOUSE_MOMENTUM_CANCELLED,
91+ GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
92+} ghostty_input_mouse_momentum_e;
93+
94+typedef enum {
95+ GHOSTTY_COLOR_SCHEME_LIGHT = 0,
96+ GHOSTTY_COLOR_SCHEME_DARK = 1,
97+} ghostty_color_scheme_e;
98+
99+// This is a packed struct (see src/input/mouse.zig) but the C standard
100+// afaik doesn't let us reliably define packed structs so we build it up
101+// from scratch.
102+typedef int ghostty_input_scroll_mods_t;
103+
104+typedef enum {
105+ GHOSTTY_MODS_NONE = 0,
106+ GHOSTTY_MODS_SHIFT = 1 << 0,
107+ GHOSTTY_MODS_CTRL = 1 << 1,
108+ GHOSTTY_MODS_ALT = 1 << 2,
109+ GHOSTTY_MODS_SUPER = 1 << 3,
110+ GHOSTTY_MODS_CAPS = 1 << 4,
111+ GHOSTTY_MODS_NUM = 1 << 5,
112+ GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6,
113+ GHOSTTY_MODS_CTRL_RIGHT = 1 << 7,
114+ GHOSTTY_MODS_ALT_RIGHT = 1 << 8,
115+ GHOSTTY_MODS_SUPER_RIGHT = 1 << 9,
116+} ghostty_input_mods_e;
117+
118+typedef enum {
119+ GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0,
120+ GHOSTTY_BINDING_FLAGS_ALL = 1 << 1,
121+ GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2,
122+ GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3,
123+} ghostty_binding_flags_e;
124+
125+typedef enum {
126+ GHOSTTY_ACTION_RELEASE,
127+ GHOSTTY_ACTION_PRESS,
128+ GHOSTTY_ACTION_REPEAT,
129+} ghostty_input_action_e;
130+
131+// Based on: https://www.w3.org/TR/uievents-code/
132+typedef enum {
133+ GHOSTTY_KEY_UNIDENTIFIED,
134+
135+ // "Writing System Keys" § 3.1.1
136+ GHOSTTY_KEY_BACKQUOTE,
137+ GHOSTTY_KEY_BACKSLASH,
138+ GHOSTTY_KEY_BRACKET_LEFT,
139+ GHOSTTY_KEY_BRACKET_RIGHT,
140+ GHOSTTY_KEY_COMMA,
141+ GHOSTTY_KEY_DIGIT_0,
142+ GHOSTTY_KEY_DIGIT_1,
143+ GHOSTTY_KEY_DIGIT_2,
144+ GHOSTTY_KEY_DIGIT_3,
145+ GHOSTTY_KEY_DIGIT_4,
146+ GHOSTTY_KEY_DIGIT_5,
147+ GHOSTTY_KEY_DIGIT_6,
148+ GHOSTTY_KEY_DIGIT_7,
149+ GHOSTTY_KEY_DIGIT_8,
150+ GHOSTTY_KEY_DIGIT_9,
151+ GHOSTTY_KEY_EQUAL,
152+ GHOSTTY_KEY_INTL_BACKSLASH,
153+ GHOSTTY_KEY_INTL_RO,
154+ GHOSTTY_KEY_INTL_YEN,
155+ GHOSTTY_KEY_A,
156+ GHOSTTY_KEY_B,
157+ GHOSTTY_KEY_C,
158+ GHOSTTY_KEY_D,
159+ GHOSTTY_KEY_E,
160+ GHOSTTY_KEY_F,
161+ GHOSTTY_KEY_G,
162+ GHOSTTY_KEY_H,
163+ GHOSTTY_KEY_I,
164+ GHOSTTY_KEY_J,
165+ GHOSTTY_KEY_K,
166+ GHOSTTY_KEY_L,
167+ GHOSTTY_KEY_M,
168+ GHOSTTY_KEY_N,
169+ GHOSTTY_KEY_O,
170+ GHOSTTY_KEY_P,
171+ GHOSTTY_KEY_Q,
172+ GHOSTTY_KEY_R,
173+ GHOSTTY_KEY_S,
174+ GHOSTTY_KEY_T,
175+ GHOSTTY_KEY_U,
176+ GHOSTTY_KEY_V,
177+ GHOSTTY_KEY_W,
178+ GHOSTTY_KEY_X,
179+ GHOSTTY_KEY_Y,
180+ GHOSTTY_KEY_Z,
181+ GHOSTTY_KEY_MINUS,
182+ GHOSTTY_KEY_PERIOD,
183+ GHOSTTY_KEY_QUOTE,
184+ GHOSTTY_KEY_SEMICOLON,
185+ GHOSTTY_KEY_SLASH,
186+
187+ // "Functional Keys" § 3.1.2
188+ GHOSTTY_KEY_ALT_LEFT,
189+ GHOSTTY_KEY_ALT_RIGHT,
190+ GHOSTTY_KEY_BACKSPACE,
191+ GHOSTTY_KEY_CAPS_LOCK,
192+ GHOSTTY_KEY_CONTEXT_MENU,
193+ GHOSTTY_KEY_CONTROL_LEFT,
194+ GHOSTTY_KEY_CONTROL_RIGHT,
195+ GHOSTTY_KEY_ENTER,
196+ GHOSTTY_KEY_META_LEFT,
197+ GHOSTTY_KEY_META_RIGHT,
198+ GHOSTTY_KEY_SHIFT_LEFT,
199+ GHOSTTY_KEY_SHIFT_RIGHT,
200+ GHOSTTY_KEY_SPACE,
201+ GHOSTTY_KEY_TAB,
202+ GHOSTTY_KEY_CONVERT,
203+ GHOSTTY_KEY_KANA_MODE,
204+ GHOSTTY_KEY_NON_CONVERT,
205+
206+ // "Control Pad Section" § 3.2
207+ GHOSTTY_KEY_DELETE,
208+ GHOSTTY_KEY_END,
209+ GHOSTTY_KEY_HELP,
210+ GHOSTTY_KEY_HOME,
211+ GHOSTTY_KEY_INSERT,
212+ GHOSTTY_KEY_PAGE_DOWN,
213+ GHOSTTY_KEY_PAGE_UP,
214+
215+ // "Arrow Pad Section" § 3.3
216+ GHOSTTY_KEY_ARROW_DOWN,
217+ GHOSTTY_KEY_ARROW_LEFT,
218+ GHOSTTY_KEY_ARROW_RIGHT,
219+ GHOSTTY_KEY_ARROW_UP,
220+
221+ // "Numpad Section" § 3.4
222+ GHOSTTY_KEY_NUM_LOCK,
223+ GHOSTTY_KEY_NUMPAD_0,
224+ GHOSTTY_KEY_NUMPAD_1,
225+ GHOSTTY_KEY_NUMPAD_2,
226+ GHOSTTY_KEY_NUMPAD_3,
227+ GHOSTTY_KEY_NUMPAD_4,
228+ GHOSTTY_KEY_NUMPAD_5,
229+ GHOSTTY_KEY_NUMPAD_6,
230+ GHOSTTY_KEY_NUMPAD_7,
231+ GHOSTTY_KEY_NUMPAD_8,
232+ GHOSTTY_KEY_NUMPAD_9,
233+ GHOSTTY_KEY_NUMPAD_ADD,
234+ GHOSTTY_KEY_NUMPAD_BACKSPACE,
235+ GHOSTTY_KEY_NUMPAD_CLEAR,
236+ GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
237+ GHOSTTY_KEY_NUMPAD_COMMA,
238+ GHOSTTY_KEY_NUMPAD_DECIMAL,
239+ GHOSTTY_KEY_NUMPAD_DIVIDE,
240+ GHOSTTY_KEY_NUMPAD_ENTER,
241+ GHOSTTY_KEY_NUMPAD_EQUAL,
242+ GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
243+ GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
244+ GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
245+ GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
246+ GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
247+ GHOSTTY_KEY_NUMPAD_MULTIPLY,
248+ GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
249+ GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
250+ GHOSTTY_KEY_NUMPAD_SUBTRACT,
251+ GHOSTTY_KEY_NUMPAD_SEPARATOR,
252+ GHOSTTY_KEY_NUMPAD_UP,
253+ GHOSTTY_KEY_NUMPAD_DOWN,
254+ GHOSTTY_KEY_NUMPAD_RIGHT,
255+ GHOSTTY_KEY_NUMPAD_LEFT,
256+ GHOSTTY_KEY_NUMPAD_BEGIN,
257+ GHOSTTY_KEY_NUMPAD_HOME,
258+ GHOSTTY_KEY_NUMPAD_END,
259+ GHOSTTY_KEY_NUMPAD_INSERT,
260+ GHOSTTY_KEY_NUMPAD_DELETE,
261+ GHOSTTY_KEY_NUMPAD_PAGE_UP,
262+ GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
263+
264+ // "Function Section" § 3.5
265+ GHOSTTY_KEY_ESCAPE,
266+ GHOSTTY_KEY_F1,
267+ GHOSTTY_KEY_F2,
268+ GHOSTTY_KEY_F3,
269+ GHOSTTY_KEY_F4,
270+ GHOSTTY_KEY_F5,
271+ GHOSTTY_KEY_F6,
272+ GHOSTTY_KEY_F7,
273+ GHOSTTY_KEY_F8,
274+ GHOSTTY_KEY_F9,
275+ GHOSTTY_KEY_F10,
276+ GHOSTTY_KEY_F11,
277+ GHOSTTY_KEY_F12,
278+ GHOSTTY_KEY_F13,
279+ GHOSTTY_KEY_F14,
280+ GHOSTTY_KEY_F15,
281+ GHOSTTY_KEY_F16,
282+ GHOSTTY_KEY_F17,
283+ GHOSTTY_KEY_F18,
284+ GHOSTTY_KEY_F19,
285+ GHOSTTY_KEY_F20,
286+ GHOSTTY_KEY_F21,
287+ GHOSTTY_KEY_F22,
288+ GHOSTTY_KEY_F23,
289+ GHOSTTY_KEY_F24,
290+ GHOSTTY_KEY_F25,
291+ GHOSTTY_KEY_FN,
292+ GHOSTTY_KEY_FN_LOCK,
293+ GHOSTTY_KEY_PRINT_SCREEN,
294+ GHOSTTY_KEY_SCROLL_LOCK,
295+ GHOSTTY_KEY_PAUSE,
296+
297+ // "Media Keys" § 3.6
298+ GHOSTTY_KEY_BROWSER_BACK,
299+ GHOSTTY_KEY_BROWSER_FAVORITES,
300+ GHOSTTY_KEY_BROWSER_FORWARD,
301+ GHOSTTY_KEY_BROWSER_HOME,
302+ GHOSTTY_KEY_BROWSER_REFRESH,
303+ GHOSTTY_KEY_BROWSER_SEARCH,
304+ GHOSTTY_KEY_BROWSER_STOP,
305+ GHOSTTY_KEY_EJECT,
306+ GHOSTTY_KEY_LAUNCH_APP_1,
307+ GHOSTTY_KEY_LAUNCH_APP_2,
308+ GHOSTTY_KEY_LAUNCH_MAIL,
309+ GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
310+ GHOSTTY_KEY_MEDIA_SELECT,
311+ GHOSTTY_KEY_MEDIA_STOP,
312+ GHOSTTY_KEY_MEDIA_TRACK_NEXT,
313+ GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
314+ GHOSTTY_KEY_POWER,
315+ GHOSTTY_KEY_SLEEP,
316+ GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
317+ GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
318+ GHOSTTY_KEY_AUDIO_VOLUME_UP,
319+ GHOSTTY_KEY_WAKE_UP,
320+
321+ // "Legacy, Non-standard, and Special Keys" § 3.7
322+ GHOSTTY_KEY_COPY,
323+ GHOSTTY_KEY_CUT,
324+ GHOSTTY_KEY_PASTE,
325+} ghostty_input_key_e;
326+
327+typedef struct {
328+ ghostty_input_action_e action;
329+ ghostty_input_mods_e mods;
330+ ghostty_input_mods_e consumed_mods;
331+ uint32_t keycode;
332+ const char* text;
333+ uint32_t unshifted_codepoint;
334+ bool composing;
335+} ghostty_input_key_s;
336+
337+typedef enum {
338+ GHOSTTY_TRIGGER_PHYSICAL,
339+ GHOSTTY_TRIGGER_UNICODE,
340+ GHOSTTY_TRIGGER_CATCH_ALL,
341+} ghostty_input_trigger_tag_e;
342+
343+typedef union {
344+ ghostty_input_key_e translated;
345+ ghostty_input_key_e physical;
346+ uint32_t unicode;
347+ // catch_all has no payload
348+} ghostty_input_trigger_key_u;
349+
350+typedef struct {
351+ ghostty_input_trigger_tag_e tag;
352+ ghostty_input_trigger_key_u key;
353+ ghostty_input_mods_e mods;
354+} ghostty_input_trigger_s;
355+
356+typedef struct {
357+ const char* action_key;
358+ const char* action;
359+ const char* title;
360+ const char* description;
361+} ghostty_command_s;
362+
363+typedef enum {
364+ GHOSTTY_BUILD_MODE_DEBUG,
365+ GHOSTTY_BUILD_MODE_RELEASE_SAFE,
366+ GHOSTTY_BUILD_MODE_RELEASE_FAST,
367+ GHOSTTY_BUILD_MODE_RELEASE_SMALL,
368+} ghostty_build_mode_e;
369+
370+typedef struct {
371+ ghostty_build_mode_e build_mode;
372+ const char* version;
373+ uintptr_t version_len;
374+} ghostty_info_s;
375+
376+typedef struct {
377+ const char* message;
378+} ghostty_diagnostic_s;
379+
380+typedef struct {
381+ const char* ptr;
382+ uintptr_t len;
383+ bool sentinel;
384+} ghostty_string_s;
385+
386+typedef struct {
387+ double tl_px_x;
388+ double tl_px_y;
389+ uint32_t offset_start;
390+ uint32_t offset_len;
391+ const char* text;
392+ uintptr_t text_len;
393+} ghostty_text_s;
394+
395+typedef enum {
396+ GHOSTTY_POINT_ACTIVE,
397+ GHOSTTY_POINT_VIEWPORT,
398+ GHOSTTY_POINT_SCREEN,
399+ GHOSTTY_POINT_SURFACE,
400+} ghostty_point_tag_e;
401+
402+typedef enum {
403+ GHOSTTY_POINT_COORD_EXACT,
404+ GHOSTTY_POINT_COORD_TOP_LEFT,
405+ GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
406+} ghostty_point_coord_e;
407+
408+typedef struct {
409+ ghostty_point_tag_e tag;
410+ ghostty_point_coord_e coord;
411+ uint32_t x;
412+ uint32_t y;
413+} ghostty_point_s;
414+
415+typedef struct {
416+ ghostty_point_s top_left;
417+ ghostty_point_s bottom_right;
418+ bool rectangle;
419+} ghostty_selection_s;
420+
421+typedef struct {
422+ const char* key;
423+ const char* value;
424+} ghostty_env_var_s;
425+
426+typedef struct {
427+ void* nsview;
428+} ghostty_platform_macos_s;
429+
430+typedef struct {
431+ void* uiview;
432+} ghostty_platform_ios_s;
433+
434+typedef union {
435+ ghostty_platform_macos_s macos;
436+ ghostty_platform_ios_s ios;
437+} ghostty_platform_u;
438+
439+typedef enum {
440+ GHOSTTY_SURFACE_CONTEXT_WINDOW = 0,
441+ GHOSTTY_SURFACE_CONTEXT_TAB = 1,
442+ GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
443+} ghostty_surface_context_e;
444+
445+typedef struct {
446+ ghostty_platform_e platform_tag;
447+ ghostty_platform_u platform;
448+ void* userdata;
449+ double scale_factor;
450+ float font_size;
451+ const char* working_directory;
452+ const char* command;
453+ ghostty_env_var_s* env_vars;
454+ size_t env_var_count;
455+ const char* initial_input;
456+ bool wait_after_command;
457+ ghostty_surface_context_e context;
458+ bool use_custom_io;
459+} ghostty_surface_config_s;
460+
461+typedef struct {
462+ uint16_t columns;
463+ uint16_t rows;
464+ uint32_t width_px;
465+ uint32_t height_px;
466+ uint32_t cell_width_px;
467+ uint32_t cell_height_px;
468+} ghostty_surface_size_s;
469+
470+// Config types
471+
472+// config.Path
473+typedef struct {
474+ const char* path;
475+ bool optional;
476+} ghostty_config_path_s;
477+
478+// config.Color
479+typedef struct {
480+ uint8_t r;
481+ uint8_t g;
482+ uint8_t b;
483+} ghostty_config_color_s;
484+
485+// config.ColorList
486+typedef struct {
487+ const ghostty_config_color_s* colors;
488+ size_t len;
489+} ghostty_config_color_list_s;
490+
491+// config.RepeatableCommand
492+typedef struct {
493+ const ghostty_command_s* commands;
494+ size_t len;
495+} ghostty_config_command_list_s;
496+
497+// config.Palette
498+typedef struct {
499+ ghostty_config_color_s colors[256];
500+} ghostty_config_palette_s;
501+
502+// config.QuickTerminalSize
503+typedef enum {
504+ GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
505+ GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
506+ GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
507+} ghostty_quick_terminal_size_tag_e;
508+
509+typedef union {
510+ float percentage;
511+ uint32_t pixels;
512+} ghostty_quick_terminal_size_value_u;
513+
514+typedef struct {
515+ ghostty_quick_terminal_size_tag_e tag;
516+ ghostty_quick_terminal_size_value_u value;
517+} ghostty_quick_terminal_size_s;
518+
519+typedef struct {
520+ ghostty_quick_terminal_size_s primary;
521+ ghostty_quick_terminal_size_s secondary;
522+} ghostty_config_quick_terminal_size_s;
523+
524+// config.Fullscreen
525+typedef enum {
526+ GHOSTTY_CONFIG_FULLSCREEN_FALSE,
527+ GHOSTTY_CONFIG_FULLSCREEN_TRUE,
528+ GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE,
529+ GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
530+ GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
531+} ghostty_config_fullscreen_e;
532+
533+// apprt.Target.Key
534+typedef enum {
535+ GHOSTTY_TARGET_APP,
536+ GHOSTTY_TARGET_SURFACE,
537+} ghostty_target_tag_e;
538+
539+typedef union {
540+ ghostty_surface_t surface;
541+} ghostty_target_u;
542+
543+typedef struct {
544+ ghostty_target_tag_e tag;
545+ ghostty_target_u target;
546+} ghostty_target_s;
547+
548+// apprt.action.SplitDirection
549+typedef enum {
550+ GHOSTTY_SPLIT_DIRECTION_RIGHT,
551+ GHOSTTY_SPLIT_DIRECTION_DOWN,
552+ GHOSTTY_SPLIT_DIRECTION_LEFT,
553+ GHOSTTY_SPLIT_DIRECTION_UP,
554+} ghostty_action_split_direction_e;
555+
556+// apprt.action.GotoSplit
557+typedef enum {
558+ GHOSTTY_GOTO_SPLIT_PREVIOUS,
559+ GHOSTTY_GOTO_SPLIT_NEXT,
560+ GHOSTTY_GOTO_SPLIT_UP,
561+ GHOSTTY_GOTO_SPLIT_LEFT,
562+ GHOSTTY_GOTO_SPLIT_DOWN,
563+ GHOSTTY_GOTO_SPLIT_RIGHT,
564+} ghostty_action_goto_split_e;
565+
566+// apprt.action.GotoWindow
567+typedef enum {
568+ GHOSTTY_GOTO_WINDOW_PREVIOUS,
569+ GHOSTTY_GOTO_WINDOW_NEXT,
570+} ghostty_action_goto_window_e;
571+
572+// apprt.action.ResizeSplit.Direction
573+typedef enum {
574+ GHOSTTY_RESIZE_SPLIT_UP,
575+ GHOSTTY_RESIZE_SPLIT_DOWN,
576+ GHOSTTY_RESIZE_SPLIT_LEFT,
577+ GHOSTTY_RESIZE_SPLIT_RIGHT,
578+} ghostty_action_resize_split_direction_e;
579+
580+// apprt.action.ResizeSplit
581+typedef struct {
582+ uint16_t amount;
583+ ghostty_action_resize_split_direction_e direction;
584+} ghostty_action_resize_split_s;
585+
586+// apprt.action.MoveTab
587+typedef struct {
588+ ssize_t amount;
589+} ghostty_action_move_tab_s;
590+
591+// apprt.action.GotoTab
592+typedef enum {
593+ GHOSTTY_GOTO_TAB_PREVIOUS = -1,
594+ GHOSTTY_GOTO_TAB_NEXT = -2,
595+ GHOSTTY_GOTO_TAB_LAST = -3,
596+} ghostty_action_goto_tab_e;
597+
598+// apprt.action.Fullscreen
599+typedef enum {
600+ GHOSTTY_FULLSCREEN_NATIVE,
601+ GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE,
602+ GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU,
603+ GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH,
604+} ghostty_action_fullscreen_e;
605+
606+// apprt.action.FloatWindow
607+typedef enum {
608+ GHOSTTY_FLOAT_WINDOW_ON,
609+ GHOSTTY_FLOAT_WINDOW_OFF,
610+ GHOSTTY_FLOAT_WINDOW_TOGGLE,
611+} ghostty_action_float_window_e;
612+
613+// apprt.action.SecureInput
614+typedef enum {
615+ GHOSTTY_SECURE_INPUT_ON,
616+ GHOSTTY_SECURE_INPUT_OFF,
617+ GHOSTTY_SECURE_INPUT_TOGGLE,
618+} ghostty_action_secure_input_e;
619+
620+// apprt.action.Inspector
621+typedef enum {
622+ GHOSTTY_INSPECTOR_TOGGLE,
623+ GHOSTTY_INSPECTOR_SHOW,
624+ GHOSTTY_INSPECTOR_HIDE,
625+} ghostty_action_inspector_e;
626+
627+// apprt.action.QuitTimer
628+typedef enum {
629+ GHOSTTY_QUIT_TIMER_START,
630+ GHOSTTY_QUIT_TIMER_STOP,
631+} ghostty_action_quit_timer_e;
632+
633+// apprt.action.Readonly
634+typedef enum {
635+ GHOSTTY_READONLY_OFF,
636+ GHOSTTY_READONLY_ON,
637+} ghostty_action_readonly_e;
638+
639+// apprt.action.DesktopNotification.C
640+typedef struct {
641+ const char* title;
642+ const char* body;
643+} ghostty_action_desktop_notification_s;
644+
645+// apprt.action.SetTitle.C
646+typedef struct {
647+ const char* title;
648+} ghostty_action_set_title_s;
649+
650+// apprt.action.PromptTitle
651+typedef enum {
652+ GHOSTTY_PROMPT_TITLE_SURFACE,
653+ GHOSTTY_PROMPT_TITLE_TAB,
654+} ghostty_action_prompt_title_e;
655+
656+// apprt.action.Pwd.C
657+typedef struct {
658+ const char* pwd;
659+} ghostty_action_pwd_s;
660+
661+// terminal.MouseShape
662+typedef enum {
663+ GHOSTTY_MOUSE_SHAPE_DEFAULT,
664+ GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
665+ GHOSTTY_MOUSE_SHAPE_HELP,
666+ GHOSTTY_MOUSE_SHAPE_POINTER,
667+ GHOSTTY_MOUSE_SHAPE_PROGRESS,
668+ GHOSTTY_MOUSE_SHAPE_WAIT,
669+ GHOSTTY_MOUSE_SHAPE_CELL,
670+ GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
671+ GHOSTTY_MOUSE_SHAPE_TEXT,
672+ GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
673+ GHOSTTY_MOUSE_SHAPE_ALIAS,
674+ GHOSTTY_MOUSE_SHAPE_COPY,
675+ GHOSTTY_MOUSE_SHAPE_MOVE,
676+ GHOSTTY_MOUSE_SHAPE_NO_DROP,
677+ GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
678+ GHOSTTY_MOUSE_SHAPE_GRAB,
679+ GHOSTTY_MOUSE_SHAPE_GRABBING,
680+ GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
681+ GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
682+ GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
683+ GHOSTTY_MOUSE_SHAPE_N_RESIZE,
684+ GHOSTTY_MOUSE_SHAPE_E_RESIZE,
685+ GHOSTTY_MOUSE_SHAPE_S_RESIZE,
686+ GHOSTTY_MOUSE_SHAPE_W_RESIZE,
687+ GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
688+ GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
689+ GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
690+ GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
691+ GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
692+ GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
693+ GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
694+ GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
695+ GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
696+ GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
697+} ghostty_action_mouse_shape_e;
698+
699+// apprt.action.MouseVisibility
700+typedef enum {
701+ GHOSTTY_MOUSE_VISIBLE,
702+ GHOSTTY_MOUSE_HIDDEN,
703+} ghostty_action_mouse_visibility_e;
704+
705+// apprt.action.MouseOverLink
706+typedef struct {
707+ const char* url;
708+ size_t len;
709+} ghostty_action_mouse_over_link_s;
710+
711+// apprt.action.SizeLimit
712+typedef struct {
713+ uint32_t min_width;
714+ uint32_t min_height;
715+ uint32_t max_width;
716+ uint32_t max_height;
717+} ghostty_action_size_limit_s;
718+
719+// apprt.action.InitialSize
720+typedef struct {
721+ uint32_t width;
722+ uint32_t height;
723+} ghostty_action_initial_size_s;
724+
725+// apprt.action.CellSize
726+typedef struct {
727+ uint32_t width;
728+ uint32_t height;
729+} ghostty_action_cell_size_s;
730+
731+// renderer.Health
732+typedef enum {
733+ GHOSTTY_RENDERER_HEALTH_HEALTHY,
734+ GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
735+} ghostty_action_renderer_health_e;
736+
737+// apprt.action.KeySequence
738+typedef struct {
739+ bool active;
740+ ghostty_input_trigger_s trigger;
741+} ghostty_action_key_sequence_s;
742+
743+// apprt.action.KeyTable.Tag
744+typedef enum {
745+ GHOSTTY_KEY_TABLE_ACTIVATE,
746+ GHOSTTY_KEY_TABLE_DEACTIVATE,
747+ GHOSTTY_KEY_TABLE_DEACTIVATE_ALL,
748+} ghostty_action_key_table_tag_e;
749+
750+// apprt.action.KeyTable.CValue
751+typedef union {
752+ struct {
753+ const char *name;
754+ size_t len;
755+ } activate;
756+} ghostty_action_key_table_u;
757+
758+// apprt.action.KeyTable.C
759+typedef struct {
760+ ghostty_action_key_table_tag_e tag;
761+ ghostty_action_key_table_u value;
762+} ghostty_action_key_table_s;
763+
764+// apprt.action.ColorKind
765+typedef enum {
766+ GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
767+ GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
768+ GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
769+} ghostty_action_color_kind_e;
770+
771+// apprt.action.ColorChange
772+typedef struct {
773+ ghostty_action_color_kind_e kind;
774+ uint8_t r;
775+ uint8_t g;
776+ uint8_t b;
777+} ghostty_action_color_change_s;
778+
779+// apprt.action.ConfigChange
780+typedef struct {
781+ ghostty_config_t config;
782+} ghostty_action_config_change_s;
783+
784+// apprt.action.ReloadConfig
785+typedef struct {
786+ bool soft;
787+} ghostty_action_reload_config_s;
788+
789+// apprt.action.OpenUrlKind
790+typedef enum {
791+ GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
792+ GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
793+ GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
794+} ghostty_action_open_url_kind_e;
795+
796+// apprt.action.OpenUrl.C
797+typedef struct {
798+ ghostty_action_open_url_kind_e kind;
799+ const char* url;
800+ uintptr_t len;
801+} ghostty_action_open_url_s;
802+
803+// apprt.action.CloseTabMode
804+typedef enum {
805+ GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
806+ GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
807+ GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
808+} ghostty_action_close_tab_mode_e;
809+
810+// apprt.surface.Message.ChildExited
811+typedef struct {
812+ uint32_t exit_code;
813+ uint64_t timetime_ms;
814+} ghostty_surface_message_childexited_s;
815+
816+// terminal.osc.Command.ProgressReport.State
817+typedef enum {
818+ GHOSTTY_PROGRESS_STATE_REMOVE,
819+ GHOSTTY_PROGRESS_STATE_SET,
820+ GHOSTTY_PROGRESS_STATE_ERROR,
821+ GHOSTTY_PROGRESS_STATE_INDETERMINATE,
822+ GHOSTTY_PROGRESS_STATE_PAUSE,
823+} ghostty_action_progress_report_state_e;
824+
825+// terminal.osc.Command.ProgressReport.C
826+typedef struct {
827+ ghostty_action_progress_report_state_e state;
828+ // -1 if no progress was reported, otherwise 0-100 indicating percent
829+ // completeness.
830+ int8_t progress;
831+} ghostty_action_progress_report_s;
832+
833+// apprt.action.CommandFinished.C
834+typedef struct {
835+ // -1 if no exit code was reported, otherwise 0-255
836+ int16_t exit_code;
837+ // number of nanoseconds that command was running for
838+ uint64_t duration;
839+} ghostty_action_command_finished_s;
840+
841+// apprt.action.StartSearch.C
842+typedef struct {
843+ const char* needle;
844+} ghostty_action_start_search_s;
845+
846+// apprt.action.SearchTotal
847+typedef struct {
848+ ssize_t total;
849+} ghostty_action_search_total_s;
850+
851+// apprt.action.SearchSelected
852+typedef struct {
853+ ssize_t selected;
854+} ghostty_action_search_selected_s;
855+
856+// terminal.Scrollbar
857+typedef struct {
858+ uint64_t total;
859+ uint64_t offset;
860+ uint64_t len;
861+} ghostty_action_scrollbar_s;
862+
863+// apprt.Action.Key
864+typedef enum {
865+ GHOSTTY_ACTION_QUIT,
866+ GHOSTTY_ACTION_NEW_WINDOW,
867+ GHOSTTY_ACTION_NEW_TAB,
868+ GHOSTTY_ACTION_CLOSE_TAB,
869+ GHOSTTY_ACTION_NEW_SPLIT,
870+ GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
871+ GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
872+ GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
873+ GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
874+ GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
875+ GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
876+ GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
877+ GHOSTTY_ACTION_TOGGLE_VISIBILITY,
878+ GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY,
879+ GHOSTTY_ACTION_MOVE_TAB,
880+ GHOSTTY_ACTION_GOTO_TAB,
881+ GHOSTTY_ACTION_GOTO_SPLIT,
882+ GHOSTTY_ACTION_GOTO_WINDOW,
883+ GHOSTTY_ACTION_RESIZE_SPLIT,
884+ GHOSTTY_ACTION_EQUALIZE_SPLITS,
885+ GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
886+ GHOSTTY_ACTION_PRESENT_TERMINAL,
887+ GHOSTTY_ACTION_SIZE_LIMIT,
888+ GHOSTTY_ACTION_RESET_WINDOW_SIZE,
889+ GHOSTTY_ACTION_INITIAL_SIZE,
890+ GHOSTTY_ACTION_CELL_SIZE,
891+ GHOSTTY_ACTION_SCROLLBAR,
892+ GHOSTTY_ACTION_RENDER,
893+ GHOSTTY_ACTION_INSPECTOR,
894+ GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
895+ GHOSTTY_ACTION_RENDER_INSPECTOR,
896+ GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
897+ GHOSTTY_ACTION_SET_TITLE,
898+ GHOSTTY_ACTION_PROMPT_TITLE,
899+ GHOSTTY_ACTION_PWD,
900+ GHOSTTY_ACTION_MOUSE_SHAPE,
901+ GHOSTTY_ACTION_MOUSE_VISIBILITY,
902+ GHOSTTY_ACTION_MOUSE_OVER_LINK,
903+ GHOSTTY_ACTION_RENDERER_HEALTH,
904+ GHOSTTY_ACTION_OPEN_CONFIG,
905+ GHOSTTY_ACTION_QUIT_TIMER,
906+ GHOSTTY_ACTION_FLOAT_WINDOW,
907+ GHOSTTY_ACTION_SECURE_INPUT,
908+ GHOSTTY_ACTION_KEY_SEQUENCE,
909+ GHOSTTY_ACTION_KEY_TABLE,
910+ GHOSTTY_ACTION_COLOR_CHANGE,
911+ GHOSTTY_ACTION_RELOAD_CONFIG,
912+ GHOSTTY_ACTION_CONFIG_CHANGE,
913+ GHOSTTY_ACTION_CLOSE_WINDOW,
914+ GHOSTTY_ACTION_RING_BELL,
915+ GHOSTTY_ACTION_UNDO,
916+ GHOSTTY_ACTION_REDO,
917+ GHOSTTY_ACTION_CHECK_FOR_UPDATES,
918+ GHOSTTY_ACTION_OPEN_URL,
919+ GHOSTTY_ACTION_SHOW_CHILD_EXITED,
920+ GHOSTTY_ACTION_PROGRESS_REPORT,
921+ GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
922+ GHOSTTY_ACTION_COMMAND_FINISHED,
923+ GHOSTTY_ACTION_START_SEARCH,
924+ GHOSTTY_ACTION_END_SEARCH,
925+ GHOSTTY_ACTION_SEARCH_TOTAL,
926+ GHOSTTY_ACTION_SEARCH_SELECTED,
927+ GHOSTTY_ACTION_READONLY,
928+ GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
929+} ghostty_action_tag_e;
930+
931+typedef union {
932+ ghostty_action_split_direction_e new_split;
933+ ghostty_action_fullscreen_e toggle_fullscreen;
934+ ghostty_action_move_tab_s move_tab;
935+ ghostty_action_goto_tab_e goto_tab;
936+ ghostty_action_goto_split_e goto_split;
937+ ghostty_action_goto_window_e goto_window;
938+ ghostty_action_resize_split_s resize_split;
939+ ghostty_action_size_limit_s size_limit;
940+ ghostty_action_initial_size_s initial_size;
941+ ghostty_action_cell_size_s cell_size;
942+ ghostty_action_scrollbar_s scrollbar;
943+ ghostty_action_inspector_e inspector;
944+ ghostty_action_desktop_notification_s desktop_notification;
945+ ghostty_action_set_title_s set_title;
946+ ghostty_action_prompt_title_e prompt_title;
947+ ghostty_action_pwd_s pwd;
948+ ghostty_action_mouse_shape_e mouse_shape;
949+ ghostty_action_mouse_visibility_e mouse_visibility;
950+ ghostty_action_mouse_over_link_s mouse_over_link;
951+ ghostty_action_renderer_health_e renderer_health;
952+ ghostty_action_quit_timer_e quit_timer;
953+ ghostty_action_float_window_e float_window;
954+ ghostty_action_secure_input_e secure_input;
955+ ghostty_action_key_sequence_s key_sequence;
956+ ghostty_action_key_table_s key_table;
957+ ghostty_action_color_change_s color_change;
958+ ghostty_action_reload_config_s reload_config;
959+ ghostty_action_config_change_s config_change;
960+ ghostty_action_open_url_s open_url;
961+ ghostty_action_close_tab_mode_e close_tab_mode;
962+ ghostty_surface_message_childexited_s child_exited;
963+ ghostty_action_progress_report_s progress_report;
964+ ghostty_action_command_finished_s command_finished;
965+ ghostty_action_start_search_s start_search;
966+ ghostty_action_search_total_s search_total;
967+ ghostty_action_search_selected_s search_selected;
968+ ghostty_action_readonly_e readonly;
969+} ghostty_action_u;
970+
971+typedef struct {
972+ ghostty_action_tag_e tag;
973+ ghostty_action_u action;
974+} ghostty_action_s;
975+
976+typedef void (*ghostty_runtime_wakeup_cb)(void*);
977+typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
978+ ghostty_clipboard_e,
979+ void*);
980+typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
981+ void*,
982+ const char*,
983+ void*,
984+ ghostty_clipboard_request_e);
985+typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
986+ ghostty_clipboard_e,
987+ const ghostty_clipboard_content_s*,
988+ size_t,
989+ bool);
990+typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
991+typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
992+ ghostty_target_s,
993+ ghostty_action_s);
994+
995+typedef struct {
996+ void* userdata;
997+ bool supports_selection_clipboard;
998+ ghostty_runtime_wakeup_cb wakeup_cb;
999+ ghostty_runtime_action_cb action_cb;
1000+ ghostty_runtime_read_clipboard_cb read_clipboard_cb;
1001+ ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
1002+ ghostty_runtime_write_clipboard_cb write_clipboard_cb;
1003+ ghostty_runtime_close_surface_cb close_surface_cb;
1004+} ghostty_runtime_config_s;
1005+
1006+// apprt.ipc.Target.Key
1007+typedef enum {
1008+ GHOSTTY_IPC_TARGET_CLASS,
1009+ GHOSTTY_IPC_TARGET_DETECT,
1010+} ghostty_ipc_target_tag_e;
1011+
1012+typedef union {
1013+ char *klass;
1014+} ghostty_ipc_target_u;
1015+
1016+typedef struct {
1017+ ghostty_ipc_target_tag_e tag;
1018+ ghostty_ipc_target_u target;
1019+} chostty_ipc_target_s;
1020+
1021+// apprt.ipc.Action.NewWindow
1022+typedef struct {
1023+ // This should be a null terminated list of strings.
1024+ const char **arguments;
1025+} ghostty_ipc_action_new_window_s;
1026+
1027+typedef union {
1028+ ghostty_ipc_action_new_window_s new_window;
1029+} ghostty_ipc_action_u;
1030+
1031+// apprt.ipc.Action.Key
1032+typedef enum {
1033+ GHOSTTY_IPC_ACTION_NEW_WINDOW,
1034+} ghostty_ipc_action_tag_e;
1035+
1036+//-------------------------------------------------------------------
1037+// Published API
1038+
1039+int ghostty_init(uintptr_t, char**);
1040+void ghostty_cli_try_action(void);
1041+ghostty_info_s ghostty_info(void);
1042+const char* ghostty_translate(const char*);
1043+void ghostty_string_free(ghostty_string_s);
1044+
1045+ghostty_config_t ghostty_config_new();
1046+void ghostty_config_free(ghostty_config_t);
1047+ghostty_config_t ghostty_config_clone(ghostty_config_t);
1048+void ghostty_config_load_cli_args(ghostty_config_t);
1049+void ghostty_config_load_file(ghostty_config_t, const char*);
1050+void ghostty_config_load_default_files(ghostty_config_t);
1051+void ghostty_config_load_recursive_files(ghostty_config_t);
1052+void ghostty_config_finalize(ghostty_config_t);
1053+bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
1054+ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
1055+ const char*,
1056+ uintptr_t);
1057+uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
1058+ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
1059+ghostty_string_s ghostty_config_open_path(void);
1060+
1061+ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
1062+ ghostty_config_t);
1063+void ghostty_app_free(ghostty_app_t);
1064+void ghostty_app_tick(ghostty_app_t);
1065+void* ghostty_app_userdata(ghostty_app_t);
1066+void ghostty_app_set_focus(ghostty_app_t, bool);
1067+bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
1068+bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
1069+void ghostty_app_keyboard_changed(ghostty_app_t);
1070+void ghostty_app_open_config(ghostty_app_t);
1071+void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
1072+bool ghostty_app_needs_confirm_quit(ghostty_app_t);
1073+bool ghostty_app_has_global_keybinds(ghostty_app_t);
1074+void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
1075+
1076+ghostty_surface_config_s ghostty_surface_config_new();
1077+
1078+ghostty_surface_t ghostty_surface_new(ghostty_app_t,
1079+ const ghostty_surface_config_s*);
1080+void ghostty_surface_free(ghostty_surface_t);
1081+void* ghostty_surface_userdata(ghostty_surface_t);
1082+ghostty_app_t ghostty_surface_app(ghostty_surface_t);
1083+ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
1084+void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
1085+bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
1086+bool ghostty_surface_process_exited(ghostty_surface_t);
1087+void ghostty_surface_refresh(ghostty_surface_t);
1088+void ghostty_surface_draw(ghostty_surface_t);
1089+// Custom I/O API - feed output into the terminal and receive writes.
1090+void ghostty_surface_feed_data(ghostty_surface_t, const uint8_t*, size_t);
1091+void ghostty_surface_set_write_callback(ghostty_surface_t,
1092+ ghostty_surface_write_fn,
1093+ void*);
1094+void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
1095+void ghostty_surface_set_focus(ghostty_surface_t, bool);
1096+void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
1097+void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
1098+ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
1099+void ghostty_surface_set_color_scheme(ghostty_surface_t,
1100+ ghostty_color_scheme_e);
1101+ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
1102+ ghostty_input_mods_e);
1103+bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
1104+bool ghostty_surface_key_is_binding(ghostty_surface_t,
1105+ ghostty_input_key_s,
1106+ ghostty_binding_flags_e*);
1107+void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
1108+void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
1109+bool ghostty_surface_mouse_captured(ghostty_surface_t);
1110+bool ghostty_surface_mouse_button(ghostty_surface_t,
1111+ ghostty_input_mouse_state_e,
1112+ ghostty_input_mouse_button_e,
1113+ ghostty_input_mods_e);
1114+void ghostty_surface_mouse_pos(ghostty_surface_t,
1115+ double,
1116+ double,
1117+ ghostty_input_mods_e);
1118+void ghostty_surface_mouse_scroll(ghostty_surface_t,
1119+ double,
1120+ double,
1121+ ghostty_input_scroll_mods_t);
1122+void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
1123+void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
1124+void ghostty_surface_request_close(ghostty_surface_t);
1125+void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
1126+void ghostty_surface_split_focus(ghostty_surface_t,
1127+ ghostty_action_goto_split_e);
1128+void ghostty_surface_split_resize(ghostty_surface_t,
1129+ ghostty_action_resize_split_direction_e,
1130+ uint16_t);
1131+void ghostty_surface_split_equalize(ghostty_surface_t);
1132+bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
1133+void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
1134+ const char*,
1135+ void*,
1136+ bool);
1137+bool ghostty_surface_has_selection(ghostty_surface_t);
1138+bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
1139+bool ghostty_surface_read_text(ghostty_surface_t,
1140+ ghostty_selection_s,
1141+ ghostty_text_s*);
1142+void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
1143+
1144+#ifdef __APPLE__
1145+void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
1146+void* ghostty_surface_quicklook_font(ghostty_surface_t);
1147+bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
1148+#endif
1149+
1150+ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
1151+void ghostty_inspector_free(ghostty_surface_t);
1152+void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
1153+void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
1154+void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
1155+void ghostty_inspector_mouse_button(ghostty_inspector_t,
1156+ ghostty_input_mouse_state_e,
1157+ ghostty_input_mouse_button_e,
1158+ ghostty_input_mods_e);
1159+void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
1160+void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
1161+ double,
1162+ double,
1163+ ghostty_input_scroll_mods_t);
1164+void ghostty_inspector_key(ghostty_inspector_t,
1165+ ghostty_input_action_e,
1166+ ghostty_input_key_e,
1167+ ghostty_input_mods_e);
1168+void ghostty_inspector_text(ghostty_inspector_t, const char*);
1169+
1170+#ifdef __APPLE__
1171+bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
1172+void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
1173+bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
1174+#endif
1175+
1176+// APIs I'd like to get rid of eventually but are still needed for now.
1177+// Don't use these unless you know what you're doing.
1178+void ghostty_set_window_background_blur(ghostty_app_t, void*);
1179+
1180+// Benchmark API, if available.
1181+bool ghostty_benchmark_cli(const char*, const char*);
1182+
1183+#ifdef __cplusplus
1184+}
1185+#endif
1186+
1187+#endif /* GHOSTTY_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt.h+87−0
@@ -0,0 +1,87 @@
1+/**
2+ * @file vt.h
3+ *
4+ * libghostty-vt - Virtual terminal emulator library
5+ *
6+ * This library provides functionality for parsing and handling terminal
7+ * escape sequences as well as maintaining terminal state such as styles,
8+ * cursor position, screen, scrollback, and more.
9+ *
10+ * WARNING: This is an incomplete, work-in-progress API. It is not yet
11+ * stable and is definitely going to change.
12+ */
13+
14+/**
15+ * @mainpage libghostty-vt - Virtual Terminal Emulator Library
16+ *
17+ * libghostty-vt is a C library which implements a modern terminal emulator,
18+ * extracted from the [Ghostty](https://ghostty.org) terminal emulator.
19+ *
20+ * libghostty-vt contains the logic for handling the core parts of a terminal
21+ * emulator: parsing terminal escape sequences, maintaining terminal state,
22+ * encoding input events, etc. It can handle scrollback, line wrapping,
23+ * reflow on resize, and more.
24+ *
25+ * @warning This library is currently in development and the API is not yet stable.
26+ * Breaking changes are expected in future versions. Use with caution in production code.
27+ *
28+ * @section groups_sec API Reference
29+ *
30+ * The API is organized into the following groups:
31+ * - @ref key "Key Encoding" - Encode key events into terminal sequences
32+ * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
33+ * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
34+ * - @ref paste "Paste Utilities" - Validate paste data safety
35+ * - @ref allocator "Memory Management" - Memory management and custom allocators
36+ * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions
37+ *
38+ * @section examples_sec Examples
39+ *
40+ * Complete working examples:
41+ * - @ref c-vt/src/main.c - OSC parser example
42+ * - @ref c-vt-key-encode/src/main.c - Key encoding example
43+ * - @ref c-vt-paste/src/main.c - Paste safety check example
44+ * - @ref c-vt-sgr/src/main.c - SGR parser example
45+ *
46+ */
47+
48+/** @example c-vt/src/main.c
49+ * This example demonstrates how to use the OSC parser to parse an OSC sequence,
50+ * extract command information, and retrieve command-specific data like window titles.
51+ */
52+
53+/** @example c-vt-key-encode/src/main.c
54+ * This example demonstrates how to use the key encoder to convert key events
55+ * into terminal escape sequences using the Kitty keyboard protocol.
56+ */
57+
58+/** @example c-vt-paste/src/main.c
59+ * This example demonstrates how to use the paste utilities to check if
60+ * paste data is safe before sending it to the terminal.
61+ */
62+
63+/** @example c-vt-sgr/src/main.c
64+ * This example demonstrates how to use the SGR parser to parse terminal
65+ * styling sequences and extract text attributes like colors and underline styles.
66+ */
67+
68+#ifndef GHOSTTY_VT_H
69+#define GHOSTTY_VT_H
70+
71+#ifdef __cplusplus
72+extern "C" {
73+#endif
74+
75+#include <ghostty/vt/result.h>
76+#include <ghostty/vt/allocator.h>
77+#include <ghostty/vt/osc.h>
78+#include <ghostty/vt/sgr.h>
79+#include <ghostty/vt/key.h>
80+#include <ghostty/vt/paste.h>
81+#include <ghostty/vt/wasm.h>
82+
83+#ifdef __cplusplus
84+}
85+#endif
86+
87+#endif /* GHOSTTY_VT_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/allocator.h+196−0
@@ -0,0 +1,196 @@
1+/**
2+ * @file allocator.h
3+ *
4+ * Memory management interface for libghostty-vt.
5+ */
6+
7+#ifndef GHOSTTY_VT_ALLOCATOR_H
8+#define GHOSTTY_VT_ALLOCATOR_H
9+
10+#include <stdbool.h>
11+#include <stddef.h>
12+#include <stdint.h>
13+
14+/** @defgroup allocator Memory Management
15+ *
16+ * libghostty-vt does require memory allocation for various operations,
17+ * but is resilient to allocation failures and will gracefully handle
18+ * out-of-memory situations by returning error codes.
19+ *
20+ * The exact memory management semantics are documented in the relevant
21+ * functions and data structures.
22+ *
23+ * libghostty-vt uses explicit memory allocation via an allocator
24+ * interface provided by GhosttyAllocator. The interface is based on the
25+ * [Zig](https://ziglang.org) allocator interface, since this has been
26+ * shown to be a flexible and powerful interface in practice and enables
27+ * a wide variety of allocation strategies.
28+ *
29+ * **For the common case, you can pass NULL as the allocator for any
30+ * function that accepts one,** and libghostty will use a default allocator.
31+ * The default allocator will be libc malloc/free if libc is linked.
32+ * Otherwise, a custom allocator is used (currently Zig's SMP allocator)
33+ * that doesn't require any external dependencies.
34+ *
35+ * ## Basic Usage
36+ *
37+ * For simple use cases, you can ignore this interface entirely by passing NULL
38+ * as the allocator parameter to functions that accept one. This will use the
39+ * default allocator (typically libc malloc/free, if libc is linked, but
40+ * we provide our own default allocator if libc isn't linked).
41+ *
42+ * To use a custom allocator:
43+ * 1. Implement the GhosttyAllocatorVtable function pointers
44+ * 2. Create a GhosttyAllocator struct with your vtable and context
45+ * 3. Pass the allocator to functions that accept one
46+ *
47+ * @{
48+ */
49+
50+/**
51+ * Function table for custom memory allocator operations.
52+ *
53+ * This vtable defines the interface for a custom memory allocator. All
54+ * function pointers must be valid and non-NULL.
55+ *
56+ * @ingroup allocator
57+ *
58+ * If you're not going to use a custom allocator, you can ignore all of
59+ * this. All functions that take an allocator pointer allow NULL to use a
60+ * default allocator.
61+ *
62+ * The interface is based on the Zig allocator interface. I'll say up front
63+ * that it is easy to look at this interface and think "wow, this is really
64+ * overcomplicated". The reason for this complexity is well thought out by
65+ * the Zig folks, and it enables a diverse set of allocation strategies
66+ * as shown by the Zig ecosystem. As a consolation, please note that many
67+ * of the arguments are only needed for advanced use cases and can be
68+ * safely ignored in simple implementations. For example, if you look at
69+ * the Zig implementation of the libc allocator in `lib/std/heap.zig`
70+ * (search for CAllocator), you'll see it is very simple.
71+ *
72+ * We chose to align with the Zig allocator interface because:
73+ *
74+ * 1. It is a proven interface that serves a wide variety of use cases
75+ * in the real world via the Zig ecosystem. It's shown to work.
76+ *
77+ * 2. Our core implementation itself is Zig, and this lets us very
78+ * cheaply and easily convert between C and Zig allocators.
79+ *
80+ * NOTE(mitchellh): In the future, we can have default implementations of
81+ * resize/remap and allow those to be null.
82+ */
83+typedef struct {
84+ /**
85+ * Return a pointer to `len` bytes with specified `alignment`, or return
86+ * `NULL` indicating the allocation failed.
87+ *
88+ * @param ctx The allocator context
89+ * @param len Number of bytes to allocate
90+ * @param alignment Required alignment for the allocation. Guaranteed to
91+ * be a power of two between 1 and 16 inclusive.
92+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
93+ * @return Pointer to allocated memory, or NULL if allocation failed
94+ */
95+ void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
96+
97+ /**
98+ * Attempt to expand or shrink memory in place.
99+ *
100+ * `memory_len` must equal the length requested from the most recent
101+ * successful call to `alloc`, `resize`, or `remap`. `alignment` must
102+ * equal the same value that was passed as the `alignment` parameter to
103+ * the original `alloc` call.
104+ *
105+ * `new_len` must be greater than zero.
106+ *
107+ * @param ctx The allocator context
108+ * @param memory Pointer to the memory block to resize
109+ * @param memory_len Current size of the memory block
110+ * @param alignment Alignment (must match original allocation)
111+ * @param new_len New requested size
112+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
113+ * @return true if resize was successful in-place, false if relocation would be required
114+ */
115+ bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
116+
117+ /**
118+ * Attempt to expand or shrink memory, allowing relocation.
119+ *
120+ * `memory_len` must equal the length requested from the most recent
121+ * successful call to `alloc`, `resize`, or `remap`. `alignment` must
122+ * equal the same value that was passed as the `alignment` parameter to
123+ * the original `alloc` call.
124+ *
125+ * A non-`NULL` return value indicates the resize was successful. The
126+ * allocation may have same address, or may have been relocated. In either
127+ * case, the allocation now has size of `new_len`. A `NULL` return value
128+ * indicates that the resize would be equivalent to allocating new memory,
129+ * copying the bytes from the old memory, and then freeing the old memory.
130+ * In such case, it is more efficient for the caller to perform the copy.
131+ *
132+ * `new_len` must be greater than zero.
133+ *
134+ * @param ctx The allocator context
135+ * @param memory Pointer to the memory block to remap
136+ * @param memory_len Current size of the memory block
137+ * @param alignment Alignment (must match original allocation)
138+ * @param new_len New requested size
139+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
140+ * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
141+ */
142+ void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
143+
144+ /**
145+ * Free and invalidate a region of memory.
146+ *
147+ * `memory_len` must equal the length requested from the most recent
148+ * successful call to `alloc`, `resize`, or `remap`. `alignment` must
149+ * equal the same value that was passed as the `alignment` parameter to
150+ * the original `alloc` call.
151+ *
152+ * @param ctx The allocator context
153+ * @param memory Pointer to the memory block to free
154+ * @param memory_len Size of the memory block
155+ * @param alignment Alignment (must match original allocation)
156+ * @param ret_addr First return address of the allocation call stack (0 if not provided)
157+ */
158+ void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
159+} GhosttyAllocatorVtable;
160+
161+/**
162+ * Custom memory allocator.
163+ *
164+ * For functions that take an allocator pointer, a NULL pointer indicates
165+ * that the default allocator should be used. The default allocator will
166+ * be libc malloc/free if we're linking to libc. If libc isn't linked,
167+ * a custom allocator is used (currently Zig's SMP allocator).
168+ *
169+ * @ingroup allocator
170+ *
171+ * Usage example:
172+ * @code
173+ * GhosttyAllocator allocator = {
174+ * .vtable = &my_allocator_vtable,
175+ * .ctx = my_allocator_state
176+ * };
177+ * @endcode
178+ */
179+typedef struct GhosttyAllocator {
180+ /**
181+ * Opaque context pointer passed to all vtable functions.
182+ * This allows the allocator implementation to maintain state
183+ * or reference external resources needed for memory management.
184+ */
185+ void *ctx;
186+
187+ /**
188+ * Pointer to the allocator's vtable containing function pointers
189+ * for memory operations (alloc, resize, remap, free).
190+ */
191+ const GhosttyAllocatorVtable *vtable;
192+} GhosttyAllocator;
193+
194+/** @} */
195+
196+#endif /* GHOSTTY_VT_ALLOCATOR_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/color.h+96−0
@@ -0,0 +1,96 @@
1+/**
2+ * @file color.h
3+ *
4+ * Color types and utilities.
5+ */
6+
7+#ifndef GHOSTTY_VT_COLOR_H
8+#define GHOSTTY_VT_COLOR_H
9+
10+#include <stdint.h>
11+
12+#ifdef __cplusplus
13+extern "C" {
14+#endif
15+
16+/**
17+ * RGB color value.
18+ *
19+ * @ingroup sgr
20+ */
21+typedef struct {
22+ uint8_t r; /**< Red component (0-255) */
23+ uint8_t g; /**< Green component (0-255) */
24+ uint8_t b; /**< Blue component (0-255) */
25+} GhosttyColorRgb;
26+
27+/**
28+ * Palette color index (0-255).
29+ *
30+ * @ingroup sgr
31+ */
32+typedef uint8_t GhosttyColorPaletteIndex;
33+
34+/** @addtogroup sgr
35+ * @{
36+ */
37+
38+/** Black color (0) @ingroup sgr */
39+#define GHOSTTY_COLOR_NAMED_BLACK 0
40+/** Red color (1) @ingroup sgr */
41+#define GHOSTTY_COLOR_NAMED_RED 1
42+/** Green color (2) @ingroup sgr */
43+#define GHOSTTY_COLOR_NAMED_GREEN 2
44+/** Yellow color (3) @ingroup sgr */
45+#define GHOSTTY_COLOR_NAMED_YELLOW 3
46+/** Blue color (4) @ingroup sgr */
47+#define GHOSTTY_COLOR_NAMED_BLUE 4
48+/** Magenta color (5) @ingroup sgr */
49+#define GHOSTTY_COLOR_NAMED_MAGENTA 5
50+/** Cyan color (6) @ingroup sgr */
51+#define GHOSTTY_COLOR_NAMED_CYAN 6
52+/** White color (7) @ingroup sgr */
53+#define GHOSTTY_COLOR_NAMED_WHITE 7
54+/** Bright black color (8) @ingroup sgr */
55+#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8
56+/** Bright red color (9) @ingroup sgr */
57+#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9
58+/** Bright green color (10) @ingroup sgr */
59+#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10
60+/** Bright yellow color (11) @ingroup sgr */
61+#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11
62+/** Bright blue color (12) @ingroup sgr */
63+#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12
64+/** Bright magenta color (13) @ingroup sgr */
65+#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13
66+/** Bright cyan color (14) @ingroup sgr */
67+#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14
68+/** Bright white color (15) @ingroup sgr */
69+#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15
70+
71+/** @} */
72+
73+/**
74+ * Get the RGB color components.
75+ *
76+ * This function extracts the individual red, green, and blue components
77+ * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments
78+ * where accessing struct fields directly is difficult.
79+ *
80+ * @param color The RGB color value
81+ * @param r Pointer to store the red component (0-255)
82+ * @param g Pointer to store the green component (0-255)
83+ * @param b Pointer to store the blue component (0-255)
84+ *
85+ * @ingroup sgr
86+ */
87+void ghostty_color_rgb_get(GhosttyColorRgb color,
88+ uint8_t* r,
89+ uint8_t* g,
90+ uint8_t* b);
91+
92+#ifdef __cplusplus
93+}
94+#endif
95+
96+#endif /* GHOSTTY_VT_COLOR_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/key.h+80−0
@@ -0,0 +1,80 @@
1+/**
2+ * @file key.h
3+ *
4+ * Key encoding module - encode key events into terminal escape sequences.
5+ */
6+
7+#ifndef GHOSTTY_VT_KEY_H
8+#define GHOSTTY_VT_KEY_H
9+
10+/** @defgroup key Key Encoding
11+ *
12+ * Utilities for encoding key events into terminal escape sequences,
13+ * supporting both legacy encoding as well as Kitty Keyboard Protocol.
14+ *
15+ * ## Basic Usage
16+ *
17+ * 1. Create an encoder instance with ghostty_key_encoder_new()
18+ * 2. Configure encoder options with ghostty_key_encoder_setopt().
19+ * 3. For each key event:
20+ * - Create a key event with ghostty_key_event_new()
21+ * - Set event properties (action, key, modifiers, etc.)
22+ * - Encode with ghostty_key_encoder_encode()
23+ * - Free the event with ghostty_key_event_free()
24+ * - Note: You can also reuse the same key event multiple times by
25+ * changing its properties.
26+ * 4. Free the encoder with ghostty_key_encoder_free() when done
27+ *
28+ * ## Example
29+ *
30+ * @code{.c}
31+ * #include <assert.h>
32+ * #include <stdio.h>
33+ * #include <ghostty/vt.h>
34+ *
35+ * int main() {
36+ * // Create encoder
37+ * GhosttyKeyEncoder encoder;
38+ * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
39+ * assert(result == GHOSTTY_SUCCESS);
40+ *
41+ * // Enable Kitty keyboard protocol with all features
42+ * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS,
43+ * &(uint8_t){GHOSTTY_KITTY_KEY_ALL});
44+ *
45+ * // Create and configure key event for Ctrl+C press
46+ * GhosttyKeyEvent event;
47+ * result = ghostty_key_event_new(NULL, &event);
48+ * assert(result == GHOSTTY_SUCCESS);
49+ * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS);
50+ * ghostty_key_event_set_key(event, GHOSTTY_KEY_C);
51+ * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL);
52+ *
53+ * // Encode the key event
54+ * char buf[128];
55+ * size_t written = 0;
56+ * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
57+ * assert(result == GHOSTTY_SUCCESS);
58+ *
59+ * // Use the encoded sequence (e.g., write to terminal)
60+ * fwrite(buf, 1, written, stdout);
61+ *
62+ * // Cleanup
63+ * ghostty_key_event_free(event);
64+ * ghostty_key_encoder_free(encoder);
65+ * return 0;
66+ * }
67+ * @endcode
68+ *
69+ * For a complete working example, see example/c-vt-key-encode in the
70+ * repository.
71+ *
72+ * @{
73+ */
74+
75+#include <ghostty/vt/key/event.h>
76+#include <ghostty/vt/key/encoder.h>
77+
78+/** @} */
79+
80+#endif /* GHOSTTY_VT_KEY_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/key/encoder.h+221−0
@@ -0,0 +1,221 @@
1+/**
2+ * @file encoder.h
3+ *
4+ * Key event encoding to terminal escape sequences.
5+ */
6+
7+#ifndef GHOSTTY_VT_KEY_ENCODER_H
8+#define GHOSTTY_VT_KEY_ENCODER_H
9+
10+#include <stddef.h>
11+#include <stdint.h>
12+#include <ghostty/vt/result.h>
13+#include <ghostty/vt/allocator.h>
14+#include <ghostty/vt/key/event.h>
15+
16+/**
17+ * Opaque handle to a key encoder instance.
18+ *
19+ * This handle represents a key encoder that converts key events into terminal
20+ * escape sequences.
21+ *
22+ * @ingroup key
23+ */
24+typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder;
25+
26+/**
27+ * Kitty keyboard protocol flags.
28+ *
29+ * Bitflags representing the various modes of the Kitty keyboard protocol.
30+ * These can be combined using bitwise OR operations. Valid values all
31+ * start with `GHOSTTY_KITTY_KEY_`.
32+ *
33+ * @ingroup key
34+ */
35+typedef uint8_t GhosttyKittyKeyFlags;
36+
37+/** Kitty keyboard protocol disabled (all flags off) */
38+#define GHOSTTY_KITTY_KEY_DISABLED 0
39+
40+/** Disambiguate escape codes */
41+#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0)
42+
43+/** Report key press and release events */
44+#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1)
45+
46+/** Report alternate key codes */
47+#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2)
48+
49+/** Report all key events including those normally handled by the terminal */
50+#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3)
51+
52+/** Report associated text with key events */
53+#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4)
54+
55+/** All Kitty keyboard protocol flags enabled */
56+#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED)
57+
58+/**
59+ * macOS option key behavior.
60+ *
61+ * Determines whether the "option" key on macOS is treated as "alt" or not.
62+ * See the Ghostty `macos-option-as-alt` configuration option for more details.
63+ *
64+ * @ingroup key
65+ */
66+typedef enum {
67+ /** Option key is not treated as alt */
68+ GHOSTTY_OPTION_AS_ALT_FALSE = 0,
69+ /** Option key is treated as alt */
70+ GHOSTTY_OPTION_AS_ALT_TRUE = 1,
71+ /** Only left option key is treated as alt */
72+ GHOSTTY_OPTION_AS_ALT_LEFT = 2,
73+ /** Only right option key is treated as alt */
74+ GHOSTTY_OPTION_AS_ALT_RIGHT = 3,
75+} GhosttyOptionAsAlt;
76+
77+/**
78+ * Key encoder option identifiers.
79+ *
80+ * These values are used with ghostty_key_encoder_setopt() to configure
81+ * the behavior of the key encoder.
82+ *
83+ * @ingroup key
84+ */
85+typedef enum {
86+ /** Terminal DEC mode 1: cursor key application mode (value: bool) */
87+ GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0,
88+
89+ /** Terminal DEC mode 66: keypad key application mode (value: bool) */
90+ GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1,
91+
92+ /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */
93+ GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2,
94+
95+ /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */
96+ GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3,
97+
98+ /** xterm modifyOtherKeys mode 2 (value: bool) */
99+ GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4,
100+
101+ /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */
102+ GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5,
103+
104+ /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */
105+ GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6,
106+} GhosttyKeyEncoderOption;
107+
108+/**
109+ * Create a new key encoder instance.
110+ *
111+ * Creates a new key encoder with default options. The encoder can be configured
112+ * using ghostty_key_encoder_setopt() and must be freed using
113+ * ghostty_key_encoder_free() when no longer needed.
114+ *
115+ * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
116+ * @param encoder Pointer to store the created encoder handle
117+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
118+ *
119+ * @ingroup key
120+ */
121+GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder);
122+
123+/**
124+ * Free a key encoder instance.
125+ *
126+ * Releases all resources associated with the key encoder. After this call,
127+ * the encoder handle becomes invalid and must not be used.
128+ *
129+ * @param encoder The encoder handle to free (may be NULL)
130+ *
131+ * @ingroup key
132+ */
133+void ghostty_key_encoder_free(GhosttyKeyEncoder encoder);
134+
135+/**
136+ * Set an option on the key encoder.
137+ *
138+ * Configures the behavior of the key encoder. Options control various aspects
139+ * of encoding such as terminal modes (cursor key application mode, keypad mode),
140+ * protocol selection (Kitty keyboard protocol flags), and platform-specific
141+ * behaviors (macOS option-as-alt).
142+ *
143+ * A null pointer value does nothing. It does not reset the value to the
144+ * default. The setopt call will do nothing.
145+ *
146+ * @param encoder The encoder handle, must not be NULL
147+ * @param option The option to set
148+ * @param value Pointer to the value to set (type depends on the option)
149+ *
150+ * @ingroup key
151+ */
152+void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value);
153+
154+/**
155+ * Encode a key event into a terminal escape sequence.
156+ *
157+ * Converts a key event into the appropriate terminal escape sequence based on
158+ * the encoder's current options. The sequence is written to the provided buffer.
159+ *
160+ * Not all key events produce output. For example, unmodified modifier keys
161+ * typically don't generate escape sequences. Check the out_len parameter to
162+ * determine if any data was written.
163+ *
164+ * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY
165+ * and out_len will contain the required buffer size. The caller can then
166+ * allocate a larger buffer and call the function again.
167+ *
168+ * @param encoder The encoder handle, must not be NULL
169+ * @param event The key event to encode, must not be NULL
170+ * @param out_buf Buffer to write the encoded sequence to
171+ * @param out_buf_size Size of the output buffer in bytes
172+ * @param out_len Pointer to store the number of bytes written (may be NULL)
173+ * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code
174+ *
175+ * ## Example: Calculate required buffer size
176+ *
177+ * @code{.c}
178+ * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY)
179+ * size_t required = 0;
180+ * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
181+ * assert(result == GHOSTTY_OUT_OF_MEMORY);
182+ *
183+ * // Allocate buffer of required size
184+ * char *buf = malloc(required);
185+ *
186+ * // Encode with properly sized buffer
187+ * size_t written = 0;
188+ * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written);
189+ * assert(result == GHOSTTY_SUCCESS);
190+ *
191+ * // Use the encoded sequence...
192+ *
193+ * free(buf);
194+ * @endcode
195+ *
196+ * ## Example: Direct encoding with static buffer
197+ *
198+ * @code{.c}
199+ * // Most escape sequences are short, so a static buffer often suffices
200+ * char buf[128];
201+ * size_t written = 0;
202+ * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
203+ *
204+ * if (result == GHOSTTY_SUCCESS) {
205+ * // Write the encoded sequence to the terminal
206+ * write(pty_fd, buf, written);
207+ * } else if (result == GHOSTTY_OUT_OF_MEMORY) {
208+ * // Buffer too small, written contains required size
209+ * char *dynamic_buf = malloc(written);
210+ * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written);
211+ * assert(result == GHOSTTY_SUCCESS);
212+ * write(pty_fd, dynamic_buf, written);
213+ * free(dynamic_buf);
214+ * }
215+ * @endcode
216+ *
217+ * @ingroup key
218+ */
219+GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len);
220+
221+#endif /* GHOSTTY_VT_KEY_ENCODER_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/key/event.h+474−0
@@ -0,0 +1,474 @@
1+/**
2+ * @file event.h
3+ *
4+ * Key event representation and manipulation.
5+ */
6+
7+#ifndef GHOSTTY_VT_KEY_EVENT_H
8+#define GHOSTTY_VT_KEY_EVENT_H
9+
10+#include <stdbool.h>
11+#include <stddef.h>
12+#include <stdint.h>
13+#include <ghostty/vt/result.h>
14+#include <ghostty/vt/allocator.h>
15+
16+/**
17+ * Opaque handle to a key event.
18+ *
19+ * This handle represents a keyboard input event containing information about
20+ * the physical key pressed, modifiers, and generated text.
21+ *
22+ * @ingroup key
23+ */
24+typedef struct GhosttyKeyEvent *GhosttyKeyEvent;
25+
26+/**
27+ * Keyboard input event types.
28+ *
29+ * @ingroup key
30+ */
31+typedef enum {
32+ /** Key was released */
33+ GHOSTTY_KEY_ACTION_RELEASE = 0,
34+ /** Key was pressed */
35+ GHOSTTY_KEY_ACTION_PRESS = 1,
36+ /** Key is being repeated (held down) */
37+ GHOSTTY_KEY_ACTION_REPEAT = 2,
38+} GhosttyKeyAction;
39+
40+/**
41+ * Keyboard modifier keys bitmask.
42+ *
43+ * A bitmask representing all keyboard modifiers. This tracks which modifier keys
44+ * are pressed and, where supported by the platform, which side (left or right)
45+ * of each modifier is active.
46+ *
47+ * Use the GHOSTTY_MODS_* constants to test and set individual modifiers.
48+ *
49+ * Modifier side bits are only meaningful when the corresponding modifier bit is set.
50+ * Not all platforms support distinguishing between left and right modifier
51+ * keys and Ghostty is built to expect that some platforms may not provide this
52+ * information.
53+ *
54+ * @ingroup key
55+ */
56+typedef uint16_t GhosttyMods;
57+
58+/** Shift key is pressed */
59+#define GHOSTTY_MODS_SHIFT (1 << 0)
60+/** Control key is pressed */
61+#define GHOSTTY_MODS_CTRL (1 << 1)
62+/** Alt/Option key is pressed */
63+#define GHOSTTY_MODS_ALT (1 << 2)
64+/** Super/Command/Windows key is pressed */
65+#define GHOSTTY_MODS_SUPER (1 << 3)
66+/** Caps Lock is active */
67+#define GHOSTTY_MODS_CAPS_LOCK (1 << 4)
68+/** Num Lock is active */
69+#define GHOSTTY_MODS_NUM_LOCK (1 << 5)
70+
71+/**
72+ * Right shift is pressed (0 = left, 1 = right).
73+ * Only meaningful when GHOSTTY_MODS_SHIFT is set.
74+ */
75+#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6)
76+/**
77+ * Right ctrl is pressed (0 = left, 1 = right).
78+ * Only meaningful when GHOSTTY_MODS_CTRL is set.
79+ */
80+#define GHOSTTY_MODS_CTRL_SIDE (1 << 7)
81+/**
82+ * Right alt is pressed (0 = left, 1 = right).
83+ * Only meaningful when GHOSTTY_MODS_ALT is set.
84+ */
85+#define GHOSTTY_MODS_ALT_SIDE (1 << 8)
86+/**
87+ * Right super is pressed (0 = left, 1 = right).
88+ * Only meaningful when GHOSTTY_MODS_SUPER is set.
89+ */
90+#define GHOSTTY_MODS_SUPER_SIDE (1 << 9)
91+
92+/**
93+ * Physical key codes.
94+ *
95+ * The set of key codes that Ghostty is aware of. These represent physical keys
96+ * on the keyboard and are layout-independent. For example, the "a" key on a US
97+ * keyboard is the same as the "ф" key on a Russian keyboard, but both will
98+ * report the same key_a value.
99+ *
100+ * Layout-dependent strings are provided separately as UTF-8 text and are produced
101+ * by the platform. These values are based on the W3C UI Events KeyboardEvent code
102+ * standard. See: https://www.w3.org/TR/uievents-code
103+ *
104+ * @ingroup key
105+ */
106+typedef enum {
107+ GHOSTTY_KEY_UNIDENTIFIED = 0,
108+
109+ // Writing System Keys (W3C § 3.1.1)
110+ GHOSTTY_KEY_BACKQUOTE,
111+ GHOSTTY_KEY_BACKSLASH,
112+ GHOSTTY_KEY_BRACKET_LEFT,
113+ GHOSTTY_KEY_BRACKET_RIGHT,
114+ GHOSTTY_KEY_COMMA,
115+ GHOSTTY_KEY_DIGIT_0,
116+ GHOSTTY_KEY_DIGIT_1,
117+ GHOSTTY_KEY_DIGIT_2,
118+ GHOSTTY_KEY_DIGIT_3,
119+ GHOSTTY_KEY_DIGIT_4,
120+ GHOSTTY_KEY_DIGIT_5,
121+ GHOSTTY_KEY_DIGIT_6,
122+ GHOSTTY_KEY_DIGIT_7,
123+ GHOSTTY_KEY_DIGIT_8,
124+ GHOSTTY_KEY_DIGIT_9,
125+ GHOSTTY_KEY_EQUAL,
126+ GHOSTTY_KEY_INTL_BACKSLASH,
127+ GHOSTTY_KEY_INTL_RO,
128+ GHOSTTY_KEY_INTL_YEN,
129+ GHOSTTY_KEY_A,
130+ GHOSTTY_KEY_B,
131+ GHOSTTY_KEY_C,
132+ GHOSTTY_KEY_D,
133+ GHOSTTY_KEY_E,
134+ GHOSTTY_KEY_F,
135+ GHOSTTY_KEY_G,
136+ GHOSTTY_KEY_H,
137+ GHOSTTY_KEY_I,
138+ GHOSTTY_KEY_J,
139+ GHOSTTY_KEY_K,
140+ GHOSTTY_KEY_L,
141+ GHOSTTY_KEY_M,
142+ GHOSTTY_KEY_N,
143+ GHOSTTY_KEY_O,
144+ GHOSTTY_KEY_P,
145+ GHOSTTY_KEY_Q,
146+ GHOSTTY_KEY_R,
147+ GHOSTTY_KEY_S,
148+ GHOSTTY_KEY_T,
149+ GHOSTTY_KEY_U,
150+ GHOSTTY_KEY_V,
151+ GHOSTTY_KEY_W,
152+ GHOSTTY_KEY_X,
153+ GHOSTTY_KEY_Y,
154+ GHOSTTY_KEY_Z,
155+ GHOSTTY_KEY_MINUS,
156+ GHOSTTY_KEY_PERIOD,
157+ GHOSTTY_KEY_QUOTE,
158+ GHOSTTY_KEY_SEMICOLON,
159+ GHOSTTY_KEY_SLASH,
160+
161+ // Functional Keys (W3C § 3.1.2)
162+ GHOSTTY_KEY_ALT_LEFT,
163+ GHOSTTY_KEY_ALT_RIGHT,
164+ GHOSTTY_KEY_BACKSPACE,
165+ GHOSTTY_KEY_CAPS_LOCK,
166+ GHOSTTY_KEY_CONTEXT_MENU,
167+ GHOSTTY_KEY_CONTROL_LEFT,
168+ GHOSTTY_KEY_CONTROL_RIGHT,
169+ GHOSTTY_KEY_ENTER,
170+ GHOSTTY_KEY_META_LEFT,
171+ GHOSTTY_KEY_META_RIGHT,
172+ GHOSTTY_KEY_SHIFT_LEFT,
173+ GHOSTTY_KEY_SHIFT_RIGHT,
174+ GHOSTTY_KEY_SPACE,
175+ GHOSTTY_KEY_TAB,
176+ GHOSTTY_KEY_CONVERT,
177+ GHOSTTY_KEY_KANA_MODE,
178+ GHOSTTY_KEY_NON_CONVERT,
179+
180+ // Control Pad Section (W3C § 3.2)
181+ GHOSTTY_KEY_DELETE,
182+ GHOSTTY_KEY_END,
183+ GHOSTTY_KEY_HELP,
184+ GHOSTTY_KEY_HOME,
185+ GHOSTTY_KEY_INSERT,
186+ GHOSTTY_KEY_PAGE_DOWN,
187+ GHOSTTY_KEY_PAGE_UP,
188+
189+ // Arrow Pad Section (W3C § 3.3)
190+ GHOSTTY_KEY_ARROW_DOWN,
191+ GHOSTTY_KEY_ARROW_LEFT,
192+ GHOSTTY_KEY_ARROW_RIGHT,
193+ GHOSTTY_KEY_ARROW_UP,
194+
195+ // Numpad Section (W3C § 3.4)
196+ GHOSTTY_KEY_NUM_LOCK,
197+ GHOSTTY_KEY_NUMPAD_0,
198+ GHOSTTY_KEY_NUMPAD_1,
199+ GHOSTTY_KEY_NUMPAD_2,
200+ GHOSTTY_KEY_NUMPAD_3,
201+ GHOSTTY_KEY_NUMPAD_4,
202+ GHOSTTY_KEY_NUMPAD_5,
203+ GHOSTTY_KEY_NUMPAD_6,
204+ GHOSTTY_KEY_NUMPAD_7,
205+ GHOSTTY_KEY_NUMPAD_8,
206+ GHOSTTY_KEY_NUMPAD_9,
207+ GHOSTTY_KEY_NUMPAD_ADD,
208+ GHOSTTY_KEY_NUMPAD_BACKSPACE,
209+ GHOSTTY_KEY_NUMPAD_CLEAR,
210+ GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
211+ GHOSTTY_KEY_NUMPAD_COMMA,
212+ GHOSTTY_KEY_NUMPAD_DECIMAL,
213+ GHOSTTY_KEY_NUMPAD_DIVIDE,
214+ GHOSTTY_KEY_NUMPAD_ENTER,
215+ GHOSTTY_KEY_NUMPAD_EQUAL,
216+ GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
217+ GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
218+ GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
219+ GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
220+ GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
221+ GHOSTTY_KEY_NUMPAD_MULTIPLY,
222+ GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
223+ GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
224+ GHOSTTY_KEY_NUMPAD_SUBTRACT,
225+ GHOSTTY_KEY_NUMPAD_SEPARATOR,
226+ GHOSTTY_KEY_NUMPAD_UP,
227+ GHOSTTY_KEY_NUMPAD_DOWN,
228+ GHOSTTY_KEY_NUMPAD_RIGHT,
229+ GHOSTTY_KEY_NUMPAD_LEFT,
230+ GHOSTTY_KEY_NUMPAD_BEGIN,
231+ GHOSTTY_KEY_NUMPAD_HOME,
232+ GHOSTTY_KEY_NUMPAD_END,
233+ GHOSTTY_KEY_NUMPAD_INSERT,
234+ GHOSTTY_KEY_NUMPAD_DELETE,
235+ GHOSTTY_KEY_NUMPAD_PAGE_UP,
236+ GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
237+
238+ // Function Section (W3C § 3.5)
239+ GHOSTTY_KEY_ESCAPE,
240+ GHOSTTY_KEY_F1,
241+ GHOSTTY_KEY_F2,
242+ GHOSTTY_KEY_F3,
243+ GHOSTTY_KEY_F4,
244+ GHOSTTY_KEY_F5,
245+ GHOSTTY_KEY_F6,
246+ GHOSTTY_KEY_F7,
247+ GHOSTTY_KEY_F8,
248+ GHOSTTY_KEY_F9,
249+ GHOSTTY_KEY_F10,
250+ GHOSTTY_KEY_F11,
251+ GHOSTTY_KEY_F12,
252+ GHOSTTY_KEY_F13,
253+ GHOSTTY_KEY_F14,
254+ GHOSTTY_KEY_F15,
255+ GHOSTTY_KEY_F16,
256+ GHOSTTY_KEY_F17,
257+ GHOSTTY_KEY_F18,
258+ GHOSTTY_KEY_F19,
259+ GHOSTTY_KEY_F20,
260+ GHOSTTY_KEY_F21,
261+ GHOSTTY_KEY_F22,
262+ GHOSTTY_KEY_F23,
263+ GHOSTTY_KEY_F24,
264+ GHOSTTY_KEY_F25,
265+ GHOSTTY_KEY_FN,
266+ GHOSTTY_KEY_FN_LOCK,
267+ GHOSTTY_KEY_PRINT_SCREEN,
268+ GHOSTTY_KEY_SCROLL_LOCK,
269+ GHOSTTY_KEY_PAUSE,
270+
271+ // Media Keys (W3C § 3.6)
272+ GHOSTTY_KEY_BROWSER_BACK,
273+ GHOSTTY_KEY_BROWSER_FAVORITES,
274+ GHOSTTY_KEY_BROWSER_FORWARD,
275+ GHOSTTY_KEY_BROWSER_HOME,
276+ GHOSTTY_KEY_BROWSER_REFRESH,
277+ GHOSTTY_KEY_BROWSER_SEARCH,
278+ GHOSTTY_KEY_BROWSER_STOP,
279+ GHOSTTY_KEY_EJECT,
280+ GHOSTTY_KEY_LAUNCH_APP_1,
281+ GHOSTTY_KEY_LAUNCH_APP_2,
282+ GHOSTTY_KEY_LAUNCH_MAIL,
283+ GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
284+ GHOSTTY_KEY_MEDIA_SELECT,
285+ GHOSTTY_KEY_MEDIA_STOP,
286+ GHOSTTY_KEY_MEDIA_TRACK_NEXT,
287+ GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
288+ GHOSTTY_KEY_POWER,
289+ GHOSTTY_KEY_SLEEP,
290+ GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
291+ GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
292+ GHOSTTY_KEY_AUDIO_VOLUME_UP,
293+ GHOSTTY_KEY_WAKE_UP,
294+
295+ // Legacy, Non-standard, and Special Keys (W3C § 3.7)
296+ GHOSTTY_KEY_COPY,
297+ GHOSTTY_KEY_CUT,
298+ GHOSTTY_KEY_PASTE,
299+} GhosttyKey;
300+
301+/**
302+ * Create a new key event instance.
303+ *
304+ * Creates a new key event with default values. The event must be freed using
305+ * ghostty_key_event_free() when no longer needed.
306+ *
307+ * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
308+ * @param event Pointer to store the created key event handle
309+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
310+ *
311+ * @ingroup key
312+ */
313+GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event);
314+
315+/**
316+ * Free a key event instance.
317+ *
318+ * Releases all resources associated with the key event. After this call,
319+ * the event handle becomes invalid and must not be used.
320+ *
321+ * @param event The key event handle to free (may be NULL)
322+ *
323+ * @ingroup key
324+ */
325+void ghostty_key_event_free(GhosttyKeyEvent event);
326+
327+/**
328+ * Set the key action (press, release, repeat).
329+ *
330+ * @param event The key event handle, must not be NULL
331+ * @param action The action to set
332+ *
333+ * @ingroup key
334+ */
335+void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action);
336+
337+/**
338+ * Get the key action (press, release, repeat).
339+ *
340+ * @param event The key event handle, must not be NULL
341+ * @return The key action
342+ *
343+ * @ingroup key
344+ */
345+GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event);
346+
347+/**
348+ * Set the physical key code.
349+ *
350+ * @param event The key event handle, must not be NULL
351+ * @param key The physical key code to set
352+ *
353+ * @ingroup key
354+ */
355+void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key);
356+
357+/**
358+ * Get the physical key code.
359+ *
360+ * @param event The key event handle, must not be NULL
361+ * @return The physical key code
362+ *
363+ * @ingroup key
364+ */
365+GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event);
366+
367+/**
368+ * Set the modifier keys bitmask.
369+ *
370+ * @param event The key event handle, must not be NULL
371+ * @param mods The modifier keys bitmask to set
372+ *
373+ * @ingroup key
374+ */
375+void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods);
376+
377+/**
378+ * Get the modifier keys bitmask.
379+ *
380+ * @param event The key event handle, must not be NULL
381+ * @return The modifier keys bitmask
382+ *
383+ * @ingroup key
384+ */
385+GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event);
386+
387+/**
388+ * Set the consumed modifiers bitmask.
389+ *
390+ * @param event The key event handle, must not be NULL
391+ * @param consumed_mods The consumed modifiers bitmask to set
392+ *
393+ * @ingroup key
394+ */
395+void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods);
396+
397+/**
398+ * Get the consumed modifiers bitmask.
399+ *
400+ * @param event The key event handle, must not be NULL
401+ * @return The consumed modifiers bitmask
402+ *
403+ * @ingroup key
404+ */
405+GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event);
406+
407+/**
408+ * Set whether the key event is part of a composition sequence.
409+ *
410+ * @param event The key event handle, must not be NULL
411+ * @param composing Whether the key event is part of a composition sequence
412+ *
413+ * @ingroup key
414+ */
415+void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing);
416+
417+/**
418+ * Get whether the key event is part of a composition sequence.
419+ *
420+ * @param event The key event handle, must not be NULL
421+ * @return Whether the key event is part of a composition sequence
422+ *
423+ * @ingroup key
424+ */
425+bool ghostty_key_event_get_composing(GhosttyKeyEvent event);
426+
427+/**
428+ * Set the UTF-8 text generated by the key event.
429+ *
430+ * The key event does NOT take ownership of the text pointer. The caller
431+ * must ensure the string remains valid for the lifetime needed by the event.
432+ *
433+ * @param event The key event handle, must not be NULL
434+ * @param utf8 The UTF-8 text to set (or NULL for empty)
435+ * @param len Length of the UTF-8 text in bytes
436+ *
437+ * @ingroup key
438+ */
439+void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len);
440+
441+/**
442+ * Get the UTF-8 text generated by the key event.
443+ *
444+ * The returned pointer is valid until the event is freed or the UTF-8 text is modified.
445+ *
446+ * @param event The key event handle, must not be NULL
447+ * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL)
448+ * @return The UTF-8 text (or NULL for empty)
449+ *
450+ * @ingroup key
451+ */
452+const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len);
453+
454+/**
455+ * Set the unshifted Unicode codepoint.
456+ *
457+ * @param event The key event handle, must not be NULL
458+ * @param codepoint The unshifted Unicode codepoint to set
459+ *
460+ * @ingroup key
461+ */
462+void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint);
463+
464+/**
465+ * Get the unshifted Unicode codepoint.
466+ *
467+ * @param event The key event handle, must not be NULL
468+ * @return The unshifted Unicode codepoint
469+ *
470+ * @ingroup key
471+ */
472+uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event);
473+
474+#endif /* GHOSTTY_VT_KEY_EVENT_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/osc.h+233−0
@@ -0,0 +1,233 @@
1+/**
2+ * @file osc.h
3+ *
4+ * OSC (Operating System Command) sequence parser and command handling.
5+ */
6+
7+#ifndef GHOSTTY_VT_OSC_H
8+#define GHOSTTY_VT_OSC_H
9+
10+#include <stdbool.h>
11+#include <stddef.h>
12+#include <stdint.h>
13+#include <ghostty/vt/result.h>
14+#include <ghostty/vt/allocator.h>
15+
16+/**
17+ * Opaque handle to an OSC parser instance.
18+ *
19+ * This handle represents an OSC (Operating System Command) parser that can
20+ * be used to parse the contents of OSC sequences.
21+ *
22+ * @ingroup osc
23+ */
24+typedef struct GhosttyOscParser *GhosttyOscParser;
25+
26+/**
27+ * Opaque handle to a single OSC command.
28+ *
29+ * This handle represents a parsed OSC (Operating System Command) command.
30+ * The command can be queried for its type and associated data.
31+ *
32+ * @ingroup osc
33+ */
34+typedef struct GhosttyOscCommand *GhosttyOscCommand;
35+
36+/** @defgroup osc OSC Parser
37+ *
38+ * OSC (Operating System Command) sequence parser and command handling.
39+ *
40+ * The parser operates in a streaming fashion, processing input byte-by-byte
41+ * to handle OSC sequences that may arrive in fragments across multiple reads.
42+ * This interface makes it easy to integrate into most environments and avoids
43+ * over-allocating buffers.
44+ *
45+ * ## Basic Usage
46+ *
47+ * 1. Create a parser instance with ghostty_osc_new()
48+ * 2. Feed bytes to the parser using ghostty_osc_next()
49+ * 3. Finalize parsing with ghostty_osc_end() to get the command
50+ * 4. Query command type and extract data using ghostty_osc_command_type()
51+ * and ghostty_osc_command_data()
52+ * 5. Free the parser with ghostty_osc_free() when done
53+ *
54+ * @{
55+ */
56+
57+/**
58+ * OSC command types.
59+ *
60+ * @ingroup osc
61+ */
62+typedef enum {
63+ GHOSTTY_OSC_COMMAND_INVALID = 0,
64+ GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
65+ GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
66+ GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3,
67+ GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4,
68+ GHOSTTY_OSC_COMMAND_REPORT_PWD = 5,
69+ GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6,
70+ GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7,
71+ GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8,
72+ GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9,
73+ GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10,
74+ GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11,
75+ GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12,
76+ GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13,
77+ GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14,
78+ GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15,
79+ GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16,
80+ GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17,
81+ GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18,
82+ GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19,
83+ GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
84+ GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
85+ GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
86+} GhosttyOscCommandType;
87+
88+/**
89+ * OSC command data types.
90+ *
91+ * These values specify what type of data to extract from an OSC command
92+ * using `ghostty_osc_command_data`.
93+ *
94+ * @ingroup osc
95+ */
96+typedef enum {
97+ /** Invalid data type. Never results in any data extraction. */
98+ GHOSTTY_OSC_DATA_INVALID = 0,
99+
100+ /**
101+ * Window title string data.
102+ *
103+ * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE
104+ *
105+ * Output type: const char ** (pointer to null-terminated string)
106+ *
107+ * Lifetime: Valid until the next call to any ghostty_osc_* function with
108+ * the same parser instance. Memory is owned by the parser.
109+ */
110+ GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
111+} GhosttyOscCommandData;
112+
113+/**
114+ * Create a new OSC parser instance.
115+ *
116+ * Creates a new OSC (Operating System Command) parser using the provided
117+ * allocator. The parser must be freed using ghostty_vt_osc_free() when
118+ * no longer needed.
119+ *
120+ * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
121+ * @param parser Pointer to store the created parser handle
122+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
123+ *
124+ * @ingroup osc
125+ */
126+GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
127+
128+/**
129+ * Free an OSC parser instance.
130+ *
131+ * Releases all resources associated with the OSC parser. After this call,
132+ * the parser handle becomes invalid and must not be used.
133+ *
134+ * @param parser The parser handle to free (may be NULL)
135+ *
136+ * @ingroup osc
137+ */
138+void ghostty_osc_free(GhosttyOscParser parser);
139+
140+/**
141+ * Reset an OSC parser instance to its initial state.
142+ *
143+ * Resets the parser state, clearing any partially parsed OSC sequences
144+ * and returning the parser to its initial state. This is useful for
145+ * reusing a parser instance or recovering from parse errors.
146+ *
147+ * @param parser The parser handle to reset, must not be null.
148+ *
149+ * @ingroup osc
150+ */
151+void ghostty_osc_reset(GhosttyOscParser parser);
152+
153+/**
154+ * Parse the next byte in an OSC sequence.
155+ *
156+ * Processes a single byte as part of an OSC sequence. The parser maintains
157+ * internal state to track the progress through the sequence. Call this
158+ * function for each byte in the sequence data.
159+ *
160+ * When finished pumping the parser with bytes, call ghostty_osc_end
161+ * to get the final result.
162+ *
163+ * @param parser The parser handle, must not be null.
164+ * @param byte The next byte to parse
165+ *
166+ * @ingroup osc
167+ */
168+void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
169+
170+/**
171+ * Finalize OSC parsing and retrieve the parsed command.
172+ *
173+ * Call this function after feeding all bytes of an OSC sequence to the parser
174+ * using ghostty_osc_next() with the exception of the terminating character
175+ * (ESC or ST). This function finalizes the parsing process and returns the
176+ * parsed OSC command.
177+ *
178+ * The return value is never NULL. Invalid commands will return a command
179+ * with type GHOSTTY_OSC_COMMAND_INVALID.
180+ *
181+ * The terminator parameter specifies the byte that terminated the OSC sequence
182+ * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
183+ * preserved in the parsed command so that responses can use the same terminator
184+ * format for better compatibility with the calling program. For commands that
185+ * do not require a response, this parameter is ignored and the resulting
186+ * command will not retain the terminator information.
187+ *
188+ * The returned command handle is valid until the next call to any
189+ * `ghostty_osc_*` function with the same parser instance with the exception
190+ * of command introspection functions such as `ghostty_osc_command_type`.
191+ *
192+ * @param parser The parser handle, must not be null.
193+ * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
194+ * @return Handle to the parsed OSC command
195+ *
196+ * @ingroup osc
197+ */
198+GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
199+
200+/**
201+ * Get the type of an OSC command.
202+ *
203+ * Returns the type identifier for the given OSC command. This can be used
204+ * to determine what kind of command was parsed and what data might be
205+ * available from it.
206+ *
207+ * @param command The OSC command handle to query (may be NULL)
208+ * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
209+ *
210+ * @ingroup osc
211+ */
212+GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
213+
214+/**
215+ * Extract data from an OSC command.
216+ *
217+ * Extracts typed data from the given OSC command based on the specified
218+ * data type. The output pointer must be of the appropriate type for the
219+ * requested data kind. Valid command types, output types, and memory
220+ * safety information are documented in the `GhosttyOscCommandData` enum.
221+ *
222+ * @param command The OSC command handle to query (may be NULL)
223+ * @param data The type of data to extract
224+ * @param out Pointer to store the extracted data (type depends on data parameter)
225+ * @return true if data extraction was successful, false otherwise
226+ *
227+ * @ingroup osc
228+ */
229+bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out);
230+
231+/** @} */
232+
233+#endif /* GHOSTTY_VT_OSC_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/paste.h+75−0
@@ -0,0 +1,75 @@
1+/**
2+ * @file paste.h
3+ *
4+ * Paste utilities - validate and encode paste data for terminal input.
5+ */
6+
7+#ifndef GHOSTTY_VT_PASTE_H
8+#define GHOSTTY_VT_PASTE_H
9+
10+/** @defgroup paste Paste Utilities
11+ *
12+ * Utilities for validating paste data safety.
13+ *
14+ * ## Basic Usage
15+ *
16+ * Use ghostty_paste_is_safe() to check if paste data contains potentially
17+ * dangerous sequences before sending it to the terminal.
18+ *
19+ * ## Example
20+ *
21+ * @code{.c}
22+ * #include <stdio.h>
23+ * #include <string.h>
24+ * #include <ghostty/vt.h>
25+ *
26+ * int main() {
27+ * const char* safe_data = "hello world";
28+ * const char* unsafe_data = "rm -rf /\n";
29+ *
30+ * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
31+ * printf("Safe to paste\n");
32+ * }
33+ *
34+ * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) {
35+ * printf("Unsafe! Contains newline\n");
36+ * }
37+ *
38+ * return 0;
39+ * }
40+ * @endcode
41+ *
42+ * @{
43+ */
44+
45+#include <stdbool.h>
46+#include <stddef.h>
47+
48+#ifdef __cplusplus
49+extern "C" {
50+#endif
51+
52+/**
53+ * Check if paste data is safe to paste into the terminal.
54+ *
55+ * Data is considered unsafe if it contains:
56+ * - Newlines (`\n`) which can inject commands
57+ * - The bracketed paste end sequence (`\x1b[201~`) which can be used
58+ * to exit bracketed paste mode and inject commands
59+ *
60+ * This check is conservative and considers data unsafe regardless of
61+ * current terminal state.
62+ *
63+ * @param data The paste data to check (must not be NULL)
64+ * @param len The length of the data in bytes
65+ * @return true if the data is safe to paste, false otherwise
66+ */
67+bool ghostty_paste_is_safe(const char* data, size_t len);
68+
69+#ifdef __cplusplus
70+}
71+#endif
72+
73+/** @} */
74+
75+#endif /* GHOSTTY_VT_PASTE_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/result.h+22−0
@@ -0,0 +1,22 @@
1+/**
2+ * @file result.h
3+ *
4+ * Result codes for libghostty-vt operations.
5+ */
6+
7+#ifndef GHOSTTY_VT_RESULT_H
8+#define GHOSTTY_VT_RESULT_H
9+
10+/**
11+ * Result codes for libghostty-vt operations.
12+ */
13+typedef enum {
14+ /** Operation completed successfully */
15+ GHOSTTY_SUCCESS = 0,
16+ /** Operation failed due to failed allocation */
17+ GHOSTTY_OUT_OF_MEMORY = -1,
18+ /** Operation failed due to invalid value */
19+ GHOSTTY_INVALID_VALUE = -2,
20+} GhosttyResult;
21+
22+#endif /* GHOSTTY_VT_RESULT_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/sgr.h+394−0
@@ -0,0 +1,394 @@
1+/**
2+ * @file sgr.h
3+ *
4+ * SGR (Select Graphic Rendition) attribute parsing and handling.
5+ */
6+
7+#ifndef GHOSTTY_VT_SGR_H
8+#define GHOSTTY_VT_SGR_H
9+
10+/** @defgroup sgr SGR Parser
11+ *
12+ * SGR (Select Graphic Rendition) attribute parser.
13+ *
14+ * SGR sequences are the syntax used to set styling attributes such as
15+ * bold, italic, underline, and colors for text in terminal emulators.
16+ * For example, you may be familiar with sequences like `ESC[1;31m`. The
17+ * `1;31` is the SGR attribute list.
18+ *
19+ * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`)
20+ * and returns individual text attributes like bold, italic, colors, etc.
21+ * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed,
22+ * and handles various color formats including 8-color, 16-color, 256-color,
23+ * X11 named colors, and RGB in multiple formats.
24+ *
25+ * ## Basic Usage
26+ *
27+ * 1. Create a parser instance with ghostty_sgr_new()
28+ * 2. Set SGR parameters with ghostty_sgr_set_params()
29+ * 3. Iterate through attributes using ghostty_sgr_next()
30+ * 4. Free the parser with ghostty_sgr_free() when done
31+ *
32+ * ## Example
33+ *
34+ * @code{.c}
35+ * #include <assert.h>
36+ * #include <stdio.h>
37+ * #include <ghostty/vt.h>
38+ *
39+ * int main() {
40+ * // Create parser
41+ * GhosttySgrParser parser;
42+ * GhosttyResult result = ghostty_sgr_new(NULL, &parser);
43+ * assert(result == GHOSTTY_SUCCESS);
44+ *
45+ * // Parse "bold, red foreground" sequence: ESC[1;31m
46+ * uint16_t params[] = {1, 31};
47+ * result = ghostty_sgr_set_params(parser, params, NULL, 2);
48+ * assert(result == GHOSTTY_SUCCESS);
49+ *
50+ * // Iterate through attributes
51+ * GhosttySgrAttribute attr;
52+ * while (ghostty_sgr_next(parser, &attr)) {
53+ * switch (attr.tag) {
54+ * case GHOSTTY_SGR_ATTR_BOLD:
55+ * printf("Bold enabled\n");
56+ * break;
57+ * case GHOSTTY_SGR_ATTR_FG_8:
58+ * printf("Foreground color: %d\n", attr.value.fg_8);
59+ * break;
60+ * default:
61+ * break;
62+ * }
63+ * }
64+ *
65+ * // Cleanup
66+ * ghostty_sgr_free(parser);
67+ * return 0;
68+ * }
69+ * @endcode
70+ *
71+ * @{
72+ */
73+
74+#include <ghostty/vt/allocator.h>
75+#include <ghostty/vt/color.h>
76+#include <ghostty/vt/result.h>
77+#include <stdbool.h>
78+#include <stddef.h>
79+#include <stdint.h>
80+
81+#ifdef __cplusplus
82+extern "C" {
83+#endif
84+
85+/**
86+ * Opaque handle to an SGR parser instance.
87+ *
88+ * This handle represents an SGR (Select Graphic Rendition) parser that can
89+ * be used to parse SGR sequences and extract individual text attributes.
90+ *
91+ * @ingroup sgr
92+ */
93+typedef struct GhosttySgrParser* GhosttySgrParser;
94+
95+/**
96+ * SGR attribute tags.
97+ *
98+ * These values identify the type of an SGR attribute in a tagged union.
99+ * Use the tag to determine which field in the attribute value union to access.
100+ *
101+ * @ingroup sgr
102+ */
103+typedef enum {
104+ GHOSTTY_SGR_ATTR_UNSET = 0,
105+ GHOSTTY_SGR_ATTR_UNKNOWN = 1,
106+ GHOSTTY_SGR_ATTR_BOLD = 2,
107+ GHOSTTY_SGR_ATTR_RESET_BOLD = 3,
108+ GHOSTTY_SGR_ATTR_ITALIC = 4,
109+ GHOSTTY_SGR_ATTR_RESET_ITALIC = 5,
110+ GHOSTTY_SGR_ATTR_FAINT = 6,
111+ GHOSTTY_SGR_ATTR_UNDERLINE = 7,
112+ GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8,
113+ GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9,
114+ GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10,
115+ GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11,
116+ GHOSTTY_SGR_ATTR_OVERLINE = 12,
117+ GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13,
118+ GHOSTTY_SGR_ATTR_BLINK = 14,
119+ GHOSTTY_SGR_ATTR_RESET_BLINK = 15,
120+ GHOSTTY_SGR_ATTR_INVERSE = 16,
121+ GHOSTTY_SGR_ATTR_RESET_INVERSE = 17,
122+ GHOSTTY_SGR_ATTR_INVISIBLE = 18,
123+ GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19,
124+ GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20,
125+ GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21,
126+ GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22,
127+ GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23,
128+ GHOSTTY_SGR_ATTR_BG_8 = 24,
129+ GHOSTTY_SGR_ATTR_FG_8 = 25,
130+ GHOSTTY_SGR_ATTR_RESET_FG = 26,
131+ GHOSTTY_SGR_ATTR_RESET_BG = 27,
132+ GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28,
133+ GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29,
134+ GHOSTTY_SGR_ATTR_BG_256 = 30,
135+ GHOSTTY_SGR_ATTR_FG_256 = 31,
136+} GhosttySgrAttributeTag;
137+
138+/**
139+ * Underline style types.
140+ *
141+ * @ingroup sgr
142+ */
143+typedef enum {
144+ GHOSTTY_SGR_UNDERLINE_NONE = 0,
145+ GHOSTTY_SGR_UNDERLINE_SINGLE = 1,
146+ GHOSTTY_SGR_UNDERLINE_DOUBLE = 2,
147+ GHOSTTY_SGR_UNDERLINE_CURLY = 3,
148+ GHOSTTY_SGR_UNDERLINE_DOTTED = 4,
149+ GHOSTTY_SGR_UNDERLINE_DASHED = 5,
150+} GhosttySgrUnderline;
151+
152+/**
153+ * Unknown SGR attribute data.
154+ *
155+ * Contains the full parameter list and the partial list where parsing
156+ * encountered an unknown or invalid sequence.
157+ *
158+ * @ingroup sgr
159+ */
160+typedef struct {
161+ const uint16_t* full_ptr;
162+ size_t full_len;
163+ const uint16_t* partial_ptr;
164+ size_t partial_len;
165+} GhosttySgrUnknown;
166+
167+/**
168+ * SGR attribute value union.
169+ *
170+ * This union contains all possible attribute values. Use the tag field
171+ * to determine which union member is active. Attributes without associated
172+ * data (like bold, italic) don't use the union value.
173+ *
174+ * @ingroup sgr
175+ */
176+typedef union {
177+ GhosttySgrUnknown unknown;
178+ GhosttySgrUnderline underline;
179+ GhosttyColorRgb underline_color;
180+ GhosttyColorPaletteIndex underline_color_256;
181+ GhosttyColorRgb direct_color_fg;
182+ GhosttyColorRgb direct_color_bg;
183+ GhosttyColorPaletteIndex bg_8;
184+ GhosttyColorPaletteIndex fg_8;
185+ GhosttyColorPaletteIndex bright_bg_8;
186+ GhosttyColorPaletteIndex bright_fg_8;
187+ GhosttyColorPaletteIndex bg_256;
188+ GhosttyColorPaletteIndex fg_256;
189+ uint64_t _padding[8];
190+} GhosttySgrAttributeValue;
191+
192+/**
193+ * SGR attribute (tagged union).
194+ *
195+ * A complete SGR attribute with both its type tag and associated value.
196+ * Always check the tag field to determine which value union member is valid.
197+ *
198+ * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be
199+ * identified by tag alone; the value union is not used for these and
200+ * the memory in the value field is undefined.
201+ *
202+ * @ingroup sgr
203+ */
204+typedef struct {
205+ GhosttySgrAttributeTag tag;
206+ GhosttySgrAttributeValue value;
207+} GhosttySgrAttribute;
208+
209+/**
210+ * Create a new SGR parser instance.
211+ *
212+ * Creates a new SGR (Select Graphic Rendition) parser using the provided
213+ * allocator. The parser must be freed using ghostty_sgr_free() when
214+ * no longer needed.
215+ *
216+ * @param allocator Pointer to the allocator to use for memory management, or
217+ * NULL to use the default allocator
218+ * @param parser Pointer to store the created parser handle
219+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
220+ *
221+ * @ingroup sgr
222+ */
223+GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator,
224+ GhosttySgrParser* parser);
225+
226+/**
227+ * Free an SGR parser instance.
228+ *
229+ * Releases all resources associated with the SGR parser. After this call,
230+ * the parser handle becomes invalid and must not be used. This includes
231+ * any attributes previously returned by ghostty_sgr_next().
232+ *
233+ * @param parser The parser handle to free (may be NULL)
234+ *
235+ * @ingroup sgr
236+ */
237+void ghostty_sgr_free(GhosttySgrParser parser);
238+
239+/**
240+ * Reset an SGR parser instance to the beginning of the parameter list.
241+ *
242+ * Resets the parser's iteration state without clearing the parameters.
243+ * After calling this, ghostty_sgr_next() will start from the beginning
244+ * of the parameter list again.
245+ *
246+ * @param parser The parser handle to reset, must not be NULL
247+ *
248+ * @ingroup sgr
249+ */
250+void ghostty_sgr_reset(GhosttySgrParser parser);
251+
252+/**
253+ * Set SGR parameters for parsing.
254+ *
255+ * Sets the SGR parameter list to parse. Parameters are the numeric values
256+ * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}).
257+ *
258+ * The separators array optionally specifies the separator type for each
259+ * parameter position. Each byte should be either ';' for semicolon or ':'
260+ * for colon. This is needed for certain color formats that use colon
261+ * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator
262+ * values are treated as semicolons. The separators array must have the same
263+ * length as the params array, if it is not NULL.
264+ *
265+ * If separators is NULL, all parameters are assumed to be semicolon-separated.
266+ *
267+ * This function makes an internal copy of the parameter and separator data,
268+ * so the caller can safely free or modify the input arrays after this call.
269+ *
270+ * After calling this function, the parser is automatically reset and ready
271+ * to iterate from the beginning.
272+ *
273+ * @param parser The parser handle, must not be NULL
274+ * @param params Array of SGR parameter values
275+ * @param separators Optional array of separator characters (';' or ':'), or
276+ * NULL
277+ * @param len Number of parameters (and separators if provided)
278+ * @return GHOSTTY_SUCCESS on success, or an error code on failure
279+ *
280+ * @ingroup sgr
281+ */
282+GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser,
283+ const uint16_t* params,
284+ const char* separators,
285+ size_t len);
286+
287+/**
288+ * Get the next SGR attribute.
289+ *
290+ * Parses and returns the next attribute from the parameter list.
291+ * Call this function repeatedly until it returns false to process
292+ * all attributes in the sequence.
293+ *
294+ * @param parser The parser handle, must not be NULL
295+ * @param attr Pointer to store the next attribute
296+ * @return true if an attribute was returned, false if no more attributes
297+ *
298+ * @ingroup sgr
299+ */
300+bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr);
301+
302+/**
303+ * Get the full parameter list from an unknown SGR attribute.
304+ *
305+ * This function retrieves the full parameter list that was provided to the
306+ * parser when an unknown attribute was encountered. Primarily useful in
307+ * WebAssembly environments where accessing struct fields directly is difficult.
308+ *
309+ * @param unknown The unknown attribute data
310+ * @param ptr Pointer to store the pointer to the parameter array (may be NULL)
311+ * @return The length of the full parameter array
312+ *
313+ * @ingroup sgr
314+ */
315+size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown,
316+ const uint16_t** ptr);
317+
318+/**
319+ * Get the partial parameter list from an unknown SGR attribute.
320+ *
321+ * This function retrieves the partial parameter list where parsing stopped
322+ * when an unknown attribute was encountered. Primarily useful in WebAssembly
323+ * environments where accessing struct fields directly is difficult.
324+ *
325+ * @param unknown The unknown attribute data
326+ * @param ptr Pointer to store the pointer to the parameter array (may be NULL)
327+ * @return The length of the partial parameter array
328+ *
329+ * @ingroup sgr
330+ */
331+size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown,
332+ const uint16_t** ptr);
333+
334+/**
335+ * Get the tag from an SGR attribute.
336+ *
337+ * This function extracts the tag that identifies which type of attribute
338+ * this is. Primarily useful in WebAssembly environments where accessing
339+ * struct fields directly is difficult.
340+ *
341+ * @param attr The SGR attribute
342+ * @return The attribute tag
343+ *
344+ * @ingroup sgr
345+ */
346+GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr);
347+
348+/**
349+ * Get the value from an SGR attribute.
350+ *
351+ * This function returns a pointer to the value union from an SGR attribute. Use
352+ * the tag to determine which field of the union is valid. Primarily useful in
353+ * WebAssembly environments where accessing struct fields directly is difficult.
354+ *
355+ * @param attr Pointer to the SGR attribute
356+ * @return Pointer to the attribute value union
357+ *
358+ * @ingroup sgr
359+ */
360+GhosttySgrAttributeValue* ghostty_sgr_attribute_value(
361+ GhosttySgrAttribute* attr);
362+
363+#ifdef __wasm__
364+/**
365+ * Allocate memory for an SGR attribute (WebAssembly only).
366+ *
367+ * This is a convenience function for WebAssembly environments to allocate
368+ * memory for an SGR attribute structure that can be passed to ghostty_sgr_next.
369+ *
370+ * @return Pointer to the allocated attribute structure
371+ *
372+ * @ingroup wasm
373+ */
374+GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void);
375+
376+/**
377+ * Free memory for an SGR attribute (WebAssembly only).
378+ *
379+ * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute.
380+ *
381+ * @param attr Pointer to the attribute structure to free
382+ *
383+ * @ingroup wasm
384+ */
385+void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr);
386+#endif
387+
388+#ifdef __cplusplus
389+}
390+#endif
391+
392+/** @} */
393+
394+#endif /* GHOSTTY_VT_SGR_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/ghostty/vt/wasm.h+159−0
@@ -0,0 +1,159 @@
1+/**
2+ * @file wasm.h
3+ *
4+ * WebAssembly utility functions for libghostty-vt.
5+ */
6+
7+#ifndef GHOSTTY_VT_WASM_H
8+#define GHOSTTY_VT_WASM_H
9+
10+#ifdef __wasm__
11+
12+#include <stddef.h>
13+#include <stdint.h>
14+
15+/** @defgroup wasm WebAssembly Utilities
16+ *
17+ * Convenience functions for allocating various types in WebAssembly builds.
18+ * **These are only available the libghostty-vt wasm module.**
19+ *
20+ * Ghostty relies on pointers to various types for ABI compatibility, and
21+ * creating those pointers in Wasm can be tedious. These functions provide
22+ * a purely additive set of utilities that simplify memory management in
23+ * Wasm environments without changing the core C library API.
24+ *
25+ * @note These functions always use the default allocator. If you need
26+ * custom allocation strategies, you should allocate types manually using
27+ * your custom allocator. This is a very rare use case in the WebAssembly
28+ * world so these are optimized for simplicity.
29+ *
30+ * ## Example Usage
31+ *
32+ * Here's a simple example of using the Wasm utilities with the key encoder:
33+ *
34+ * @code
35+ * const { exports } = wasmInstance;
36+ * const view = new DataView(wasmMemory.buffer);
37+ *
38+ * // Create key encoder
39+ * const encoderPtr = exports.ghostty_wasm_alloc_opaque();
40+ * exports.ghostty_key_encoder_new(null, encoderPtr);
41+ * const encoder = view.getUint32(encoder, true);
42+ *
43+ * // Configure encoder with Kitty protocol flags
44+ * const flagsPtr = exports.ghostty_wasm_alloc_u8();
45+ * view.setUint8(flagsPtr, 0x1F);
46+ * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr);
47+ *
48+ * // Allocate output buffer and size pointer
49+ * const bufferSize = 32;
50+ * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize);
51+ * const writtenPtr = exports.ghostty_wasm_alloc_usize();
52+ *
53+ * // Encode the key event
54+ * exports.ghostty_key_encoder_encode(
55+ * encoder, eventPtr, bufPtr, bufferSize, writtenPtr
56+ * );
57+ *
58+ * // Read encoded output
59+ * const bytesWritten = view.getUint32(writtenPtr, true);
60+ * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten);
61+ * @endcode
62+ *
63+ * @remark The code above is pretty ugly! This is the lowest level interface
64+ * to the libghostty-vt Wasm module. In practice, this should be wrapped
65+ * in a higher-level API that abstracts away all this.
66+ *
67+ * @{
68+ */
69+
70+/**
71+ * Allocate an opaque pointer. This can be used for any opaque pointer
72+ * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc.
73+ *
74+ * @return Pointer to allocated opaque pointer, or NULL if allocation failed
75+ * @ingroup wasm
76+ */
77+void** ghostty_wasm_alloc_opaque(void);
78+
79+/**
80+ * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque().
81+ *
82+ * @param ptr Pointer to free, or NULL (NULL is safely ignored)
83+ * @ingroup wasm
84+ */
85+void ghostty_wasm_free_opaque(void **ptr);
86+
87+/**
88+ * Allocate an array of uint8_t values.
89+ *
90+ * @param len Number of uint8_t elements to allocate
91+ * @return Pointer to allocated array, or NULL if allocation failed
92+ * @ingroup wasm
93+ */
94+uint8_t* ghostty_wasm_alloc_u8_array(size_t len);
95+
96+/**
97+ * Free an array allocated by ghostty_wasm_alloc_u8_array().
98+ *
99+ * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
100+ * @param len Length of the array (must match the length passed to alloc)
101+ * @ingroup wasm
102+ */
103+void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len);
104+
105+/**
106+ * Allocate an array of uint16_t values.
107+ *
108+ * @param len Number of uint16_t elements to allocate
109+ * @return Pointer to allocated array, or NULL if allocation failed
110+ * @ingroup wasm
111+ */
112+uint16_t* ghostty_wasm_alloc_u16_array(size_t len);
113+
114+/**
115+ * Free an array allocated by ghostty_wasm_alloc_u16_array().
116+ *
117+ * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
118+ * @param len Length of the array (must match the length passed to alloc)
119+ * @ingroup wasm
120+ */
121+void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len);
122+
123+/**
124+ * Allocate a single uint8_t value.
125+ *
126+ * @return Pointer to allocated uint8_t, or NULL if allocation failed
127+ * @ingroup wasm
128+ */
129+uint8_t* ghostty_wasm_alloc_u8(void);
130+
131+/**
132+ * Free a uint8_t allocated by ghostty_wasm_alloc_u8().
133+ *
134+ * @param ptr Pointer to free, or NULL (NULL is safely ignored)
135+ * @ingroup wasm
136+ */
137+void ghostty_wasm_free_u8(uint8_t *ptr);
138+
139+/**
140+ * Allocate a single size_t value.
141+ *
142+ * @return Pointer to allocated size_t, or NULL if allocation failed
143+ * @ingroup wasm
144+ */
145+size_t* ghostty_wasm_alloc_usize(void);
146+
147+/**
148+ * Free a size_t allocated by ghostty_wasm_alloc_usize().
149+ *
150+ * @param ptr Pointer to free, or NULL (NULL is safely ignored)
151+ * @ingroup wasm
152+ */
153+void ghostty_wasm_free_usize(size_t *ptr);
154+
155+/** @} */
156+
157+#endif /* __wasm__ */
158+
159+#endif /* GHOSTTY_VT_WASM_H */
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/Headers/module.modulemap+7−0
@@ -0,0 +1,7 @@
1+// This makes Ghostty available to the XCode build for the macOS app.
2+// We append "Kit" to it not to be cute, but because targets have to have
3+// unique names and we use Ghostty for other things.
4+module GhosttyKit {
5+ umbrella header "ghostty.h"
6+ export *
7+}
apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a+0−0

Binary file not shown.

apps/mobile/modules/t3-terminal/Vendor/libghostty/VERSION+1−0
@@ -0,0 +1 @@
1+268a0a9d761fb19673f05d28042488e2002300f2
apps/mobile/modules/t3-terminal/android/build.gradle+19−0
@@ -0,0 +1,19 @@
1+apply plugin: 'com.android.library'
2+apply plugin: 'org.jetbrains.kotlin.android'
3+
4+group = 'com.t3tools.terminal'
5+version = '0.0.0'
6+
7+android {
8+ namespace 'expo.modules.t3terminal'
9+ compileSdk rootProject.ext.compileSdkVersion
10+
11+ defaultConfig {
12+ minSdkVersion rootProject.ext.minSdkVersion
13+ targetSdkVersion rootProject.ext.targetSdkVersion
14+ }
15+}
16+
17+dependencies {
18+ implementation project(':expo-modules-core')
19+}
apps/mobile/modules/t3-terminal/android/src/main/AndroidManifest.xml+1−0
@@ -0,0 +1 @@
1+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt+46−0
@@ -0,0 +1,46 @@
1+package expo.modules.t3terminal
2+
3+import expo.modules.kotlin.modules.Module
4+import expo.modules.kotlin.modules.ModuleDefinition
5+
6+class T3TerminalModule : Module() {
7+ override fun definition() = ModuleDefinition {
8+ Name("T3TerminalSurface")
9+
10+ View(T3TerminalView::class) {
11+ Prop("terminalKey") { view: T3TerminalView, terminalKey: String ->
12+ view.terminalKey = terminalKey
13+ }
14+
15+ Prop("initialBuffer") { view: T3TerminalView, initialBuffer: String ->
16+ view.initialBuffer = initialBuffer
17+ }
18+
19+ Prop("fontSize") { view: T3TerminalView, fontSize: Double ->
20+ view.fontSize = fontSize.toFloat()
21+ }
22+
23+ Prop("appearanceScheme") { view: T3TerminalView, appearanceScheme: String ->
24+ view.appearanceScheme = appearanceScheme
25+ }
26+
27+ Prop("themeConfig") { view: T3TerminalView, themeConfig: String ->
28+ view.themeConfig = themeConfig
29+ }
30+
31+ Prop("backgroundColor") { view: T3TerminalView, backgroundColor: String ->
32+ view.backgroundColorHex = backgroundColor
33+ }
34+
35+ Prop("foregroundColor") { view: T3TerminalView, foregroundColor: String ->
36+ view.foregroundColorHex = foregroundColor
37+ }
38+
39+ Prop("mutedForegroundColor") { view: T3TerminalView, mutedForegroundColor: String ->
40+ view.mutedForegroundColorHex = mutedForegroundColor
41+ }
42+
43+ Events("onInput", "onResize")
44+ }
45+ }
46+}
apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt+210−0
@@ -0,0 +1,210 @@
1+package expo.modules.t3terminal
2+
3+import android.content.Context
4+import android.graphics.Color
5+import android.graphics.Typeface
6+import android.view.View
7+import android.view.ViewGroup
8+import android.view.inputmethod.InputMethodManager
9+import android.view.inputmethod.EditorInfo
10+import android.widget.EditText
11+import android.widget.LinearLayout
12+import android.widget.ScrollView
13+import android.widget.TextView
14+import androidx.core.widget.doAfterTextChanged
15+import expo.modules.kotlin.AppContext
16+import expo.modules.kotlin.views.ExpoView
17+import expo.modules.kotlin.viewevent.EventDispatcher
18+import kotlin.math.max
19+import kotlin.math.min
20+
21+class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
22+ private val container = LinearLayout(context)
23+ private val scrollView = ScrollView(context)
24+ private val textView = TextView(context)
25+ private val inputView = EditText(context)
26+ private val onInput by EventDispatcher()
27+ private val onResize by EventDispatcher()
28+ private var lastWidth = 0
29+ private var lastHeight = 0
30+ private var clearingInput = false
31+ private var backgroundColorValue = Color.parseColor("#24292E")
32+ private var foregroundColorValue = Color.parseColor("#D1D5DA")
33+ private var mutedForegroundColorValue = Color.parseColor("#959DA5")
34+
35+ var terminalKey: String = ""
36+ set(value) {
37+ field = value
38+ contentDescription = "t3-terminal-$value"
39+ }
40+
41+ var initialBuffer: String = ""
42+ set(value) {
43+ field = value
44+ textView.text = value.ifEmpty { "$ " }
45+ scrollView.post {
46+ scrollView.fullScroll(View.FOCUS_DOWN)
47+ }
48+ }
49+
50+ var fontSize: Float = 12f
51+ set(value) {
52+ field = value
53+ textView.textSize = value
54+ emitResize()
55+ }
56+
57+ var appearanceScheme: String = "dark"
58+ set(value) {
59+ field = value
60+ }
61+
62+ var themeConfig: String = ""
63+
64+ var backgroundColorHex: String = "#24292E"
65+ set(value) {
66+ field = value
67+ backgroundColorValue = parseColor(value, backgroundColorValue)
68+ applyTheme()
69+ }
70+
71+ var foregroundColorHex: String = "#D1D5DA"
72+ set(value) {
73+ field = value
74+ foregroundColorValue = parseColor(value, foregroundColorValue)
75+ applyTheme()
76+ }
77+
78+ var mutedForegroundColorHex: String = "#959DA5"
79+ set(value) {
80+ field = value
81+ mutedForegroundColorValue = parseColor(value, mutedForegroundColorValue)
82+ applyTheme()
83+ }
84+
85+ init {
86+ applyTheme()
87+ container.orientation = LinearLayout.VERTICAL
88+ textView.typeface = Typeface.MONOSPACE
89+ textView.textSize = fontSize
90+ textView.setPadding(8, 8, 8, 8)
91+ textView.text = "$ "
92+
93+ inputView.setSingleLine(true)
94+ inputView.setTextColor(Color.TRANSPARENT)
95+ inputView.setHintTextColor(Color.TRANSPARENT)
96+ inputView.setBackgroundColor(Color.TRANSPARENT)
97+ inputView.typeface = Typeface.MONOSPACE
98+ inputView.textSize = max(fontSize, 13f)
99+ inputView.hint = ""
100+ inputView.alpha = 0.02f
101+ inputView.imeOptions = EditorInfo.IME_ACTION_SEND
102+ inputView.setPadding(0, 0, 0, 0)
103+ inputView.setOnFocusChangeListener { _, hasFocus ->
104+ if (hasFocus) {
105+ showKeyboard()
106+ }
107+ }
108+ inputView.setOnEditorActionListener { view, actionId, _ ->
109+ if (actionId != EditorInfo.IME_ACTION_SEND) return@setOnEditorActionListener false
110+ onInput(mapOf("data" to "\n"))
111+ true
112+ }
113+ inputView.setOnKeyListener { _, keyCode, event ->
114+ if (event.action != android.view.KeyEvent.ACTION_DOWN) return@setOnKeyListener false
115+ when (keyCode) {
116+ android.view.KeyEvent.KEYCODE_DEL -> {
117+ onInput(mapOf("data" to "\u007F"))
118+ true
119+ }
120+ else -> false
121+ }
122+ }
123+ inputView.doAfterTextChanged { editable ->
124+ if (clearingInput) return@doAfterTextChanged
125+ val text = editable?.toString().orEmpty()
126+ if (text.isEmpty()) return@doAfterTextChanged
127+ onInput(mapOf("data" to text))
128+ clearingInput = true
129+ inputView.text?.clear()
130+ clearingInput = false
131+ }
132+
133+ textView.setOnClickListener { requestKeyboardFocus() }
134+ scrollView.setOnClickListener { requestKeyboardFocus() }
135+ container.setOnClickListener { requestKeyboardFocus() }
136+ isClickable = true
137+ setOnClickListener { requestKeyboardFocus() }
138+
139+ scrollView.addView(
140+ textView,
141+ LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
142+ )
143+ container.addView(
144+ scrollView,
145+ LinearLayout.LayoutParams(
146+ ViewGroup.LayoutParams.MATCH_PARENT,
147+ 0,
148+ 1f,
149+ ),
150+ )
151+ container.addView(
152+ inputView,
153+ LinearLayout.LayoutParams(1, 1),
154+ )
155+ addView(
156+ container,
157+ LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT),
158+ )
159+
160+ post {
161+ requestKeyboardFocus()
162+ }
163+ }
164+
165+ override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
166+ super.onSizeChanged(width, height, oldWidth, oldHeight)
167+ if (width == lastWidth && height == lastHeight) return
168+ lastWidth = width
169+ lastHeight = height
170+ emitResize()
171+ }
172+
173+ private fun emitResize() {
174+ if (width <= 0 || height <= 0) return
175+ val density = resources.displayMetrics.scaledDensity
176+ val fontPx = max(fontSize * density, 1f)
177+ val cols = max(20, min(400, (width / (fontPx * 0.62f)).toInt()))
178+ val terminalHeight = max(height - inputView.height, 0)
179+ val rows = max(5, min(200, (terminalHeight / (fontPx * 1.35f)).toInt()))
180+ onResize(mapOf("cols" to cols, "rows" to rows))
181+ }
182+
183+ private fun requestKeyboardFocus() {
184+ inputView.requestFocus()
185+ showKeyboard()
186+ }
187+
188+ private fun applyTheme() {
189+ setBackgroundColor(backgroundColorValue)
190+ container.setBackgroundColor(backgroundColorValue)
191+ scrollView.setBackgroundColor(backgroundColorValue)
192+ textView.setTextColor(foregroundColorValue)
193+ textView.setBackgroundColor(backgroundColorValue)
194+ inputView.setTextColor(Color.TRANSPARENT)
195+ inputView.setHintTextColor(mutedForegroundColorValue)
196+ inputView.setBackgroundColor(Color.TRANSPARENT)
197+ }
198+
199+ private fun parseColor(value: String, fallback: Int): Int =
200+ try {
201+ Color.parseColor(value)
202+ } catch (_: IllegalArgumentException) {
203+ fallback
204+ }
205+
206+ private fun showKeyboard() {
207+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
208+ imm?.showSoftInput(inputView, InputMethodManager.SHOW_IMPLICIT)
209+ }
210+}
apps/mobile/modules/t3-terminal/expo-module.config.json+10−0
@@ -0,0 +1,10 @@
1+{
2+ "platforms": ["apple", "android"],
3+ "apple": {
4+ "modules": ["T3TerminalModule"],
5+ "podspecPath": "T3TerminalNative.podspec"
6+ },
7+ "android": {
8+ "modules": ["expo.modules.t3terminal.T3TerminalModule"]
9+ }
10+}
apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift+43−0
@@ -0,0 +1,43 @@
1+import ExpoModulesCore
2+
3+public class T3TerminalModule: Module {
4+ public func definition() -> ModuleDefinition {
5+ Name("T3TerminalSurface")
6+
7+ View(T3TerminalView.self) {
8+ Prop("terminalKey") { (view: T3TerminalView, terminalKey: String) in
9+ view.terminalKey = terminalKey
10+ }
11+
12+ Prop("initialBuffer") { (view: T3TerminalView, initialBuffer: String) in
13+ view.initialBuffer = initialBuffer
14+ }
15+
16+ Prop("fontSize") { (view: T3TerminalView, fontSize: Double) in
17+ view.fontSize = CGFloat(fontSize)
18+ }
19+
20+ Prop("appearanceScheme") { (view: T3TerminalView, appearanceScheme: String) in
21+ view.appearanceScheme = appearanceScheme
22+ }
23+
24+ Prop("themeConfig") { (view: T3TerminalView, themeConfig: String) in
25+ view.themeConfig = themeConfig
26+ }
27+
28+ Prop("backgroundColor") { (view: T3TerminalView, backgroundColor: String) in
29+ view.backgroundColorHex = backgroundColor
30+ }
31+
32+ Prop("foregroundColor") { (view: T3TerminalView, foregroundColor: String) in
33+ view.foregroundColorHex = foregroundColor
34+ }
35+
36+ Prop("mutedForegroundColor") { (view: T3TerminalView, mutedForegroundColor: String) in
37+ view.mutedForegroundColorHex = mutedForegroundColor
38+ }
39+
40+ Events("onInput", "onResize")
41+ }
42+ }
43+}
apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift+565−0
@@ -0,0 +1,565 @@
1+import ExpoModulesCore
2+import Foundation
3+import GhosttyKit
4+import QuartzCore
5+import UIKit
6+
7+private enum GhosttyRuntime {
8+ private static let lock = NSLock()
9+ private static var initialized = false
10+
11+ static func ensureInitialized() -> Bool {
12+ lock.lock()
13+ defer { lock.unlock() }
14+
15+ if initialized {
16+ return true
17+ }
18+
19+ let result = ghostty_init(0, nil)
20+ initialized = result == GHOSTTY_SUCCESS
21+ return initialized
22+ }
23+}
24+
25+private final class TerminalInputField: UITextField {
26+ var onDeleteBackward: (() -> Void)?
27+
28+ override func deleteBackward() {
29+ onDeleteBackward?()
30+ super.deleteBackward()
31+ }
32+}
33+
34+private enum TerminalAppearanceScheme: String {
35+ case light
36+ case dark
37+
38+ init(value: String) {
39+ self = TerminalAppearanceScheme(rawValue: value) ?? .dark
40+ }
41+
42+ var ghosttyColorScheme: ghostty_color_scheme_e {
43+ switch self {
44+ case .light:
45+ return GHOSTTY_COLOR_SCHEME_LIGHT
46+ case .dark:
47+ return GHOSTTY_COLOR_SCHEME_DARK
48+ }
49+ }
50+}
51+
52+private extension UIColor {
53+ convenience init(hexString: String) {
54+ let sanitized = hexString.replacingOccurrences(of: "#", with: "")
55+ let value = Int(sanitized, radix: 16) ?? 0
56+ self.init(
57+ red: CGFloat((value >> 16) & 0xFF) / 255,
58+ green: CGFloat((value >> 8) & 0xFF) / 255,
59+ blue: CGFloat(value & 0xFF) / 255,
60+ alpha: 1
61+ )
62+ }
63+}
64+
65+public final class T3TerminalView: ExpoView, UITextFieldDelegate {
66+ private static let minimumVerticalScrollStepPoints: CGFloat = 18
67+ private static let verticalScrollStepMultiplier: CGFloat = 1.15
68+
69+ private let terminalViewport = UIView()
70+ private let inputField = TerminalInputField()
71+ private let focusTapGesture = UITapGestureRecognizer()
72+ private let scrollPanGesture = UIPanGestureRecognizer()
73+ private var lastViewportSize: CGSize = .zero
74+ private var lastContentScale: CGFloat = 0
75+ private var lastReportedGrid: (cols: Int, rows: Int)?
76+ private var lastAppliedBuffer = ""
77+ private var pendingVerticalScrollPoints: CGFloat = 0
78+ private var app: ghostty_app_t?
79+ private var surface: ghostty_surface_t?
80+ private var isCreatingSurface = false
81+ private var surfaceCreationFailed = false
82+ private var appearance = TerminalAppearanceScheme.dark
83+ private var backgroundColorValue = UIColor(hexString: "#24292e")
84+
85+ let onInput = EventDispatcher()
86+ let onResize = EventDispatcher()
87+
88+ var terminalKey: String = "" {
89+ didSet {
90+ accessibilityIdentifier = "t3-terminal-\(terminalKey)"
91+ if oldValue != terminalKey {
92+ resetSurface()
93+ }
94+ }
95+ }
96+
97+ var initialBuffer: String = "" {
98+ didSet {
99+ applyRemoteBuffer(initialBuffer)
100+ }
101+ }
102+
103+ var fontSize: CGFloat = 12 {
104+ didSet {
105+ inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular)
106+ resetSurface()
107+ }
108+ }
109+
110+ var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue {
111+ didSet {
112+ appearance = TerminalAppearanceScheme(value: appearanceScheme)
113+ updateGhosttyTheme()
114+ }
115+ }
116+
117+ var themeConfig: String = "" {
118+ didSet {
119+ updateGhosttyTheme()
120+ }
121+ }
122+
123+ var backgroundColorHex: String = "#24292e" {
124+ didSet {
125+ backgroundColorValue = UIColor(hexString: backgroundColorHex)
126+ applyTheme()
127+ }
128+ }
129+
130+ var foregroundColorHex: String = "#d1d5da"
131+ var mutedForegroundColorHex: String = "#959da5"
132+
133+ public required init(appContext: AppContext? = nil) {
134+ super.init(appContext: appContext)
135+
136+ applyTheme()
137+ clipsToBounds = true
138+ contentScaleFactor = UIScreen.main.scale
139+
140+ terminalViewport.clipsToBounds = true
141+ terminalViewport.contentScaleFactor = contentScaleFactor
142+ terminalViewport.translatesAutoresizingMaskIntoConstraints = false
143+ terminalViewport.isUserInteractionEnabled = true
144+
145+ inputField.delegate = self
146+ inputField.backgroundColor = UIColor.clear
147+ inputField.textColor = UIColor.clear
148+ inputField.tintColor = UIColor.clear
149+ inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular)
150+ inputField.placeholder = ""
151+ inputField.autocorrectionType = .no
152+ inputField.autocapitalizationType = .none
153+ inputField.spellCheckingType = .no
154+ inputField.smartDashesType = .no
155+ inputField.smartQuotesType = .no
156+ inputField.returnKeyType = .send
157+ inputField.keyboardType = .asciiCapable
158+ inputField.enablesReturnKeyAutomatically = false
159+ inputField.translatesAutoresizingMaskIntoConstraints = false
160+ inputField.alpha = 0.02
161+ inputField.isAccessibilityElement = false
162+ inputField.accessibilityElementsHidden = true
163+ inputField.addTarget(self, action: #selector(handleInputEditingDidBegin), for: .editingDidBegin)
164+ inputField.onDeleteBackward = { [weak self] in
165+ self?.emitInput("\u{7F}")
166+ }
167+
168+ focusTapGesture.addTarget(self, action: #selector(handleViewportTap))
169+ terminalViewport.addGestureRecognizer(focusTapGesture)
170+ scrollPanGesture.addTarget(self, action: #selector(handleViewportPan(_:)))
171+ scrollPanGesture.maximumNumberOfTouches = 1
172+ scrollPanGesture.cancelsTouchesInView = false
173+ terminalViewport.addGestureRecognizer(scrollPanGesture)
174+
175+ addSubview(terminalViewport)
176+ addSubview(inputField)
177+
178+ NSLayoutConstraint.activate([
179+ terminalViewport.leadingAnchor.constraint(equalTo: leadingAnchor),
180+ terminalViewport.trailingAnchor.constraint(equalTo: trailingAnchor),
181+ terminalViewport.topAnchor.constraint(equalTo: topAnchor),
182+ terminalViewport.bottomAnchor.constraint(equalTo: bottomAnchor),
183+
184+ inputField.trailingAnchor.constraint(equalTo: trailingAnchor),
185+ inputField.topAnchor.constraint(equalTo: bottomAnchor, constant: 8),
186+ inputField.widthAnchor.constraint(equalToConstant: 1),
187+ inputField.heightAnchor.constraint(equalToConstant: 1),
188+ ])
189+ }
190+
191+ deinit {
192+ destroySurface()
193+ }
194+
195+ public override func layoutSubviews() {
196+ super.layoutSubviews()
197+ updateContentScale()
198+
199+ let viewportSize = terminalViewport.bounds.size
200+ if surface == nil {
201+ createSurfaceIfPossible()
202+ }
203+
204+ guard viewportSize != lastViewportSize || contentScaleFactor != lastContentScale else {
205+ return
206+ }
207+
208+ lastViewportSize = viewportSize
209+ lastContentScale = contentScaleFactor
210+ resizeSurface()
211+ }
212+
213+ public override func didMoveToWindow() {
214+ super.didMoveToWindow()
215+
216+ guard window != nil else { return }
217+ DispatchQueue.main.async { [weak self] in
218+ self?.requestKeyboardFocus()
219+ }
220+ }
221+
222+ public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
223+ if !string.isEmpty {
224+ emitInput(string)
225+ return false
226+ }
227+
228+ return false
229+ }
230+
231+ public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
232+ emitInput("\n")
233+ textField.text = ""
234+ return false
235+ }
236+
237+ @objc
238+ private func handleViewportTap() {
239+ requestKeyboardFocus()
240+ }
241+
242+ @objc
243+ private func handleViewportPan(_ gesture: UIPanGestureRecognizer) {
244+ guard let surface else { return }
245+
246+ let location = gesture.location(in: terminalViewport)
247+ ghostty_surface_mouse_pos(
248+ surface,
249+ Double(location.x * contentScaleFactor),
250+ Double(location.y * contentScaleFactor),
251+ GHOSTTY_MODS_NONE
252+ )
253+
254+ switch gesture.state {
255+ case .began:
256+ pendingVerticalScrollPoints = 0
257+ gesture.setTranslation(.zero, in: terminalViewport)
258+ case .changed:
259+ let translation = gesture.translation(in: terminalViewport)
260+ let stepSize = max(
261+ fontSize * Self.verticalScrollStepMultiplier,
262+ Self.minimumVerticalScrollStepPoints
263+ )
264+ let totalVerticalPoints = pendingVerticalScrollPoints + translation.y
265+ let verticalSteps = Int(totalVerticalPoints / stepSize)
266+ pendingVerticalScrollPoints = totalVerticalPoints - (CGFloat(verticalSteps) * stepSize)
267+
268+ guard verticalSteps != 0 else {
269+ gesture.setTranslation(.zero, in: terminalViewport)
270+ return
271+ }
272+
273+ ghostty_surface_mouse_scroll(surface, 0, Double(verticalSteps), 0)
274+ redrawSurface()
275+ gesture.setTranslation(.zero, in: terminalViewport)
276+ default:
277+ pendingVerticalScrollPoints = 0
278+ gesture.setTranslation(.zero, in: terminalViewport)
279+ }
280+ }
281+
282+ @objc
283+ private func handleInputEditingDidBegin() {
284+ textInputModeDidChange()
285+ }
286+
287+ private func createSurfaceIfPossible() {
288+ guard surface == nil, app == nil, !isCreatingSurface, !surfaceCreationFailed else { return }
289+ guard terminalViewport.bounds.width > 0, terminalViewport.bounds.height > 0 else { return }
290+ guard GhosttyRuntime.ensureInitialized() else {
291+ surfaceCreationFailed = true
292+ return
293+ }
294+
295+ isCreatingSurface = true
296+ defer { isCreatingSurface = false }
297+
298+ var runtimeConfig = ghostty_runtime_config_s(
299+ userdata: Unmanaged.passUnretained(self).toOpaque(),
300+ supports_selection_clipboard: false,
301+ wakeup_cb: { _ in },
302+ action_cb: { _, _, _ in false },
303+ read_clipboard_cb: { _, _, _ in },
304+ confirm_read_clipboard_cb: { _, _, _, _ in },
305+ write_clipboard_cb: { _, _, _, _, _ in },
306+ close_surface_cb: { _, _ in }
307+ )
308+
309+ guard let config = ghostty_config_new() else {
310+ surfaceCreationFailed = true
311+ return
312+ }
313+ loadThemeConfig(into: config)
314+ ghostty_config_finalize(config)
315+ defer { ghostty_config_free(config) }
316+
317+ guard let createdApp = ghostty_app_new(&runtimeConfig, config) else {
318+ surfaceCreationFailed = true
319+ return
320+ }
321+
322+ var surfaceConfig = ghostty_surface_config_new()
323+ surfaceConfig.platform_tag = GHOSTTY_PLATFORM_IOS
324+ surfaceConfig.platform.ios.uiview = Unmanaged.passUnretained(terminalViewport).toOpaque()
325+ surfaceConfig.userdata = Unmanaged.passUnretained(self).toOpaque()
326+ surfaceConfig.scale_factor = Double(contentScaleFactor)
327+ surfaceConfig.font_size = Float(fontSize)
328+ surfaceConfig.context = GHOSTTY_SURFACE_CONTEXT_WINDOW
329+ surfaceConfig.use_custom_io = true
330+
331+ guard let createdSurface = ghostty_surface_new(createdApp, &surfaceConfig) else {
332+ ghostty_app_free(createdApp)
333+ surfaceCreationFailed = true
334+ return
335+ }
336+
337+ app = createdApp
338+ surface = createdSurface
339+ ghostty_app_set_color_scheme(createdApp, appearance.ghosttyColorScheme)
340+ ghostty_surface_set_color_scheme(createdSurface, appearance.ghosttyColorScheme)
341+ setupWriteCallback()
342+ resizeSurface()
343+ feedBuffer(initialBuffer)
344+ }
345+
346+ private func resetSurface() {
347+ destroySurface()
348+ lastAppliedBuffer = ""
349+ lastViewportSize = .zero
350+ lastContentScale = 0
351+ lastReportedGrid = nil
352+ surfaceCreationFailed = false
353+ setNeedsLayout()
354+ }
355+
356+ private func destroySurface() {
357+ if let surface {
358+ ghostty_surface_set_write_callback(surface, nil, nil)
359+ ghostty_surface_free(surface)
360+ }
361+ if let app {
362+ ghostty_app_free(app)
363+ }
364+ surface = nil
365+ app = nil
366+ }
367+
368+ private func applyRemoteBuffer(_ buffer: String) {
369+ guard surface != nil else {
370+ createSurfaceIfPossible()
371+ return
372+ }
373+
374+ if buffer.hasPrefix(lastAppliedBuffer) {
375+ let suffix = String(buffer.dropFirst(lastAppliedBuffer.count))
376+ feedData(Data(suffix.utf8))
377+ lastAppliedBuffer = buffer
378+ return
379+ }
380+
381+ resetSurface()
382+ createSurfaceIfPossible()
383+ }
384+
385+ private func feedBuffer(_ buffer: String) {
386+ guard !buffer.isEmpty else { return }
387+ feedData(Data(buffer.utf8))
388+ lastAppliedBuffer = buffer
389+ }
390+
391+ private func feedData(_ data: Data) {
392+ guard let surface, !data.isEmpty else { return }
393+
394+ data.withUnsafeBytes { buffer in
395+ guard let pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
396+ return
397+ }
398+ ghostty_surface_feed_data(surface, pointer, buffer.count)
399+ }
400+
401+ redrawSurface()
402+ }
403+
404+ private func setupWriteCallback() {
405+ guard let surface else { return }
406+
407+ let userdata = Unmanaged.passUnretained(self).toOpaque()
408+ ghostty_surface_set_write_callback(surface, { userdata, data, len in
409+ guard let userdata, let data, len > 0 else { return }
410+ let view = Unmanaged<T3TerminalView>.fromOpaque(userdata).takeUnretainedValue()
411+ let bytes = Data(bytes: data, count: len)
412+ guard let input = String(data: bytes, encoding: .utf8), !input.isEmpty else { return }
413+
414+ DispatchQueue.main.async {
415+ view.onInput(["data": input])
416+ }
417+ }, userdata)
418+ }
419+
420+ private func resizeSurface() {
421+ guard let surface else {
422+ emitEstimatedResize()
423+ return
424+ }
425+
426+ let scale = contentScaleFactor
427+ let width = UInt32(max(floor(terminalViewport.bounds.width * scale), 1))
428+ let height = UInt32(max(floor(terminalViewport.bounds.height * scale), 1))
429+
430+ terminalViewport.contentScaleFactor = scale
431+ ghostty_surface_set_content_scale(surface, Double(scale), Double(scale))
432+ ghostty_surface_set_size(surface, width, height)
433+ ghostty_surface_set_occlusion(surface, window != nil)
434+ configureIOSurfaceLayers()
435+ redrawSurface()
436+ emitGhosttyResize()
437+ }
438+
439+ private func redrawSurface() {
440+ guard let surface else { return }
441+ ghostty_surface_refresh(surface)
442+ ghostty_surface_draw(surface)
443+ markIOSurfaceLayersForDisplay()
444+ emitGhosttyResize()
445+ }
446+
447+ private func emitGhosttyResize() {
448+ guard let surface else {
449+ emitEstimatedResize()
450+ return
451+ }
452+
453+ let size = ghostty_surface_size(surface)
454+ let cols = max(1, Int(size.columns))
455+ let rows = max(1, Int(size.rows))
456+ emitResize(cols: cols, rows: rows)
457+ }
458+
459+ private func emitEstimatedResize() {
460+ guard bounds.width > 0, bounds.height > 0 else { return }
461+
462+ let cellWidth = max(fontSize * 0.62, 1)
463+ let cellHeight = max(fontSize * 1.35, 1)
464+ let cols = max(20, min(400, Int(bounds.width / cellWidth)))
465+ let terminalHeight = max(bounds.height, 0)
466+ let rows = max(5, min(200, Int(terminalHeight / cellHeight)))
467+ emitResize(cols: cols, rows: rows)
468+ }
469+
470+ private func emitResize(cols: Int, rows: Int) {
471+ guard lastReportedGrid?.cols != cols || lastReportedGrid?.rows != rows else {
472+ return
473+ }
474+
475+ lastReportedGrid = (cols, rows)
476+ onResize([
477+ "cols": cols,
478+ "rows": rows,
479+ ])
480+ }
481+
482+ private func updateContentScale() {
483+ let scale = window?.screen.scale ?? UIScreen.main.scale
484+ if contentScaleFactor != scale {
485+ contentScaleFactor = scale
486+ }
487+ }
488+
489+ private func requestKeyboardFocus() {
490+ guard window != nil else { return }
491+ inputField.becomeFirstResponder()
492+ textInputModeDidChange()
493+ }
494+
495+ private func emitInput(_ data: String) {
496+ guard !data.isEmpty else { return }
497+ onInput(["data": data])
498+ }
499+
500+ private func textInputModeDidChange() {
501+ guard let app else { return }
502+ ghostty_app_keyboard_changed(app)
503+ }
504+
505+ private func configureIOSurfaceLayers() {
506+ let targetBounds = CGRect(origin: .zero, size: terminalViewport.bounds.size)
507+ CATransaction.begin()
508+ CATransaction.setDisableActions(true)
509+ terminalViewport.layer.sublayers?.forEach { sublayer in
510+ sublayer.frame = targetBounds
511+ sublayer.contentsScale = contentScaleFactor
512+ }
513+ CATransaction.commit()
514+ }
515+
516+ private func markIOSurfaceLayersForDisplay() {
517+ terminalViewport.layer.setNeedsDisplay()
518+ terminalViewport.layer.sublayers?.forEach { layer in
519+ layer.setNeedsDisplay()
520+ }
521+ }
522+
523+ private func applyTheme() {
524+ backgroundColor = backgroundColorValue
525+ terminalViewport.backgroundColor = backgroundColorValue
526+ }
527+
528+ private func updateGhosttyTheme() {
529+ guard let app, let surface else { return }
530+ guard let config = ghostty_config_new() else { return }
531+ loadThemeConfig(into: config)
532+ ghostty_config_finalize(config)
533+ ghostty_app_update_config(app, config)
534+ ghostty_surface_update_config(surface, config)
535+ ghostty_app_set_color_scheme(app, appearance.ghosttyColorScheme)
536+ ghostty_surface_set_color_scheme(surface, appearance.ghosttyColorScheme)
537+ ghostty_config_free(config)
538+ redrawSurface()
539+ }
540+
541+ private func loadThemeConfig(into config: ghostty_config_t) {
542+ guard let path = writeThemeConfigFile() else { return }
543+ path.withCString { cString in
544+ ghostty_config_load_file(config, cString)
545+ }
546+ }
547+
548+ private func writeThemeConfigFile() -> String? {
549+ guard !themeConfig.isEmpty else { return nil }
550+ let configContents = themeConfig
551+ let url = URL(fileURLWithPath: NSTemporaryDirectory())
552+ .appendingPathComponent("t3-terminal-theme-\(appearance.rawValue).ghostty")
553+
554+ do {
555+ if let existing = try? String(contentsOf: url, encoding: .utf8), existing == configContents {
556+ return url.path
557+ }
558+
559+ try configContents.write(to: url, atomically: true, encoding: .utf8)
560+ return url.path
561+ } catch {
562+ return nil
563+ }
564+ }
565+}
apps/mobile/modules/t3-terminal/package.json+17−0
@@ -0,0 +1,17 @@
1+{
2+ "name": "@t3tools/mobile-terminal-native",
3+ "version": "0.0.0",
4+ "private": true,
5+ "expo-module": {
6+ "ios": {
7+ "modules": [
8+ "T3TerminalModule"
9+ ]
10+ },
11+ "android": {
12+ "modules": [
13+ "expo.modules.t3terminal.T3TerminalModule"
14+ ]
15+ }
16+ }
17+}
apps/mobile/package.json+4−0
@@ -30,6 +30,7 @@ · 29 unmodified lines
3030 "config:dev": "APP_VARIANT=development expo config",
3131 "config:preview": "APP_VARIANT=preview expo config",
3232 "config:prod": "APP_VARIANT=production expo config",
33+ "test": "vitest run",
3334 "typecheck": "tsc --noEmit"
3435 },
3536 "dependencies": {
@@ -45,11 +46,13 @@ · 9 unmodified lines
4546 "@shikijs/themes": "3.23.0",
4647 "@t3tools/client-runtime": "workspace:*",
4748 "@t3tools/contracts": "workspace:*",
49+ "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal",
4850 "@t3tools/shared": "workspace:*",
4951 "clsx": "^2.1.1",
5052 "diff": "8.0.3",
5153 "effect": "catalog:",
5254 "expo": "~55.0.14",
55+ "expo-build-properties": "~55.0.13",
5356 "expo-camera": "~55.0.15",
5457 "expo-clipboard": "~55.0.9",
5558 "expo-constants": "^55.0.9",
@@ -60,6 +63,7 @@ · 4 unmodified lines
6063 "expo-haptics": "^55.0.9",
6164 "expo-image-picker": "~55.0.14",
6265 "expo-linking": "~55.0.12",
66+ "expo-modules-core": "~55.0.22",
6367 "expo-paste-input": "^0.1.15",
6468 "expo-router": "~55.0.12",
6569 "expo-secure-store": "~55.0.9",
apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx+10−0
@@ -65,6 +65,16 @@ export default function ThreadLayout() { · 64 unmodified lines
6565 sheetGrabberVisible: true,
6666 }}
6767 />
68+ <Stack.Screen
69+ name="terminal"
70+ options={{
71+ animation: "slide_from_right",
72+ contentStyle: { backgroundColor: "#050505" },
73+ headerBackButtonDisplayMode: "minimal",
74+ headerShown: true,
75+ headerShadowVisible: false,
76+ }}
77+ />
6878 </Stack>
6979 );
7080 }
apps/mobile/src/app/threads/[environmentId]/[threadId]/terminal.tsx+5−0
@@ -0,0 +1,5 @@
1+import { ThreadTerminalRouteScreen } from "../../../../features/threads/ThreadTerminalRouteScreen";
2+
3+export default function ThreadTerminalRoute() {
4+ return <ThreadTerminalRouteScreen />;
5+}
apps/mobile/src/features/threads/ThreadGitControls.tsx+63−1
@@ -1,4 +1,4 @@
1−import type { GitRunStackedActionResult, GitStatusResult } from "@t3tools/contracts";
1+import type { GitRunStackedActionResult, GitStatusResult, ProjectScript } from "@t3tools/contracts";
22 import {
33 type GitActionRequestInput,
44 requiresDefaultBranchConfirmation,
@@ -9,6 +9,14 @@ import Stack from "expo-router/stack"; · 4 unmodified lines
99 import { useCallback, useMemo } from "react";
1010 import { Alert, Linking } from "react-native";
1111 import { buildThreadReviewRoutePath } from "../../lib/routes";
12+import {
13+ basename,
14+ getTerminalLabel,
15+ getTerminalStatusLabel,
16+ projectScriptMenuIcon,
17+ projectScriptMenuLabel,
18+ type TerminalMenuSession,
19+} from "./terminalMenu";
1220
1321 function truncateMiddle(value: string, maxLength: number): string {
1422 if (value.length <= maxLength) {
@@ -55,6 +63,12 @@ export function ThreadGitControls(props: { · 40 unmodified lines
5563 readonly currentBranch: string | null;
5664 readonly gitStatus: GitStatusResult | null;
5765 readonly gitOperationLabel: string | null;
66+ readonly canOpenTerminal: boolean;
67+ readonly projectScripts: ReadonlyArray<ProjectScript>;
68+ readonly terminalSessions: ReadonlyArray<TerminalMenuSession>;
69+ readonly onOpenTerminal: (terminalId?: string | null) => void;
70+ readonly onOpenNewTerminal: () => void;
71+ readonly onRunProjectScript: (script: ProjectScript) => Promise<void>;
5872 readonly onPull: () => Promise<void>;
5973 readonly onRunAction: (input: GitActionRequestInput) => Promise<GitRunStackedActionResult | null>;
6074 }) {
@@ -167,6 +181,54 @@ export function ThreadGitControls(props: { · 106 unmodified lines
167181
168182 return (
169183 <Stack.Toolbar placement="right">
184+ <Stack.Toolbar.Menu icon="terminal" disabled={!props.canOpenTerminal} separateBackground>
185+ {props.projectScripts.length > 0 ? (
186+ <Stack.Toolbar.Menu icon="play" inline title="Project scripts">
187+ <Stack.Toolbar.Label>Project scripts</Stack.Toolbar.Label>
188+ {props.projectScripts.map((script) => (
189+ <Stack.Toolbar.MenuAction
190+ key={script.id}
191+ icon={projectScriptMenuIcon(script.icon)}
192+ onPress={() => void props.onRunProjectScript(script)}
193+ subtitle={script.command}
194+ >
195+ <Stack.Toolbar.Label>{projectScriptMenuLabel(script)}</Stack.Toolbar.Label>
196+ </Stack.Toolbar.MenuAction>
197+ ))}
198+ </Stack.Toolbar.Menu>
199+ ) : (
200+ <Stack.Toolbar.MenuAction
201+ icon="play"
202+ disabled
203+ onPress={() => {}}
204+ subtitle="This project has no saved scripts yet"
205+ >
206+ <Stack.Toolbar.Label>No project scripts</Stack.Toolbar.Label>
207+ </Stack.Toolbar.MenuAction>
208+ )}
209+ <Stack.Toolbar.Menu icon="terminal" inline title="Terminals">
210+ <Stack.Toolbar.Label>Terminals</Stack.Toolbar.Label>
211+ {props.terminalSessions.map((session) => (
212+ <Stack.Toolbar.MenuAction
213+ key={session.terminalId}
214+ icon="terminal"
215+ onPress={() => props.onOpenTerminal(session.terminalId)}
216+ subtitle={[getTerminalStatusLabel({ status: session.status }), basename(session.cwd)]
217+ .filter(Boolean)
218+ .join(" · ")}
219+ >
220+ <Stack.Toolbar.Label>{getTerminalLabel(session.terminalId)}</Stack.Toolbar.Label>
221+ </Stack.Toolbar.MenuAction>
222+ ))}
223+ <Stack.Toolbar.MenuAction
224+ icon="plus"
225+ onPress={props.onOpenNewTerminal}
226+ subtitle="Start another shell for this thread"
227+ >
228+ <Stack.Toolbar.Label>Open new terminal</Stack.Toolbar.Label>
229+ </Stack.Toolbar.MenuAction>
230+ </Stack.Toolbar.Menu>
231+ </Stack.Toolbar.Menu>
170232 <Stack.Toolbar.Menu icon="point.topleft.down.curvedto.point.bottomright.up">
171233 <Stack.Toolbar.MenuAction
172234 icon="point.topleft.down.curvedto.point.bottomright.up"
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+114−10
@@ -3,6 +3,8 @@ import { useCallback, useMemo, useState } from "react"; · 2 unmodified lines
33 import * as Arr from "effect/Array";
44 import * as Option from "effect/Option";
55 import { pipe } from "effect/Function";
6+import type { ProjectScript } from "@t3tools/contracts";
7+import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
68 import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native";
79 import { useThemeColor } from "../../lib/useThemeColor";
810 import { useGitStatus, gitStatusManager } from "../../state/use-git-status";
@@ -10,24 +12,33 @@ import { dismissGitActionResult, useGitActionProgress } from "../../state/use-gi · 1 unmodified line
1012
1113 import { EmptyState } from "../../components/EmptyState";
1214 import { LoadingScreen } from "../../components/LoadingScreen";
13−import { buildThreadRoutePath } from "../../lib/routes";
15+import { buildThreadRoutePath, buildThreadTerminalRoutePath } from "../../lib/routes";
1416 import { scopedThreadKey } from "../../lib/scopedEntities";
1517 import { connectionTone } from "../connection/connectionTone";
1618
1719 import { useRemoteCatalog } from "../../state/use-remote-catalog";
1820 import {
21+ getEnvironmentClient,
1922 useRemoteConnectionStatus,
2023 useRemoteEnvironmentState,
2124 } from "../../state/use-remote-environment-registry";
25+import { terminalSessionManager, useKnownTerminalSessions } from "../../state/use-terminal-session";
2226 import { useSelectedThreadDetail } from "../../state/use-thread-detail";
2327 import { useThreadSelection } from "../../state/use-thread-selection";
2428 import { GitActionProgressOverlay } from "./GitActionProgressOverlay";
29+import {
30+ buildTerminalMenuSessions,
31+ nextTerminalId,
32+ resolveProjectScriptTerminalId,
33+} from "./terminalMenu";
34+import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext";
2535 import { ThreadDetailScreen } from "./ThreadDetailScreen";
2636 import { ThreadGitControls } from "./ThreadGitControls";
2737 import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer";
2838 import { useSelectedThreadCommands } from "./use-selected-thread-commands";
2939 import { useSelectedThreadGitActions } from "./use-selected-thread-git-actions";
3040 import { useSelectedThreadGitState } from "./use-selected-thread-git-state";
41+import { useSelectedThreadWorktree } from "./use-selected-thread-worktree";
3142 import { useThreadComposerState } from "./use-thread-composer-state";
3243
3344 function firstRouteParam(value: string | string[] | undefined): string | null {
@@ -47,6 +58,7 @@ export function ThreadRouteScreen() { · 13 unmodified lines
4758 const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } =
4859 useThreadSelection();
4960 const selectedThreadDetail = useSelectedThreadDetail();
61+ const { selectedThreadCwd } = useSelectedThreadWorktree();
5062 const composer = useThreadComposerState();
5163 const gitState = useSelectedThreadGitState();
5264 const gitActions = useSelectedThreadGitActions();
@@ -80,20 +92,29 @@ export function ThreadRouteScreen() { · 27 unmodified lines
8092 /* ─── Git status for native header trigger ───────────────────────── */
8193 const gitStatus = useGitStatus({
8294 environmentId: selectedThread?.environmentId ?? "",
83− cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
95+ cwd: selectedThreadCwd,
96+ });
97+ const knownTerminalSessions = useKnownTerminalSessions({
98+ environmentId: selectedThread?.environmentId ?? null,
99+ threadId: selectedThread?.id ?? null,
84100 });
101+ const terminalMenuSessions = useMemo(
102+ () =>
103+ buildTerminalMenuSessions({
104+ knownSessions: knownTerminalSessions,
105+ workspaceRoot: selectedThreadProject?.workspaceRoot ?? null,
106+ }),
107+ [knownTerminalSessions, selectedThreadProject?.workspaceRoot],
108+ );
109+ const selectedThreadDetailWorktreePath = selectedThreadDetail?.worktreePath ?? null;
85110
86111 /* ─── Git action progress (for overlay banner) ──────────────────── */
87112 const gitActionProgressTarget = useMemo(
88113 () => ({
89114 environmentId: selectedThread?.environmentId ?? null,
90− cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
115+ cwd: selectedThreadCwd,
91116 }),
92− [
93− selectedThread?.environmentId,
94− selectedThread?.worktreePath,
95− selectedThreadProject?.workspaceRoot,
96− ],
117+ [selectedThread?.environmentId, selectedThreadCwd],
97118 );
98119 const gitActionProgress = useGitActionProgress(gitActionProgressTarget);
99120
@@ -101,9 +122,9 @@ export function ThreadRouteScreen() { · 1 unmodified line
101122 if (!selectedThread) return;
102123 await gitStatusManager.refresh({
103124 environmentId: selectedThread.environmentId,
104− cwd: selectedThread.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
125+ cwd: selectedThreadCwd,
105126 });
106− }, [selectedThread, selectedThreadProject?.workspaceRoot]);
127+ }, [selectedThread, selectedThreadCwd]);
107128
108129 /** Wraps thread refresh + git status refresh for pull-to-refresh */
109130 const handleRefreshAll = useCallback(async () => {
@@ -119,6 +140,83 @@ export function ThreadRouteScreen() { · 9 unmodified lines
119140 void router.push("/connections");
120141 }, [router]);
121142
143+ const handleOpenTerminal = useCallback(
144+ (nextTerminalId?: string | null) => {
145+ if (!selectedThread || !selectedThreadProject?.workspaceRoot) {
146+ return;
147+ }
148+
149+ void router.push(buildThreadTerminalRoutePath(selectedThread, nextTerminalId));
150+ },
151+ [router, selectedThread, selectedThreadProject?.workspaceRoot],
152+ );
153+
154+ const handleOpenNewTerminal = useCallback(() => {
155+ if (!selectedThread || !selectedThreadProject?.workspaceRoot) {
156+ return;
157+ }
158+
159+ const nextId = nextTerminalId(terminalMenuSessions.map((session) => session.terminalId));
160+ void router.push(buildThreadTerminalRoutePath(selectedThread, nextId));
161+ }, [router, selectedThread, selectedThreadProject?.workspaceRoot, terminalMenuSessions]);
162+
163+ const handleRunProjectScript = useCallback(
164+ async (script: ProjectScript) => {
165+ if (!selectedThread || !selectedThreadProject?.workspaceRoot) {
166+ return;
167+ }
168+
169+ const client = getEnvironmentClient(selectedThread.environmentId);
170+ if (!client) {
171+ return;
172+ }
173+
174+ const targetTerminalId = resolveProjectScriptTerminalId({
175+ existingTerminalIds: terminalMenuSessions.map((session) => session.terminalId),
176+ hasRunningTerminal: terminalMenuSessions.some(
177+ (session) => session.status === "running" || session.status === "starting",
178+ ),
179+ });
180+ const preferredWorktreePath = resolvePreferredThreadWorktreePath({
181+ threadShellWorktreePath: selectedThread.worktreePath ?? null,
182+ threadDetailWorktreePath: selectedThreadDetailWorktreePath,
183+ });
184+ const cwd = projectScriptCwd({
185+ project: { cwd: selectedThreadProject.workspaceRoot },
186+ worktreePath: preferredWorktreePath,
187+ });
188+ const env = projectScriptRuntimeEnv({
189+ project: { cwd: selectedThreadProject.workspaceRoot },
190+ worktreePath: preferredWorktreePath,
191+ });
192+ const snapshot = await client.terminal.open({
193+ threadId: selectedThread.id,
194+ terminalId: targetTerminalId,
195+ cwd,
196+ worktreePath: preferredWorktreePath,
197+ env,
198+ });
199+
200+ terminalSessionManager.syncSnapshot(
201+ { environmentId: selectedThread.environmentId },
202+ snapshot,
203+ );
204+ await client.terminal.write({
205+ threadId: selectedThread.id,
206+ terminalId: targetTerminalId,
207+ data: `${script.command}\r`,
208+ });
209+ void router.push(buildThreadTerminalRoutePath(selectedThread, targetTerminalId));
210+ },
211+ [
212+ router,
213+ selectedThread,
214+ selectedThreadDetailWorktreePath,
215+ selectedThreadProject,
216+ terminalMenuSessions,
217+ ],
218+ );
219+
122220 if (!environmentId || !threadId) {
123221 return <LoadingScreen message="Opening thread…" />;
124222 }
@@ -223,6 +321,12 @@ export function ThreadRouteScreen() { · 98 unmodified lines
223321 currentBranch={selectedThreadDetail.branch}
224322 gitStatus={gitStatus.data}
225323 gitOperationLabel={gitState.gitOperationLabel}
324+ canOpenTerminal={Boolean(selectedThreadProject?.workspaceRoot)}
325+ projectScripts={selectedThreadProject?.scripts ?? []}
326+ terminalSessions={terminalMenuSessions}
327+ onOpenTerminal={handleOpenTerminal}
328+ onOpenNewTerminal={handleOpenNewTerminal}
329+ onRunProjectScript={handleRunProjectScript}
226330 onPull={gitActions.onPullSelectedThreadBranch}
227331 onRunAction={gitActions.onRunSelectedThreadGitAction}
228332 />
apps/mobile/src/features/threads/ThreadTerminalPanel.tsx+170−0
@@ -0,0 +1,170 @@
1+import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts";
2+import { SymbolView } from "expo-symbols";
3+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4+import { Pressable, View } from "react-native";
5+
6+import { AppText as Text } from "../../components/AppText";
7+import { getEnvironmentClient } from "../../state/use-remote-environment-registry";
8+import { useTerminalSession, useTerminalSessionTarget } from "../../state/use-terminal-session";
9+import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface";
10+import { hasNativeTerminalSurface } from "../../native/terminal/nativeTerminalModule";
11+
12+interface ThreadTerminalPanelProps {
13+ readonly environmentId: string;
14+ readonly threadId: string;
15+ readonly cwd: string;
16+ readonly worktreePath: string | null;
17+ readonly visible: boolean;
18+ readonly onClose: () => void;
19+}
20+
21+const DEFAULT_TERMINAL_COLS = 80;
22+const DEFAULT_TERMINAL_ROWS = 24;
23+
24+export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(
25+ props: ThreadTerminalPanelProps,
26+) {
27+ const nativeTerminalAvailable = hasNativeTerminalSurface();
28+ const terminalId = DEFAULT_TERMINAL_ID;
29+ const target = useTerminalSessionTarget({
30+ environmentId: props.environmentId,
31+ threadId: props.threadId,
32+ terminalId,
33+ });
34+ const terminal = useTerminalSession(target);
35+ const [lastGridSize, setLastGridSize] = useState({
36+ cols: DEFAULT_TERMINAL_COLS,
37+ rows: DEFAULT_TERMINAL_ROWS,
38+ });
39+ const hasOpenedRef = useRef(false);
40+
41+ const terminalKey = useMemo(
42+ () => `${props.environmentId}:${props.threadId}:${terminalId}`,
43+ [props.environmentId, props.threadId, terminalId],
44+ );
45+ const isRunning = terminal.status === "running" || terminal.status === "starting";
46+
47+ useEffect(() => {
48+ hasOpenedRef.current = false;
49+ }, [terminalKey]);
50+
51+ const openTerminal = useCallback(async () => {
52+ const client = getEnvironmentClient(props.environmentId);
53+ if (!client) {
54+ return;
55+ }
56+
57+ await client.terminal.open({
58+ threadId: props.threadId,
59+ terminalId,
60+ cwd: props.cwd,
61+ worktreePath: props.worktreePath,
62+ cols: lastGridSize.cols,
63+ rows: lastGridSize.rows,
64+ });
65+ }, [
66+ lastGridSize.cols,
67+ lastGridSize.rows,
68+ props.cwd,
69+ props.environmentId,
70+ props.threadId,
71+ props.worktreePath,
72+ terminalId,
73+ ]);
74+
75+ useEffect(() => {
76+ if (!props.visible || hasOpenedRef.current || terminal.status !== "closed") {
77+ return;
78+ }
79+
80+ hasOpenedRef.current = true;
81+ void openTerminal().catch(() => {
82+ hasOpenedRef.current = false;
83+ });
84+ }, [openTerminal, props.visible, terminal.status]);
85+
86+ const handleInput = useCallback(
87+ (data: string) => {
88+ const client = getEnvironmentClient(props.environmentId);
89+ if (!client || !isRunning) {
90+ return;
91+ }
92+
93+ void client.terminal.write({
94+ threadId: props.threadId,
95+ terminalId,
96+ data,
97+ });
98+ },
99+ [isRunning, props.environmentId, props.threadId, terminalId],
100+ );
101+
102+ const handleResize = useCallback(
103+ (size: { readonly cols: number; readonly rows: number }) => {
104+ if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) {
105+ return;
106+ }
107+
108+ setLastGridSize(size);
109+ const client = getEnvironmentClient(props.environmentId);
110+ if (!client || !isRunning) {
111+ return;
112+ }
113+
114+ void client.terminal.resize({
115+ threadId: props.threadId,
116+ terminalId,
117+ cols: size.cols,
118+ rows: size.rows,
119+ });
120+ },
121+ [
122+ isRunning,
123+ lastGridSize.cols,
124+ lastGridSize.rows,
125+ props.environmentId,
126+ props.threadId,
127+ terminalId,
128+ ],
129+ );
130+
131+ if (!props.visible) {
132+ return null;
133+ }
134+
135+ return (
136+ <View className="absolute inset-x-3 bottom-28 top-28 overflow-hidden rounded-[8px] border border-white/10 bg-neutral-950 shadow-2xl">
137+ <View className="flex-row items-center justify-between border-b border-white/10 px-3 py-2">
138+ <View className="min-w-0 flex-1">
139+ <Text className="font-t3-bold text-[13px] text-neutral-100" numberOfLines={1}>
140+ Terminal
141+ </Text>
142+ <Text className="text-[11px] text-neutral-500" numberOfLines={1}>
143+ {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"}
144+ </Text>
145+ </View>
146+ <View className="flex-row items-center gap-2">
147+ {terminal.error ? (
148+ <Text className="max-w-44 text-right text-[11px] text-red-300" numberOfLines={1}>
149+ {terminal.error}
150+ </Text>
151+ ) : null}
152+ <Pressable
153+ className="h-8 w-8 items-center justify-center rounded-[8px] bg-white/10"
154+ onPress={props.onClose}
155+ >
156+ <SymbolView name="xmark" size={13} tintColor="#e5e5e5" type="monochrome" />
157+ </Pressable>
158+ </View>
159+ </View>
160+ <TerminalSurface
161+ terminalKey={terminalKey}
162+ buffer={terminal.buffer}
163+ isRunning={isRunning}
164+ onInput={handleInput}
165+ onResize={handleResize}
166+ style={{ flex: 1 }}
167+ />
168+ </View>
169+ );
170+});
apps/mobile/src/features/threads/ThreadTerminalRouteScreen.test.ts+66−0
@@ -0,0 +1,66 @@
1+import { describe, expect, it } from "vitest";
2+
3+import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap";
4+
5+describe("resolveTerminalRouteBootstrap", () => {
6+ it("redirects bare terminal routes to another already-running terminal for the thread", () => {
7+ expect(
8+ resolveTerminalRouteBootstrap({
9+ hasThread: true,
10+ hasWorkspaceRoot: true,
11+ hasOpened: false,
12+ requestedTerminalId: null,
13+ currentTerminalId: "default",
14+ runningTerminalId: "term-2",
15+ }),
16+ ).toEqual({
17+ kind: "redirect",
18+ terminalId: "term-2",
19+ });
20+ });
21+
22+ it("hydrates the current running terminal instead of skipping open", () => {
23+ expect(
24+ resolveTerminalRouteBootstrap({
25+ hasThread: true,
26+ hasWorkspaceRoot: true,
27+ hasOpened: false,
28+ requestedTerminalId: null,
29+ currentTerminalId: "default",
30+ runningTerminalId: "default",
31+ }),
32+ ).toEqual({
33+ kind: "open",
34+ });
35+ });
36+
37+ it("opens explicit terminal routes once so they replay existing history", () => {
38+ expect(
39+ resolveTerminalRouteBootstrap({
40+ hasThread: true,
41+ hasWorkspaceRoot: true,
42+ hasOpened: false,
43+ requestedTerminalId: "term-2",
44+ currentTerminalId: "term-2",
45+ runningTerminalId: "term-2",
46+ }),
47+ ).toEqual({
48+ kind: "open",
49+ });
50+ });
51+
52+ it("stays idle after the route already bootstrapped once", () => {
53+ expect(
54+ resolveTerminalRouteBootstrap({
55+ hasThread: true,
56+ hasWorkspaceRoot: true,
57+ hasOpened: true,
58+ requestedTerminalId: null,
59+ currentTerminalId: "default",
60+ runningTerminalId: "default",
61+ }),
62+ ).toEqual({
63+ kind: "idle",
64+ });
65+ });
66+});
apps/mobile/src/features/threads/ThreadTerminalRouteScreen.tsx+734−0
@@ -0,0 +1,734 @@
1+import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts";
2+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
3+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4+import { Pressable, ScrollView, Text as RNText, View } from "react-native";
5+import { KeyboardStickyView } from "react-native-keyboard-controller";
6+import Animated, { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated";
7+
8+import { EmptyState } from "../../components/EmptyState";
9+import { LoadingScreen } from "../../components/LoadingScreen";
10+import { buildThreadTerminalRoutePath } from "../../lib/routes";
11+import {
12+ getEnvironmentClient,
13+ useRemoteEnvironmentState,
14+} from "../../state/use-remote-environment-registry";
15+import {
16+ terminalSessionManager,
17+ useKnownTerminalSessions,
18+ useTerminalSession,
19+ useTerminalSessionTarget,
20+} from "../../state/use-terminal-session";
21+import { useThreadSelection } from "../../state/use-thread-selection";
22+import { useSelectedThreadDetail } from "../../state/use-thread-detail";
23+import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface";
24+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
25+import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap";
26+import { resolveTerminalOpenLocation } from "./terminalLaunchContext";
27+import {
28+ basename,
29+ buildTerminalMenuSessions,
30+ getTerminalLabel,
31+ getTerminalStatusLabel,
32+ nextTerminalId,
33+} from "./terminalMenu";
34+import {
35+ DEFAULT_TERMINAL_FONT_SIZE,
36+ MAX_TERMINAL_FONT_SIZE,
37+ MIN_TERMINAL_FONT_SIZE,
38+ TERMINAL_FONT_SIZE_STEP,
39+ normalizeTerminalFontSize,
40+} from "./terminalPreferences";
41+
42+const DEFAULT_TERMINAL_COLS = 80;
43+const DEFAULT_TERMINAL_ROWS = 24;
44+const TERMINAL_ACCESSORY_HEIGHT = 52;
45+
46+type PendingModifier = "ctrl" | "meta";
47+type HostPlatform = "mac" | "linux" | "windows" | "unknown";
48+
49+type TerminalToolbarAction =
50+ | { readonly kind: "send"; readonly key: string; readonly label: string; readonly data: string }
51+ | {
52+ readonly kind: "modifier";
53+ readonly key: string;
54+ readonly label: string;
55+ readonly modifier: PendingModifier;
56+ };
57+
58+interface TerminalMenuSession {
59+ readonly terminalId: string;
60+ readonly cwd: string | null;
61+ readonly status: "starting" | "running" | "exited" | "error" | "closed";
62+ readonly updatedAt: string | null;
63+}
64+
65+function getTerminalStatusTone(input: {
66+ readonly status: TerminalMenuSession["status"];
67+ readonly hasRunningSubprocess?: boolean;
68+}): {
69+ readonly tintColor: string;
70+ readonly textColor: string;
71+} {
72+ if (input.status === "running") {
73+ if (input.hasRunningSubprocess) {
74+ return {
75+ tintColor: "#fbbf24",
76+ textColor: "#a3a3a3",
77+ };
78+ }
79+
80+ return {
81+ tintColor: "#34d399",
82+ textColor: "#a3a3a3",
83+ };
84+ }
85+
86+ if (input.status === "starting") {
87+ return {
88+ tintColor: "#f59e0b",
89+ textColor: "#a3a3a3",
90+ };
91+ }
92+
93+ if (input.status === "error") {
94+ return {
95+ tintColor: "#ef4444",
96+ textColor: "#fca5a5",
97+ };
98+ }
99+
100+ return {
101+ tintColor: "#ef4444",
102+ textColor: "#a3a3a3",
103+ };
104+}
105+
106+function firstRouteParam(value: string | string[] | undefined): string | null {
107+ if (Array.isArray(value)) {
108+ return value[0] ?? null;
109+ }
110+
111+ return value ?? null;
112+}
113+
114+function inferHostPlatform(environmentLabel: string | null): HostPlatform {
115+ const value = environmentLabel?.toLowerCase() ?? "";
116+ if (
117+ value.includes("mac") ||
118+ value.includes("macbook") ||
119+ value.includes("mac mini") ||
120+ value.includes("imac") ||
121+ value.includes("darwin")
122+ ) {
123+ return "mac";
124+ }
125+ if (value.includes("windows") || value.includes("win")) {
126+ return "windows";
127+ }
128+ if (value.includes("linux") || value.includes("ubuntu") || value.includes("debian")) {
129+ return "linux";
130+ }
131+
132+ return "unknown";
133+}
134+
135+function applyCtrlModifier(input: string): string {
136+ const firstCharacter = input[0];
137+ if (!firstCharacter) {
138+ return input;
139+ }
140+
141+ const lowerCharacter = firstCharacter.toLowerCase();
142+ if (lowerCharacter >= "a" && lowerCharacter <= "z") {
143+ return String.fromCharCode(lowerCharacter.charCodeAt(0) - 96);
144+ }
145+
146+ if (firstCharacter === "@") return "\u0000";
147+ if (firstCharacter === "[") return "\u001b";
148+ if (firstCharacter === "\\") return "\u001c";
149+ if (firstCharacter === "]") return "\u001d";
150+ if (firstCharacter === "^") return "\u001e";
151+ if (firstCharacter === "_") return "\u001f";
152+ if (firstCharacter === "?") return "\u007f";
153+
154+ return input;
155+}
156+
157+export function ThreadTerminalRouteScreen() {
158+ const router = useRouter();
159+ const keyboard = useAnimatedKeyboard();
160+ const { isLoadingSavedConnection } = useRemoteEnvironmentState();
161+ const params = useLocalSearchParams<{
162+ environmentId?: string | string[];
163+ threadId?: string | string[];
164+ terminalId?: string | string[];
165+ }>();
166+ const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } =
167+ useThreadSelection();
168+ const selectedThreadDetail = useSelectedThreadDetail();
169+ const requestedTerminalId = firstRouteParam(params.terminalId);
170+ const knownSessions = useKnownTerminalSessions({
171+ environmentId: selectedThread?.environmentId ?? null,
172+ threadId: selectedThread?.id ?? null,
173+ });
174+ const [lastGridSize, setLastGridSize] = useState({
175+ cols: DEFAULT_TERMINAL_COLS,
176+ rows: DEFAULT_TERMINAL_ROWS,
177+ });
178+ const [pendingModifier, setPendingModifier] = useState<PendingModifier | null>(null);
179+ const [fontSize, setFontSize] = useState(DEFAULT_TERMINAL_FONT_SIZE);
180+ const hasOpenedRef = useRef(false);
181+ const hasLoadedFontPreferenceRef = useRef(false);
182+
183+ const terminalId = requestedTerminalId ?? DEFAULT_TERMINAL_ID;
184+ const target = useTerminalSessionTarget({
185+ environmentId: selectedThread?.environmentId ?? null,
186+ threadId: selectedThread?.id ?? null,
187+ terminalId,
188+ });
189+ const terminal = useTerminalSession(target);
190+ const terminalKey = useMemo(
191+ () =>
192+ selectedThread
193+ ? `${selectedThread.environmentId}:${selectedThread.id}:${terminalId}`
194+ : terminalId,
195+ [selectedThread, terminalId],
196+ );
197+ const isRunning = terminal.status === "running" || terminal.status === "starting";
198+ const cwd = terminal.snapshot?.cwd ?? selectedThreadProject?.workspaceRoot ?? null;
199+ const hostPlatform = useMemo(
200+ () => inferHostPlatform(selectedEnvironmentConnection?.environmentLabel ?? null),
201+ [selectedEnvironmentConnection?.environmentLabel],
202+ );
203+ const runningSession = useMemo(
204+ () =>
205+ knownSessions.find(
206+ (session) => session.state.status === "running" || session.state.status === "starting",
207+ ) ?? null,
208+ [knownSessions],
209+ );
210+ const activeKnownSession = useMemo(
211+ () => knownSessions.find((session) => session.target.terminalId === terminalId) ?? null,
212+ [knownSessions, terminalId],
213+ );
214+ const terminalStatusTone = useMemo(
215+ () =>
216+ getTerminalStatusTone({
217+ status: terminal.status,
218+ hasRunningSubprocess: terminal.hasRunningSubprocess,
219+ }),
220+ [terminal.hasRunningSubprocess, terminal.status],
221+ );
222+ const headerTitle = useMemo(() => {
223+ const topLineParts = [
224+ selectedEnvironmentConnection?.environmentLabel ?? null,
225+ selectedThreadProject?.title ?? null,
226+ ].filter((value): value is string => Boolean(value));
227+
228+ return {
229+ topLine: topLineParts.join(" \u00b7 "),
230+ bottomLine: cwd ?? selectedThreadProject?.workspaceRoot ?? "",
231+ };
232+ }, [
233+ cwd,
234+ selectedEnvironmentConnection?.environmentLabel,
235+ selectedThreadProject?.title,
236+ selectedThreadProject?.workspaceRoot,
237+ ]);
238+ const terminalToolbarActions = useMemo<ReadonlyArray<TerminalToolbarAction>>(() => {
239+ const modifierActions: ReadonlyArray<TerminalToolbarAction> =
240+ hostPlatform === "mac"
241+ ? [
242+ { kind: "modifier", key: "cmd", label: "cmd", modifier: "meta" },
243+ { kind: "modifier", key: "ctrl", label: "ctrl", modifier: "ctrl" },
244+ ]
245+ : [
246+ { kind: "modifier", key: "ctrl", label: "ctrl", modifier: "ctrl" },
247+ { kind: "modifier", key: "alt", label: "alt", modifier: "meta" },
248+ ];
249+
250+ return [
251+ { kind: "send", key: "esc", label: "esc", data: "\u001b" },
252+ ...modifierActions,
253+ { kind: "send", key: "tab", label: "tab", data: "\t" },
254+ { kind: "send", key: "up", label: "↑", data: "\u001b[A" },
255+ { kind: "send", key: "down", label: "↓", data: "\u001b[B" },
256+ { kind: "send", key: "left", label: "←", data: "\u001b[D" },
257+ { kind: "send", key: "right", label: "→", data: "\u001b[C" },
258+ { kind: "send", key: "tilde", label: "~", data: "~" },
259+ { kind: "send", key: "pipe", label: "|", data: "|" },
260+ { kind: "send", key: "slash", label: "/", data: "/" },
261+ { kind: "send", key: "dash", label: "-", data: "-" },
262+ ];
263+ }, [hostPlatform]);
264+ const terminalBottomInset = TERMINAL_ACCESSORY_HEIGHT;
265+ const terminalContainerAnimatedStyle = useAnimatedStyle(() => ({
266+ paddingBottom:
267+ keyboard.height.value > 0
268+ ? keyboard.height.value + TERMINAL_ACCESSORY_HEIGHT
269+ : terminalBottomInset,
270+ }));
271+
272+ const terminalMenuSessions = useMemo<ReadonlyArray<TerminalMenuSession>>(
273+ () =>
274+ buildTerminalMenuSessions({
275+ knownSessions,
276+ workspaceRoot: selectedThreadProject?.workspaceRoot ?? null,
277+ currentSession: {
278+ terminalId,
279+ cwd: cwd ?? null,
280+ status: terminal.status,
281+ updatedAt: terminal.updatedAt,
282+ },
283+ }),
284+ [
285+ cwd,
286+ knownSessions,
287+ selectedThreadProject?.workspaceRoot,
288+ terminal.status,
289+ terminal.updatedAt,
290+ terminalId,
291+ ],
292+ );
293+
294+ const openTerminal = useCallback(async () => {
295+ if (!selectedThread || !selectedThreadProject?.workspaceRoot) {
296+ return;
297+ }
298+
299+ const client = getEnvironmentClient(selectedThread.environmentId);
300+ if (!client) {
301+ return;
302+ }
303+
304+ const launchLocation = resolveTerminalOpenLocation({
305+ terminalSnapshot: terminal.snapshot,
306+ activeSessionSnapshot: activeKnownSession?.state.snapshot ?? null,
307+ workspaceRoot: selectedThreadProject.workspaceRoot,
308+ threadShellWorktreePath: selectedThread.worktreePath ?? null,
309+ threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null,
310+ });
311+
312+ const snapshot = await client.terminal.open({
313+ threadId: selectedThread.id,
314+ terminalId,
315+ cwd: launchLocation.cwd,
316+ worktreePath: launchLocation.worktreePath,
317+ cols: lastGridSize.cols,
318+ rows: lastGridSize.rows,
319+ });
320+
321+ terminalSessionManager.syncSnapshot({ environmentId: selectedThread.environmentId }, snapshot);
322+ }, [
323+ lastGridSize.cols,
324+ lastGridSize.rows,
325+ activeKnownSession?.state.snapshot,
326+ selectedThreadDetail?.worktreePath,
327+ selectedThread,
328+ selectedThreadProject?.workspaceRoot,
329+ terminal.snapshot,
330+ terminalId,
331+ ]);
332+
333+ useEffect(() => {
334+ hasOpenedRef.current = false;
335+ }, [terminalKey]);
336+
337+ useEffect(() => {
338+ let cancelled = false;
339+
340+ void loadPreferences()
341+ .then((preferences) => {
342+ if (cancelled) {
343+ return;
344+ }
345+
346+ setFontSize(normalizeTerminalFontSize(preferences.terminalFontSize));
347+ hasLoadedFontPreferenceRef.current = true;
348+ })
349+ .catch(() => {
350+ if (cancelled) {
351+ return;
352+ }
353+
354+ hasLoadedFontPreferenceRef.current = true;
355+ });
356+
357+ return () => {
358+ cancelled = true;
359+ };
360+ }, []);
361+
362+ useEffect(() => {
363+ if (!hasLoadedFontPreferenceRef.current) {
364+ return;
365+ }
366+
367+ void savePreferencesPatch({
368+ terminalFontSize: normalizeTerminalFontSize(fontSize),
369+ });
370+ }, [fontSize]);
371+
372+ useEffect(() => {
373+ const bootstrapAction = resolveTerminalRouteBootstrap({
374+ hasThread: selectedThread !== null,
375+ hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot),
376+ hasOpened: hasOpenedRef.current,
377+ requestedTerminalId,
378+ currentTerminalId: terminalId,
379+ runningTerminalId: runningSession?.target.terminalId ?? null,
380+ });
381+ if (bootstrapAction.kind === "idle" || !selectedThread) {
382+ return;
383+ }
384+
385+ if (bootstrapAction.kind === "redirect") {
386+ router.replace(buildThreadTerminalRoutePath(selectedThread, bootstrapAction.terminalId));
387+ return;
388+ }
389+
390+ hasOpenedRef.current = true;
391+ void openTerminal().catch(() => {
392+ hasOpenedRef.current = false;
393+ });
394+ }, [
395+ openTerminal,
396+ requestedTerminalId,
397+ router,
398+ runningSession,
399+ selectedThread,
400+ selectedThreadProject?.workspaceRoot,
401+ terminalId,
402+ ]);
403+
404+ useEffect(() => {
405+ setPendingModifier(null);
406+ }, [terminalId]);
407+
408+ const writeInput = useCallback(
409+ (data: string) => {
410+ if (!selectedThread || !isRunning) {
411+ return;
412+ }
413+
414+ const client = getEnvironmentClient(selectedThread.environmentId);
415+ if (!client) {
416+ return;
417+ }
418+
419+ void client.terminal.write({
420+ threadId: selectedThread.id,
421+ terminalId,
422+ data,
423+ });
424+ },
425+ [isRunning, selectedThread, terminalId],
426+ );
427+
428+ const handleInput = useCallback(
429+ (data: string) => {
430+ if (data.length === 0) {
431+ return;
432+ }
433+
434+ if (pendingModifier === "ctrl") {
435+ setPendingModifier(null);
436+ writeInput(applyCtrlModifier(data));
437+ } else if (pendingModifier === "meta") {
438+ setPendingModifier(null);
439+ writeInput(`\u001b${data}`);
440+ } else {
441+ writeInput(data);
442+ }
443+ },
444+ [pendingModifier, writeInput],
445+ );
446+
447+ const handleResize = useCallback(
448+ (size: { readonly cols: number; readonly rows: number }) => {
449+ if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) {
450+ return;
451+ }
452+
453+ setLastGridSize(size);
454+ if (!selectedThread || !isRunning) {
455+ return;
456+ }
457+
458+ const client = getEnvironmentClient(selectedThread.environmentId);
459+ if (!client) {
460+ return;
461+ }
462+
463+ void client.terminal.resize({
464+ threadId: selectedThread.id,
465+ terminalId,
466+ cols: size.cols,
467+ rows: size.rows,
468+ });
469+ },
470+ [isRunning, lastGridSize.cols, lastGridSize.rows, selectedThread, terminalId],
471+ );
472+
473+ const handleSelectTerminal = useCallback(
474+ (nextTerminalId: string) => {
475+ if (!selectedThread || nextTerminalId === terminalId) {
476+ return;
477+ }
478+
479+ router.replace(buildThreadTerminalRoutePath(selectedThread, nextTerminalId));
480+ },
481+ [router, selectedThread, terminalId],
482+ );
483+
484+ const handleOpenNewTerminal = useCallback(() => {
485+ if (!selectedThread) {
486+ return;
487+ }
488+
489+ router.replace(
490+ buildThreadTerminalRoutePath(
491+ selectedThread,
492+ nextTerminalId(terminalMenuSessions.map((session) => session.terminalId)),
493+ ),
494+ );
495+ }, [router, selectedThread, terminalMenuSessions]);
496+
497+ const adjustFontSize = useCallback((delta: number) => {
498+ setTimeout(() => {
499+ setFontSize((current) => normalizeTerminalFontSize(current + delta));
500+ }, 0);
501+ }, []);
502+
503+ const handleDecreaseFontSize = useCallback(() => {
504+ adjustFontSize(-TERMINAL_FONT_SIZE_STEP);
505+ }, [adjustFontSize]);
506+
507+ const handleIncreaseFontSize = useCallback(() => {
508+ adjustFontSize(TERMINAL_FONT_SIZE_STEP);
509+ }, [adjustFontSize]);
510+
511+ const handleToolbarActionPress = useCallback(
512+ (action: TerminalToolbarAction) => {
513+ if (action.kind === "modifier") {
514+ setPendingModifier((current) => (current === action.modifier ? null : action.modifier));
515+ return;
516+ }
517+
518+ setPendingModifier(null);
519+ if (pendingModifier === "ctrl") {
520+ writeInput(applyCtrlModifier(action.data));
521+ } else if (pendingModifier === "meta") {
522+ writeInput(`\u001b${action.data}`);
523+ } else {
524+ writeInput(action.data);
525+ }
526+ },
527+ [pendingModifier, writeInput],
528+ );
529+
530+ if (!selectedThread) {
531+ if (isLoadingSavedConnection) {
532+ return <LoadingScreen message="Opening terminal…" />;
533+ }
534+
535+ return (
536+ <View className="flex-1 bg-screen">
537+ <EmptyState
538+ title="Thread unavailable"
539+ detail="This terminal route needs an active thread and workspace."
540+ />
541+ </View>
542+ );
543+ }
544+
545+ if (!selectedThreadProject?.workspaceRoot) {
546+ return (
547+ <View className="flex-1 bg-screen">
548+ <EmptyState
549+ title="Terminal unavailable"
550+ detail="This thread does not have a workspace root yet, so there is nowhere to open a shell."
551+ />
552+ </View>
553+ );
554+ }
555+
556+ return (
557+ <>
558+ <Stack.Screen
559+ options={{
560+ headerShown: true,
561+ headerBackButtonDisplayMode: "minimal",
562+ headerBackTitle: "",
563+ headerShadowVisible: false,
564+ headerStyle: { backgroundColor: "#0a0a0a" },
565+ headerTintColor: "#f5f5f5",
566+ headerTitleAlign: "center",
567+ title: "",
568+ headerTitle: () => (
569+ <View
570+ style={{
571+ alignItems: "center",
572+ gap: 1,
573+ maxWidth: 240,
574+ }}
575+ >
576+ <RNText
577+ numberOfLines={1}
578+ style={{
579+ color: "#f5f5f5",
580+ fontFamily: "DMSans_700Bold",
581+ fontSize: 13,
582+ lineHeight: 16,
583+ }}
584+ >
585+ {headerTitle.topLine}
586+ </RNText>
587+ <RNText
588+ ellipsizeMode="middle"
589+ numberOfLines={1}
590+ style={{
591+ color: "#8f8f94",
592+ fontFamily: "Menlo",
593+ fontSize: 11,
594+ lineHeight: 14,
595+ }}
596+ >
597+ {headerTitle.bottomLine}
598+ </RNText>
599+ </View>
600+ ),
601+ }}
602+ />
603+
604+ <Stack.Toolbar placement="right">
605+ <Stack.Toolbar.Menu
606+ icon="circle.fill"
607+ style={{
608+ color: terminalStatusTone.textColor,
609+ fontFamily: "DMSans_700Bold",
610+ fontSize: 12,
611+ fontWeight: "700",
612+ }}
613+ tintColor={terminalStatusTone.tintColor}
614+ title="Terminal options"
615+ >
616+ <Stack.Toolbar.Label>
617+ {getTerminalStatusLabel({
618+ status: terminal.status,
619+ hasRunningSubprocess: terminal.hasRunningSubprocess,
620+ })}
621+ </Stack.Toolbar.Label>
622+ <Stack.Toolbar.Menu icon="textformat.size" inline title="Text size">
623+ <Stack.Toolbar.Label>Text size</Stack.Toolbar.Label>
624+ <Stack.Toolbar.MenuAction
625+ disabled={fontSize <= MIN_TERMINAL_FONT_SIZE}
626+ discoverabilityLabel="Decrease terminal text size"
627+ onPress={handleDecreaseFontSize}
628+ >
629+ <Stack.Toolbar.Label>{`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`}</Stack.Toolbar.Label>
630+ </Stack.Toolbar.MenuAction>
631+ <Stack.Toolbar.MenuAction
632+ disabled={fontSize >= MAX_TERMINAL_FONT_SIZE}
633+ discoverabilityLabel="Increase terminal text size"
634+ onPress={handleIncreaseFontSize}
635+ >
636+ <Stack.Toolbar.Label>{`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`}</Stack.Toolbar.Label>
637+ </Stack.Toolbar.MenuAction>
638+ </Stack.Toolbar.Menu>
639+ {terminalMenuSessions.map((session) => (
640+ <Stack.Toolbar.MenuAction
641+ key={session.terminalId}
642+ icon={session.terminalId === terminalId ? "checkmark" : "terminal"}
643+ onPress={() => handleSelectTerminal(session.terminalId)}
644+ subtitle={[getTerminalStatusLabel({ status: session.status }), basename(session.cwd)]
645+ .filter(Boolean)
646+ .join(" · ")}
647+ >
648+ <Stack.Toolbar.Label>{getTerminalLabel(session.terminalId)}</Stack.Toolbar.Label>
649+ </Stack.Toolbar.MenuAction>
650+ ))}
651+ <Stack.Toolbar.MenuAction
652+ icon="plus"
653+ onPress={handleOpenNewTerminal}
654+ subtitle={`Start another shell in ${basename(selectedThreadProject.workspaceRoot) ?? "this workspace"}`}
655+ >
656+ <Stack.Toolbar.Label>Open new terminal</Stack.Toolbar.Label>
657+ </Stack.Toolbar.MenuAction>
658+ </Stack.Toolbar.Menu>
659+ </Stack.Toolbar>
660+
661+ <View style={{ flex: 1, backgroundColor: "#050505" }}>
662+ <Animated.View style={[{ flex: 1 }, terminalContainerAnimatedStyle]}>
663+ <TerminalSurface
664+ buffer={terminal.buffer}
665+ fontSize={fontSize}
666+ isRunning={isRunning}
667+ onInput={handleInput}
668+ onResize={handleResize}
669+ style={{ flex: 1 }}
670+ terminalKey={terminalKey}
671+ />
672+ </Animated.View>
673+
674+ <KeyboardStickyView style={{ position: "absolute", bottom: 0, left: 0, right: 0 }}>
675+ <View
676+ style={{
677+ backgroundColor: "#0a0a0a",
678+ borderTopColor: "rgba(255,255,255,0.08)",
679+ borderTopWidth: 1,
680+ minHeight: TERMINAL_ACCESSORY_HEIGHT,
681+ paddingBottom: 4,
682+ paddingHorizontal: 8,
683+ paddingTop: 4,
684+ }}
685+ >
686+ <ScrollView
687+ horizontal
688+ contentContainerStyle={{ alignItems: "center", gap: 6, paddingRight: 2 }}
689+ showsHorizontalScrollIndicator={false}
690+ >
691+ {terminalToolbarActions.map((action) => {
692+ const active = action.kind === "modifier" && pendingModifier === action.modifier;
693+
694+ return (
695+ <Pressable
696+ key={action.key}
697+ onPress={() => handleToolbarActionPress(action)}
698+ style={({ pressed }) => ({
699+ alignItems: "center",
700+ backgroundColor: active
701+ ? "rgba(74, 222, 128, 0.18)"
702+ : pressed
703+ ? "rgba(255,255,255,0.12)"
704+ : "rgba(255,255,255,0.06)",
705+ borderColor: active ? "rgba(74, 222, 128, 0.36)" : "rgba(255,255,255,0.08)",
706+ borderRadius: 12,
707+ borderWidth: 1,
708+ justifyContent: "center",
709+ minWidth: action.label.length > 1 ? 46 : 38,
710+ paddingHorizontal: 11,
711+ paddingVertical: 8,
712+ })}
713+ >
714+ <RNText
715+ style={{
716+ color: active ? "#bbf7d0" : "#f5f5f5",
717+ fontFamily: "DMSans_700Bold",
718+ fontSize: 12,
719+ fontWeight: "700",
720+ textTransform: action.kind === "modifier" ? "uppercase" : "none",
721+ }}
722+ >
723+ {action.label}
724+ </RNText>
725+ </Pressable>
726+ );
727+ })}
728+ </ScrollView>
729+ </View>
730+ </KeyboardStickyView>
731+ </View>
732+ </>
733+ );
734+}
apps/mobile/src/features/threads/git/GitBranchesSheet.tsx+5−3
@@ -10,12 +10,14 @@ import { useGitStatus } from "../../../state/use-git-status"; · 9 unmodified lines
1010 import { useThreadSelection } from "../../../state/use-thread-selection";
1111 import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";
1212 import { useSelectedThreadGitState } from "../use-selected-thread-git-state";
13+import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
1314 import { SheetActionButton } from "./gitSheetComponents";
1415
1516 export function GitBranchesSheet() {
1617 const router = useRouter();
1718 const insets = useSafeAreaInsets();
18− const { selectedThread, selectedThreadProject } = useThreadSelection();
19+ const { selectedThread } = useThreadSelection();
20+ const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
1921 const gitState = useSelectedThreadGitState();
2022 const gitActions = useSelectedThreadGitActions();
2123
@@ -27,11 +29,11 @@ export function GitBranchesSheet() { · 5 unmodified lines
2729
2830 const gitStatus = useGitStatus({
2931 environmentId: selectedThread?.environmentId ?? "",
30− cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
32+ cwd: selectedThreadCwd,
3133 });
3234
3335 const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD";
34− const currentWorktreePath = selectedThread?.worktreePath ?? null;
36+ const currentWorktreePath = selectedThreadWorktreePath;
3537 const availableBranches = gitState.selectedThreadBranches;
3638 const branchesLoading = gitState.selectedThreadBranchesLoading;
3739 const busy = gitState.gitOperationLabel !== null;
apps/mobile/src/features/threads/git/GitCommitSheet.tsx+4−2
@@ -9,13 +9,15 @@ import { useGitStatus } from "../../../state/use-git-status"; · 8 unmodified lines
99 import { useThreadSelection } from "../../../state/use-thread-selection";
1010 import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";
1111 import { useSelectedThreadGitState } from "../use-selected-thread-git-state";
12+import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
1213 import { SheetActionButton } from "./gitSheetComponents";
1314
1415 export function GitCommitSheet() {
1516 const router = useRouter();
1617 const insets = useSafeAreaInsets();
1718 const isDarkMode = useColorScheme() === "dark";
18− const { selectedThread, selectedThreadProject } = useThreadSelection();
19+ const { selectedThread } = useThreadSelection();
20+ const { selectedThreadCwd } = useSelectedThreadWorktree();
1921 const gitState = useSelectedThreadGitState();
2022 const gitActions = useSelectedThreadGitActions();
2123
@@ -27,7 +29,7 @@ export function GitCommitSheet() { · 5 unmodified lines
2729
2830 const gitStatus = useGitStatus({
2931 environmentId: selectedThread?.environmentId ?? "",
30− cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
32+ cwd: selectedThreadCwd,
3133 });
3234
3335 const busy = gitState.gitOperationLabel !== null;
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+5−3
@@ -17,6 +17,7 @@ import { useGitStatus } from "../../../state/use-git-status"; · 16 unmodified lines
1717 import { useThreadSelection } from "../../../state/use-thread-selection";
1818 import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";
1919 import { useSelectedThreadGitState } from "../use-selected-thread-git-state";
20+import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
2021 import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents";
2122
2223 export function GitOverviewSheet() {
@@ -26,7 +27,8 @@ export function GitOverviewSheet() { · 3 unmodified lines
2627 environmentId: string;
2728 threadId: string;
2829 }>();
29− const { selectedThread, selectedThreadProject } = useThreadSelection();
30+ const { selectedThread } = useThreadSelection();
31+ const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
3032 const gitState = useSelectedThreadGitState();
3133 const gitActions = useSelectedThreadGitActions();
3234
@@ -35,11 +37,11 @@ export function GitOverviewSheet() { · 2 unmodified lines
3537
3638 const gitStatus = useGitStatus({
3739 environmentId: selectedThread?.environmentId ?? "",
38− cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
40+ cwd: selectedThreadCwd,
3941 });
4042
4143 const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD";
42− const currentWorktreePath = selectedThread?.worktreePath ?? null;
44+ const currentWorktreePath = selectedThreadWorktreePath;
4345 const gitOperationLabel = gitState.gitOperationLabel;
4446 const busy = gitOperationLabel !== null;
4547 const isRepo = gitStatus.data?.isRepo ?? true;
apps/mobile/src/features/threads/review/ReviewSheet.tsx+3−3
@@ -23,8 +23,8 @@ import { cn } from "../../../lib/cn"; · 22 unmodified lines
2323 import { useThemeColor } from "../../../lib/useThemeColor";
2424 import { getEnvironmentClient } from "../../../state/use-remote-environment-registry";
2525 import { useSelectedThreadDetail } from "../../../state/use-thread-detail";
26−import { useThreadSelection } from "../../../state/use-thread-selection";
2726 import { useThreadDraftForThread } from "../use-thread-composer-state";
27+import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
2828 import {
2929 getCachedReviewParsedDiff,
3030 setReviewGitSections,
@@ -445,7 +445,6 @@ export function ReviewSheet() { · 414 unmodified lines
445445 }>();
446446 const { draftMessage } = useThreadDraftForThread({ environmentId, threadId });
447447 const reviewCache = useReviewCacheForThread({ environmentId, threadId });
448− const { selectedThreadProject } = useThreadSelection();
449448 const selectedThread = useSelectedThreadDetail();
450449 const [loadingTurnIds, setLoadingTurnIds] = useState<Record<string, boolean>>({});
451450 const [loadingGitDiffs, setLoadingGitDiffs] = useState(false);
@@ -464,8 +463,9 @@ export function ReviewSheet() { · 12 unmodified lines
464463 const highlightQueueActiveRef = useRef(false);
465464 const highlightableFilesByIdRef = useRef<ReadonlyMap<string, ReviewRenderableFile>>(new Map());
466465 const selectedThemeRef = useRef<ReviewDiffTheme>(selectedTheme);
466+ const { selectedThreadCwd } = useSelectedThreadWorktree();
467467
468− const cwd = selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null;
468+ const cwd = selectedThreadCwd;
469469 const readyCheckpoints = useMemo(
470470 () => getReadyReviewCheckpoints(selectedThread?.checkpoints ?? []),
471471 [selectedThread?.checkpoints],
apps/mobile/src/features/threads/terminalLaunchContext.test.ts+61−0
@@ -0,0 +1,61 @@
1+import { describe, expect, it } from "vitest";
2+
3+import {
4+ resolvePreferredThreadWorktreePath,
5+ resolveTerminalOpenLocation,
6+} from "./terminalLaunchContext";
7+
8+describe("resolvePreferredThreadWorktreePath", () => {
9+ it("prefers thread detail worktree paths over thread shell paths", () => {
10+ expect(
11+ resolvePreferredThreadWorktreePath({
12+ threadShellWorktreePath: "/repo/root",
13+ threadDetailWorktreePath: "/repo/worktrees/feature",
14+ }),
15+ ).toBe("/repo/worktrees/feature");
16+ });
17+
18+ it("falls back to the thread shell worktree path when detail is unavailable", () => {
19+ expect(
20+ resolvePreferredThreadWorktreePath({
21+ threadShellWorktreePath: "/repo/worktrees/feature",
22+ threadDetailWorktreePath: null,
23+ }),
24+ ).toBe("/repo/worktrees/feature");
25+ });
26+});
27+
28+describe("resolveTerminalOpenLocation", () => {
29+ it("uses the thread detail worktree path before the workspace root for a fresh mobile open", () => {
30+ expect(
31+ resolveTerminalOpenLocation({
32+ terminalSnapshot: null,
33+ activeSessionSnapshot: null,
34+ workspaceRoot: "/repo/root",
35+ threadShellWorktreePath: null,
36+ threadDetailWorktreePath: "/repo/worktrees/feature",
37+ }),
38+ ).toEqual({
39+ cwd: "/repo/worktrees/feature",
40+ worktreePath: "/repo/worktrees/feature",
41+ });
42+ });
43+
44+ it("preserves the running terminal snapshot cwd when attaching to an existing session", () => {
45+ expect(
46+ resolveTerminalOpenLocation({
47+ terminalSnapshot: null,
48+ activeSessionSnapshot: {
49+ cwd: "/repo/worktrees/feature",
50+ worktreePath: "/repo/worktrees/feature",
51+ },
52+ workspaceRoot: "/repo/root",
53+ threadShellWorktreePath: null,
54+ threadDetailWorktreePath: "/repo/worktrees/other",
55+ }),
56+ ).toEqual({
57+ cwd: "/repo/worktrees/feature",
58+ worktreePath: "/repo/worktrees/feature",
59+ });
60+ });
61+});
apps/mobile/src/features/threads/terminalLaunchContext.ts+39−0
@@ -0,0 +1,39 @@
1+interface TerminalSnapshotLike {
2+ readonly cwd: string;
3+ readonly worktreePath: string | null;
4+}
5+
6+export function resolvePreferredThreadWorktreePath(input: {
7+ readonly threadShellWorktreePath: string | null;
8+ readonly threadDetailWorktreePath: string | null;
9+}): string | null {
10+ return input.threadDetailWorktreePath ?? input.threadShellWorktreePath ?? null;
11+}
12+
13+export function resolveTerminalOpenLocation(input: {
14+ readonly terminalSnapshot: TerminalSnapshotLike | null;
15+ readonly activeSessionSnapshot: TerminalSnapshotLike | null;
16+ readonly workspaceRoot: string;
17+ readonly threadShellWorktreePath: string | null;
18+ readonly threadDetailWorktreePath: string | null;
19+}): {
20+ readonly cwd: string;
21+ readonly worktreePath: string | null;
22+} {
23+ const preferredThreadWorktreePath = resolvePreferredThreadWorktreePath({
24+ threadShellWorktreePath: input.threadShellWorktreePath,
25+ threadDetailWorktreePath: input.threadDetailWorktreePath,
26+ });
27+
28+ return {
29+ cwd:
30+ input.terminalSnapshot?.cwd ??
31+ input.activeSessionSnapshot?.cwd ??
32+ preferredThreadWorktreePath ??
33+ input.workspaceRoot,
34+ worktreePath:
35+ input.terminalSnapshot?.worktreePath ??
36+ input.activeSessionSnapshot?.worktreePath ??
37+ preferredThreadWorktreePath,
38+ };
39+}
apps/mobile/src/features/threads/terminalMenu.test.ts+128−0
@@ -0,0 +1,128 @@
1+import { describe, expect, it } from "vitest";
2+
3+import type { KnownTerminalSession } from "@t3tools/client-runtime";
4+import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts";
5+
6+import { buildTerminalMenuSessions, resolveProjectScriptTerminalId } from "./terminalMenu";
7+
8+function makeKnownSession(input: {
9+ readonly terminalId: string;
10+ readonly status: KnownTerminalSession["state"]["status"];
11+ readonly cwd?: string | null;
12+ readonly updatedAt?: string | null;
13+}): KnownTerminalSession {
14+ return {
15+ target: {
16+ environmentId: "env-1",
17+ threadId: "thread-1",
18+ terminalId: input.terminalId,
19+ },
20+ state: {
21+ snapshot: input.cwd
22+ ? {
23+ threadId: "thread-1",
24+ terminalId: input.terminalId,
25+ cwd: input.cwd,
26+ worktreePath: input.cwd,
27+ status: input.status === "closed" ? "error" : input.status,
28+ pid: input.status === "running" ? 123 : null,
29+ history: "",
30+ exitCode: null,
31+ exitSignal: null,
32+ updatedAt: input.updatedAt ?? "2026-04-15T20:00:00.000Z",
33+ }
34+ : null,
35+ buffer: "",
36+ status: input.status,
37+ error: null,
38+ hasRunningSubprocess: false,
39+ updatedAt: input.updatedAt ?? "2026-04-15T20:00:00.000Z",
40+ version: 1,
41+ },
42+ };
43+}
44+
45+describe("buildTerminalMenuSessions", () => {
46+ it("keeps the default shell and only includes running sessions by default", () => {
47+ expect(
48+ buildTerminalMenuSessions({
49+ knownSessions: [
50+ makeKnownSession({
51+ terminalId: "term-3",
52+ status: "running",
53+ cwd: "/workspace/feature",
54+ updatedAt: "2026-04-15T20:05:00.000Z",
55+ }),
56+ makeKnownSession({
57+ terminalId: "term-2",
58+ status: "exited",
59+ cwd: "/workspace/exited",
60+ updatedAt: "2026-04-15T20:06:00.000Z",
61+ }),
62+ ],
63+ workspaceRoot: "/workspace/root",
64+ }),
65+ ).toEqual([
66+ {
67+ terminalId: DEFAULT_TERMINAL_ID,
68+ cwd: "/workspace/root",
69+ status: "closed",
70+ updatedAt: null,
71+ },
72+ {
73+ terminalId: "term-3",
74+ cwd: "/workspace/feature",
75+ status: "running",
76+ updatedAt: "2026-04-15T20:05:00.000Z",
77+ },
78+ ]);
79+ });
80+
81+ it("keeps the current terminal visible even if it is no longer running", () => {
82+ expect(
83+ buildTerminalMenuSessions({
84+ knownSessions: [],
85+ workspaceRoot: "/workspace/root",
86+ currentSession: {
87+ terminalId: "term-4",
88+ cwd: "/workspace/exited",
89+ status: "exited",
90+ updatedAt: "2026-04-15T20:07:00.000Z",
91+ },
92+ }),
93+ ).toEqual([
94+ {
95+ terminalId: DEFAULT_TERMINAL_ID,
96+ cwd: "/workspace/root",
97+ status: "closed",
98+ updatedAt: null,
99+ },
100+ {
101+ terminalId: "term-4",
102+ cwd: "/workspace/exited",
103+ status: "exited",
104+ updatedAt: "2026-04-15T20:07:00.000Z",
105+ },
106+ ]);
107+ });
108+});
109+
110+describe("resolveProjectScriptTerminalId", () => {
111+ it("reuses the default shell when no terminal is running", () => {
112+ expect(
113+ resolveProjectScriptTerminalId({
114+ existingTerminalIds: [DEFAULT_TERMINAL_ID],
115+ hasRunningTerminal: false,
116+ }),
117+ ).toBe(DEFAULT_TERMINAL_ID);
118+ });
119+
120+ it("opens a new terminal when a shell is already running", () => {
121+ expect(
122+ resolveProjectScriptTerminalId({
123+ existingTerminalIds: [DEFAULT_TERMINAL_ID, "term-2", "term-4"],
124+ hasRunningTerminal: true,
125+ }),
126+ ).toBe("term-3");
127+ });
128+});
apps/mobile/src/features/threads/terminalMenu.ts+139−0
@@ -0,0 +1,139 @@
1+import type { KnownTerminalSession } from "@t3tools/client-runtime";
2+import { DEFAULT_TERMINAL_ID, type ProjectScript } from "@t3tools/contracts";
3+
4+export interface TerminalMenuSession {
5+ readonly terminalId: string;
6+ readonly cwd: string | null;
7+ readonly status: "starting" | "running" | "exited" | "error" | "closed";
8+ readonly updatedAt: string | null;
9+}
10+
11+export function basename(input: string | null): string | null {
12+ if (!input) {
13+ return null;
14+ }
15+
16+ const normalized = input.replace(/\/+$/, "");
17+ if (normalized.length === 0) {
18+ return "/";
19+ }
20+
21+ const segments = normalized.split("/");
22+ return segments[segments.length - 1] ?? normalized;
23+}
24+
25+export function getTerminalLabel(terminalId: string): string {
26+ if (terminalId === DEFAULT_TERMINAL_ID) {
27+ return "Main shell";
28+ }
29+
30+ const numericSuffix = /^term(?:inal)?-(\d+)$/i.exec(terminalId)?.[1];
31+ if (numericSuffix) {
32+ return `Terminal ${numericSuffix}`;
33+ }
34+
35+ return terminalId;
36+}
37+
38+export function getTerminalStatusLabel(input: {
39+ readonly status: TerminalMenuSession["status"];
40+ readonly hasRunningSubprocess?: boolean;
41+}): string {
42+ if (input.status === "running") {
43+ return input.hasRunningSubprocess ? "Task running" : "Ready";
44+ }
45+ if (input.status === "starting") {
46+ return "Starting";
47+ }
48+ if (input.status === "exited") {
49+ return "Exited";
50+ }
51+ if (input.status === "error") {
52+ return "Error";
53+ }
54+
55+ return "Not started";
56+}
57+
58+export function nextTerminalId(existingTerminalIds: ReadonlyArray<string>): string {
59+ const usedIds = new Set(existingTerminalIds);
60+ let nextIndex = 2;
61+ while (usedIds.has(`term-${nextIndex}`)) {
62+ nextIndex += 1;
63+ }
64+
65+ return `term-${nextIndex}`;
66+}
67+
68+export function buildTerminalMenuSessions(input: {
69+ readonly knownSessions: ReadonlyArray<KnownTerminalSession>;
70+ readonly workspaceRoot: string | null;
71+ readonly currentSession?: TerminalMenuSession | null;
72+}): ReadonlyArray<TerminalMenuSession> {
73+ const sessionsById = new Map<string, TerminalMenuSession>();
74+
75+ sessionsById.set(DEFAULT_TERMINAL_ID, {
76+ terminalId: DEFAULT_TERMINAL_ID,
77+ cwd: input.workspaceRoot,
78+ status: "closed",
79+ updatedAt: null,
80+ });
81+
82+ for (const session of input.knownSessions) {
83+ if (
84+ session.state.status !== "running" &&
85+ session.state.status !== "starting" &&
86+ session.target.terminalId !== input.currentSession?.terminalId
87+ ) {
88+ continue;
89+ }
90+
91+ sessionsById.set(session.target.terminalId, {
92+ terminalId: session.target.terminalId,
93+ cwd: session.state.snapshot?.cwd ?? input.workspaceRoot,
94+ status: session.state.status,
95+ updatedAt: session.state.updatedAt,
96+ });
97+ }
98+
99+ if (input.currentSession && !sessionsById.has(input.currentSession.terminalId)) {
100+ sessionsById.set(input.currentSession.terminalId, input.currentSession);
101+ }
102+
103+ return Array.from(sessionsById.values()).sort((left, right) => {
104+ if (left.terminalId === DEFAULT_TERMINAL_ID) return -1;
105+ if (right.terminalId === DEFAULT_TERMINAL_ID) return 1;
106+
107+ const leftUpdatedAt = left.updatedAt ? Date.parse(left.updatedAt) : 0;
108+ const rightUpdatedAt = right.updatedAt ? Date.parse(right.updatedAt) : 0;
109+ if (leftUpdatedAt !== rightUpdatedAt) {
110+ return rightUpdatedAt - leftUpdatedAt;
111+ }
112+
113+ return left.terminalId.localeCompare(right.terminalId);
114+ });
115+}
116+
117+export function resolveProjectScriptTerminalId(input: {
118+ readonly existingTerminalIds: ReadonlyArray<string>;
119+ readonly hasRunningTerminal: boolean;
120+}): string {
121+ if (!input.hasRunningTerminal) {
122+ return DEFAULT_TERMINAL_ID;
123+ }
124+
125+ return nextTerminalId(input.existingTerminalIds);
126+}
127+
128+export function projectScriptMenuLabel(script: ProjectScript): string {
129+ return script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name;
130+}
131+
132+export function projectScriptMenuIcon(icon: ProjectScript["icon"]) {
133+ if (icon === "test") return "flask";
134+ if (icon === "lint") return "checklist";
135+ if (icon === "configure") return "wrench.and.screwdriver";
136+ if (icon === "build") return "hammer";
137+ if (icon === "debug") return "ladybug";
138+ return "play";
139+}
apps/mobile/src/features/threads/terminalPreferences.test.ts+28−0
@@ -0,0 +1,28 @@
1+import { describe, expect, it } from "vitest";
2+
3+import {
4+ DEFAULT_TERMINAL_FONT_SIZE,
5+ MAX_TERMINAL_FONT_SIZE,
6+ MIN_TERMINAL_FONT_SIZE,
7+ normalizeTerminalFontSize,
8+} from "./terminalPreferences";
9+
10+describe("normalizeTerminalFontSize", () => {
11+ it("returns the default size for missing or invalid values", () => {
12+ expect(normalizeTerminalFontSize(undefined)).toBe(DEFAULT_TERMINAL_FONT_SIZE);
13+ expect(normalizeTerminalFontSize(null)).toBe(DEFAULT_TERMINAL_FONT_SIZE);
14+ expect(normalizeTerminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE);
15+ });
16+
17+ it("clamps below the minimum", () => {
18+ expect(normalizeTerminalFontSize(MIN_TERMINAL_FONT_SIZE - 4)).toBe(MIN_TERMINAL_FONT_SIZE);
19+ });
20+
21+ it("clamps above the maximum", () => {
22+ expect(normalizeTerminalFontSize(MAX_TERMINAL_FONT_SIZE + 4)).toBe(MAX_TERMINAL_FONT_SIZE);
23+ });
24+
25+ it("preserves in-range values", () => {
26+ expect(normalizeTerminalFontSize(9.5)).toBe(9.5);
27+ });
28+});
apps/mobile/src/features/threads/terminalPreferences.ts+12−0
@@ -0,0 +1,12 @@
1+export const DEFAULT_TERMINAL_FONT_SIZE = 10;
2+export const TERMINAL_FONT_SIZE_STEP = 0.5;
3+export const MIN_TERMINAL_FONT_SIZE = 6;
4+export const MAX_TERMINAL_FONT_SIZE = 14;
5+
6+export function normalizeTerminalFontSize(value: number | null | undefined): number {
7+ if (typeof value !== "number" || !Number.isFinite(value)) {
8+ return DEFAULT_TERMINAL_FONT_SIZE;
9+ }
10+
11+ return Math.min(MAX_TERMINAL_FONT_SIZE, Math.max(MIN_TERMINAL_FONT_SIZE, value));
12+}
apps/mobile/src/features/threads/terminalRouteBootstrap.ts+25−0
@@ -0,0 +1,25 @@
1+export function resolveTerminalRouteBootstrap(input: {
2+ readonly hasThread: boolean;
3+ readonly hasWorkspaceRoot: boolean;
4+ readonly hasOpened: boolean;
5+ readonly requestedTerminalId: string | null;
6+ readonly currentTerminalId: string;
7+ readonly runningTerminalId: string | null;
8+}):
9+ | { readonly kind: "idle" }
10+ | { readonly kind: "redirect"; readonly terminalId: string }
11+ | { readonly kind: "open" } {
12+ if (!input.hasThread || !input.hasWorkspaceRoot || input.hasOpened) {
13+ return { kind: "idle" };
14+ }
15+
16+ if (
17+ input.requestedTerminalId === null &&
18+ input.runningTerminalId !== null &&
19+ input.runningTerminalId !== input.currentTerminalId
20+ ) {
21+ return { kind: "redirect", terminalId: input.runningTerminalId };
22+ }
23+
24+ return { kind: "open" };
25+}
apps/mobile/src/features/threads/use-selected-thread-git-actions.ts+17−11
@@ -25,9 +25,11 @@ import { gitActionManager, showGitActionResult } from "../../state/use-git-actio · 24 unmodified lines
2525 import { gitBranchManager } from "../../state/use-git-branches";
2626 import { gitStatusManager } from "../../state/use-git-status";
2727 import { useThreadSelection } from "../../state/use-thread-selection";
28+import { useSelectedThreadWorktree } from "./use-selected-thread-worktree";
2829
2930 export function useSelectedThreadGitActions() {
3031 const { selectedThread, selectedThreadProject } = useThreadSelection();
32+ const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
3133
3234 const selectedThreadGitRootCwd = selectedThreadProject?.workspaceRoot ?? null;
3335
@@ -61,8 +63,7 @@ export function useSelectedThreadGitActions() { · 27 unmodified lines
6163 return null;
6264 }
6365
64− const cwd =
65− options?.cwd ?? selectedThread.worktreePath ?? selectedThreadProject.workspaceRoot;
66+ const cwd = options?.cwd ?? selectedThreadCwd;
6667 if (!cwd) {
6768 return null;
6869 }
@@ -86,7 +87,7 @@ export function useSelectedThreadGitActions() { · 17 unmodified lines
8687 return null;
8788 }
8889 },
89− [selectedThread, selectedThreadProject],
90+ [selectedThread, selectedThreadCwd, selectedThreadProject],
9091 );
9192
9293 useEffect(() => {
@@ -109,7 +110,7 @@ export function useSelectedThreadGitActions() { · 16 unmodified lines
109110 return null;
110111 }
111112
112− const cwd = selectedThread.worktreePath ?? selectedThreadProject.workspaceRoot;
113+ const cwd = selectedThreadCwd;
113114 if (!cwd) {
114115 return null;
115116 }
@@ -128,7 +129,7 @@ export function useSelectedThreadGitActions() { · 12 unmodified lines
128129 return null;
129130 }
130131 },
131− [selectedThread, selectedThreadProject],
132+ [selectedThread, selectedThreadCwd, selectedThreadProject],
132133 );
133134
134135 const refreshSelectedThreadBranches = useCallback(async (): Promise<ReadonlyArray<GitBranch>> => {
@@ -204,12 +205,12 @@ export function useSelectedThreadGitActions() { · 69 unmodified lines
204205 cwd,
205206 nextThreadState: {
206207 branch: result?.branch ?? thread.branch,
207− worktreePath: thread.worktreePath,
208+ worktreePath: selectedThreadWorktreePath,
208209 },
209210 });
210211 });
211212 },
212− [runSelectedThreadGitMutation, syncSelectedThreadBranchState],
213+ [runSelectedThreadGitMutation, selectedThreadWorktreePath, syncSelectedThreadBranchState],
213214 );
214215
215216 const onCreateSelectedThreadBranch = useCallback(
@@ -227,12 +228,12 @@ export function useSelectedThreadGitActions() { · 11 unmodified lines
227228 cwd,
228229 nextThreadState: {
229230 branch: result?.branch ?? thread.branch,
230− worktreePath: thread.worktreePath,
231+ worktreePath: selectedThreadWorktreePath,
231232 },
232233 });
233234 });
234235 },
235− [runSelectedThreadGitMutation, syncSelectedThreadBranchState],
236+ [runSelectedThreadGitMutation, selectedThreadWorktreePath, syncSelectedThreadBranchState],
236237 );
237238
238239 const onCreateSelectedThreadWorktree = useCallback(
@@ -316,7 +317,7 @@ export function useSelectedThreadGitActions() { · 77 unmodified lines
316317 cwd,
317318 nextThreadState: {
318319 branch: result.branch.name,
319− worktreePath: thread.worktreePath,
320+ worktreePath: selectedThreadWorktreePath,
320321 },
321322 });
322323 return result;
@@ -326,7 +327,12 @@ export function useSelectedThreadGitActions() { · 3 unmodified lines
326327 return result;
327328 });
328329 },
329− [refreshSelectedThreadGitStatus, runSelectedThreadGitMutation, syncSelectedThreadBranchState],
330+ [
331+ refreshSelectedThreadGitStatus,
332+ runSelectedThreadGitMutation,
333+ selectedThreadWorktreePath,
334+ syncSelectedThreadBranchState,
335+ ],
330336 );
331337
332338 return {
apps/mobile/src/features/threads/use-selected-thread-git-state.ts+4−6
@@ -5,20 +5,18 @@ import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git"; · 4 unmodified lines
55 import { useGitActionState } from "../../state/use-git-action-state";
66 import { useGitBranches } from "../../state/use-git-branches";
77 import { useThreadSelection } from "../../state/use-thread-selection";
8+import { useSelectedThreadWorktree } from "./use-selected-thread-worktree";
89
910 export function useSelectedThreadGitState() {
1011 const { selectedThread, selectedThreadProject } = useThreadSelection();
12+ const { selectedThreadCwd } = useSelectedThreadWorktree();
1113
1214 const selectedThreadGitTarget = useMemo(
1315 () => ({
1416 environmentId: selectedThread?.environmentId ?? null,
15− cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
17+ cwd: selectedThreadCwd,
1618 }),
17− [
18− selectedThread?.environmentId,
19− selectedThread?.worktreePath,
20− selectedThreadProject?.workspaceRoot,
21− ],
19+ [selectedThread?.environmentId, selectedThreadCwd],
2220 );
2321 const gitActionState = useGitActionState(selectedThreadGitTarget);
2422
apps/mobile/src/features/threads/use-selected-thread-worktree.ts+24−0
@@ -0,0 +1,24 @@
1+import { useMemo } from "react";
2+
3+import { useSelectedThreadDetail } from "../../state/use-thread-detail";
4+import { useThreadSelection } from "../../state/use-thread-selection";
5+import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext";
6+
7+export function useSelectedThreadWorktree() {
8+ const { selectedThread, selectedThreadProject } = useThreadSelection();
9+ const selectedThreadDetail = useSelectedThreadDetail();
10+
11+ const selectedThreadWorktreePath = useMemo(
12+ () =>
13+ resolvePreferredThreadWorktreePath({
14+ threadShellWorktreePath: selectedThread?.worktreePath ?? null,
15+ threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null,
16+ }),
17+ [selectedThread?.worktreePath, selectedThreadDetail?.worktreePath],
18+ );
19+
20+ return {
21+ selectedThreadWorktreePath,
22+ selectedThreadCwd: selectedThreadWorktreePath ?? selectedThreadProject?.workspaceRoot ?? null,
23+ };
24+}
apps/mobile/src/lib/routes.ts+12−0
@@ -29,6 +29,18 @@ export function buildThreadReviewRoutePath( · 28 unmodified lines
2929 return `${buildThreadRoutePath(input)}/review`;
3030 }
3131
32+export function buildThreadTerminalRoutePath(
33+ input: ThreadRouteInput | PlainThreadRouteInput,
34+ terminalId?: string | null,
35+): string {
36+ const basePath = `${buildThreadRoutePath(input)}/terminal`;
37+ if (!terminalId) {
38+ return basePath;
39+ }
40+
41+ return `${basePath}?terminalId=${encodeURIComponent(terminalId)}`;
42+}
43+
3244 export function dismissRoute(router: Router) {
3345 if (router.canGoBack()) {
3446 router.back();
apps/mobile/src/lib/storage.ts+48−10
@@ -5,6 +5,11 @@ import * as SecureStore from "expo-secure-store"; · 4 unmodified lines
55 import type { SavedRemoteConnection } from "./connection";
66
77 const CONNECTIONS_KEY = "t3code.connections";
8+const PREFERENCES_KEY = "t3code.preferences";
9+
10+export interface MobilePreferences {
11+ readonly terminalFontSize?: number;
12+}
813
914 async function readStorageItem(key: string): Promise<string | null> {
1015 return await SecureStore.getItemAsync(key);
@@ -14,23 +19,31 @@ async function writeStorageItem(key: string, value: string): Promise<void> { · 3 unmodified lines
1419 await SecureStore.setItemAsync(key, value);
1520 }
1621
17−export async function loadSavedConnections(): Promise<ReadonlyArray<SavedRemoteConnection>> {
18− const raw = (await readStorageItem(CONNECTIONS_KEY)) ?? "";
22+async function readJsonStorageItem<T>(key: string): Promise<T | null> {
23+ const raw = (await readStorageItem(key)) ?? "";
1924 if (!raw.trim()) {
20− return [];
25+ return null;
2126 }
2227
2328 try {
24− const parsed = JSON.parse(raw) as {
25− readonly connections?: ReadonlyArray<SavedRemoteConnection>;
26− };
27− return pipe(
28− parsed.connections ?? [],
29− Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()),
30− );
29+ return JSON.parse(raw) as T;
3130 } catch {
31+ return null;
32+ }
33+}
34+
35+export async function loadSavedConnections(): Promise<ReadonlyArray<SavedRemoteConnection>> {
36+ const parsed = await readJsonStorageItem<{
37+ readonly connections?: ReadonlyArray<SavedRemoteConnection>;
38+ }>(CONNECTIONS_KEY);
39+ if (!parsed) {
3240 return [];
3341 }
42+
43+ return pipe(
44+ parsed.connections ?? [],
45+ Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()),
46+ );
3447 }
3548
3649 export async function saveConnection(connection: SavedRemoteConnection): Promise<void> {
@@ -53,3 +66,28 @@ export async function clearSavedConnection(environmentId: string): Promise<void> · 16 unmodified lines
5366 );
5467 await writeStorageItem(CONNECTIONS_KEY, JSON.stringify({ connections: next }));
5568 }
69+
70+export async function loadPreferences(): Promise<MobilePreferences> {
71+ const parsed = await readJsonStorageItem<MobilePreferences>(PREFERENCES_KEY);
72+ if (!parsed || typeof parsed !== "object") {
73+ return {};
74+ }
75+
76+ if (typeof parsed.terminalFontSize === "number") {
77+ return { terminalFontSize: parsed.terminalFontSize };
78+ }
79+
80+ return {};
81+}
82+
83+export async function savePreferencesPatch(
84+ patch: Partial<MobilePreferences>,
85+): Promise<MobilePreferences> {
86+ const current = await loadPreferences();
87+ const next: MobilePreferences = {
88+ ...current,
89+ ...patch,
90+ };
91+ await writeStorageItem(PREFERENCES_KEY, JSON.stringify(next));
92+ return next;
93+}
apps/mobile/src/native/terminal/NativeTerminalSurface.tsx+206−0
@@ -0,0 +1,206 @@
1+import { memo, useCallback } from "react";
2+import {
3+ Pressable,
4+ ScrollView,
5+ TextInput,
6+ View,
7+ type LayoutChangeEvent,
8+ type NativeSyntheticEvent,
9+ type ViewProps,
10+ useColorScheme,
11+} from "react-native";
12+
13+import { AppText as Text } from "../../components/AppText";
14+import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule";
15+import {
16+ buildGhosttyThemeConfig,
17+ getGitHubTerminalTheme,
18+ type TerminalTheme,
19+} from "./terminalTheme";
20+
21+interface TerminalInputEvent {
22+ readonly data: string;
23+}
24+
25+interface TerminalResizeEvent {
26+ readonly cols: number;
27+ readonly rows: number;
28+}
29+
30+interface TerminalSurfaceProps extends ViewProps {
31+ readonly terminalKey: string;
32+ readonly buffer: string;
33+ readonly fontSize?: number;
34+ readonly isRunning: boolean;
35+ readonly theme?: TerminalTheme;
36+ readonly onInput: (data: string) => void;
37+ readonly onResize: (size: { readonly cols: number; readonly rows: number }) => void;
38+}
39+
40+function estimateGridSize(input: {
41+ readonly width: number;
42+ readonly height: number;
43+ readonly fontSize: number;
44+}): { readonly cols: number; readonly rows: number } {
45+ const cellWidth = input.fontSize * 0.62;
46+ const cellHeight = input.fontSize * 1.35;
47+ return {
48+ cols: Math.max(20, Math.min(400, Math.floor(input.width / cellWidth))),
49+ rows: Math.max(5, Math.min(200, Math.floor(input.height / cellHeight))),
50+ };
51+}
52+
53+const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) {
54+ const fontSize = props.fontSize ?? 12;
55+ const appearanceScheme = useColorScheme() === "light" ? "light" : "dark";
56+ const theme = props.theme ?? getGitHubTerminalTheme(appearanceScheme);
57+ const statusLabel = props.isRunning
58+ ? "Native terminal unavailable. Using text fallback."
59+ : "Open terminal to start a shell.";
60+
61+ const handleLayout = (event: LayoutChangeEvent) => {
62+ const { width, height } = event.nativeEvent.layout;
63+ props.onResize(estimateGridSize({ width, height, fontSize }));
64+ };
65+
66+ return (
67+ <View
68+ style={[
69+ {
70+ flex: 1,
71+ backgroundColor: theme.background,
72+ borderRadius: 8,
73+ overflow: "hidden",
74+ },
75+ props.style,
76+ ]}
77+ onLayout={handleLayout}
78+ >
79+ <View style={{ flex: 1, paddingHorizontal: 10, paddingVertical: 8 }}>
80+ <Text
81+ style={{
82+ color: theme.mutedForeground,
83+ fontSize: 11,
84+ paddingBottom: 8,
85+ }}
86+ >
87+ {statusLabel}
88+ </Text>
89+ <ScrollView
90+ style={{ flex: 1 }}
91+ contentContainerStyle={{ paddingBottom: 12 }}
92+ showsVerticalScrollIndicator={false}
93+ >
94+ <Text
95+ selectable
96+ style={{
97+ color: theme.foreground,
98+ fontFamily: "Menlo",
99+ fontSize,
100+ lineHeight: Math.round(fontSize * 1.35),
101+ }}
102+ >
103+ {props.buffer || "$ "}
104+ </Text>
105+ </ScrollView>
106+ </View>
107+ <View
108+ style={{
109+ borderTopWidth: 1,
110+ borderTopColor: theme.border,
111+ flexDirection: "row",
112+ alignItems: "center",
113+ gap: 8,
114+ padding: 8,
115+ }}
116+ >
117+ <TextInput
118+ autoCapitalize="none"
119+ autoCorrect={false}
120+ blurOnSubmit={false}
121+ editable={props.isRunning}
122+ placeholder="type and press return"
123+ placeholderTextColor={theme.mutedForeground}
124+ returnKeyType="send"
125+ style={{
126+ color: theme.foreground,
127+ flex: 1,
128+ fontFamily: "Menlo",
129+ fontSize: 13,
130+ padding: 0,
131+ }}
132+ onSubmitEditing={(event) => {
133+ const text = event.nativeEvent.text;
134+ if (text.length > 0) {
135+ props.onInput(`${text}\n`);
136+ }
137+ }}
138+ />
139+ <Pressable
140+ disabled={!props.isRunning}
141+ style={({ pressed }) => ({
142+ opacity: !props.isRunning ? 0.35 : pressed ? 0.65 : 1,
143+ paddingHorizontal: 10,
144+ paddingVertical: 6,
145+ borderRadius: 8,
146+ backgroundColor: theme.border,
147+ })}
148+ onPress={() => props.onInput("\u0003")}
149+ >
150+ <Text
151+ style={{
152+ color: theme.foreground,
153+ fontFamily: "DMSans_700Bold",
154+ fontSize: 11,
155+ }}
156+ >
157+ Ctrl-C
158+ </Text>
159+ </Pressable>
160+ </View>
161+ </View>
162+ );
163+});
164+
165+export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) {
166+ const fontSize = props.fontSize ?? 12;
167+ const appearanceScheme = useColorScheme() === "light" ? "light" : "dark";
168+ const theme = props.theme ?? getGitHubTerminalTheme(appearanceScheme);
169+ const { onInput, onResize } = props;
170+ const NativeTerminalSurfaceView = resolveNativeTerminalSurfaceView();
171+ const handleNativeInput = useCallback(
172+ (event: NativeSyntheticEvent<TerminalInputEvent>) => {
173+ onInput(event.nativeEvent.data);
174+ },
175+ [onInput],
176+ );
177+ const handleNativeResize = useCallback(
178+ (event: NativeSyntheticEvent<TerminalResizeEvent>) => {
179+ onResize({
180+ cols: event.nativeEvent.cols,
181+ rows: event.nativeEvent.rows,
182+ });
183+ },
184+ [onResize],
185+ );
186+
187+ if (NativeTerminalSurfaceView) {
188+ return (
189+ <NativeTerminalSurfaceView
190+ {...props}
191+ appearanceScheme={appearanceScheme}
192+ backgroundColor={theme.background}
193+ foregroundColor={theme.foreground}
194+ mutedForegroundColor={theme.mutedForeground}
195+ terminalKey={props.terminalKey}
196+ initialBuffer={props.buffer}
197+ fontSize={fontSize}
198+ themeConfig={buildGhosttyThemeConfig(theme)}
199+ onInput={handleNativeInput}
200+ onResize={handleNativeResize}
201+ />
202+ );
203+ }
204+
205+ return <FallbackTerminalSurface {...props} fontSize={fontSize} theme={theme} />;
206+});
apps/mobile/src/native/terminal/nativeTerminalModule.test.ts+52−0
@@ -0,0 +1,52 @@
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+const expoModulesCoreMocks = vi.hoisted(() => ({
4+ requireNativeViewManager: vi.fn(),
5+}));
6+const nativeView = () => null;
7+const originalExpo = globalThis.expo;
8+
9+function setExpoViewConfigAvailable() {
10+ globalThis.expo = {
11+ getViewConfig: vi.fn().mockReturnValue({ validAttributes: {}, directEventTypes: {} }),
12+ } as unknown as typeof globalThis.expo;
13+}
14+
15+vi.mock("expo-modules-core", () => ({
16+ requireNativeViewManager: expoModulesCoreMocks.requireNativeViewManager,
17+}));
18+
19+describe("resolveNativeTerminalSurfaceView", () => {
20+ beforeEach(() => {
21+ vi.clearAllMocks();
22+ vi.resetModules();
23+ globalThis.expo = undefined as unknown as typeof globalThis.expo;
24+ });
25+
26+ afterEach(() => {
27+ globalThis.expo = originalExpo;
28+ });
29+
30+ it("returns null when the native terminal view config is unavailable", async () => {
31+ const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule");
32+ expect(resolveNativeTerminalSurfaceView()).toBeNull();
33+ expect(expoModulesCoreMocks.requireNativeViewManager).not.toHaveBeenCalled();
34+ });
35+
36+ it("returns the native terminal view when the view config is installed", async () => {
37+ setExpoViewConfigAvailable();
38+ expoModulesCoreMocks.requireNativeViewManager.mockReturnValue(nativeView);
39+ const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule");
40+ expect(resolveNativeTerminalSurfaceView()).toBe(nativeView);
41+ expect(expoModulesCoreMocks.requireNativeViewManager).toHaveBeenCalledWith("T3TerminalSurface");
42+ });
43+
44+ it("returns null when the view manager cannot be required", async () => {
45+ setExpoViewConfigAvailable();
46+ expoModulesCoreMocks.requireNativeViewManager.mockImplementation(() => {
47+ throw new Error("boom");
48+ });
49+ const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule");
50+ expect(resolveNativeTerminalSurfaceView()).toBeNull();
51+ });
52+});
apps/mobile/src/native/terminal/nativeTerminalModule.ts+65−0
@@ -0,0 +1,65 @@
1+import type { ComponentType } from "react";
2+import type { NativeSyntheticEvent, ViewProps } from "react-native";
3+import { requireNativeViewManager } from "expo-modules-core";
4+
5+const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface";
6+
7+interface ExpoGlobalWithViewConfig {
8+ readonly expo?: {
9+ getViewConfig?: (moduleName: string, viewName?: string) => unknown;
10+ };
11+}
12+
13+interface TerminalInputEvent {
14+ readonly data: string;
15+}
16+
17+interface TerminalResizeEvent {
18+ readonly cols: number;
19+ readonly rows: number;
20+}
21+
22+export interface NativeTerminalSurfaceProps extends ViewProps {
23+ readonly appearanceScheme?: "light" | "dark";
24+ readonly themeConfig?: string;
25+ readonly backgroundColor?: string;
26+ readonly foregroundColor?: string;
27+ readonly mutedForegroundColor?: string;
28+ readonly terminalKey: string;
29+ readonly initialBuffer: string;
30+ readonly fontSize: number;
31+ readonly onInput?: (event: NativeSyntheticEvent<TerminalInputEvent>) => void;
32+ readonly onResize?: (event: NativeSyntheticEvent<TerminalResizeEvent>) => void;
33+}
34+
35+let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined;
36+
37+function getExpoViewConfig(moduleName: string) {
38+ return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
39+ moduleName,
40+ );
41+}
42+
43+export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminalSurfaceProps> | null {
44+ if (cachedNativeTerminalSurfaceView) {
45+ return cachedNativeTerminalSurfaceView;
46+ }
47+
48+ if (getExpoViewConfig(NATIVE_TERMINAL_MODULE_NAME) == null) {
49+ return null;
50+ }
51+
52+ try {
53+ cachedNativeTerminalSurfaceView = requireNativeViewManager<NativeTerminalSurfaceProps>(
54+ NATIVE_TERMINAL_MODULE_NAME,
55+ );
56+ } catch {
57+ return null;
58+ }
59+
60+ return cachedNativeTerminalSurfaceView ?? null;
61+}
62+
63+export function hasNativeTerminalSurface() {
64+ return resolveNativeTerminalSurfaceView() !== null;
65+}
apps/mobile/src/native/terminal/terminalTheme.test.ts+36−0
@@ -0,0 +1,36 @@
1+import { describe, expect, it } from "vitest";
2+
3+import { buildGhosttyThemeConfig, getGitHubTerminalTheme } from "./terminalTheme";
4+
5+describe("getGitHubTerminalTheme", () => {
6+ it("returns the GitHub light terminal palette", () => {
7+ expect(getGitHubTerminalTheme("light")).toMatchObject({
8+ background: "#ffffff",
9+ foreground: "#586069",
10+ cursorForeground: "#005cc5",
11+ cursorBackground: "#d1d5da",
12+ });
13+ });
14+
15+ it("returns the GitHub dark terminal palette", () => {
16+ expect(getGitHubTerminalTheme("dark")).toMatchObject({
17+ background: "#24292e",
18+ foreground: "#d1d5da",
19+ cursorForeground: "#79b8ff",
20+ cursorBackground: "#586069",
21+ });
22+ });
23+});
24+
25+describe("buildGhosttyThemeConfig", () => {
26+ it("serializes theme colors into a ghostty config file", () => {
27+ const config = buildGhosttyThemeConfig(getGitHubTerminalTheme("dark"));
28+
29+ expect(config).toContain("background = #24292e");
30+ expect(config).toContain("foreground = #d1d5da");
31+ expect(config).toContain("cursor-color = #79b8ff");
32+ expect(config).toContain("palette = 0=#586069");
33+ expect(config).toContain("palette = 15=#fafbfc");
34+ expect(config.endsWith("\n")).toBe(true);
35+ });
36+});
apps/mobile/src/native/terminal/terminalTheme.ts+85−0
@@ -0,0 +1,85 @@
1+export type TerminalAppearanceScheme = "light" | "dark";
2+
3+export interface TerminalTheme {
4+ readonly background: string;
5+ readonly foreground: string;
6+ readonly mutedForeground: string;
7+ readonly border: string;
8+ readonly cursorForeground: string;
9+ readonly cursorBackground: string;
10+ readonly palette: readonly string[];
11+}
12+
13+const GITHUB_LIGHT_THEME: TerminalTheme = {
14+ // Sourced from GitHub's official VS Code theme terminal palette.
15+ background: "#ffffff",
16+ foreground: "#586069",
17+ mutedForeground: "#6a737d",
18+ border: "rgba(27,31,35,0.12)",
19+ cursorForeground: "#005cc5",
20+ cursorBackground: "#d1d5da",
21+ palette: [
22+ "#24292e",
23+ "#d73a49",
24+ "#28a745",
25+ "#dbab09",
26+ "#0366d6",
27+ "#5a32a3",
28+ "#1b7c83",
29+ "#6a737d",
30+ "#959da5",
31+ "#cb2431",
32+ "#22863a",
33+ "#b08800",
34+ "#005cc5",
35+ "#5a32a3",
36+ "#3192aa",
37+ "#d1d5da",
38+ ],
39+};
40+
41+const GITHUB_DARK_THEME: TerminalTheme = {
42+ background: "#24292e",
43+ foreground: "#d1d5da",
44+ mutedForeground: "#959da5",
45+ border: "rgba(240,246,252,0.12)",
46+ cursorForeground: "#79b8ff",
47+ cursorBackground: "#586069",
48+ palette: [
49+ "#586069",
50+ "#ea4a5a",
51+ "#34d058",
52+ "#ffea7f",
53+ "#2188ff",
54+ "#b392f0",
55+ "#39c5cf",
56+ "#d1d5da",
57+ "#959da5",
58+ "#f97583",
59+ "#85e89d",
60+ "#ffea7f",
61+ "#79b8ff",
62+ "#b392f0",
63+ "#56d4dd",
64+ "#fafbfc",
65+ ],
66+};
67+
68+export function getGitHubTerminalTheme(scheme: TerminalAppearanceScheme): TerminalTheme {
69+ return scheme === "light" ? GITHUB_LIGHT_THEME : GITHUB_DARK_THEME;
70+}
71+
72+export function buildGhosttyThemeConfig(theme: TerminalTheme): string {
73+ const lines = [
74+ `background = ${theme.background}`,
75+ `foreground = ${theme.foreground}`,
76+ `cursor-color = ${theme.cursorForeground}`,
77+ `cursor-text = ${theme.cursorBackground}`,
78+ ];
79+
80+ for (const [index, color] of theme.palette.entries()) {
81+ lines.push(`palette = ${index}=${color}`);
82+ }
83+
84+ return `${lines.join("\n")}\n`;
85+}
apps/mobile/src/state/use-remote-environment-registry.ts+6−1
@@ -23,6 +23,7 @@ import { appAtomRegistry } from "./atom-registry"; · 22 unmodified lines
2323 import { type ConnectedEnvironmentSummary, type EnvironmentSession } from "./remote-runtime-types";
2424 import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
2525 import { shellSnapshotManager } from "./use-shell-snapshot";
26+import { terminalSessionManager } from "./use-terminal-session";
2627
2728 const environmentSessions = new Map<string, EnvironmentSession>();
2829 const environmentConnectionListeners = new Set<() => void>();
@@ -154,6 +155,7 @@ export async function disconnectEnvironment( · 125 unmodified lines
154155 notifyEnvironmentConnectionListeners();
155156 await session?.connection.dispose();
156157 shellSnapshotManager.invalidate({ environmentId });
158+ terminalSessionManager.invalidateEnvironment(environmentId);
157159 environmentRuntimeManager.invalidate({ environmentId });
158160
159161 if (options?.removeSaved) {
@@ -244,7 +246,9 @@ export async function connectSavedEnvironment( · 84 unmodified lines
244246 onShellResubscribe: (environmentId) => {
245247 shellSnapshotManager.markPending({ environmentId });
246248 },
247− applyTerminalEvent: () => undefined,
249+ applyTerminalEvent: (event, environmentId) => {
250+ terminalSessionManager.applyEvent({ environmentId }, event);
251+ },
248252 onConfigSnapshot: (serverConfig) => {
249253 environmentRuntimeManager.patch({ environmentId: connection.environmentId }, (runtime) => ({
250254 ...runtime,
@@ -333,6 +337,7 @@ export function useRemoteEnvironmentBootstrap() { · 82 unmodified lines
333337 environmentSessions.clear();
334338 environmentRuntimeManager.invalidate();
335339 shellSnapshotManager.invalidate();
340+ terminalSessionManager.invalidate();
336341 notifyEnvironmentConnectionListeners();
337342 };
338343 }, []);
apps/mobile/src/state/use-terminal-session.ts+63−0
@@ -0,0 +1,63 @@
1+import { useAtomValue } from "@effect/atom-react";
2+import {
3+ createTerminalSessionManager,
4+ EMPTY_TERMINAL_SESSION_ATOM,
5+ EMPTY_TERMINAL_SESSION_STATE,
6+ getTerminalSessionTargetKey,
7+ knownTerminalSessionsAtom,
8+ terminalSessionStateAtom,
9+ type TerminalSessionState,
10+} from "@t3tools/client-runtime";
11+import { useMemo } from "react";
12+
13+import { appAtomRegistry } from "./atom-registry";
14+
15+export const terminalSessionManager = createTerminalSessionManager({
16+ getRegistry: () => appAtomRegistry,
17+});
18+
19+export function useTerminalSession(input: {
20+ readonly environmentId: string | null;
21+ readonly threadId: string | null;
22+ readonly terminalId: string | null;
23+}): TerminalSessionState {
24+ const targetKey = getTerminalSessionTargetKey(input);
25+ const state = useAtomValue(
26+ targetKey !== null ? terminalSessionStateAtom(targetKey) : EMPTY_TERMINAL_SESSION_ATOM,
27+ );
28+ return targetKey === null ? EMPTY_TERMINAL_SESSION_STATE : state;
29+}
30+
31+export function useTerminalSessionTarget(input: {
32+ readonly environmentId: string | null;
33+ readonly threadId: string | null;
34+ readonly terminalId: string | null;
35+}) {
36+ return useMemo(
37+ () => ({
38+ environmentId: input.environmentId,
39+ threadId: input.threadId,
40+ terminalId: input.terminalId,
41+ }),
42+ [input.environmentId, input.threadId, input.terminalId],
43+ );
44+}
45+
46+export function useKnownTerminalSessions(input: {
47+ readonly environmentId: string | null;
48+ readonly threadId: string | null;
49+}) {
50+ const knownSessionsIndex = useAtomValue(knownTerminalSessionsAtom);
51+
52+ return useMemo(() => {
53+ if (input.environmentId === null || input.threadId === null) {
54+ return [];
55+ }
56+
57+ void knownSessionsIndex;
58+ return terminalSessionManager.listSessions({
59+ environmentId: input.environmentId,
60+ threadId: input.threadId,
61+ });
62+ }, [input.environmentId, input.threadId, knownSessionsIndex]);
63+}
apps/server/src/terminal/Layers/Manager.test.ts+41−0
@@ -440,6 +440,32 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", ( · 439 unmodified lines
440440 }),
441441 );
442442
443+ it.effect("restarts a running session when open is called with a different cwd", () =>
444+ Effect.gen(function* () {
445+ const { manager, ptyAdapter, logsDir, baseDir } = yield* createManager();
446+ const originalCwd = path.join(baseDir, "original");
447+ const differentCwd = path.join(baseDir, "different");
448+ yield* makeDirectory(originalCwd);
449+ yield* makeDirectory(differentCwd);
450+
451+ yield* manager.open(openInput({ cwd: originalCwd }));
452+ const firstProcess = ptyAdapter.processes[0];
453+ expect(firstProcess).toBeDefined();
454+ if (!firstProcess) return;
455+
456+ firstProcess.emitData("before reopen\n");
457+ yield* waitFor(pathExists(historyLogPath(logsDir)));
458+
459+ const reopened = yield* manager.open(openInput({ cwd: differentCwd }));
460+
461+ expect(ptyAdapter.spawnInputs).toHaveLength(2);
462+ assert.equal(firstProcess.killed, true);
463+ assert.equal(reopened.cwd, differentCwd);
464+ assert.equal(reopened.history, "");
465+ yield* waitFor(Effect.map(readFileString(historyLogPath(logsDir)), (text) => text === ""));
466+ }),
467+ );
468+
443469 it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () =>
444470 Effect.gen(function* () {
445471 const { manager, getEvents, baseDir } = yield* createManager();
@@ -778,6 +804,21 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", ( · 332 unmodified lines
778804 }).pipe(Effect.provide(TestClock.layer())),
779805 );
780806
807+ it.effect("publishes closed events when terminals are explicitly closed", () =>
808+ Effect.gen(function* () {
809+ const { manager, getEvents } = yield* createManager();
810+ yield* manager.open(openInput({ terminalId: "default" }));
811+ yield* manager.open(openInput({ terminalId: "sidecar" }));
812+
813+ yield* manager.close({ threadId: "thread-1" });
814+
815+ const closedEvents = (yield* getEvents).filter(
816+ (event): event is Extract<TerminalEvent, { type: "closed" }> => event.type === "closed",
817+ );
818+ expect(closedEvents.map((event) => event.terminalId).sort()).toEqual(["default", "sidecar"]);
819+ }),
820+ );
821+
781822 it.effect("evicts oldest inactive terminal sessions when retention limit is exceeded", () =>
782823 Effect.gen(function* () {
783824 const { manager, ptyAdapter, logsDir, getEvents } = yield* createManager(5, {
apps/server/src/terminal/Layers/Manager.ts+24−7
@@ -1484,15 +1484,24 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 1483 unmodified lines
14841484
14851485 yield* flushPersist(threadId, terminalId);
14861486
1487− yield* modifyManagerState((state) => {
1487+ const removed = yield* modifyManagerState((state) => {
14881488 if (!state.sessions.has(key)) {
1489− return [undefined, state] as const;
1489+ return [false, state] as const;
14901490 }
14911491 const sessions = new Map(state.sessions);
14921492 sessions.delete(key);
1493− return [undefined, { ...state, sessions }] as const;
1493+ return [true, { ...state, sessions }] as const;
14941494 });
14951495
1496+ if (removed) {
1497+ yield* publishEvent({
1498+ type: "closed",
1499+ threadId,
1500+ terminalId,
1501+ createdAt: new Date().toISOString(),
1502+ });
1503+ }
1504+
14961505 if (deleteHistoryOnClose) {
14971506 yield* deleteHistory(threadId, terminalId);
14981507 }
@@ -1683,11 +1692,19 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 184 unmodified lines
16831692 const targetCols = input.cols ?? liveSession.cols;
16841693 const targetRows = input.rows ?? liveSession.rows;
16851694 const runtimeEnvChanged = !Equal.equals(currentRuntimeEnv, nextRuntimeEnv);
1686−
1687− if (liveSession.cwd !== input.cwd || runtimeEnvChanged) {
1695+ const nextWorktreePath =
1696+ input.worktreePath !== undefined
1697+ ? (input.worktreePath ?? null)
1698+ : liveSession.worktreePath;
1699+ const launchContextChanged =
1700+ liveSession.cwd !== input.cwd ||
1701+ runtimeEnvChanged ||
1702+ liveSession.worktreePath !== nextWorktreePath;
1703+
1704+ if (launchContextChanged) {
16881705 yield* stopProcess(liveSession);
16891706 liveSession.cwd = input.cwd;
1690− liveSession.worktreePath = input.worktreePath ?? null;
1707+ liveSession.worktreePath = nextWorktreePath;
16911708 liveSession.runtimeEnv = nextRuntimeEnv;
16921709 liveSession.history = "";
16931710 liveSession.pendingHistoryControlSequence = "";
@@ -1701,7 +1718,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 7 unmodified lines
17011718 );
17021719 } else if (liveSession.status === "exited" || liveSession.status === "error") {
17031720 liveSession.runtimeEnv = nextRuntimeEnv;
1704− liveSession.worktreePath = input.worktreePath ?? null;
1721+ liveSession.worktreePath = nextWorktreePath;
17051722 liveSession.history = "";
17061723 liveSession.pendingHistoryControlSequence = "";
17071724 liveSession.pendingProcessEvents = [];
apps/web/src/components/ThreadTerminalDrawer.tsx+15−10
@@ -602,16 +602,21 @@ export function TerminalViewport({ · 601 unmodified lines
602602 return;
603603 }
604604
605− const details = [
606− typeof event.exitCode === "number" ? `code ${event.exitCode}` : null,
607− typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null,
608− ]
609− .filter((value): value is string => value !== null)
610− .join(", ");
611− writeSystemMessage(
612− activeTerminal,
613− details.length > 0 ? `Process exited (${details})` : "Process exited",
614− );
605+ if (event.type === "closed") {
606+ writeSystemMessage(activeTerminal, "Terminal closed");
607+ } else {
608+ const details = [
609+ typeof event.exitCode === "number" ? `code ${event.exitCode}` : null,
610+ typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null,
611+ ]
612+ .filter((value): value is string => value !== null)
613+ .join(", ");
614+ writeSystemMessage(
615+ activeTerminal,
616+ details.length > 0 ? `Process exited (${details})` : "Process exited",
617+ );
618+ }
619+
615620 if (hasHandledExitRef.current) {
616621 return;
617622 }
apps/web/src/terminalActivity.ts+1−0
@@ -7,6 +7,7 @@ export function terminalRunningSubprocessFromEvent(event: TerminalEvent): boolea · 6 unmodified lines
77 case "started":
88 case "restarted":
99 case "exited":
10+ case "closed":
1011 return false;
1112 default:
1213 return null;
apps/web/src/terminalStateStore.test.ts+2−0
@@ -34,6 +34,8 @@ function makeTerminalEvent( · 33 unmodified lines
3434 return { ...base, type, ...overrides } as TerminalEvent;
3535 case "exited":
3636 return { ...base, type, exitCode: 0, exitSignal: null, ...overrides } as TerminalEvent;
37+ case "closed":
38+ return { ...base, type, ...overrides } as TerminalEvent;
3739 case "started":
3840 case "restarted":
3941 return {
bun.lock+7−0
@@ -57,11 +57,13 @@ · 56 unmodified lines
5757 "@shikijs/themes": "3.23.0",
5858 "@t3tools/client-runtime": "workspace:*",
5959 "@t3tools/contracts": "workspace:*",
60+ "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal",
6061 "@t3tools/shared": "workspace:*",
6162 "clsx": "^2.1.1",
6263 "diff": "8.0.3",
6364 "effect": "catalog:",
6465 "expo": "~55.0.14",
66+ "expo-build-properties": "~55.0.13",
6567 "expo-camera": "~55.0.15",
6668 "expo-clipboard": "~55.0.9",
6769 "expo-constants": "^55.0.9",
@@ -72,6 +74,7 @@ · 4 unmodified lines
7274 "expo-haptics": "^55.0.9",
7375 "expo-image-picker": "~55.0.14",
7476 "expo-linking": "~55.0.12",
77+ "expo-modules-core": "~55.0.22",
7578 "expo-paste-input": "^0.1.15",
7679 "expo-router": "~55.0.12",
7780 "expo-secure-store": "~55.0.9",
@@ -1662,6 +1665,8 @@ · 1584 unmodified lines
16621665
16631666 "expo-asset": ["expo-asset@55.0.15", "", { "dependencies": { "@expo/image-utils": "^0.8.13", "expo-constants": "~55.0.14" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-d3FIpHJ6ZngYXxRItYWBGT5H8Wkk7/l4fMe8Mmd2xDyKrO0/CM7c8r/J5M71D+BJr5P3My8wertGYZXHSiZYxQ=="],
16641667
1668+ "expo-build-properties": ["expo-build-properties@55.0.13", "", { "dependencies": { "@expo/schema-utils": "^55.0.3", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-UYZhUKyh7YQhbJdkBvo68WUQ7fOtZeSV7F8kfYkjEiN/ADRHG0WfEIiddvGfi9cH/5iwpptv/+Lu5cx6uvfegA=="],
1669+
16651670 "expo-camera": ["expo-camera@55.0.15", "", { "dependencies": { "barcode-detector": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-WRVsZf+2p7EsxudwyiUMYijJS8M98t/BVP6yG7N+08JSUotkGjmZcemom1gM36uy27P8QsSVP0hD+FravmQiBA=="],
16661671
16671672 "expo-clipboard": ["expo-clipboard@55.0.13", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew=="],
@@ -3032,6 +3037,8 @@ · 1364 unmodified lines
30323037
30333038 "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
30343039
3040+ "@t3tools/mobile/@t3tools/mobile-terminal-native": ["@t3tools/mobile-terminal-native@file:apps/mobile/modules/t3-terminal", {}],
3041+
30353042 "@tailwindcss/node/tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
30363043
30373044 "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
packages/client-runtime/src/index.ts+1−0
@@ -11,6 +11,7 @@ export * from "./gitActions.ts"; · 10 unmodified lines
1111 export * from "./gitActionState.ts";
1212 export * from "./gitBranchState.ts";
1313 export * from "./gitStatusState.ts";
14+export * from "./terminalSessionState.ts";
1415 export * from "./transportError.ts";
1516 export * from "./wsRpcProtocol.ts";
1617 export * from "./wsTransport.ts";
packages/client-runtime/src/terminalSessionState.test.ts+276−0
@@ -0,0 +1,276 @@
1+import { AtomRegistry } from "effect/unstable/reactivity";
2+import { afterEach, describe, expect, it } from "vitest";
3+
4+import type { TerminalSessionSnapshot } from "@t3tools/contracts";
5+
6+import { createTerminalSessionManager } from "./terminalSessionState";
7+
8+let atomRegistry = AtomRegistry.make();
9+
10+function resetAtomRegistry() {
11+ atomRegistry.dispose();
12+ atomRegistry = AtomRegistry.make();
13+}
14+
15+const TARGET = {
16+ environmentId: "env-local",
17+ threadId: "thread-1",
18+ terminalId: "default",
19+} as const;
20+
21+const BASE_SNAPSHOT: TerminalSessionSnapshot = {
22+ threadId: TARGET.threadId,
23+ terminalId: TARGET.terminalId,
24+ cwd: "/repo",
25+ worktreePath: null,
26+ status: "running",
27+ pid: 123,
28+ history: "hello",
29+ exitCode: null,
30+ exitSignal: null,
31+ updatedAt: "2026-04-01T00:00:00.000Z",
32+};
33+
34+describe("createTerminalSessionManager", () => {
35+ afterEach(() => {
36+ resetAtomRegistry();
37+ });
38+
39+ it("hydrates from started snapshots and appends output events", () => {
40+ const manager = createTerminalSessionManager({
41+ getRegistry: () => atomRegistry,
42+ });
43+
44+ manager.applyEvent(TARGET, {
45+ type: "started",
46+ threadId: TARGET.threadId,
47+ terminalId: TARGET.terminalId,
48+ createdAt: BASE_SNAPSHOT.updatedAt,
49+ snapshot: BASE_SNAPSHOT,
50+ });
51+ manager.applyEvent(TARGET, {
52+ type: "output",
53+ threadId: TARGET.threadId,
54+ terminalId: TARGET.terminalId,
55+ createdAt: "2026-04-01T00:00:01.000Z",
56+ data: " world",
57+ });
58+
59+ expect(manager.getSnapshot(TARGET)).toMatchObject({
60+ snapshot: BASE_SNAPSHOT,
61+ buffer: "hello world",
62+ status: "running",
63+ error: null,
64+ updatedAt: "2026-04-01T00:00:01.000Z",
65+ });
66+ });
67+
68+ it("caps retained output", () => {
69+ const manager = createTerminalSessionManager({
70+ getRegistry: () => atomRegistry,
71+ maxBufferBytes: 5,
72+ });
73+
74+ manager.applyEvent(TARGET, {
75+ type: "output",
76+ threadId: TARGET.threadId,
77+ terminalId: TARGET.terminalId,
78+ createdAt: "2026-04-01T00:00:01.000Z",
79+ data: "abcdef",
80+ });
81+
82+ expect(manager.getSnapshot(TARGET).buffer).toBe("bcdef");
83+ });
84+
85+ it("caps retained output by utf-8 byte length", () => {
86+ const manager = createTerminalSessionManager({
87+ getRegistry: () => atomRegistry,
88+ maxBufferBytes: 4,
89+ });
90+
91+ manager.applyEvent(TARGET, {
92+ type: "output",
93+ threadId: TARGET.threadId,
94+ terminalId: TARGET.terminalId,
95+ createdAt: "2026-04-01T00:00:01.000Z",
96+ data: "🙂🙂",
97+ });
98+
99+ expect(manager.getSnapshot(TARGET).buffer).toBe("🙂");
100+ });
101+
102+ it("invalidates one environment without clearing others", () => {
103+ const manager = createTerminalSessionManager({
104+ getRegistry: () => atomRegistry,
105+ });
106+ const otherTarget = {
107+ environmentId: "env-remote",
108+ threadId: "thread-1",
109+ terminalId: "default",
110+ } as const;
111+
112+ for (const target of [TARGET, otherTarget]) {
113+ manager.applyEvent(target, {
114+ type: "output",
115+ threadId: target.threadId,
116+ terminalId: target.terminalId,
117+ createdAt: "2026-04-01T00:00:01.000Z",
118+ data: target.environmentId,
119+ });
120+ }
121+
122+ manager.invalidateEnvironment(TARGET.environmentId);
123+
124+ expect(manager.getSnapshot(TARGET).buffer).toBe("");
125+ expect(manager.getSnapshot(otherTarget).buffer).toBe("env-remote");
126+ });
127+
128+ it("lists known sessions for a thread ordered by recency", () => {
129+ const manager = createTerminalSessionManager({
130+ getRegistry: () => atomRegistry,
131+ });
132+
133+ manager.applyEvent(TARGET, {
134+ type: "started",
135+ threadId: TARGET.threadId,
136+ terminalId: TARGET.terminalId,
137+ createdAt: "2026-04-01T00:00:00.000Z",
138+ snapshot: BASE_SNAPSHOT,
139+ });
140+ manager.applyEvent(
141+ {
142+ environmentId: TARGET.environmentId,
143+ },
144+ {
145+ type: "started",
146+ threadId: TARGET.threadId,
147+ terminalId: "term-2",
148+ createdAt: "2026-04-01T00:00:02.000Z",
149+ snapshot: {
150+ ...BASE_SNAPSHOT,
151+ terminalId: "term-2",
152+ updatedAt: "2026-04-01T00:00:02.000Z",
153+ },
154+ },
155+ );
156+
157+ expect(
158+ manager
159+ .listSessions({
160+ environmentId: TARGET.environmentId,
161+ threadId: TARGET.threadId,
162+ })
163+ .map((session) => session.target.terminalId),
164+ ).toEqual(["term-2", "default"]);
165+ });
166+
167+ it("drops known sessions when an environment is invalidated", () => {
168+ const manager = createTerminalSessionManager({
169+ getRegistry: () => atomRegistry,
170+ });
171+
172+ manager.applyEvent(TARGET, {
173+ type: "output",
174+ threadId: TARGET.threadId,
175+ terminalId: TARGET.terminalId,
176+ createdAt: "2026-04-01T00:00:01.000Z",
177+ data: "hello",
178+ });
179+
180+ manager.invalidateEnvironment(TARGET.environmentId);
181+
182+ expect(
183+ manager.listSessions({
184+ environmentId: TARGET.environmentId,
185+ threadId: TARGET.threadId,
186+ }),
187+ ).toEqual([]);
188+ });
189+
190+ it("removes closed sessions from the known-session index while keeping local closed state", () => {
191+ const manager = createTerminalSessionManager({
192+ getRegistry: () => atomRegistry,
193+ });
194+
195+ manager.applyEvent(TARGET, {
196+ type: "started",
197+ threadId: TARGET.threadId,
198+ terminalId: TARGET.terminalId,
199+ createdAt: BASE_SNAPSHOT.updatedAt,
200+ snapshot: BASE_SNAPSHOT,
201+ });
202+
203+ manager.applyEvent(TARGET, {
204+ type: "closed",
205+ threadId: TARGET.threadId,
206+ terminalId: TARGET.terminalId,
207+ createdAt: "2026-04-01T00:00:04.000Z",
208+ });
209+
210+ expect(
211+ manager.listSessions({
212+ environmentId: TARGET.environmentId,
213+ threadId: TARGET.threadId,
214+ }),
215+ ).toEqual([]);
216+ expect(manager.getSnapshot(TARGET)).toMatchObject({
217+ buffer: "hello",
218+ status: "closed",
219+ snapshot: null,
220+ updatedAt: "2026-04-01T00:00:04.000Z",
221+ });
222+ });
223+
224+ it("clears locally retained closed state on reset", () => {
225+ const manager = createTerminalSessionManager({
226+ getRegistry: () => atomRegistry,
227+ });
228+
229+ manager.applyEvent(TARGET, {
230+ type: "started",
231+ threadId: TARGET.threadId,
232+ terminalId: TARGET.terminalId,
233+ createdAt: BASE_SNAPSHOT.updatedAt,
234+ snapshot: BASE_SNAPSHOT,
235+ });
236+ manager.applyEvent(TARGET, {
237+ type: "closed",
238+ threadId: TARGET.threadId,
239+ terminalId: TARGET.terminalId,
240+ createdAt: "2026-04-01T00:00:04.000Z",
241+ });
242+
243+ manager.reset();
244+
245+ expect(manager.getSnapshot(TARGET)).toEqual({
246+ snapshot: null,
247+ buffer: "",
248+ status: "closed",
249+ error: null,
250+ hasRunningSubprocess: false,
251+ updatedAt: null,
252+ version: 0,
253+ });
254+ });
255+
256+ it("syncs snapshots returned from open calls immediately", () => {
257+ const manager = createTerminalSessionManager({
258+ getRegistry: () => atomRegistry,
259+ });
260+
261+ manager.syncSnapshot(
262+ { environmentId: TARGET.environmentId },
263+ {
264+ ...BASE_SNAPSHOT,
265+ history: "prompt$ ",
266+ updatedAt: "2026-04-01T00:00:03.000Z",
267+ },
268+ );
269+
270+ expect(manager.getSnapshot(TARGET)).toMatchObject({
271+ buffer: "prompt$ ",
272+ status: "running",
273+ updatedAt: "2026-04-01T00:00:03.000Z",
274+ });
275+ });
276+});
packages/client-runtime/src/terminalSessionState.ts+404−0
@@ -0,0 +1,404 @@
1+import type { TerminalEvent, TerminalSessionSnapshot } from "@t3tools/contracts";
2+import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
3+
4+export interface TerminalSessionState {
5+ readonly snapshot: TerminalSessionSnapshot | null;
6+ readonly buffer: string;
7+ readonly status: TerminalSessionSnapshot["status"] | "closed";
8+ readonly error: string | null;
9+ readonly hasRunningSubprocess: boolean;
10+ readonly updatedAt: string | null;
11+ readonly version: number;
12+}
13+
14+export interface TerminalSessionTarget {
15+ readonly environmentId: string | null;
16+ readonly threadId: string | null;
17+ readonly terminalId: string | null;
18+}
19+
20+export interface KnownTerminalSessionTarget {
21+ readonly environmentId: string;
22+ readonly threadId: string;
23+ readonly terminalId: string;
24+}
25+
26+export interface KnownTerminalSession {
27+ readonly target: KnownTerminalSessionTarget;
28+ readonly state: TerminalSessionState;
29+}
30+
31+export interface TerminalSessionManagerConfig {
32+ readonly getRegistry: () => AtomRegistry.AtomRegistry;
33+ readonly maxBufferBytes?: number;
34+}
35+
36+export const EMPTY_TERMINAL_SESSION_STATE = Object.freeze<TerminalSessionState>({
37+ snapshot: null,
38+ buffer: "",
39+ status: "closed",
40+ error: null,
41+ hasRunningSubprocess: false,
42+ updatedAt: null,
43+ version: 0,
44+});
45+
46+const DEFAULT_MAX_BUFFER_BYTES = 512 * 1024;
47+const knownTerminalSessionKeys = new Set<string>();
48+const textEncoder = new TextEncoder();
49+const textDecoder = new TextDecoder();
50+
51+export const terminalSessionStateAtom = Atom.family((key: string) => {
52+ knownTerminalSessionKeys.add(key);
53+ return Atom.make(EMPTY_TERMINAL_SESSION_STATE).pipe(
54+ Atom.keepAlive,
55+ Atom.withLabel(`terminal-session:${key}`),
56+ );
57+});
58+
59+export const EMPTY_TERMINAL_SESSION_ATOM = Atom.make(EMPTY_TERMINAL_SESSION_STATE).pipe(
60+ Atom.keepAlive,
61+ Atom.withLabel("terminal-session:null"),
62+);
63+
64+export const knownTerminalSessionsAtom = Atom.make<Record<string, KnownTerminalSessionTarget>>(
65+ {},
66+).pipe(Atom.keepAlive, Atom.withLabel("terminal-session:index"));
67+
68+export function getTerminalSessionTargetKey(target: TerminalSessionTarget): string | null {
69+ if (target.environmentId === null || target.threadId === null || target.terminalId === null) {
70+ return null;
71+ }
72+
73+ return `${target.environmentId}:${target.threadId}:${target.terminalId}`;
74+}
75+
76+function toKnownTarget(target: TerminalSessionTarget): KnownTerminalSessionTarget | null {
77+ const targetKey = getTerminalSessionTargetKey(target);
78+ if (targetKey === null) {
79+ return null;
80+ }
81+
82+ const environmentId = target.environmentId;
83+ const threadId = target.threadId;
84+ const terminalId = target.terminalId;
85+ if (environmentId === null || threadId === null || terminalId === null) {
86+ return null;
87+ }
88+
89+ return {
90+ environmentId,
91+ threadId,
92+ terminalId,
93+ };
94+}
95+
96+function trimBufferToBytes(buffer: string, maxBufferBytes: number): string {
97+ if (maxBufferBytes <= 0) {
98+ return "";
99+ }
100+
101+ const encoded = textEncoder.encode(buffer);
102+ if (encoded.byteLength <= maxBufferBytes) {
103+ return buffer;
104+ }
105+
106+ let start = encoded.byteLength - maxBufferBytes;
107+ while (start < encoded.length) {
108+ const byte = encoded[start];
109+ if (byte === undefined || (byte & 0b1100_0000) !== 0b1000_0000) {
110+ break;
111+ }
112+ start += 1;
113+ }
114+
115+ return textDecoder.decode(encoded.subarray(start));
116+}
117+
118+function stateFromSnapshot(
119+ snapshot: TerminalSessionSnapshot,
120+ maxBufferBytes: number,
121+): TerminalSessionState {
122+ return {
123+ snapshot,
124+ buffer: trimBufferToBytes(snapshot.history, maxBufferBytes),
125+ status: snapshot.status,
126+ error: null,
127+ hasRunningSubprocess: false,
128+ updatedAt: snapshot.updatedAt,
129+ version: 1,
130+ };
131+}
132+
133+export function createTerminalSessionManager(config: TerminalSessionManagerConfig) {
134+ const maxBufferBytes = config.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
135+ const closedSessionStates = new Map<string, TerminalSessionState>();
136+
137+ function clearKnownSessionAtomState(targetKey: string): void {
138+ knownTerminalSessionKeys.delete(targetKey);
139+ config.getRegistry().set(terminalSessionStateAtom(targetKey), EMPTY_TERMINAL_SESSION_STATE);
140+ }
141+
142+ function rememberTarget(target: TerminalSessionTarget): string | null {
143+ const targetKey = getTerminalSessionTargetKey(target);
144+ const knownTarget = toKnownTarget(target);
145+ if (targetKey === null || knownTarget === null) {
146+ return null;
147+ }
148+
149+ closedSessionStates.delete(targetKey);
150+
151+ const current = config.getRegistry().get(knownTerminalSessionsAtom);
152+ const existing = current[targetKey];
153+ if (
154+ existing?.environmentId === knownTarget.environmentId &&
155+ existing.threadId === knownTarget.threadId &&
156+ existing.terminalId === knownTarget.terminalId
157+ ) {
158+ return targetKey;
159+ }
160+
161+ config.getRegistry().set(knownTerminalSessionsAtom, {
162+ ...current,
163+ [targetKey]: knownTarget,
164+ });
165+ return targetKey;
166+ }
167+
168+ function removeTargets(
169+ match: (target: KnownTerminalSessionTarget) => boolean,
170+ ): ReadonlyArray<string> {
171+ const current = config.getRegistry().get(knownTerminalSessionsAtom);
172+ const removedKeys: string[] = [];
173+ const next = Object.fromEntries(
174+ Object.entries(current).filter(([key, target]) => {
175+ const shouldRemove = match(target);
176+ if (shouldRemove) {
177+ removedKeys.push(key);
178+ }
179+ return !shouldRemove;
180+ }),
181+ ) as Record<string, KnownTerminalSessionTarget>;
182+ if (Object.keys(next).length === Object.keys(current).length) {
183+ return removedKeys;
184+ }
185+
186+ config.getRegistry().set(knownTerminalSessionsAtom, next);
187+ return removedKeys;
188+ }
189+
190+ function getSnapshot(target: TerminalSessionTarget): TerminalSessionState {
191+ const targetKey = getTerminalSessionTargetKey(target);
192+ if (targetKey === null) {
193+ return EMPTY_TERMINAL_SESSION_STATE;
194+ }
195+
196+ const closedState = closedSessionStates.get(targetKey);
197+ if (closedState) {
198+ return closedState;
199+ }
200+
201+ const rememberedTargetKey = rememberTarget(target);
202+ if (rememberedTargetKey === null) {
203+ return EMPTY_TERMINAL_SESSION_STATE;
204+ }
205+
206+ return config.getRegistry().get(terminalSessionStateAtom(rememberedTargetKey));
207+ }
208+
209+ function setState(targetKey: string, nextState: TerminalSessionState): void {
210+ config.getRegistry().set(terminalSessionStateAtom(targetKey), nextState);
211+ }
212+
213+ function syncSnapshot(
214+ target: Pick<TerminalSessionTarget, "environmentId">,
215+ snapshot: TerminalSessionSnapshot,
216+ ): void {
217+ const targetKey = rememberTarget({
218+ environmentId: target.environmentId,
219+ threadId: snapshot.threadId,
220+ terminalId: snapshot.terminalId,
221+ });
222+ if (targetKey === null) {
223+ return;
224+ }
225+
226+ setState(targetKey, stateFromSnapshot(snapshot, maxBufferBytes));
227+ }
228+
229+ function applyEvent(
230+ target: Pick<TerminalSessionTarget, "environmentId">,
231+ event: TerminalEvent,
232+ ): void {
233+ const targetKey = rememberTarget({
234+ environmentId: target.environmentId,
235+ threadId: event.threadId,
236+ terminalId: event.terminalId,
237+ });
238+ if (targetKey === null) {
239+ return;
240+ }
241+
242+ const current = config.getRegistry().get(terminalSessionStateAtom(targetKey));
243+ switch (event.type) {
244+ case "started":
245+ case "restarted":
246+ setState(targetKey, stateFromSnapshot(event.snapshot, maxBufferBytes));
247+ return;
248+ case "output":
249+ setState(targetKey, {
250+ ...current,
251+ buffer: trimBufferToBytes(`${current.buffer}${event.data}`, maxBufferBytes),
252+ status: current.status === "closed" ? "running" : current.status,
253+ error: null,
254+ updatedAt: event.createdAt,
255+ version: current.version + 1,
256+ });
257+ return;
258+ case "cleared":
259+ setState(targetKey, {
260+ ...current,
261+ buffer: "",
262+ error: null,
263+ updatedAt: event.createdAt,
264+ version: current.version + 1,
265+ });
266+ return;
267+ case "exited":
268+ setState(targetKey, {
269+ ...current,
270+ snapshot: current.snapshot
271+ ? {
272+ ...current.snapshot,
273+ status: "exited",
274+ exitCode: event.exitCode,
275+ exitSignal: event.exitSignal,
276+ updatedAt: event.createdAt,
277+ }
278+ : null,
279+ status: "exited",
280+ hasRunningSubprocess: false,
281+ updatedAt: event.createdAt,
282+ version: current.version + 1,
283+ });
284+ return;
285+ case "closed":
286+ closedSessionStates.set(targetKey, {
287+ ...current,
288+ snapshot: null,
289+ status: "closed",
290+ error: null,
291+ hasRunningSubprocess: false,
292+ updatedAt: event.createdAt,
293+ version: current.version + 1,
294+ });
295+ clearKnownSessionAtomState(targetKey);
296+ removeTargets(
297+ (knownTarget) =>
298+ knownTarget.environmentId === target.environmentId &&
299+ knownTarget.threadId === event.threadId &&
300+ knownTarget.terminalId === event.terminalId,
301+ );
302+ return;
303+ case "error":
304+ setState(targetKey, {
305+ ...current,
306+ status: "error",
307+ error: event.message,
308+ hasRunningSubprocess: false,
309+ updatedAt: event.createdAt,
310+ version: current.version + 1,
311+ });
312+ return;
313+ case "activity":
314+ setState(targetKey, {
315+ ...current,
316+ hasRunningSubprocess: event.hasRunningSubprocess,
317+ updatedAt: event.createdAt,
318+ version: current.version + 1,
319+ });
320+ return;
321+ }
322+ }
323+
324+ function invalidate(target?: TerminalSessionTarget): void {
325+ if (target) {
326+ const targetKey = getTerminalSessionTargetKey(target);
327+ if (targetKey !== null) {
328+ closedSessionStates.delete(targetKey);
329+ setState(targetKey, EMPTY_TERMINAL_SESSION_STATE);
330+ }
331+ return;
332+ }
333+
334+ for (const key of knownTerminalSessionKeys) {
335+ setState(key, EMPTY_TERMINAL_SESSION_STATE);
336+ }
337+ closedSessionStates.clear();
338+ knownTerminalSessionKeys.clear();
339+ config.getRegistry().set(knownTerminalSessionsAtom, {});
340+ }
341+
342+ function invalidateEnvironment(environmentId: string): void {
343+ const prefix = `${environmentId}:`;
344+ for (const key of knownTerminalSessionKeys) {
345+ if (key.startsWith(prefix)) {
346+ setState(key, EMPTY_TERMINAL_SESSION_STATE);
347+ }
348+ }
349+ for (const key of closedSessionStates.keys()) {
350+ if (key.startsWith(prefix)) {
351+ closedSessionStates.delete(key);
352+ }
353+ }
354+ for (const key of removeTargets((target) => target.environmentId === environmentId)) {
355+ closedSessionStates.delete(key);
356+ clearKnownSessionAtomState(key);
357+ }
358+ }
359+
360+ function reset(): void {
361+ invalidate();
362+ }
363+
364+ function listSessions(
365+ filter?: Partial<KnownTerminalSessionTarget>,
366+ ): ReadonlyArray<KnownTerminalSession> {
367+ const knownTargets = Object.values(config.getRegistry().get(knownTerminalSessionsAtom));
368+ return knownTargets
369+ .filter((target) => {
370+ if (filter?.environmentId && target.environmentId !== filter.environmentId) {
371+ return false;
372+ }
373+ if (filter?.threadId && target.threadId !== filter.threadId) {
374+ return false;
375+ }
376+ if (filter?.terminalId && target.terminalId !== filter.terminalId) {
377+ return false;
378+ }
379+ return true;
380+ })
381+ .map((target) => ({
382+ target,
383+ state: getSnapshot(target),
384+ }))
385+ .sort((left, right) => {
386+ const leftUpdatedAt = left.state.updatedAt ? Date.parse(left.state.updatedAt) : 0;
387+ const rightUpdatedAt = right.state.updatedAt ? Date.parse(right.state.updatedAt) : 0;
388+ if (leftUpdatedAt !== rightUpdatedAt) {
389+ return rightUpdatedAt - leftUpdatedAt;
390+ }
391+ return left.target.terminalId.localeCompare(right.target.terminalId);
392+ });
393+ }
394+
395+ return {
396+ applyEvent,
397+ getSnapshot,
398+ invalidate,
399+ invalidateEnvironment,
400+ listSessions,
401+ syncSnapshot,
402+ reset,
403+ };
404+}
packages/contracts/src/terminal.test.ts+11−0
@@ -197,6 +197,17 @@ describe("TerminalEvent", () => { · 196 unmodified lines
197197 ).toBe(true);
198198 });
199199
200+ it("accepts closed events", () => {
201+ expect(
202+ decodes(TerminalEvent, {
203+ type: "closed",
204+ threadId: "thread-1",
205+ terminalId: DEFAULT_TERMINAL_ID,
206+ createdAt: new Date().toISOString(),
207+ }),
208+ ).toBe(true);
209+ });
210+
200211 it("accepts activity events", () => {
201212 expect(
202213 decodes(TerminalEvent, {
packages/contracts/src/terminal.ts+6−0
@@ -119,6 +119,11 @@ const TerminalExitedEvent = Schema.Struct({ · 118 unmodified lines
119119 exitSignal: Schema.NullOr(Schema.Int),
120120 });
121121
122+const TerminalClosedEvent = Schema.Struct({
123+ ...TerminalEventBaseSchema.fields,
124+ type: Schema.Literal("closed"),
125+});
126+
122127 const TerminalErrorEvent = Schema.Struct({
123128 ...TerminalEventBaseSchema.fields,
124129 type: Schema.Literal("error"),
@@ -146,6 +151,7 @@ export const TerminalEvent = Schema.Union([ · 21 unmodified lines
146151 TerminalStartedEvent,
147152 TerminalOutputEvent,
148153 TerminalExitedEvent,
154+ TerminalClosedEvent,
149155 TerminalErrorEvent,
150156 TerminalClearedEvent,
151157 TerminalRestartedEvent,