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
114 },114 },
115 },115 },
116 ],116 ],
117 [
118 "expo-build-properties",
119 {
120 ios: {
121 deploymentTarget: "16.1",
122 },
123 },
124 ],
117 "expo-secure-store",125 "expo-secure-store",
118 "expo-router",126 "expo-router",
119 "./plugins/withAndroidCleartextTraffic.cjs",127 "./plugins/withAndroidCleartextTraffic.cjs",
apps/mobile/modules/t3-terminal/README.md+25−0
@@ -0,0 +1,25 @@
1# T3 Mobile Terminal Native Module
2
3This local Expo module owns the native terminal surface for the mobile app.
4
5The 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
11The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the same custom-I/O
12Ghostty fork used by VVTerm. `T3TerminalView` owns a `libghostty` surface and mirrors VVTerm's
13custom I/O model:
14
151. initialize libghostty once for the process
162. create one Ghostty app and surface per native view
173. feed remote output into the surface with `ghostty_surface_feed_data`
184. send user input back to JS with the write callback
195. emit Ghostty's measured terminal size through `onResize`
20
21Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the
22React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a
23future Android Ghostty build without changing JS.
24
25Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`.
apps/mobile/modules/t3-terminal/T3TerminalNative.podspec+21−0
@@ -0,0 +1,21 @@
1require 'json'
2
3package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
5Pod::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'
21end
apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md+15−0
@@ -0,0 +1,15 @@
1# Third-Party Notices
2
3## Ghostty / libghostty
4
5The iOS terminal renderer vendors `GhosttyKit.xcframework`, a libghostty build produced from
6VVTerm'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
14Ghostty'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
12extern "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
29typedef void* ghostty_app_t;
30typedef void* ghostty_config_t;
31typedef void* ghostty_surface_t;
32typedef 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.
37typedef 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.
44typedef void (*ghostty_surface_write_fn)(void* userdata,
45 const uint8_t* data,
46 size_t len);
47
48typedef enum {
49 GHOSTTY_CLIPBOARD_STANDARD,
50 GHOSTTY_CLIPBOARD_SELECTION,
51} ghostty_clipboard_e;
52
53typedef struct {
54 const char *mime;
55 const char *data;
56} ghostty_clipboard_content_s;
57
58typedef 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
64typedef enum {
65 GHOSTTY_MOUSE_RELEASE,
66 GHOSTTY_MOUSE_PRESS,
67} ghostty_input_mouse_state_e;
68
69typedef 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
84typedef 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
94typedef 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.
102typedef int ghostty_input_scroll_mods_t;
103
104typedef 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
118typedef 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
125typedef 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/
132typedef 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
327typedef 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
337typedef enum {
338 GHOSTTY_TRIGGER_PHYSICAL,
339 GHOSTTY_TRIGGER_UNICODE,
340 GHOSTTY_TRIGGER_CATCH_ALL,
341} ghostty_input_trigger_tag_e;
342
343typedef 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
350typedef 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
356typedef struct {
357 const char* action_key;
358 const char* action;
359 const char* title;
360 const char* description;
361} ghostty_command_s;
362
363typedef 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
370typedef struct {
371 ghostty_build_mode_e build_mode;
372 const char* version;
373 uintptr_t version_len;
374} ghostty_info_s;
375
376typedef struct {
377 const char* message;
378} ghostty_diagnostic_s;
379
380typedef struct {
381 const char* ptr;
382 uintptr_t len;
383 bool sentinel;
384} ghostty_string_s;
385
386typedef 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
395typedef enum {
396 GHOSTTY_POINT_ACTIVE,
397 GHOSTTY_POINT_VIEWPORT,
398 GHOSTTY_POINT_SCREEN,
399 GHOSTTY_POINT_SURFACE,
400} ghostty_point_tag_e;
401
402typedef enum {
403 GHOSTTY_POINT_COORD_EXACT,
404 GHOSTTY_POINT_COORD_TOP_LEFT,
405 GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
406} ghostty_point_coord_e;
407
408typedef 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
415typedef struct {
416 ghostty_point_s top_left;
417 ghostty_point_s bottom_right;
418 bool rectangle;
419} ghostty_selection_s;
420
421typedef struct {
422 const char* key;
423 const char* value;
424} ghostty_env_var_s;
425
426typedef struct {
427 void* nsview;
428} ghostty_platform_macos_s;
429
430typedef struct {
431 void* uiview;
432} ghostty_platform_ios_s;
433
434typedef union {
435 ghostty_platform_macos_s macos;
436 ghostty_platform_ios_s ios;
437} ghostty_platform_u;
438
439typedef 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
445typedef 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
461typedef 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
473typedef struct {
474 const char* path;
475 bool optional;
476} ghostty_config_path_s;
477
478// config.Color
479typedef struct {
480 uint8_t r;
481 uint8_t g;
482 uint8_t b;
483} ghostty_config_color_s;
484
485// config.ColorList
486typedef struct {
487 const ghostty_config_color_s* colors;
488 size_t len;
489} ghostty_config_color_list_s;
490
491// config.RepeatableCommand
492typedef struct {
493 const ghostty_command_s* commands;
494 size_t len;
495} ghostty_config_command_list_s;
496
497// config.Palette
498typedef struct {
499 ghostty_config_color_s colors[256];
500} ghostty_config_palette_s;
501
502// config.QuickTerminalSize
503typedef 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
509typedef union {
510 float percentage;
511 uint32_t pixels;
512} ghostty_quick_terminal_size_value_u;
513
514typedef struct {
515 ghostty_quick_terminal_size_tag_e tag;
516 ghostty_quick_terminal_size_value_u value;
517} ghostty_quick_terminal_size_s;
518
519typedef 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
525typedef 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
534typedef enum {
535 GHOSTTY_TARGET_APP,
536 GHOSTTY_TARGET_SURFACE,
537} ghostty_target_tag_e;
538
539typedef union {
540 ghostty_surface_t surface;
541} ghostty_target_u;
542
543typedef struct {
544 ghostty_target_tag_e tag;
545 ghostty_target_u target;
546} ghostty_target_s;
547
548// apprt.action.SplitDirection
549typedef 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
557typedef 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
567typedef enum {
568 GHOSTTY_GOTO_WINDOW_PREVIOUS,
569 GHOSTTY_GOTO_WINDOW_NEXT,
570} ghostty_action_goto_window_e;
571
572// apprt.action.ResizeSplit.Direction
573typedef 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
581typedef struct {
582 uint16_t amount;
583 ghostty_action_resize_split_direction_e direction;
584} ghostty_action_resize_split_s;
585
586// apprt.action.MoveTab
587typedef struct {
588 ssize_t amount;
589} ghostty_action_move_tab_s;
590
591// apprt.action.GotoTab
592typedef 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
599typedef 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
607typedef 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
614typedef 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
621typedef enum {
622 GHOSTTY_INSPECTOR_TOGGLE,
623 GHOSTTY_INSPECTOR_SHOW,
624 GHOSTTY_INSPECTOR_HIDE,
625} ghostty_action_inspector_e;
626
627// apprt.action.QuitTimer
628typedef enum {
629 GHOSTTY_QUIT_TIMER_START,
630 GHOSTTY_QUIT_TIMER_STOP,
631} ghostty_action_quit_timer_e;
632
633// apprt.action.Readonly
634typedef enum {
635 GHOSTTY_READONLY_OFF,
636 GHOSTTY_READONLY_ON,
637} ghostty_action_readonly_e;
638
639// apprt.action.DesktopNotification.C
640typedef struct {
641 const char* title;
642 const char* body;
643} ghostty_action_desktop_notification_s;
644
645// apprt.action.SetTitle.C
646typedef struct {
647 const char* title;
648} ghostty_action_set_title_s;
649
650// apprt.action.PromptTitle
651typedef enum {
652 GHOSTTY_PROMPT_TITLE_SURFACE,
653 GHOSTTY_PROMPT_TITLE_TAB,
654} ghostty_action_prompt_title_e;
655
656// apprt.action.Pwd.C
657typedef struct {
658 const char* pwd;
659} ghostty_action_pwd_s;
660
661// terminal.MouseShape
662typedef 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
700typedef enum {
701 GHOSTTY_MOUSE_VISIBLE,
702 GHOSTTY_MOUSE_HIDDEN,
703} ghostty_action_mouse_visibility_e;
704
705// apprt.action.MouseOverLink
706typedef struct {
707 const char* url;
708 size_t len;
709} ghostty_action_mouse_over_link_s;
710
711// apprt.action.SizeLimit
712typedef 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
720typedef struct {
721 uint32_t width;
722 uint32_t height;
723} ghostty_action_initial_size_s;
724
725// apprt.action.CellSize
726typedef struct {
727 uint32_t width;
728 uint32_t height;
729} ghostty_action_cell_size_s;
730
731// renderer.Health
732typedef enum {
733 GHOSTTY_RENDERER_HEALTH_HEALTHY,
734 GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
735} ghostty_action_renderer_health_e;
736
737// apprt.action.KeySequence
738typedef struct {
739 bool active;
740 ghostty_input_trigger_s trigger;
741} ghostty_action_key_sequence_s;
742
743// apprt.action.KeyTable.Tag
744typedef 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
751typedef 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
759typedef 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
765typedef 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
772typedef 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
780typedef struct {
781 ghostty_config_t config;
782} ghostty_action_config_change_s;
783
784// apprt.action.ReloadConfig
785typedef struct {
786 bool soft;
787} ghostty_action_reload_config_s;
788
789// apprt.action.OpenUrlKind
790typedef 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
797typedef 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
804typedef 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
811typedef struct {
812 uint32_t exit_code;
813 uint64_t timetime_ms;
814} ghostty_surface_message_childexited_s;
815
816// terminal.osc.Command.ProgressReport.State
817typedef 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
826typedef 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
834typedef 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
842typedef struct {
843 const char* needle;
844} ghostty_action_start_search_s;
845
846// apprt.action.SearchTotal
847typedef struct {
848 ssize_t total;
849} ghostty_action_search_total_s;
850
851// apprt.action.SearchSelected
852typedef struct {
853 ssize_t selected;
854} ghostty_action_search_selected_s;
855
856// terminal.Scrollbar
857typedef struct {
858 uint64_t total;
859 uint64_t offset;
860 uint64_t len;
861} ghostty_action_scrollbar_s;
862
863// apprt.Action.Key
864typedef 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
931typedef 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
971typedef struct {
972 ghostty_action_tag_e tag;
973 ghostty_action_u action;
974} ghostty_action_s;
975
976typedef void (*ghostty_runtime_wakeup_cb)(void*);
977typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
978 ghostty_clipboard_e,
979 void*);
980typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
981 void*,
982 const char*,
983 void*,
984 ghostty_clipboard_request_e);
985typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
986 ghostty_clipboard_e,
987 const ghostty_clipboard_content_s*,
988 size_t,
989 bool);
990typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
991typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
992 ghostty_target_s,
993 ghostty_action_s);
994
995typedef 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
1007typedef enum {
1008 GHOSTTY_IPC_TARGET_CLASS,
1009 GHOSTTY_IPC_TARGET_DETECT,
1010} ghostty_ipc_target_tag_e;
1011
1012typedef union {
1013 char *klass;
1014} ghostty_ipc_target_u;
1015
1016typedef 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
1022typedef struct {
1023 // This should be a null terminated list of strings.
1024 const char **arguments;
1025} ghostty_ipc_action_new_window_s;
1026
1027typedef union {
1028 ghostty_ipc_action_new_window_s new_window;
1029} ghostty_ipc_action_u;
1030
1031// apprt.ipc.Action.Key
1032typedef enum {
1033 GHOSTTY_IPC_ACTION_NEW_WINDOW,
1034} ghostty_ipc_action_tag_e;
1035
1036//-------------------------------------------------------------------
1037// Published API
1038
1039int ghostty_init(uintptr_t, char**);
1040void ghostty_cli_try_action(void);
1041ghostty_info_s ghostty_info(void);
1042const char* ghostty_translate(const char*);
1043void ghostty_string_free(ghostty_string_s);
1044
1045ghostty_config_t ghostty_config_new();
1046void ghostty_config_free(ghostty_config_t);
1047ghostty_config_t ghostty_config_clone(ghostty_config_t);
1048void ghostty_config_load_cli_args(ghostty_config_t);
1049void ghostty_config_load_file(ghostty_config_t, const char*);
1050void ghostty_config_load_default_files(ghostty_config_t);
1051void ghostty_config_load_recursive_files(ghostty_config_t);
1052void ghostty_config_finalize(ghostty_config_t);
1053bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
1054ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
1055 const char*,
1056 uintptr_t);
1057uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
1058ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
1059ghostty_string_s ghostty_config_open_path(void);
1060
1061ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
1062 ghostty_config_t);
1063void ghostty_app_free(ghostty_app_t);
1064void ghostty_app_tick(ghostty_app_t);
1065void* ghostty_app_userdata(ghostty_app_t);
1066void ghostty_app_set_focus(ghostty_app_t, bool);
1067bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
1068bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
1069void ghostty_app_keyboard_changed(ghostty_app_t);
1070void ghostty_app_open_config(ghostty_app_t);
1071void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
1072bool ghostty_app_needs_confirm_quit(ghostty_app_t);
1073bool ghostty_app_has_global_keybinds(ghostty_app_t);
1074void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
1075
1076ghostty_surface_config_s ghostty_surface_config_new();
1077
1078ghostty_surface_t ghostty_surface_new(ghostty_app_t,
1079 const ghostty_surface_config_s*);
1080void ghostty_surface_free(ghostty_surface_t);
1081void* ghostty_surface_userdata(ghostty_surface_t);
1082ghostty_app_t ghostty_surface_app(ghostty_surface_t);
1083ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
1084void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
1085bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
1086bool ghostty_surface_process_exited(ghostty_surface_t);
1087void ghostty_surface_refresh(ghostty_surface_t);
1088void ghostty_surface_draw(ghostty_surface_t);
1089// Custom I/O API - feed output into the terminal and receive writes.
1090void ghostty_surface_feed_data(ghostty_surface_t, const uint8_t*, size_t);
1091void ghostty_surface_set_write_callback(ghostty_surface_t,
1092 ghostty_surface_write_fn,
1093 void*);
1094void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
1095void ghostty_surface_set_focus(ghostty_surface_t, bool);
1096void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
1097void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
1098ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
1099void ghostty_surface_set_color_scheme(ghostty_surface_t,
1100 ghostty_color_scheme_e);
1101ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
1102 ghostty_input_mods_e);
1103bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
1104bool ghostty_surface_key_is_binding(ghostty_surface_t,
1105 ghostty_input_key_s,
1106 ghostty_binding_flags_e*);
1107void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
1108void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
1109bool ghostty_surface_mouse_captured(ghostty_surface_t);
1110bool ghostty_surface_mouse_button(ghostty_surface_t,
1111 ghostty_input_mouse_state_e,
1112 ghostty_input_mouse_button_e,
1113 ghostty_input_mods_e);
1114void ghostty_surface_mouse_pos(ghostty_surface_t,
1115 double,
1116 double,
1117 ghostty_input_mods_e);
1118void ghostty_surface_mouse_scroll(ghostty_surface_t,
1119 double,
1120 double,
1121 ghostty_input_scroll_mods_t);
1122void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
1123void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
1124void ghostty_surface_request_close(ghostty_surface_t);
1125void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
1126void ghostty_surface_split_focus(ghostty_surface_t,
1127 ghostty_action_goto_split_e);
1128void ghostty_surface_split_resize(ghostty_surface_t,
1129 ghostty_action_resize_split_direction_e,
1130 uint16_t);
1131void ghostty_surface_split_equalize(ghostty_surface_t);
1132bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
1133void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
1134 const char*,
1135 void*,
1136 bool);
1137bool ghostty_surface_has_selection(ghostty_surface_t);
1138bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
1139bool ghostty_surface_read_text(ghostty_surface_t,
1140 ghostty_selection_s,
1141 ghostty_text_s*);
1142void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
1143
1144#ifdef __APPLE__
1145void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
1146void* ghostty_surface_quicklook_font(ghostty_surface_t);
1147bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
1148#endif
1149
1150ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
1151void ghostty_inspector_free(ghostty_surface_t);
1152void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
1153void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
1154void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
1155void ghostty_inspector_mouse_button(ghostty_inspector_t,
1156 ghostty_input_mouse_state_e,
1157 ghostty_input_mouse_button_e,
1158 ghostty_input_mods_e);
1159void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
1160void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
1161 double,
1162 double,
1163 ghostty_input_scroll_mods_t);
1164void ghostty_inspector_key(ghostty_inspector_t,
1165 ghostty_input_action_e,
1166 ghostty_input_key_e,
1167 ghostty_input_mods_e);
1168void ghostty_inspector_text(ghostty_inspector_t, const char*);
1169
1170#ifdef __APPLE__
1171bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
1172void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
1173bool 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.
1178void ghostty_set_window_background_blur(ghostty_app_t, void*);
1179
1180// Benchmark API, if available.
1181bool 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
72extern "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 */
83typedef 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 */
179typedef 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
13extern "C" {
14#endif
15
16/**
17 * RGB color value.
18 *
19 * @ingroup sgr
20 */
21typedef 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 */
32typedef 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 */
87void 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 */
24typedef 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 */
35typedef 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 */
66typedef 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 */
85typedef 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 */
121GhosttyResult 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 */
133void 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 */
152void 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 */
219GhosttyResult 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 */
24typedef struct GhosttyKeyEvent *GhosttyKeyEvent;
25
26/**
27 * Keyboard input event types.
28 *
29 * @ingroup key
30 */
31typedef 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 */
56typedef 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 */
106typedef 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 */
313GhosttyResult 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 */
325void 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 */
335void 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 */
345GhosttyKeyAction 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 */
355void 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 */
365GhosttyKey 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 */
375void 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 */
385GhosttyMods 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 */
395void 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 */
405GhosttyMods 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 */
415void 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 */
425bool 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 */
439void 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 */
452const 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 */
462void 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 */
472uint32_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 */
24typedef 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 */
34typedef 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 */
62typedef 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 */
96typedef 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 */
126GhosttyResult 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 */
138void 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 */
151void 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 */
168void 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 */
198GhosttyOscCommand 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 */
212GhosttyOscCommandType 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 */
229bool 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
49extern "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 */
67bool 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 */
13typedef 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
82extern "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 */
93typedef 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 */
103typedef 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 */
143typedef 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 */
160typedef 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 */
176typedef 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 */
204typedef 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 */
223GhosttyResult 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 */
237void 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 */
250void 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 */
282GhosttyResult 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 */
300bool 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 */
315size_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 */
331size_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 */
346GhosttySgrAttributeTag 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 */
360GhosttySgrAttributeValue* 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 */
374GhosttySgrAttribute* 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 */
385void 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 */
77void** 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 */
85void 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 */
94uint8_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 */
103void 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 */
112uint16_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 */
121void 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 */
129uint8_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 */
137void 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 */
145size_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 */
153void 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.
4module 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
12extern "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
29typedef void* ghostty_app_t;
30typedef void* ghostty_config_t;
31typedef void* ghostty_surface_t;
32typedef 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.
37typedef 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.
44typedef void (*ghostty_surface_write_fn)(void* userdata,
45 const uint8_t* data,
46 size_t len);
47
48typedef enum {
49 GHOSTTY_CLIPBOARD_STANDARD,
50 GHOSTTY_CLIPBOARD_SELECTION,
51} ghostty_clipboard_e;
52
53typedef struct {
54 const char *mime;
55 const char *data;
56} ghostty_clipboard_content_s;
57
58typedef 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
64typedef enum {
65 GHOSTTY_MOUSE_RELEASE,
66 GHOSTTY_MOUSE_PRESS,
67} ghostty_input_mouse_state_e;
68
69typedef 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
84typedef 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
94typedef 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.
102typedef int ghostty_input_scroll_mods_t;
103
104typedef 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
118typedef 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
125typedef 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/
132typedef 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
327typedef 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
337typedef enum {
338 GHOSTTY_TRIGGER_PHYSICAL,
339 GHOSTTY_TRIGGER_UNICODE,
340 GHOSTTY_TRIGGER_CATCH_ALL,
341} ghostty_input_trigger_tag_e;
342
343typedef 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
350typedef 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
356typedef struct {
357 const char* action_key;
358 const char* action;
359 const char* title;
360 const char* description;
361} ghostty_command_s;
362
363typedef 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
370typedef struct {
371 ghostty_build_mode_e build_mode;
372 const char* version;
373 uintptr_t version_len;
374} ghostty_info_s;
375
376typedef struct {
377 const char* message;
378} ghostty_diagnostic_s;
379
380typedef struct {
381 const char* ptr;
382 uintptr_t len;
383 bool sentinel;
384} ghostty_string_s;
385
386typedef 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
395typedef enum {
396 GHOSTTY_POINT_ACTIVE,
397 GHOSTTY_POINT_VIEWPORT,
398 GHOSTTY_POINT_SCREEN,
399 GHOSTTY_POINT_SURFACE,
400} ghostty_point_tag_e;
401
402typedef enum {
403 GHOSTTY_POINT_COORD_EXACT,
404 GHOSTTY_POINT_COORD_TOP_LEFT,
405 GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
406} ghostty_point_coord_e;
407
408typedef 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
415typedef struct {
416 ghostty_point_s top_left;
417 ghostty_point_s bottom_right;
418 bool rectangle;
419} ghostty_selection_s;
420
421typedef struct {
422 const char* key;
423 const char* value;
424} ghostty_env_var_s;
425
426typedef struct {
427 void* nsview;
428} ghostty_platform_macos_s;
429
430typedef struct {
431 void* uiview;
432} ghostty_platform_ios_s;
433
434typedef union {
435 ghostty_platform_macos_s macos;
436 ghostty_platform_ios_s ios;
437} ghostty_platform_u;
438
439typedef 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
445typedef 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
461typedef 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
473typedef struct {
474 const char* path;
475 bool optional;
476} ghostty_config_path_s;
477
478// config.Color
479typedef struct {
480 uint8_t r;
481 uint8_t g;
482 uint8_t b;
483} ghostty_config_color_s;
484
485// config.ColorList
486typedef struct {
487 const ghostty_config_color_s* colors;
488 size_t len;
489} ghostty_config_color_list_s;
490
491// config.RepeatableCommand
492typedef struct {
493 const ghostty_command_s* commands;
494 size_t len;
495} ghostty_config_command_list_s;
496
497// config.Palette
498typedef struct {
499 ghostty_config_color_s colors[256];
500} ghostty_config_palette_s;
501
502// config.QuickTerminalSize
503typedef 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
509typedef union {
510 float percentage;
511 uint32_t pixels;
512} ghostty_quick_terminal_size_value_u;
513
514typedef struct {
515 ghostty_quick_terminal_size_tag_e tag;
516 ghostty_quick_terminal_size_value_u value;
517} ghostty_quick_terminal_size_s;
518
519typedef 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
525typedef 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
534typedef enum {
535 GHOSTTY_TARGET_APP,
536 GHOSTTY_TARGET_SURFACE,
537} ghostty_target_tag_e;
538
539typedef union {
540 ghostty_surface_t surface;
541} ghostty_target_u;
542
543typedef struct {
544 ghostty_target_tag_e tag;
545 ghostty_target_u target;
546} ghostty_target_s;
547
548// apprt.action.SplitDirection
549typedef 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
557typedef 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
567typedef enum {
568 GHOSTTY_GOTO_WINDOW_PREVIOUS,
569 GHOSTTY_GOTO_WINDOW_NEXT,
570} ghostty_action_goto_window_e;
571
572// apprt.action.ResizeSplit.Direction
573typedef 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
581typedef struct {
582 uint16_t amount;
583 ghostty_action_resize_split_direction_e direction;
584} ghostty_action_resize_split_s;
585
586// apprt.action.MoveTab
587typedef struct {
588 ssize_t amount;
589} ghostty_action_move_tab_s;
590
591// apprt.action.GotoTab
592typedef 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
599typedef 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
607typedef 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
614typedef 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
621typedef enum {
622 GHOSTTY_INSPECTOR_TOGGLE,
623 GHOSTTY_INSPECTOR_SHOW,
624 GHOSTTY_INSPECTOR_HIDE,
625} ghostty_action_inspector_e;
626
627// apprt.action.QuitTimer
628typedef enum {
629 GHOSTTY_QUIT_TIMER_START,
630 GHOSTTY_QUIT_TIMER_STOP,
631} ghostty_action_quit_timer_e;
632
633// apprt.action.Readonly
634typedef enum {
635 GHOSTTY_READONLY_OFF,
636 GHOSTTY_READONLY_ON,
637} ghostty_action_readonly_e;
638
639// apprt.action.DesktopNotification.C
640typedef struct {
641 const char* title;
642 const char* body;
643} ghostty_action_desktop_notification_s;
644
645// apprt.action.SetTitle.C
646typedef struct {
647 const char* title;
648} ghostty_action_set_title_s;
649
650// apprt.action.PromptTitle
651typedef enum {
652 GHOSTTY_PROMPT_TITLE_SURFACE,
653 GHOSTTY_PROMPT_TITLE_TAB,
654} ghostty_action_prompt_title_e;
655
656// apprt.action.Pwd.C
657typedef struct {
658 const char* pwd;
659} ghostty_action_pwd_s;
660
661// terminal.MouseShape
662typedef 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
700typedef enum {
701 GHOSTTY_MOUSE_VISIBLE,
702 GHOSTTY_MOUSE_HIDDEN,
703} ghostty_action_mouse_visibility_e;
704
705// apprt.action.MouseOverLink
706typedef struct {
707 const char* url;
708 size_t len;
709} ghostty_action_mouse_over_link_s;
710
711// apprt.action.SizeLimit
712typedef 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
720typedef struct {
721 uint32_t width;
722 uint32_t height;
723} ghostty_action_initial_size_s;
724
725// apprt.action.CellSize
726typedef struct {
727 uint32_t width;
728 uint32_t height;
729} ghostty_action_cell_size_s;
730
731// renderer.Health
732typedef enum {
733 GHOSTTY_RENDERER_HEALTH_HEALTHY,
734 GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
735} ghostty_action_renderer_health_e;
736
737// apprt.action.KeySequence
738typedef struct {
739 bool active;
740 ghostty_input_trigger_s trigger;
741} ghostty_action_key_sequence_s;
742
743// apprt.action.KeyTable.Tag
744typedef 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
751typedef 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
759typedef 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
765typedef 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
772typedef 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
780typedef struct {
781 ghostty_config_t config;
782} ghostty_action_config_change_s;
783
784// apprt.action.ReloadConfig
785typedef struct {
786 bool soft;
787} ghostty_action_reload_config_s;
788
789// apprt.action.OpenUrlKind
790typedef 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
797typedef 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
804typedef 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
811typedef struct {
812 uint32_t exit_code;
813 uint64_t timetime_ms;
814} ghostty_surface_message_childexited_s;
815
816// terminal.osc.Command.ProgressReport.State
817typedef 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
826typedef 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
834typedef 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
842typedef struct {
843 const char* needle;
844} ghostty_action_start_search_s;
845
846// apprt.action.SearchTotal
847typedef struct {
848 ssize_t total;
849} ghostty_action_search_total_s;
850
851// apprt.action.SearchSelected
852typedef struct {
853 ssize_t selected;
854} ghostty_action_search_selected_s;
855
856// terminal.Scrollbar
857typedef struct {
858 uint64_t total;
859 uint64_t offset;
860 uint64_t len;
861} ghostty_action_scrollbar_s;
862
863// apprt.Action.Key
864typedef 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
931typedef 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
971typedef struct {
972 ghostty_action_tag_e tag;
973 ghostty_action_u action;
974} ghostty_action_s;
975
976typedef void (*ghostty_runtime_wakeup_cb)(void*);
977typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
978 ghostty_clipboard_e,
979 void*);
980typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
981 void*,
982 const char*,
983 void*,
984 ghostty_clipboard_request_e);
985typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
986 ghostty_clipboard_e,
987 const ghostty_clipboard_content_s*,
988 size_t,
989 bool);
990typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
991typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
992 ghostty_target_s,
993 ghostty_action_s);
994
995typedef 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
1007typedef enum {
1008 GHOSTTY_IPC_TARGET_CLASS,
1009 GHOSTTY_IPC_TARGET_DETECT,
1010} ghostty_ipc_target_tag_e;
1011
1012typedef union {
1013 char *klass;
1014} ghostty_ipc_target_u;
1015
1016typedef 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
1022typedef struct {
1023 // This should be a null terminated list of strings.
1024 const char **arguments;
1025} ghostty_ipc_action_new_window_s;
1026
1027typedef union {
1028 ghostty_ipc_action_new_window_s new_window;
1029} ghostty_ipc_action_u;
1030
1031// apprt.ipc.Action.Key
1032typedef enum {
1033 GHOSTTY_IPC_ACTION_NEW_WINDOW,
1034} ghostty_ipc_action_tag_e;
1035
1036//-------------------------------------------------------------------
1037// Published API
1038
1039int ghostty_init(uintptr_t, char**);
1040void ghostty_cli_try_action(void);
1041ghostty_info_s ghostty_info(void);
1042const char* ghostty_translate(const char*);
1043void ghostty_string_free(ghostty_string_s);
1044
1045ghostty_config_t ghostty_config_new();
1046void ghostty_config_free(ghostty_config_t);
1047ghostty_config_t ghostty_config_clone(ghostty_config_t);
1048void ghostty_config_load_cli_args(ghostty_config_t);
1049void ghostty_config_load_file(ghostty_config_t, const char*);
1050void ghostty_config_load_default_files(ghostty_config_t);
1051void ghostty_config_load_recursive_files(ghostty_config_t);
1052void ghostty_config_finalize(ghostty_config_t);
1053bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
1054ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
1055 const char*,
1056 uintptr_t);
1057uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
1058ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
1059ghostty_string_s ghostty_config_open_path(void);
1060
1061ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
1062 ghostty_config_t);
1063void ghostty_app_free(ghostty_app_t);
1064void ghostty_app_tick(ghostty_app_t);
1065void* ghostty_app_userdata(ghostty_app_t);
1066void ghostty_app_set_focus(ghostty_app_t, bool);
1067bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
1068bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
1069void ghostty_app_keyboard_changed(ghostty_app_t);
1070void ghostty_app_open_config(ghostty_app_t);
1071void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
1072bool ghostty_app_needs_confirm_quit(ghostty_app_t);
1073bool ghostty_app_has_global_keybinds(ghostty_app_t);
1074void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
1075
1076ghostty_surface_config_s ghostty_surface_config_new();
1077
1078ghostty_surface_t ghostty_surface_new(ghostty_app_t,
1079 const ghostty_surface_config_s*);
1080void ghostty_surface_free(ghostty_surface_t);
1081void* ghostty_surface_userdata(ghostty_surface_t);
1082ghostty_app_t ghostty_surface_app(ghostty_surface_t);
1083ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
1084void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
1085bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
1086bool ghostty_surface_process_exited(ghostty_surface_t);
1087void ghostty_surface_refresh(ghostty_surface_t);
1088void ghostty_surface_draw(ghostty_surface_t);
1089// Custom I/O API - feed output into the terminal and receive writes.
1090void ghostty_surface_feed_data(ghostty_surface_t, const uint8_t*, size_t);
1091void ghostty_surface_set_write_callback(ghostty_surface_t,
1092 ghostty_surface_write_fn,
1093 void*);
1094void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
1095void ghostty_surface_set_focus(ghostty_surface_t, bool);
1096void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
1097void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
1098ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
1099void ghostty_surface_set_color_scheme(ghostty_surface_t,
1100 ghostty_color_scheme_e);
1101ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
1102 ghostty_input_mods_e);
1103bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
1104bool ghostty_surface_key_is_binding(ghostty_surface_t,
1105 ghostty_input_key_s,
1106 ghostty_binding_flags_e*);
1107void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
1108void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
1109bool ghostty_surface_mouse_captured(ghostty_surface_t);
1110bool ghostty_surface_mouse_button(ghostty_surface_t,
1111 ghostty_input_mouse_state_e,
1112 ghostty_input_mouse_button_e,
1113 ghostty_input_mods_e);
1114void ghostty_surface_mouse_pos(ghostty_surface_t,
1115 double,
1116 double,
1117 ghostty_input_mods_e);
1118void ghostty_surface_mouse_scroll(ghostty_surface_t,
1119 double,
1120 double,
1121 ghostty_input_scroll_mods_t);
1122void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
1123void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
1124void ghostty_surface_request_close(ghostty_surface_t);
1125void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
1126void ghostty_surface_split_focus(ghostty_surface_t,
1127 ghostty_action_goto_split_e);
1128void ghostty_surface_split_resize(ghostty_surface_t,
1129 ghostty_action_resize_split_direction_e,
1130 uint16_t);
1131void ghostty_surface_split_equalize(ghostty_surface_t);
1132bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
1133void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
1134 const char*,
1135 void*,
1136 bool);
1137bool ghostty_surface_has_selection(ghostty_surface_t);
1138bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
1139bool ghostty_surface_read_text(ghostty_surface_t,
1140 ghostty_selection_s,
1141 ghostty_text_s*);
1142void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
1143
1144#ifdef __APPLE__
1145void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
1146void* ghostty_surface_quicklook_font(ghostty_surface_t);
1147bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
1148#endif
1149
1150ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
1151void ghostty_inspector_free(ghostty_surface_t);
1152void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
1153void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
1154void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
1155void ghostty_inspector_mouse_button(ghostty_inspector_t,
1156 ghostty_input_mouse_state_e,
1157 ghostty_input_mouse_button_e,
1158 ghostty_input_mods_e);
1159void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
1160void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
1161 double,
1162 double,
1163 ghostty_input_scroll_mods_t);
1164void ghostty_inspector_key(ghostty_inspector_t,
1165 ghostty_input_action_e,
1166 ghostty_input_key_e,
1167 ghostty_input_mods_e);
1168void ghostty_inspector_text(ghostty_inspector_t, const char*);
1169
1170#ifdef __APPLE__
1171bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
1172void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
1173bool 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.
1178void ghostty_set_window_background_blur(ghostty_app_t, void*);
1179
1180// Benchmark API, if available.
1181bool 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
72extern "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 */
83typedef 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 */
179typedef 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
13extern "C" {
14#endif
15
16/**
17 * RGB color value.
18 *
19 * @ingroup sgr
20 */
21typedef 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 */
32typedef 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 */
87void 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 */
24typedef 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 */
35typedef 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 */
66typedef 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 */
85typedef 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 */
121GhosttyResult 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 */
133void 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 */
152void 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 */
219GhosttyResult 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 */
24typedef struct GhosttyKeyEvent *GhosttyKeyEvent;
25
26/**
27 * Keyboard input event types.
28 *
29 * @ingroup key
30 */
31typedef 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 */
56typedef 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 */
106typedef 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 */
313GhosttyResult 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 */
325void 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 */
335void 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 */
345GhosttyKeyAction 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 */
355void 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 */
365GhosttyKey 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 */
375void 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 */
385GhosttyMods 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 */
395void 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 */
405GhosttyMods 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 */
415void 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 */
425bool 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 */
439void 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 */
452const 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 */
462void 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 */
472uint32_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 */
24typedef 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 */
34typedef 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 */
62typedef 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 */
96typedef 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 */
126GhosttyResult 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 */
138void 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 */
151void 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 */
168void 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 */
198GhosttyOscCommand 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 */
212GhosttyOscCommandType 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 */
229bool 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
49extern "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 */
67bool 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 */
13typedef 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
82extern "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 */
93typedef 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 */
103typedef 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 */
143typedef 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 */
160typedef 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 */
176typedef 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 */
204typedef 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 */
223GhosttyResult 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 */
237void 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 */
250void 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 */
282GhosttyResult 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 */
300bool 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 */
315size_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 */
331size_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 */
346GhosttySgrAttributeTag 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 */
360GhosttySgrAttributeValue* 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 */
374GhosttySgrAttribute* 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 */
385void 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 */
77void** 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 */
85void 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 */
94uint8_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 */
103void 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 */
112uint16_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 */
121void 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 */
129uint8_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 */
137void 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 */
145size_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 */
153void 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.
4module 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 @@
1268a0a9d761fb19673f05d28042488e2002300f2
apps/mobile/modules/t3-terminal/android/build.gradle+19−0
@@ -0,0 +1,19 @@
1apply plugin: 'com.android.library'
2apply plugin: 'org.jetbrains.kotlin.android'
3
4group = 'com.t3tools.terminal'
5version = '0.0.0'
6
7android {
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
17dependencies {
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 @@
1package expo.modules.t3terminal
2
3import expo.modules.kotlin.modules.Module
4import expo.modules.kotlin.modules.ModuleDefinition
5
6class 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 @@
1package expo.modules.t3terminal
2
3import android.content.Context
4import android.graphics.Color
5import android.graphics.Typeface
6import android.view.View
7import android.view.ViewGroup
8import android.view.inputmethod.InputMethodManager
9import android.view.inputmethod.EditorInfo
10import android.widget.EditText
11import android.widget.LinearLayout
12import android.widget.ScrollView
13import android.widget.TextView
14import androidx.core.widget.doAfterTextChanged
15import expo.modules.kotlin.AppContext
16import expo.modules.kotlin.views.ExpoView
17import expo.modules.kotlin.viewevent.EventDispatcher
18import kotlin.math.max
19import kotlin.math.min
20
21class 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 @@
1import ExpoModulesCore
2
3public 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 @@
1import ExpoModulesCore
2import Foundation
3import GhosttyKit
4import QuartzCore
5import UIKit
6
7private 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
25private final class TerminalInputField: UITextField {
26 var onDeleteBackward: (() -> Void)?
27
28 override func deleteBackward() {
29 onDeleteBackward?()
30 super.deleteBackward()
31 }
32}
33
34private 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
52private 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
65public 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
30 "config:dev": "APP_VARIANT=development expo config",30 "config:dev": "APP_VARIANT=development expo config",
31 "config:preview": "APP_VARIANT=preview expo config",31 "config:preview": "APP_VARIANT=preview expo config",
32 "config:prod": "APP_VARIANT=production expo config",32 "config:prod": "APP_VARIANT=production expo config",
33 "test": "vitest run",
33 "typecheck": "tsc --noEmit"34 "typecheck": "tsc --noEmit"
34 },35 },
35 "dependencies": {36 "dependencies": {
@@ -45,11 +46,13 @@ · 9 unmodified lines
45 "@shikijs/themes": "3.23.0",46 "@shikijs/themes": "3.23.0",
46 "@t3tools/client-runtime": "workspace:*",47 "@t3tools/client-runtime": "workspace:*",
47 "@t3tools/contracts": "workspace:*",48 "@t3tools/contracts": "workspace:*",
49 "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal",
48 "@t3tools/shared": "workspace:*",50 "@t3tools/shared": "workspace:*",
49 "clsx": "^2.1.1",51 "clsx": "^2.1.1",
50 "diff": "8.0.3",52 "diff": "8.0.3",
51 "effect": "catalog:",53 "effect": "catalog:",
52 "expo": "~55.0.14",54 "expo": "~55.0.14",
55 "expo-build-properties": "~55.0.13",
53 "expo-camera": "~55.0.15",56 "expo-camera": "~55.0.15",
54 "expo-clipboard": "~55.0.9",57 "expo-clipboard": "~55.0.9",
55 "expo-constants": "^55.0.9",58 "expo-constants": "^55.0.9",
@@ -60,6 +63,7 @@ · 4 unmodified lines
60 "expo-haptics": "^55.0.9",63 "expo-haptics": "^55.0.9",
61 "expo-image-picker": "~55.0.14",64 "expo-image-picker": "~55.0.14",
62 "expo-linking": "~55.0.12",65 "expo-linking": "~55.0.12",
66 "expo-modules-core": "~55.0.22",
63 "expo-paste-input": "^0.1.15",67 "expo-paste-input": "^0.1.15",
64 "expo-router": "~55.0.12",68 "expo-router": "~55.0.12",
65 "expo-secure-store": "~55.0.9",69 "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
65 sheetGrabberVisible: true,65 sheetGrabberVisible: true,
66 }}66 }}
67 />67 />
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 />
68 </Stack>78 </Stack>
69 );79 );
70}80}
apps/mobile/src/app/threads/[environmentId]/[threadId]/terminal.tsx+5−0
@@ -0,0 +1,5 @@
1import { ThreadTerminalRouteScreen } from "../../../../features/threads/ThreadTerminalRouteScreen";
2
3export default function ThreadTerminalRoute() {
4 return <ThreadTerminalRouteScreen />;
5}
apps/mobile/src/features/threads/ThreadGitControls.tsx+63−1
@@ -1,4 +1,4 @@
1import type { GitRunStackedActionResult, GitStatusResult } from "@t3tools/contracts";1import type { GitRunStackedActionResult, GitStatusResult, ProjectScript } from "@t3tools/contracts";
2import {2import {
3 type GitActionRequestInput,3 type GitActionRequestInput,
4 requiresDefaultBranchConfirmation,4 requiresDefaultBranchConfirmation,
@@ -9,6 +9,14 @@ import Stack from "expo-router/stack"; · 4 unmodified lines
9import { useCallback, useMemo } from "react";9import { useCallback, useMemo } from "react";
10import { Alert, Linking } from "react-native";10import { Alert, Linking } from "react-native";
11import { buildThreadReviewRoutePath } from "../../lib/routes";11import { buildThreadReviewRoutePath } from "../../lib/routes";
12import {
13 basename,
14 getTerminalLabel,
15 getTerminalStatusLabel,
16 projectScriptMenuIcon,
17 projectScriptMenuLabel,
18 type TerminalMenuSession,
19} from "./terminalMenu";
12 20
13function truncateMiddle(value: string, maxLength: number): string {21function truncateMiddle(value: string, maxLength: number): string {
14 if (value.length <= maxLength) {22 if (value.length <= maxLength) {
@@ -55,6 +63,12 @@ export function ThreadGitControls(props: { · 40 unmodified lines
55 readonly currentBranch: string | null;63 readonly currentBranch: string | null;
56 readonly gitStatus: GitStatusResult | null;64 readonly gitStatus: GitStatusResult | null;
57 readonly gitOperationLabel: string | null;65 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>;
58 readonly onPull: () => Promise<void>;72 readonly onPull: () => Promise<void>;
59 readonly onRunAction: (input: GitActionRequestInput) => Promise<GitRunStackedActionResult | null>;73 readonly onRunAction: (input: GitActionRequestInput) => Promise<GitRunStackedActionResult | null>;
60}) {74}) {
@@ -167,6 +181,54 @@ export function ThreadGitControls(props: { · 106 unmodified lines
167 181
168 return (182 return (
169 <Stack.Toolbar placement="right">183 <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>
170 <Stack.Toolbar.Menu icon="point.topleft.down.curvedto.point.bottomright.up">232 <Stack.Toolbar.Menu icon="point.topleft.down.curvedto.point.bottomright.up">
171 <Stack.Toolbar.MenuAction233 <Stack.Toolbar.MenuAction
172 icon="point.topleft.down.curvedto.point.bottomright.up"234 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
3import * as Arr from "effect/Array";3import * as Arr from "effect/Array";
4import * as Option from "effect/Option";4import * as Option from "effect/Option";
5import { pipe } from "effect/Function";5import { pipe } from "effect/Function";
6import type { ProjectScript } from "@t3tools/contracts";
7import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
6import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native";8import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native";
7import { useThemeColor } from "../../lib/useThemeColor";9import { useThemeColor } from "../../lib/useThemeColor";
8import { useGitStatus, gitStatusManager } from "../../state/use-git-status";10import { useGitStatus, gitStatusManager } from "../../state/use-git-status";
@@ -10,24 +12,33 @@ import { dismissGitActionResult, useGitActionProgress } from "../../state/use-gi · 1 unmodified line
10 12
11import { EmptyState } from "../../components/EmptyState";13import { EmptyState } from "../../components/EmptyState";
12import { LoadingScreen } from "../../components/LoadingScreen";14import { LoadingScreen } from "../../components/LoadingScreen";
13import { buildThreadRoutePath } from "../../lib/routes";15import { buildThreadRoutePath, buildThreadTerminalRoutePath } from "../../lib/routes";
14import { scopedThreadKey } from "../../lib/scopedEntities";16import { scopedThreadKey } from "../../lib/scopedEntities";
15import { connectionTone } from "../connection/connectionTone";17import { connectionTone } from "../connection/connectionTone";
16 18
17import { useRemoteCatalog } from "../../state/use-remote-catalog";19import { useRemoteCatalog } from "../../state/use-remote-catalog";
18import {20import {
21 getEnvironmentClient,
19 useRemoteConnectionStatus,22 useRemoteConnectionStatus,
20 useRemoteEnvironmentState,23 useRemoteEnvironmentState,
21} from "../../state/use-remote-environment-registry";24} from "../../state/use-remote-environment-registry";
25import { terminalSessionManager, useKnownTerminalSessions } from "../../state/use-terminal-session";
22import { useSelectedThreadDetail } from "../../state/use-thread-detail";26import { useSelectedThreadDetail } from "../../state/use-thread-detail";
23import { useThreadSelection } from "../../state/use-thread-selection";27import { useThreadSelection } from "../../state/use-thread-selection";
24import { GitActionProgressOverlay } from "./GitActionProgressOverlay";28import { GitActionProgressOverlay } from "./GitActionProgressOverlay";
29import {
30 buildTerminalMenuSessions,
31 nextTerminalId,
32 resolveProjectScriptTerminalId,
33} from "./terminalMenu";
34import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext";
25import { ThreadDetailScreen } from "./ThreadDetailScreen";35import { ThreadDetailScreen } from "./ThreadDetailScreen";
26import { ThreadGitControls } from "./ThreadGitControls";36import { ThreadGitControls } from "./ThreadGitControls";
27import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer";37import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer";
28import { useSelectedThreadCommands } from "./use-selected-thread-commands";38import { useSelectedThreadCommands } from "./use-selected-thread-commands";
29import { useSelectedThreadGitActions } from "./use-selected-thread-git-actions";39import { useSelectedThreadGitActions } from "./use-selected-thread-git-actions";
30import { useSelectedThreadGitState } from "./use-selected-thread-git-state";40import { useSelectedThreadGitState } from "./use-selected-thread-git-state";
41import { useSelectedThreadWorktree } from "./use-selected-thread-worktree";
31import { useThreadComposerState } from "./use-thread-composer-state";42import { useThreadComposerState } from "./use-thread-composer-state";
32 43
33function firstRouteParam(value: string | string[] | undefined): string | null {44function firstRouteParam(value: string | string[] | undefined): string | null {
@@ -47,6 +58,7 @@ export function ThreadRouteScreen() { · 13 unmodified lines
47 const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } =58 const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } =
48 useThreadSelection();59 useThreadSelection();
49 const selectedThreadDetail = useSelectedThreadDetail();60 const selectedThreadDetail = useSelectedThreadDetail();
61 const { selectedThreadCwd } = useSelectedThreadWorktree();
50 const composer = useThreadComposerState();62 const composer = useThreadComposerState();
51 const gitState = useSelectedThreadGitState();63 const gitState = useSelectedThreadGitState();
52 const gitActions = useSelectedThreadGitActions();64 const gitActions = useSelectedThreadGitActions();
@@ -80,20 +92,29 @@ export function ThreadRouteScreen() { · 27 unmodified lines
80 /* ─── Git status for native header trigger ───────────────────────── */92 /* ─── Git status for native header trigger ───────────────────────── */
81 const gitStatus = useGitStatus({93 const gitStatus = useGitStatus({
82 environmentId: selectedThread?.environmentId ?? "",94 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,
84 });100 });
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;
85 110
86 /* ─── Git action progress (for overlay banner) ──────────────────── */111 /* ─── Git action progress (for overlay banner) ──────────────────── */
87 const gitActionProgressTarget = useMemo(112 const gitActionProgressTarget = useMemo(
88 () => ({113 () => ({
89 environmentId: selectedThread?.environmentId ?? null,114 environmentId: selectedThread?.environmentId ?? null,
90 cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,115 cwd: selectedThreadCwd,
91 }),116 }),
92 [117 [selectedThread?.environmentId, selectedThreadCwd],
93 selectedThread?.environmentId,
94 selectedThread?.worktreePath,
95 selectedThreadProject?.workspaceRoot,
96 ],
97 );118 );
98 const gitActionProgress = useGitActionProgress(gitActionProgressTarget);119 const gitActionProgress = useGitActionProgress(gitActionProgressTarget);
99 120
@@ -101,9 +122,9 @@ export function ThreadRouteScreen() { · 1 unmodified line
101 if (!selectedThread) return;122 if (!selectedThread) return;
102 await gitStatusManager.refresh({123 await gitStatusManager.refresh({
103 environmentId: selectedThread.environmentId,124 environmentId: selectedThread.environmentId,
104 cwd: selectedThread.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,125 cwd: selectedThreadCwd,
105 });126 });
106 }, [selectedThread, selectedThreadProject?.workspaceRoot]);127 }, [selectedThread, selectedThreadCwd]);
107 128
108 /** Wraps thread refresh + git status refresh for pull-to-refresh */129 /** Wraps thread refresh + git status refresh for pull-to-refresh */
109 const handleRefreshAll = useCallback(async () => {130 const handleRefreshAll = useCallback(async () => {
@@ -119,6 +140,83 @@ export function ThreadRouteScreen() { · 9 unmodified lines
119 void router.push("/connections");140 void router.push("/connections");
120 }, [router]);141 }, [router]);
121 142
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
122 if (!environmentId || !threadId) {220 if (!environmentId || !threadId) {
123 return <LoadingScreen message="Opening thread…" />;221 return <LoadingScreen message="Opening thread…" />;
124 }222 }
@@ -223,6 +321,12 @@ export function ThreadRouteScreen() { · 98 unmodified lines
223 currentBranch={selectedThreadDetail.branch}321 currentBranch={selectedThreadDetail.branch}
224 gitStatus={gitStatus.data}322 gitStatus={gitStatus.data}
225 gitOperationLabel={gitState.gitOperationLabel}323 gitOperationLabel={gitState.gitOperationLabel}
324 canOpenTerminal={Boolean(selectedThreadProject?.workspaceRoot)}
325 projectScripts={selectedThreadProject?.scripts ?? []}
326 terminalSessions={terminalMenuSessions}
327 onOpenTerminal={handleOpenTerminal}
328 onOpenNewTerminal={handleOpenNewTerminal}
329 onRunProjectScript={handleRunProjectScript}
226 onPull={gitActions.onPullSelectedThreadBranch}330 onPull={gitActions.onPullSelectedThreadBranch}
227 onRunAction={gitActions.onRunSelectedThreadGitAction}331 onRunAction={gitActions.onRunSelectedThreadGitAction}
228 />332 />
apps/mobile/src/features/threads/ThreadTerminalPanel.tsx+170−0
@@ -0,0 +1,170 @@
1import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts";
2import { SymbolView } from "expo-symbols";
3import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4import { Pressable, View } from "react-native";
5
6import { AppText as Text } from "../../components/AppText";
7import { getEnvironmentClient } from "../../state/use-remote-environment-registry";
8import { useTerminalSession, useTerminalSessionTarget } from "../../state/use-terminal-session";
9import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface";
10import { hasNativeTerminalSurface } from "../../native/terminal/nativeTerminalModule";
11
12interface 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
21const DEFAULT_TERMINAL_COLS = 80;
22const DEFAULT_TERMINAL_ROWS = 24;
23
24export 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 @@
1import { describe, expect, it } from "vitest";
2
3import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap";
4
5describe("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 @@
1import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts";
2import { Stack, useLocalSearchParams, useRouter } from "expo-router";
3import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4import { Pressable, ScrollView, Text as RNText, View } from "react-native";
5import { KeyboardStickyView } from "react-native-keyboard-controller";
6import Animated, { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated";
7
8import { EmptyState } from "../../components/EmptyState";
9import { LoadingScreen } from "../../components/LoadingScreen";
10import { buildThreadTerminalRoutePath } from "../../lib/routes";
11import {
12 getEnvironmentClient,
13 useRemoteEnvironmentState,
14} from "../../state/use-remote-environment-registry";
15import {
16 terminalSessionManager,
17 useKnownTerminalSessions,
18 useTerminalSession,
19 useTerminalSessionTarget,
20} from "../../state/use-terminal-session";
21import { useThreadSelection } from "../../state/use-thread-selection";
22import { useSelectedThreadDetail } from "../../state/use-thread-detail";
23import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface";
24import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
25import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap";
26import { resolveTerminalOpenLocation } from "./terminalLaunchContext";
27import {
28 basename,
29 buildTerminalMenuSessions,
30 getTerminalLabel,
31 getTerminalStatusLabel,
32 nextTerminalId,
33} from "./terminalMenu";
34import {
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
42const DEFAULT_TERMINAL_COLS = 80;
43const DEFAULT_TERMINAL_ROWS = 24;
44const TERMINAL_ACCESSORY_HEIGHT = 52;
45
46type PendingModifier = "ctrl" | "meta";
47type HostPlatform = "mac" | "linux" | "windows" | "unknown";
48
49type 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
58interface 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
65function 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
106function 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
114function 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
135function 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
157export 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
10import { useThreadSelection } from "../../../state/use-thread-selection";10import { useThreadSelection } from "../../../state/use-thread-selection";
11import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";11import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";
12import { useSelectedThreadGitState } from "../use-selected-thread-git-state";12import { useSelectedThreadGitState } from "../use-selected-thread-git-state";
13import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
13import { SheetActionButton } from "./gitSheetComponents";14import { SheetActionButton } from "./gitSheetComponents";
14 15
15export function GitBranchesSheet() {16export function GitBranchesSheet() {
16 const router = useRouter();17 const router = useRouter();
17 const insets = useSafeAreaInsets();18 const insets = useSafeAreaInsets();
18 const { selectedThread, selectedThreadProject } = useThreadSelection();19 const { selectedThread } = useThreadSelection();
20 const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
19 const gitState = useSelectedThreadGitState();21 const gitState = useSelectedThreadGitState();
20 const gitActions = useSelectedThreadGitActions();22 const gitActions = useSelectedThreadGitActions();
21 23
@@ -27,11 +29,11 @@ export function GitBranchesSheet() { · 5 unmodified lines
27 29
28 const gitStatus = useGitStatus({30 const gitStatus = useGitStatus({
29 environmentId: selectedThread?.environmentId ?? "",31 environmentId: selectedThread?.environmentId ?? "",
30 cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,32 cwd: selectedThreadCwd,
31 });33 });
32 34
33 const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD";35 const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD";
34 const currentWorktreePath = selectedThread?.worktreePath ?? null;36 const currentWorktreePath = selectedThreadWorktreePath;
35 const availableBranches = gitState.selectedThreadBranches;37 const availableBranches = gitState.selectedThreadBranches;
36 const branchesLoading = gitState.selectedThreadBranchesLoading;38 const branchesLoading = gitState.selectedThreadBranchesLoading;
37 const busy = gitState.gitOperationLabel !== null;39 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
9import { useThreadSelection } from "../../../state/use-thread-selection";9import { useThreadSelection } from "../../../state/use-thread-selection";
10import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";10import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";
11import { useSelectedThreadGitState } from "../use-selected-thread-git-state";11import { useSelectedThreadGitState } from "../use-selected-thread-git-state";
12import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
12import { SheetActionButton } from "./gitSheetComponents";13import { SheetActionButton } from "./gitSheetComponents";
13 14
14export function GitCommitSheet() {15export function GitCommitSheet() {
15 const router = useRouter();16 const router = useRouter();
16 const insets = useSafeAreaInsets();17 const insets = useSafeAreaInsets();
17 const isDarkMode = useColorScheme() === "dark";18 const isDarkMode = useColorScheme() === "dark";
18 const { selectedThread, selectedThreadProject } = useThreadSelection();19 const { selectedThread } = useThreadSelection();
20 const { selectedThreadCwd } = useSelectedThreadWorktree();
19 const gitState = useSelectedThreadGitState();21 const gitState = useSelectedThreadGitState();
20 const gitActions = useSelectedThreadGitActions();22 const gitActions = useSelectedThreadGitActions();
21 23
@@ -27,7 +29,7 @@ export function GitCommitSheet() { · 5 unmodified lines
27 29
28 const gitStatus = useGitStatus({30 const gitStatus = useGitStatus({
29 environmentId: selectedThread?.environmentId ?? "",31 environmentId: selectedThread?.environmentId ?? "",
30 cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,32 cwd: selectedThreadCwd,
31 });33 });
32 34
33 const busy = gitState.gitOperationLabel !== null;35 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
17import { useThreadSelection } from "../../../state/use-thread-selection";17import { useThreadSelection } from "../../../state/use-thread-selection";
18import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";18import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions";
19import { useSelectedThreadGitState } from "../use-selected-thread-git-state";19import { useSelectedThreadGitState } from "../use-selected-thread-git-state";
20import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
20import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents";21import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents";
21 22
22export function GitOverviewSheet() {23export function GitOverviewSheet() {
@@ -26,7 +27,8 @@ export function GitOverviewSheet() { · 3 unmodified lines
26 environmentId: string;27 environmentId: string;
27 threadId: string;28 threadId: string;
28 }>();29 }>();
29 const { selectedThread, selectedThreadProject } = useThreadSelection();30 const { selectedThread } = useThreadSelection();
31 const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
30 const gitState = useSelectedThreadGitState();32 const gitState = useSelectedThreadGitState();
31 const gitActions = useSelectedThreadGitActions();33 const gitActions = useSelectedThreadGitActions();
32 34
@@ -35,11 +37,11 @@ export function GitOverviewSheet() { · 2 unmodified lines
35 37
36 const gitStatus = useGitStatus({38 const gitStatus = useGitStatus({
37 environmentId: selectedThread?.environmentId ?? "",39 environmentId: selectedThread?.environmentId ?? "",
38 cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,40 cwd: selectedThreadCwd,
39 });41 });
40 42
41 const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD";43 const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD";
42 const currentWorktreePath = selectedThread?.worktreePath ?? null;44 const currentWorktreePath = selectedThreadWorktreePath;
43 const gitOperationLabel = gitState.gitOperationLabel;45 const gitOperationLabel = gitState.gitOperationLabel;
44 const busy = gitOperationLabel !== null;46 const busy = gitOperationLabel !== null;
45 const isRepo = gitStatus.data?.isRepo ?? true;47 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
23import { useThemeColor } from "../../../lib/useThemeColor";23import { useThemeColor } from "../../../lib/useThemeColor";
24import { getEnvironmentClient } from "../../../state/use-remote-environment-registry";24import { getEnvironmentClient } from "../../../state/use-remote-environment-registry";
25import { useSelectedThreadDetail } from "../../../state/use-thread-detail";25import { useSelectedThreadDetail } from "../../../state/use-thread-detail";
26import { useThreadSelection } from "../../../state/use-thread-selection";
27import { useThreadDraftForThread } from "../use-thread-composer-state";26import { useThreadDraftForThread } from "../use-thread-composer-state";
27import { useSelectedThreadWorktree } from "../use-selected-thread-worktree";
28import {28import {
29 getCachedReviewParsedDiff,29 getCachedReviewParsedDiff,
30 setReviewGitSections,30 setReviewGitSections,
@@ -445,7 +445,6 @@ export function ReviewSheet() { · 414 unmodified lines
445 }>();445 }>();
446 const { draftMessage } = useThreadDraftForThread({ environmentId, threadId });446 const { draftMessage } = useThreadDraftForThread({ environmentId, threadId });
447 const reviewCache = useReviewCacheForThread({ environmentId, threadId });447 const reviewCache = useReviewCacheForThread({ environmentId, threadId });
448 const { selectedThreadProject } = useThreadSelection();
449 const selectedThread = useSelectedThreadDetail();448 const selectedThread = useSelectedThreadDetail();
450 const [loadingTurnIds, setLoadingTurnIds] = useState<Record<string, boolean>>({});449 const [loadingTurnIds, setLoadingTurnIds] = useState<Record<string, boolean>>({});
451 const [loadingGitDiffs, setLoadingGitDiffs] = useState(false);450 const [loadingGitDiffs, setLoadingGitDiffs] = useState(false);
@@ -464,8 +463,9 @@ export function ReviewSheet() { · 12 unmodified lines
464 const highlightQueueActiveRef = useRef(false);463 const highlightQueueActiveRef = useRef(false);
465 const highlightableFilesByIdRef = useRef<ReadonlyMap<string, ReviewRenderableFile>>(new Map());464 const highlightableFilesByIdRef = useRef<ReadonlyMap<string, ReviewRenderableFile>>(new Map());
466 const selectedThemeRef = useRef<ReviewDiffTheme>(selectedTheme);465 const selectedThemeRef = useRef<ReviewDiffTheme>(selectedTheme);
466 const { selectedThreadCwd } = useSelectedThreadWorktree();
467 467
468 const cwd = selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null;468 const cwd = selectedThreadCwd;
469 const readyCheckpoints = useMemo(469 const readyCheckpoints = useMemo(
470 () => getReadyReviewCheckpoints(selectedThread?.checkpoints ?? []),470 () => getReadyReviewCheckpoints(selectedThread?.checkpoints ?? []),
471 [selectedThread?.checkpoints],471 [selectedThread?.checkpoints],
apps/mobile/src/features/threads/terminalLaunchContext.test.ts+61−0
@@ -0,0 +1,61 @@
1import { describe, expect, it } from "vitest";
2
3import {
4 resolvePreferredThreadWorktreePath,
5 resolveTerminalOpenLocation,
6} from "./terminalLaunchContext";
7
8describe("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
28describe("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 @@
1interface TerminalSnapshotLike {
2 readonly cwd: string;
3 readonly worktreePath: string | null;
4}
5
6export 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
13export 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 @@
1import { describe, expect, it } from "vitest";
2
3import type { KnownTerminalSession } from "@t3tools/client-runtime";
4import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts";
5
6import { buildTerminalMenuSessions, resolveProjectScriptTerminalId } from "./terminalMenu";
7
8function 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
45describe("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
110describe("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 @@
1import type { KnownTerminalSession } from "@t3tools/client-runtime";
2import { DEFAULT_TERMINAL_ID, type ProjectScript } from "@t3tools/contracts";
3
4export 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
11export 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
25export 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
38export 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
58export 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
68export 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
117export 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
128export function projectScriptMenuLabel(script: ProjectScript): string {
129 return script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name;
130}
131
132export 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 @@
1import { describe, expect, it } from "vitest";
2
3import {
4 DEFAULT_TERMINAL_FONT_SIZE,
5 MAX_TERMINAL_FONT_SIZE,
6 MIN_TERMINAL_FONT_SIZE,
7 normalizeTerminalFontSize,
8} from "./terminalPreferences";
9
10describe("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 @@
1export const DEFAULT_TERMINAL_FONT_SIZE = 10;
2export const TERMINAL_FONT_SIZE_STEP = 0.5;
3export const MIN_TERMINAL_FONT_SIZE = 6;
4export const MAX_TERMINAL_FONT_SIZE = 14;
5
6export 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 @@
1export 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
25import { gitBranchManager } from "../../state/use-git-branches";25import { gitBranchManager } from "../../state/use-git-branches";
26import { gitStatusManager } from "../../state/use-git-status";26import { gitStatusManager } from "../../state/use-git-status";
27import { useThreadSelection } from "../../state/use-thread-selection";27import { useThreadSelection } from "../../state/use-thread-selection";
28import { useSelectedThreadWorktree } from "./use-selected-thread-worktree";
28 29
29export function useSelectedThreadGitActions() {30export function useSelectedThreadGitActions() {
30 const { selectedThread, selectedThreadProject } = useThreadSelection();31 const { selectedThread, selectedThreadProject } = useThreadSelection();
32 const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
31 33
32 const selectedThreadGitRootCwd = selectedThreadProject?.workspaceRoot ?? null;34 const selectedThreadGitRootCwd = selectedThreadProject?.workspaceRoot ?? null;
33 35
@@ -61,8 +63,7 @@ export function useSelectedThreadGitActions() { · 27 unmodified lines
61 return null;63 return null;
62 }64 }
63 65
64 const cwd =66 const cwd = options?.cwd ?? selectedThreadCwd;
65 options?.cwd ?? selectedThread.worktreePath ?? selectedThreadProject.workspaceRoot;
66 if (!cwd) {67 if (!cwd) {
67 return null;68 return null;
68 }69 }
@@ -86,7 +87,7 @@ export function useSelectedThreadGitActions() { · 17 unmodified lines
86 return null;87 return null;
87 }88 }
88 },89 },
89 [selectedThread, selectedThreadProject],90 [selectedThread, selectedThreadCwd, selectedThreadProject],
90 );91 );
91 92
92 useEffect(() => {93 useEffect(() => {
@@ -109,7 +110,7 @@ export function useSelectedThreadGitActions() { · 16 unmodified lines
109 return null;110 return null;
110 }111 }
111 112
112 const cwd = selectedThread.worktreePath ?? selectedThreadProject.workspaceRoot;113 const cwd = selectedThreadCwd;
113 if (!cwd) {114 if (!cwd) {
114 return null;115 return null;
115 }116 }
@@ -128,7 +129,7 @@ export function useSelectedThreadGitActions() { · 12 unmodified lines
128 return null;129 return null;
129 }130 }
130 },131 },
131 [selectedThread, selectedThreadProject],132 [selectedThread, selectedThreadCwd, selectedThreadProject],
132 );133 );
133 134
134 const refreshSelectedThreadBranches = useCallback(async (): Promise<ReadonlyArray<GitBranch>> => {135 const refreshSelectedThreadBranches = useCallback(async (): Promise<ReadonlyArray<GitBranch>> => {
@@ -204,12 +205,12 @@ export function useSelectedThreadGitActions() { · 69 unmodified lines
204 cwd,205 cwd,
205 nextThreadState: {206 nextThreadState: {
206 branch: result?.branch ?? thread.branch,207 branch: result?.branch ?? thread.branch,
207 worktreePath: thread.worktreePath,208 worktreePath: selectedThreadWorktreePath,
208 },209 },
209 });210 });
210 });211 });
211 },212 },
212 [runSelectedThreadGitMutation, syncSelectedThreadBranchState],213 [runSelectedThreadGitMutation, selectedThreadWorktreePath, syncSelectedThreadBranchState],
213 );214 );
214 215
215 const onCreateSelectedThreadBranch = useCallback(216 const onCreateSelectedThreadBranch = useCallback(
@@ -227,12 +228,12 @@ export function useSelectedThreadGitActions() { · 11 unmodified lines
227 cwd,228 cwd,
228 nextThreadState: {229 nextThreadState: {
229 branch: result?.branch ?? thread.branch,230 branch: result?.branch ?? thread.branch,
230 worktreePath: thread.worktreePath,231 worktreePath: selectedThreadWorktreePath,
231 },232 },
232 });233 });
233 });234 });
234 },235 },
235 [runSelectedThreadGitMutation, syncSelectedThreadBranchState],236 [runSelectedThreadGitMutation, selectedThreadWorktreePath, syncSelectedThreadBranchState],
236 );237 );
237 238
238 const onCreateSelectedThreadWorktree = useCallback(239 const onCreateSelectedThreadWorktree = useCallback(
@@ -316,7 +317,7 @@ export function useSelectedThreadGitActions() { · 77 unmodified lines
316 cwd,317 cwd,
317 nextThreadState: {318 nextThreadState: {
318 branch: result.branch.name,319 branch: result.branch.name,
319 worktreePath: thread.worktreePath,320 worktreePath: selectedThreadWorktreePath,
320 },321 },
321 });322 });
322 return result;323 return result;
@@ -326,7 +327,12 @@ export function useSelectedThreadGitActions() { · 3 unmodified lines
326 return result;327 return result;
327 });328 });
328 },329 },
329 [refreshSelectedThreadGitStatus, runSelectedThreadGitMutation, syncSelectedThreadBranchState],330 [
331 refreshSelectedThreadGitStatus,
332 runSelectedThreadGitMutation,
333 selectedThreadWorktreePath,
334 syncSelectedThreadBranchState,
335 ],
330 );336 );
331 337
332 return {338 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
5import { useGitActionState } from "../../state/use-git-action-state";5import { useGitActionState } from "../../state/use-git-action-state";
6import { useGitBranches } from "../../state/use-git-branches";6import { useGitBranches } from "../../state/use-git-branches";
7import { useThreadSelection } from "../../state/use-thread-selection";7import { useThreadSelection } from "../../state/use-thread-selection";
8import { useSelectedThreadWorktree } from "./use-selected-thread-worktree";
8 9
9export function useSelectedThreadGitState() {10export function useSelectedThreadGitState() {
10 const { selectedThread, selectedThreadProject } = useThreadSelection();11 const { selectedThread, selectedThreadProject } = useThreadSelection();
12 const { selectedThreadCwd } = useSelectedThreadWorktree();
11 13
12 const selectedThreadGitTarget = useMemo(14 const selectedThreadGitTarget = useMemo(
13 () => ({15 () => ({
14 environmentId: selectedThread?.environmentId ?? null,16 environmentId: selectedThread?.environmentId ?? null,
15 cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null,17 cwd: selectedThreadCwd,
16 }),18 }),
17 [19 [selectedThread?.environmentId, selectedThreadCwd],
18 selectedThread?.environmentId,
19 selectedThread?.worktreePath,
20 selectedThreadProject?.workspaceRoot,
21 ],
22 );20 );
23 const gitActionState = useGitActionState(selectedThreadGitTarget);21 const gitActionState = useGitActionState(selectedThreadGitTarget);
24 22
apps/mobile/src/features/threads/use-selected-thread-worktree.ts+24−0
@@ -0,0 +1,24 @@
1import { useMemo } from "react";
2
3import { useSelectedThreadDetail } from "../../state/use-thread-detail";
4import { useThreadSelection } from "../../state/use-thread-selection";
5import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext";
6
7export 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
29 return `${buildThreadRoutePath(input)}/review`;29 return `${buildThreadRoutePath(input)}/review`;
30}30}
31 31
32export 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
32export function dismissRoute(router: Router) {44export function dismissRoute(router: Router) {
33 if (router.canGoBack()) {45 if (router.canGoBack()) {
34 router.back();46 router.back();
apps/mobile/src/lib/storage.ts+48−10
@@ -5,6 +5,11 @@ import * as SecureStore from "expo-secure-store"; · 4 unmodified lines
5import type { SavedRemoteConnection } from "./connection";5import type { SavedRemoteConnection } from "./connection";
6 6
7const CONNECTIONS_KEY = "t3code.connections";7const CONNECTIONS_KEY = "t3code.connections";
8const PREFERENCES_KEY = "t3code.preferences";
9
10export interface MobilePreferences {
11 readonly terminalFontSize?: number;
12}
8 13
9async function readStorageItem(key: string): Promise<string | null> {14async function readStorageItem(key: string): Promise<string | null> {
10 return await SecureStore.getItemAsync(key);15 return await SecureStore.getItemAsync(key);
@@ -14,23 +19,31 @@ async function writeStorageItem(key: string, value: string): Promise<void> { · 3 unmodified lines
14 await SecureStore.setItemAsync(key, value);19 await SecureStore.setItemAsync(key, value);
15}20}
16 21
17export async function loadSavedConnections(): Promise<ReadonlyArray<SavedRemoteConnection>> {22async function readJsonStorageItem<T>(key: string): Promise<T | null> {
18 const raw = (await readStorageItem(CONNECTIONS_KEY)) ?? "";23 const raw = (await readStorageItem(key)) ?? "";
19 if (!raw.trim()) {24 if (!raw.trim()) {
20 return [];25 return null;
21 }26 }
22 27
23 try {28 try {
24 const parsed = JSON.parse(raw) as {29 return JSON.parse(raw) as T;
25 readonly connections?: ReadonlyArray<SavedRemoteConnection>;
26 };
27 return pipe(
28 parsed.connections ?? [],
29 Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()),
30 );
31 } catch {30 } catch {
31 return null;
32 }
33}
34
35export async function loadSavedConnections(): Promise<ReadonlyArray<SavedRemoteConnection>> {
36 const parsed = await readJsonStorageItem<{
37 readonly connections?: ReadonlyArray<SavedRemoteConnection>;
38 }>(CONNECTIONS_KEY);
39 if (!parsed) {
32 return [];40 return [];
33 }41 }
42
43 return pipe(
44 parsed.connections ?? [],
45 Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()),
46 );
34}47}
35 48
36export async function saveConnection(connection: SavedRemoteConnection): Promise<void> {49export async function saveConnection(connection: SavedRemoteConnection): Promise<void> {
@@ -53,3 +66,28 @@ export async function clearSavedConnection(environmentId: string): Promise<void> · 16 unmodified lines
53 );66 );
54 await writeStorageItem(CONNECTIONS_KEY, JSON.stringify({ connections: next }));67 await writeStorageItem(CONNECTIONS_KEY, JSON.stringify({ connections: next }));
55}68}
69
70export 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
83export 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 @@
1import { memo, useCallback } from "react";
2import {
3 Pressable,
4 ScrollView,
5 TextInput,
6 View,
7 type LayoutChangeEvent,
8 type NativeSyntheticEvent,
9 type ViewProps,
10 useColorScheme,
11} from "react-native";
12
13import { AppText as Text } from "../../components/AppText";
14import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule";
15import {
16 buildGhosttyThemeConfig,
17 getGitHubTerminalTheme,
18 type TerminalTheme,
19} from "./terminalTheme";
20
21interface TerminalInputEvent {
22 readonly data: string;
23}
24
25interface TerminalResizeEvent {
26 readonly cols: number;
27 readonly rows: number;
28}
29
30interface 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
40function 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
53const 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
165export 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 @@
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
3const expoModulesCoreMocks = vi.hoisted(() => ({
4 requireNativeViewManager: vi.fn(),
5}));
6const nativeView = () => null;
7const originalExpo = globalThis.expo;
8
9function setExpoViewConfigAvailable() {
10 globalThis.expo = {
11 getViewConfig: vi.fn().mockReturnValue({ validAttributes: {}, directEventTypes: {} }),
12 } as unknown as typeof globalThis.expo;
13}
14
15vi.mock("expo-modules-core", () => ({
16 requireNativeViewManager: expoModulesCoreMocks.requireNativeViewManager,
17}));
18
19describe("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 @@
1import type { ComponentType } from "react";
2import type { NativeSyntheticEvent, ViewProps } from "react-native";
3import { requireNativeViewManager } from "expo-modules-core";
4
5const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface";
6
7interface ExpoGlobalWithViewConfig {
8 readonly expo?: {
9 getViewConfig?: (moduleName: string, viewName?: string) => unknown;
10 };
11}
12
13interface TerminalInputEvent {
14 readonly data: string;
15}
16
17interface TerminalResizeEvent {
18 readonly cols: number;
19 readonly rows: number;
20}
21
22export 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
35let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined;
36
37function getExpoViewConfig(moduleName: string) {
38 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
39 moduleName,
40 );
41}
42
43export 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
63export function hasNativeTerminalSurface() {
64 return resolveNativeTerminalSurfaceView() !== null;
65}
apps/mobile/src/native/terminal/terminalTheme.test.ts+36−0
@@ -0,0 +1,36 @@
1import { describe, expect, it } from "vitest";
2
3import { buildGhosttyThemeConfig, getGitHubTerminalTheme } from "./terminalTheme";
4
5describe("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
25describe("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 @@
1export type TerminalAppearanceScheme = "light" | "dark";
2
3export 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
13const 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
41const 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
68export function getGitHubTerminalTheme(scheme: TerminalAppearanceScheme): TerminalTheme {
69 return scheme === "light" ? GITHUB_LIGHT_THEME : GITHUB_DARK_THEME;
70}
71
72export 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
23import { type ConnectedEnvironmentSummary, type EnvironmentSession } from "./remote-runtime-types";23import { type ConnectedEnvironmentSummary, type EnvironmentSession } from "./remote-runtime-types";
24import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";24import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
25import { shellSnapshotManager } from "./use-shell-snapshot";25import { shellSnapshotManager } from "./use-shell-snapshot";
26import { terminalSessionManager } from "./use-terminal-session";
26 27
27const environmentSessions = new Map<string, EnvironmentSession>();28const environmentSessions = new Map<string, EnvironmentSession>();
28const environmentConnectionListeners = new Set<() => void>();29const environmentConnectionListeners = new Set<() => void>();
@@ -154,6 +155,7 @@ export async function disconnectEnvironment( · 125 unmodified lines
154 notifyEnvironmentConnectionListeners();155 notifyEnvironmentConnectionListeners();
155 await session?.connection.dispose();156 await session?.connection.dispose();
156 shellSnapshotManager.invalidate({ environmentId });157 shellSnapshotManager.invalidate({ environmentId });
158 terminalSessionManager.invalidateEnvironment(environmentId);
157 environmentRuntimeManager.invalidate({ environmentId });159 environmentRuntimeManager.invalidate({ environmentId });
158 160
159 if (options?.removeSaved) {161 if (options?.removeSaved) {
@@ -244,7 +246,9 @@ export async function connectSavedEnvironment( · 84 unmodified lines
244 onShellResubscribe: (environmentId) => {246 onShellResubscribe: (environmentId) => {
245 shellSnapshotManager.markPending({ environmentId });247 shellSnapshotManager.markPending({ environmentId });
246 },248 },
247 applyTerminalEvent: () => undefined,249 applyTerminalEvent: (event, environmentId) => {
250 terminalSessionManager.applyEvent({ environmentId }, event);
251 },
248 onConfigSnapshot: (serverConfig) => {252 onConfigSnapshot: (serverConfig) => {
249 environmentRuntimeManager.patch({ environmentId: connection.environmentId }, (runtime) => ({253 environmentRuntimeManager.patch({ environmentId: connection.environmentId }, (runtime) => ({
250 ...runtime,254 ...runtime,
@@ -333,6 +337,7 @@ export function useRemoteEnvironmentBootstrap() { · 82 unmodified lines
333 environmentSessions.clear();337 environmentSessions.clear();
334 environmentRuntimeManager.invalidate();338 environmentRuntimeManager.invalidate();
335 shellSnapshotManager.invalidate();339 shellSnapshotManager.invalidate();
340 terminalSessionManager.invalidate();
336 notifyEnvironmentConnectionListeners();341 notifyEnvironmentConnectionListeners();
337 };342 };
338 }, []);343 }, []);
apps/mobile/src/state/use-terminal-session.ts+63−0
@@ -0,0 +1,63 @@
1import { useAtomValue } from "@effect/atom-react";
2import {
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";
11import { useMemo } from "react";
12
13import { appAtomRegistry } from "./atom-registry";
14
15export const terminalSessionManager = createTerminalSessionManager({
16 getRegistry: () => appAtomRegistry,
17});
18
19export 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
31export 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
46export 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
440 }),440 }),
441 );441 );
442 442
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
443 it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () =>469 it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () =>
444 Effect.gen(function* () {470 Effect.gen(function* () {
445 const { manager, getEvents, baseDir } = yield* createManager();471 const { manager, getEvents, baseDir } = yield* createManager();
@@ -778,6 +804,21 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", ( · 332 unmodified lines
778 }).pipe(Effect.provide(TestClock.layer())),804 }).pipe(Effect.provide(TestClock.layer())),
779 );805 );
780 806
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
781 it.effect("evicts oldest inactive terminal sessions when retention limit is exceeded", () =>822 it.effect("evicts oldest inactive terminal sessions when retention limit is exceeded", () =>
782 Effect.gen(function* () {823 Effect.gen(function* () {
783 const { manager, ptyAdapter, logsDir, getEvents } = yield* createManager(5, {824 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
1484 1484
1485 yield* flushPersist(threadId, terminalId);1485 yield* flushPersist(threadId, terminalId);
1486 1486
1487 yield* modifyManagerState((state) => {1487 const removed = yield* modifyManagerState((state) => {
1488 if (!state.sessions.has(key)) {1488 if (!state.sessions.has(key)) {
1489 return [undefined, state] as const;1489 return [false, state] as const;
1490 }1490 }
1491 const sessions = new Map(state.sessions);1491 const sessions = new Map(state.sessions);
1492 sessions.delete(key);1492 sessions.delete(key);
1493 return [undefined, { ...state, sessions }] as const;1493 return [true, { ...state, sessions }] as const;
1494 });1494 });
1495 1495
1496 if (removed) {
1497 yield* publishEvent({
1498 type: "closed",
1499 threadId,
1500 terminalId,
1501 createdAt: new Date().toISOString(),
1502 });
1503 }
1504
1496 if (deleteHistoryOnClose) {1505 if (deleteHistoryOnClose) {
1497 yield* deleteHistory(threadId, terminalId);1506 yield* deleteHistory(threadId, terminalId);
1498 }1507 }
@@ -1683,11 +1692,19 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 184 unmodified lines
1683 const targetCols = input.cols ?? liveSession.cols;1692 const targetCols = input.cols ?? liveSession.cols;
1684 const targetRows = input.rows ?? liveSession.rows;1693 const targetRows = input.rows ?? liveSession.rows;
1685 const runtimeEnvChanged = !Equal.equals(currentRuntimeEnv, nextRuntimeEnv);1694 const runtimeEnvChanged = !Equal.equals(currentRuntimeEnv, nextRuntimeEnv);
1686 1695 const nextWorktreePath =
1687 if (liveSession.cwd !== input.cwd || runtimeEnvChanged) {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) {
1688 yield* stopProcess(liveSession);1705 yield* stopProcess(liveSession);
1689 liveSession.cwd = input.cwd;1706 liveSession.cwd = input.cwd;
1690 liveSession.worktreePath = input.worktreePath ?? null;1707 liveSession.worktreePath = nextWorktreePath;
1691 liveSession.runtimeEnv = nextRuntimeEnv;1708 liveSession.runtimeEnv = nextRuntimeEnv;
1692 liveSession.history = "";1709 liveSession.history = "";
1693 liveSession.pendingHistoryControlSequence = "";1710 liveSession.pendingHistoryControlSequence = "";
@@ -1701,7 +1718,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 7 unmodified lines
1701 );1718 );
1702 } else if (liveSession.status === "exited" || liveSession.status === "error") {1719 } else if (liveSession.status === "exited" || liveSession.status === "error") {
1703 liveSession.runtimeEnv = nextRuntimeEnv;1720 liveSession.runtimeEnv = nextRuntimeEnv;
1704 liveSession.worktreePath = input.worktreePath ?? null;1721 liveSession.worktreePath = nextWorktreePath;
1705 liveSession.history = "";1722 liveSession.history = "";
1706 liveSession.pendingHistoryControlSequence = "";1723 liveSession.pendingHistoryControlSequence = "";
1707 liveSession.pendingProcessEvents = [];1724 liveSession.pendingProcessEvents = [];
apps/web/src/components/ThreadTerminalDrawer.tsx+15−10
@@ -602,16 +602,21 @@ export function TerminalViewport({ · 601 unmodified lines
602 return;602 return;
603 }603 }
604 604
605 const details = [605 if (event.type === "closed") {
606 typeof event.exitCode === "number" ? `code ${event.exitCode}` : null,606 writeSystemMessage(activeTerminal, "Terminal closed");
607 typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null,607 } else {
608 ]608 const details = [
609 .filter((value): value is string => value !== null)609 typeof event.exitCode === "number" ? `code ${event.exitCode}` : null,
610 .join(", ");610 typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null,
611 writeSystemMessage(611 ]
612 activeTerminal,612 .filter((value): value is string => value !== null)
613 details.length > 0 ? `Process exited (${details})` : "Process exited",613 .join(", ");
614 );614 writeSystemMessage(
615 activeTerminal,
616 details.length > 0 ? `Process exited (${details})` : "Process exited",
617 );
618 }
619
615 if (hasHandledExitRef.current) {620 if (hasHandledExitRef.current) {
616 return;621 return;
617 }622 }
apps/web/src/terminalActivity.ts+1−0
@@ -7,6 +7,7 @@ export function terminalRunningSubprocessFromEvent(event: TerminalEvent): boolea · 6 unmodified lines
7 case "started":7 case "started":
8 case "restarted":8 case "restarted":
9 case "exited":9 case "exited":
10 case "closed":
10 return false;11 return false;
11 default:12 default:
12 return null;13 return null;
apps/web/src/terminalStateStore.test.ts+2−0
@@ -34,6 +34,8 @@ function makeTerminalEvent( · 33 unmodified lines
34 return { ...base, type, ...overrides } as TerminalEvent;34 return { ...base, type, ...overrides } as TerminalEvent;
35 case "exited":35 case "exited":
36 return { ...base, type, exitCode: 0, exitSignal: null, ...overrides } as TerminalEvent;36 return { ...base, type, exitCode: 0, exitSignal: null, ...overrides } as TerminalEvent;
37 case "closed":
38 return { ...base, type, ...overrides } as TerminalEvent;
37 case "started":39 case "started":
38 case "restarted":40 case "restarted":
39 return {41 return {
bun.lock+7−0
@@ -57,11 +57,13 @@ · 56 unmodified lines
57 "@shikijs/themes": "3.23.0",57 "@shikijs/themes": "3.23.0",
58 "@t3tools/client-runtime": "workspace:*",58 "@t3tools/client-runtime": "workspace:*",
59 "@t3tools/contracts": "workspace:*",59 "@t3tools/contracts": "workspace:*",
60 "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal",
60 "@t3tools/shared": "workspace:*",61 "@t3tools/shared": "workspace:*",
61 "clsx": "^2.1.1",62 "clsx": "^2.1.1",
62 "diff": "8.0.3",63 "diff": "8.0.3",
63 "effect": "catalog:",64 "effect": "catalog:",
64 "expo": "~55.0.14",65 "expo": "~55.0.14",
66 "expo-build-properties": "~55.0.13",
65 "expo-camera": "~55.0.15",67 "expo-camera": "~55.0.15",
66 "expo-clipboard": "~55.0.9",68 "expo-clipboard": "~55.0.9",
67 "expo-constants": "^55.0.9",69 "expo-constants": "^55.0.9",
@@ -72,6 +74,7 @@ · 4 unmodified lines
72 "expo-haptics": "^55.0.9",74 "expo-haptics": "^55.0.9",
73 "expo-image-picker": "~55.0.14",75 "expo-image-picker": "~55.0.14",
74 "expo-linking": "~55.0.12",76 "expo-linking": "~55.0.12",
77 "expo-modules-core": "~55.0.22",
75 "expo-paste-input": "^0.1.15",78 "expo-paste-input": "^0.1.15",
76 "expo-router": "~55.0.12",79 "expo-router": "~55.0.12",
77 "expo-secure-store": "~55.0.9",80 "expo-secure-store": "~55.0.9",
@@ -1662,6 +1665,8 @@ · 1584 unmodified lines
1662 1665
1663 "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=="],1666 "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=="],
1664 1667
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
1665 "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=="],1670 "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=="],
1666 1671
1667 "expo-clipboard": ["expo-clipboard@55.0.13", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew=="],1672 "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
3032 3037
3033 "@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=="],3038 "@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=="],
3034 3039
3040 "@t3tools/mobile/@t3tools/mobile-terminal-native": ["@t3tools/mobile-terminal-native@file:apps/mobile/modules/t3-terminal", {}],
3041
3035 "@tailwindcss/node/tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],3042 "@tailwindcss/node/tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
3036 3043
3037 "@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=="],3044 "@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
11export * from "./gitActionState.ts";11export * from "./gitActionState.ts";
12export * from "./gitBranchState.ts";12export * from "./gitBranchState.ts";
13export * from "./gitStatusState.ts";13export * from "./gitStatusState.ts";
14export * from "./terminalSessionState.ts";
14export * from "./transportError.ts";15export * from "./transportError.ts";
15export * from "./wsRpcProtocol.ts";16export * from "./wsRpcProtocol.ts";
16export * from "./wsTransport.ts";17export * from "./wsTransport.ts";
packages/client-runtime/src/terminalSessionState.test.ts+276−0
@@ -0,0 +1,276 @@
1import { AtomRegistry } from "effect/unstable/reactivity";
2import { afterEach, describe, expect, it } from "vitest";
3
4import type { TerminalSessionSnapshot } from "@t3tools/contracts";
5
6import { createTerminalSessionManager } from "./terminalSessionState";
7
8let atomRegistry = AtomRegistry.make();
9
10function resetAtomRegistry() {
11 atomRegistry.dispose();
12 atomRegistry = AtomRegistry.make();
13}
14
15const TARGET = {
16 environmentId: "env-local",
17 threadId: "thread-1",
18 terminalId: "default",
19} as const;
20
21const 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
34describe("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 @@
1import type { TerminalEvent, TerminalSessionSnapshot } from "@t3tools/contracts";
2import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
3
4export 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
14export interface TerminalSessionTarget {
15 readonly environmentId: string | null;
16 readonly threadId: string | null;
17 readonly terminalId: string | null;
18}
19
20export interface KnownTerminalSessionTarget {
21 readonly environmentId: string;
22 readonly threadId: string;
23 readonly terminalId: string;
24}
25
26export interface KnownTerminalSession {
27 readonly target: KnownTerminalSessionTarget;
28 readonly state: TerminalSessionState;
29}
30
31export interface TerminalSessionManagerConfig {
32 readonly getRegistry: () => AtomRegistry.AtomRegistry;
33 readonly maxBufferBytes?: number;
34}
35
36export 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
46const DEFAULT_MAX_BUFFER_BYTES = 512 * 1024;
47const knownTerminalSessionKeys = new Set<string>();
48const textEncoder = new TextEncoder();
49const textDecoder = new TextDecoder();
50
51export 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
59export const EMPTY_TERMINAL_SESSION_ATOM = Atom.make(EMPTY_TERMINAL_SESSION_STATE).pipe(
60 Atom.keepAlive,
61 Atom.withLabel("terminal-session:null"),
62);
63
64export const knownTerminalSessionsAtom = Atom.make<Record<string, KnownTerminalSessionTarget>>(
65 {},
66).pipe(Atom.keepAlive, Atom.withLabel("terminal-session:index"));
67
68export 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
76function 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
96function 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
118function 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
133export 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
197 ).toBe(true);197 ).toBe(true);
198 });198 });
199 199
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
200 it("accepts activity events", () => {211 it("accepts activity events", () => {
201 expect(212 expect(
202 decodes(TerminalEvent, {213 decodes(TerminalEvent, {
packages/contracts/src/terminal.ts+6−0
@@ -119,6 +119,11 @@ const TerminalExitedEvent = Schema.Struct({ · 118 unmodified lines
119 exitSignal: Schema.NullOr(Schema.Int),119 exitSignal: Schema.NullOr(Schema.Int),
120});120});
121 121
122const TerminalClosedEvent = Schema.Struct({
123 ...TerminalEventBaseSchema.fields,
124 type: Schema.Literal("closed"),
125});
126
122const TerminalErrorEvent = Schema.Struct({127const TerminalErrorEvent = Schema.Struct({
123 ...TerminalEventBaseSchema.fields,128 ...TerminalEventBaseSchema.fields,
124 type: Schema.Literal("error"),129 type: Schema.Literal("error"),
@@ -146,6 +151,7 @@ export const TerminalEvent = Schema.Union([ · 21 unmodified lines
146 TerminalStartedEvent,151 TerminalStartedEvent,
147 TerminalOutputEvent,152 TerminalOutputEvent,
148 TerminalExitedEvent,153 TerminalExitedEvent,
154 TerminalClosedEvent,
149 TerminalErrorEvent,155 TerminalErrorEvent,
150 TerminalClearedEvent,156 TerminalClearedEvent,
151 TerminalRestartedEvent,157 TerminalRestartedEvent,