Unified · Split
| @@ -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", |
| @@ -0,0 +1,25 @@ | |||
| 1 | # T3 Mobile Terminal Native Module | ||
| 2 | |||
| 3 | This local Expo module owns the native terminal surface for the mobile app. | ||
| 4 | |||
| 5 | The JavaScript contract is intentionally small: | ||
| 6 | |||
| 7 | - input from the native surface is emitted as `{ data: string }` | ||
| 8 | - resize from the native surface is emitted as `{ cols: number, rows: number }` | ||
| 9 | - remote PTY output is delivered by the existing `WsRpcClient.terminal` RPC stream | ||
| 10 | |||
| 11 | The iOS implementation uses the vendored `GhosttyKit.xcframework` built from the same custom-I/O | ||
| 12 | Ghostty fork used by VVTerm. `T3TerminalView` owns a `libghostty` surface and mirrors VVTerm's | ||
| 13 | custom I/O model: | ||
| 14 | |||
| 15 | 1. initialize libghostty once for the process | ||
| 16 | 2. create one Ghostty app and surface per native view | ||
| 17 | 3. feed remote output into the surface with `ghostty_surface_feed_data` | ||
| 18 | 4. send user input back to JS with the write callback | ||
| 19 | 5. emit Ghostty's measured terminal size through `onResize` | ||
| 20 | |||
| 21 | Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the | ||
| 22 | React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a | ||
| 23 | future Android Ghostty build without changing JS. | ||
| 24 | |||
| 25 | Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`. | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | require 'json' | ||
| 2 | |||
| 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) | ||
| 4 | |||
| 5 | Pod::Spec.new do |s| | ||
| 6 | s.name = 'T3TerminalNative' | ||
| 7 | s.version = package['version'] | ||
| 8 | s.summary = 'Native terminal surface for T3 Code mobile.' | ||
| 9 | s.description = 'Native terminal surface bridge used by the T3 Code React Native app.' | ||
| 10 | s.homepage = 'https://t3tools.com' | ||
| 11 | s.license = { :type => 'UNLICENSED' } | ||
| 12 | s.author = { 'T3 Tools' => 'hello@t3tools.com' } | ||
| 13 | s.platforms = { :ios => '16.1' } | ||
| 14 | s.source = { :path => '.' } | ||
| 15 | s.source_files = 'ios/**/*.{h,m,mm,swift}' | ||
| 16 | s.vendored_frameworks = 'Vendor/libghostty/GhosttyKit.xcframework' | ||
| 17 | s.frameworks = 'IOSurface', 'Metal', 'MetalKit', 'QuartzCore', 'UIKit' | ||
| 18 | s.libraries = 'c++', 'z' | ||
| 19 | s.swift_version = '5.9' | ||
| 20 | s.dependency 'ExpoModulesCore' | ||
| 21 | end | ||
| @@ -0,0 +1,15 @@ | |||
| 1 | # Third-Party Notices | ||
| 2 | |||
| 3 | ## Ghostty / libghostty | ||
| 4 | |||
| 5 | The iOS terminal renderer vendors `GhosttyKit.xcframework`, a libghostty build produced from | ||
| 6 | VVTerm's custom-I/O Ghostty fork. | ||
| 7 | |||
| 8 | - Upstream project: https://github.com/ghostty-org/ghostty | ||
| 9 | - Vendored source fork: https://github.com/wiedymi/ghostty | ||
| 10 | - Vendored revision: `268a0a9d761fb19673f05d28042488e2002300f2` | ||
| 11 | - Reference integration: https://github.com/vivy-company/vvterm | ||
| 12 | - License: MIT | ||
| 13 | |||
| 14 | Ghostty's MIT license applies to the vendored framework. Keep this notice in sync when updating | ||
| 15 | `Vendor/libghostty`. | ||
| @@ -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> | ||
| @@ -0,0 +1,1187 @@ | |||
| 1 | // Ghostty embedding API. The documentation for the embedding API is | ||
| 2 | // only within the Zig source files that define the implementations. This | ||
| 3 | // isn't meant to be a general purpose embedding API (yet) so there hasn't | ||
| 4 | // been documentation or example work beyond that. | ||
| 5 | // | ||
| 6 | // The only consumer of this API is the macOS app, but the API is built to | ||
| 7 | // be more general purpose. | ||
| 8 | #ifndef GHOSTTY_H | ||
| 9 | #define GHOSTTY_H | ||
| 10 | |||
| 11 | #ifdef __cplusplus | ||
| 12 | extern "C" { | ||
| 13 | #endif | ||
| 14 | |||
| 15 | #include <stdbool.h> | ||
| 16 | #include <stddef.h> | ||
| 17 | #include <stdint.h> | ||
| 18 | #include <sys/types.h> | ||
| 19 | |||
| 20 | //------------------------------------------------------------------- | ||
| 21 | // Macros | ||
| 22 | |||
| 23 | #define GHOSTTY_SUCCESS 0 | ||
| 24 | |||
| 25 | //------------------------------------------------------------------- | ||
| 26 | // Types | ||
| 27 | |||
| 28 | // Opaque types | ||
| 29 | typedef void* ghostty_app_t; | ||
| 30 | typedef void* ghostty_config_t; | ||
| 31 | typedef void* ghostty_surface_t; | ||
| 32 | typedef void* ghostty_inspector_t; | ||
| 33 | |||
| 34 | // All the types below are fully defined and must be kept in sync with | ||
| 35 | // their Zig counterparts. Any changes to these types MUST have an associated | ||
| 36 | // Zig change. | ||
| 37 | typedef enum { | ||
| 38 | GHOSTTY_PLATFORM_INVALID, | ||
| 39 | GHOSTTY_PLATFORM_MACOS, | ||
| 40 | GHOSTTY_PLATFORM_IOS, | ||
| 41 | } ghostty_platform_e; | ||
| 42 | |||
| 43 | // Callback for custom I/O write handler. | ||
| 44 | typedef void (*ghostty_surface_write_fn)(void* userdata, | ||
| 45 | const uint8_t* data, | ||
| 46 | size_t len); | ||
| 47 | |||
| 48 | typedef enum { | ||
| 49 | GHOSTTY_CLIPBOARD_STANDARD, | ||
| 50 | GHOSTTY_CLIPBOARD_SELECTION, | ||
| 51 | } ghostty_clipboard_e; | ||
| 52 | |||
| 53 | typedef struct { | ||
| 54 | const char *mime; | ||
| 55 | const char *data; | ||
| 56 | } ghostty_clipboard_content_s; | ||
| 57 | |||
| 58 | typedef enum { | ||
| 59 | GHOSTTY_CLIPBOARD_REQUEST_PASTE, | ||
| 60 | GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ, | ||
| 61 | GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE, | ||
| 62 | } ghostty_clipboard_request_e; | ||
| 63 | |||
| 64 | typedef enum { | ||
| 65 | GHOSTTY_MOUSE_RELEASE, | ||
| 66 | GHOSTTY_MOUSE_PRESS, | ||
| 67 | } ghostty_input_mouse_state_e; | ||
| 68 | |||
| 69 | typedef enum { | ||
| 70 | GHOSTTY_MOUSE_UNKNOWN, | ||
| 71 | GHOSTTY_MOUSE_LEFT, | ||
| 72 | GHOSTTY_MOUSE_RIGHT, | ||
| 73 | GHOSTTY_MOUSE_MIDDLE, | ||
| 74 | GHOSTTY_MOUSE_FOUR, | ||
| 75 | GHOSTTY_MOUSE_FIVE, | ||
| 76 | GHOSTTY_MOUSE_SIX, | ||
| 77 | GHOSTTY_MOUSE_SEVEN, | ||
| 78 | GHOSTTY_MOUSE_EIGHT, | ||
| 79 | GHOSTTY_MOUSE_NINE, | ||
| 80 | GHOSTTY_MOUSE_TEN, | ||
| 81 | GHOSTTY_MOUSE_ELEVEN, | ||
| 82 | } ghostty_input_mouse_button_e; | ||
| 83 | |||
| 84 | typedef enum { | ||
| 85 | GHOSTTY_MOUSE_MOMENTUM_NONE, | ||
| 86 | GHOSTTY_MOUSE_MOMENTUM_BEGAN, | ||
| 87 | GHOSTTY_MOUSE_MOMENTUM_STATIONARY, | ||
| 88 | GHOSTTY_MOUSE_MOMENTUM_CHANGED, | ||
| 89 | GHOSTTY_MOUSE_MOMENTUM_ENDED, | ||
| 90 | GHOSTTY_MOUSE_MOMENTUM_CANCELLED, | ||
| 91 | GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN, | ||
| 92 | } ghostty_input_mouse_momentum_e; | ||
| 93 | |||
| 94 | typedef enum { | ||
| 95 | GHOSTTY_COLOR_SCHEME_LIGHT = 0, | ||
| 96 | GHOSTTY_COLOR_SCHEME_DARK = 1, | ||
| 97 | } ghostty_color_scheme_e; | ||
| 98 | |||
| 99 | // This is a packed struct (see src/input/mouse.zig) but the C standard | ||
| 100 | // afaik doesn't let us reliably define packed structs so we build it up | ||
| 101 | // from scratch. | ||
| 102 | typedef int ghostty_input_scroll_mods_t; | ||
| 103 | |||
| 104 | typedef enum { | ||
| 105 | GHOSTTY_MODS_NONE = 0, | ||
| 106 | GHOSTTY_MODS_SHIFT = 1 << 0, | ||
| 107 | GHOSTTY_MODS_CTRL = 1 << 1, | ||
| 108 | GHOSTTY_MODS_ALT = 1 << 2, | ||
| 109 | GHOSTTY_MODS_SUPER = 1 << 3, | ||
| 110 | GHOSTTY_MODS_CAPS = 1 << 4, | ||
| 111 | GHOSTTY_MODS_NUM = 1 << 5, | ||
| 112 | GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6, | ||
| 113 | GHOSTTY_MODS_CTRL_RIGHT = 1 << 7, | ||
| 114 | GHOSTTY_MODS_ALT_RIGHT = 1 << 8, | ||
| 115 | GHOSTTY_MODS_SUPER_RIGHT = 1 << 9, | ||
| 116 | } ghostty_input_mods_e; | ||
| 117 | |||
| 118 | typedef enum { | ||
| 119 | GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0, | ||
| 120 | GHOSTTY_BINDING_FLAGS_ALL = 1 << 1, | ||
| 121 | GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2, | ||
| 122 | GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3, | ||
| 123 | } ghostty_binding_flags_e; | ||
| 124 | |||
| 125 | typedef enum { | ||
| 126 | GHOSTTY_ACTION_RELEASE, | ||
| 127 | GHOSTTY_ACTION_PRESS, | ||
| 128 | GHOSTTY_ACTION_REPEAT, | ||
| 129 | } ghostty_input_action_e; | ||
| 130 | |||
| 131 | // Based on: https://www.w3.org/TR/uievents-code/ | ||
| 132 | typedef enum { | ||
| 133 | GHOSTTY_KEY_UNIDENTIFIED, | ||
| 134 | |||
| 135 | // "Writing System Keys" § 3.1.1 | ||
| 136 | GHOSTTY_KEY_BACKQUOTE, | ||
| 137 | GHOSTTY_KEY_BACKSLASH, | ||
| 138 | GHOSTTY_KEY_BRACKET_LEFT, | ||
| 139 | GHOSTTY_KEY_BRACKET_RIGHT, | ||
| 140 | GHOSTTY_KEY_COMMA, | ||
| 141 | GHOSTTY_KEY_DIGIT_0, | ||
| 142 | GHOSTTY_KEY_DIGIT_1, | ||
| 143 | GHOSTTY_KEY_DIGIT_2, | ||
| 144 | GHOSTTY_KEY_DIGIT_3, | ||
| 145 | GHOSTTY_KEY_DIGIT_4, | ||
| 146 | GHOSTTY_KEY_DIGIT_5, | ||
| 147 | GHOSTTY_KEY_DIGIT_6, | ||
| 148 | GHOSTTY_KEY_DIGIT_7, | ||
| 149 | GHOSTTY_KEY_DIGIT_8, | ||
| 150 | GHOSTTY_KEY_DIGIT_9, | ||
| 151 | GHOSTTY_KEY_EQUAL, | ||
| 152 | GHOSTTY_KEY_INTL_BACKSLASH, | ||
| 153 | GHOSTTY_KEY_INTL_RO, | ||
| 154 | GHOSTTY_KEY_INTL_YEN, | ||
| 155 | GHOSTTY_KEY_A, | ||
| 156 | GHOSTTY_KEY_B, | ||
| 157 | GHOSTTY_KEY_C, | ||
| 158 | GHOSTTY_KEY_D, | ||
| 159 | GHOSTTY_KEY_E, | ||
| 160 | GHOSTTY_KEY_F, | ||
| 161 | GHOSTTY_KEY_G, | ||
| 162 | GHOSTTY_KEY_H, | ||
| 163 | GHOSTTY_KEY_I, | ||
| 164 | GHOSTTY_KEY_J, | ||
| 165 | GHOSTTY_KEY_K, | ||
| 166 | GHOSTTY_KEY_L, | ||
| 167 | GHOSTTY_KEY_M, | ||
| 168 | GHOSTTY_KEY_N, | ||
| 169 | GHOSTTY_KEY_O, | ||
| 170 | GHOSTTY_KEY_P, | ||
| 171 | GHOSTTY_KEY_Q, | ||
| 172 | GHOSTTY_KEY_R, | ||
| 173 | GHOSTTY_KEY_S, | ||
| 174 | GHOSTTY_KEY_T, | ||
| 175 | GHOSTTY_KEY_U, | ||
| 176 | GHOSTTY_KEY_V, | ||
| 177 | GHOSTTY_KEY_W, | ||
| 178 | GHOSTTY_KEY_X, | ||
| 179 | GHOSTTY_KEY_Y, | ||
| 180 | GHOSTTY_KEY_Z, | ||
| 181 | GHOSTTY_KEY_MINUS, | ||
| 182 | GHOSTTY_KEY_PERIOD, | ||
| 183 | GHOSTTY_KEY_QUOTE, | ||
| 184 | GHOSTTY_KEY_SEMICOLON, | ||
| 185 | GHOSTTY_KEY_SLASH, | ||
| 186 | |||
| 187 | // "Functional Keys" § 3.1.2 | ||
| 188 | GHOSTTY_KEY_ALT_LEFT, | ||
| 189 | GHOSTTY_KEY_ALT_RIGHT, | ||
| 190 | GHOSTTY_KEY_BACKSPACE, | ||
| 191 | GHOSTTY_KEY_CAPS_LOCK, | ||
| 192 | GHOSTTY_KEY_CONTEXT_MENU, | ||
| 193 | GHOSTTY_KEY_CONTROL_LEFT, | ||
| 194 | GHOSTTY_KEY_CONTROL_RIGHT, | ||
| 195 | GHOSTTY_KEY_ENTER, | ||
| 196 | GHOSTTY_KEY_META_LEFT, | ||
| 197 | GHOSTTY_KEY_META_RIGHT, | ||
| 198 | GHOSTTY_KEY_SHIFT_LEFT, | ||
| 199 | GHOSTTY_KEY_SHIFT_RIGHT, | ||
| 200 | GHOSTTY_KEY_SPACE, | ||
| 201 | GHOSTTY_KEY_TAB, | ||
| 202 | GHOSTTY_KEY_CONVERT, | ||
| 203 | GHOSTTY_KEY_KANA_MODE, | ||
| 204 | GHOSTTY_KEY_NON_CONVERT, | ||
| 205 | |||
| 206 | // "Control Pad Section" § 3.2 | ||
| 207 | GHOSTTY_KEY_DELETE, | ||
| 208 | GHOSTTY_KEY_END, | ||
| 209 | GHOSTTY_KEY_HELP, | ||
| 210 | GHOSTTY_KEY_HOME, | ||
| 211 | GHOSTTY_KEY_INSERT, | ||
| 212 | GHOSTTY_KEY_PAGE_DOWN, | ||
| 213 | GHOSTTY_KEY_PAGE_UP, | ||
| 214 | |||
| 215 | // "Arrow Pad Section" § 3.3 | ||
| 216 | GHOSTTY_KEY_ARROW_DOWN, | ||
| 217 | GHOSTTY_KEY_ARROW_LEFT, | ||
| 218 | GHOSTTY_KEY_ARROW_RIGHT, | ||
| 219 | GHOSTTY_KEY_ARROW_UP, | ||
| 220 | |||
| 221 | // "Numpad Section" § 3.4 | ||
| 222 | GHOSTTY_KEY_NUM_LOCK, | ||
| 223 | GHOSTTY_KEY_NUMPAD_0, | ||
| 224 | GHOSTTY_KEY_NUMPAD_1, | ||
| 225 | GHOSTTY_KEY_NUMPAD_2, | ||
| 226 | GHOSTTY_KEY_NUMPAD_3, | ||
| 227 | GHOSTTY_KEY_NUMPAD_4, | ||
| 228 | GHOSTTY_KEY_NUMPAD_5, | ||
| 229 | GHOSTTY_KEY_NUMPAD_6, | ||
| 230 | GHOSTTY_KEY_NUMPAD_7, | ||
| 231 | GHOSTTY_KEY_NUMPAD_8, | ||
| 232 | GHOSTTY_KEY_NUMPAD_9, | ||
| 233 | GHOSTTY_KEY_NUMPAD_ADD, | ||
| 234 | GHOSTTY_KEY_NUMPAD_BACKSPACE, | ||
| 235 | GHOSTTY_KEY_NUMPAD_CLEAR, | ||
| 236 | GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, | ||
| 237 | GHOSTTY_KEY_NUMPAD_COMMA, | ||
| 238 | GHOSTTY_KEY_NUMPAD_DECIMAL, | ||
| 239 | GHOSTTY_KEY_NUMPAD_DIVIDE, | ||
| 240 | GHOSTTY_KEY_NUMPAD_ENTER, | ||
| 241 | GHOSTTY_KEY_NUMPAD_EQUAL, | ||
| 242 | GHOSTTY_KEY_NUMPAD_MEMORY_ADD, | ||
| 243 | GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, | ||
| 244 | GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, | ||
| 245 | GHOSTTY_KEY_NUMPAD_MEMORY_STORE, | ||
| 246 | GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, | ||
| 247 | GHOSTTY_KEY_NUMPAD_MULTIPLY, | ||
| 248 | GHOSTTY_KEY_NUMPAD_PAREN_LEFT, | ||
| 249 | GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, | ||
| 250 | GHOSTTY_KEY_NUMPAD_SUBTRACT, | ||
| 251 | GHOSTTY_KEY_NUMPAD_SEPARATOR, | ||
| 252 | GHOSTTY_KEY_NUMPAD_UP, | ||
| 253 | GHOSTTY_KEY_NUMPAD_DOWN, | ||
| 254 | GHOSTTY_KEY_NUMPAD_RIGHT, | ||
| 255 | GHOSTTY_KEY_NUMPAD_LEFT, | ||
| 256 | GHOSTTY_KEY_NUMPAD_BEGIN, | ||
| 257 | GHOSTTY_KEY_NUMPAD_HOME, | ||
| 258 | GHOSTTY_KEY_NUMPAD_END, | ||
| 259 | GHOSTTY_KEY_NUMPAD_INSERT, | ||
| 260 | GHOSTTY_KEY_NUMPAD_DELETE, | ||
| 261 | GHOSTTY_KEY_NUMPAD_PAGE_UP, | ||
| 262 | GHOSTTY_KEY_NUMPAD_PAGE_DOWN, | ||
| 263 | |||
| 264 | // "Function Section" § 3.5 | ||
| 265 | GHOSTTY_KEY_ESCAPE, | ||
| 266 | GHOSTTY_KEY_F1, | ||
| 267 | GHOSTTY_KEY_F2, | ||
| 268 | GHOSTTY_KEY_F3, | ||
| 269 | GHOSTTY_KEY_F4, | ||
| 270 | GHOSTTY_KEY_F5, | ||
| 271 | GHOSTTY_KEY_F6, | ||
| 272 | GHOSTTY_KEY_F7, | ||
| 273 | GHOSTTY_KEY_F8, | ||
| 274 | GHOSTTY_KEY_F9, | ||
| 275 | GHOSTTY_KEY_F10, | ||
| 276 | GHOSTTY_KEY_F11, | ||
| 277 | GHOSTTY_KEY_F12, | ||
| 278 | GHOSTTY_KEY_F13, | ||
| 279 | GHOSTTY_KEY_F14, | ||
| 280 | GHOSTTY_KEY_F15, | ||
| 281 | GHOSTTY_KEY_F16, | ||
| 282 | GHOSTTY_KEY_F17, | ||
| 283 | GHOSTTY_KEY_F18, | ||
| 284 | GHOSTTY_KEY_F19, | ||
| 285 | GHOSTTY_KEY_F20, | ||
| 286 | GHOSTTY_KEY_F21, | ||
| 287 | GHOSTTY_KEY_F22, | ||
| 288 | GHOSTTY_KEY_F23, | ||
| 289 | GHOSTTY_KEY_F24, | ||
| 290 | GHOSTTY_KEY_F25, | ||
| 291 | GHOSTTY_KEY_FN, | ||
| 292 | GHOSTTY_KEY_FN_LOCK, | ||
| 293 | GHOSTTY_KEY_PRINT_SCREEN, | ||
| 294 | GHOSTTY_KEY_SCROLL_LOCK, | ||
| 295 | GHOSTTY_KEY_PAUSE, | ||
| 296 | |||
| 297 | // "Media Keys" § 3.6 | ||
| 298 | GHOSTTY_KEY_BROWSER_BACK, | ||
| 299 | GHOSTTY_KEY_BROWSER_FAVORITES, | ||
| 300 | GHOSTTY_KEY_BROWSER_FORWARD, | ||
| 301 | GHOSTTY_KEY_BROWSER_HOME, | ||
| 302 | GHOSTTY_KEY_BROWSER_REFRESH, | ||
| 303 | GHOSTTY_KEY_BROWSER_SEARCH, | ||
| 304 | GHOSTTY_KEY_BROWSER_STOP, | ||
| 305 | GHOSTTY_KEY_EJECT, | ||
| 306 | GHOSTTY_KEY_LAUNCH_APP_1, | ||
| 307 | GHOSTTY_KEY_LAUNCH_APP_2, | ||
| 308 | GHOSTTY_KEY_LAUNCH_MAIL, | ||
| 309 | GHOSTTY_KEY_MEDIA_PLAY_PAUSE, | ||
| 310 | GHOSTTY_KEY_MEDIA_SELECT, | ||
| 311 | GHOSTTY_KEY_MEDIA_STOP, | ||
| 312 | GHOSTTY_KEY_MEDIA_TRACK_NEXT, | ||
| 313 | GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, | ||
| 314 | GHOSTTY_KEY_POWER, | ||
| 315 | GHOSTTY_KEY_SLEEP, | ||
| 316 | GHOSTTY_KEY_AUDIO_VOLUME_DOWN, | ||
| 317 | GHOSTTY_KEY_AUDIO_VOLUME_MUTE, | ||
| 318 | GHOSTTY_KEY_AUDIO_VOLUME_UP, | ||
| 319 | GHOSTTY_KEY_WAKE_UP, | ||
| 320 | |||
| 321 | // "Legacy, Non-standard, and Special Keys" § 3.7 | ||
| 322 | GHOSTTY_KEY_COPY, | ||
| 323 | GHOSTTY_KEY_CUT, | ||
| 324 | GHOSTTY_KEY_PASTE, | ||
| 325 | } ghostty_input_key_e; | ||
| 326 | |||
| 327 | typedef struct { | ||
| 328 | ghostty_input_action_e action; | ||
| 329 | ghostty_input_mods_e mods; | ||
| 330 | ghostty_input_mods_e consumed_mods; | ||
| 331 | uint32_t keycode; | ||
| 332 | const char* text; | ||
| 333 | uint32_t unshifted_codepoint; | ||
| 334 | bool composing; | ||
| 335 | } ghostty_input_key_s; | ||
| 336 | |||
| 337 | typedef enum { | ||
| 338 | GHOSTTY_TRIGGER_PHYSICAL, | ||
| 339 | GHOSTTY_TRIGGER_UNICODE, | ||
| 340 | GHOSTTY_TRIGGER_CATCH_ALL, | ||
| 341 | } ghostty_input_trigger_tag_e; | ||
| 342 | |||
| 343 | typedef union { | ||
| 344 | ghostty_input_key_e translated; | ||
| 345 | ghostty_input_key_e physical; | ||
| 346 | uint32_t unicode; | ||
| 347 | // catch_all has no payload | ||
| 348 | } ghostty_input_trigger_key_u; | ||
| 349 | |||
| 350 | typedef struct { | ||
| 351 | ghostty_input_trigger_tag_e tag; | ||
| 352 | ghostty_input_trigger_key_u key; | ||
| 353 | ghostty_input_mods_e mods; | ||
| 354 | } ghostty_input_trigger_s; | ||
| 355 | |||
| 356 | typedef struct { | ||
| 357 | const char* action_key; | ||
| 358 | const char* action; | ||
| 359 | const char* title; | ||
| 360 | const char* description; | ||
| 361 | } ghostty_command_s; | ||
| 362 | |||
| 363 | typedef enum { | ||
| 364 | GHOSTTY_BUILD_MODE_DEBUG, | ||
| 365 | GHOSTTY_BUILD_MODE_RELEASE_SAFE, | ||
| 366 | GHOSTTY_BUILD_MODE_RELEASE_FAST, | ||
| 367 | GHOSTTY_BUILD_MODE_RELEASE_SMALL, | ||
| 368 | } ghostty_build_mode_e; | ||
| 369 | |||
| 370 | typedef struct { | ||
| 371 | ghostty_build_mode_e build_mode; | ||
| 372 | const char* version; | ||
| 373 | uintptr_t version_len; | ||
| 374 | } ghostty_info_s; | ||
| 375 | |||
| 376 | typedef struct { | ||
| 377 | const char* message; | ||
| 378 | } ghostty_diagnostic_s; | ||
| 379 | |||
| 380 | typedef struct { | ||
| 381 | const char* ptr; | ||
| 382 | uintptr_t len; | ||
| 383 | bool sentinel; | ||
| 384 | } ghostty_string_s; | ||
| 385 | |||
| 386 | typedef struct { | ||
| 387 | double tl_px_x; | ||
| 388 | double tl_px_y; | ||
| 389 | uint32_t offset_start; | ||
| 390 | uint32_t offset_len; | ||
| 391 | const char* text; | ||
| 392 | uintptr_t text_len; | ||
| 393 | } ghostty_text_s; | ||
| 394 | |||
| 395 | typedef enum { | ||
| 396 | GHOSTTY_POINT_ACTIVE, | ||
| 397 | GHOSTTY_POINT_VIEWPORT, | ||
| 398 | GHOSTTY_POINT_SCREEN, | ||
| 399 | GHOSTTY_POINT_SURFACE, | ||
| 400 | } ghostty_point_tag_e; | ||
| 401 | |||
| 402 | typedef enum { | ||
| 403 | GHOSTTY_POINT_COORD_EXACT, | ||
| 404 | GHOSTTY_POINT_COORD_TOP_LEFT, | ||
| 405 | GHOSTTY_POINT_COORD_BOTTOM_RIGHT, | ||
| 406 | } ghostty_point_coord_e; | ||
| 407 | |||
| 408 | typedef struct { | ||
| 409 | ghostty_point_tag_e tag; | ||
| 410 | ghostty_point_coord_e coord; | ||
| 411 | uint32_t x; | ||
| 412 | uint32_t y; | ||
| 413 | } ghostty_point_s; | ||
| 414 | |||
| 415 | typedef struct { | ||
| 416 | ghostty_point_s top_left; | ||
| 417 | ghostty_point_s bottom_right; | ||
| 418 | bool rectangle; | ||
| 419 | } ghostty_selection_s; | ||
| 420 | |||
| 421 | typedef struct { | ||
| 422 | const char* key; | ||
| 423 | const char* value; | ||
| 424 | } ghostty_env_var_s; | ||
| 425 | |||
| 426 | typedef struct { | ||
| 427 | void* nsview; | ||
| 428 | } ghostty_platform_macos_s; | ||
| 429 | |||
| 430 | typedef struct { | ||
| 431 | void* uiview; | ||
| 432 | } ghostty_platform_ios_s; | ||
| 433 | |||
| 434 | typedef union { | ||
| 435 | ghostty_platform_macos_s macos; | ||
| 436 | ghostty_platform_ios_s ios; | ||
| 437 | } ghostty_platform_u; | ||
| 438 | |||
| 439 | typedef enum { | ||
| 440 | GHOSTTY_SURFACE_CONTEXT_WINDOW = 0, | ||
| 441 | GHOSTTY_SURFACE_CONTEXT_TAB = 1, | ||
| 442 | GHOSTTY_SURFACE_CONTEXT_SPLIT = 2, | ||
| 443 | } ghostty_surface_context_e; | ||
| 444 | |||
| 445 | typedef struct { | ||
| 446 | ghostty_platform_e platform_tag; | ||
| 447 | ghostty_platform_u platform; | ||
| 448 | void* userdata; | ||
| 449 | double scale_factor; | ||
| 450 | float font_size; | ||
| 451 | const char* working_directory; | ||
| 452 | const char* command; | ||
| 453 | ghostty_env_var_s* env_vars; | ||
| 454 | size_t env_var_count; | ||
| 455 | const char* initial_input; | ||
| 456 | bool wait_after_command; | ||
| 457 | ghostty_surface_context_e context; | ||
| 458 | bool use_custom_io; | ||
| 459 | } ghostty_surface_config_s; | ||
| 460 | |||
| 461 | typedef struct { | ||
| 462 | uint16_t columns; | ||
| 463 | uint16_t rows; | ||
| 464 | uint32_t width_px; | ||
| 465 | uint32_t height_px; | ||
| 466 | uint32_t cell_width_px; | ||
| 467 | uint32_t cell_height_px; | ||
| 468 | } ghostty_surface_size_s; | ||
| 469 | |||
| 470 | // Config types | ||
| 471 | |||
| 472 | // config.Path | ||
| 473 | typedef struct { | ||
| 474 | const char* path; | ||
| 475 | bool optional; | ||
| 476 | } ghostty_config_path_s; | ||
| 477 | |||
| 478 | // config.Color | ||
| 479 | typedef struct { | ||
| 480 | uint8_t r; | ||
| 481 | uint8_t g; | ||
| 482 | uint8_t b; | ||
| 483 | } ghostty_config_color_s; | ||
| 484 | |||
| 485 | // config.ColorList | ||
| 486 | typedef struct { | ||
| 487 | const ghostty_config_color_s* colors; | ||
| 488 | size_t len; | ||
| 489 | } ghostty_config_color_list_s; | ||
| 490 | |||
| 491 | // config.RepeatableCommand | ||
| 492 | typedef struct { | ||
| 493 | const ghostty_command_s* commands; | ||
| 494 | size_t len; | ||
| 495 | } ghostty_config_command_list_s; | ||
| 496 | |||
| 497 | // config.Palette | ||
| 498 | typedef struct { | ||
| 499 | ghostty_config_color_s colors[256]; | ||
| 500 | } ghostty_config_palette_s; | ||
| 501 | |||
| 502 | // config.QuickTerminalSize | ||
| 503 | typedef enum { | ||
| 504 | GHOSTTY_QUICK_TERMINAL_SIZE_NONE, | ||
| 505 | GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE, | ||
| 506 | GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS, | ||
| 507 | } ghostty_quick_terminal_size_tag_e; | ||
| 508 | |||
| 509 | typedef union { | ||
| 510 | float percentage; | ||
| 511 | uint32_t pixels; | ||
| 512 | } ghostty_quick_terminal_size_value_u; | ||
| 513 | |||
| 514 | typedef struct { | ||
| 515 | ghostty_quick_terminal_size_tag_e tag; | ||
| 516 | ghostty_quick_terminal_size_value_u value; | ||
| 517 | } ghostty_quick_terminal_size_s; | ||
| 518 | |||
| 519 | typedef struct { | ||
| 520 | ghostty_quick_terminal_size_s primary; | ||
| 521 | ghostty_quick_terminal_size_s secondary; | ||
| 522 | } ghostty_config_quick_terminal_size_s; | ||
| 523 | |||
| 524 | // config.Fullscreen | ||
| 525 | typedef enum { | ||
| 526 | GHOSTTY_CONFIG_FULLSCREEN_FALSE, | ||
| 527 | GHOSTTY_CONFIG_FULLSCREEN_TRUE, | ||
| 528 | GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE, | ||
| 529 | GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, | ||
| 530 | GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, | ||
| 531 | } ghostty_config_fullscreen_e; | ||
| 532 | |||
| 533 | // apprt.Target.Key | ||
| 534 | typedef enum { | ||
| 535 | GHOSTTY_TARGET_APP, | ||
| 536 | GHOSTTY_TARGET_SURFACE, | ||
| 537 | } ghostty_target_tag_e; | ||
| 538 | |||
| 539 | typedef union { | ||
| 540 | ghostty_surface_t surface; | ||
| 541 | } ghostty_target_u; | ||
| 542 | |||
| 543 | typedef struct { | ||
| 544 | ghostty_target_tag_e tag; | ||
| 545 | ghostty_target_u target; | ||
| 546 | } ghostty_target_s; | ||
| 547 | |||
| 548 | // apprt.action.SplitDirection | ||
| 549 | typedef enum { | ||
| 550 | GHOSTTY_SPLIT_DIRECTION_RIGHT, | ||
| 551 | GHOSTTY_SPLIT_DIRECTION_DOWN, | ||
| 552 | GHOSTTY_SPLIT_DIRECTION_LEFT, | ||
| 553 | GHOSTTY_SPLIT_DIRECTION_UP, | ||
| 554 | } ghostty_action_split_direction_e; | ||
| 555 | |||
| 556 | // apprt.action.GotoSplit | ||
| 557 | typedef enum { | ||
| 558 | GHOSTTY_GOTO_SPLIT_PREVIOUS, | ||
| 559 | GHOSTTY_GOTO_SPLIT_NEXT, | ||
| 560 | GHOSTTY_GOTO_SPLIT_UP, | ||
| 561 | GHOSTTY_GOTO_SPLIT_LEFT, | ||
| 562 | GHOSTTY_GOTO_SPLIT_DOWN, | ||
| 563 | GHOSTTY_GOTO_SPLIT_RIGHT, | ||
| 564 | } ghostty_action_goto_split_e; | ||
| 565 | |||
| 566 | // apprt.action.GotoWindow | ||
| 567 | typedef enum { | ||
| 568 | GHOSTTY_GOTO_WINDOW_PREVIOUS, | ||
| 569 | GHOSTTY_GOTO_WINDOW_NEXT, | ||
| 570 | } ghostty_action_goto_window_e; | ||
| 571 | |||
| 572 | // apprt.action.ResizeSplit.Direction | ||
| 573 | typedef enum { | ||
| 574 | GHOSTTY_RESIZE_SPLIT_UP, | ||
| 575 | GHOSTTY_RESIZE_SPLIT_DOWN, | ||
| 576 | GHOSTTY_RESIZE_SPLIT_LEFT, | ||
| 577 | GHOSTTY_RESIZE_SPLIT_RIGHT, | ||
| 578 | } ghostty_action_resize_split_direction_e; | ||
| 579 | |||
| 580 | // apprt.action.ResizeSplit | ||
| 581 | typedef struct { | ||
| 582 | uint16_t amount; | ||
| 583 | ghostty_action_resize_split_direction_e direction; | ||
| 584 | } ghostty_action_resize_split_s; | ||
| 585 | |||
| 586 | // apprt.action.MoveTab | ||
| 587 | typedef struct { | ||
| 588 | ssize_t amount; | ||
| 589 | } ghostty_action_move_tab_s; | ||
| 590 | |||
| 591 | // apprt.action.GotoTab | ||
| 592 | typedef enum { | ||
| 593 | GHOSTTY_GOTO_TAB_PREVIOUS = -1, | ||
| 594 | GHOSTTY_GOTO_TAB_NEXT = -2, | ||
| 595 | GHOSTTY_GOTO_TAB_LAST = -3, | ||
| 596 | } ghostty_action_goto_tab_e; | ||
| 597 | |||
| 598 | // apprt.action.Fullscreen | ||
| 599 | typedef enum { | ||
| 600 | GHOSTTY_FULLSCREEN_NATIVE, | ||
| 601 | GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE, | ||
| 602 | GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU, | ||
| 603 | GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH, | ||
| 604 | } ghostty_action_fullscreen_e; | ||
| 605 | |||
| 606 | // apprt.action.FloatWindow | ||
| 607 | typedef enum { | ||
| 608 | GHOSTTY_FLOAT_WINDOW_ON, | ||
| 609 | GHOSTTY_FLOAT_WINDOW_OFF, | ||
| 610 | GHOSTTY_FLOAT_WINDOW_TOGGLE, | ||
| 611 | } ghostty_action_float_window_e; | ||
| 612 | |||
| 613 | // apprt.action.SecureInput | ||
| 614 | typedef enum { | ||
| 615 | GHOSTTY_SECURE_INPUT_ON, | ||
| 616 | GHOSTTY_SECURE_INPUT_OFF, | ||
| 617 | GHOSTTY_SECURE_INPUT_TOGGLE, | ||
| 618 | } ghostty_action_secure_input_e; | ||
| 619 | |||
| 620 | // apprt.action.Inspector | ||
| 621 | typedef enum { | ||
| 622 | GHOSTTY_INSPECTOR_TOGGLE, | ||
| 623 | GHOSTTY_INSPECTOR_SHOW, | ||
| 624 | GHOSTTY_INSPECTOR_HIDE, | ||
| 625 | } ghostty_action_inspector_e; | ||
| 626 | |||
| 627 | // apprt.action.QuitTimer | ||
| 628 | typedef enum { | ||
| 629 | GHOSTTY_QUIT_TIMER_START, | ||
| 630 | GHOSTTY_QUIT_TIMER_STOP, | ||
| 631 | } ghostty_action_quit_timer_e; | ||
| 632 | |||
| 633 | // apprt.action.Readonly | ||
| 634 | typedef enum { | ||
| 635 | GHOSTTY_READONLY_OFF, | ||
| 636 | GHOSTTY_READONLY_ON, | ||
| 637 | } ghostty_action_readonly_e; | ||
| 638 | |||
| 639 | // apprt.action.DesktopNotification.C | ||
| 640 | typedef struct { | ||
| 641 | const char* title; | ||
| 642 | const char* body; | ||
| 643 | } ghostty_action_desktop_notification_s; | ||
| 644 | |||
| 645 | // apprt.action.SetTitle.C | ||
| 646 | typedef struct { | ||
| 647 | const char* title; | ||
| 648 | } ghostty_action_set_title_s; | ||
| 649 | |||
| 650 | // apprt.action.PromptTitle | ||
| 651 | typedef enum { | ||
| 652 | GHOSTTY_PROMPT_TITLE_SURFACE, | ||
| 653 | GHOSTTY_PROMPT_TITLE_TAB, | ||
| 654 | } ghostty_action_prompt_title_e; | ||
| 655 | |||
| 656 | // apprt.action.Pwd.C | ||
| 657 | typedef struct { | ||
| 658 | const char* pwd; | ||
| 659 | } ghostty_action_pwd_s; | ||
| 660 | |||
| 661 | // terminal.MouseShape | ||
| 662 | typedef enum { | ||
| 663 | GHOSTTY_MOUSE_SHAPE_DEFAULT, | ||
| 664 | GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU, | ||
| 665 | GHOSTTY_MOUSE_SHAPE_HELP, | ||
| 666 | GHOSTTY_MOUSE_SHAPE_POINTER, | ||
| 667 | GHOSTTY_MOUSE_SHAPE_PROGRESS, | ||
| 668 | GHOSTTY_MOUSE_SHAPE_WAIT, | ||
| 669 | GHOSTTY_MOUSE_SHAPE_CELL, | ||
| 670 | GHOSTTY_MOUSE_SHAPE_CROSSHAIR, | ||
| 671 | GHOSTTY_MOUSE_SHAPE_TEXT, | ||
| 672 | GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT, | ||
| 673 | GHOSTTY_MOUSE_SHAPE_ALIAS, | ||
| 674 | GHOSTTY_MOUSE_SHAPE_COPY, | ||
| 675 | GHOSTTY_MOUSE_SHAPE_MOVE, | ||
| 676 | GHOSTTY_MOUSE_SHAPE_NO_DROP, | ||
| 677 | GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED, | ||
| 678 | GHOSTTY_MOUSE_SHAPE_GRAB, | ||
| 679 | GHOSTTY_MOUSE_SHAPE_GRABBING, | ||
| 680 | GHOSTTY_MOUSE_SHAPE_ALL_SCROLL, | ||
| 681 | GHOSTTY_MOUSE_SHAPE_COL_RESIZE, | ||
| 682 | GHOSTTY_MOUSE_SHAPE_ROW_RESIZE, | ||
| 683 | GHOSTTY_MOUSE_SHAPE_N_RESIZE, | ||
| 684 | GHOSTTY_MOUSE_SHAPE_E_RESIZE, | ||
| 685 | GHOSTTY_MOUSE_SHAPE_S_RESIZE, | ||
| 686 | GHOSTTY_MOUSE_SHAPE_W_RESIZE, | ||
| 687 | GHOSTTY_MOUSE_SHAPE_NE_RESIZE, | ||
| 688 | GHOSTTY_MOUSE_SHAPE_NW_RESIZE, | ||
| 689 | GHOSTTY_MOUSE_SHAPE_SE_RESIZE, | ||
| 690 | GHOSTTY_MOUSE_SHAPE_SW_RESIZE, | ||
| 691 | GHOSTTY_MOUSE_SHAPE_EW_RESIZE, | ||
| 692 | GHOSTTY_MOUSE_SHAPE_NS_RESIZE, | ||
| 693 | GHOSTTY_MOUSE_SHAPE_NESW_RESIZE, | ||
| 694 | GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE, | ||
| 695 | GHOSTTY_MOUSE_SHAPE_ZOOM_IN, | ||
| 696 | GHOSTTY_MOUSE_SHAPE_ZOOM_OUT, | ||
| 697 | } ghostty_action_mouse_shape_e; | ||
| 698 | |||
| 699 | // apprt.action.MouseVisibility | ||
| 700 | typedef enum { | ||
| 701 | GHOSTTY_MOUSE_VISIBLE, | ||
| 702 | GHOSTTY_MOUSE_HIDDEN, | ||
| 703 | } ghostty_action_mouse_visibility_e; | ||
| 704 | |||
| 705 | // apprt.action.MouseOverLink | ||
| 706 | typedef struct { | ||
| 707 | const char* url; | ||
| 708 | size_t len; | ||
| 709 | } ghostty_action_mouse_over_link_s; | ||
| 710 | |||
| 711 | // apprt.action.SizeLimit | ||
| 712 | typedef struct { | ||
| 713 | uint32_t min_width; | ||
| 714 | uint32_t min_height; | ||
| 715 | uint32_t max_width; | ||
| 716 | uint32_t max_height; | ||
| 717 | } ghostty_action_size_limit_s; | ||
| 718 | |||
| 719 | // apprt.action.InitialSize | ||
| 720 | typedef struct { | ||
| 721 | uint32_t width; | ||
| 722 | uint32_t height; | ||
| 723 | } ghostty_action_initial_size_s; | ||
| 724 | |||
| 725 | // apprt.action.CellSize | ||
| 726 | typedef struct { | ||
| 727 | uint32_t width; | ||
| 728 | uint32_t height; | ||
| 729 | } ghostty_action_cell_size_s; | ||
| 730 | |||
| 731 | // renderer.Health | ||
| 732 | typedef enum { | ||
| 733 | GHOSTTY_RENDERER_HEALTH_HEALTHY, | ||
| 734 | GHOSTTY_RENDERER_HEALTH_UNHEALTHY, | ||
| 735 | } ghostty_action_renderer_health_e; | ||
| 736 | |||
| 737 | // apprt.action.KeySequence | ||
| 738 | typedef struct { | ||
| 739 | bool active; | ||
| 740 | ghostty_input_trigger_s trigger; | ||
| 741 | } ghostty_action_key_sequence_s; | ||
| 742 | |||
| 743 | // apprt.action.KeyTable.Tag | ||
| 744 | typedef enum { | ||
| 745 | GHOSTTY_KEY_TABLE_ACTIVATE, | ||
| 746 | GHOSTTY_KEY_TABLE_DEACTIVATE, | ||
| 747 | GHOSTTY_KEY_TABLE_DEACTIVATE_ALL, | ||
| 748 | } ghostty_action_key_table_tag_e; | ||
| 749 | |||
| 750 | // apprt.action.KeyTable.CValue | ||
| 751 | typedef union { | ||
| 752 | struct { | ||
| 753 | const char *name; | ||
| 754 | size_t len; | ||
| 755 | } activate; | ||
| 756 | } ghostty_action_key_table_u; | ||
| 757 | |||
| 758 | // apprt.action.KeyTable.C | ||
| 759 | typedef struct { | ||
| 760 | ghostty_action_key_table_tag_e tag; | ||
| 761 | ghostty_action_key_table_u value; | ||
| 762 | } ghostty_action_key_table_s; | ||
| 763 | |||
| 764 | // apprt.action.ColorKind | ||
| 765 | typedef enum { | ||
| 766 | GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1, | ||
| 767 | GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2, | ||
| 768 | GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3, | ||
| 769 | } ghostty_action_color_kind_e; | ||
| 770 | |||
| 771 | // apprt.action.ColorChange | ||
| 772 | typedef struct { | ||
| 773 | ghostty_action_color_kind_e kind; | ||
| 774 | uint8_t r; | ||
| 775 | uint8_t g; | ||
| 776 | uint8_t b; | ||
| 777 | } ghostty_action_color_change_s; | ||
| 778 | |||
| 779 | // apprt.action.ConfigChange | ||
| 780 | typedef struct { | ||
| 781 | ghostty_config_t config; | ||
| 782 | } ghostty_action_config_change_s; | ||
| 783 | |||
| 784 | // apprt.action.ReloadConfig | ||
| 785 | typedef struct { | ||
| 786 | bool soft; | ||
| 787 | } ghostty_action_reload_config_s; | ||
| 788 | |||
| 789 | // apprt.action.OpenUrlKind | ||
| 790 | typedef enum { | ||
| 791 | GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, | ||
| 792 | GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, | ||
| 793 | GHOSTTY_ACTION_OPEN_URL_KIND_HTML, | ||
| 794 | } ghostty_action_open_url_kind_e; | ||
| 795 | |||
| 796 | // apprt.action.OpenUrl.C | ||
| 797 | typedef struct { | ||
| 798 | ghostty_action_open_url_kind_e kind; | ||
| 799 | const char* url; | ||
| 800 | uintptr_t len; | ||
| 801 | } ghostty_action_open_url_s; | ||
| 802 | |||
| 803 | // apprt.action.CloseTabMode | ||
| 804 | typedef enum { | ||
| 805 | GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, | ||
| 806 | GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, | ||
| 807 | GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT, | ||
| 808 | } ghostty_action_close_tab_mode_e; | ||
| 809 | |||
| 810 | // apprt.surface.Message.ChildExited | ||
| 811 | typedef struct { | ||
| 812 | uint32_t exit_code; | ||
| 813 | uint64_t timetime_ms; | ||
| 814 | } ghostty_surface_message_childexited_s; | ||
| 815 | |||
| 816 | // terminal.osc.Command.ProgressReport.State | ||
| 817 | typedef enum { | ||
| 818 | GHOSTTY_PROGRESS_STATE_REMOVE, | ||
| 819 | GHOSTTY_PROGRESS_STATE_SET, | ||
| 820 | GHOSTTY_PROGRESS_STATE_ERROR, | ||
| 821 | GHOSTTY_PROGRESS_STATE_INDETERMINATE, | ||
| 822 | GHOSTTY_PROGRESS_STATE_PAUSE, | ||
| 823 | } ghostty_action_progress_report_state_e; | ||
| 824 | |||
| 825 | // terminal.osc.Command.ProgressReport.C | ||
| 826 | typedef struct { | ||
| 827 | ghostty_action_progress_report_state_e state; | ||
| 828 | // -1 if no progress was reported, otherwise 0-100 indicating percent | ||
| 829 | // completeness. | ||
| 830 | int8_t progress; | ||
| 831 | } ghostty_action_progress_report_s; | ||
| 832 | |||
| 833 | // apprt.action.CommandFinished.C | ||
| 834 | typedef struct { | ||
| 835 | // -1 if no exit code was reported, otherwise 0-255 | ||
| 836 | int16_t exit_code; | ||
| 837 | // number of nanoseconds that command was running for | ||
| 838 | uint64_t duration; | ||
| 839 | } ghostty_action_command_finished_s; | ||
| 840 | |||
| 841 | // apprt.action.StartSearch.C | ||
| 842 | typedef struct { | ||
| 843 | const char* needle; | ||
| 844 | } ghostty_action_start_search_s; | ||
| 845 | |||
| 846 | // apprt.action.SearchTotal | ||
| 847 | typedef struct { | ||
| 848 | ssize_t total; | ||
| 849 | } ghostty_action_search_total_s; | ||
| 850 | |||
| 851 | // apprt.action.SearchSelected | ||
| 852 | typedef struct { | ||
| 853 | ssize_t selected; | ||
| 854 | } ghostty_action_search_selected_s; | ||
| 855 | |||
| 856 | // terminal.Scrollbar | ||
| 857 | typedef struct { | ||
| 858 | uint64_t total; | ||
| 859 | uint64_t offset; | ||
| 860 | uint64_t len; | ||
| 861 | } ghostty_action_scrollbar_s; | ||
| 862 | |||
| 863 | // apprt.Action.Key | ||
| 864 | typedef enum { | ||
| 865 | GHOSTTY_ACTION_QUIT, | ||
| 866 | GHOSTTY_ACTION_NEW_WINDOW, | ||
| 867 | GHOSTTY_ACTION_NEW_TAB, | ||
| 868 | GHOSTTY_ACTION_CLOSE_TAB, | ||
| 869 | GHOSTTY_ACTION_NEW_SPLIT, | ||
| 870 | GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, | ||
| 871 | GHOSTTY_ACTION_TOGGLE_MAXIMIZE, | ||
| 872 | GHOSTTY_ACTION_TOGGLE_FULLSCREEN, | ||
| 873 | GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW, | ||
| 874 | GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, | ||
| 875 | GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, | ||
| 876 | GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE, | ||
| 877 | GHOSTTY_ACTION_TOGGLE_VISIBILITY, | ||
| 878 | GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY, | ||
| 879 | GHOSTTY_ACTION_MOVE_TAB, | ||
| 880 | GHOSTTY_ACTION_GOTO_TAB, | ||
| 881 | GHOSTTY_ACTION_GOTO_SPLIT, | ||
| 882 | GHOSTTY_ACTION_GOTO_WINDOW, | ||
| 883 | GHOSTTY_ACTION_RESIZE_SPLIT, | ||
| 884 | GHOSTTY_ACTION_EQUALIZE_SPLITS, | ||
| 885 | GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM, | ||
| 886 | GHOSTTY_ACTION_PRESENT_TERMINAL, | ||
| 887 | GHOSTTY_ACTION_SIZE_LIMIT, | ||
| 888 | GHOSTTY_ACTION_RESET_WINDOW_SIZE, | ||
| 889 | GHOSTTY_ACTION_INITIAL_SIZE, | ||
| 890 | GHOSTTY_ACTION_CELL_SIZE, | ||
| 891 | GHOSTTY_ACTION_SCROLLBAR, | ||
| 892 | GHOSTTY_ACTION_RENDER, | ||
| 893 | GHOSTTY_ACTION_INSPECTOR, | ||
| 894 | GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, | ||
| 895 | GHOSTTY_ACTION_RENDER_INSPECTOR, | ||
| 896 | GHOSTTY_ACTION_DESKTOP_NOTIFICATION, | ||
| 897 | GHOSTTY_ACTION_SET_TITLE, | ||
| 898 | GHOSTTY_ACTION_PROMPT_TITLE, | ||
| 899 | GHOSTTY_ACTION_PWD, | ||
| 900 | GHOSTTY_ACTION_MOUSE_SHAPE, | ||
| 901 | GHOSTTY_ACTION_MOUSE_VISIBILITY, | ||
| 902 | GHOSTTY_ACTION_MOUSE_OVER_LINK, | ||
| 903 | GHOSTTY_ACTION_RENDERER_HEALTH, | ||
| 904 | GHOSTTY_ACTION_OPEN_CONFIG, | ||
| 905 | GHOSTTY_ACTION_QUIT_TIMER, | ||
| 906 | GHOSTTY_ACTION_FLOAT_WINDOW, | ||
| 907 | GHOSTTY_ACTION_SECURE_INPUT, | ||
| 908 | GHOSTTY_ACTION_KEY_SEQUENCE, | ||
| 909 | GHOSTTY_ACTION_KEY_TABLE, | ||
| 910 | GHOSTTY_ACTION_COLOR_CHANGE, | ||
| 911 | GHOSTTY_ACTION_RELOAD_CONFIG, | ||
| 912 | GHOSTTY_ACTION_CONFIG_CHANGE, | ||
| 913 | GHOSTTY_ACTION_CLOSE_WINDOW, | ||
| 914 | GHOSTTY_ACTION_RING_BELL, | ||
| 915 | GHOSTTY_ACTION_UNDO, | ||
| 916 | GHOSTTY_ACTION_REDO, | ||
| 917 | GHOSTTY_ACTION_CHECK_FOR_UPDATES, | ||
| 918 | GHOSTTY_ACTION_OPEN_URL, | ||
| 919 | GHOSTTY_ACTION_SHOW_CHILD_EXITED, | ||
| 920 | GHOSTTY_ACTION_PROGRESS_REPORT, | ||
| 921 | GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, | ||
| 922 | GHOSTTY_ACTION_COMMAND_FINISHED, | ||
| 923 | GHOSTTY_ACTION_START_SEARCH, | ||
| 924 | GHOSTTY_ACTION_END_SEARCH, | ||
| 925 | GHOSTTY_ACTION_SEARCH_TOTAL, | ||
| 926 | GHOSTTY_ACTION_SEARCH_SELECTED, | ||
| 927 | GHOSTTY_ACTION_READONLY, | ||
| 928 | GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD, | ||
| 929 | } ghostty_action_tag_e; | ||
| 930 | |||
| 931 | typedef union { | ||
| 932 | ghostty_action_split_direction_e new_split; | ||
| 933 | ghostty_action_fullscreen_e toggle_fullscreen; | ||
| 934 | ghostty_action_move_tab_s move_tab; | ||
| 935 | ghostty_action_goto_tab_e goto_tab; | ||
| 936 | ghostty_action_goto_split_e goto_split; | ||
| 937 | ghostty_action_goto_window_e goto_window; | ||
| 938 | ghostty_action_resize_split_s resize_split; | ||
| 939 | ghostty_action_size_limit_s size_limit; | ||
| 940 | ghostty_action_initial_size_s initial_size; | ||
| 941 | ghostty_action_cell_size_s cell_size; | ||
| 942 | ghostty_action_scrollbar_s scrollbar; | ||
| 943 | ghostty_action_inspector_e inspector; | ||
| 944 | ghostty_action_desktop_notification_s desktop_notification; | ||
| 945 | ghostty_action_set_title_s set_title; | ||
| 946 | ghostty_action_prompt_title_e prompt_title; | ||
| 947 | ghostty_action_pwd_s pwd; | ||
| 948 | ghostty_action_mouse_shape_e mouse_shape; | ||
| 949 | ghostty_action_mouse_visibility_e mouse_visibility; | ||
| 950 | ghostty_action_mouse_over_link_s mouse_over_link; | ||
| 951 | ghostty_action_renderer_health_e renderer_health; | ||
| 952 | ghostty_action_quit_timer_e quit_timer; | ||
| 953 | ghostty_action_float_window_e float_window; | ||
| 954 | ghostty_action_secure_input_e secure_input; | ||
| 955 | ghostty_action_key_sequence_s key_sequence; | ||
| 956 | ghostty_action_key_table_s key_table; | ||
| 957 | ghostty_action_color_change_s color_change; | ||
| 958 | ghostty_action_reload_config_s reload_config; | ||
| 959 | ghostty_action_config_change_s config_change; | ||
| 960 | ghostty_action_open_url_s open_url; | ||
| 961 | ghostty_action_close_tab_mode_e close_tab_mode; | ||
| 962 | ghostty_surface_message_childexited_s child_exited; | ||
| 963 | ghostty_action_progress_report_s progress_report; | ||
| 964 | ghostty_action_command_finished_s command_finished; | ||
| 965 | ghostty_action_start_search_s start_search; | ||
| 966 | ghostty_action_search_total_s search_total; | ||
| 967 | ghostty_action_search_selected_s search_selected; | ||
| 968 | ghostty_action_readonly_e readonly; | ||
| 969 | } ghostty_action_u; | ||
| 970 | |||
| 971 | typedef struct { | ||
| 972 | ghostty_action_tag_e tag; | ||
| 973 | ghostty_action_u action; | ||
| 974 | } ghostty_action_s; | ||
| 975 | |||
| 976 | typedef void (*ghostty_runtime_wakeup_cb)(void*); | ||
| 977 | typedef void (*ghostty_runtime_read_clipboard_cb)(void*, | ||
| 978 | ghostty_clipboard_e, | ||
| 979 | void*); | ||
| 980 | typedef void (*ghostty_runtime_confirm_read_clipboard_cb)( | ||
| 981 | void*, | ||
| 982 | const char*, | ||
| 983 | void*, | ||
| 984 | ghostty_clipboard_request_e); | ||
| 985 | typedef void (*ghostty_runtime_write_clipboard_cb)(void*, | ||
| 986 | ghostty_clipboard_e, | ||
| 987 | const ghostty_clipboard_content_s*, | ||
| 988 | size_t, | ||
| 989 | bool); | ||
| 990 | typedef void (*ghostty_runtime_close_surface_cb)(void*, bool); | ||
| 991 | typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t, | ||
| 992 | ghostty_target_s, | ||
| 993 | ghostty_action_s); | ||
| 994 | |||
| 995 | typedef struct { | ||
| 996 | void* userdata; | ||
| 997 | bool supports_selection_clipboard; | ||
| 998 | ghostty_runtime_wakeup_cb wakeup_cb; | ||
| 999 | ghostty_runtime_action_cb action_cb; | ||
| 1000 | ghostty_runtime_read_clipboard_cb read_clipboard_cb; | ||
| 1001 | ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; | ||
| 1002 | ghostty_runtime_write_clipboard_cb write_clipboard_cb; | ||
| 1003 | ghostty_runtime_close_surface_cb close_surface_cb; | ||
| 1004 | } ghostty_runtime_config_s; | ||
| 1005 | |||
| 1006 | // apprt.ipc.Target.Key | ||
| 1007 | typedef enum { | ||
| 1008 | GHOSTTY_IPC_TARGET_CLASS, | ||
| 1009 | GHOSTTY_IPC_TARGET_DETECT, | ||
| 1010 | } ghostty_ipc_target_tag_e; | ||
| 1011 | |||
| 1012 | typedef union { | ||
| 1013 | char *klass; | ||
| 1014 | } ghostty_ipc_target_u; | ||
| 1015 | |||
| 1016 | typedef struct { | ||
| 1017 | ghostty_ipc_target_tag_e tag; | ||
| 1018 | ghostty_ipc_target_u target; | ||
| 1019 | } chostty_ipc_target_s; | ||
| 1020 | |||
| 1021 | // apprt.ipc.Action.NewWindow | ||
| 1022 | typedef struct { | ||
| 1023 | // This should be a null terminated list of strings. | ||
| 1024 | const char **arguments; | ||
| 1025 | } ghostty_ipc_action_new_window_s; | ||
| 1026 | |||
| 1027 | typedef union { | ||
| 1028 | ghostty_ipc_action_new_window_s new_window; | ||
| 1029 | } ghostty_ipc_action_u; | ||
| 1030 | |||
| 1031 | // apprt.ipc.Action.Key | ||
| 1032 | typedef enum { | ||
| 1033 | GHOSTTY_IPC_ACTION_NEW_WINDOW, | ||
| 1034 | } ghostty_ipc_action_tag_e; | ||
| 1035 | |||
| 1036 | //------------------------------------------------------------------- | ||
| 1037 | // Published API | ||
| 1038 | |||
| 1039 | int ghostty_init(uintptr_t, char**); | ||
| 1040 | void ghostty_cli_try_action(void); | ||
| 1041 | ghostty_info_s ghostty_info(void); | ||
| 1042 | const char* ghostty_translate(const char*); | ||
| 1043 | void ghostty_string_free(ghostty_string_s); | ||
| 1044 | |||
| 1045 | ghostty_config_t ghostty_config_new(); | ||
| 1046 | void ghostty_config_free(ghostty_config_t); | ||
| 1047 | ghostty_config_t ghostty_config_clone(ghostty_config_t); | ||
| 1048 | void ghostty_config_load_cli_args(ghostty_config_t); | ||
| 1049 | void ghostty_config_load_file(ghostty_config_t, const char*); | ||
| 1050 | void ghostty_config_load_default_files(ghostty_config_t); | ||
| 1051 | void ghostty_config_load_recursive_files(ghostty_config_t); | ||
| 1052 | void ghostty_config_finalize(ghostty_config_t); | ||
| 1053 | bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t); | ||
| 1054 | ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, | ||
| 1055 | const char*, | ||
| 1056 | uintptr_t); | ||
| 1057 | uint32_t ghostty_config_diagnostics_count(ghostty_config_t); | ||
| 1058 | ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); | ||
| 1059 | ghostty_string_s ghostty_config_open_path(void); | ||
| 1060 | |||
| 1061 | ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, | ||
| 1062 | ghostty_config_t); | ||
| 1063 | void ghostty_app_free(ghostty_app_t); | ||
| 1064 | void ghostty_app_tick(ghostty_app_t); | ||
| 1065 | void* ghostty_app_userdata(ghostty_app_t); | ||
| 1066 | void ghostty_app_set_focus(ghostty_app_t, bool); | ||
| 1067 | bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); | ||
| 1068 | bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s); | ||
| 1069 | void ghostty_app_keyboard_changed(ghostty_app_t); | ||
| 1070 | void ghostty_app_open_config(ghostty_app_t); | ||
| 1071 | void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); | ||
| 1072 | bool ghostty_app_needs_confirm_quit(ghostty_app_t); | ||
| 1073 | bool ghostty_app_has_global_keybinds(ghostty_app_t); | ||
| 1074 | void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); | ||
| 1075 | |||
| 1076 | ghostty_surface_config_s ghostty_surface_config_new(); | ||
| 1077 | |||
| 1078 | ghostty_surface_t ghostty_surface_new(ghostty_app_t, | ||
| 1079 | const ghostty_surface_config_s*); | ||
| 1080 | void ghostty_surface_free(ghostty_surface_t); | ||
| 1081 | void* ghostty_surface_userdata(ghostty_surface_t); | ||
| 1082 | ghostty_app_t ghostty_surface_app(ghostty_surface_t); | ||
| 1083 | ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); | ||
| 1084 | void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); | ||
| 1085 | bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); | ||
| 1086 | bool ghostty_surface_process_exited(ghostty_surface_t); | ||
| 1087 | void ghostty_surface_refresh(ghostty_surface_t); | ||
| 1088 | void ghostty_surface_draw(ghostty_surface_t); | ||
| 1089 | // Custom I/O API - feed output into the terminal and receive writes. | ||
| 1090 | void ghostty_surface_feed_data(ghostty_surface_t, const uint8_t*, size_t); | ||
| 1091 | void ghostty_surface_set_write_callback(ghostty_surface_t, | ||
| 1092 | ghostty_surface_write_fn, | ||
| 1093 | void*); | ||
| 1094 | void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); | ||
| 1095 | void ghostty_surface_set_focus(ghostty_surface_t, bool); | ||
| 1096 | void ghostty_surface_set_occlusion(ghostty_surface_t, bool); | ||
| 1097 | void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); | ||
| 1098 | ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); | ||
| 1099 | void ghostty_surface_set_color_scheme(ghostty_surface_t, | ||
| 1100 | ghostty_color_scheme_e); | ||
| 1101 | ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, | ||
| 1102 | ghostty_input_mods_e); | ||
| 1103 | bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); | ||
| 1104 | bool ghostty_surface_key_is_binding(ghostty_surface_t, | ||
| 1105 | ghostty_input_key_s, | ||
| 1106 | ghostty_binding_flags_e*); | ||
| 1107 | void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); | ||
| 1108 | void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); | ||
| 1109 | bool ghostty_surface_mouse_captured(ghostty_surface_t); | ||
| 1110 | bool ghostty_surface_mouse_button(ghostty_surface_t, | ||
| 1111 | ghostty_input_mouse_state_e, | ||
| 1112 | ghostty_input_mouse_button_e, | ||
| 1113 | ghostty_input_mods_e); | ||
| 1114 | void ghostty_surface_mouse_pos(ghostty_surface_t, | ||
| 1115 | double, | ||
| 1116 | double, | ||
| 1117 | ghostty_input_mods_e); | ||
| 1118 | void ghostty_surface_mouse_scroll(ghostty_surface_t, | ||
| 1119 | double, | ||
| 1120 | double, | ||
| 1121 | ghostty_input_scroll_mods_t); | ||
| 1122 | void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); | ||
| 1123 | void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*); | ||
| 1124 | void ghostty_surface_request_close(ghostty_surface_t); | ||
| 1125 | void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); | ||
| 1126 | void ghostty_surface_split_focus(ghostty_surface_t, | ||
| 1127 | ghostty_action_goto_split_e); | ||
| 1128 | void ghostty_surface_split_resize(ghostty_surface_t, | ||
| 1129 | ghostty_action_resize_split_direction_e, | ||
| 1130 | uint16_t); | ||
| 1131 | void ghostty_surface_split_equalize(ghostty_surface_t); | ||
| 1132 | bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); | ||
| 1133 | void ghostty_surface_complete_clipboard_request(ghostty_surface_t, | ||
| 1134 | const char*, | ||
| 1135 | void*, | ||
| 1136 | bool); | ||
| 1137 | bool ghostty_surface_has_selection(ghostty_surface_t); | ||
| 1138 | bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); | ||
| 1139 | bool ghostty_surface_read_text(ghostty_surface_t, | ||
| 1140 | ghostty_selection_s, | ||
| 1141 | ghostty_text_s*); | ||
| 1142 | void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); | ||
| 1143 | |||
| 1144 | #ifdef __APPLE__ | ||
| 1145 | void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); | ||
| 1146 | void* ghostty_surface_quicklook_font(ghostty_surface_t); | ||
| 1147 | bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); | ||
| 1148 | #endif | ||
| 1149 | |||
| 1150 | ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); | ||
| 1151 | void ghostty_inspector_free(ghostty_surface_t); | ||
| 1152 | void ghostty_inspector_set_focus(ghostty_inspector_t, bool); | ||
| 1153 | void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double); | ||
| 1154 | void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t); | ||
| 1155 | void ghostty_inspector_mouse_button(ghostty_inspector_t, | ||
| 1156 | ghostty_input_mouse_state_e, | ||
| 1157 | ghostty_input_mouse_button_e, | ||
| 1158 | ghostty_input_mods_e); | ||
| 1159 | void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double); | ||
| 1160 | void ghostty_inspector_mouse_scroll(ghostty_inspector_t, | ||
| 1161 | double, | ||
| 1162 | double, | ||
| 1163 | ghostty_input_scroll_mods_t); | ||
| 1164 | void ghostty_inspector_key(ghostty_inspector_t, | ||
| 1165 | ghostty_input_action_e, | ||
| 1166 | ghostty_input_key_e, | ||
| 1167 | ghostty_input_mods_e); | ||
| 1168 | void ghostty_inspector_text(ghostty_inspector_t, const char*); | ||
| 1169 | |||
| 1170 | #ifdef __APPLE__ | ||
| 1171 | bool ghostty_inspector_metal_init(ghostty_inspector_t, void*); | ||
| 1172 | void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*); | ||
| 1173 | bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); | ||
| 1174 | #endif | ||
| 1175 | |||
| 1176 | // APIs I'd like to get rid of eventually but are still needed for now. | ||
| 1177 | // Don't use these unless you know what you're doing. | ||
| 1178 | void ghostty_set_window_background_blur(ghostty_app_t, void*); | ||
| 1179 | |||
| 1180 | // Benchmark API, if available. | ||
| 1181 | bool ghostty_benchmark_cli(const char*, const char*); | ||
| 1182 | |||
| 1183 | #ifdef __cplusplus | ||
| 1184 | } | ||
| 1185 | #endif | ||
| 1186 | |||
| 1187 | #endif /* GHOSTTY_H */ | ||
| @@ -0,0 +1,87 @@ | |||
| 1 | /** | ||
| 2 | * @file vt.h | ||
| 3 | * | ||
| 4 | * libghostty-vt - Virtual terminal emulator library | ||
| 5 | * | ||
| 6 | * This library provides functionality for parsing and handling terminal | ||
| 7 | * escape sequences as well as maintaining terminal state such as styles, | ||
| 8 | * cursor position, screen, scrollback, and more. | ||
| 9 | * | ||
| 10 | * WARNING: This is an incomplete, work-in-progress API. It is not yet | ||
| 11 | * stable and is definitely going to change. | ||
| 12 | */ | ||
| 13 | |||
| 14 | /** | ||
| 15 | * @mainpage libghostty-vt - Virtual Terminal Emulator Library | ||
| 16 | * | ||
| 17 | * libghostty-vt is a C library which implements a modern terminal emulator, | ||
| 18 | * extracted from the [Ghostty](https://ghostty.org) terminal emulator. | ||
| 19 | * | ||
| 20 | * libghostty-vt contains the logic for handling the core parts of a terminal | ||
| 21 | * emulator: parsing terminal escape sequences, maintaining terminal state, | ||
| 22 | * encoding input events, etc. It can handle scrollback, line wrapping, | ||
| 23 | * reflow on resize, and more. | ||
| 24 | * | ||
| 25 | * @warning This library is currently in development and the API is not yet stable. | ||
| 26 | * Breaking changes are expected in future versions. Use with caution in production code. | ||
| 27 | * | ||
| 28 | * @section groups_sec API Reference | ||
| 29 | * | ||
| 30 | * The API is organized into the following groups: | ||
| 31 | * - @ref key "Key Encoding" - Encode key events into terminal sequences | ||
| 32 | * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences | ||
| 33 | * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences | ||
| 34 | * - @ref paste "Paste Utilities" - Validate paste data safety | ||
| 35 | * - @ref allocator "Memory Management" - Memory management and custom allocators | ||
| 36 | * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions | ||
| 37 | * | ||
| 38 | * @section examples_sec Examples | ||
| 39 | * | ||
| 40 | * Complete working examples: | ||
| 41 | * - @ref c-vt/src/main.c - OSC parser example | ||
| 42 | * - @ref c-vt-key-encode/src/main.c - Key encoding example | ||
| 43 | * - @ref c-vt-paste/src/main.c - Paste safety check example | ||
| 44 | * - @ref c-vt-sgr/src/main.c - SGR parser example | ||
| 45 | * | ||
| 46 | */ | ||
| 47 | |||
| 48 | /** @example c-vt/src/main.c | ||
| 49 | * This example demonstrates how to use the OSC parser to parse an OSC sequence, | ||
| 50 | * extract command information, and retrieve command-specific data like window titles. | ||
| 51 | */ | ||
| 52 | |||
| 53 | /** @example c-vt-key-encode/src/main.c | ||
| 54 | * This example demonstrates how to use the key encoder to convert key events | ||
| 55 | * into terminal escape sequences using the Kitty keyboard protocol. | ||
| 56 | */ | ||
| 57 | |||
| 58 | /** @example c-vt-paste/src/main.c | ||
| 59 | * This example demonstrates how to use the paste utilities to check if | ||
| 60 | * paste data is safe before sending it to the terminal. | ||
| 61 | */ | ||
| 62 | |||
| 63 | /** @example c-vt-sgr/src/main.c | ||
| 64 | * This example demonstrates how to use the SGR parser to parse terminal | ||
| 65 | * styling sequences and extract text attributes like colors and underline styles. | ||
| 66 | */ | ||
| 67 | |||
| 68 | #ifndef GHOSTTY_VT_H | ||
| 69 | #define GHOSTTY_VT_H | ||
| 70 | |||
| 71 | #ifdef __cplusplus | ||
| 72 | extern "C" { | ||
| 73 | #endif | ||
| 74 | |||
| 75 | #include <ghostty/vt/result.h> | ||
| 76 | #include <ghostty/vt/allocator.h> | ||
| 77 | #include <ghostty/vt/osc.h> | ||
| 78 | #include <ghostty/vt/sgr.h> | ||
| 79 | #include <ghostty/vt/key.h> | ||
| 80 | #include <ghostty/vt/paste.h> | ||
| 81 | #include <ghostty/vt/wasm.h> | ||
| 82 | |||
| 83 | #ifdef __cplusplus | ||
| 84 | } | ||
| 85 | #endif | ||
| 86 | |||
| 87 | #endif /* GHOSTTY_VT_H */ | ||
| @@ -0,0 +1,196 @@ | |||
| 1 | /** | ||
| 2 | * @file allocator.h | ||
| 3 | * | ||
| 4 | * Memory management interface for libghostty-vt. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_ALLOCATOR_H | ||
| 8 | #define GHOSTTY_VT_ALLOCATOR_H | ||
| 9 | |||
| 10 | #include <stdbool.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <stdint.h> | ||
| 13 | |||
| 14 | /** @defgroup allocator Memory Management | ||
| 15 | * | ||
| 16 | * libghostty-vt does require memory allocation for various operations, | ||
| 17 | * but is resilient to allocation failures and will gracefully handle | ||
| 18 | * out-of-memory situations by returning error codes. | ||
| 19 | * | ||
| 20 | * The exact memory management semantics are documented in the relevant | ||
| 21 | * functions and data structures. | ||
| 22 | * | ||
| 23 | * libghostty-vt uses explicit memory allocation via an allocator | ||
| 24 | * interface provided by GhosttyAllocator. The interface is based on the | ||
| 25 | * [Zig](https://ziglang.org) allocator interface, since this has been | ||
| 26 | * shown to be a flexible and powerful interface in practice and enables | ||
| 27 | * a wide variety of allocation strategies. | ||
| 28 | * | ||
| 29 | * **For the common case, you can pass NULL as the allocator for any | ||
| 30 | * function that accepts one,** and libghostty will use a default allocator. | ||
| 31 | * The default allocator will be libc malloc/free if libc is linked. | ||
| 32 | * Otherwise, a custom allocator is used (currently Zig's SMP allocator) | ||
| 33 | * that doesn't require any external dependencies. | ||
| 34 | * | ||
| 35 | * ## Basic Usage | ||
| 36 | * | ||
| 37 | * For simple use cases, you can ignore this interface entirely by passing NULL | ||
| 38 | * as the allocator parameter to functions that accept one. This will use the | ||
| 39 | * default allocator (typically libc malloc/free, if libc is linked, but | ||
| 40 | * we provide our own default allocator if libc isn't linked). | ||
| 41 | * | ||
| 42 | * To use a custom allocator: | ||
| 43 | * 1. Implement the GhosttyAllocatorVtable function pointers | ||
| 44 | * 2. Create a GhosttyAllocator struct with your vtable and context | ||
| 45 | * 3. Pass the allocator to functions that accept one | ||
| 46 | * | ||
| 47 | * @{ | ||
| 48 | */ | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Function table for custom memory allocator operations. | ||
| 52 | * | ||
| 53 | * This vtable defines the interface for a custom memory allocator. All | ||
| 54 | * function pointers must be valid and non-NULL. | ||
| 55 | * | ||
| 56 | * @ingroup allocator | ||
| 57 | * | ||
| 58 | * If you're not going to use a custom allocator, you can ignore all of | ||
| 59 | * this. All functions that take an allocator pointer allow NULL to use a | ||
| 60 | * default allocator. | ||
| 61 | * | ||
| 62 | * The interface is based on the Zig allocator interface. I'll say up front | ||
| 63 | * that it is easy to look at this interface and think "wow, this is really | ||
| 64 | * overcomplicated". The reason for this complexity is well thought out by | ||
| 65 | * the Zig folks, and it enables a diverse set of allocation strategies | ||
| 66 | * as shown by the Zig ecosystem. As a consolation, please note that many | ||
| 67 | * of the arguments are only needed for advanced use cases and can be | ||
| 68 | * safely ignored in simple implementations. For example, if you look at | ||
| 69 | * the Zig implementation of the libc allocator in `lib/std/heap.zig` | ||
| 70 | * (search for CAllocator), you'll see it is very simple. | ||
| 71 | * | ||
| 72 | * We chose to align with the Zig allocator interface because: | ||
| 73 | * | ||
| 74 | * 1. It is a proven interface that serves a wide variety of use cases | ||
| 75 | * in the real world via the Zig ecosystem. It's shown to work. | ||
| 76 | * | ||
| 77 | * 2. Our core implementation itself is Zig, and this lets us very | ||
| 78 | * cheaply and easily convert between C and Zig allocators. | ||
| 79 | * | ||
| 80 | * NOTE(mitchellh): In the future, we can have default implementations of | ||
| 81 | * resize/remap and allow those to be null. | ||
| 82 | */ | ||
| 83 | typedef struct { | ||
| 84 | /** | ||
| 85 | * Return a pointer to `len` bytes with specified `alignment`, or return | ||
| 86 | * `NULL` indicating the allocation failed. | ||
| 87 | * | ||
| 88 | * @param ctx The allocator context | ||
| 89 | * @param len Number of bytes to allocate | ||
| 90 | * @param alignment Required alignment for the allocation. Guaranteed to | ||
| 91 | * be a power of two between 1 and 16 inclusive. | ||
| 92 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 93 | * @return Pointer to allocated memory, or NULL if allocation failed | ||
| 94 | */ | ||
| 95 | void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); | ||
| 96 | |||
| 97 | /** | ||
| 98 | * Attempt to expand or shrink memory in place. | ||
| 99 | * | ||
| 100 | * `memory_len` must equal the length requested from the most recent | ||
| 101 | * successful call to `alloc`, `resize`, or `remap`. `alignment` must | ||
| 102 | * equal the same value that was passed as the `alignment` parameter to | ||
| 103 | * the original `alloc` call. | ||
| 104 | * | ||
| 105 | * `new_len` must be greater than zero. | ||
| 106 | * | ||
| 107 | * @param ctx The allocator context | ||
| 108 | * @param memory Pointer to the memory block to resize | ||
| 109 | * @param memory_len Current size of the memory block | ||
| 110 | * @param alignment Alignment (must match original allocation) | ||
| 111 | * @param new_len New requested size | ||
| 112 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 113 | * @return true if resize was successful in-place, false if relocation would be required | ||
| 114 | */ | ||
| 115 | bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); | ||
| 116 | |||
| 117 | /** | ||
| 118 | * Attempt to expand or shrink memory, allowing relocation. | ||
| 119 | * | ||
| 120 | * `memory_len` must equal the length requested from the most recent | ||
| 121 | * successful call to `alloc`, `resize`, or `remap`. `alignment` must | ||
| 122 | * equal the same value that was passed as the `alignment` parameter to | ||
| 123 | * the original `alloc` call. | ||
| 124 | * | ||
| 125 | * A non-`NULL` return value indicates the resize was successful. The | ||
| 126 | * allocation may have same address, or may have been relocated. In either | ||
| 127 | * case, the allocation now has size of `new_len`. A `NULL` return value | ||
| 128 | * indicates that the resize would be equivalent to allocating new memory, | ||
| 129 | * copying the bytes from the old memory, and then freeing the old memory. | ||
| 130 | * In such case, it is more efficient for the caller to perform the copy. | ||
| 131 | * | ||
| 132 | * `new_len` must be greater than zero. | ||
| 133 | * | ||
| 134 | * @param ctx The allocator context | ||
| 135 | * @param memory Pointer to the memory block to remap | ||
| 136 | * @param memory_len Current size of the memory block | ||
| 137 | * @param alignment Alignment (must match original allocation) | ||
| 138 | * @param new_len New requested size | ||
| 139 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 140 | * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed | ||
| 141 | */ | ||
| 142 | void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Free and invalidate a region of memory. | ||
| 146 | * | ||
| 147 | * `memory_len` must equal the length requested from the most recent | ||
| 148 | * successful call to `alloc`, `resize`, or `remap`. `alignment` must | ||
| 149 | * equal the same value that was passed as the `alignment` parameter to | ||
| 150 | * the original `alloc` call. | ||
| 151 | * | ||
| 152 | * @param ctx The allocator context | ||
| 153 | * @param memory Pointer to the memory block to free | ||
| 154 | * @param memory_len Size of the memory block | ||
| 155 | * @param alignment Alignment (must match original allocation) | ||
| 156 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 157 | */ | ||
| 158 | void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); | ||
| 159 | } GhosttyAllocatorVtable; | ||
| 160 | |||
| 161 | /** | ||
| 162 | * Custom memory allocator. | ||
| 163 | * | ||
| 164 | * For functions that take an allocator pointer, a NULL pointer indicates | ||
| 165 | * that the default allocator should be used. The default allocator will | ||
| 166 | * be libc malloc/free if we're linking to libc. If libc isn't linked, | ||
| 167 | * a custom allocator is used (currently Zig's SMP allocator). | ||
| 168 | * | ||
| 169 | * @ingroup allocator | ||
| 170 | * | ||
| 171 | * Usage example: | ||
| 172 | * @code | ||
| 173 | * GhosttyAllocator allocator = { | ||
| 174 | * .vtable = &my_allocator_vtable, | ||
| 175 | * .ctx = my_allocator_state | ||
| 176 | * }; | ||
| 177 | * @endcode | ||
| 178 | */ | ||
| 179 | typedef struct GhosttyAllocator { | ||
| 180 | /** | ||
| 181 | * Opaque context pointer passed to all vtable functions. | ||
| 182 | * This allows the allocator implementation to maintain state | ||
| 183 | * or reference external resources needed for memory management. | ||
| 184 | */ | ||
| 185 | void *ctx; | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Pointer to the allocator's vtable containing function pointers | ||
| 189 | * for memory operations (alloc, resize, remap, free). | ||
| 190 | */ | ||
| 191 | const GhosttyAllocatorVtable *vtable; | ||
| 192 | } GhosttyAllocator; | ||
| 193 | |||
| 194 | /** @} */ | ||
| 195 | |||
| 196 | #endif /* GHOSTTY_VT_ALLOCATOR_H */ | ||
| @@ -0,0 +1,96 @@ | |||
| 1 | /** | ||
| 2 | * @file color.h | ||
| 3 | * | ||
| 4 | * Color types and utilities. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_COLOR_H | ||
| 8 | #define GHOSTTY_VT_COLOR_H | ||
| 9 | |||
| 10 | #include <stdint.h> | ||
| 11 | |||
| 12 | #ifdef __cplusplus | ||
| 13 | extern "C" { | ||
| 14 | #endif | ||
| 15 | |||
| 16 | /** | ||
| 17 | * RGB color value. | ||
| 18 | * | ||
| 19 | * @ingroup sgr | ||
| 20 | */ | ||
| 21 | typedef struct { | ||
| 22 | uint8_t r; /**< Red component (0-255) */ | ||
| 23 | uint8_t g; /**< Green component (0-255) */ | ||
| 24 | uint8_t b; /**< Blue component (0-255) */ | ||
| 25 | } GhosttyColorRgb; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Palette color index (0-255). | ||
| 29 | * | ||
| 30 | * @ingroup sgr | ||
| 31 | */ | ||
| 32 | typedef uint8_t GhosttyColorPaletteIndex; | ||
| 33 | |||
| 34 | /** @addtogroup sgr | ||
| 35 | * @{ | ||
| 36 | */ | ||
| 37 | |||
| 38 | /** Black color (0) @ingroup sgr */ | ||
| 39 | #define GHOSTTY_COLOR_NAMED_BLACK 0 | ||
| 40 | /** Red color (1) @ingroup sgr */ | ||
| 41 | #define GHOSTTY_COLOR_NAMED_RED 1 | ||
| 42 | /** Green color (2) @ingroup sgr */ | ||
| 43 | #define GHOSTTY_COLOR_NAMED_GREEN 2 | ||
| 44 | /** Yellow color (3) @ingroup sgr */ | ||
| 45 | #define GHOSTTY_COLOR_NAMED_YELLOW 3 | ||
| 46 | /** Blue color (4) @ingroup sgr */ | ||
| 47 | #define GHOSTTY_COLOR_NAMED_BLUE 4 | ||
| 48 | /** Magenta color (5) @ingroup sgr */ | ||
| 49 | #define GHOSTTY_COLOR_NAMED_MAGENTA 5 | ||
| 50 | /** Cyan color (6) @ingroup sgr */ | ||
| 51 | #define GHOSTTY_COLOR_NAMED_CYAN 6 | ||
| 52 | /** White color (7) @ingroup sgr */ | ||
| 53 | #define GHOSTTY_COLOR_NAMED_WHITE 7 | ||
| 54 | /** Bright black color (8) @ingroup sgr */ | ||
| 55 | #define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8 | ||
| 56 | /** Bright red color (9) @ingroup sgr */ | ||
| 57 | #define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9 | ||
| 58 | /** Bright green color (10) @ingroup sgr */ | ||
| 59 | #define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10 | ||
| 60 | /** Bright yellow color (11) @ingroup sgr */ | ||
| 61 | #define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11 | ||
| 62 | /** Bright blue color (12) @ingroup sgr */ | ||
| 63 | #define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12 | ||
| 64 | /** Bright magenta color (13) @ingroup sgr */ | ||
| 65 | #define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13 | ||
| 66 | /** Bright cyan color (14) @ingroup sgr */ | ||
| 67 | #define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14 | ||
| 68 | /** Bright white color (15) @ingroup sgr */ | ||
| 69 | #define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15 | ||
| 70 | |||
| 71 | /** @} */ | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Get the RGB color components. | ||
| 75 | * | ||
| 76 | * This function extracts the individual red, green, and blue components | ||
| 77 | * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments | ||
| 78 | * where accessing struct fields directly is difficult. | ||
| 79 | * | ||
| 80 | * @param color The RGB color value | ||
| 81 | * @param r Pointer to store the red component (0-255) | ||
| 82 | * @param g Pointer to store the green component (0-255) | ||
| 83 | * @param b Pointer to store the blue component (0-255) | ||
| 84 | * | ||
| 85 | * @ingroup sgr | ||
| 86 | */ | ||
| 87 | void ghostty_color_rgb_get(GhosttyColorRgb color, | ||
| 88 | uint8_t* r, | ||
| 89 | uint8_t* g, | ||
| 90 | uint8_t* b); | ||
| 91 | |||
| 92 | #ifdef __cplusplus | ||
| 93 | } | ||
| 94 | #endif | ||
| 95 | |||
| 96 | #endif /* GHOSTTY_VT_COLOR_H */ | ||
| @@ -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 */ | ||
| @@ -0,0 +1,221 @@ | |||
| 1 | /** | ||
| 2 | * @file encoder.h | ||
| 3 | * | ||
| 4 | * Key event encoding to terminal escape sequences. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_KEY_ENCODER_H | ||
| 8 | #define GHOSTTY_VT_KEY_ENCODER_H | ||
| 9 | |||
| 10 | #include <stddef.h> | ||
| 11 | #include <stdint.h> | ||
| 12 | #include <ghostty/vt/result.h> | ||
| 13 | #include <ghostty/vt/allocator.h> | ||
| 14 | #include <ghostty/vt/key/event.h> | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Opaque handle to a key encoder instance. | ||
| 18 | * | ||
| 19 | * This handle represents a key encoder that converts key events into terminal | ||
| 20 | * escape sequences. | ||
| 21 | * | ||
| 22 | * @ingroup key | ||
| 23 | */ | ||
| 24 | typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Kitty keyboard protocol flags. | ||
| 28 | * | ||
| 29 | * Bitflags representing the various modes of the Kitty keyboard protocol. | ||
| 30 | * These can be combined using bitwise OR operations. Valid values all | ||
| 31 | * start with `GHOSTTY_KITTY_KEY_`. | ||
| 32 | * | ||
| 33 | * @ingroup key | ||
| 34 | */ | ||
| 35 | typedef uint8_t GhosttyKittyKeyFlags; | ||
| 36 | |||
| 37 | /** Kitty keyboard protocol disabled (all flags off) */ | ||
| 38 | #define GHOSTTY_KITTY_KEY_DISABLED 0 | ||
| 39 | |||
| 40 | /** Disambiguate escape codes */ | ||
| 41 | #define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) | ||
| 42 | |||
| 43 | /** Report key press and release events */ | ||
| 44 | #define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) | ||
| 45 | |||
| 46 | /** Report alternate key codes */ | ||
| 47 | #define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) | ||
| 48 | |||
| 49 | /** Report all key events including those normally handled by the terminal */ | ||
| 50 | #define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) | ||
| 51 | |||
| 52 | /** Report associated text with key events */ | ||
| 53 | #define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) | ||
| 54 | |||
| 55 | /** All Kitty keyboard protocol flags enabled */ | ||
| 56 | #define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) | ||
| 57 | |||
| 58 | /** | ||
| 59 | * macOS option key behavior. | ||
| 60 | * | ||
| 61 | * Determines whether the "option" key on macOS is treated as "alt" or not. | ||
| 62 | * See the Ghostty `macos-option-as-alt` configuration option for more details. | ||
| 63 | * | ||
| 64 | * @ingroup key | ||
| 65 | */ | ||
| 66 | typedef enum { | ||
| 67 | /** Option key is not treated as alt */ | ||
| 68 | GHOSTTY_OPTION_AS_ALT_FALSE = 0, | ||
| 69 | /** Option key is treated as alt */ | ||
| 70 | GHOSTTY_OPTION_AS_ALT_TRUE = 1, | ||
| 71 | /** Only left option key is treated as alt */ | ||
| 72 | GHOSTTY_OPTION_AS_ALT_LEFT = 2, | ||
| 73 | /** Only right option key is treated as alt */ | ||
| 74 | GHOSTTY_OPTION_AS_ALT_RIGHT = 3, | ||
| 75 | } GhosttyOptionAsAlt; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Key encoder option identifiers. | ||
| 79 | * | ||
| 80 | * These values are used with ghostty_key_encoder_setopt() to configure | ||
| 81 | * the behavior of the key encoder. | ||
| 82 | * | ||
| 83 | * @ingroup key | ||
| 84 | */ | ||
| 85 | typedef enum { | ||
| 86 | /** Terminal DEC mode 1: cursor key application mode (value: bool) */ | ||
| 87 | GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, | ||
| 88 | |||
| 89 | /** Terminal DEC mode 66: keypad key application mode (value: bool) */ | ||
| 90 | GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, | ||
| 91 | |||
| 92 | /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ | ||
| 93 | GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, | ||
| 94 | |||
| 95 | /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ | ||
| 96 | GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, | ||
| 97 | |||
| 98 | /** xterm modifyOtherKeys mode 2 (value: bool) */ | ||
| 99 | GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, | ||
| 100 | |||
| 101 | /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ | ||
| 102 | GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, | ||
| 103 | |||
| 104 | /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ | ||
| 105 | GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, | ||
| 106 | } GhosttyKeyEncoderOption; | ||
| 107 | |||
| 108 | /** | ||
| 109 | * Create a new key encoder instance. | ||
| 110 | * | ||
| 111 | * Creates a new key encoder with default options. The encoder can be configured | ||
| 112 | * using ghostty_key_encoder_setopt() and must be freed using | ||
| 113 | * ghostty_key_encoder_free() when no longer needed. | ||
| 114 | * | ||
| 115 | * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator | ||
| 116 | * @param encoder Pointer to store the created encoder handle | ||
| 117 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 118 | * | ||
| 119 | * @ingroup key | ||
| 120 | */ | ||
| 121 | GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Free a key encoder instance. | ||
| 125 | * | ||
| 126 | * Releases all resources associated with the key encoder. After this call, | ||
| 127 | * the encoder handle becomes invalid and must not be used. | ||
| 128 | * | ||
| 129 | * @param encoder The encoder handle to free (may be NULL) | ||
| 130 | * | ||
| 131 | * @ingroup key | ||
| 132 | */ | ||
| 133 | void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Set an option on the key encoder. | ||
| 137 | * | ||
| 138 | * Configures the behavior of the key encoder. Options control various aspects | ||
| 139 | * of encoding such as terminal modes (cursor key application mode, keypad mode), | ||
| 140 | * protocol selection (Kitty keyboard protocol flags), and platform-specific | ||
| 141 | * behaviors (macOS option-as-alt). | ||
| 142 | * | ||
| 143 | * A null pointer value does nothing. It does not reset the value to the | ||
| 144 | * default. The setopt call will do nothing. | ||
| 145 | * | ||
| 146 | * @param encoder The encoder handle, must not be NULL | ||
| 147 | * @param option The option to set | ||
| 148 | * @param value Pointer to the value to set (type depends on the option) | ||
| 149 | * | ||
| 150 | * @ingroup key | ||
| 151 | */ | ||
| 152 | void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Encode a key event into a terminal escape sequence. | ||
| 156 | * | ||
| 157 | * Converts a key event into the appropriate terminal escape sequence based on | ||
| 158 | * the encoder's current options. The sequence is written to the provided buffer. | ||
| 159 | * | ||
| 160 | * Not all key events produce output. For example, unmodified modifier keys | ||
| 161 | * typically don't generate escape sequences. Check the out_len parameter to | ||
| 162 | * determine if any data was written. | ||
| 163 | * | ||
| 164 | * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY | ||
| 165 | * and out_len will contain the required buffer size. The caller can then | ||
| 166 | * allocate a larger buffer and call the function again. | ||
| 167 | * | ||
| 168 | * @param encoder The encoder handle, must not be NULL | ||
| 169 | * @param event The key event to encode, must not be NULL | ||
| 170 | * @param out_buf Buffer to write the encoded sequence to | ||
| 171 | * @param out_buf_size Size of the output buffer in bytes | ||
| 172 | * @param out_len Pointer to store the number of bytes written (may be NULL) | ||
| 173 | * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code | ||
| 174 | * | ||
| 175 | * ## Example: Calculate required buffer size | ||
| 176 | * | ||
| 177 | * @code{.c} | ||
| 178 | * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) | ||
| 179 | * size_t required = 0; | ||
| 180 | * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); | ||
| 181 | * assert(result == GHOSTTY_OUT_OF_MEMORY); | ||
| 182 | * | ||
| 183 | * // Allocate buffer of required size | ||
| 184 | * char *buf = malloc(required); | ||
| 185 | * | ||
| 186 | * // Encode with properly sized buffer | ||
| 187 | * size_t written = 0; | ||
| 188 | * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); | ||
| 189 | * assert(result == GHOSTTY_SUCCESS); | ||
| 190 | * | ||
| 191 | * // Use the encoded sequence... | ||
| 192 | * | ||
| 193 | * free(buf); | ||
| 194 | * @endcode | ||
| 195 | * | ||
| 196 | * ## Example: Direct encoding with static buffer | ||
| 197 | * | ||
| 198 | * @code{.c} | ||
| 199 | * // Most escape sequences are short, so a static buffer often suffices | ||
| 200 | * char buf[128]; | ||
| 201 | * size_t written = 0; | ||
| 202 | * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); | ||
| 203 | * | ||
| 204 | * if (result == GHOSTTY_SUCCESS) { | ||
| 205 | * // Write the encoded sequence to the terminal | ||
| 206 | * write(pty_fd, buf, written); | ||
| 207 | * } else if (result == GHOSTTY_OUT_OF_MEMORY) { | ||
| 208 | * // Buffer too small, written contains required size | ||
| 209 | * char *dynamic_buf = malloc(written); | ||
| 210 | * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); | ||
| 211 | * assert(result == GHOSTTY_SUCCESS); | ||
| 212 | * write(pty_fd, dynamic_buf, written); | ||
| 213 | * free(dynamic_buf); | ||
| 214 | * } | ||
| 215 | * @endcode | ||
| 216 | * | ||
| 217 | * @ingroup key | ||
| 218 | */ | ||
| 219 | GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); | ||
| 220 | |||
| 221 | #endif /* GHOSTTY_VT_KEY_ENCODER_H */ | ||
| @@ -0,0 +1,474 @@ | |||
| 1 | /** | ||
| 2 | * @file event.h | ||
| 3 | * | ||
| 4 | * Key event representation and manipulation. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_KEY_EVENT_H | ||
| 8 | #define GHOSTTY_VT_KEY_EVENT_H | ||
| 9 | |||
| 10 | #include <stdbool.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <stdint.h> | ||
| 13 | #include <ghostty/vt/result.h> | ||
| 14 | #include <ghostty/vt/allocator.h> | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Opaque handle to a key event. | ||
| 18 | * | ||
| 19 | * This handle represents a keyboard input event containing information about | ||
| 20 | * the physical key pressed, modifiers, and generated text. | ||
| 21 | * | ||
| 22 | * @ingroup key | ||
| 23 | */ | ||
| 24 | typedef struct GhosttyKeyEvent *GhosttyKeyEvent; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Keyboard input event types. | ||
| 28 | * | ||
| 29 | * @ingroup key | ||
| 30 | */ | ||
| 31 | typedef enum { | ||
| 32 | /** Key was released */ | ||
| 33 | GHOSTTY_KEY_ACTION_RELEASE = 0, | ||
| 34 | /** Key was pressed */ | ||
| 35 | GHOSTTY_KEY_ACTION_PRESS = 1, | ||
| 36 | /** Key is being repeated (held down) */ | ||
| 37 | GHOSTTY_KEY_ACTION_REPEAT = 2, | ||
| 38 | } GhosttyKeyAction; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Keyboard modifier keys bitmask. | ||
| 42 | * | ||
| 43 | * A bitmask representing all keyboard modifiers. This tracks which modifier keys | ||
| 44 | * are pressed and, where supported by the platform, which side (left or right) | ||
| 45 | * of each modifier is active. | ||
| 46 | * | ||
| 47 | * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. | ||
| 48 | * | ||
| 49 | * Modifier side bits are only meaningful when the corresponding modifier bit is set. | ||
| 50 | * Not all platforms support distinguishing between left and right modifier | ||
| 51 | * keys and Ghostty is built to expect that some platforms may not provide this | ||
| 52 | * information. | ||
| 53 | * | ||
| 54 | * @ingroup key | ||
| 55 | */ | ||
| 56 | typedef uint16_t GhosttyMods; | ||
| 57 | |||
| 58 | /** Shift key is pressed */ | ||
| 59 | #define GHOSTTY_MODS_SHIFT (1 << 0) | ||
| 60 | /** Control key is pressed */ | ||
| 61 | #define GHOSTTY_MODS_CTRL (1 << 1) | ||
| 62 | /** Alt/Option key is pressed */ | ||
| 63 | #define GHOSTTY_MODS_ALT (1 << 2) | ||
| 64 | /** Super/Command/Windows key is pressed */ | ||
| 65 | #define GHOSTTY_MODS_SUPER (1 << 3) | ||
| 66 | /** Caps Lock is active */ | ||
| 67 | #define GHOSTTY_MODS_CAPS_LOCK (1 << 4) | ||
| 68 | /** Num Lock is active */ | ||
| 69 | #define GHOSTTY_MODS_NUM_LOCK (1 << 5) | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Right shift is pressed (0 = left, 1 = right). | ||
| 73 | * Only meaningful when GHOSTTY_MODS_SHIFT is set. | ||
| 74 | */ | ||
| 75 | #define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) | ||
| 76 | /** | ||
| 77 | * Right ctrl is pressed (0 = left, 1 = right). | ||
| 78 | * Only meaningful when GHOSTTY_MODS_CTRL is set. | ||
| 79 | */ | ||
| 80 | #define GHOSTTY_MODS_CTRL_SIDE (1 << 7) | ||
| 81 | /** | ||
| 82 | * Right alt is pressed (0 = left, 1 = right). | ||
| 83 | * Only meaningful when GHOSTTY_MODS_ALT is set. | ||
| 84 | */ | ||
| 85 | #define GHOSTTY_MODS_ALT_SIDE (1 << 8) | ||
| 86 | /** | ||
| 87 | * Right super is pressed (0 = left, 1 = right). | ||
| 88 | * Only meaningful when GHOSTTY_MODS_SUPER is set. | ||
| 89 | */ | ||
| 90 | #define GHOSTTY_MODS_SUPER_SIDE (1 << 9) | ||
| 91 | |||
| 92 | /** | ||
| 93 | * Physical key codes. | ||
| 94 | * | ||
| 95 | * The set of key codes that Ghostty is aware of. These represent physical keys | ||
| 96 | * on the keyboard and are layout-independent. For example, the "a" key on a US | ||
| 97 | * keyboard is the same as the "ф" key on a Russian keyboard, but both will | ||
| 98 | * report the same key_a value. | ||
| 99 | * | ||
| 100 | * Layout-dependent strings are provided separately as UTF-8 text and are produced | ||
| 101 | * by the platform. These values are based on the W3C UI Events KeyboardEvent code | ||
| 102 | * standard. See: https://www.w3.org/TR/uievents-code | ||
| 103 | * | ||
| 104 | * @ingroup key | ||
| 105 | */ | ||
| 106 | typedef enum { | ||
| 107 | GHOSTTY_KEY_UNIDENTIFIED = 0, | ||
| 108 | |||
| 109 | // Writing System Keys (W3C § 3.1.1) | ||
| 110 | GHOSTTY_KEY_BACKQUOTE, | ||
| 111 | GHOSTTY_KEY_BACKSLASH, | ||
| 112 | GHOSTTY_KEY_BRACKET_LEFT, | ||
| 113 | GHOSTTY_KEY_BRACKET_RIGHT, | ||
| 114 | GHOSTTY_KEY_COMMA, | ||
| 115 | GHOSTTY_KEY_DIGIT_0, | ||
| 116 | GHOSTTY_KEY_DIGIT_1, | ||
| 117 | GHOSTTY_KEY_DIGIT_2, | ||
| 118 | GHOSTTY_KEY_DIGIT_3, | ||
| 119 | GHOSTTY_KEY_DIGIT_4, | ||
| 120 | GHOSTTY_KEY_DIGIT_5, | ||
| 121 | GHOSTTY_KEY_DIGIT_6, | ||
| 122 | GHOSTTY_KEY_DIGIT_7, | ||
| 123 | GHOSTTY_KEY_DIGIT_8, | ||
| 124 | GHOSTTY_KEY_DIGIT_9, | ||
| 125 | GHOSTTY_KEY_EQUAL, | ||
| 126 | GHOSTTY_KEY_INTL_BACKSLASH, | ||
| 127 | GHOSTTY_KEY_INTL_RO, | ||
| 128 | GHOSTTY_KEY_INTL_YEN, | ||
| 129 | GHOSTTY_KEY_A, | ||
| 130 | GHOSTTY_KEY_B, | ||
| 131 | GHOSTTY_KEY_C, | ||
| 132 | GHOSTTY_KEY_D, | ||
| 133 | GHOSTTY_KEY_E, | ||
| 134 | GHOSTTY_KEY_F, | ||
| 135 | GHOSTTY_KEY_G, | ||
| 136 | GHOSTTY_KEY_H, | ||
| 137 | GHOSTTY_KEY_I, | ||
| 138 | GHOSTTY_KEY_J, | ||
| 139 | GHOSTTY_KEY_K, | ||
| 140 | GHOSTTY_KEY_L, | ||
| 141 | GHOSTTY_KEY_M, | ||
| 142 | GHOSTTY_KEY_N, | ||
| 143 | GHOSTTY_KEY_O, | ||
| 144 | GHOSTTY_KEY_P, | ||
| 145 | GHOSTTY_KEY_Q, | ||
| 146 | GHOSTTY_KEY_R, | ||
| 147 | GHOSTTY_KEY_S, | ||
| 148 | GHOSTTY_KEY_T, | ||
| 149 | GHOSTTY_KEY_U, | ||
| 150 | GHOSTTY_KEY_V, | ||
| 151 | GHOSTTY_KEY_W, | ||
| 152 | GHOSTTY_KEY_X, | ||
| 153 | GHOSTTY_KEY_Y, | ||
| 154 | GHOSTTY_KEY_Z, | ||
| 155 | GHOSTTY_KEY_MINUS, | ||
| 156 | GHOSTTY_KEY_PERIOD, | ||
| 157 | GHOSTTY_KEY_QUOTE, | ||
| 158 | GHOSTTY_KEY_SEMICOLON, | ||
| 159 | GHOSTTY_KEY_SLASH, | ||
| 160 | |||
| 161 | // Functional Keys (W3C § 3.1.2) | ||
| 162 | GHOSTTY_KEY_ALT_LEFT, | ||
| 163 | GHOSTTY_KEY_ALT_RIGHT, | ||
| 164 | GHOSTTY_KEY_BACKSPACE, | ||
| 165 | GHOSTTY_KEY_CAPS_LOCK, | ||
| 166 | GHOSTTY_KEY_CONTEXT_MENU, | ||
| 167 | GHOSTTY_KEY_CONTROL_LEFT, | ||
| 168 | GHOSTTY_KEY_CONTROL_RIGHT, | ||
| 169 | GHOSTTY_KEY_ENTER, | ||
| 170 | GHOSTTY_KEY_META_LEFT, | ||
| 171 | GHOSTTY_KEY_META_RIGHT, | ||
| 172 | GHOSTTY_KEY_SHIFT_LEFT, | ||
| 173 | GHOSTTY_KEY_SHIFT_RIGHT, | ||
| 174 | GHOSTTY_KEY_SPACE, | ||
| 175 | GHOSTTY_KEY_TAB, | ||
| 176 | GHOSTTY_KEY_CONVERT, | ||
| 177 | GHOSTTY_KEY_KANA_MODE, | ||
| 178 | GHOSTTY_KEY_NON_CONVERT, | ||
| 179 | |||
| 180 | // Control Pad Section (W3C § 3.2) | ||
| 181 | GHOSTTY_KEY_DELETE, | ||
| 182 | GHOSTTY_KEY_END, | ||
| 183 | GHOSTTY_KEY_HELP, | ||
| 184 | GHOSTTY_KEY_HOME, | ||
| 185 | GHOSTTY_KEY_INSERT, | ||
| 186 | GHOSTTY_KEY_PAGE_DOWN, | ||
| 187 | GHOSTTY_KEY_PAGE_UP, | ||
| 188 | |||
| 189 | // Arrow Pad Section (W3C § 3.3) | ||
| 190 | GHOSTTY_KEY_ARROW_DOWN, | ||
| 191 | GHOSTTY_KEY_ARROW_LEFT, | ||
| 192 | GHOSTTY_KEY_ARROW_RIGHT, | ||
| 193 | GHOSTTY_KEY_ARROW_UP, | ||
| 194 | |||
| 195 | // Numpad Section (W3C § 3.4) | ||
| 196 | GHOSTTY_KEY_NUM_LOCK, | ||
| 197 | GHOSTTY_KEY_NUMPAD_0, | ||
| 198 | GHOSTTY_KEY_NUMPAD_1, | ||
| 199 | GHOSTTY_KEY_NUMPAD_2, | ||
| 200 | GHOSTTY_KEY_NUMPAD_3, | ||
| 201 | GHOSTTY_KEY_NUMPAD_4, | ||
| 202 | GHOSTTY_KEY_NUMPAD_5, | ||
| 203 | GHOSTTY_KEY_NUMPAD_6, | ||
| 204 | GHOSTTY_KEY_NUMPAD_7, | ||
| 205 | GHOSTTY_KEY_NUMPAD_8, | ||
| 206 | GHOSTTY_KEY_NUMPAD_9, | ||
| 207 | GHOSTTY_KEY_NUMPAD_ADD, | ||
| 208 | GHOSTTY_KEY_NUMPAD_BACKSPACE, | ||
| 209 | GHOSTTY_KEY_NUMPAD_CLEAR, | ||
| 210 | GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, | ||
| 211 | GHOSTTY_KEY_NUMPAD_COMMA, | ||
| 212 | GHOSTTY_KEY_NUMPAD_DECIMAL, | ||
| 213 | GHOSTTY_KEY_NUMPAD_DIVIDE, | ||
| 214 | GHOSTTY_KEY_NUMPAD_ENTER, | ||
| 215 | GHOSTTY_KEY_NUMPAD_EQUAL, | ||
| 216 | GHOSTTY_KEY_NUMPAD_MEMORY_ADD, | ||
| 217 | GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, | ||
| 218 | GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, | ||
| 219 | GHOSTTY_KEY_NUMPAD_MEMORY_STORE, | ||
| 220 | GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, | ||
| 221 | GHOSTTY_KEY_NUMPAD_MULTIPLY, | ||
| 222 | GHOSTTY_KEY_NUMPAD_PAREN_LEFT, | ||
| 223 | GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, | ||
| 224 | GHOSTTY_KEY_NUMPAD_SUBTRACT, | ||
| 225 | GHOSTTY_KEY_NUMPAD_SEPARATOR, | ||
| 226 | GHOSTTY_KEY_NUMPAD_UP, | ||
| 227 | GHOSTTY_KEY_NUMPAD_DOWN, | ||
| 228 | GHOSTTY_KEY_NUMPAD_RIGHT, | ||
| 229 | GHOSTTY_KEY_NUMPAD_LEFT, | ||
| 230 | GHOSTTY_KEY_NUMPAD_BEGIN, | ||
| 231 | GHOSTTY_KEY_NUMPAD_HOME, | ||
| 232 | GHOSTTY_KEY_NUMPAD_END, | ||
| 233 | GHOSTTY_KEY_NUMPAD_INSERT, | ||
| 234 | GHOSTTY_KEY_NUMPAD_DELETE, | ||
| 235 | GHOSTTY_KEY_NUMPAD_PAGE_UP, | ||
| 236 | GHOSTTY_KEY_NUMPAD_PAGE_DOWN, | ||
| 237 | |||
| 238 | // Function Section (W3C § 3.5) | ||
| 239 | GHOSTTY_KEY_ESCAPE, | ||
| 240 | GHOSTTY_KEY_F1, | ||
| 241 | GHOSTTY_KEY_F2, | ||
| 242 | GHOSTTY_KEY_F3, | ||
| 243 | GHOSTTY_KEY_F4, | ||
| 244 | GHOSTTY_KEY_F5, | ||
| 245 | GHOSTTY_KEY_F6, | ||
| 246 | GHOSTTY_KEY_F7, | ||
| 247 | GHOSTTY_KEY_F8, | ||
| 248 | GHOSTTY_KEY_F9, | ||
| 249 | GHOSTTY_KEY_F10, | ||
| 250 | GHOSTTY_KEY_F11, | ||
| 251 | GHOSTTY_KEY_F12, | ||
| 252 | GHOSTTY_KEY_F13, | ||
| 253 | GHOSTTY_KEY_F14, | ||
| 254 | GHOSTTY_KEY_F15, | ||
| 255 | GHOSTTY_KEY_F16, | ||
| 256 | GHOSTTY_KEY_F17, | ||
| 257 | GHOSTTY_KEY_F18, | ||
| 258 | GHOSTTY_KEY_F19, | ||
| 259 | GHOSTTY_KEY_F20, | ||
| 260 | GHOSTTY_KEY_F21, | ||
| 261 | GHOSTTY_KEY_F22, | ||
| 262 | GHOSTTY_KEY_F23, | ||
| 263 | GHOSTTY_KEY_F24, | ||
| 264 | GHOSTTY_KEY_F25, | ||
| 265 | GHOSTTY_KEY_FN, | ||
| 266 | GHOSTTY_KEY_FN_LOCK, | ||
| 267 | GHOSTTY_KEY_PRINT_SCREEN, | ||
| 268 | GHOSTTY_KEY_SCROLL_LOCK, | ||
| 269 | GHOSTTY_KEY_PAUSE, | ||
| 270 | |||
| 271 | // Media Keys (W3C § 3.6) | ||
| 272 | GHOSTTY_KEY_BROWSER_BACK, | ||
| 273 | GHOSTTY_KEY_BROWSER_FAVORITES, | ||
| 274 | GHOSTTY_KEY_BROWSER_FORWARD, | ||
| 275 | GHOSTTY_KEY_BROWSER_HOME, | ||
| 276 | GHOSTTY_KEY_BROWSER_REFRESH, | ||
| 277 | GHOSTTY_KEY_BROWSER_SEARCH, | ||
| 278 | GHOSTTY_KEY_BROWSER_STOP, | ||
| 279 | GHOSTTY_KEY_EJECT, | ||
| 280 | GHOSTTY_KEY_LAUNCH_APP_1, | ||
| 281 | GHOSTTY_KEY_LAUNCH_APP_2, | ||
| 282 | GHOSTTY_KEY_LAUNCH_MAIL, | ||
| 283 | GHOSTTY_KEY_MEDIA_PLAY_PAUSE, | ||
| 284 | GHOSTTY_KEY_MEDIA_SELECT, | ||
| 285 | GHOSTTY_KEY_MEDIA_STOP, | ||
| 286 | GHOSTTY_KEY_MEDIA_TRACK_NEXT, | ||
| 287 | GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, | ||
| 288 | GHOSTTY_KEY_POWER, | ||
| 289 | GHOSTTY_KEY_SLEEP, | ||
| 290 | GHOSTTY_KEY_AUDIO_VOLUME_DOWN, | ||
| 291 | GHOSTTY_KEY_AUDIO_VOLUME_MUTE, | ||
| 292 | GHOSTTY_KEY_AUDIO_VOLUME_UP, | ||
| 293 | GHOSTTY_KEY_WAKE_UP, | ||
| 294 | |||
| 295 | // Legacy, Non-standard, and Special Keys (W3C § 3.7) | ||
| 296 | GHOSTTY_KEY_COPY, | ||
| 297 | GHOSTTY_KEY_CUT, | ||
| 298 | GHOSTTY_KEY_PASTE, | ||
| 299 | } GhosttyKey; | ||
| 300 | |||
| 301 | /** | ||
| 302 | * Create a new key event instance. | ||
| 303 | * | ||
| 304 | * Creates a new key event with default values. The event must be freed using | ||
| 305 | * ghostty_key_event_free() when no longer needed. | ||
| 306 | * | ||
| 307 | * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator | ||
| 308 | * @param event Pointer to store the created key event handle | ||
| 309 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 310 | * | ||
| 311 | * @ingroup key | ||
| 312 | */ | ||
| 313 | GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Free a key event instance. | ||
| 317 | * | ||
| 318 | * Releases all resources associated with the key event. After this call, | ||
| 319 | * the event handle becomes invalid and must not be used. | ||
| 320 | * | ||
| 321 | * @param event The key event handle to free (may be NULL) | ||
| 322 | * | ||
| 323 | * @ingroup key | ||
| 324 | */ | ||
| 325 | void ghostty_key_event_free(GhosttyKeyEvent event); | ||
| 326 | |||
| 327 | /** | ||
| 328 | * Set the key action (press, release, repeat). | ||
| 329 | * | ||
| 330 | * @param event The key event handle, must not be NULL | ||
| 331 | * @param action The action to set | ||
| 332 | * | ||
| 333 | * @ingroup key | ||
| 334 | */ | ||
| 335 | void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); | ||
| 336 | |||
| 337 | /** | ||
| 338 | * Get the key action (press, release, repeat). | ||
| 339 | * | ||
| 340 | * @param event The key event handle, must not be NULL | ||
| 341 | * @return The key action | ||
| 342 | * | ||
| 343 | * @ingroup key | ||
| 344 | */ | ||
| 345 | GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); | ||
| 346 | |||
| 347 | /** | ||
| 348 | * Set the physical key code. | ||
| 349 | * | ||
| 350 | * @param event The key event handle, must not be NULL | ||
| 351 | * @param key The physical key code to set | ||
| 352 | * | ||
| 353 | * @ingroup key | ||
| 354 | */ | ||
| 355 | void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); | ||
| 356 | |||
| 357 | /** | ||
| 358 | * Get the physical key code. | ||
| 359 | * | ||
| 360 | * @param event The key event handle, must not be NULL | ||
| 361 | * @return The physical key code | ||
| 362 | * | ||
| 363 | * @ingroup key | ||
| 364 | */ | ||
| 365 | GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); | ||
| 366 | |||
| 367 | /** | ||
| 368 | * Set the modifier keys bitmask. | ||
| 369 | * | ||
| 370 | * @param event The key event handle, must not be NULL | ||
| 371 | * @param mods The modifier keys bitmask to set | ||
| 372 | * | ||
| 373 | * @ingroup key | ||
| 374 | */ | ||
| 375 | void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); | ||
| 376 | |||
| 377 | /** | ||
| 378 | * Get the modifier keys bitmask. | ||
| 379 | * | ||
| 380 | * @param event The key event handle, must not be NULL | ||
| 381 | * @return The modifier keys bitmask | ||
| 382 | * | ||
| 383 | * @ingroup key | ||
| 384 | */ | ||
| 385 | GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); | ||
| 386 | |||
| 387 | /** | ||
| 388 | * Set the consumed modifiers bitmask. | ||
| 389 | * | ||
| 390 | * @param event The key event handle, must not be NULL | ||
| 391 | * @param consumed_mods The consumed modifiers bitmask to set | ||
| 392 | * | ||
| 393 | * @ingroup key | ||
| 394 | */ | ||
| 395 | void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); | ||
| 396 | |||
| 397 | /** | ||
| 398 | * Get the consumed modifiers bitmask. | ||
| 399 | * | ||
| 400 | * @param event The key event handle, must not be NULL | ||
| 401 | * @return The consumed modifiers bitmask | ||
| 402 | * | ||
| 403 | * @ingroup key | ||
| 404 | */ | ||
| 405 | GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); | ||
| 406 | |||
| 407 | /** | ||
| 408 | * Set whether the key event is part of a composition sequence. | ||
| 409 | * | ||
| 410 | * @param event The key event handle, must not be NULL | ||
| 411 | * @param composing Whether the key event is part of a composition sequence | ||
| 412 | * | ||
| 413 | * @ingroup key | ||
| 414 | */ | ||
| 415 | void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); | ||
| 416 | |||
| 417 | /** | ||
| 418 | * Get whether the key event is part of a composition sequence. | ||
| 419 | * | ||
| 420 | * @param event The key event handle, must not be NULL | ||
| 421 | * @return Whether the key event is part of a composition sequence | ||
| 422 | * | ||
| 423 | * @ingroup key | ||
| 424 | */ | ||
| 425 | bool ghostty_key_event_get_composing(GhosttyKeyEvent event); | ||
| 426 | |||
| 427 | /** | ||
| 428 | * Set the UTF-8 text generated by the key event. | ||
| 429 | * | ||
| 430 | * The key event does NOT take ownership of the text pointer. The caller | ||
| 431 | * must ensure the string remains valid for the lifetime needed by the event. | ||
| 432 | * | ||
| 433 | * @param event The key event handle, must not be NULL | ||
| 434 | * @param utf8 The UTF-8 text to set (or NULL for empty) | ||
| 435 | * @param len Length of the UTF-8 text in bytes | ||
| 436 | * | ||
| 437 | * @ingroup key | ||
| 438 | */ | ||
| 439 | void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); | ||
| 440 | |||
| 441 | /** | ||
| 442 | * Get the UTF-8 text generated by the key event. | ||
| 443 | * | ||
| 444 | * The returned pointer is valid until the event is freed or the UTF-8 text is modified. | ||
| 445 | * | ||
| 446 | * @param event The key event handle, must not be NULL | ||
| 447 | * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) | ||
| 448 | * @return The UTF-8 text (or NULL for empty) | ||
| 449 | * | ||
| 450 | * @ingroup key | ||
| 451 | */ | ||
| 452 | const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); | ||
| 453 | |||
| 454 | /** | ||
| 455 | * Set the unshifted Unicode codepoint. | ||
| 456 | * | ||
| 457 | * @param event The key event handle, must not be NULL | ||
| 458 | * @param codepoint The unshifted Unicode codepoint to set | ||
| 459 | * | ||
| 460 | * @ingroup key | ||
| 461 | */ | ||
| 462 | void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); | ||
| 463 | |||
| 464 | /** | ||
| 465 | * Get the unshifted Unicode codepoint. | ||
| 466 | * | ||
| 467 | * @param event The key event handle, must not be NULL | ||
| 468 | * @return The unshifted Unicode codepoint | ||
| 469 | * | ||
| 470 | * @ingroup key | ||
| 471 | */ | ||
| 472 | uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); | ||
| 473 | |||
| 474 | #endif /* GHOSTTY_VT_KEY_EVENT_H */ | ||
| @@ -0,0 +1,233 @@ | |||
| 1 | /** | ||
| 2 | * @file osc.h | ||
| 3 | * | ||
| 4 | * OSC (Operating System Command) sequence parser and command handling. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_OSC_H | ||
| 8 | #define GHOSTTY_VT_OSC_H | ||
| 9 | |||
| 10 | #include <stdbool.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <stdint.h> | ||
| 13 | #include <ghostty/vt/result.h> | ||
| 14 | #include <ghostty/vt/allocator.h> | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Opaque handle to an OSC parser instance. | ||
| 18 | * | ||
| 19 | * This handle represents an OSC (Operating System Command) parser that can | ||
| 20 | * be used to parse the contents of OSC sequences. | ||
| 21 | * | ||
| 22 | * @ingroup osc | ||
| 23 | */ | ||
| 24 | typedef struct GhosttyOscParser *GhosttyOscParser; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Opaque handle to a single OSC command. | ||
| 28 | * | ||
| 29 | * This handle represents a parsed OSC (Operating System Command) command. | ||
| 30 | * The command can be queried for its type and associated data. | ||
| 31 | * | ||
| 32 | * @ingroup osc | ||
| 33 | */ | ||
| 34 | typedef struct GhosttyOscCommand *GhosttyOscCommand; | ||
| 35 | |||
| 36 | /** @defgroup osc OSC Parser | ||
| 37 | * | ||
| 38 | * OSC (Operating System Command) sequence parser and command handling. | ||
| 39 | * | ||
| 40 | * The parser operates in a streaming fashion, processing input byte-by-byte | ||
| 41 | * to handle OSC sequences that may arrive in fragments across multiple reads. | ||
| 42 | * This interface makes it easy to integrate into most environments and avoids | ||
| 43 | * over-allocating buffers. | ||
| 44 | * | ||
| 45 | * ## Basic Usage | ||
| 46 | * | ||
| 47 | * 1. Create a parser instance with ghostty_osc_new() | ||
| 48 | * 2. Feed bytes to the parser using ghostty_osc_next() | ||
| 49 | * 3. Finalize parsing with ghostty_osc_end() to get the command | ||
| 50 | * 4. Query command type and extract data using ghostty_osc_command_type() | ||
| 51 | * and ghostty_osc_command_data() | ||
| 52 | * 5. Free the parser with ghostty_osc_free() when done | ||
| 53 | * | ||
| 54 | * @{ | ||
| 55 | */ | ||
| 56 | |||
| 57 | /** | ||
| 58 | * OSC command types. | ||
| 59 | * | ||
| 60 | * @ingroup osc | ||
| 61 | */ | ||
| 62 | typedef enum { | ||
| 63 | GHOSTTY_OSC_COMMAND_INVALID = 0, | ||
| 64 | GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, | ||
| 65 | GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, | ||
| 66 | GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3, | ||
| 67 | GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4, | ||
| 68 | GHOSTTY_OSC_COMMAND_REPORT_PWD = 5, | ||
| 69 | GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6, | ||
| 70 | GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7, | ||
| 71 | GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8, | ||
| 72 | GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9, | ||
| 73 | GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10, | ||
| 74 | GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11, | ||
| 75 | GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12, | ||
| 76 | GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13, | ||
| 77 | GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14, | ||
| 78 | GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15, | ||
| 79 | GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16, | ||
| 80 | GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17, | ||
| 81 | GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18, | ||
| 82 | GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19, | ||
| 83 | GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, | ||
| 84 | GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, | ||
| 85 | GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, | ||
| 86 | } GhosttyOscCommandType; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * OSC command data types. | ||
| 90 | * | ||
| 91 | * These values specify what type of data to extract from an OSC command | ||
| 92 | * using `ghostty_osc_command_data`. | ||
| 93 | * | ||
| 94 | * @ingroup osc | ||
| 95 | */ | ||
| 96 | typedef enum { | ||
| 97 | /** Invalid data type. Never results in any data extraction. */ | ||
| 98 | GHOSTTY_OSC_DATA_INVALID = 0, | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Window title string data. | ||
| 102 | * | ||
| 103 | * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE | ||
| 104 | * | ||
| 105 | * Output type: const char ** (pointer to null-terminated string) | ||
| 106 | * | ||
| 107 | * Lifetime: Valid until the next call to any ghostty_osc_* function with | ||
| 108 | * the same parser instance. Memory is owned by the parser. | ||
| 109 | */ | ||
| 110 | GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, | ||
| 111 | } GhosttyOscCommandData; | ||
| 112 | |||
| 113 | /** | ||
| 114 | * Create a new OSC parser instance. | ||
| 115 | * | ||
| 116 | * Creates a new OSC (Operating System Command) parser using the provided | ||
| 117 | * allocator. The parser must be freed using ghostty_vt_osc_free() when | ||
| 118 | * no longer needed. | ||
| 119 | * | ||
| 120 | * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator | ||
| 121 | * @param parser Pointer to store the created parser handle | ||
| 122 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 123 | * | ||
| 124 | * @ingroup osc | ||
| 125 | */ | ||
| 126 | GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Free an OSC parser instance. | ||
| 130 | * | ||
| 131 | * Releases all resources associated with the OSC parser. After this call, | ||
| 132 | * the parser handle becomes invalid and must not be used. | ||
| 133 | * | ||
| 134 | * @param parser The parser handle to free (may be NULL) | ||
| 135 | * | ||
| 136 | * @ingroup osc | ||
| 137 | */ | ||
| 138 | void ghostty_osc_free(GhosttyOscParser parser); | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Reset an OSC parser instance to its initial state. | ||
| 142 | * | ||
| 143 | * Resets the parser state, clearing any partially parsed OSC sequences | ||
| 144 | * and returning the parser to its initial state. This is useful for | ||
| 145 | * reusing a parser instance or recovering from parse errors. | ||
| 146 | * | ||
| 147 | * @param parser The parser handle to reset, must not be null. | ||
| 148 | * | ||
| 149 | * @ingroup osc | ||
| 150 | */ | ||
| 151 | void ghostty_osc_reset(GhosttyOscParser parser); | ||
| 152 | |||
| 153 | /** | ||
| 154 | * Parse the next byte in an OSC sequence. | ||
| 155 | * | ||
| 156 | * Processes a single byte as part of an OSC sequence. The parser maintains | ||
| 157 | * internal state to track the progress through the sequence. Call this | ||
| 158 | * function for each byte in the sequence data. | ||
| 159 | * | ||
| 160 | * When finished pumping the parser with bytes, call ghostty_osc_end | ||
| 161 | * to get the final result. | ||
| 162 | * | ||
| 163 | * @param parser The parser handle, must not be null. | ||
| 164 | * @param byte The next byte to parse | ||
| 165 | * | ||
| 166 | * @ingroup osc | ||
| 167 | */ | ||
| 168 | void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); | ||
| 169 | |||
| 170 | /** | ||
| 171 | * Finalize OSC parsing and retrieve the parsed command. | ||
| 172 | * | ||
| 173 | * Call this function after feeding all bytes of an OSC sequence to the parser | ||
| 174 | * using ghostty_osc_next() with the exception of the terminating character | ||
| 175 | * (ESC or ST). This function finalizes the parsing process and returns the | ||
| 176 | * parsed OSC command. | ||
| 177 | * | ||
| 178 | * The return value is never NULL. Invalid commands will return a command | ||
| 179 | * with type GHOSTTY_OSC_COMMAND_INVALID. | ||
| 180 | * | ||
| 181 | * The terminator parameter specifies the byte that terminated the OSC sequence | ||
| 182 | * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is | ||
| 183 | * preserved in the parsed command so that responses can use the same terminator | ||
| 184 | * format for better compatibility with the calling program. For commands that | ||
| 185 | * do not require a response, this parameter is ignored and the resulting | ||
| 186 | * command will not retain the terminator information. | ||
| 187 | * | ||
| 188 | * The returned command handle is valid until the next call to any | ||
| 189 | * `ghostty_osc_*` function with the same parser instance with the exception | ||
| 190 | * of command introspection functions such as `ghostty_osc_command_type`. | ||
| 191 | * | ||
| 192 | * @param parser The parser handle, must not be null. | ||
| 193 | * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) | ||
| 194 | * @return Handle to the parsed OSC command | ||
| 195 | * | ||
| 196 | * @ingroup osc | ||
| 197 | */ | ||
| 198 | GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Get the type of an OSC command. | ||
| 202 | * | ||
| 203 | * Returns the type identifier for the given OSC command. This can be used | ||
| 204 | * to determine what kind of command was parsed and what data might be | ||
| 205 | * available from it. | ||
| 206 | * | ||
| 207 | * @param command The OSC command handle to query (may be NULL) | ||
| 208 | * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL | ||
| 209 | * | ||
| 210 | * @ingroup osc | ||
| 211 | */ | ||
| 212 | GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); | ||
| 213 | |||
| 214 | /** | ||
| 215 | * Extract data from an OSC command. | ||
| 216 | * | ||
| 217 | * Extracts typed data from the given OSC command based on the specified | ||
| 218 | * data type. The output pointer must be of the appropriate type for the | ||
| 219 | * requested data kind. Valid command types, output types, and memory | ||
| 220 | * safety information are documented in the `GhosttyOscCommandData` enum. | ||
| 221 | * | ||
| 222 | * @param command The OSC command handle to query (may be NULL) | ||
| 223 | * @param data The type of data to extract | ||
| 224 | * @param out Pointer to store the extracted data (type depends on data parameter) | ||
| 225 | * @return true if data extraction was successful, false otherwise | ||
| 226 | * | ||
| 227 | * @ingroup osc | ||
| 228 | */ | ||
| 229 | bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); | ||
| 230 | |||
| 231 | /** @} */ | ||
| 232 | |||
| 233 | #endif /* GHOSTTY_VT_OSC_H */ | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | /** | ||
| 2 | * @file paste.h | ||
| 3 | * | ||
| 4 | * Paste utilities - validate and encode paste data for terminal input. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_PASTE_H | ||
| 8 | #define GHOSTTY_VT_PASTE_H | ||
| 9 | |||
| 10 | /** @defgroup paste Paste Utilities | ||
| 11 | * | ||
| 12 | * Utilities for validating paste data safety. | ||
| 13 | * | ||
| 14 | * ## Basic Usage | ||
| 15 | * | ||
| 16 | * Use ghostty_paste_is_safe() to check if paste data contains potentially | ||
| 17 | * dangerous sequences before sending it to the terminal. | ||
| 18 | * | ||
| 19 | * ## Example | ||
| 20 | * | ||
| 21 | * @code{.c} | ||
| 22 | * #include <stdio.h> | ||
| 23 | * #include <string.h> | ||
| 24 | * #include <ghostty/vt.h> | ||
| 25 | * | ||
| 26 | * int main() { | ||
| 27 | * const char* safe_data = "hello world"; | ||
| 28 | * const char* unsafe_data = "rm -rf /\n"; | ||
| 29 | * | ||
| 30 | * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { | ||
| 31 | * printf("Safe to paste\n"); | ||
| 32 | * } | ||
| 33 | * | ||
| 34 | * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { | ||
| 35 | * printf("Unsafe! Contains newline\n"); | ||
| 36 | * } | ||
| 37 | * | ||
| 38 | * return 0; | ||
| 39 | * } | ||
| 40 | * @endcode | ||
| 41 | * | ||
| 42 | * @{ | ||
| 43 | */ | ||
| 44 | |||
| 45 | #include <stdbool.h> | ||
| 46 | #include <stddef.h> | ||
| 47 | |||
| 48 | #ifdef __cplusplus | ||
| 49 | extern "C" { | ||
| 50 | #endif | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Check if paste data is safe to paste into the terminal. | ||
| 54 | * | ||
| 55 | * Data is considered unsafe if it contains: | ||
| 56 | * - Newlines (`\n`) which can inject commands | ||
| 57 | * - The bracketed paste end sequence (`\x1b[201~`) which can be used | ||
| 58 | * to exit bracketed paste mode and inject commands | ||
| 59 | * | ||
| 60 | * This check is conservative and considers data unsafe regardless of | ||
| 61 | * current terminal state. | ||
| 62 | * | ||
| 63 | * @param data The paste data to check (must not be NULL) | ||
| 64 | * @param len The length of the data in bytes | ||
| 65 | * @return true if the data is safe to paste, false otherwise | ||
| 66 | */ | ||
| 67 | bool ghostty_paste_is_safe(const char* data, size_t len); | ||
| 68 | |||
| 69 | #ifdef __cplusplus | ||
| 70 | } | ||
| 71 | #endif | ||
| 72 | |||
| 73 | /** @} */ | ||
| 74 | |||
| 75 | #endif /* GHOSTTY_VT_PASTE_H */ | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | /** | ||
| 2 | * @file result.h | ||
| 3 | * | ||
| 4 | * Result codes for libghostty-vt operations. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_RESULT_H | ||
| 8 | #define GHOSTTY_VT_RESULT_H | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Result codes for libghostty-vt operations. | ||
| 12 | */ | ||
| 13 | typedef enum { | ||
| 14 | /** Operation completed successfully */ | ||
| 15 | GHOSTTY_SUCCESS = 0, | ||
| 16 | /** Operation failed due to failed allocation */ | ||
| 17 | GHOSTTY_OUT_OF_MEMORY = -1, | ||
| 18 | /** Operation failed due to invalid value */ | ||
| 19 | GHOSTTY_INVALID_VALUE = -2, | ||
| 20 | } GhosttyResult; | ||
| 21 | |||
| 22 | #endif /* GHOSTTY_VT_RESULT_H */ | ||
| @@ -0,0 +1,394 @@ | |||
| 1 | /** | ||
| 2 | * @file sgr.h | ||
| 3 | * | ||
| 4 | * SGR (Select Graphic Rendition) attribute parsing and handling. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_SGR_H | ||
| 8 | #define GHOSTTY_VT_SGR_H | ||
| 9 | |||
| 10 | /** @defgroup sgr SGR Parser | ||
| 11 | * | ||
| 12 | * SGR (Select Graphic Rendition) attribute parser. | ||
| 13 | * | ||
| 14 | * SGR sequences are the syntax used to set styling attributes such as | ||
| 15 | * bold, italic, underline, and colors for text in terminal emulators. | ||
| 16 | * For example, you may be familiar with sequences like `ESC[1;31m`. The | ||
| 17 | * `1;31` is the SGR attribute list. | ||
| 18 | * | ||
| 19 | * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`) | ||
| 20 | * and returns individual text attributes like bold, italic, colors, etc. | ||
| 21 | * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed, | ||
| 22 | * and handles various color formats including 8-color, 16-color, 256-color, | ||
| 23 | * X11 named colors, and RGB in multiple formats. | ||
| 24 | * | ||
| 25 | * ## Basic Usage | ||
| 26 | * | ||
| 27 | * 1. Create a parser instance with ghostty_sgr_new() | ||
| 28 | * 2. Set SGR parameters with ghostty_sgr_set_params() | ||
| 29 | * 3. Iterate through attributes using ghostty_sgr_next() | ||
| 30 | * 4. Free the parser with ghostty_sgr_free() when done | ||
| 31 | * | ||
| 32 | * ## Example | ||
| 33 | * | ||
| 34 | * @code{.c} | ||
| 35 | * #include <assert.h> | ||
| 36 | * #include <stdio.h> | ||
| 37 | * #include <ghostty/vt.h> | ||
| 38 | * | ||
| 39 | * int main() { | ||
| 40 | * // Create parser | ||
| 41 | * GhosttySgrParser parser; | ||
| 42 | * GhosttyResult result = ghostty_sgr_new(NULL, &parser); | ||
| 43 | * assert(result == GHOSTTY_SUCCESS); | ||
| 44 | * | ||
| 45 | * // Parse "bold, red foreground" sequence: ESC[1;31m | ||
| 46 | * uint16_t params[] = {1, 31}; | ||
| 47 | * result = ghostty_sgr_set_params(parser, params, NULL, 2); | ||
| 48 | * assert(result == GHOSTTY_SUCCESS); | ||
| 49 | * | ||
| 50 | * // Iterate through attributes | ||
| 51 | * GhosttySgrAttribute attr; | ||
| 52 | * while (ghostty_sgr_next(parser, &attr)) { | ||
| 53 | * switch (attr.tag) { | ||
| 54 | * case GHOSTTY_SGR_ATTR_BOLD: | ||
| 55 | * printf("Bold enabled\n"); | ||
| 56 | * break; | ||
| 57 | * case GHOSTTY_SGR_ATTR_FG_8: | ||
| 58 | * printf("Foreground color: %d\n", attr.value.fg_8); | ||
| 59 | * break; | ||
| 60 | * default: | ||
| 61 | * break; | ||
| 62 | * } | ||
| 63 | * } | ||
| 64 | * | ||
| 65 | * // Cleanup | ||
| 66 | * ghostty_sgr_free(parser); | ||
| 67 | * return 0; | ||
| 68 | * } | ||
| 69 | * @endcode | ||
| 70 | * | ||
| 71 | * @{ | ||
| 72 | */ | ||
| 73 | |||
| 74 | #include <ghostty/vt/allocator.h> | ||
| 75 | #include <ghostty/vt/color.h> | ||
| 76 | #include <ghostty/vt/result.h> | ||
| 77 | #include <stdbool.h> | ||
| 78 | #include <stddef.h> | ||
| 79 | #include <stdint.h> | ||
| 80 | |||
| 81 | #ifdef __cplusplus | ||
| 82 | extern "C" { | ||
| 83 | #endif | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Opaque handle to an SGR parser instance. | ||
| 87 | * | ||
| 88 | * This handle represents an SGR (Select Graphic Rendition) parser that can | ||
| 89 | * be used to parse SGR sequences and extract individual text attributes. | ||
| 90 | * | ||
| 91 | * @ingroup sgr | ||
| 92 | */ | ||
| 93 | typedef struct GhosttySgrParser* GhosttySgrParser; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * SGR attribute tags. | ||
| 97 | * | ||
| 98 | * These values identify the type of an SGR attribute in a tagged union. | ||
| 99 | * Use the tag to determine which field in the attribute value union to access. | ||
| 100 | * | ||
| 101 | * @ingroup sgr | ||
| 102 | */ | ||
| 103 | typedef enum { | ||
| 104 | GHOSTTY_SGR_ATTR_UNSET = 0, | ||
| 105 | GHOSTTY_SGR_ATTR_UNKNOWN = 1, | ||
| 106 | GHOSTTY_SGR_ATTR_BOLD = 2, | ||
| 107 | GHOSTTY_SGR_ATTR_RESET_BOLD = 3, | ||
| 108 | GHOSTTY_SGR_ATTR_ITALIC = 4, | ||
| 109 | GHOSTTY_SGR_ATTR_RESET_ITALIC = 5, | ||
| 110 | GHOSTTY_SGR_ATTR_FAINT = 6, | ||
| 111 | GHOSTTY_SGR_ATTR_UNDERLINE = 7, | ||
| 112 | GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8, | ||
| 113 | GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9, | ||
| 114 | GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10, | ||
| 115 | GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11, | ||
| 116 | GHOSTTY_SGR_ATTR_OVERLINE = 12, | ||
| 117 | GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13, | ||
| 118 | GHOSTTY_SGR_ATTR_BLINK = 14, | ||
| 119 | GHOSTTY_SGR_ATTR_RESET_BLINK = 15, | ||
| 120 | GHOSTTY_SGR_ATTR_INVERSE = 16, | ||
| 121 | GHOSTTY_SGR_ATTR_RESET_INVERSE = 17, | ||
| 122 | GHOSTTY_SGR_ATTR_INVISIBLE = 18, | ||
| 123 | GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19, | ||
| 124 | GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20, | ||
| 125 | GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21, | ||
| 126 | GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22, | ||
| 127 | GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23, | ||
| 128 | GHOSTTY_SGR_ATTR_BG_8 = 24, | ||
| 129 | GHOSTTY_SGR_ATTR_FG_8 = 25, | ||
| 130 | GHOSTTY_SGR_ATTR_RESET_FG = 26, | ||
| 131 | GHOSTTY_SGR_ATTR_RESET_BG = 27, | ||
| 132 | GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28, | ||
| 133 | GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29, | ||
| 134 | GHOSTTY_SGR_ATTR_BG_256 = 30, | ||
| 135 | GHOSTTY_SGR_ATTR_FG_256 = 31, | ||
| 136 | } GhosttySgrAttributeTag; | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Underline style types. | ||
| 140 | * | ||
| 141 | * @ingroup sgr | ||
| 142 | */ | ||
| 143 | typedef enum { | ||
| 144 | GHOSTTY_SGR_UNDERLINE_NONE = 0, | ||
| 145 | GHOSTTY_SGR_UNDERLINE_SINGLE = 1, | ||
| 146 | GHOSTTY_SGR_UNDERLINE_DOUBLE = 2, | ||
| 147 | GHOSTTY_SGR_UNDERLINE_CURLY = 3, | ||
| 148 | GHOSTTY_SGR_UNDERLINE_DOTTED = 4, | ||
| 149 | GHOSTTY_SGR_UNDERLINE_DASHED = 5, | ||
| 150 | } GhosttySgrUnderline; | ||
| 151 | |||
| 152 | /** | ||
| 153 | * Unknown SGR attribute data. | ||
| 154 | * | ||
| 155 | * Contains the full parameter list and the partial list where parsing | ||
| 156 | * encountered an unknown or invalid sequence. | ||
| 157 | * | ||
| 158 | * @ingroup sgr | ||
| 159 | */ | ||
| 160 | typedef struct { | ||
| 161 | const uint16_t* full_ptr; | ||
| 162 | size_t full_len; | ||
| 163 | const uint16_t* partial_ptr; | ||
| 164 | size_t partial_len; | ||
| 165 | } GhosttySgrUnknown; | ||
| 166 | |||
| 167 | /** | ||
| 168 | * SGR attribute value union. | ||
| 169 | * | ||
| 170 | * This union contains all possible attribute values. Use the tag field | ||
| 171 | * to determine which union member is active. Attributes without associated | ||
| 172 | * data (like bold, italic) don't use the union value. | ||
| 173 | * | ||
| 174 | * @ingroup sgr | ||
| 175 | */ | ||
| 176 | typedef union { | ||
| 177 | GhosttySgrUnknown unknown; | ||
| 178 | GhosttySgrUnderline underline; | ||
| 179 | GhosttyColorRgb underline_color; | ||
| 180 | GhosttyColorPaletteIndex underline_color_256; | ||
| 181 | GhosttyColorRgb direct_color_fg; | ||
| 182 | GhosttyColorRgb direct_color_bg; | ||
| 183 | GhosttyColorPaletteIndex bg_8; | ||
| 184 | GhosttyColorPaletteIndex fg_8; | ||
| 185 | GhosttyColorPaletteIndex bright_bg_8; | ||
| 186 | GhosttyColorPaletteIndex bright_fg_8; | ||
| 187 | GhosttyColorPaletteIndex bg_256; | ||
| 188 | GhosttyColorPaletteIndex fg_256; | ||
| 189 | uint64_t _padding[8]; | ||
| 190 | } GhosttySgrAttributeValue; | ||
| 191 | |||
| 192 | /** | ||
| 193 | * SGR attribute (tagged union). | ||
| 194 | * | ||
| 195 | * A complete SGR attribute with both its type tag and associated value. | ||
| 196 | * Always check the tag field to determine which value union member is valid. | ||
| 197 | * | ||
| 198 | * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be | ||
| 199 | * identified by tag alone; the value union is not used for these and | ||
| 200 | * the memory in the value field is undefined. | ||
| 201 | * | ||
| 202 | * @ingroup sgr | ||
| 203 | */ | ||
| 204 | typedef struct { | ||
| 205 | GhosttySgrAttributeTag tag; | ||
| 206 | GhosttySgrAttributeValue value; | ||
| 207 | } GhosttySgrAttribute; | ||
| 208 | |||
| 209 | /** | ||
| 210 | * Create a new SGR parser instance. | ||
| 211 | * | ||
| 212 | * Creates a new SGR (Select Graphic Rendition) parser using the provided | ||
| 213 | * allocator. The parser must be freed using ghostty_sgr_free() when | ||
| 214 | * no longer needed. | ||
| 215 | * | ||
| 216 | * @param allocator Pointer to the allocator to use for memory management, or | ||
| 217 | * NULL to use the default allocator | ||
| 218 | * @param parser Pointer to store the created parser handle | ||
| 219 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 220 | * | ||
| 221 | * @ingroup sgr | ||
| 222 | */ | ||
| 223 | GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, | ||
| 224 | GhosttySgrParser* parser); | ||
| 225 | |||
| 226 | /** | ||
| 227 | * Free an SGR parser instance. | ||
| 228 | * | ||
| 229 | * Releases all resources associated with the SGR parser. After this call, | ||
| 230 | * the parser handle becomes invalid and must not be used. This includes | ||
| 231 | * any attributes previously returned by ghostty_sgr_next(). | ||
| 232 | * | ||
| 233 | * @param parser The parser handle to free (may be NULL) | ||
| 234 | * | ||
| 235 | * @ingroup sgr | ||
| 236 | */ | ||
| 237 | void ghostty_sgr_free(GhosttySgrParser parser); | ||
| 238 | |||
| 239 | /** | ||
| 240 | * Reset an SGR parser instance to the beginning of the parameter list. | ||
| 241 | * | ||
| 242 | * Resets the parser's iteration state without clearing the parameters. | ||
| 243 | * After calling this, ghostty_sgr_next() will start from the beginning | ||
| 244 | * of the parameter list again. | ||
| 245 | * | ||
| 246 | * @param parser The parser handle to reset, must not be NULL | ||
| 247 | * | ||
| 248 | * @ingroup sgr | ||
| 249 | */ | ||
| 250 | void ghostty_sgr_reset(GhosttySgrParser parser); | ||
| 251 | |||
| 252 | /** | ||
| 253 | * Set SGR parameters for parsing. | ||
| 254 | * | ||
| 255 | * Sets the SGR parameter list to parse. Parameters are the numeric values | ||
| 256 | * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}). | ||
| 257 | * | ||
| 258 | * The separators array optionally specifies the separator type for each | ||
| 259 | * parameter position. Each byte should be either ';' for semicolon or ':' | ||
| 260 | * for colon. This is needed for certain color formats that use colon | ||
| 261 | * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator | ||
| 262 | * values are treated as semicolons. The separators array must have the same | ||
| 263 | * length as the params array, if it is not NULL. | ||
| 264 | * | ||
| 265 | * If separators is NULL, all parameters are assumed to be semicolon-separated. | ||
| 266 | * | ||
| 267 | * This function makes an internal copy of the parameter and separator data, | ||
| 268 | * so the caller can safely free or modify the input arrays after this call. | ||
| 269 | * | ||
| 270 | * After calling this function, the parser is automatically reset and ready | ||
| 271 | * to iterate from the beginning. | ||
| 272 | * | ||
| 273 | * @param parser The parser handle, must not be NULL | ||
| 274 | * @param params Array of SGR parameter values | ||
| 275 | * @param separators Optional array of separator characters (';' or ':'), or | ||
| 276 | * NULL | ||
| 277 | * @param len Number of parameters (and separators if provided) | ||
| 278 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 279 | * | ||
| 280 | * @ingroup sgr | ||
| 281 | */ | ||
| 282 | GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, | ||
| 283 | const uint16_t* params, | ||
| 284 | const char* separators, | ||
| 285 | size_t len); | ||
| 286 | |||
| 287 | /** | ||
| 288 | * Get the next SGR attribute. | ||
| 289 | * | ||
| 290 | * Parses and returns the next attribute from the parameter list. | ||
| 291 | * Call this function repeatedly until it returns false to process | ||
| 292 | * all attributes in the sequence. | ||
| 293 | * | ||
| 294 | * @param parser The parser handle, must not be NULL | ||
| 295 | * @param attr Pointer to store the next attribute | ||
| 296 | * @return true if an attribute was returned, false if no more attributes | ||
| 297 | * | ||
| 298 | * @ingroup sgr | ||
| 299 | */ | ||
| 300 | bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); | ||
| 301 | |||
| 302 | /** | ||
| 303 | * Get the full parameter list from an unknown SGR attribute. | ||
| 304 | * | ||
| 305 | * This function retrieves the full parameter list that was provided to the | ||
| 306 | * parser when an unknown attribute was encountered. Primarily useful in | ||
| 307 | * WebAssembly environments where accessing struct fields directly is difficult. | ||
| 308 | * | ||
| 309 | * @param unknown The unknown attribute data | ||
| 310 | * @param ptr Pointer to store the pointer to the parameter array (may be NULL) | ||
| 311 | * @return The length of the full parameter array | ||
| 312 | * | ||
| 313 | * @ingroup sgr | ||
| 314 | */ | ||
| 315 | size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, | ||
| 316 | const uint16_t** ptr); | ||
| 317 | |||
| 318 | /** | ||
| 319 | * Get the partial parameter list from an unknown SGR attribute. | ||
| 320 | * | ||
| 321 | * This function retrieves the partial parameter list where parsing stopped | ||
| 322 | * when an unknown attribute was encountered. Primarily useful in WebAssembly | ||
| 323 | * environments where accessing struct fields directly is difficult. | ||
| 324 | * | ||
| 325 | * @param unknown The unknown attribute data | ||
| 326 | * @param ptr Pointer to store the pointer to the parameter array (may be NULL) | ||
| 327 | * @return The length of the partial parameter array | ||
| 328 | * | ||
| 329 | * @ingroup sgr | ||
| 330 | */ | ||
| 331 | size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, | ||
| 332 | const uint16_t** ptr); | ||
| 333 | |||
| 334 | /** | ||
| 335 | * Get the tag from an SGR attribute. | ||
| 336 | * | ||
| 337 | * This function extracts the tag that identifies which type of attribute | ||
| 338 | * this is. Primarily useful in WebAssembly environments where accessing | ||
| 339 | * struct fields directly is difficult. | ||
| 340 | * | ||
| 341 | * @param attr The SGR attribute | ||
| 342 | * @return The attribute tag | ||
| 343 | * | ||
| 344 | * @ingroup sgr | ||
| 345 | */ | ||
| 346 | GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); | ||
| 347 | |||
| 348 | /** | ||
| 349 | * Get the value from an SGR attribute. | ||
| 350 | * | ||
| 351 | * This function returns a pointer to the value union from an SGR attribute. Use | ||
| 352 | * the tag to determine which field of the union is valid. Primarily useful in | ||
| 353 | * WebAssembly environments where accessing struct fields directly is difficult. | ||
| 354 | * | ||
| 355 | * @param attr Pointer to the SGR attribute | ||
| 356 | * @return Pointer to the attribute value union | ||
| 357 | * | ||
| 358 | * @ingroup sgr | ||
| 359 | */ | ||
| 360 | GhosttySgrAttributeValue* ghostty_sgr_attribute_value( | ||
| 361 | GhosttySgrAttribute* attr); | ||
| 362 | |||
| 363 | #ifdef __wasm__ | ||
| 364 | /** | ||
| 365 | * Allocate memory for an SGR attribute (WebAssembly only). | ||
| 366 | * | ||
| 367 | * This is a convenience function for WebAssembly environments to allocate | ||
| 368 | * memory for an SGR attribute structure that can be passed to ghostty_sgr_next. | ||
| 369 | * | ||
| 370 | * @return Pointer to the allocated attribute structure | ||
| 371 | * | ||
| 372 | * @ingroup wasm | ||
| 373 | */ | ||
| 374 | GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); | ||
| 375 | |||
| 376 | /** | ||
| 377 | * Free memory for an SGR attribute (WebAssembly only). | ||
| 378 | * | ||
| 379 | * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute. | ||
| 380 | * | ||
| 381 | * @param attr Pointer to the attribute structure to free | ||
| 382 | * | ||
| 383 | * @ingroup wasm | ||
| 384 | */ | ||
| 385 | void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr); | ||
| 386 | #endif | ||
| 387 | |||
| 388 | #ifdef __cplusplus | ||
| 389 | } | ||
| 390 | #endif | ||
| 391 | |||
| 392 | /** @} */ | ||
| 393 | |||
| 394 | #endif /* GHOSTTY_VT_SGR_H */ | ||
| @@ -0,0 +1,159 @@ | |||
| 1 | /** | ||
| 2 | * @file wasm.h | ||
| 3 | * | ||
| 4 | * WebAssembly utility functions for libghostty-vt. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_WASM_H | ||
| 8 | #define GHOSTTY_VT_WASM_H | ||
| 9 | |||
| 10 | #ifdef __wasm__ | ||
| 11 | |||
| 12 | #include <stddef.h> | ||
| 13 | #include <stdint.h> | ||
| 14 | |||
| 15 | /** @defgroup wasm WebAssembly Utilities | ||
| 16 | * | ||
| 17 | * Convenience functions for allocating various types in WebAssembly builds. | ||
| 18 | * **These are only available the libghostty-vt wasm module.** | ||
| 19 | * | ||
| 20 | * Ghostty relies on pointers to various types for ABI compatibility, and | ||
| 21 | * creating those pointers in Wasm can be tedious. These functions provide | ||
| 22 | * a purely additive set of utilities that simplify memory management in | ||
| 23 | * Wasm environments without changing the core C library API. | ||
| 24 | * | ||
| 25 | * @note These functions always use the default allocator. If you need | ||
| 26 | * custom allocation strategies, you should allocate types manually using | ||
| 27 | * your custom allocator. This is a very rare use case in the WebAssembly | ||
| 28 | * world so these are optimized for simplicity. | ||
| 29 | * | ||
| 30 | * ## Example Usage | ||
| 31 | * | ||
| 32 | * Here's a simple example of using the Wasm utilities with the key encoder: | ||
| 33 | * | ||
| 34 | * @code | ||
| 35 | * const { exports } = wasmInstance; | ||
| 36 | * const view = new DataView(wasmMemory.buffer); | ||
| 37 | * | ||
| 38 | * // Create key encoder | ||
| 39 | * const encoderPtr = exports.ghostty_wasm_alloc_opaque(); | ||
| 40 | * exports.ghostty_key_encoder_new(null, encoderPtr); | ||
| 41 | * const encoder = view.getUint32(encoder, true); | ||
| 42 | * | ||
| 43 | * // Configure encoder with Kitty protocol flags | ||
| 44 | * const flagsPtr = exports.ghostty_wasm_alloc_u8(); | ||
| 45 | * view.setUint8(flagsPtr, 0x1F); | ||
| 46 | * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr); | ||
| 47 | * | ||
| 48 | * // Allocate output buffer and size pointer | ||
| 49 | * const bufferSize = 32; | ||
| 50 | * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize); | ||
| 51 | * const writtenPtr = exports.ghostty_wasm_alloc_usize(); | ||
| 52 | * | ||
| 53 | * // Encode the key event | ||
| 54 | * exports.ghostty_key_encoder_encode( | ||
| 55 | * encoder, eventPtr, bufPtr, bufferSize, writtenPtr | ||
| 56 | * ); | ||
| 57 | * | ||
| 58 | * // Read encoded output | ||
| 59 | * const bytesWritten = view.getUint32(writtenPtr, true); | ||
| 60 | * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten); | ||
| 61 | * @endcode | ||
| 62 | * | ||
| 63 | * @remark The code above is pretty ugly! This is the lowest level interface | ||
| 64 | * to the libghostty-vt Wasm module. In practice, this should be wrapped | ||
| 65 | * in a higher-level API that abstracts away all this. | ||
| 66 | * | ||
| 67 | * @{ | ||
| 68 | */ | ||
| 69 | |||
| 70 | /** | ||
| 71 | * Allocate an opaque pointer. This can be used for any opaque pointer | ||
| 72 | * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc. | ||
| 73 | * | ||
| 74 | * @return Pointer to allocated opaque pointer, or NULL if allocation failed | ||
| 75 | * @ingroup wasm | ||
| 76 | */ | ||
| 77 | void** ghostty_wasm_alloc_opaque(void); | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque(). | ||
| 81 | * | ||
| 82 | * @param ptr Pointer to free, or NULL (NULL is safely ignored) | ||
| 83 | * @ingroup wasm | ||
| 84 | */ | ||
| 85 | void ghostty_wasm_free_opaque(void **ptr); | ||
| 86 | |||
| 87 | /** | ||
| 88 | * Allocate an array of uint8_t values. | ||
| 89 | * | ||
| 90 | * @param len Number of uint8_t elements to allocate | ||
| 91 | * @return Pointer to allocated array, or NULL if allocation failed | ||
| 92 | * @ingroup wasm | ||
| 93 | */ | ||
| 94 | uint8_t* ghostty_wasm_alloc_u8_array(size_t len); | ||
| 95 | |||
| 96 | /** | ||
| 97 | * Free an array allocated by ghostty_wasm_alloc_u8_array(). | ||
| 98 | * | ||
| 99 | * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) | ||
| 100 | * @param len Length of the array (must match the length passed to alloc) | ||
| 101 | * @ingroup wasm | ||
| 102 | */ | ||
| 103 | void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Allocate an array of uint16_t values. | ||
| 107 | * | ||
| 108 | * @param len Number of uint16_t elements to allocate | ||
| 109 | * @return Pointer to allocated array, or NULL if allocation failed | ||
| 110 | * @ingroup wasm | ||
| 111 | */ | ||
| 112 | uint16_t* ghostty_wasm_alloc_u16_array(size_t len); | ||
| 113 | |||
| 114 | /** | ||
| 115 | * Free an array allocated by ghostty_wasm_alloc_u16_array(). | ||
| 116 | * | ||
| 117 | * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) | ||
| 118 | * @param len Length of the array (must match the length passed to alloc) | ||
| 119 | * @ingroup wasm | ||
| 120 | */ | ||
| 121 | void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Allocate a single uint8_t value. | ||
| 125 | * | ||
| 126 | * @return Pointer to allocated uint8_t, or NULL if allocation failed | ||
| 127 | * @ingroup wasm | ||
| 128 | */ | ||
| 129 | uint8_t* ghostty_wasm_alloc_u8(void); | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Free a uint8_t allocated by ghostty_wasm_alloc_u8(). | ||
| 133 | * | ||
| 134 | * @param ptr Pointer to free, or NULL (NULL is safely ignored) | ||
| 135 | * @ingroup wasm | ||
| 136 | */ | ||
| 137 | void ghostty_wasm_free_u8(uint8_t *ptr); | ||
| 138 | |||
| 139 | /** | ||
| 140 | * Allocate a single size_t value. | ||
| 141 | * | ||
| 142 | * @return Pointer to allocated size_t, or NULL if allocation failed | ||
| 143 | * @ingroup wasm | ||
| 144 | */ | ||
| 145 | size_t* ghostty_wasm_alloc_usize(void); | ||
| 146 | |||
| 147 | /** | ||
| 148 | * Free a size_t allocated by ghostty_wasm_alloc_usize(). | ||
| 149 | * | ||
| 150 | * @param ptr Pointer to free, or NULL (NULL is safely ignored) | ||
| 151 | * @ingroup wasm | ||
| 152 | */ | ||
| 153 | void ghostty_wasm_free_usize(size_t *ptr); | ||
| 154 | |||
| 155 | /** @} */ | ||
| 156 | |||
| 157 | #endif /* __wasm__ */ | ||
| 158 | |||
| 159 | #endif /* GHOSTTY_VT_WASM_H */ | ||
| @@ -0,0 +1,7 @@ | |||
| 1 | // This makes Ghostty available to the XCode build for the macOS app. | ||
| 2 | // We append "Kit" to it not to be cute, but because targets have to have | ||
| 3 | // unique names and we use Ghostty for other things. | ||
| 4 | module GhosttyKit { | ||
| 5 | umbrella header "ghostty.h" | ||
| 6 | export * | ||
| 7 | } | ||
Binary file not shown.
| @@ -0,0 +1,1187 @@ | |||
| 1 | // Ghostty embedding API. The documentation for the embedding API is | ||
| 2 | // only within the Zig source files that define the implementations. This | ||
| 3 | // isn't meant to be a general purpose embedding API (yet) so there hasn't | ||
| 4 | // been documentation or example work beyond that. | ||
| 5 | // | ||
| 6 | // The only consumer of this API is the macOS app, but the API is built to | ||
| 7 | // be more general purpose. | ||
| 8 | #ifndef GHOSTTY_H | ||
| 9 | #define GHOSTTY_H | ||
| 10 | |||
| 11 | #ifdef __cplusplus | ||
| 12 | extern "C" { | ||
| 13 | #endif | ||
| 14 | |||
| 15 | #include <stdbool.h> | ||
| 16 | #include <stddef.h> | ||
| 17 | #include <stdint.h> | ||
| 18 | #include <sys/types.h> | ||
| 19 | |||
| 20 | //------------------------------------------------------------------- | ||
| 21 | // Macros | ||
| 22 | |||
| 23 | #define GHOSTTY_SUCCESS 0 | ||
| 24 | |||
| 25 | //------------------------------------------------------------------- | ||
| 26 | // Types | ||
| 27 | |||
| 28 | // Opaque types | ||
| 29 | typedef void* ghostty_app_t; | ||
| 30 | typedef void* ghostty_config_t; | ||
| 31 | typedef void* ghostty_surface_t; | ||
| 32 | typedef void* ghostty_inspector_t; | ||
| 33 | |||
| 34 | // All the types below are fully defined and must be kept in sync with | ||
| 35 | // their Zig counterparts. Any changes to these types MUST have an associated | ||
| 36 | // Zig change. | ||
| 37 | typedef enum { | ||
| 38 | GHOSTTY_PLATFORM_INVALID, | ||
| 39 | GHOSTTY_PLATFORM_MACOS, | ||
| 40 | GHOSTTY_PLATFORM_IOS, | ||
| 41 | } ghostty_platform_e; | ||
| 42 | |||
| 43 | // Callback for custom I/O write handler. | ||
| 44 | typedef void (*ghostty_surface_write_fn)(void* userdata, | ||
| 45 | const uint8_t* data, | ||
| 46 | size_t len); | ||
| 47 | |||
| 48 | typedef enum { | ||
| 49 | GHOSTTY_CLIPBOARD_STANDARD, | ||
| 50 | GHOSTTY_CLIPBOARD_SELECTION, | ||
| 51 | } ghostty_clipboard_e; | ||
| 52 | |||
| 53 | typedef struct { | ||
| 54 | const char *mime; | ||
| 55 | const char *data; | ||
| 56 | } ghostty_clipboard_content_s; | ||
| 57 | |||
| 58 | typedef enum { | ||
| 59 | GHOSTTY_CLIPBOARD_REQUEST_PASTE, | ||
| 60 | GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ, | ||
| 61 | GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE, | ||
| 62 | } ghostty_clipboard_request_e; | ||
| 63 | |||
| 64 | typedef enum { | ||
| 65 | GHOSTTY_MOUSE_RELEASE, | ||
| 66 | GHOSTTY_MOUSE_PRESS, | ||
| 67 | } ghostty_input_mouse_state_e; | ||
| 68 | |||
| 69 | typedef enum { | ||
| 70 | GHOSTTY_MOUSE_UNKNOWN, | ||
| 71 | GHOSTTY_MOUSE_LEFT, | ||
| 72 | GHOSTTY_MOUSE_RIGHT, | ||
| 73 | GHOSTTY_MOUSE_MIDDLE, | ||
| 74 | GHOSTTY_MOUSE_FOUR, | ||
| 75 | GHOSTTY_MOUSE_FIVE, | ||
| 76 | GHOSTTY_MOUSE_SIX, | ||
| 77 | GHOSTTY_MOUSE_SEVEN, | ||
| 78 | GHOSTTY_MOUSE_EIGHT, | ||
| 79 | GHOSTTY_MOUSE_NINE, | ||
| 80 | GHOSTTY_MOUSE_TEN, | ||
| 81 | GHOSTTY_MOUSE_ELEVEN, | ||
| 82 | } ghostty_input_mouse_button_e; | ||
| 83 | |||
| 84 | typedef enum { | ||
| 85 | GHOSTTY_MOUSE_MOMENTUM_NONE, | ||
| 86 | GHOSTTY_MOUSE_MOMENTUM_BEGAN, | ||
| 87 | GHOSTTY_MOUSE_MOMENTUM_STATIONARY, | ||
| 88 | GHOSTTY_MOUSE_MOMENTUM_CHANGED, | ||
| 89 | GHOSTTY_MOUSE_MOMENTUM_ENDED, | ||
| 90 | GHOSTTY_MOUSE_MOMENTUM_CANCELLED, | ||
| 91 | GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN, | ||
| 92 | } ghostty_input_mouse_momentum_e; | ||
| 93 | |||
| 94 | typedef enum { | ||
| 95 | GHOSTTY_COLOR_SCHEME_LIGHT = 0, | ||
| 96 | GHOSTTY_COLOR_SCHEME_DARK = 1, | ||
| 97 | } ghostty_color_scheme_e; | ||
| 98 | |||
| 99 | // This is a packed struct (see src/input/mouse.zig) but the C standard | ||
| 100 | // afaik doesn't let us reliably define packed structs so we build it up | ||
| 101 | // from scratch. | ||
| 102 | typedef int ghostty_input_scroll_mods_t; | ||
| 103 | |||
| 104 | typedef enum { | ||
| 105 | GHOSTTY_MODS_NONE = 0, | ||
| 106 | GHOSTTY_MODS_SHIFT = 1 << 0, | ||
| 107 | GHOSTTY_MODS_CTRL = 1 << 1, | ||
| 108 | GHOSTTY_MODS_ALT = 1 << 2, | ||
| 109 | GHOSTTY_MODS_SUPER = 1 << 3, | ||
| 110 | GHOSTTY_MODS_CAPS = 1 << 4, | ||
| 111 | GHOSTTY_MODS_NUM = 1 << 5, | ||
| 112 | GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6, | ||
| 113 | GHOSTTY_MODS_CTRL_RIGHT = 1 << 7, | ||
| 114 | GHOSTTY_MODS_ALT_RIGHT = 1 << 8, | ||
| 115 | GHOSTTY_MODS_SUPER_RIGHT = 1 << 9, | ||
| 116 | } ghostty_input_mods_e; | ||
| 117 | |||
| 118 | typedef enum { | ||
| 119 | GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0, | ||
| 120 | GHOSTTY_BINDING_FLAGS_ALL = 1 << 1, | ||
| 121 | GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2, | ||
| 122 | GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3, | ||
| 123 | } ghostty_binding_flags_e; | ||
| 124 | |||
| 125 | typedef enum { | ||
| 126 | GHOSTTY_ACTION_RELEASE, | ||
| 127 | GHOSTTY_ACTION_PRESS, | ||
| 128 | GHOSTTY_ACTION_REPEAT, | ||
| 129 | } ghostty_input_action_e; | ||
| 130 | |||
| 131 | // Based on: https://www.w3.org/TR/uievents-code/ | ||
| 132 | typedef enum { | ||
| 133 | GHOSTTY_KEY_UNIDENTIFIED, | ||
| 134 | |||
| 135 | // "Writing System Keys" § 3.1.1 | ||
| 136 | GHOSTTY_KEY_BACKQUOTE, | ||
| 137 | GHOSTTY_KEY_BACKSLASH, | ||
| 138 | GHOSTTY_KEY_BRACKET_LEFT, | ||
| 139 | GHOSTTY_KEY_BRACKET_RIGHT, | ||
| 140 | GHOSTTY_KEY_COMMA, | ||
| 141 | GHOSTTY_KEY_DIGIT_0, | ||
| 142 | GHOSTTY_KEY_DIGIT_1, | ||
| 143 | GHOSTTY_KEY_DIGIT_2, | ||
| 144 | GHOSTTY_KEY_DIGIT_3, | ||
| 145 | GHOSTTY_KEY_DIGIT_4, | ||
| 146 | GHOSTTY_KEY_DIGIT_5, | ||
| 147 | GHOSTTY_KEY_DIGIT_6, | ||
| 148 | GHOSTTY_KEY_DIGIT_7, | ||
| 149 | GHOSTTY_KEY_DIGIT_8, | ||
| 150 | GHOSTTY_KEY_DIGIT_9, | ||
| 151 | GHOSTTY_KEY_EQUAL, | ||
| 152 | GHOSTTY_KEY_INTL_BACKSLASH, | ||
| 153 | GHOSTTY_KEY_INTL_RO, | ||
| 154 | GHOSTTY_KEY_INTL_YEN, | ||
| 155 | GHOSTTY_KEY_A, | ||
| 156 | GHOSTTY_KEY_B, | ||
| 157 | GHOSTTY_KEY_C, | ||
| 158 | GHOSTTY_KEY_D, | ||
| 159 | GHOSTTY_KEY_E, | ||
| 160 | GHOSTTY_KEY_F, | ||
| 161 | GHOSTTY_KEY_G, | ||
| 162 | GHOSTTY_KEY_H, | ||
| 163 | GHOSTTY_KEY_I, | ||
| 164 | GHOSTTY_KEY_J, | ||
| 165 | GHOSTTY_KEY_K, | ||
| 166 | GHOSTTY_KEY_L, | ||
| 167 | GHOSTTY_KEY_M, | ||
| 168 | GHOSTTY_KEY_N, | ||
| 169 | GHOSTTY_KEY_O, | ||
| 170 | GHOSTTY_KEY_P, | ||
| 171 | GHOSTTY_KEY_Q, | ||
| 172 | GHOSTTY_KEY_R, | ||
| 173 | GHOSTTY_KEY_S, | ||
| 174 | GHOSTTY_KEY_T, | ||
| 175 | GHOSTTY_KEY_U, | ||
| 176 | GHOSTTY_KEY_V, | ||
| 177 | GHOSTTY_KEY_W, | ||
| 178 | GHOSTTY_KEY_X, | ||
| 179 | GHOSTTY_KEY_Y, | ||
| 180 | GHOSTTY_KEY_Z, | ||
| 181 | GHOSTTY_KEY_MINUS, | ||
| 182 | GHOSTTY_KEY_PERIOD, | ||
| 183 | GHOSTTY_KEY_QUOTE, | ||
| 184 | GHOSTTY_KEY_SEMICOLON, | ||
| 185 | GHOSTTY_KEY_SLASH, | ||
| 186 | |||
| 187 | // "Functional Keys" § 3.1.2 | ||
| 188 | GHOSTTY_KEY_ALT_LEFT, | ||
| 189 | GHOSTTY_KEY_ALT_RIGHT, | ||
| 190 | GHOSTTY_KEY_BACKSPACE, | ||
| 191 | GHOSTTY_KEY_CAPS_LOCK, | ||
| 192 | GHOSTTY_KEY_CONTEXT_MENU, | ||
| 193 | GHOSTTY_KEY_CONTROL_LEFT, | ||
| 194 | GHOSTTY_KEY_CONTROL_RIGHT, | ||
| 195 | GHOSTTY_KEY_ENTER, | ||
| 196 | GHOSTTY_KEY_META_LEFT, | ||
| 197 | GHOSTTY_KEY_META_RIGHT, | ||
| 198 | GHOSTTY_KEY_SHIFT_LEFT, | ||
| 199 | GHOSTTY_KEY_SHIFT_RIGHT, | ||
| 200 | GHOSTTY_KEY_SPACE, | ||
| 201 | GHOSTTY_KEY_TAB, | ||
| 202 | GHOSTTY_KEY_CONVERT, | ||
| 203 | GHOSTTY_KEY_KANA_MODE, | ||
| 204 | GHOSTTY_KEY_NON_CONVERT, | ||
| 205 | |||
| 206 | // "Control Pad Section" § 3.2 | ||
| 207 | GHOSTTY_KEY_DELETE, | ||
| 208 | GHOSTTY_KEY_END, | ||
| 209 | GHOSTTY_KEY_HELP, | ||
| 210 | GHOSTTY_KEY_HOME, | ||
| 211 | GHOSTTY_KEY_INSERT, | ||
| 212 | GHOSTTY_KEY_PAGE_DOWN, | ||
| 213 | GHOSTTY_KEY_PAGE_UP, | ||
| 214 | |||
| 215 | // "Arrow Pad Section" § 3.3 | ||
| 216 | GHOSTTY_KEY_ARROW_DOWN, | ||
| 217 | GHOSTTY_KEY_ARROW_LEFT, | ||
| 218 | GHOSTTY_KEY_ARROW_RIGHT, | ||
| 219 | GHOSTTY_KEY_ARROW_UP, | ||
| 220 | |||
| 221 | // "Numpad Section" § 3.4 | ||
| 222 | GHOSTTY_KEY_NUM_LOCK, | ||
| 223 | GHOSTTY_KEY_NUMPAD_0, | ||
| 224 | GHOSTTY_KEY_NUMPAD_1, | ||
| 225 | GHOSTTY_KEY_NUMPAD_2, | ||
| 226 | GHOSTTY_KEY_NUMPAD_3, | ||
| 227 | GHOSTTY_KEY_NUMPAD_4, | ||
| 228 | GHOSTTY_KEY_NUMPAD_5, | ||
| 229 | GHOSTTY_KEY_NUMPAD_6, | ||
| 230 | GHOSTTY_KEY_NUMPAD_7, | ||
| 231 | GHOSTTY_KEY_NUMPAD_8, | ||
| 232 | GHOSTTY_KEY_NUMPAD_9, | ||
| 233 | GHOSTTY_KEY_NUMPAD_ADD, | ||
| 234 | GHOSTTY_KEY_NUMPAD_BACKSPACE, | ||
| 235 | GHOSTTY_KEY_NUMPAD_CLEAR, | ||
| 236 | GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, | ||
| 237 | GHOSTTY_KEY_NUMPAD_COMMA, | ||
| 238 | GHOSTTY_KEY_NUMPAD_DECIMAL, | ||
| 239 | GHOSTTY_KEY_NUMPAD_DIVIDE, | ||
| 240 | GHOSTTY_KEY_NUMPAD_ENTER, | ||
| 241 | GHOSTTY_KEY_NUMPAD_EQUAL, | ||
| 242 | GHOSTTY_KEY_NUMPAD_MEMORY_ADD, | ||
| 243 | GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, | ||
| 244 | GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, | ||
| 245 | GHOSTTY_KEY_NUMPAD_MEMORY_STORE, | ||
| 246 | GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, | ||
| 247 | GHOSTTY_KEY_NUMPAD_MULTIPLY, | ||
| 248 | GHOSTTY_KEY_NUMPAD_PAREN_LEFT, | ||
| 249 | GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, | ||
| 250 | GHOSTTY_KEY_NUMPAD_SUBTRACT, | ||
| 251 | GHOSTTY_KEY_NUMPAD_SEPARATOR, | ||
| 252 | GHOSTTY_KEY_NUMPAD_UP, | ||
| 253 | GHOSTTY_KEY_NUMPAD_DOWN, | ||
| 254 | GHOSTTY_KEY_NUMPAD_RIGHT, | ||
| 255 | GHOSTTY_KEY_NUMPAD_LEFT, | ||
| 256 | GHOSTTY_KEY_NUMPAD_BEGIN, | ||
| 257 | GHOSTTY_KEY_NUMPAD_HOME, | ||
| 258 | GHOSTTY_KEY_NUMPAD_END, | ||
| 259 | GHOSTTY_KEY_NUMPAD_INSERT, | ||
| 260 | GHOSTTY_KEY_NUMPAD_DELETE, | ||
| 261 | GHOSTTY_KEY_NUMPAD_PAGE_UP, | ||
| 262 | GHOSTTY_KEY_NUMPAD_PAGE_DOWN, | ||
| 263 | |||
| 264 | // "Function Section" § 3.5 | ||
| 265 | GHOSTTY_KEY_ESCAPE, | ||
| 266 | GHOSTTY_KEY_F1, | ||
| 267 | GHOSTTY_KEY_F2, | ||
| 268 | GHOSTTY_KEY_F3, | ||
| 269 | GHOSTTY_KEY_F4, | ||
| 270 | GHOSTTY_KEY_F5, | ||
| 271 | GHOSTTY_KEY_F6, | ||
| 272 | GHOSTTY_KEY_F7, | ||
| 273 | GHOSTTY_KEY_F8, | ||
| 274 | GHOSTTY_KEY_F9, | ||
| 275 | GHOSTTY_KEY_F10, | ||
| 276 | GHOSTTY_KEY_F11, | ||
| 277 | GHOSTTY_KEY_F12, | ||
| 278 | GHOSTTY_KEY_F13, | ||
| 279 | GHOSTTY_KEY_F14, | ||
| 280 | GHOSTTY_KEY_F15, | ||
| 281 | GHOSTTY_KEY_F16, | ||
| 282 | GHOSTTY_KEY_F17, | ||
| 283 | GHOSTTY_KEY_F18, | ||
| 284 | GHOSTTY_KEY_F19, | ||
| 285 | GHOSTTY_KEY_F20, | ||
| 286 | GHOSTTY_KEY_F21, | ||
| 287 | GHOSTTY_KEY_F22, | ||
| 288 | GHOSTTY_KEY_F23, | ||
| 289 | GHOSTTY_KEY_F24, | ||
| 290 | GHOSTTY_KEY_F25, | ||
| 291 | GHOSTTY_KEY_FN, | ||
| 292 | GHOSTTY_KEY_FN_LOCK, | ||
| 293 | GHOSTTY_KEY_PRINT_SCREEN, | ||
| 294 | GHOSTTY_KEY_SCROLL_LOCK, | ||
| 295 | GHOSTTY_KEY_PAUSE, | ||
| 296 | |||
| 297 | // "Media Keys" § 3.6 | ||
| 298 | GHOSTTY_KEY_BROWSER_BACK, | ||
| 299 | GHOSTTY_KEY_BROWSER_FAVORITES, | ||
| 300 | GHOSTTY_KEY_BROWSER_FORWARD, | ||
| 301 | GHOSTTY_KEY_BROWSER_HOME, | ||
| 302 | GHOSTTY_KEY_BROWSER_REFRESH, | ||
| 303 | GHOSTTY_KEY_BROWSER_SEARCH, | ||
| 304 | GHOSTTY_KEY_BROWSER_STOP, | ||
| 305 | GHOSTTY_KEY_EJECT, | ||
| 306 | GHOSTTY_KEY_LAUNCH_APP_1, | ||
| 307 | GHOSTTY_KEY_LAUNCH_APP_2, | ||
| 308 | GHOSTTY_KEY_LAUNCH_MAIL, | ||
| 309 | GHOSTTY_KEY_MEDIA_PLAY_PAUSE, | ||
| 310 | GHOSTTY_KEY_MEDIA_SELECT, | ||
| 311 | GHOSTTY_KEY_MEDIA_STOP, | ||
| 312 | GHOSTTY_KEY_MEDIA_TRACK_NEXT, | ||
| 313 | GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, | ||
| 314 | GHOSTTY_KEY_POWER, | ||
| 315 | GHOSTTY_KEY_SLEEP, | ||
| 316 | GHOSTTY_KEY_AUDIO_VOLUME_DOWN, | ||
| 317 | GHOSTTY_KEY_AUDIO_VOLUME_MUTE, | ||
| 318 | GHOSTTY_KEY_AUDIO_VOLUME_UP, | ||
| 319 | GHOSTTY_KEY_WAKE_UP, | ||
| 320 | |||
| 321 | // "Legacy, Non-standard, and Special Keys" § 3.7 | ||
| 322 | GHOSTTY_KEY_COPY, | ||
| 323 | GHOSTTY_KEY_CUT, | ||
| 324 | GHOSTTY_KEY_PASTE, | ||
| 325 | } ghostty_input_key_e; | ||
| 326 | |||
| 327 | typedef struct { | ||
| 328 | ghostty_input_action_e action; | ||
| 329 | ghostty_input_mods_e mods; | ||
| 330 | ghostty_input_mods_e consumed_mods; | ||
| 331 | uint32_t keycode; | ||
| 332 | const char* text; | ||
| 333 | uint32_t unshifted_codepoint; | ||
| 334 | bool composing; | ||
| 335 | } ghostty_input_key_s; | ||
| 336 | |||
| 337 | typedef enum { | ||
| 338 | GHOSTTY_TRIGGER_PHYSICAL, | ||
| 339 | GHOSTTY_TRIGGER_UNICODE, | ||
| 340 | GHOSTTY_TRIGGER_CATCH_ALL, | ||
| 341 | } ghostty_input_trigger_tag_e; | ||
| 342 | |||
| 343 | typedef union { | ||
| 344 | ghostty_input_key_e translated; | ||
| 345 | ghostty_input_key_e physical; | ||
| 346 | uint32_t unicode; | ||
| 347 | // catch_all has no payload | ||
| 348 | } ghostty_input_trigger_key_u; | ||
| 349 | |||
| 350 | typedef struct { | ||
| 351 | ghostty_input_trigger_tag_e tag; | ||
| 352 | ghostty_input_trigger_key_u key; | ||
| 353 | ghostty_input_mods_e mods; | ||
| 354 | } ghostty_input_trigger_s; | ||
| 355 | |||
| 356 | typedef struct { | ||
| 357 | const char* action_key; | ||
| 358 | const char* action; | ||
| 359 | const char* title; | ||
| 360 | const char* description; | ||
| 361 | } ghostty_command_s; | ||
| 362 | |||
| 363 | typedef enum { | ||
| 364 | GHOSTTY_BUILD_MODE_DEBUG, | ||
| 365 | GHOSTTY_BUILD_MODE_RELEASE_SAFE, | ||
| 366 | GHOSTTY_BUILD_MODE_RELEASE_FAST, | ||
| 367 | GHOSTTY_BUILD_MODE_RELEASE_SMALL, | ||
| 368 | } ghostty_build_mode_e; | ||
| 369 | |||
| 370 | typedef struct { | ||
| 371 | ghostty_build_mode_e build_mode; | ||
| 372 | const char* version; | ||
| 373 | uintptr_t version_len; | ||
| 374 | } ghostty_info_s; | ||
| 375 | |||
| 376 | typedef struct { | ||
| 377 | const char* message; | ||
| 378 | } ghostty_diagnostic_s; | ||
| 379 | |||
| 380 | typedef struct { | ||
| 381 | const char* ptr; | ||
| 382 | uintptr_t len; | ||
| 383 | bool sentinel; | ||
| 384 | } ghostty_string_s; | ||
| 385 | |||
| 386 | typedef struct { | ||
| 387 | double tl_px_x; | ||
| 388 | double tl_px_y; | ||
| 389 | uint32_t offset_start; | ||
| 390 | uint32_t offset_len; | ||
| 391 | const char* text; | ||
| 392 | uintptr_t text_len; | ||
| 393 | } ghostty_text_s; | ||
| 394 | |||
| 395 | typedef enum { | ||
| 396 | GHOSTTY_POINT_ACTIVE, | ||
| 397 | GHOSTTY_POINT_VIEWPORT, | ||
| 398 | GHOSTTY_POINT_SCREEN, | ||
| 399 | GHOSTTY_POINT_SURFACE, | ||
| 400 | } ghostty_point_tag_e; | ||
| 401 | |||
| 402 | typedef enum { | ||
| 403 | GHOSTTY_POINT_COORD_EXACT, | ||
| 404 | GHOSTTY_POINT_COORD_TOP_LEFT, | ||
| 405 | GHOSTTY_POINT_COORD_BOTTOM_RIGHT, | ||
| 406 | } ghostty_point_coord_e; | ||
| 407 | |||
| 408 | typedef struct { | ||
| 409 | ghostty_point_tag_e tag; | ||
| 410 | ghostty_point_coord_e coord; | ||
| 411 | uint32_t x; | ||
| 412 | uint32_t y; | ||
| 413 | } ghostty_point_s; | ||
| 414 | |||
| 415 | typedef struct { | ||
| 416 | ghostty_point_s top_left; | ||
| 417 | ghostty_point_s bottom_right; | ||
| 418 | bool rectangle; | ||
| 419 | } ghostty_selection_s; | ||
| 420 | |||
| 421 | typedef struct { | ||
| 422 | const char* key; | ||
| 423 | const char* value; | ||
| 424 | } ghostty_env_var_s; | ||
| 425 | |||
| 426 | typedef struct { | ||
| 427 | void* nsview; | ||
| 428 | } ghostty_platform_macos_s; | ||
| 429 | |||
| 430 | typedef struct { | ||
| 431 | void* uiview; | ||
| 432 | } ghostty_platform_ios_s; | ||
| 433 | |||
| 434 | typedef union { | ||
| 435 | ghostty_platform_macos_s macos; | ||
| 436 | ghostty_platform_ios_s ios; | ||
| 437 | } ghostty_platform_u; | ||
| 438 | |||
| 439 | typedef enum { | ||
| 440 | GHOSTTY_SURFACE_CONTEXT_WINDOW = 0, | ||
| 441 | GHOSTTY_SURFACE_CONTEXT_TAB = 1, | ||
| 442 | GHOSTTY_SURFACE_CONTEXT_SPLIT = 2, | ||
| 443 | } ghostty_surface_context_e; | ||
| 444 | |||
| 445 | typedef struct { | ||
| 446 | ghostty_platform_e platform_tag; | ||
| 447 | ghostty_platform_u platform; | ||
| 448 | void* userdata; | ||
| 449 | double scale_factor; | ||
| 450 | float font_size; | ||
| 451 | const char* working_directory; | ||
| 452 | const char* command; | ||
| 453 | ghostty_env_var_s* env_vars; | ||
| 454 | size_t env_var_count; | ||
| 455 | const char* initial_input; | ||
| 456 | bool wait_after_command; | ||
| 457 | ghostty_surface_context_e context; | ||
| 458 | bool use_custom_io; | ||
| 459 | } ghostty_surface_config_s; | ||
| 460 | |||
| 461 | typedef struct { | ||
| 462 | uint16_t columns; | ||
| 463 | uint16_t rows; | ||
| 464 | uint32_t width_px; | ||
| 465 | uint32_t height_px; | ||
| 466 | uint32_t cell_width_px; | ||
| 467 | uint32_t cell_height_px; | ||
| 468 | } ghostty_surface_size_s; | ||
| 469 | |||
| 470 | // Config types | ||
| 471 | |||
| 472 | // config.Path | ||
| 473 | typedef struct { | ||
| 474 | const char* path; | ||
| 475 | bool optional; | ||
| 476 | } ghostty_config_path_s; | ||
| 477 | |||
| 478 | // config.Color | ||
| 479 | typedef struct { | ||
| 480 | uint8_t r; | ||
| 481 | uint8_t g; | ||
| 482 | uint8_t b; | ||
| 483 | } ghostty_config_color_s; | ||
| 484 | |||
| 485 | // config.ColorList | ||
| 486 | typedef struct { | ||
| 487 | const ghostty_config_color_s* colors; | ||
| 488 | size_t len; | ||
| 489 | } ghostty_config_color_list_s; | ||
| 490 | |||
| 491 | // config.RepeatableCommand | ||
| 492 | typedef struct { | ||
| 493 | const ghostty_command_s* commands; | ||
| 494 | size_t len; | ||
| 495 | } ghostty_config_command_list_s; | ||
| 496 | |||
| 497 | // config.Palette | ||
| 498 | typedef struct { | ||
| 499 | ghostty_config_color_s colors[256]; | ||
| 500 | } ghostty_config_palette_s; | ||
| 501 | |||
| 502 | // config.QuickTerminalSize | ||
| 503 | typedef enum { | ||
| 504 | GHOSTTY_QUICK_TERMINAL_SIZE_NONE, | ||
| 505 | GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE, | ||
| 506 | GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS, | ||
| 507 | } ghostty_quick_terminal_size_tag_e; | ||
| 508 | |||
| 509 | typedef union { | ||
| 510 | float percentage; | ||
| 511 | uint32_t pixels; | ||
| 512 | } ghostty_quick_terminal_size_value_u; | ||
| 513 | |||
| 514 | typedef struct { | ||
| 515 | ghostty_quick_terminal_size_tag_e tag; | ||
| 516 | ghostty_quick_terminal_size_value_u value; | ||
| 517 | } ghostty_quick_terminal_size_s; | ||
| 518 | |||
| 519 | typedef struct { | ||
| 520 | ghostty_quick_terminal_size_s primary; | ||
| 521 | ghostty_quick_terminal_size_s secondary; | ||
| 522 | } ghostty_config_quick_terminal_size_s; | ||
| 523 | |||
| 524 | // config.Fullscreen | ||
| 525 | typedef enum { | ||
| 526 | GHOSTTY_CONFIG_FULLSCREEN_FALSE, | ||
| 527 | GHOSTTY_CONFIG_FULLSCREEN_TRUE, | ||
| 528 | GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE, | ||
| 529 | GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, | ||
| 530 | GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, | ||
| 531 | } ghostty_config_fullscreen_e; | ||
| 532 | |||
| 533 | // apprt.Target.Key | ||
| 534 | typedef enum { | ||
| 535 | GHOSTTY_TARGET_APP, | ||
| 536 | GHOSTTY_TARGET_SURFACE, | ||
| 537 | } ghostty_target_tag_e; | ||
| 538 | |||
| 539 | typedef union { | ||
| 540 | ghostty_surface_t surface; | ||
| 541 | } ghostty_target_u; | ||
| 542 | |||
| 543 | typedef struct { | ||
| 544 | ghostty_target_tag_e tag; | ||
| 545 | ghostty_target_u target; | ||
| 546 | } ghostty_target_s; | ||
| 547 | |||
| 548 | // apprt.action.SplitDirection | ||
| 549 | typedef enum { | ||
| 550 | GHOSTTY_SPLIT_DIRECTION_RIGHT, | ||
| 551 | GHOSTTY_SPLIT_DIRECTION_DOWN, | ||
| 552 | GHOSTTY_SPLIT_DIRECTION_LEFT, | ||
| 553 | GHOSTTY_SPLIT_DIRECTION_UP, | ||
| 554 | } ghostty_action_split_direction_e; | ||
| 555 | |||
| 556 | // apprt.action.GotoSplit | ||
| 557 | typedef enum { | ||
| 558 | GHOSTTY_GOTO_SPLIT_PREVIOUS, | ||
| 559 | GHOSTTY_GOTO_SPLIT_NEXT, | ||
| 560 | GHOSTTY_GOTO_SPLIT_UP, | ||
| 561 | GHOSTTY_GOTO_SPLIT_LEFT, | ||
| 562 | GHOSTTY_GOTO_SPLIT_DOWN, | ||
| 563 | GHOSTTY_GOTO_SPLIT_RIGHT, | ||
| 564 | } ghostty_action_goto_split_e; | ||
| 565 | |||
| 566 | // apprt.action.GotoWindow | ||
| 567 | typedef enum { | ||
| 568 | GHOSTTY_GOTO_WINDOW_PREVIOUS, | ||
| 569 | GHOSTTY_GOTO_WINDOW_NEXT, | ||
| 570 | } ghostty_action_goto_window_e; | ||
| 571 | |||
| 572 | // apprt.action.ResizeSplit.Direction | ||
| 573 | typedef enum { | ||
| 574 | GHOSTTY_RESIZE_SPLIT_UP, | ||
| 575 | GHOSTTY_RESIZE_SPLIT_DOWN, | ||
| 576 | GHOSTTY_RESIZE_SPLIT_LEFT, | ||
| 577 | GHOSTTY_RESIZE_SPLIT_RIGHT, | ||
| 578 | } ghostty_action_resize_split_direction_e; | ||
| 579 | |||
| 580 | // apprt.action.ResizeSplit | ||
| 581 | typedef struct { | ||
| 582 | uint16_t amount; | ||
| 583 | ghostty_action_resize_split_direction_e direction; | ||
| 584 | } ghostty_action_resize_split_s; | ||
| 585 | |||
| 586 | // apprt.action.MoveTab | ||
| 587 | typedef struct { | ||
| 588 | ssize_t amount; | ||
| 589 | } ghostty_action_move_tab_s; | ||
| 590 | |||
| 591 | // apprt.action.GotoTab | ||
| 592 | typedef enum { | ||
| 593 | GHOSTTY_GOTO_TAB_PREVIOUS = -1, | ||
| 594 | GHOSTTY_GOTO_TAB_NEXT = -2, | ||
| 595 | GHOSTTY_GOTO_TAB_LAST = -3, | ||
| 596 | } ghostty_action_goto_tab_e; | ||
| 597 | |||
| 598 | // apprt.action.Fullscreen | ||
| 599 | typedef enum { | ||
| 600 | GHOSTTY_FULLSCREEN_NATIVE, | ||
| 601 | GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE, | ||
| 602 | GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU, | ||
| 603 | GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH, | ||
| 604 | } ghostty_action_fullscreen_e; | ||
| 605 | |||
| 606 | // apprt.action.FloatWindow | ||
| 607 | typedef enum { | ||
| 608 | GHOSTTY_FLOAT_WINDOW_ON, | ||
| 609 | GHOSTTY_FLOAT_WINDOW_OFF, | ||
| 610 | GHOSTTY_FLOAT_WINDOW_TOGGLE, | ||
| 611 | } ghostty_action_float_window_e; | ||
| 612 | |||
| 613 | // apprt.action.SecureInput | ||
| 614 | typedef enum { | ||
| 615 | GHOSTTY_SECURE_INPUT_ON, | ||
| 616 | GHOSTTY_SECURE_INPUT_OFF, | ||
| 617 | GHOSTTY_SECURE_INPUT_TOGGLE, | ||
| 618 | } ghostty_action_secure_input_e; | ||
| 619 | |||
| 620 | // apprt.action.Inspector | ||
| 621 | typedef enum { | ||
| 622 | GHOSTTY_INSPECTOR_TOGGLE, | ||
| 623 | GHOSTTY_INSPECTOR_SHOW, | ||
| 624 | GHOSTTY_INSPECTOR_HIDE, | ||
| 625 | } ghostty_action_inspector_e; | ||
| 626 | |||
| 627 | // apprt.action.QuitTimer | ||
| 628 | typedef enum { | ||
| 629 | GHOSTTY_QUIT_TIMER_START, | ||
| 630 | GHOSTTY_QUIT_TIMER_STOP, | ||
| 631 | } ghostty_action_quit_timer_e; | ||
| 632 | |||
| 633 | // apprt.action.Readonly | ||
| 634 | typedef enum { | ||
| 635 | GHOSTTY_READONLY_OFF, | ||
| 636 | GHOSTTY_READONLY_ON, | ||
| 637 | } ghostty_action_readonly_e; | ||
| 638 | |||
| 639 | // apprt.action.DesktopNotification.C | ||
| 640 | typedef struct { | ||
| 641 | const char* title; | ||
| 642 | const char* body; | ||
| 643 | } ghostty_action_desktop_notification_s; | ||
| 644 | |||
| 645 | // apprt.action.SetTitle.C | ||
| 646 | typedef struct { | ||
| 647 | const char* title; | ||
| 648 | } ghostty_action_set_title_s; | ||
| 649 | |||
| 650 | // apprt.action.PromptTitle | ||
| 651 | typedef enum { | ||
| 652 | GHOSTTY_PROMPT_TITLE_SURFACE, | ||
| 653 | GHOSTTY_PROMPT_TITLE_TAB, | ||
| 654 | } ghostty_action_prompt_title_e; | ||
| 655 | |||
| 656 | // apprt.action.Pwd.C | ||
| 657 | typedef struct { | ||
| 658 | const char* pwd; | ||
| 659 | } ghostty_action_pwd_s; | ||
| 660 | |||
| 661 | // terminal.MouseShape | ||
| 662 | typedef enum { | ||
| 663 | GHOSTTY_MOUSE_SHAPE_DEFAULT, | ||
| 664 | GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU, | ||
| 665 | GHOSTTY_MOUSE_SHAPE_HELP, | ||
| 666 | GHOSTTY_MOUSE_SHAPE_POINTER, | ||
| 667 | GHOSTTY_MOUSE_SHAPE_PROGRESS, | ||
| 668 | GHOSTTY_MOUSE_SHAPE_WAIT, | ||
| 669 | GHOSTTY_MOUSE_SHAPE_CELL, | ||
| 670 | GHOSTTY_MOUSE_SHAPE_CROSSHAIR, | ||
| 671 | GHOSTTY_MOUSE_SHAPE_TEXT, | ||
| 672 | GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT, | ||
| 673 | GHOSTTY_MOUSE_SHAPE_ALIAS, | ||
| 674 | GHOSTTY_MOUSE_SHAPE_COPY, | ||
| 675 | GHOSTTY_MOUSE_SHAPE_MOVE, | ||
| 676 | GHOSTTY_MOUSE_SHAPE_NO_DROP, | ||
| 677 | GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED, | ||
| 678 | GHOSTTY_MOUSE_SHAPE_GRAB, | ||
| 679 | GHOSTTY_MOUSE_SHAPE_GRABBING, | ||
| 680 | GHOSTTY_MOUSE_SHAPE_ALL_SCROLL, | ||
| 681 | GHOSTTY_MOUSE_SHAPE_COL_RESIZE, | ||
| 682 | GHOSTTY_MOUSE_SHAPE_ROW_RESIZE, | ||
| 683 | GHOSTTY_MOUSE_SHAPE_N_RESIZE, | ||
| 684 | GHOSTTY_MOUSE_SHAPE_E_RESIZE, | ||
| 685 | GHOSTTY_MOUSE_SHAPE_S_RESIZE, | ||
| 686 | GHOSTTY_MOUSE_SHAPE_W_RESIZE, | ||
| 687 | GHOSTTY_MOUSE_SHAPE_NE_RESIZE, | ||
| 688 | GHOSTTY_MOUSE_SHAPE_NW_RESIZE, | ||
| 689 | GHOSTTY_MOUSE_SHAPE_SE_RESIZE, | ||
| 690 | GHOSTTY_MOUSE_SHAPE_SW_RESIZE, | ||
| 691 | GHOSTTY_MOUSE_SHAPE_EW_RESIZE, | ||
| 692 | GHOSTTY_MOUSE_SHAPE_NS_RESIZE, | ||
| 693 | GHOSTTY_MOUSE_SHAPE_NESW_RESIZE, | ||
| 694 | GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE, | ||
| 695 | GHOSTTY_MOUSE_SHAPE_ZOOM_IN, | ||
| 696 | GHOSTTY_MOUSE_SHAPE_ZOOM_OUT, | ||
| 697 | } ghostty_action_mouse_shape_e; | ||
| 698 | |||
| 699 | // apprt.action.MouseVisibility | ||
| 700 | typedef enum { | ||
| 701 | GHOSTTY_MOUSE_VISIBLE, | ||
| 702 | GHOSTTY_MOUSE_HIDDEN, | ||
| 703 | } ghostty_action_mouse_visibility_e; | ||
| 704 | |||
| 705 | // apprt.action.MouseOverLink | ||
| 706 | typedef struct { | ||
| 707 | const char* url; | ||
| 708 | size_t len; | ||
| 709 | } ghostty_action_mouse_over_link_s; | ||
| 710 | |||
| 711 | // apprt.action.SizeLimit | ||
| 712 | typedef struct { | ||
| 713 | uint32_t min_width; | ||
| 714 | uint32_t min_height; | ||
| 715 | uint32_t max_width; | ||
| 716 | uint32_t max_height; | ||
| 717 | } ghostty_action_size_limit_s; | ||
| 718 | |||
| 719 | // apprt.action.InitialSize | ||
| 720 | typedef struct { | ||
| 721 | uint32_t width; | ||
| 722 | uint32_t height; | ||
| 723 | } ghostty_action_initial_size_s; | ||
| 724 | |||
| 725 | // apprt.action.CellSize | ||
| 726 | typedef struct { | ||
| 727 | uint32_t width; | ||
| 728 | uint32_t height; | ||
| 729 | } ghostty_action_cell_size_s; | ||
| 730 | |||
| 731 | // renderer.Health | ||
| 732 | typedef enum { | ||
| 733 | GHOSTTY_RENDERER_HEALTH_HEALTHY, | ||
| 734 | GHOSTTY_RENDERER_HEALTH_UNHEALTHY, | ||
| 735 | } ghostty_action_renderer_health_e; | ||
| 736 | |||
| 737 | // apprt.action.KeySequence | ||
| 738 | typedef struct { | ||
| 739 | bool active; | ||
| 740 | ghostty_input_trigger_s trigger; | ||
| 741 | } ghostty_action_key_sequence_s; | ||
| 742 | |||
| 743 | // apprt.action.KeyTable.Tag | ||
| 744 | typedef enum { | ||
| 745 | GHOSTTY_KEY_TABLE_ACTIVATE, | ||
| 746 | GHOSTTY_KEY_TABLE_DEACTIVATE, | ||
| 747 | GHOSTTY_KEY_TABLE_DEACTIVATE_ALL, | ||
| 748 | } ghostty_action_key_table_tag_e; | ||
| 749 | |||
| 750 | // apprt.action.KeyTable.CValue | ||
| 751 | typedef union { | ||
| 752 | struct { | ||
| 753 | const char *name; | ||
| 754 | size_t len; | ||
| 755 | } activate; | ||
| 756 | } ghostty_action_key_table_u; | ||
| 757 | |||
| 758 | // apprt.action.KeyTable.C | ||
| 759 | typedef struct { | ||
| 760 | ghostty_action_key_table_tag_e tag; | ||
| 761 | ghostty_action_key_table_u value; | ||
| 762 | } ghostty_action_key_table_s; | ||
| 763 | |||
| 764 | // apprt.action.ColorKind | ||
| 765 | typedef enum { | ||
| 766 | GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1, | ||
| 767 | GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2, | ||
| 768 | GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3, | ||
| 769 | } ghostty_action_color_kind_e; | ||
| 770 | |||
| 771 | // apprt.action.ColorChange | ||
| 772 | typedef struct { | ||
| 773 | ghostty_action_color_kind_e kind; | ||
| 774 | uint8_t r; | ||
| 775 | uint8_t g; | ||
| 776 | uint8_t b; | ||
| 777 | } ghostty_action_color_change_s; | ||
| 778 | |||
| 779 | // apprt.action.ConfigChange | ||
| 780 | typedef struct { | ||
| 781 | ghostty_config_t config; | ||
| 782 | } ghostty_action_config_change_s; | ||
| 783 | |||
| 784 | // apprt.action.ReloadConfig | ||
| 785 | typedef struct { | ||
| 786 | bool soft; | ||
| 787 | } ghostty_action_reload_config_s; | ||
| 788 | |||
| 789 | // apprt.action.OpenUrlKind | ||
| 790 | typedef enum { | ||
| 791 | GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, | ||
| 792 | GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, | ||
| 793 | GHOSTTY_ACTION_OPEN_URL_KIND_HTML, | ||
| 794 | } ghostty_action_open_url_kind_e; | ||
| 795 | |||
| 796 | // apprt.action.OpenUrl.C | ||
| 797 | typedef struct { | ||
| 798 | ghostty_action_open_url_kind_e kind; | ||
| 799 | const char* url; | ||
| 800 | uintptr_t len; | ||
| 801 | } ghostty_action_open_url_s; | ||
| 802 | |||
| 803 | // apprt.action.CloseTabMode | ||
| 804 | typedef enum { | ||
| 805 | GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, | ||
| 806 | GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, | ||
| 807 | GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT, | ||
| 808 | } ghostty_action_close_tab_mode_e; | ||
| 809 | |||
| 810 | // apprt.surface.Message.ChildExited | ||
| 811 | typedef struct { | ||
| 812 | uint32_t exit_code; | ||
| 813 | uint64_t timetime_ms; | ||
| 814 | } ghostty_surface_message_childexited_s; | ||
| 815 | |||
| 816 | // terminal.osc.Command.ProgressReport.State | ||
| 817 | typedef enum { | ||
| 818 | GHOSTTY_PROGRESS_STATE_REMOVE, | ||
| 819 | GHOSTTY_PROGRESS_STATE_SET, | ||
| 820 | GHOSTTY_PROGRESS_STATE_ERROR, | ||
| 821 | GHOSTTY_PROGRESS_STATE_INDETERMINATE, | ||
| 822 | GHOSTTY_PROGRESS_STATE_PAUSE, | ||
| 823 | } ghostty_action_progress_report_state_e; | ||
| 824 | |||
| 825 | // terminal.osc.Command.ProgressReport.C | ||
| 826 | typedef struct { | ||
| 827 | ghostty_action_progress_report_state_e state; | ||
| 828 | // -1 if no progress was reported, otherwise 0-100 indicating percent | ||
| 829 | // completeness. | ||
| 830 | int8_t progress; | ||
| 831 | } ghostty_action_progress_report_s; | ||
| 832 | |||
| 833 | // apprt.action.CommandFinished.C | ||
| 834 | typedef struct { | ||
| 835 | // -1 if no exit code was reported, otherwise 0-255 | ||
| 836 | int16_t exit_code; | ||
| 837 | // number of nanoseconds that command was running for | ||
| 838 | uint64_t duration; | ||
| 839 | } ghostty_action_command_finished_s; | ||
| 840 | |||
| 841 | // apprt.action.StartSearch.C | ||
| 842 | typedef struct { | ||
| 843 | const char* needle; | ||
| 844 | } ghostty_action_start_search_s; | ||
| 845 | |||
| 846 | // apprt.action.SearchTotal | ||
| 847 | typedef struct { | ||
| 848 | ssize_t total; | ||
| 849 | } ghostty_action_search_total_s; | ||
| 850 | |||
| 851 | // apprt.action.SearchSelected | ||
| 852 | typedef struct { | ||
| 853 | ssize_t selected; | ||
| 854 | } ghostty_action_search_selected_s; | ||
| 855 | |||
| 856 | // terminal.Scrollbar | ||
| 857 | typedef struct { | ||
| 858 | uint64_t total; | ||
| 859 | uint64_t offset; | ||
| 860 | uint64_t len; | ||
| 861 | } ghostty_action_scrollbar_s; | ||
| 862 | |||
| 863 | // apprt.Action.Key | ||
| 864 | typedef enum { | ||
| 865 | GHOSTTY_ACTION_QUIT, | ||
| 866 | GHOSTTY_ACTION_NEW_WINDOW, | ||
| 867 | GHOSTTY_ACTION_NEW_TAB, | ||
| 868 | GHOSTTY_ACTION_CLOSE_TAB, | ||
| 869 | GHOSTTY_ACTION_NEW_SPLIT, | ||
| 870 | GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, | ||
| 871 | GHOSTTY_ACTION_TOGGLE_MAXIMIZE, | ||
| 872 | GHOSTTY_ACTION_TOGGLE_FULLSCREEN, | ||
| 873 | GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW, | ||
| 874 | GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, | ||
| 875 | GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, | ||
| 876 | GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE, | ||
| 877 | GHOSTTY_ACTION_TOGGLE_VISIBILITY, | ||
| 878 | GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY, | ||
| 879 | GHOSTTY_ACTION_MOVE_TAB, | ||
| 880 | GHOSTTY_ACTION_GOTO_TAB, | ||
| 881 | GHOSTTY_ACTION_GOTO_SPLIT, | ||
| 882 | GHOSTTY_ACTION_GOTO_WINDOW, | ||
| 883 | GHOSTTY_ACTION_RESIZE_SPLIT, | ||
| 884 | GHOSTTY_ACTION_EQUALIZE_SPLITS, | ||
| 885 | GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM, | ||
| 886 | GHOSTTY_ACTION_PRESENT_TERMINAL, | ||
| 887 | GHOSTTY_ACTION_SIZE_LIMIT, | ||
| 888 | GHOSTTY_ACTION_RESET_WINDOW_SIZE, | ||
| 889 | GHOSTTY_ACTION_INITIAL_SIZE, | ||
| 890 | GHOSTTY_ACTION_CELL_SIZE, | ||
| 891 | GHOSTTY_ACTION_SCROLLBAR, | ||
| 892 | GHOSTTY_ACTION_RENDER, | ||
| 893 | GHOSTTY_ACTION_INSPECTOR, | ||
| 894 | GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, | ||
| 895 | GHOSTTY_ACTION_RENDER_INSPECTOR, | ||
| 896 | GHOSTTY_ACTION_DESKTOP_NOTIFICATION, | ||
| 897 | GHOSTTY_ACTION_SET_TITLE, | ||
| 898 | GHOSTTY_ACTION_PROMPT_TITLE, | ||
| 899 | GHOSTTY_ACTION_PWD, | ||
| 900 | GHOSTTY_ACTION_MOUSE_SHAPE, | ||
| 901 | GHOSTTY_ACTION_MOUSE_VISIBILITY, | ||
| 902 | GHOSTTY_ACTION_MOUSE_OVER_LINK, | ||
| 903 | GHOSTTY_ACTION_RENDERER_HEALTH, | ||
| 904 | GHOSTTY_ACTION_OPEN_CONFIG, | ||
| 905 | GHOSTTY_ACTION_QUIT_TIMER, | ||
| 906 | GHOSTTY_ACTION_FLOAT_WINDOW, | ||
| 907 | GHOSTTY_ACTION_SECURE_INPUT, | ||
| 908 | GHOSTTY_ACTION_KEY_SEQUENCE, | ||
| 909 | GHOSTTY_ACTION_KEY_TABLE, | ||
| 910 | GHOSTTY_ACTION_COLOR_CHANGE, | ||
| 911 | GHOSTTY_ACTION_RELOAD_CONFIG, | ||
| 912 | GHOSTTY_ACTION_CONFIG_CHANGE, | ||
| 913 | GHOSTTY_ACTION_CLOSE_WINDOW, | ||
| 914 | GHOSTTY_ACTION_RING_BELL, | ||
| 915 | GHOSTTY_ACTION_UNDO, | ||
| 916 | GHOSTTY_ACTION_REDO, | ||
| 917 | GHOSTTY_ACTION_CHECK_FOR_UPDATES, | ||
| 918 | GHOSTTY_ACTION_OPEN_URL, | ||
| 919 | GHOSTTY_ACTION_SHOW_CHILD_EXITED, | ||
| 920 | GHOSTTY_ACTION_PROGRESS_REPORT, | ||
| 921 | GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, | ||
| 922 | GHOSTTY_ACTION_COMMAND_FINISHED, | ||
| 923 | GHOSTTY_ACTION_START_SEARCH, | ||
| 924 | GHOSTTY_ACTION_END_SEARCH, | ||
| 925 | GHOSTTY_ACTION_SEARCH_TOTAL, | ||
| 926 | GHOSTTY_ACTION_SEARCH_SELECTED, | ||
| 927 | GHOSTTY_ACTION_READONLY, | ||
| 928 | GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD, | ||
| 929 | } ghostty_action_tag_e; | ||
| 930 | |||
| 931 | typedef union { | ||
| 932 | ghostty_action_split_direction_e new_split; | ||
| 933 | ghostty_action_fullscreen_e toggle_fullscreen; | ||
| 934 | ghostty_action_move_tab_s move_tab; | ||
| 935 | ghostty_action_goto_tab_e goto_tab; | ||
| 936 | ghostty_action_goto_split_e goto_split; | ||
| 937 | ghostty_action_goto_window_e goto_window; | ||
| 938 | ghostty_action_resize_split_s resize_split; | ||
| 939 | ghostty_action_size_limit_s size_limit; | ||
| 940 | ghostty_action_initial_size_s initial_size; | ||
| 941 | ghostty_action_cell_size_s cell_size; | ||
| 942 | ghostty_action_scrollbar_s scrollbar; | ||
| 943 | ghostty_action_inspector_e inspector; | ||
| 944 | ghostty_action_desktop_notification_s desktop_notification; | ||
| 945 | ghostty_action_set_title_s set_title; | ||
| 946 | ghostty_action_prompt_title_e prompt_title; | ||
| 947 | ghostty_action_pwd_s pwd; | ||
| 948 | ghostty_action_mouse_shape_e mouse_shape; | ||
| 949 | ghostty_action_mouse_visibility_e mouse_visibility; | ||
| 950 | ghostty_action_mouse_over_link_s mouse_over_link; | ||
| 951 | ghostty_action_renderer_health_e renderer_health; | ||
| 952 | ghostty_action_quit_timer_e quit_timer; | ||
| 953 | ghostty_action_float_window_e float_window; | ||
| 954 | ghostty_action_secure_input_e secure_input; | ||
| 955 | ghostty_action_key_sequence_s key_sequence; | ||
| 956 | ghostty_action_key_table_s key_table; | ||
| 957 | ghostty_action_color_change_s color_change; | ||
| 958 | ghostty_action_reload_config_s reload_config; | ||
| 959 | ghostty_action_config_change_s config_change; | ||
| 960 | ghostty_action_open_url_s open_url; | ||
| 961 | ghostty_action_close_tab_mode_e close_tab_mode; | ||
| 962 | ghostty_surface_message_childexited_s child_exited; | ||
| 963 | ghostty_action_progress_report_s progress_report; | ||
| 964 | ghostty_action_command_finished_s command_finished; | ||
| 965 | ghostty_action_start_search_s start_search; | ||
| 966 | ghostty_action_search_total_s search_total; | ||
| 967 | ghostty_action_search_selected_s search_selected; | ||
| 968 | ghostty_action_readonly_e readonly; | ||
| 969 | } ghostty_action_u; | ||
| 970 | |||
| 971 | typedef struct { | ||
| 972 | ghostty_action_tag_e tag; | ||
| 973 | ghostty_action_u action; | ||
| 974 | } ghostty_action_s; | ||
| 975 | |||
| 976 | typedef void (*ghostty_runtime_wakeup_cb)(void*); | ||
| 977 | typedef void (*ghostty_runtime_read_clipboard_cb)(void*, | ||
| 978 | ghostty_clipboard_e, | ||
| 979 | void*); | ||
| 980 | typedef void (*ghostty_runtime_confirm_read_clipboard_cb)( | ||
| 981 | void*, | ||
| 982 | const char*, | ||
| 983 | void*, | ||
| 984 | ghostty_clipboard_request_e); | ||
| 985 | typedef void (*ghostty_runtime_write_clipboard_cb)(void*, | ||
| 986 | ghostty_clipboard_e, | ||
| 987 | const ghostty_clipboard_content_s*, | ||
| 988 | size_t, | ||
| 989 | bool); | ||
| 990 | typedef void (*ghostty_runtime_close_surface_cb)(void*, bool); | ||
| 991 | typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t, | ||
| 992 | ghostty_target_s, | ||
| 993 | ghostty_action_s); | ||
| 994 | |||
| 995 | typedef struct { | ||
| 996 | void* userdata; | ||
| 997 | bool supports_selection_clipboard; | ||
| 998 | ghostty_runtime_wakeup_cb wakeup_cb; | ||
| 999 | ghostty_runtime_action_cb action_cb; | ||
| 1000 | ghostty_runtime_read_clipboard_cb read_clipboard_cb; | ||
| 1001 | ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; | ||
| 1002 | ghostty_runtime_write_clipboard_cb write_clipboard_cb; | ||
| 1003 | ghostty_runtime_close_surface_cb close_surface_cb; | ||
| 1004 | } ghostty_runtime_config_s; | ||
| 1005 | |||
| 1006 | // apprt.ipc.Target.Key | ||
| 1007 | typedef enum { | ||
| 1008 | GHOSTTY_IPC_TARGET_CLASS, | ||
| 1009 | GHOSTTY_IPC_TARGET_DETECT, | ||
| 1010 | } ghostty_ipc_target_tag_e; | ||
| 1011 | |||
| 1012 | typedef union { | ||
| 1013 | char *klass; | ||
| 1014 | } ghostty_ipc_target_u; | ||
| 1015 | |||
| 1016 | typedef struct { | ||
| 1017 | ghostty_ipc_target_tag_e tag; | ||
| 1018 | ghostty_ipc_target_u target; | ||
| 1019 | } chostty_ipc_target_s; | ||
| 1020 | |||
| 1021 | // apprt.ipc.Action.NewWindow | ||
| 1022 | typedef struct { | ||
| 1023 | // This should be a null terminated list of strings. | ||
| 1024 | const char **arguments; | ||
| 1025 | } ghostty_ipc_action_new_window_s; | ||
| 1026 | |||
| 1027 | typedef union { | ||
| 1028 | ghostty_ipc_action_new_window_s new_window; | ||
| 1029 | } ghostty_ipc_action_u; | ||
| 1030 | |||
| 1031 | // apprt.ipc.Action.Key | ||
| 1032 | typedef enum { | ||
| 1033 | GHOSTTY_IPC_ACTION_NEW_WINDOW, | ||
| 1034 | } ghostty_ipc_action_tag_e; | ||
| 1035 | |||
| 1036 | //------------------------------------------------------------------- | ||
| 1037 | // Published API | ||
| 1038 | |||
| 1039 | int ghostty_init(uintptr_t, char**); | ||
| 1040 | void ghostty_cli_try_action(void); | ||
| 1041 | ghostty_info_s ghostty_info(void); | ||
| 1042 | const char* ghostty_translate(const char*); | ||
| 1043 | void ghostty_string_free(ghostty_string_s); | ||
| 1044 | |||
| 1045 | ghostty_config_t ghostty_config_new(); | ||
| 1046 | void ghostty_config_free(ghostty_config_t); | ||
| 1047 | ghostty_config_t ghostty_config_clone(ghostty_config_t); | ||
| 1048 | void ghostty_config_load_cli_args(ghostty_config_t); | ||
| 1049 | void ghostty_config_load_file(ghostty_config_t, const char*); | ||
| 1050 | void ghostty_config_load_default_files(ghostty_config_t); | ||
| 1051 | void ghostty_config_load_recursive_files(ghostty_config_t); | ||
| 1052 | void ghostty_config_finalize(ghostty_config_t); | ||
| 1053 | bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t); | ||
| 1054 | ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, | ||
| 1055 | const char*, | ||
| 1056 | uintptr_t); | ||
| 1057 | uint32_t ghostty_config_diagnostics_count(ghostty_config_t); | ||
| 1058 | ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); | ||
| 1059 | ghostty_string_s ghostty_config_open_path(void); | ||
| 1060 | |||
| 1061 | ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, | ||
| 1062 | ghostty_config_t); | ||
| 1063 | void ghostty_app_free(ghostty_app_t); | ||
| 1064 | void ghostty_app_tick(ghostty_app_t); | ||
| 1065 | void* ghostty_app_userdata(ghostty_app_t); | ||
| 1066 | void ghostty_app_set_focus(ghostty_app_t, bool); | ||
| 1067 | bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); | ||
| 1068 | bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s); | ||
| 1069 | void ghostty_app_keyboard_changed(ghostty_app_t); | ||
| 1070 | void ghostty_app_open_config(ghostty_app_t); | ||
| 1071 | void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); | ||
| 1072 | bool ghostty_app_needs_confirm_quit(ghostty_app_t); | ||
| 1073 | bool ghostty_app_has_global_keybinds(ghostty_app_t); | ||
| 1074 | void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); | ||
| 1075 | |||
| 1076 | ghostty_surface_config_s ghostty_surface_config_new(); | ||
| 1077 | |||
| 1078 | ghostty_surface_t ghostty_surface_new(ghostty_app_t, | ||
| 1079 | const ghostty_surface_config_s*); | ||
| 1080 | void ghostty_surface_free(ghostty_surface_t); | ||
| 1081 | void* ghostty_surface_userdata(ghostty_surface_t); | ||
| 1082 | ghostty_app_t ghostty_surface_app(ghostty_surface_t); | ||
| 1083 | ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); | ||
| 1084 | void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); | ||
| 1085 | bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); | ||
| 1086 | bool ghostty_surface_process_exited(ghostty_surface_t); | ||
| 1087 | void ghostty_surface_refresh(ghostty_surface_t); | ||
| 1088 | void ghostty_surface_draw(ghostty_surface_t); | ||
| 1089 | // Custom I/O API - feed output into the terminal and receive writes. | ||
| 1090 | void ghostty_surface_feed_data(ghostty_surface_t, const uint8_t*, size_t); | ||
| 1091 | void ghostty_surface_set_write_callback(ghostty_surface_t, | ||
| 1092 | ghostty_surface_write_fn, | ||
| 1093 | void*); | ||
| 1094 | void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); | ||
| 1095 | void ghostty_surface_set_focus(ghostty_surface_t, bool); | ||
| 1096 | void ghostty_surface_set_occlusion(ghostty_surface_t, bool); | ||
| 1097 | void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); | ||
| 1098 | ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); | ||
| 1099 | void ghostty_surface_set_color_scheme(ghostty_surface_t, | ||
| 1100 | ghostty_color_scheme_e); | ||
| 1101 | ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, | ||
| 1102 | ghostty_input_mods_e); | ||
| 1103 | bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); | ||
| 1104 | bool ghostty_surface_key_is_binding(ghostty_surface_t, | ||
| 1105 | ghostty_input_key_s, | ||
| 1106 | ghostty_binding_flags_e*); | ||
| 1107 | void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); | ||
| 1108 | void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); | ||
| 1109 | bool ghostty_surface_mouse_captured(ghostty_surface_t); | ||
| 1110 | bool ghostty_surface_mouse_button(ghostty_surface_t, | ||
| 1111 | ghostty_input_mouse_state_e, | ||
| 1112 | ghostty_input_mouse_button_e, | ||
| 1113 | ghostty_input_mods_e); | ||
| 1114 | void ghostty_surface_mouse_pos(ghostty_surface_t, | ||
| 1115 | double, | ||
| 1116 | double, | ||
| 1117 | ghostty_input_mods_e); | ||
| 1118 | void ghostty_surface_mouse_scroll(ghostty_surface_t, | ||
| 1119 | double, | ||
| 1120 | double, | ||
| 1121 | ghostty_input_scroll_mods_t); | ||
| 1122 | void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); | ||
| 1123 | void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*); | ||
| 1124 | void ghostty_surface_request_close(ghostty_surface_t); | ||
| 1125 | void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); | ||
| 1126 | void ghostty_surface_split_focus(ghostty_surface_t, | ||
| 1127 | ghostty_action_goto_split_e); | ||
| 1128 | void ghostty_surface_split_resize(ghostty_surface_t, | ||
| 1129 | ghostty_action_resize_split_direction_e, | ||
| 1130 | uint16_t); | ||
| 1131 | void ghostty_surface_split_equalize(ghostty_surface_t); | ||
| 1132 | bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); | ||
| 1133 | void ghostty_surface_complete_clipboard_request(ghostty_surface_t, | ||
| 1134 | const char*, | ||
| 1135 | void*, | ||
| 1136 | bool); | ||
| 1137 | bool ghostty_surface_has_selection(ghostty_surface_t); | ||
| 1138 | bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); | ||
| 1139 | bool ghostty_surface_read_text(ghostty_surface_t, | ||
| 1140 | ghostty_selection_s, | ||
| 1141 | ghostty_text_s*); | ||
| 1142 | void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); | ||
| 1143 | |||
| 1144 | #ifdef __APPLE__ | ||
| 1145 | void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); | ||
| 1146 | void* ghostty_surface_quicklook_font(ghostty_surface_t); | ||
| 1147 | bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); | ||
| 1148 | #endif | ||
| 1149 | |||
| 1150 | ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); | ||
| 1151 | void ghostty_inspector_free(ghostty_surface_t); | ||
| 1152 | void ghostty_inspector_set_focus(ghostty_inspector_t, bool); | ||
| 1153 | void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double); | ||
| 1154 | void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t); | ||
| 1155 | void ghostty_inspector_mouse_button(ghostty_inspector_t, | ||
| 1156 | ghostty_input_mouse_state_e, | ||
| 1157 | ghostty_input_mouse_button_e, | ||
| 1158 | ghostty_input_mods_e); | ||
| 1159 | void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double); | ||
| 1160 | void ghostty_inspector_mouse_scroll(ghostty_inspector_t, | ||
| 1161 | double, | ||
| 1162 | double, | ||
| 1163 | ghostty_input_scroll_mods_t); | ||
| 1164 | void ghostty_inspector_key(ghostty_inspector_t, | ||
| 1165 | ghostty_input_action_e, | ||
| 1166 | ghostty_input_key_e, | ||
| 1167 | ghostty_input_mods_e); | ||
| 1168 | void ghostty_inspector_text(ghostty_inspector_t, const char*); | ||
| 1169 | |||
| 1170 | #ifdef __APPLE__ | ||
| 1171 | bool ghostty_inspector_metal_init(ghostty_inspector_t, void*); | ||
| 1172 | void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*); | ||
| 1173 | bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); | ||
| 1174 | #endif | ||
| 1175 | |||
| 1176 | // APIs I'd like to get rid of eventually but are still needed for now. | ||
| 1177 | // Don't use these unless you know what you're doing. | ||
| 1178 | void ghostty_set_window_background_blur(ghostty_app_t, void*); | ||
| 1179 | |||
| 1180 | // Benchmark API, if available. | ||
| 1181 | bool ghostty_benchmark_cli(const char*, const char*); | ||
| 1182 | |||
| 1183 | #ifdef __cplusplus | ||
| 1184 | } | ||
| 1185 | #endif | ||
| 1186 | |||
| 1187 | #endif /* GHOSTTY_H */ | ||
| @@ -0,0 +1,87 @@ | |||
| 1 | /** | ||
| 2 | * @file vt.h | ||
| 3 | * | ||
| 4 | * libghostty-vt - Virtual terminal emulator library | ||
| 5 | * | ||
| 6 | * This library provides functionality for parsing and handling terminal | ||
| 7 | * escape sequences as well as maintaining terminal state such as styles, | ||
| 8 | * cursor position, screen, scrollback, and more. | ||
| 9 | * | ||
| 10 | * WARNING: This is an incomplete, work-in-progress API. It is not yet | ||
| 11 | * stable and is definitely going to change. | ||
| 12 | */ | ||
| 13 | |||
| 14 | /** | ||
| 15 | * @mainpage libghostty-vt - Virtual Terminal Emulator Library | ||
| 16 | * | ||
| 17 | * libghostty-vt is a C library which implements a modern terminal emulator, | ||
| 18 | * extracted from the [Ghostty](https://ghostty.org) terminal emulator. | ||
| 19 | * | ||
| 20 | * libghostty-vt contains the logic for handling the core parts of a terminal | ||
| 21 | * emulator: parsing terminal escape sequences, maintaining terminal state, | ||
| 22 | * encoding input events, etc. It can handle scrollback, line wrapping, | ||
| 23 | * reflow on resize, and more. | ||
| 24 | * | ||
| 25 | * @warning This library is currently in development and the API is not yet stable. | ||
| 26 | * Breaking changes are expected in future versions. Use with caution in production code. | ||
| 27 | * | ||
| 28 | * @section groups_sec API Reference | ||
| 29 | * | ||
| 30 | * The API is organized into the following groups: | ||
| 31 | * - @ref key "Key Encoding" - Encode key events into terminal sequences | ||
| 32 | * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences | ||
| 33 | * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences | ||
| 34 | * - @ref paste "Paste Utilities" - Validate paste data safety | ||
| 35 | * - @ref allocator "Memory Management" - Memory management and custom allocators | ||
| 36 | * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions | ||
| 37 | * | ||
| 38 | * @section examples_sec Examples | ||
| 39 | * | ||
| 40 | * Complete working examples: | ||
| 41 | * - @ref c-vt/src/main.c - OSC parser example | ||
| 42 | * - @ref c-vt-key-encode/src/main.c - Key encoding example | ||
| 43 | * - @ref c-vt-paste/src/main.c - Paste safety check example | ||
| 44 | * - @ref c-vt-sgr/src/main.c - SGR parser example | ||
| 45 | * | ||
| 46 | */ | ||
| 47 | |||
| 48 | /** @example c-vt/src/main.c | ||
| 49 | * This example demonstrates how to use the OSC parser to parse an OSC sequence, | ||
| 50 | * extract command information, and retrieve command-specific data like window titles. | ||
| 51 | */ | ||
| 52 | |||
| 53 | /** @example c-vt-key-encode/src/main.c | ||
| 54 | * This example demonstrates how to use the key encoder to convert key events | ||
| 55 | * into terminal escape sequences using the Kitty keyboard protocol. | ||
| 56 | */ | ||
| 57 | |||
| 58 | /** @example c-vt-paste/src/main.c | ||
| 59 | * This example demonstrates how to use the paste utilities to check if | ||
| 60 | * paste data is safe before sending it to the terminal. | ||
| 61 | */ | ||
| 62 | |||
| 63 | /** @example c-vt-sgr/src/main.c | ||
| 64 | * This example demonstrates how to use the SGR parser to parse terminal | ||
| 65 | * styling sequences and extract text attributes like colors and underline styles. | ||
| 66 | */ | ||
| 67 | |||
| 68 | #ifndef GHOSTTY_VT_H | ||
| 69 | #define GHOSTTY_VT_H | ||
| 70 | |||
| 71 | #ifdef __cplusplus | ||
| 72 | extern "C" { | ||
| 73 | #endif | ||
| 74 | |||
| 75 | #include <ghostty/vt/result.h> | ||
| 76 | #include <ghostty/vt/allocator.h> | ||
| 77 | #include <ghostty/vt/osc.h> | ||
| 78 | #include <ghostty/vt/sgr.h> | ||
| 79 | #include <ghostty/vt/key.h> | ||
| 80 | #include <ghostty/vt/paste.h> | ||
| 81 | #include <ghostty/vt/wasm.h> | ||
| 82 | |||
| 83 | #ifdef __cplusplus | ||
| 84 | } | ||
| 85 | #endif | ||
| 86 | |||
| 87 | #endif /* GHOSTTY_VT_H */ | ||
| @@ -0,0 +1,196 @@ | |||
| 1 | /** | ||
| 2 | * @file allocator.h | ||
| 3 | * | ||
| 4 | * Memory management interface for libghostty-vt. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_ALLOCATOR_H | ||
| 8 | #define GHOSTTY_VT_ALLOCATOR_H | ||
| 9 | |||
| 10 | #include <stdbool.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <stdint.h> | ||
| 13 | |||
| 14 | /** @defgroup allocator Memory Management | ||
| 15 | * | ||
| 16 | * libghostty-vt does require memory allocation for various operations, | ||
| 17 | * but is resilient to allocation failures and will gracefully handle | ||
| 18 | * out-of-memory situations by returning error codes. | ||
| 19 | * | ||
| 20 | * The exact memory management semantics are documented in the relevant | ||
| 21 | * functions and data structures. | ||
| 22 | * | ||
| 23 | * libghostty-vt uses explicit memory allocation via an allocator | ||
| 24 | * interface provided by GhosttyAllocator. The interface is based on the | ||
| 25 | * [Zig](https://ziglang.org) allocator interface, since this has been | ||
| 26 | * shown to be a flexible and powerful interface in practice and enables | ||
| 27 | * a wide variety of allocation strategies. | ||
| 28 | * | ||
| 29 | * **For the common case, you can pass NULL as the allocator for any | ||
| 30 | * function that accepts one,** and libghostty will use a default allocator. | ||
| 31 | * The default allocator will be libc malloc/free if libc is linked. | ||
| 32 | * Otherwise, a custom allocator is used (currently Zig's SMP allocator) | ||
| 33 | * that doesn't require any external dependencies. | ||
| 34 | * | ||
| 35 | * ## Basic Usage | ||
| 36 | * | ||
| 37 | * For simple use cases, you can ignore this interface entirely by passing NULL | ||
| 38 | * as the allocator parameter to functions that accept one. This will use the | ||
| 39 | * default allocator (typically libc malloc/free, if libc is linked, but | ||
| 40 | * we provide our own default allocator if libc isn't linked). | ||
| 41 | * | ||
| 42 | * To use a custom allocator: | ||
| 43 | * 1. Implement the GhosttyAllocatorVtable function pointers | ||
| 44 | * 2. Create a GhosttyAllocator struct with your vtable and context | ||
| 45 | * 3. Pass the allocator to functions that accept one | ||
| 46 | * | ||
| 47 | * @{ | ||
| 48 | */ | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Function table for custom memory allocator operations. | ||
| 52 | * | ||
| 53 | * This vtable defines the interface for a custom memory allocator. All | ||
| 54 | * function pointers must be valid and non-NULL. | ||
| 55 | * | ||
| 56 | * @ingroup allocator | ||
| 57 | * | ||
| 58 | * If you're not going to use a custom allocator, you can ignore all of | ||
| 59 | * this. All functions that take an allocator pointer allow NULL to use a | ||
| 60 | * default allocator. | ||
| 61 | * | ||
| 62 | * The interface is based on the Zig allocator interface. I'll say up front | ||
| 63 | * that it is easy to look at this interface and think "wow, this is really | ||
| 64 | * overcomplicated". The reason for this complexity is well thought out by | ||
| 65 | * the Zig folks, and it enables a diverse set of allocation strategies | ||
| 66 | * as shown by the Zig ecosystem. As a consolation, please note that many | ||
| 67 | * of the arguments are only needed for advanced use cases and can be | ||
| 68 | * safely ignored in simple implementations. For example, if you look at | ||
| 69 | * the Zig implementation of the libc allocator in `lib/std/heap.zig` | ||
| 70 | * (search for CAllocator), you'll see it is very simple. | ||
| 71 | * | ||
| 72 | * We chose to align with the Zig allocator interface because: | ||
| 73 | * | ||
| 74 | * 1. It is a proven interface that serves a wide variety of use cases | ||
| 75 | * in the real world via the Zig ecosystem. It's shown to work. | ||
| 76 | * | ||
| 77 | * 2. Our core implementation itself is Zig, and this lets us very | ||
| 78 | * cheaply and easily convert between C and Zig allocators. | ||
| 79 | * | ||
| 80 | * NOTE(mitchellh): In the future, we can have default implementations of | ||
| 81 | * resize/remap and allow those to be null. | ||
| 82 | */ | ||
| 83 | typedef struct { | ||
| 84 | /** | ||
| 85 | * Return a pointer to `len` bytes with specified `alignment`, or return | ||
| 86 | * `NULL` indicating the allocation failed. | ||
| 87 | * | ||
| 88 | * @param ctx The allocator context | ||
| 89 | * @param len Number of bytes to allocate | ||
| 90 | * @param alignment Required alignment for the allocation. Guaranteed to | ||
| 91 | * be a power of two between 1 and 16 inclusive. | ||
| 92 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 93 | * @return Pointer to allocated memory, or NULL if allocation failed | ||
| 94 | */ | ||
| 95 | void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); | ||
| 96 | |||
| 97 | /** | ||
| 98 | * Attempt to expand or shrink memory in place. | ||
| 99 | * | ||
| 100 | * `memory_len` must equal the length requested from the most recent | ||
| 101 | * successful call to `alloc`, `resize`, or `remap`. `alignment` must | ||
| 102 | * equal the same value that was passed as the `alignment` parameter to | ||
| 103 | * the original `alloc` call. | ||
| 104 | * | ||
| 105 | * `new_len` must be greater than zero. | ||
| 106 | * | ||
| 107 | * @param ctx The allocator context | ||
| 108 | * @param memory Pointer to the memory block to resize | ||
| 109 | * @param memory_len Current size of the memory block | ||
| 110 | * @param alignment Alignment (must match original allocation) | ||
| 111 | * @param new_len New requested size | ||
| 112 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 113 | * @return true if resize was successful in-place, false if relocation would be required | ||
| 114 | */ | ||
| 115 | bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); | ||
| 116 | |||
| 117 | /** | ||
| 118 | * Attempt to expand or shrink memory, allowing relocation. | ||
| 119 | * | ||
| 120 | * `memory_len` must equal the length requested from the most recent | ||
| 121 | * successful call to `alloc`, `resize`, or `remap`. `alignment` must | ||
| 122 | * equal the same value that was passed as the `alignment` parameter to | ||
| 123 | * the original `alloc` call. | ||
| 124 | * | ||
| 125 | * A non-`NULL` return value indicates the resize was successful. The | ||
| 126 | * allocation may have same address, or may have been relocated. In either | ||
| 127 | * case, the allocation now has size of `new_len`. A `NULL` return value | ||
| 128 | * indicates that the resize would be equivalent to allocating new memory, | ||
| 129 | * copying the bytes from the old memory, and then freeing the old memory. | ||
| 130 | * In such case, it is more efficient for the caller to perform the copy. | ||
| 131 | * | ||
| 132 | * `new_len` must be greater than zero. | ||
| 133 | * | ||
| 134 | * @param ctx The allocator context | ||
| 135 | * @param memory Pointer to the memory block to remap | ||
| 136 | * @param memory_len Current size of the memory block | ||
| 137 | * @param alignment Alignment (must match original allocation) | ||
| 138 | * @param new_len New requested size | ||
| 139 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 140 | * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed | ||
| 141 | */ | ||
| 142 | void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Free and invalidate a region of memory. | ||
| 146 | * | ||
| 147 | * `memory_len` must equal the length requested from the most recent | ||
| 148 | * successful call to `alloc`, `resize`, or `remap`. `alignment` must | ||
| 149 | * equal the same value that was passed as the `alignment` parameter to | ||
| 150 | * the original `alloc` call. | ||
| 151 | * | ||
| 152 | * @param ctx The allocator context | ||
| 153 | * @param memory Pointer to the memory block to free | ||
| 154 | * @param memory_len Size of the memory block | ||
| 155 | * @param alignment Alignment (must match original allocation) | ||
| 156 | * @param ret_addr First return address of the allocation call stack (0 if not provided) | ||
| 157 | */ | ||
| 158 | void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); | ||
| 159 | } GhosttyAllocatorVtable; | ||
| 160 | |||
| 161 | /** | ||
| 162 | * Custom memory allocator. | ||
| 163 | * | ||
| 164 | * For functions that take an allocator pointer, a NULL pointer indicates | ||
| 165 | * that the default allocator should be used. The default allocator will | ||
| 166 | * be libc malloc/free if we're linking to libc. If libc isn't linked, | ||
| 167 | * a custom allocator is used (currently Zig's SMP allocator). | ||
| 168 | * | ||
| 169 | * @ingroup allocator | ||
| 170 | * | ||
| 171 | * Usage example: | ||
| 172 | * @code | ||
| 173 | * GhosttyAllocator allocator = { | ||
| 174 | * .vtable = &my_allocator_vtable, | ||
| 175 | * .ctx = my_allocator_state | ||
| 176 | * }; | ||
| 177 | * @endcode | ||
| 178 | */ | ||
| 179 | typedef struct GhosttyAllocator { | ||
| 180 | /** | ||
| 181 | * Opaque context pointer passed to all vtable functions. | ||
| 182 | * This allows the allocator implementation to maintain state | ||
| 183 | * or reference external resources needed for memory management. | ||
| 184 | */ | ||
| 185 | void *ctx; | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Pointer to the allocator's vtable containing function pointers | ||
| 189 | * for memory operations (alloc, resize, remap, free). | ||
| 190 | */ | ||
| 191 | const GhosttyAllocatorVtable *vtable; | ||
| 192 | } GhosttyAllocator; | ||
| 193 | |||
| 194 | /** @} */ | ||
| 195 | |||
| 196 | #endif /* GHOSTTY_VT_ALLOCATOR_H */ | ||
| @@ -0,0 +1,96 @@ | |||
| 1 | /** | ||
| 2 | * @file color.h | ||
| 3 | * | ||
| 4 | * Color types and utilities. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_COLOR_H | ||
| 8 | #define GHOSTTY_VT_COLOR_H | ||
| 9 | |||
| 10 | #include <stdint.h> | ||
| 11 | |||
| 12 | #ifdef __cplusplus | ||
| 13 | extern "C" { | ||
| 14 | #endif | ||
| 15 | |||
| 16 | /** | ||
| 17 | * RGB color value. | ||
| 18 | * | ||
| 19 | * @ingroup sgr | ||
| 20 | */ | ||
| 21 | typedef struct { | ||
| 22 | uint8_t r; /**< Red component (0-255) */ | ||
| 23 | uint8_t g; /**< Green component (0-255) */ | ||
| 24 | uint8_t b; /**< Blue component (0-255) */ | ||
| 25 | } GhosttyColorRgb; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Palette color index (0-255). | ||
| 29 | * | ||
| 30 | * @ingroup sgr | ||
| 31 | */ | ||
| 32 | typedef uint8_t GhosttyColorPaletteIndex; | ||
| 33 | |||
| 34 | /** @addtogroup sgr | ||
| 35 | * @{ | ||
| 36 | */ | ||
| 37 | |||
| 38 | /** Black color (0) @ingroup sgr */ | ||
| 39 | #define GHOSTTY_COLOR_NAMED_BLACK 0 | ||
| 40 | /** Red color (1) @ingroup sgr */ | ||
| 41 | #define GHOSTTY_COLOR_NAMED_RED 1 | ||
| 42 | /** Green color (2) @ingroup sgr */ | ||
| 43 | #define GHOSTTY_COLOR_NAMED_GREEN 2 | ||
| 44 | /** Yellow color (3) @ingroup sgr */ | ||
| 45 | #define GHOSTTY_COLOR_NAMED_YELLOW 3 | ||
| 46 | /** Blue color (4) @ingroup sgr */ | ||
| 47 | #define GHOSTTY_COLOR_NAMED_BLUE 4 | ||
| 48 | /** Magenta color (5) @ingroup sgr */ | ||
| 49 | #define GHOSTTY_COLOR_NAMED_MAGENTA 5 | ||
| 50 | /** Cyan color (6) @ingroup sgr */ | ||
| 51 | #define GHOSTTY_COLOR_NAMED_CYAN 6 | ||
| 52 | /** White color (7) @ingroup sgr */ | ||
| 53 | #define GHOSTTY_COLOR_NAMED_WHITE 7 | ||
| 54 | /** Bright black color (8) @ingroup sgr */ | ||
| 55 | #define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8 | ||
| 56 | /** Bright red color (9) @ingroup sgr */ | ||
| 57 | #define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9 | ||
| 58 | /** Bright green color (10) @ingroup sgr */ | ||
| 59 | #define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10 | ||
| 60 | /** Bright yellow color (11) @ingroup sgr */ | ||
| 61 | #define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11 | ||
| 62 | /** Bright blue color (12) @ingroup sgr */ | ||
| 63 | #define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12 | ||
| 64 | /** Bright magenta color (13) @ingroup sgr */ | ||
| 65 | #define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13 | ||
| 66 | /** Bright cyan color (14) @ingroup sgr */ | ||
| 67 | #define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14 | ||
| 68 | /** Bright white color (15) @ingroup sgr */ | ||
| 69 | #define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15 | ||
| 70 | |||
| 71 | /** @} */ | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Get the RGB color components. | ||
| 75 | * | ||
| 76 | * This function extracts the individual red, green, and blue components | ||
| 77 | * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments | ||
| 78 | * where accessing struct fields directly is difficult. | ||
| 79 | * | ||
| 80 | * @param color The RGB color value | ||
| 81 | * @param r Pointer to store the red component (0-255) | ||
| 82 | * @param g Pointer to store the green component (0-255) | ||
| 83 | * @param b Pointer to store the blue component (0-255) | ||
| 84 | * | ||
| 85 | * @ingroup sgr | ||
| 86 | */ | ||
| 87 | void ghostty_color_rgb_get(GhosttyColorRgb color, | ||
| 88 | uint8_t* r, | ||
| 89 | uint8_t* g, | ||
| 90 | uint8_t* b); | ||
| 91 | |||
| 92 | #ifdef __cplusplus | ||
| 93 | } | ||
| 94 | #endif | ||
| 95 | |||
| 96 | #endif /* GHOSTTY_VT_COLOR_H */ | ||
| @@ -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 */ | ||
| @@ -0,0 +1,221 @@ | |||
| 1 | /** | ||
| 2 | * @file encoder.h | ||
| 3 | * | ||
| 4 | * Key event encoding to terminal escape sequences. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_KEY_ENCODER_H | ||
| 8 | #define GHOSTTY_VT_KEY_ENCODER_H | ||
| 9 | |||
| 10 | #include <stddef.h> | ||
| 11 | #include <stdint.h> | ||
| 12 | #include <ghostty/vt/result.h> | ||
| 13 | #include <ghostty/vt/allocator.h> | ||
| 14 | #include <ghostty/vt/key/event.h> | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Opaque handle to a key encoder instance. | ||
| 18 | * | ||
| 19 | * This handle represents a key encoder that converts key events into terminal | ||
| 20 | * escape sequences. | ||
| 21 | * | ||
| 22 | * @ingroup key | ||
| 23 | */ | ||
| 24 | typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Kitty keyboard protocol flags. | ||
| 28 | * | ||
| 29 | * Bitflags representing the various modes of the Kitty keyboard protocol. | ||
| 30 | * These can be combined using bitwise OR operations. Valid values all | ||
| 31 | * start with `GHOSTTY_KITTY_KEY_`. | ||
| 32 | * | ||
| 33 | * @ingroup key | ||
| 34 | */ | ||
| 35 | typedef uint8_t GhosttyKittyKeyFlags; | ||
| 36 | |||
| 37 | /** Kitty keyboard protocol disabled (all flags off) */ | ||
| 38 | #define GHOSTTY_KITTY_KEY_DISABLED 0 | ||
| 39 | |||
| 40 | /** Disambiguate escape codes */ | ||
| 41 | #define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) | ||
| 42 | |||
| 43 | /** Report key press and release events */ | ||
| 44 | #define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) | ||
| 45 | |||
| 46 | /** Report alternate key codes */ | ||
| 47 | #define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) | ||
| 48 | |||
| 49 | /** Report all key events including those normally handled by the terminal */ | ||
| 50 | #define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) | ||
| 51 | |||
| 52 | /** Report associated text with key events */ | ||
| 53 | #define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) | ||
| 54 | |||
| 55 | /** All Kitty keyboard protocol flags enabled */ | ||
| 56 | #define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) | ||
| 57 | |||
| 58 | /** | ||
| 59 | * macOS option key behavior. | ||
| 60 | * | ||
| 61 | * Determines whether the "option" key on macOS is treated as "alt" or not. | ||
| 62 | * See the Ghostty `macos-option-as-alt` configuration option for more details. | ||
| 63 | * | ||
| 64 | * @ingroup key | ||
| 65 | */ | ||
| 66 | typedef enum { | ||
| 67 | /** Option key is not treated as alt */ | ||
| 68 | GHOSTTY_OPTION_AS_ALT_FALSE = 0, | ||
| 69 | /** Option key is treated as alt */ | ||
| 70 | GHOSTTY_OPTION_AS_ALT_TRUE = 1, | ||
| 71 | /** Only left option key is treated as alt */ | ||
| 72 | GHOSTTY_OPTION_AS_ALT_LEFT = 2, | ||
| 73 | /** Only right option key is treated as alt */ | ||
| 74 | GHOSTTY_OPTION_AS_ALT_RIGHT = 3, | ||
| 75 | } GhosttyOptionAsAlt; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Key encoder option identifiers. | ||
| 79 | * | ||
| 80 | * These values are used with ghostty_key_encoder_setopt() to configure | ||
| 81 | * the behavior of the key encoder. | ||
| 82 | * | ||
| 83 | * @ingroup key | ||
| 84 | */ | ||
| 85 | typedef enum { | ||
| 86 | /** Terminal DEC mode 1: cursor key application mode (value: bool) */ | ||
| 87 | GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, | ||
| 88 | |||
| 89 | /** Terminal DEC mode 66: keypad key application mode (value: bool) */ | ||
| 90 | GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, | ||
| 91 | |||
| 92 | /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ | ||
| 93 | GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, | ||
| 94 | |||
| 95 | /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ | ||
| 96 | GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, | ||
| 97 | |||
| 98 | /** xterm modifyOtherKeys mode 2 (value: bool) */ | ||
| 99 | GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, | ||
| 100 | |||
| 101 | /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ | ||
| 102 | GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, | ||
| 103 | |||
| 104 | /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ | ||
| 105 | GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, | ||
| 106 | } GhosttyKeyEncoderOption; | ||
| 107 | |||
| 108 | /** | ||
| 109 | * Create a new key encoder instance. | ||
| 110 | * | ||
| 111 | * Creates a new key encoder with default options. The encoder can be configured | ||
| 112 | * using ghostty_key_encoder_setopt() and must be freed using | ||
| 113 | * ghostty_key_encoder_free() when no longer needed. | ||
| 114 | * | ||
| 115 | * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator | ||
| 116 | * @param encoder Pointer to store the created encoder handle | ||
| 117 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 118 | * | ||
| 119 | * @ingroup key | ||
| 120 | */ | ||
| 121 | GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Free a key encoder instance. | ||
| 125 | * | ||
| 126 | * Releases all resources associated with the key encoder. After this call, | ||
| 127 | * the encoder handle becomes invalid and must not be used. | ||
| 128 | * | ||
| 129 | * @param encoder The encoder handle to free (may be NULL) | ||
| 130 | * | ||
| 131 | * @ingroup key | ||
| 132 | */ | ||
| 133 | void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Set an option on the key encoder. | ||
| 137 | * | ||
| 138 | * Configures the behavior of the key encoder. Options control various aspects | ||
| 139 | * of encoding such as terminal modes (cursor key application mode, keypad mode), | ||
| 140 | * protocol selection (Kitty keyboard protocol flags), and platform-specific | ||
| 141 | * behaviors (macOS option-as-alt). | ||
| 142 | * | ||
| 143 | * A null pointer value does nothing. It does not reset the value to the | ||
| 144 | * default. The setopt call will do nothing. | ||
| 145 | * | ||
| 146 | * @param encoder The encoder handle, must not be NULL | ||
| 147 | * @param option The option to set | ||
| 148 | * @param value Pointer to the value to set (type depends on the option) | ||
| 149 | * | ||
| 150 | * @ingroup key | ||
| 151 | */ | ||
| 152 | void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Encode a key event into a terminal escape sequence. | ||
| 156 | * | ||
| 157 | * Converts a key event into the appropriate terminal escape sequence based on | ||
| 158 | * the encoder's current options. The sequence is written to the provided buffer. | ||
| 159 | * | ||
| 160 | * Not all key events produce output. For example, unmodified modifier keys | ||
| 161 | * typically don't generate escape sequences. Check the out_len parameter to | ||
| 162 | * determine if any data was written. | ||
| 163 | * | ||
| 164 | * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY | ||
| 165 | * and out_len will contain the required buffer size. The caller can then | ||
| 166 | * allocate a larger buffer and call the function again. | ||
| 167 | * | ||
| 168 | * @param encoder The encoder handle, must not be NULL | ||
| 169 | * @param event The key event to encode, must not be NULL | ||
| 170 | * @param out_buf Buffer to write the encoded sequence to | ||
| 171 | * @param out_buf_size Size of the output buffer in bytes | ||
| 172 | * @param out_len Pointer to store the number of bytes written (may be NULL) | ||
| 173 | * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code | ||
| 174 | * | ||
| 175 | * ## Example: Calculate required buffer size | ||
| 176 | * | ||
| 177 | * @code{.c} | ||
| 178 | * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) | ||
| 179 | * size_t required = 0; | ||
| 180 | * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); | ||
| 181 | * assert(result == GHOSTTY_OUT_OF_MEMORY); | ||
| 182 | * | ||
| 183 | * // Allocate buffer of required size | ||
| 184 | * char *buf = malloc(required); | ||
| 185 | * | ||
| 186 | * // Encode with properly sized buffer | ||
| 187 | * size_t written = 0; | ||
| 188 | * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); | ||
| 189 | * assert(result == GHOSTTY_SUCCESS); | ||
| 190 | * | ||
| 191 | * // Use the encoded sequence... | ||
| 192 | * | ||
| 193 | * free(buf); | ||
| 194 | * @endcode | ||
| 195 | * | ||
| 196 | * ## Example: Direct encoding with static buffer | ||
| 197 | * | ||
| 198 | * @code{.c} | ||
| 199 | * // Most escape sequences are short, so a static buffer often suffices | ||
| 200 | * char buf[128]; | ||
| 201 | * size_t written = 0; | ||
| 202 | * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); | ||
| 203 | * | ||
| 204 | * if (result == GHOSTTY_SUCCESS) { | ||
| 205 | * // Write the encoded sequence to the terminal | ||
| 206 | * write(pty_fd, buf, written); | ||
| 207 | * } else if (result == GHOSTTY_OUT_OF_MEMORY) { | ||
| 208 | * // Buffer too small, written contains required size | ||
| 209 | * char *dynamic_buf = malloc(written); | ||
| 210 | * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); | ||
| 211 | * assert(result == GHOSTTY_SUCCESS); | ||
| 212 | * write(pty_fd, dynamic_buf, written); | ||
| 213 | * free(dynamic_buf); | ||
| 214 | * } | ||
| 215 | * @endcode | ||
| 216 | * | ||
| 217 | * @ingroup key | ||
| 218 | */ | ||
| 219 | GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); | ||
| 220 | |||
| 221 | #endif /* GHOSTTY_VT_KEY_ENCODER_H */ | ||
| @@ -0,0 +1,474 @@ | |||
| 1 | /** | ||
| 2 | * @file event.h | ||
| 3 | * | ||
| 4 | * Key event representation and manipulation. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_KEY_EVENT_H | ||
| 8 | #define GHOSTTY_VT_KEY_EVENT_H | ||
| 9 | |||
| 10 | #include <stdbool.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <stdint.h> | ||
| 13 | #include <ghostty/vt/result.h> | ||
| 14 | #include <ghostty/vt/allocator.h> | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Opaque handle to a key event. | ||
| 18 | * | ||
| 19 | * This handle represents a keyboard input event containing information about | ||
| 20 | * the physical key pressed, modifiers, and generated text. | ||
| 21 | * | ||
| 22 | * @ingroup key | ||
| 23 | */ | ||
| 24 | typedef struct GhosttyKeyEvent *GhosttyKeyEvent; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Keyboard input event types. | ||
| 28 | * | ||
| 29 | * @ingroup key | ||
| 30 | */ | ||
| 31 | typedef enum { | ||
| 32 | /** Key was released */ | ||
| 33 | GHOSTTY_KEY_ACTION_RELEASE = 0, | ||
| 34 | /** Key was pressed */ | ||
| 35 | GHOSTTY_KEY_ACTION_PRESS = 1, | ||
| 36 | /** Key is being repeated (held down) */ | ||
| 37 | GHOSTTY_KEY_ACTION_REPEAT = 2, | ||
| 38 | } GhosttyKeyAction; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Keyboard modifier keys bitmask. | ||
| 42 | * | ||
| 43 | * A bitmask representing all keyboard modifiers. This tracks which modifier keys | ||
| 44 | * are pressed and, where supported by the platform, which side (left or right) | ||
| 45 | * of each modifier is active. | ||
| 46 | * | ||
| 47 | * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. | ||
| 48 | * | ||
| 49 | * Modifier side bits are only meaningful when the corresponding modifier bit is set. | ||
| 50 | * Not all platforms support distinguishing between left and right modifier | ||
| 51 | * keys and Ghostty is built to expect that some platforms may not provide this | ||
| 52 | * information. | ||
| 53 | * | ||
| 54 | * @ingroup key | ||
| 55 | */ | ||
| 56 | typedef uint16_t GhosttyMods; | ||
| 57 | |||
| 58 | /** Shift key is pressed */ | ||
| 59 | #define GHOSTTY_MODS_SHIFT (1 << 0) | ||
| 60 | /** Control key is pressed */ | ||
| 61 | #define GHOSTTY_MODS_CTRL (1 << 1) | ||
| 62 | /** Alt/Option key is pressed */ | ||
| 63 | #define GHOSTTY_MODS_ALT (1 << 2) | ||
| 64 | /** Super/Command/Windows key is pressed */ | ||
| 65 | #define GHOSTTY_MODS_SUPER (1 << 3) | ||
| 66 | /** Caps Lock is active */ | ||
| 67 | #define GHOSTTY_MODS_CAPS_LOCK (1 << 4) | ||
| 68 | /** Num Lock is active */ | ||
| 69 | #define GHOSTTY_MODS_NUM_LOCK (1 << 5) | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Right shift is pressed (0 = left, 1 = right). | ||
| 73 | * Only meaningful when GHOSTTY_MODS_SHIFT is set. | ||
| 74 | */ | ||
| 75 | #define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) | ||
| 76 | /** | ||
| 77 | * Right ctrl is pressed (0 = left, 1 = right). | ||
| 78 | * Only meaningful when GHOSTTY_MODS_CTRL is set. | ||
| 79 | */ | ||
| 80 | #define GHOSTTY_MODS_CTRL_SIDE (1 << 7) | ||
| 81 | /** | ||
| 82 | * Right alt is pressed (0 = left, 1 = right). | ||
| 83 | * Only meaningful when GHOSTTY_MODS_ALT is set. | ||
| 84 | */ | ||
| 85 | #define GHOSTTY_MODS_ALT_SIDE (1 << 8) | ||
| 86 | /** | ||
| 87 | * Right super is pressed (0 = left, 1 = right). | ||
| 88 | * Only meaningful when GHOSTTY_MODS_SUPER is set. | ||
| 89 | */ | ||
| 90 | #define GHOSTTY_MODS_SUPER_SIDE (1 << 9) | ||
| 91 | |||
| 92 | /** | ||
| 93 | * Physical key codes. | ||
| 94 | * | ||
| 95 | * The set of key codes that Ghostty is aware of. These represent physical keys | ||
| 96 | * on the keyboard and are layout-independent. For example, the "a" key on a US | ||
| 97 | * keyboard is the same as the "ф" key on a Russian keyboard, but both will | ||
| 98 | * report the same key_a value. | ||
| 99 | * | ||
| 100 | * Layout-dependent strings are provided separately as UTF-8 text and are produced | ||
| 101 | * by the platform. These values are based on the W3C UI Events KeyboardEvent code | ||
| 102 | * standard. See: https://www.w3.org/TR/uievents-code | ||
| 103 | * | ||
| 104 | * @ingroup key | ||
| 105 | */ | ||
| 106 | typedef enum { | ||
| 107 | GHOSTTY_KEY_UNIDENTIFIED = 0, | ||
| 108 | |||
| 109 | // Writing System Keys (W3C § 3.1.1) | ||
| 110 | GHOSTTY_KEY_BACKQUOTE, | ||
| 111 | GHOSTTY_KEY_BACKSLASH, | ||
| 112 | GHOSTTY_KEY_BRACKET_LEFT, | ||
| 113 | GHOSTTY_KEY_BRACKET_RIGHT, | ||
| 114 | GHOSTTY_KEY_COMMA, | ||
| 115 | GHOSTTY_KEY_DIGIT_0, | ||
| 116 | GHOSTTY_KEY_DIGIT_1, | ||
| 117 | GHOSTTY_KEY_DIGIT_2, | ||
| 118 | GHOSTTY_KEY_DIGIT_3, | ||
| 119 | GHOSTTY_KEY_DIGIT_4, | ||
| 120 | GHOSTTY_KEY_DIGIT_5, | ||
| 121 | GHOSTTY_KEY_DIGIT_6, | ||
| 122 | GHOSTTY_KEY_DIGIT_7, | ||
| 123 | GHOSTTY_KEY_DIGIT_8, | ||
| 124 | GHOSTTY_KEY_DIGIT_9, | ||
| 125 | GHOSTTY_KEY_EQUAL, | ||
| 126 | GHOSTTY_KEY_INTL_BACKSLASH, | ||
| 127 | GHOSTTY_KEY_INTL_RO, | ||
| 128 | GHOSTTY_KEY_INTL_YEN, | ||
| 129 | GHOSTTY_KEY_A, | ||
| 130 | GHOSTTY_KEY_B, | ||
| 131 | GHOSTTY_KEY_C, | ||
| 132 | GHOSTTY_KEY_D, | ||
| 133 | GHOSTTY_KEY_E, | ||
| 134 | GHOSTTY_KEY_F, | ||
| 135 | GHOSTTY_KEY_G, | ||
| 136 | GHOSTTY_KEY_H, | ||
| 137 | GHOSTTY_KEY_I, | ||
| 138 | GHOSTTY_KEY_J, | ||
| 139 | GHOSTTY_KEY_K, | ||
| 140 | GHOSTTY_KEY_L, | ||
| 141 | GHOSTTY_KEY_M, | ||
| 142 | GHOSTTY_KEY_N, | ||
| 143 | GHOSTTY_KEY_O, | ||
| 144 | GHOSTTY_KEY_P, | ||
| 145 | GHOSTTY_KEY_Q, | ||
| 146 | GHOSTTY_KEY_R, | ||
| 147 | GHOSTTY_KEY_S, | ||
| 148 | GHOSTTY_KEY_T, | ||
| 149 | GHOSTTY_KEY_U, | ||
| 150 | GHOSTTY_KEY_V, | ||
| 151 | GHOSTTY_KEY_W, | ||
| 152 | GHOSTTY_KEY_X, | ||
| 153 | GHOSTTY_KEY_Y, | ||
| 154 | GHOSTTY_KEY_Z, | ||
| 155 | GHOSTTY_KEY_MINUS, | ||
| 156 | GHOSTTY_KEY_PERIOD, | ||
| 157 | GHOSTTY_KEY_QUOTE, | ||
| 158 | GHOSTTY_KEY_SEMICOLON, | ||
| 159 | GHOSTTY_KEY_SLASH, | ||
| 160 | |||
| 161 | // Functional Keys (W3C § 3.1.2) | ||
| 162 | GHOSTTY_KEY_ALT_LEFT, | ||
| 163 | GHOSTTY_KEY_ALT_RIGHT, | ||
| 164 | GHOSTTY_KEY_BACKSPACE, | ||
| 165 | GHOSTTY_KEY_CAPS_LOCK, | ||
| 166 | GHOSTTY_KEY_CONTEXT_MENU, | ||
| 167 | GHOSTTY_KEY_CONTROL_LEFT, | ||
| 168 | GHOSTTY_KEY_CONTROL_RIGHT, | ||
| 169 | GHOSTTY_KEY_ENTER, | ||
| 170 | GHOSTTY_KEY_META_LEFT, | ||
| 171 | GHOSTTY_KEY_META_RIGHT, | ||
| 172 | GHOSTTY_KEY_SHIFT_LEFT, | ||
| 173 | GHOSTTY_KEY_SHIFT_RIGHT, | ||
| 174 | GHOSTTY_KEY_SPACE, | ||
| 175 | GHOSTTY_KEY_TAB, | ||
| 176 | GHOSTTY_KEY_CONVERT, | ||
| 177 | GHOSTTY_KEY_KANA_MODE, | ||
| 178 | GHOSTTY_KEY_NON_CONVERT, | ||
| 179 | |||
| 180 | // Control Pad Section (W3C § 3.2) | ||
| 181 | GHOSTTY_KEY_DELETE, | ||
| 182 | GHOSTTY_KEY_END, | ||
| 183 | GHOSTTY_KEY_HELP, | ||
| 184 | GHOSTTY_KEY_HOME, | ||
| 185 | GHOSTTY_KEY_INSERT, | ||
| 186 | GHOSTTY_KEY_PAGE_DOWN, | ||
| 187 | GHOSTTY_KEY_PAGE_UP, | ||
| 188 | |||
| 189 | // Arrow Pad Section (W3C § 3.3) | ||
| 190 | GHOSTTY_KEY_ARROW_DOWN, | ||
| 191 | GHOSTTY_KEY_ARROW_LEFT, | ||
| 192 | GHOSTTY_KEY_ARROW_RIGHT, | ||
| 193 | GHOSTTY_KEY_ARROW_UP, | ||
| 194 | |||
| 195 | // Numpad Section (W3C § 3.4) | ||
| 196 | GHOSTTY_KEY_NUM_LOCK, | ||
| 197 | GHOSTTY_KEY_NUMPAD_0, | ||
| 198 | GHOSTTY_KEY_NUMPAD_1, | ||
| 199 | GHOSTTY_KEY_NUMPAD_2, | ||
| 200 | GHOSTTY_KEY_NUMPAD_3, | ||
| 201 | GHOSTTY_KEY_NUMPAD_4, | ||
| 202 | GHOSTTY_KEY_NUMPAD_5, | ||
| 203 | GHOSTTY_KEY_NUMPAD_6, | ||
| 204 | GHOSTTY_KEY_NUMPAD_7, | ||
| 205 | GHOSTTY_KEY_NUMPAD_8, | ||
| 206 | GHOSTTY_KEY_NUMPAD_9, | ||
| 207 | GHOSTTY_KEY_NUMPAD_ADD, | ||
| 208 | GHOSTTY_KEY_NUMPAD_BACKSPACE, | ||
| 209 | GHOSTTY_KEY_NUMPAD_CLEAR, | ||
| 210 | GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, | ||
| 211 | GHOSTTY_KEY_NUMPAD_COMMA, | ||
| 212 | GHOSTTY_KEY_NUMPAD_DECIMAL, | ||
| 213 | GHOSTTY_KEY_NUMPAD_DIVIDE, | ||
| 214 | GHOSTTY_KEY_NUMPAD_ENTER, | ||
| 215 | GHOSTTY_KEY_NUMPAD_EQUAL, | ||
| 216 | GHOSTTY_KEY_NUMPAD_MEMORY_ADD, | ||
| 217 | GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, | ||
| 218 | GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, | ||
| 219 | GHOSTTY_KEY_NUMPAD_MEMORY_STORE, | ||
| 220 | GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, | ||
| 221 | GHOSTTY_KEY_NUMPAD_MULTIPLY, | ||
| 222 | GHOSTTY_KEY_NUMPAD_PAREN_LEFT, | ||
| 223 | GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, | ||
| 224 | GHOSTTY_KEY_NUMPAD_SUBTRACT, | ||
| 225 | GHOSTTY_KEY_NUMPAD_SEPARATOR, | ||
| 226 | GHOSTTY_KEY_NUMPAD_UP, | ||
| 227 | GHOSTTY_KEY_NUMPAD_DOWN, | ||
| 228 | GHOSTTY_KEY_NUMPAD_RIGHT, | ||
| 229 | GHOSTTY_KEY_NUMPAD_LEFT, | ||
| 230 | GHOSTTY_KEY_NUMPAD_BEGIN, | ||
| 231 | GHOSTTY_KEY_NUMPAD_HOME, | ||
| 232 | GHOSTTY_KEY_NUMPAD_END, | ||
| 233 | GHOSTTY_KEY_NUMPAD_INSERT, | ||
| 234 | GHOSTTY_KEY_NUMPAD_DELETE, | ||
| 235 | GHOSTTY_KEY_NUMPAD_PAGE_UP, | ||
| 236 | GHOSTTY_KEY_NUMPAD_PAGE_DOWN, | ||
| 237 | |||
| 238 | // Function Section (W3C § 3.5) | ||
| 239 | GHOSTTY_KEY_ESCAPE, | ||
| 240 | GHOSTTY_KEY_F1, | ||
| 241 | GHOSTTY_KEY_F2, | ||
| 242 | GHOSTTY_KEY_F3, | ||
| 243 | GHOSTTY_KEY_F4, | ||
| 244 | GHOSTTY_KEY_F5, | ||
| 245 | GHOSTTY_KEY_F6, | ||
| 246 | GHOSTTY_KEY_F7, | ||
| 247 | GHOSTTY_KEY_F8, | ||
| 248 | GHOSTTY_KEY_F9, | ||
| 249 | GHOSTTY_KEY_F10, | ||
| 250 | GHOSTTY_KEY_F11, | ||
| 251 | GHOSTTY_KEY_F12, | ||
| 252 | GHOSTTY_KEY_F13, | ||
| 253 | GHOSTTY_KEY_F14, | ||
| 254 | GHOSTTY_KEY_F15, | ||
| 255 | GHOSTTY_KEY_F16, | ||
| 256 | GHOSTTY_KEY_F17, | ||
| 257 | GHOSTTY_KEY_F18, | ||
| 258 | GHOSTTY_KEY_F19, | ||
| 259 | GHOSTTY_KEY_F20, | ||
| 260 | GHOSTTY_KEY_F21, | ||
| 261 | GHOSTTY_KEY_F22, | ||
| 262 | GHOSTTY_KEY_F23, | ||
| 263 | GHOSTTY_KEY_F24, | ||
| 264 | GHOSTTY_KEY_F25, | ||
| 265 | GHOSTTY_KEY_FN, | ||
| 266 | GHOSTTY_KEY_FN_LOCK, | ||
| 267 | GHOSTTY_KEY_PRINT_SCREEN, | ||
| 268 | GHOSTTY_KEY_SCROLL_LOCK, | ||
| 269 | GHOSTTY_KEY_PAUSE, | ||
| 270 | |||
| 271 | // Media Keys (W3C § 3.6) | ||
| 272 | GHOSTTY_KEY_BROWSER_BACK, | ||
| 273 | GHOSTTY_KEY_BROWSER_FAVORITES, | ||
| 274 | GHOSTTY_KEY_BROWSER_FORWARD, | ||
| 275 | GHOSTTY_KEY_BROWSER_HOME, | ||
| 276 | GHOSTTY_KEY_BROWSER_REFRESH, | ||
| 277 | GHOSTTY_KEY_BROWSER_SEARCH, | ||
| 278 | GHOSTTY_KEY_BROWSER_STOP, | ||
| 279 | GHOSTTY_KEY_EJECT, | ||
| 280 | GHOSTTY_KEY_LAUNCH_APP_1, | ||
| 281 | GHOSTTY_KEY_LAUNCH_APP_2, | ||
| 282 | GHOSTTY_KEY_LAUNCH_MAIL, | ||
| 283 | GHOSTTY_KEY_MEDIA_PLAY_PAUSE, | ||
| 284 | GHOSTTY_KEY_MEDIA_SELECT, | ||
| 285 | GHOSTTY_KEY_MEDIA_STOP, | ||
| 286 | GHOSTTY_KEY_MEDIA_TRACK_NEXT, | ||
| 287 | GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, | ||
| 288 | GHOSTTY_KEY_POWER, | ||
| 289 | GHOSTTY_KEY_SLEEP, | ||
| 290 | GHOSTTY_KEY_AUDIO_VOLUME_DOWN, | ||
| 291 | GHOSTTY_KEY_AUDIO_VOLUME_MUTE, | ||
| 292 | GHOSTTY_KEY_AUDIO_VOLUME_UP, | ||
| 293 | GHOSTTY_KEY_WAKE_UP, | ||
| 294 | |||
| 295 | // Legacy, Non-standard, and Special Keys (W3C § 3.7) | ||
| 296 | GHOSTTY_KEY_COPY, | ||
| 297 | GHOSTTY_KEY_CUT, | ||
| 298 | GHOSTTY_KEY_PASTE, | ||
| 299 | } GhosttyKey; | ||
| 300 | |||
| 301 | /** | ||
| 302 | * Create a new key event instance. | ||
| 303 | * | ||
| 304 | * Creates a new key event with default values. The event must be freed using | ||
| 305 | * ghostty_key_event_free() when no longer needed. | ||
| 306 | * | ||
| 307 | * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator | ||
| 308 | * @param event Pointer to store the created key event handle | ||
| 309 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 310 | * | ||
| 311 | * @ingroup key | ||
| 312 | */ | ||
| 313 | GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Free a key event instance. | ||
| 317 | * | ||
| 318 | * Releases all resources associated with the key event. After this call, | ||
| 319 | * the event handle becomes invalid and must not be used. | ||
| 320 | * | ||
| 321 | * @param event The key event handle to free (may be NULL) | ||
| 322 | * | ||
| 323 | * @ingroup key | ||
| 324 | */ | ||
| 325 | void ghostty_key_event_free(GhosttyKeyEvent event); | ||
| 326 | |||
| 327 | /** | ||
| 328 | * Set the key action (press, release, repeat). | ||
| 329 | * | ||
| 330 | * @param event The key event handle, must not be NULL | ||
| 331 | * @param action The action to set | ||
| 332 | * | ||
| 333 | * @ingroup key | ||
| 334 | */ | ||
| 335 | void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); | ||
| 336 | |||
| 337 | /** | ||
| 338 | * Get the key action (press, release, repeat). | ||
| 339 | * | ||
| 340 | * @param event The key event handle, must not be NULL | ||
| 341 | * @return The key action | ||
| 342 | * | ||
| 343 | * @ingroup key | ||
| 344 | */ | ||
| 345 | GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); | ||
| 346 | |||
| 347 | /** | ||
| 348 | * Set the physical key code. | ||
| 349 | * | ||
| 350 | * @param event The key event handle, must not be NULL | ||
| 351 | * @param key The physical key code to set | ||
| 352 | * | ||
| 353 | * @ingroup key | ||
| 354 | */ | ||
| 355 | void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); | ||
| 356 | |||
| 357 | /** | ||
| 358 | * Get the physical key code. | ||
| 359 | * | ||
| 360 | * @param event The key event handle, must not be NULL | ||
| 361 | * @return The physical key code | ||
| 362 | * | ||
| 363 | * @ingroup key | ||
| 364 | */ | ||
| 365 | GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); | ||
| 366 | |||
| 367 | /** | ||
| 368 | * Set the modifier keys bitmask. | ||
| 369 | * | ||
| 370 | * @param event The key event handle, must not be NULL | ||
| 371 | * @param mods The modifier keys bitmask to set | ||
| 372 | * | ||
| 373 | * @ingroup key | ||
| 374 | */ | ||
| 375 | void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); | ||
| 376 | |||
| 377 | /** | ||
| 378 | * Get the modifier keys bitmask. | ||
| 379 | * | ||
| 380 | * @param event The key event handle, must not be NULL | ||
| 381 | * @return The modifier keys bitmask | ||
| 382 | * | ||
| 383 | * @ingroup key | ||
| 384 | */ | ||
| 385 | GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); | ||
| 386 | |||
| 387 | /** | ||
| 388 | * Set the consumed modifiers bitmask. | ||
| 389 | * | ||
| 390 | * @param event The key event handle, must not be NULL | ||
| 391 | * @param consumed_mods The consumed modifiers bitmask to set | ||
| 392 | * | ||
| 393 | * @ingroup key | ||
| 394 | */ | ||
| 395 | void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); | ||
| 396 | |||
| 397 | /** | ||
| 398 | * Get the consumed modifiers bitmask. | ||
| 399 | * | ||
| 400 | * @param event The key event handle, must not be NULL | ||
| 401 | * @return The consumed modifiers bitmask | ||
| 402 | * | ||
| 403 | * @ingroup key | ||
| 404 | */ | ||
| 405 | GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); | ||
| 406 | |||
| 407 | /** | ||
| 408 | * Set whether the key event is part of a composition sequence. | ||
| 409 | * | ||
| 410 | * @param event The key event handle, must not be NULL | ||
| 411 | * @param composing Whether the key event is part of a composition sequence | ||
| 412 | * | ||
| 413 | * @ingroup key | ||
| 414 | */ | ||
| 415 | void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); | ||
| 416 | |||
| 417 | /** | ||
| 418 | * Get whether the key event is part of a composition sequence. | ||
| 419 | * | ||
| 420 | * @param event The key event handle, must not be NULL | ||
| 421 | * @return Whether the key event is part of a composition sequence | ||
| 422 | * | ||
| 423 | * @ingroup key | ||
| 424 | */ | ||
| 425 | bool ghostty_key_event_get_composing(GhosttyKeyEvent event); | ||
| 426 | |||
| 427 | /** | ||
| 428 | * Set the UTF-8 text generated by the key event. | ||
| 429 | * | ||
| 430 | * The key event does NOT take ownership of the text pointer. The caller | ||
| 431 | * must ensure the string remains valid for the lifetime needed by the event. | ||
| 432 | * | ||
| 433 | * @param event The key event handle, must not be NULL | ||
| 434 | * @param utf8 The UTF-8 text to set (or NULL for empty) | ||
| 435 | * @param len Length of the UTF-8 text in bytes | ||
| 436 | * | ||
| 437 | * @ingroup key | ||
| 438 | */ | ||
| 439 | void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); | ||
| 440 | |||
| 441 | /** | ||
| 442 | * Get the UTF-8 text generated by the key event. | ||
| 443 | * | ||
| 444 | * The returned pointer is valid until the event is freed or the UTF-8 text is modified. | ||
| 445 | * | ||
| 446 | * @param event The key event handle, must not be NULL | ||
| 447 | * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) | ||
| 448 | * @return The UTF-8 text (or NULL for empty) | ||
| 449 | * | ||
| 450 | * @ingroup key | ||
| 451 | */ | ||
| 452 | const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); | ||
| 453 | |||
| 454 | /** | ||
| 455 | * Set the unshifted Unicode codepoint. | ||
| 456 | * | ||
| 457 | * @param event The key event handle, must not be NULL | ||
| 458 | * @param codepoint The unshifted Unicode codepoint to set | ||
| 459 | * | ||
| 460 | * @ingroup key | ||
| 461 | */ | ||
| 462 | void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); | ||
| 463 | |||
| 464 | /** | ||
| 465 | * Get the unshifted Unicode codepoint. | ||
| 466 | * | ||
| 467 | * @param event The key event handle, must not be NULL | ||
| 468 | * @return The unshifted Unicode codepoint | ||
| 469 | * | ||
| 470 | * @ingroup key | ||
| 471 | */ | ||
| 472 | uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); | ||
| 473 | |||
| 474 | #endif /* GHOSTTY_VT_KEY_EVENT_H */ | ||
| @@ -0,0 +1,233 @@ | |||
| 1 | /** | ||
| 2 | * @file osc.h | ||
| 3 | * | ||
| 4 | * OSC (Operating System Command) sequence parser and command handling. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_OSC_H | ||
| 8 | #define GHOSTTY_VT_OSC_H | ||
| 9 | |||
| 10 | #include <stdbool.h> | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <stdint.h> | ||
| 13 | #include <ghostty/vt/result.h> | ||
| 14 | #include <ghostty/vt/allocator.h> | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Opaque handle to an OSC parser instance. | ||
| 18 | * | ||
| 19 | * This handle represents an OSC (Operating System Command) parser that can | ||
| 20 | * be used to parse the contents of OSC sequences. | ||
| 21 | * | ||
| 22 | * @ingroup osc | ||
| 23 | */ | ||
| 24 | typedef struct GhosttyOscParser *GhosttyOscParser; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Opaque handle to a single OSC command. | ||
| 28 | * | ||
| 29 | * This handle represents a parsed OSC (Operating System Command) command. | ||
| 30 | * The command can be queried for its type and associated data. | ||
| 31 | * | ||
| 32 | * @ingroup osc | ||
| 33 | */ | ||
| 34 | typedef struct GhosttyOscCommand *GhosttyOscCommand; | ||
| 35 | |||
| 36 | /** @defgroup osc OSC Parser | ||
| 37 | * | ||
| 38 | * OSC (Operating System Command) sequence parser and command handling. | ||
| 39 | * | ||
| 40 | * The parser operates in a streaming fashion, processing input byte-by-byte | ||
| 41 | * to handle OSC sequences that may arrive in fragments across multiple reads. | ||
| 42 | * This interface makes it easy to integrate into most environments and avoids | ||
| 43 | * over-allocating buffers. | ||
| 44 | * | ||
| 45 | * ## Basic Usage | ||
| 46 | * | ||
| 47 | * 1. Create a parser instance with ghostty_osc_new() | ||
| 48 | * 2. Feed bytes to the parser using ghostty_osc_next() | ||
| 49 | * 3. Finalize parsing with ghostty_osc_end() to get the command | ||
| 50 | * 4. Query command type and extract data using ghostty_osc_command_type() | ||
| 51 | * and ghostty_osc_command_data() | ||
| 52 | * 5. Free the parser with ghostty_osc_free() when done | ||
| 53 | * | ||
| 54 | * @{ | ||
| 55 | */ | ||
| 56 | |||
| 57 | /** | ||
| 58 | * OSC command types. | ||
| 59 | * | ||
| 60 | * @ingroup osc | ||
| 61 | */ | ||
| 62 | typedef enum { | ||
| 63 | GHOSTTY_OSC_COMMAND_INVALID = 0, | ||
| 64 | GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, | ||
| 65 | GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, | ||
| 66 | GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3, | ||
| 67 | GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4, | ||
| 68 | GHOSTTY_OSC_COMMAND_REPORT_PWD = 5, | ||
| 69 | GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6, | ||
| 70 | GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7, | ||
| 71 | GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8, | ||
| 72 | GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9, | ||
| 73 | GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10, | ||
| 74 | GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11, | ||
| 75 | GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12, | ||
| 76 | GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13, | ||
| 77 | GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14, | ||
| 78 | GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15, | ||
| 79 | GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16, | ||
| 80 | GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17, | ||
| 81 | GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18, | ||
| 82 | GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19, | ||
| 83 | GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, | ||
| 84 | GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, | ||
| 85 | GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, | ||
| 86 | } GhosttyOscCommandType; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * OSC command data types. | ||
| 90 | * | ||
| 91 | * These values specify what type of data to extract from an OSC command | ||
| 92 | * using `ghostty_osc_command_data`. | ||
| 93 | * | ||
| 94 | * @ingroup osc | ||
| 95 | */ | ||
| 96 | typedef enum { | ||
| 97 | /** Invalid data type. Never results in any data extraction. */ | ||
| 98 | GHOSTTY_OSC_DATA_INVALID = 0, | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Window title string data. | ||
| 102 | * | ||
| 103 | * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE | ||
| 104 | * | ||
| 105 | * Output type: const char ** (pointer to null-terminated string) | ||
| 106 | * | ||
| 107 | * Lifetime: Valid until the next call to any ghostty_osc_* function with | ||
| 108 | * the same parser instance. Memory is owned by the parser. | ||
| 109 | */ | ||
| 110 | GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, | ||
| 111 | } GhosttyOscCommandData; | ||
| 112 | |||
| 113 | /** | ||
| 114 | * Create a new OSC parser instance. | ||
| 115 | * | ||
| 116 | * Creates a new OSC (Operating System Command) parser using the provided | ||
| 117 | * allocator. The parser must be freed using ghostty_vt_osc_free() when | ||
| 118 | * no longer needed. | ||
| 119 | * | ||
| 120 | * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator | ||
| 121 | * @param parser Pointer to store the created parser handle | ||
| 122 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 123 | * | ||
| 124 | * @ingroup osc | ||
| 125 | */ | ||
| 126 | GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Free an OSC parser instance. | ||
| 130 | * | ||
| 131 | * Releases all resources associated with the OSC parser. After this call, | ||
| 132 | * the parser handle becomes invalid and must not be used. | ||
| 133 | * | ||
| 134 | * @param parser The parser handle to free (may be NULL) | ||
| 135 | * | ||
| 136 | * @ingroup osc | ||
| 137 | */ | ||
| 138 | void ghostty_osc_free(GhosttyOscParser parser); | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Reset an OSC parser instance to its initial state. | ||
| 142 | * | ||
| 143 | * Resets the parser state, clearing any partially parsed OSC sequences | ||
| 144 | * and returning the parser to its initial state. This is useful for | ||
| 145 | * reusing a parser instance or recovering from parse errors. | ||
| 146 | * | ||
| 147 | * @param parser The parser handle to reset, must not be null. | ||
| 148 | * | ||
| 149 | * @ingroup osc | ||
| 150 | */ | ||
| 151 | void ghostty_osc_reset(GhosttyOscParser parser); | ||
| 152 | |||
| 153 | /** | ||
| 154 | * Parse the next byte in an OSC sequence. | ||
| 155 | * | ||
| 156 | * Processes a single byte as part of an OSC sequence. The parser maintains | ||
| 157 | * internal state to track the progress through the sequence. Call this | ||
| 158 | * function for each byte in the sequence data. | ||
| 159 | * | ||
| 160 | * When finished pumping the parser with bytes, call ghostty_osc_end | ||
| 161 | * to get the final result. | ||
| 162 | * | ||
| 163 | * @param parser The parser handle, must not be null. | ||
| 164 | * @param byte The next byte to parse | ||
| 165 | * | ||
| 166 | * @ingroup osc | ||
| 167 | */ | ||
| 168 | void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); | ||
| 169 | |||
| 170 | /** | ||
| 171 | * Finalize OSC parsing and retrieve the parsed command. | ||
| 172 | * | ||
| 173 | * Call this function after feeding all bytes of an OSC sequence to the parser | ||
| 174 | * using ghostty_osc_next() with the exception of the terminating character | ||
| 175 | * (ESC or ST). This function finalizes the parsing process and returns the | ||
| 176 | * parsed OSC command. | ||
| 177 | * | ||
| 178 | * The return value is never NULL. Invalid commands will return a command | ||
| 179 | * with type GHOSTTY_OSC_COMMAND_INVALID. | ||
| 180 | * | ||
| 181 | * The terminator parameter specifies the byte that terminated the OSC sequence | ||
| 182 | * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is | ||
| 183 | * preserved in the parsed command so that responses can use the same terminator | ||
| 184 | * format for better compatibility with the calling program. For commands that | ||
| 185 | * do not require a response, this parameter is ignored and the resulting | ||
| 186 | * command will not retain the terminator information. | ||
| 187 | * | ||
| 188 | * The returned command handle is valid until the next call to any | ||
| 189 | * `ghostty_osc_*` function with the same parser instance with the exception | ||
| 190 | * of command introspection functions such as `ghostty_osc_command_type`. | ||
| 191 | * | ||
| 192 | * @param parser The parser handle, must not be null. | ||
| 193 | * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) | ||
| 194 | * @return Handle to the parsed OSC command | ||
| 195 | * | ||
| 196 | * @ingroup osc | ||
| 197 | */ | ||
| 198 | GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Get the type of an OSC command. | ||
| 202 | * | ||
| 203 | * Returns the type identifier for the given OSC command. This can be used | ||
| 204 | * to determine what kind of command was parsed and what data might be | ||
| 205 | * available from it. | ||
| 206 | * | ||
| 207 | * @param command The OSC command handle to query (may be NULL) | ||
| 208 | * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL | ||
| 209 | * | ||
| 210 | * @ingroup osc | ||
| 211 | */ | ||
| 212 | GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); | ||
| 213 | |||
| 214 | /** | ||
| 215 | * Extract data from an OSC command. | ||
| 216 | * | ||
| 217 | * Extracts typed data from the given OSC command based on the specified | ||
| 218 | * data type. The output pointer must be of the appropriate type for the | ||
| 219 | * requested data kind. Valid command types, output types, and memory | ||
| 220 | * safety information are documented in the `GhosttyOscCommandData` enum. | ||
| 221 | * | ||
| 222 | * @param command The OSC command handle to query (may be NULL) | ||
| 223 | * @param data The type of data to extract | ||
| 224 | * @param out Pointer to store the extracted data (type depends on data parameter) | ||
| 225 | * @return true if data extraction was successful, false otherwise | ||
| 226 | * | ||
| 227 | * @ingroup osc | ||
| 228 | */ | ||
| 229 | bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); | ||
| 230 | |||
| 231 | /** @} */ | ||
| 232 | |||
| 233 | #endif /* GHOSTTY_VT_OSC_H */ | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | /** | ||
| 2 | * @file paste.h | ||
| 3 | * | ||
| 4 | * Paste utilities - validate and encode paste data for terminal input. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_PASTE_H | ||
| 8 | #define GHOSTTY_VT_PASTE_H | ||
| 9 | |||
| 10 | /** @defgroup paste Paste Utilities | ||
| 11 | * | ||
| 12 | * Utilities for validating paste data safety. | ||
| 13 | * | ||
| 14 | * ## Basic Usage | ||
| 15 | * | ||
| 16 | * Use ghostty_paste_is_safe() to check if paste data contains potentially | ||
| 17 | * dangerous sequences before sending it to the terminal. | ||
| 18 | * | ||
| 19 | * ## Example | ||
| 20 | * | ||
| 21 | * @code{.c} | ||
| 22 | * #include <stdio.h> | ||
| 23 | * #include <string.h> | ||
| 24 | * #include <ghostty/vt.h> | ||
| 25 | * | ||
| 26 | * int main() { | ||
| 27 | * const char* safe_data = "hello world"; | ||
| 28 | * const char* unsafe_data = "rm -rf /\n"; | ||
| 29 | * | ||
| 30 | * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { | ||
| 31 | * printf("Safe to paste\n"); | ||
| 32 | * } | ||
| 33 | * | ||
| 34 | * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { | ||
| 35 | * printf("Unsafe! Contains newline\n"); | ||
| 36 | * } | ||
| 37 | * | ||
| 38 | * return 0; | ||
| 39 | * } | ||
| 40 | * @endcode | ||
| 41 | * | ||
| 42 | * @{ | ||
| 43 | */ | ||
| 44 | |||
| 45 | #include <stdbool.h> | ||
| 46 | #include <stddef.h> | ||
| 47 | |||
| 48 | #ifdef __cplusplus | ||
| 49 | extern "C" { | ||
| 50 | #endif | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Check if paste data is safe to paste into the terminal. | ||
| 54 | * | ||
| 55 | * Data is considered unsafe if it contains: | ||
| 56 | * - Newlines (`\n`) which can inject commands | ||
| 57 | * - The bracketed paste end sequence (`\x1b[201~`) which can be used | ||
| 58 | * to exit bracketed paste mode and inject commands | ||
| 59 | * | ||
| 60 | * This check is conservative and considers data unsafe regardless of | ||
| 61 | * current terminal state. | ||
| 62 | * | ||
| 63 | * @param data The paste data to check (must not be NULL) | ||
| 64 | * @param len The length of the data in bytes | ||
| 65 | * @return true if the data is safe to paste, false otherwise | ||
| 66 | */ | ||
| 67 | bool ghostty_paste_is_safe(const char* data, size_t len); | ||
| 68 | |||
| 69 | #ifdef __cplusplus | ||
| 70 | } | ||
| 71 | #endif | ||
| 72 | |||
| 73 | /** @} */ | ||
| 74 | |||
| 75 | #endif /* GHOSTTY_VT_PASTE_H */ | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | /** | ||
| 2 | * @file result.h | ||
| 3 | * | ||
| 4 | * Result codes for libghostty-vt operations. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_RESULT_H | ||
| 8 | #define GHOSTTY_VT_RESULT_H | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Result codes for libghostty-vt operations. | ||
| 12 | */ | ||
| 13 | typedef enum { | ||
| 14 | /** Operation completed successfully */ | ||
| 15 | GHOSTTY_SUCCESS = 0, | ||
| 16 | /** Operation failed due to failed allocation */ | ||
| 17 | GHOSTTY_OUT_OF_MEMORY = -1, | ||
| 18 | /** Operation failed due to invalid value */ | ||
| 19 | GHOSTTY_INVALID_VALUE = -2, | ||
| 20 | } GhosttyResult; | ||
| 21 | |||
| 22 | #endif /* GHOSTTY_VT_RESULT_H */ | ||
| @@ -0,0 +1,394 @@ | |||
| 1 | /** | ||
| 2 | * @file sgr.h | ||
| 3 | * | ||
| 4 | * SGR (Select Graphic Rendition) attribute parsing and handling. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_SGR_H | ||
| 8 | #define GHOSTTY_VT_SGR_H | ||
| 9 | |||
| 10 | /** @defgroup sgr SGR Parser | ||
| 11 | * | ||
| 12 | * SGR (Select Graphic Rendition) attribute parser. | ||
| 13 | * | ||
| 14 | * SGR sequences are the syntax used to set styling attributes such as | ||
| 15 | * bold, italic, underline, and colors for text in terminal emulators. | ||
| 16 | * For example, you may be familiar with sequences like `ESC[1;31m`. The | ||
| 17 | * `1;31` is the SGR attribute list. | ||
| 18 | * | ||
| 19 | * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`) | ||
| 20 | * and returns individual text attributes like bold, italic, colors, etc. | ||
| 21 | * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed, | ||
| 22 | * and handles various color formats including 8-color, 16-color, 256-color, | ||
| 23 | * X11 named colors, and RGB in multiple formats. | ||
| 24 | * | ||
| 25 | * ## Basic Usage | ||
| 26 | * | ||
| 27 | * 1. Create a parser instance with ghostty_sgr_new() | ||
| 28 | * 2. Set SGR parameters with ghostty_sgr_set_params() | ||
| 29 | * 3. Iterate through attributes using ghostty_sgr_next() | ||
| 30 | * 4. Free the parser with ghostty_sgr_free() when done | ||
| 31 | * | ||
| 32 | * ## Example | ||
| 33 | * | ||
| 34 | * @code{.c} | ||
| 35 | * #include <assert.h> | ||
| 36 | * #include <stdio.h> | ||
| 37 | * #include <ghostty/vt.h> | ||
| 38 | * | ||
| 39 | * int main() { | ||
| 40 | * // Create parser | ||
| 41 | * GhosttySgrParser parser; | ||
| 42 | * GhosttyResult result = ghostty_sgr_new(NULL, &parser); | ||
| 43 | * assert(result == GHOSTTY_SUCCESS); | ||
| 44 | * | ||
| 45 | * // Parse "bold, red foreground" sequence: ESC[1;31m | ||
| 46 | * uint16_t params[] = {1, 31}; | ||
| 47 | * result = ghostty_sgr_set_params(parser, params, NULL, 2); | ||
| 48 | * assert(result == GHOSTTY_SUCCESS); | ||
| 49 | * | ||
| 50 | * // Iterate through attributes | ||
| 51 | * GhosttySgrAttribute attr; | ||
| 52 | * while (ghostty_sgr_next(parser, &attr)) { | ||
| 53 | * switch (attr.tag) { | ||
| 54 | * case GHOSTTY_SGR_ATTR_BOLD: | ||
| 55 | * printf("Bold enabled\n"); | ||
| 56 | * break; | ||
| 57 | * case GHOSTTY_SGR_ATTR_FG_8: | ||
| 58 | * printf("Foreground color: %d\n", attr.value.fg_8); | ||
| 59 | * break; | ||
| 60 | * default: | ||
| 61 | * break; | ||
| 62 | * } | ||
| 63 | * } | ||
| 64 | * | ||
| 65 | * // Cleanup | ||
| 66 | * ghostty_sgr_free(parser); | ||
| 67 | * return 0; | ||
| 68 | * } | ||
| 69 | * @endcode | ||
| 70 | * | ||
| 71 | * @{ | ||
| 72 | */ | ||
| 73 | |||
| 74 | #include <ghostty/vt/allocator.h> | ||
| 75 | #include <ghostty/vt/color.h> | ||
| 76 | #include <ghostty/vt/result.h> | ||
| 77 | #include <stdbool.h> | ||
| 78 | #include <stddef.h> | ||
| 79 | #include <stdint.h> | ||
| 80 | |||
| 81 | #ifdef __cplusplus | ||
| 82 | extern "C" { | ||
| 83 | #endif | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Opaque handle to an SGR parser instance. | ||
| 87 | * | ||
| 88 | * This handle represents an SGR (Select Graphic Rendition) parser that can | ||
| 89 | * be used to parse SGR sequences and extract individual text attributes. | ||
| 90 | * | ||
| 91 | * @ingroup sgr | ||
| 92 | */ | ||
| 93 | typedef struct GhosttySgrParser* GhosttySgrParser; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * SGR attribute tags. | ||
| 97 | * | ||
| 98 | * These values identify the type of an SGR attribute in a tagged union. | ||
| 99 | * Use the tag to determine which field in the attribute value union to access. | ||
| 100 | * | ||
| 101 | * @ingroup sgr | ||
| 102 | */ | ||
| 103 | typedef enum { | ||
| 104 | GHOSTTY_SGR_ATTR_UNSET = 0, | ||
| 105 | GHOSTTY_SGR_ATTR_UNKNOWN = 1, | ||
| 106 | GHOSTTY_SGR_ATTR_BOLD = 2, | ||
| 107 | GHOSTTY_SGR_ATTR_RESET_BOLD = 3, | ||
| 108 | GHOSTTY_SGR_ATTR_ITALIC = 4, | ||
| 109 | GHOSTTY_SGR_ATTR_RESET_ITALIC = 5, | ||
| 110 | GHOSTTY_SGR_ATTR_FAINT = 6, | ||
| 111 | GHOSTTY_SGR_ATTR_UNDERLINE = 7, | ||
| 112 | GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8, | ||
| 113 | GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9, | ||
| 114 | GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10, | ||
| 115 | GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11, | ||
| 116 | GHOSTTY_SGR_ATTR_OVERLINE = 12, | ||
| 117 | GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13, | ||
| 118 | GHOSTTY_SGR_ATTR_BLINK = 14, | ||
| 119 | GHOSTTY_SGR_ATTR_RESET_BLINK = 15, | ||
| 120 | GHOSTTY_SGR_ATTR_INVERSE = 16, | ||
| 121 | GHOSTTY_SGR_ATTR_RESET_INVERSE = 17, | ||
| 122 | GHOSTTY_SGR_ATTR_INVISIBLE = 18, | ||
| 123 | GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19, | ||
| 124 | GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20, | ||
| 125 | GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21, | ||
| 126 | GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22, | ||
| 127 | GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23, | ||
| 128 | GHOSTTY_SGR_ATTR_BG_8 = 24, | ||
| 129 | GHOSTTY_SGR_ATTR_FG_8 = 25, | ||
| 130 | GHOSTTY_SGR_ATTR_RESET_FG = 26, | ||
| 131 | GHOSTTY_SGR_ATTR_RESET_BG = 27, | ||
| 132 | GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28, | ||
| 133 | GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29, | ||
| 134 | GHOSTTY_SGR_ATTR_BG_256 = 30, | ||
| 135 | GHOSTTY_SGR_ATTR_FG_256 = 31, | ||
| 136 | } GhosttySgrAttributeTag; | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Underline style types. | ||
| 140 | * | ||
| 141 | * @ingroup sgr | ||
| 142 | */ | ||
| 143 | typedef enum { | ||
| 144 | GHOSTTY_SGR_UNDERLINE_NONE = 0, | ||
| 145 | GHOSTTY_SGR_UNDERLINE_SINGLE = 1, | ||
| 146 | GHOSTTY_SGR_UNDERLINE_DOUBLE = 2, | ||
| 147 | GHOSTTY_SGR_UNDERLINE_CURLY = 3, | ||
| 148 | GHOSTTY_SGR_UNDERLINE_DOTTED = 4, | ||
| 149 | GHOSTTY_SGR_UNDERLINE_DASHED = 5, | ||
| 150 | } GhosttySgrUnderline; | ||
| 151 | |||
| 152 | /** | ||
| 153 | * Unknown SGR attribute data. | ||
| 154 | * | ||
| 155 | * Contains the full parameter list and the partial list where parsing | ||
| 156 | * encountered an unknown or invalid sequence. | ||
| 157 | * | ||
| 158 | * @ingroup sgr | ||
| 159 | */ | ||
| 160 | typedef struct { | ||
| 161 | const uint16_t* full_ptr; | ||
| 162 | size_t full_len; | ||
| 163 | const uint16_t* partial_ptr; | ||
| 164 | size_t partial_len; | ||
| 165 | } GhosttySgrUnknown; | ||
| 166 | |||
| 167 | /** | ||
| 168 | * SGR attribute value union. | ||
| 169 | * | ||
| 170 | * This union contains all possible attribute values. Use the tag field | ||
| 171 | * to determine which union member is active. Attributes without associated | ||
| 172 | * data (like bold, italic) don't use the union value. | ||
| 173 | * | ||
| 174 | * @ingroup sgr | ||
| 175 | */ | ||
| 176 | typedef union { | ||
| 177 | GhosttySgrUnknown unknown; | ||
| 178 | GhosttySgrUnderline underline; | ||
| 179 | GhosttyColorRgb underline_color; | ||
| 180 | GhosttyColorPaletteIndex underline_color_256; | ||
| 181 | GhosttyColorRgb direct_color_fg; | ||
| 182 | GhosttyColorRgb direct_color_bg; | ||
| 183 | GhosttyColorPaletteIndex bg_8; | ||
| 184 | GhosttyColorPaletteIndex fg_8; | ||
| 185 | GhosttyColorPaletteIndex bright_bg_8; | ||
| 186 | GhosttyColorPaletteIndex bright_fg_8; | ||
| 187 | GhosttyColorPaletteIndex bg_256; | ||
| 188 | GhosttyColorPaletteIndex fg_256; | ||
| 189 | uint64_t _padding[8]; | ||
| 190 | } GhosttySgrAttributeValue; | ||
| 191 | |||
| 192 | /** | ||
| 193 | * SGR attribute (tagged union). | ||
| 194 | * | ||
| 195 | * A complete SGR attribute with both its type tag and associated value. | ||
| 196 | * Always check the tag field to determine which value union member is valid. | ||
| 197 | * | ||
| 198 | * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be | ||
| 199 | * identified by tag alone; the value union is not used for these and | ||
| 200 | * the memory in the value field is undefined. | ||
| 201 | * | ||
| 202 | * @ingroup sgr | ||
| 203 | */ | ||
| 204 | typedef struct { | ||
| 205 | GhosttySgrAttributeTag tag; | ||
| 206 | GhosttySgrAttributeValue value; | ||
| 207 | } GhosttySgrAttribute; | ||
| 208 | |||
| 209 | /** | ||
| 210 | * Create a new SGR parser instance. | ||
| 211 | * | ||
| 212 | * Creates a new SGR (Select Graphic Rendition) parser using the provided | ||
| 213 | * allocator. The parser must be freed using ghostty_sgr_free() when | ||
| 214 | * no longer needed. | ||
| 215 | * | ||
| 216 | * @param allocator Pointer to the allocator to use for memory management, or | ||
| 217 | * NULL to use the default allocator | ||
| 218 | * @param parser Pointer to store the created parser handle | ||
| 219 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 220 | * | ||
| 221 | * @ingroup sgr | ||
| 222 | */ | ||
| 223 | GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, | ||
| 224 | GhosttySgrParser* parser); | ||
| 225 | |||
| 226 | /** | ||
| 227 | * Free an SGR parser instance. | ||
| 228 | * | ||
| 229 | * Releases all resources associated with the SGR parser. After this call, | ||
| 230 | * the parser handle becomes invalid and must not be used. This includes | ||
| 231 | * any attributes previously returned by ghostty_sgr_next(). | ||
| 232 | * | ||
| 233 | * @param parser The parser handle to free (may be NULL) | ||
| 234 | * | ||
| 235 | * @ingroup sgr | ||
| 236 | */ | ||
| 237 | void ghostty_sgr_free(GhosttySgrParser parser); | ||
| 238 | |||
| 239 | /** | ||
| 240 | * Reset an SGR parser instance to the beginning of the parameter list. | ||
| 241 | * | ||
| 242 | * Resets the parser's iteration state without clearing the parameters. | ||
| 243 | * After calling this, ghostty_sgr_next() will start from the beginning | ||
| 244 | * of the parameter list again. | ||
| 245 | * | ||
| 246 | * @param parser The parser handle to reset, must not be NULL | ||
| 247 | * | ||
| 248 | * @ingroup sgr | ||
| 249 | */ | ||
| 250 | void ghostty_sgr_reset(GhosttySgrParser parser); | ||
| 251 | |||
| 252 | /** | ||
| 253 | * Set SGR parameters for parsing. | ||
| 254 | * | ||
| 255 | * Sets the SGR parameter list to parse. Parameters are the numeric values | ||
| 256 | * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}). | ||
| 257 | * | ||
| 258 | * The separators array optionally specifies the separator type for each | ||
| 259 | * parameter position. Each byte should be either ';' for semicolon or ':' | ||
| 260 | * for colon. This is needed for certain color formats that use colon | ||
| 261 | * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator | ||
| 262 | * values are treated as semicolons. The separators array must have the same | ||
| 263 | * length as the params array, if it is not NULL. | ||
| 264 | * | ||
| 265 | * If separators is NULL, all parameters are assumed to be semicolon-separated. | ||
| 266 | * | ||
| 267 | * This function makes an internal copy of the parameter and separator data, | ||
| 268 | * so the caller can safely free or modify the input arrays after this call. | ||
| 269 | * | ||
| 270 | * After calling this function, the parser is automatically reset and ready | ||
| 271 | * to iterate from the beginning. | ||
| 272 | * | ||
| 273 | * @param parser The parser handle, must not be NULL | ||
| 274 | * @param params Array of SGR parameter values | ||
| 275 | * @param separators Optional array of separator characters (';' or ':'), or | ||
| 276 | * NULL | ||
| 277 | * @param len Number of parameters (and separators if provided) | ||
| 278 | * @return GHOSTTY_SUCCESS on success, or an error code on failure | ||
| 279 | * | ||
| 280 | * @ingroup sgr | ||
| 281 | */ | ||
| 282 | GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, | ||
| 283 | const uint16_t* params, | ||
| 284 | const char* separators, | ||
| 285 | size_t len); | ||
| 286 | |||
| 287 | /** | ||
| 288 | * Get the next SGR attribute. | ||
| 289 | * | ||
| 290 | * Parses and returns the next attribute from the parameter list. | ||
| 291 | * Call this function repeatedly until it returns false to process | ||
| 292 | * all attributes in the sequence. | ||
| 293 | * | ||
| 294 | * @param parser The parser handle, must not be NULL | ||
| 295 | * @param attr Pointer to store the next attribute | ||
| 296 | * @return true if an attribute was returned, false if no more attributes | ||
| 297 | * | ||
| 298 | * @ingroup sgr | ||
| 299 | */ | ||
| 300 | bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); | ||
| 301 | |||
| 302 | /** | ||
| 303 | * Get the full parameter list from an unknown SGR attribute. | ||
| 304 | * | ||
| 305 | * This function retrieves the full parameter list that was provided to the | ||
| 306 | * parser when an unknown attribute was encountered. Primarily useful in | ||
| 307 | * WebAssembly environments where accessing struct fields directly is difficult. | ||
| 308 | * | ||
| 309 | * @param unknown The unknown attribute data | ||
| 310 | * @param ptr Pointer to store the pointer to the parameter array (may be NULL) | ||
| 311 | * @return The length of the full parameter array | ||
| 312 | * | ||
| 313 | * @ingroup sgr | ||
| 314 | */ | ||
| 315 | size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, | ||
| 316 | const uint16_t** ptr); | ||
| 317 | |||
| 318 | /** | ||
| 319 | * Get the partial parameter list from an unknown SGR attribute. | ||
| 320 | * | ||
| 321 | * This function retrieves the partial parameter list where parsing stopped | ||
| 322 | * when an unknown attribute was encountered. Primarily useful in WebAssembly | ||
| 323 | * environments where accessing struct fields directly is difficult. | ||
| 324 | * | ||
| 325 | * @param unknown The unknown attribute data | ||
| 326 | * @param ptr Pointer to store the pointer to the parameter array (may be NULL) | ||
| 327 | * @return The length of the partial parameter array | ||
| 328 | * | ||
| 329 | * @ingroup sgr | ||
| 330 | */ | ||
| 331 | size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, | ||
| 332 | const uint16_t** ptr); | ||
| 333 | |||
| 334 | /** | ||
| 335 | * Get the tag from an SGR attribute. | ||
| 336 | * | ||
| 337 | * This function extracts the tag that identifies which type of attribute | ||
| 338 | * this is. Primarily useful in WebAssembly environments where accessing | ||
| 339 | * struct fields directly is difficult. | ||
| 340 | * | ||
| 341 | * @param attr The SGR attribute | ||
| 342 | * @return The attribute tag | ||
| 343 | * | ||
| 344 | * @ingroup sgr | ||
| 345 | */ | ||
| 346 | GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); | ||
| 347 | |||
| 348 | /** | ||
| 349 | * Get the value from an SGR attribute. | ||
| 350 | * | ||
| 351 | * This function returns a pointer to the value union from an SGR attribute. Use | ||
| 352 | * the tag to determine which field of the union is valid. Primarily useful in | ||
| 353 | * WebAssembly environments where accessing struct fields directly is difficult. | ||
| 354 | * | ||
| 355 | * @param attr Pointer to the SGR attribute | ||
| 356 | * @return Pointer to the attribute value union | ||
| 357 | * | ||
| 358 | * @ingroup sgr | ||
| 359 | */ | ||
| 360 | GhosttySgrAttributeValue* ghostty_sgr_attribute_value( | ||
| 361 | GhosttySgrAttribute* attr); | ||
| 362 | |||
| 363 | #ifdef __wasm__ | ||
| 364 | /** | ||
| 365 | * Allocate memory for an SGR attribute (WebAssembly only). | ||
| 366 | * | ||
| 367 | * This is a convenience function for WebAssembly environments to allocate | ||
| 368 | * memory for an SGR attribute structure that can be passed to ghostty_sgr_next. | ||
| 369 | * | ||
| 370 | * @return Pointer to the allocated attribute structure | ||
| 371 | * | ||
| 372 | * @ingroup wasm | ||
| 373 | */ | ||
| 374 | GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); | ||
| 375 | |||
| 376 | /** | ||
| 377 | * Free memory for an SGR attribute (WebAssembly only). | ||
| 378 | * | ||
| 379 | * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute. | ||
| 380 | * | ||
| 381 | * @param attr Pointer to the attribute structure to free | ||
| 382 | * | ||
| 383 | * @ingroup wasm | ||
| 384 | */ | ||
| 385 | void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr); | ||
| 386 | #endif | ||
| 387 | |||
| 388 | #ifdef __cplusplus | ||
| 389 | } | ||
| 390 | #endif | ||
| 391 | |||
| 392 | /** @} */ | ||
| 393 | |||
| 394 | #endif /* GHOSTTY_VT_SGR_H */ | ||
| @@ -0,0 +1,159 @@ | |||
| 1 | /** | ||
| 2 | * @file wasm.h | ||
| 3 | * | ||
| 4 | * WebAssembly utility functions for libghostty-vt. | ||
| 5 | */ | ||
| 6 | |||
| 7 | #ifndef GHOSTTY_VT_WASM_H | ||
| 8 | #define GHOSTTY_VT_WASM_H | ||
| 9 | |||
| 10 | #ifdef __wasm__ | ||
| 11 | |||
| 12 | #include <stddef.h> | ||
| 13 | #include <stdint.h> | ||
| 14 | |||
| 15 | /** @defgroup wasm WebAssembly Utilities | ||
| 16 | * | ||
| 17 | * Convenience functions for allocating various types in WebAssembly builds. | ||
| 18 | * **These are only available the libghostty-vt wasm module.** | ||
| 19 | * | ||
| 20 | * Ghostty relies on pointers to various types for ABI compatibility, and | ||
| 21 | * creating those pointers in Wasm can be tedious. These functions provide | ||
| 22 | * a purely additive set of utilities that simplify memory management in | ||
| 23 | * Wasm environments without changing the core C library API. | ||
| 24 | * | ||
| 25 | * @note These functions always use the default allocator. If you need | ||
| 26 | * custom allocation strategies, you should allocate types manually using | ||
| 27 | * your custom allocator. This is a very rare use case in the WebAssembly | ||
| 28 | * world so these are optimized for simplicity. | ||
| 29 | * | ||
| 30 | * ## Example Usage | ||
| 31 | * | ||
| 32 | * Here's a simple example of using the Wasm utilities with the key encoder: | ||
| 33 | * | ||
| 34 | * @code | ||
| 35 | * const { exports } = wasmInstance; | ||
| 36 | * const view = new DataView(wasmMemory.buffer); | ||
| 37 | * | ||
| 38 | * // Create key encoder | ||
| 39 | * const encoderPtr = exports.ghostty_wasm_alloc_opaque(); | ||
| 40 | * exports.ghostty_key_encoder_new(null, encoderPtr); | ||
| 41 | * const encoder = view.getUint32(encoder, true); | ||
| 42 | * | ||
| 43 | * // Configure encoder with Kitty protocol flags | ||
| 44 | * const flagsPtr = exports.ghostty_wasm_alloc_u8(); | ||
| 45 | * view.setUint8(flagsPtr, 0x1F); | ||
| 46 | * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr); | ||
| 47 | * | ||
| 48 | * // Allocate output buffer and size pointer | ||
| 49 | * const bufferSize = 32; | ||
| 50 | * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize); | ||
| 51 | * const writtenPtr = exports.ghostty_wasm_alloc_usize(); | ||
| 52 | * | ||
| 53 | * // Encode the key event | ||
| 54 | * exports.ghostty_key_encoder_encode( | ||
| 55 | * encoder, eventPtr, bufPtr, bufferSize, writtenPtr | ||
| 56 | * ); | ||
| 57 | * | ||
| 58 | * // Read encoded output | ||
| 59 | * const bytesWritten = view.getUint32(writtenPtr, true); | ||
| 60 | * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten); | ||
| 61 | * @endcode | ||
| 62 | * | ||
| 63 | * @remark The code above is pretty ugly! This is the lowest level interface | ||
| 64 | * to the libghostty-vt Wasm module. In practice, this should be wrapped | ||
| 65 | * in a higher-level API that abstracts away all this. | ||
| 66 | * | ||
| 67 | * @{ | ||
| 68 | */ | ||
| 69 | |||
| 70 | /** | ||
| 71 | * Allocate an opaque pointer. This can be used for any opaque pointer | ||
| 72 | * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc. | ||
| 73 | * | ||
| 74 | * @return Pointer to allocated opaque pointer, or NULL if allocation failed | ||
| 75 | * @ingroup wasm | ||
| 76 | */ | ||
| 77 | void** ghostty_wasm_alloc_opaque(void); | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque(). | ||
| 81 | * | ||
| 82 | * @param ptr Pointer to free, or NULL (NULL is safely ignored) | ||
| 83 | * @ingroup wasm | ||
| 84 | */ | ||
| 85 | void ghostty_wasm_free_opaque(void **ptr); | ||
| 86 | |||
| 87 | /** | ||
| 88 | * Allocate an array of uint8_t values. | ||
| 89 | * | ||
| 90 | * @param len Number of uint8_t elements to allocate | ||
| 91 | * @return Pointer to allocated array, or NULL if allocation failed | ||
| 92 | * @ingroup wasm | ||
| 93 | */ | ||
| 94 | uint8_t* ghostty_wasm_alloc_u8_array(size_t len); | ||
| 95 | |||
| 96 | /** | ||
| 97 | * Free an array allocated by ghostty_wasm_alloc_u8_array(). | ||
| 98 | * | ||
| 99 | * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) | ||
| 100 | * @param len Length of the array (must match the length passed to alloc) | ||
| 101 | * @ingroup wasm | ||
| 102 | */ | ||
| 103 | void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Allocate an array of uint16_t values. | ||
| 107 | * | ||
| 108 | * @param len Number of uint16_t elements to allocate | ||
| 109 | * @return Pointer to allocated array, or NULL if allocation failed | ||
| 110 | * @ingroup wasm | ||
| 111 | */ | ||
| 112 | uint16_t* ghostty_wasm_alloc_u16_array(size_t len); | ||
| 113 | |||
| 114 | /** | ||
| 115 | * Free an array allocated by ghostty_wasm_alloc_u16_array(). | ||
| 116 | * | ||
| 117 | * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) | ||
| 118 | * @param len Length of the array (must match the length passed to alloc) | ||
| 119 | * @ingroup wasm | ||
| 120 | */ | ||
| 121 | void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Allocate a single uint8_t value. | ||
| 125 | * | ||
| 126 | * @return Pointer to allocated uint8_t, or NULL if allocation failed | ||
| 127 | * @ingroup wasm | ||
| 128 | */ | ||
| 129 | uint8_t* ghostty_wasm_alloc_u8(void); | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Free a uint8_t allocated by ghostty_wasm_alloc_u8(). | ||
| 133 | * | ||
| 134 | * @param ptr Pointer to free, or NULL (NULL is safely ignored) | ||
| 135 | * @ingroup wasm | ||
| 136 | */ | ||
| 137 | void ghostty_wasm_free_u8(uint8_t *ptr); | ||
| 138 | |||
| 139 | /** | ||
| 140 | * Allocate a single size_t value. | ||
| 141 | * | ||
| 142 | * @return Pointer to allocated size_t, or NULL if allocation failed | ||
| 143 | * @ingroup wasm | ||
| 144 | */ | ||
| 145 | size_t* ghostty_wasm_alloc_usize(void); | ||
| 146 | |||
| 147 | /** | ||
| 148 | * Free a size_t allocated by ghostty_wasm_alloc_usize(). | ||
| 149 | * | ||
| 150 | * @param ptr Pointer to free, or NULL (NULL is safely ignored) | ||
| 151 | * @ingroup wasm | ||
| 152 | */ | ||
| 153 | void ghostty_wasm_free_usize(size_t *ptr); | ||
| 154 | |||
| 155 | /** @} */ | ||
| 156 | |||
| 157 | #endif /* __wasm__ */ | ||
| 158 | |||
| 159 | #endif /* GHOSTTY_VT_WASM_H */ | ||
| @@ -0,0 +1,7 @@ | |||
| 1 | // This makes Ghostty available to the XCode build for the macOS app. | ||
| 2 | // We append "Kit" to it not to be cute, but because targets have to have | ||
| 3 | // unique names and we use Ghostty for other things. | ||
| 4 | module GhosttyKit { | ||
| 5 | umbrella header "ghostty.h" | ||
| 6 | export * | ||
| 7 | } | ||
Binary file not shown.
| @@ -0,0 +1 @@ | |||
| 1 | 268a0a9d761fb19673f05d28042488e2002300f2 | ||
| @@ -0,0 +1,19 @@ | |||
| 1 | apply plugin: 'com.android.library' | ||
| 2 | apply plugin: 'org.jetbrains.kotlin.android' | ||
| 3 | |||
| 4 | group = 'com.t3tools.terminal' | ||
| 5 | version = '0.0.0' | ||
| 6 | |||
| 7 | android { | ||
| 8 | namespace 'expo.modules.t3terminal' | ||
| 9 | compileSdk rootProject.ext.compileSdkVersion | ||
| 10 | |||
| 11 | defaultConfig { | ||
| 12 | minSdkVersion rootProject.ext.minSdkVersion | ||
| 13 | targetSdkVersion rootProject.ext.targetSdkVersion | ||
| 14 | } | ||
| 15 | } | ||
| 16 | |||
| 17 | dependencies { | ||
| 18 | implementation project(':expo-modules-core') | ||
| 19 | } | ||
| @@ -0,0 +1 @@ | |||
| 1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" /> | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | package expo.modules.t3terminal | ||
| 2 | |||
| 3 | import expo.modules.kotlin.modules.Module | ||
| 4 | import expo.modules.kotlin.modules.ModuleDefinition | ||
| 5 | |||
| 6 | class T3TerminalModule : Module() { | ||
| 7 | override fun definition() = ModuleDefinition { | ||
| 8 | Name("T3TerminalSurface") | ||
| 9 | |||
| 10 | View(T3TerminalView::class) { | ||
| 11 | Prop("terminalKey") { view: T3TerminalView, terminalKey: String -> | ||
| 12 | view.terminalKey = terminalKey | ||
| 13 | } | ||
| 14 | |||
| 15 | Prop("initialBuffer") { view: T3TerminalView, initialBuffer: String -> | ||
| 16 | view.initialBuffer = initialBuffer | ||
| 17 | } | ||
| 18 | |||
| 19 | Prop("fontSize") { view: T3TerminalView, fontSize: Double -> | ||
| 20 | view.fontSize = fontSize.toFloat() | ||
| 21 | } | ||
| 22 | |||
| 23 | Prop("appearanceScheme") { view: T3TerminalView, appearanceScheme: String -> | ||
| 24 | view.appearanceScheme = appearanceScheme | ||
| 25 | } | ||
| 26 | |||
| 27 | Prop("themeConfig") { view: T3TerminalView, themeConfig: String -> | ||
| 28 | view.themeConfig = themeConfig | ||
| 29 | } | ||
| 30 | |||
| 31 | Prop("backgroundColor") { view: T3TerminalView, backgroundColor: String -> | ||
| 32 | view.backgroundColorHex = backgroundColor | ||
| 33 | } | ||
| 34 | |||
| 35 | Prop("foregroundColor") { view: T3TerminalView, foregroundColor: String -> | ||
| 36 | view.foregroundColorHex = foregroundColor | ||
| 37 | } | ||
| 38 | |||
| 39 | Prop("mutedForegroundColor") { view: T3TerminalView, mutedForegroundColor: String -> | ||
| 40 | view.mutedForegroundColorHex = mutedForegroundColor | ||
| 41 | } | ||
| 42 | |||
| 43 | Events("onInput", "onResize") | ||
| 44 | } | ||
| 45 | } | ||
| 46 | } | ||
| @@ -0,0 +1,210 @@ | |||
| 1 | package expo.modules.t3terminal | ||
| 2 | |||
| 3 | import android.content.Context | ||
| 4 | import android.graphics.Color | ||
| 5 | import android.graphics.Typeface | ||
| 6 | import android.view.View | ||
| 7 | import android.view.ViewGroup | ||
| 8 | import android.view.inputmethod.InputMethodManager | ||
| 9 | import android.view.inputmethod.EditorInfo | ||
| 10 | import android.widget.EditText | ||
| 11 | import android.widget.LinearLayout | ||
| 12 | import android.widget.ScrollView | ||
| 13 | import android.widget.TextView | ||
| 14 | import androidx.core.widget.doAfterTextChanged | ||
| 15 | import expo.modules.kotlin.AppContext | ||
| 16 | import expo.modules.kotlin.views.ExpoView | ||
| 17 | import expo.modules.kotlin.viewevent.EventDispatcher | ||
| 18 | import kotlin.math.max | ||
| 19 | import kotlin.math.min | ||
| 20 | |||
| 21 | class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { | ||
| 22 | private val container = LinearLayout(context) | ||
| 23 | private val scrollView = ScrollView(context) | ||
| 24 | private val textView = TextView(context) | ||
| 25 | private val inputView = EditText(context) | ||
| 26 | private val onInput by EventDispatcher() | ||
| 27 | private val onResize by EventDispatcher() | ||
| 28 | private var lastWidth = 0 | ||
| 29 | private var lastHeight = 0 | ||
| 30 | private var clearingInput = false | ||
| 31 | private var backgroundColorValue = Color.parseColor("#24292E") | ||
| 32 | private var foregroundColorValue = Color.parseColor("#D1D5DA") | ||
| 33 | private var mutedForegroundColorValue = Color.parseColor("#959DA5") | ||
| 34 | |||
| 35 | var terminalKey: String = "" | ||
| 36 | set(value) { | ||
| 37 | field = value | ||
| 38 | contentDescription = "t3-terminal-$value" | ||
| 39 | } | ||
| 40 | |||
| 41 | var initialBuffer: String = "" | ||
| 42 | set(value) { | ||
| 43 | field = value | ||
| 44 | textView.text = value.ifEmpty { "$ " } | ||
| 45 | scrollView.post { | ||
| 46 | scrollView.fullScroll(View.FOCUS_DOWN) | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | var fontSize: Float = 12f | ||
| 51 | set(value) { | ||
| 52 | field = value | ||
| 53 | textView.textSize = value | ||
| 54 | emitResize() | ||
| 55 | } | ||
| 56 | |||
| 57 | var appearanceScheme: String = "dark" | ||
| 58 | set(value) { | ||
| 59 | field = value | ||
| 60 | } | ||
| 61 | |||
| 62 | var themeConfig: String = "" | ||
| 63 | |||
| 64 | var backgroundColorHex: String = "#24292E" | ||
| 65 | set(value) { | ||
| 66 | field = value | ||
| 67 | backgroundColorValue = parseColor(value, backgroundColorValue) | ||
| 68 | applyTheme() | ||
| 69 | } | ||
| 70 | |||
| 71 | var foregroundColorHex: String = "#D1D5DA" | ||
| 72 | set(value) { | ||
| 73 | field = value | ||
| 74 | foregroundColorValue = parseColor(value, foregroundColorValue) | ||
| 75 | applyTheme() | ||
| 76 | } | ||
| 77 | |||
| 78 | var mutedForegroundColorHex: String = "#959DA5" | ||
| 79 | set(value) { | ||
| 80 | field = value | ||
| 81 | mutedForegroundColorValue = parseColor(value, mutedForegroundColorValue) | ||
| 82 | applyTheme() | ||
| 83 | } | ||
| 84 | |||
| 85 | init { | ||
| 86 | applyTheme() | ||
| 87 | container.orientation = LinearLayout.VERTICAL | ||
| 88 | textView.typeface = Typeface.MONOSPACE | ||
| 89 | textView.textSize = fontSize | ||
| 90 | textView.setPadding(8, 8, 8, 8) | ||
| 91 | textView.text = "$ " | ||
| 92 | |||
| 93 | inputView.setSingleLine(true) | ||
| 94 | inputView.setTextColor(Color.TRANSPARENT) | ||
| 95 | inputView.setHintTextColor(Color.TRANSPARENT) | ||
| 96 | inputView.setBackgroundColor(Color.TRANSPARENT) | ||
| 97 | inputView.typeface = Typeface.MONOSPACE | ||
| 98 | inputView.textSize = max(fontSize, 13f) | ||
| 99 | inputView.hint = "" | ||
| 100 | inputView.alpha = 0.02f | ||
| 101 | inputView.imeOptions = EditorInfo.IME_ACTION_SEND | ||
| 102 | inputView.setPadding(0, 0, 0, 0) | ||
| 103 | inputView.setOnFocusChangeListener { _, hasFocus -> | ||
| 104 | if (hasFocus) { | ||
| 105 | showKeyboard() | ||
| 106 | } | ||
| 107 | } | ||
| 108 | inputView.setOnEditorActionListener { view, actionId, _ -> | ||
| 109 | if (actionId != EditorInfo.IME_ACTION_SEND) return@setOnEditorActionListener false | ||
| 110 | onInput(mapOf("data" to "\n")) | ||
| 111 | true | ||
| 112 | } | ||
| 113 | inputView.setOnKeyListener { _, keyCode, event -> | ||
| 114 | if (event.action != android.view.KeyEvent.ACTION_DOWN) return@setOnKeyListener false | ||
| 115 | when (keyCode) { | ||
| 116 | android.view.KeyEvent.KEYCODE_DEL -> { | ||
| 117 | onInput(mapOf("data" to "\u007F")) | ||
| 118 | true | ||
| 119 | } | ||
| 120 | else -> false | ||
| 121 | } | ||
| 122 | } | ||
| 123 | inputView.doAfterTextChanged { editable -> | ||
| 124 | if (clearingInput) return@doAfterTextChanged | ||
| 125 | val text = editable?.toString().orEmpty() | ||
| 126 | if (text.isEmpty()) return@doAfterTextChanged | ||
| 127 | onInput(mapOf("data" to text)) | ||
| 128 | clearingInput = true | ||
| 129 | inputView.text?.clear() | ||
| 130 | clearingInput = false | ||
| 131 | } | ||
| 132 | |||
| 133 | textView.setOnClickListener { requestKeyboardFocus() } | ||
| 134 | scrollView.setOnClickListener { requestKeyboardFocus() } | ||
| 135 | container.setOnClickListener { requestKeyboardFocus() } | ||
| 136 | isClickable = true | ||
| 137 | setOnClickListener { requestKeyboardFocus() } | ||
| 138 | |||
| 139 | scrollView.addView( | ||
| 140 | textView, | ||
| 141 | LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT), | ||
| 142 | ) | ||
| 143 | container.addView( | ||
| 144 | scrollView, | ||
| 145 | LinearLayout.LayoutParams( | ||
| 146 | ViewGroup.LayoutParams.MATCH_PARENT, | ||
| 147 | 0, | ||
| 148 | 1f, | ||
| 149 | ), | ||
| 150 | ) | ||
| 151 | container.addView( | ||
| 152 | inputView, | ||
| 153 | LinearLayout.LayoutParams(1, 1), | ||
| 154 | ) | ||
| 155 | addView( | ||
| 156 | container, | ||
| 157 | LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT), | ||
| 158 | ) | ||
| 159 | |||
| 160 | post { | ||
| 161 | requestKeyboardFocus() | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { | ||
| 166 | super.onSizeChanged(width, height, oldWidth, oldHeight) | ||
| 167 | if (width == lastWidth && height == lastHeight) return | ||
| 168 | lastWidth = width | ||
| 169 | lastHeight = height | ||
| 170 | emitResize() | ||
| 171 | } | ||
| 172 | |||
| 173 | private fun emitResize() { | ||
| 174 | if (width <= 0 || height <= 0) return | ||
| 175 | val density = resources.displayMetrics.scaledDensity | ||
| 176 | val fontPx = max(fontSize * density, 1f) | ||
| 177 | val cols = max(20, min(400, (width / (fontPx * 0.62f)).toInt())) | ||
| 178 | val terminalHeight = max(height - inputView.height, 0) | ||
| 179 | val rows = max(5, min(200, (terminalHeight / (fontPx * 1.35f)).toInt())) | ||
| 180 | onResize(mapOf("cols" to cols, "rows" to rows)) | ||
| 181 | } | ||
| 182 | |||
| 183 | private fun requestKeyboardFocus() { | ||
| 184 | inputView.requestFocus() | ||
| 185 | showKeyboard() | ||
| 186 | } | ||
| 187 | |||
| 188 | private fun applyTheme() { | ||
| 189 | setBackgroundColor(backgroundColorValue) | ||
| 190 | container.setBackgroundColor(backgroundColorValue) | ||
| 191 | scrollView.setBackgroundColor(backgroundColorValue) | ||
| 192 | textView.setTextColor(foregroundColorValue) | ||
| 193 | textView.setBackgroundColor(backgroundColorValue) | ||
| 194 | inputView.setTextColor(Color.TRANSPARENT) | ||
| 195 | inputView.setHintTextColor(mutedForegroundColorValue) | ||
| 196 | inputView.setBackgroundColor(Color.TRANSPARENT) | ||
| 197 | } | ||
| 198 | |||
| 199 | private fun parseColor(value: String, fallback: Int): Int = | ||
| 200 | try { | ||
| 201 | Color.parseColor(value) | ||
| 202 | } catch (_: IllegalArgumentException) { | ||
| 203 | fallback | ||
| 204 | } | ||
| 205 | |||
| 206 | private fun showKeyboard() { | ||
| 207 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager | ||
| 208 | imm?.showSoftInput(inputView, InputMethodManager.SHOW_IMPLICIT) | ||
| 209 | } | ||
| 210 | } | ||
| @@ -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 | } | ||
| @@ -0,0 +1,43 @@ | |||
| 1 | import ExpoModulesCore | ||
| 2 | |||
| 3 | public class T3TerminalModule: Module { | ||
| 4 | public func definition() -> ModuleDefinition { | ||
| 5 | Name("T3TerminalSurface") | ||
| 6 | |||
| 7 | View(T3TerminalView.self) { | ||
| 8 | Prop("terminalKey") { (view: T3TerminalView, terminalKey: String) in | ||
| 9 | view.terminalKey = terminalKey | ||
| 10 | } | ||
| 11 | |||
| 12 | Prop("initialBuffer") { (view: T3TerminalView, initialBuffer: String) in | ||
| 13 | view.initialBuffer = initialBuffer | ||
| 14 | } | ||
| 15 | |||
| 16 | Prop("fontSize") { (view: T3TerminalView, fontSize: Double) in | ||
| 17 | view.fontSize = CGFloat(fontSize) | ||
| 18 | } | ||
| 19 | |||
| 20 | Prop("appearanceScheme") { (view: T3TerminalView, appearanceScheme: String) in | ||
| 21 | view.appearanceScheme = appearanceScheme | ||
| 22 | } | ||
| 23 | |||
| 24 | Prop("themeConfig") { (view: T3TerminalView, themeConfig: String) in | ||
| 25 | view.themeConfig = themeConfig | ||
| 26 | } | ||
| 27 | |||
| 28 | Prop("backgroundColor") { (view: T3TerminalView, backgroundColor: String) in | ||
| 29 | view.backgroundColorHex = backgroundColor | ||
| 30 | } | ||
| 31 | |||
| 32 | Prop("foregroundColor") { (view: T3TerminalView, foregroundColor: String) in | ||
| 33 | view.foregroundColorHex = foregroundColor | ||
| 34 | } | ||
| 35 | |||
| 36 | Prop("mutedForegroundColor") { (view: T3TerminalView, mutedForegroundColor: String) in | ||
| 37 | view.mutedForegroundColorHex = mutedForegroundColor | ||
| 38 | } | ||
| 39 | |||
| 40 | Events("onInput", "onResize") | ||
| 41 | } | ||
| 42 | } | ||
| 43 | } | ||
| @@ -0,0 +1,565 @@ | |||
| 1 | import ExpoModulesCore | ||
| 2 | import Foundation | ||
| 3 | import GhosttyKit | ||
| 4 | import QuartzCore | ||
| 5 | import UIKit | ||
| 6 | |||
| 7 | private enum GhosttyRuntime { | ||
| 8 | private static let lock = NSLock() | ||
| 9 | private static var initialized = false | ||
| 10 | |||
| 11 | static func ensureInitialized() -> Bool { | ||
| 12 | lock.lock() | ||
| 13 | defer { lock.unlock() } | ||
| 14 | |||
| 15 | if initialized { | ||
| 16 | return true | ||
| 17 | } | ||
| 18 | |||
| 19 | let result = ghostty_init(0, nil) | ||
| 20 | initialized = result == GHOSTTY_SUCCESS | ||
| 21 | return initialized | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | private final class TerminalInputField: UITextField { | ||
| 26 | var onDeleteBackward: (() -> Void)? | ||
| 27 | |||
| 28 | override func deleteBackward() { | ||
| 29 | onDeleteBackward?() | ||
| 30 | super.deleteBackward() | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | private enum TerminalAppearanceScheme: String { | ||
| 35 | case light | ||
| 36 | case dark | ||
| 37 | |||
| 38 | init(value: String) { | ||
| 39 | self = TerminalAppearanceScheme(rawValue: value) ?? .dark | ||
| 40 | } | ||
| 41 | |||
| 42 | var ghosttyColorScheme: ghostty_color_scheme_e { | ||
| 43 | switch self { | ||
| 44 | case .light: | ||
| 45 | return GHOSTTY_COLOR_SCHEME_LIGHT | ||
| 46 | case .dark: | ||
| 47 | return GHOSTTY_COLOR_SCHEME_DARK | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | private extension UIColor { | ||
| 53 | convenience init(hexString: String) { | ||
| 54 | let sanitized = hexString.replacingOccurrences(of: "#", with: "") | ||
| 55 | let value = Int(sanitized, radix: 16) ?? 0 | ||
| 56 | self.init( | ||
| 57 | red: CGFloat((value >> 16) & 0xFF) / 255, | ||
| 58 | green: CGFloat((value >> 8) & 0xFF) / 255, | ||
| 59 | blue: CGFloat(value & 0xFF) / 255, | ||
| 60 | alpha: 1 | ||
| 61 | ) | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | public final class T3TerminalView: ExpoView, UITextFieldDelegate { | ||
| 66 | private static let minimumVerticalScrollStepPoints: CGFloat = 18 | ||
| 67 | private static let verticalScrollStepMultiplier: CGFloat = 1.15 | ||
| 68 | |||
| 69 | private let terminalViewport = UIView() | ||
| 70 | private let inputField = TerminalInputField() | ||
| 71 | private let focusTapGesture = UITapGestureRecognizer() | ||
| 72 | private let scrollPanGesture = UIPanGestureRecognizer() | ||
| 73 | private var lastViewportSize: CGSize = .zero | ||
| 74 | private var lastContentScale: CGFloat = 0 | ||
| 75 | private var lastReportedGrid: (cols: Int, rows: Int)? | ||
| 76 | private var lastAppliedBuffer = "" | ||
| 77 | private var pendingVerticalScrollPoints: CGFloat = 0 | ||
| 78 | private var app: ghostty_app_t? | ||
| 79 | private var surface: ghostty_surface_t? | ||
| 80 | private var isCreatingSurface = false | ||
| 81 | private var surfaceCreationFailed = false | ||
| 82 | private var appearance = TerminalAppearanceScheme.dark | ||
| 83 | private var backgroundColorValue = UIColor(hexString: "#24292e") | ||
| 84 | |||
| 85 | let onInput = EventDispatcher() | ||
| 86 | let onResize = EventDispatcher() | ||
| 87 | |||
| 88 | var terminalKey: String = "" { | ||
| 89 | didSet { | ||
| 90 | accessibilityIdentifier = "t3-terminal-\(terminalKey)" | ||
| 91 | if oldValue != terminalKey { | ||
| 92 | resetSurface() | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | var initialBuffer: String = "" { | ||
| 98 | didSet { | ||
| 99 | applyRemoteBuffer(initialBuffer) | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | var fontSize: CGFloat = 12 { | ||
| 104 | didSet { | ||
| 105 | inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular) | ||
| 106 | resetSurface() | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue { | ||
| 111 | didSet { | ||
| 112 | appearance = TerminalAppearanceScheme(value: appearanceScheme) | ||
| 113 | updateGhosttyTheme() | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | var themeConfig: String = "" { | ||
| 118 | didSet { | ||
| 119 | updateGhosttyTheme() | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | var backgroundColorHex: String = "#24292e" { | ||
| 124 | didSet { | ||
| 125 | backgroundColorValue = UIColor(hexString: backgroundColorHex) | ||
| 126 | applyTheme() | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | var foregroundColorHex: String = "#d1d5da" | ||
| 131 | var mutedForegroundColorHex: String = "#959da5" | ||
| 132 | |||
| 133 | public required init(appContext: AppContext? = nil) { | ||
| 134 | super.init(appContext: appContext) | ||
| 135 | |||
| 136 | applyTheme() | ||
| 137 | clipsToBounds = true | ||
| 138 | contentScaleFactor = UIScreen.main.scale | ||
| 139 | |||
| 140 | terminalViewport.clipsToBounds = true | ||
| 141 | terminalViewport.contentScaleFactor = contentScaleFactor | ||
| 142 | terminalViewport.translatesAutoresizingMaskIntoConstraints = false | ||
| 143 | terminalViewport.isUserInteractionEnabled = true | ||
| 144 | |||
| 145 | inputField.delegate = self | ||
| 146 | inputField.backgroundColor = UIColor.clear | ||
| 147 | inputField.textColor = UIColor.clear | ||
| 148 | inputField.tintColor = UIColor.clear | ||
| 149 | inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular) | ||
| 150 | inputField.placeholder = "" | ||
| 151 | inputField.autocorrectionType = .no | ||
| 152 | inputField.autocapitalizationType = .none | ||
| 153 | inputField.spellCheckingType = .no | ||
| 154 | inputField.smartDashesType = .no | ||
| 155 | inputField.smartQuotesType = .no | ||
| 156 | inputField.returnKeyType = .send | ||
| 157 | inputField.keyboardType = .asciiCapable | ||
| 158 | inputField.enablesReturnKeyAutomatically = false | ||
| 159 | inputField.translatesAutoresizingMaskIntoConstraints = false | ||
| 160 | inputField.alpha = 0.02 | ||
| 161 | inputField.isAccessibilityElement = false | ||
| 162 | inputField.accessibilityElementsHidden = true | ||
| 163 | inputField.addTarget(self, action: #selector(handleInputEditingDidBegin), for: .editingDidBegin) | ||
| 164 | inputField.onDeleteBackward = { [weak self] in | ||
| 165 | self?.emitInput("\u{7F}") | ||
| 166 | } | ||
| 167 | |||
| 168 | focusTapGesture.addTarget(self, action: #selector(handleViewportTap)) | ||
| 169 | terminalViewport.addGestureRecognizer(focusTapGesture) | ||
| 170 | scrollPanGesture.addTarget(self, action: #selector(handleViewportPan(_:))) | ||
| 171 | scrollPanGesture.maximumNumberOfTouches = 1 | ||
| 172 | scrollPanGesture.cancelsTouchesInView = false | ||
| 173 | terminalViewport.addGestureRecognizer(scrollPanGesture) | ||
| 174 | |||
| 175 | addSubview(terminalViewport) | ||
| 176 | addSubview(inputField) | ||
| 177 | |||
| 178 | NSLayoutConstraint.activate([ | ||
| 179 | terminalViewport.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
| 180 | terminalViewport.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
| 181 | terminalViewport.topAnchor.constraint(equalTo: topAnchor), | ||
| 182 | terminalViewport.bottomAnchor.constraint(equalTo: bottomAnchor), | ||
| 183 | |||
| 184 | inputField.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
| 185 | inputField.topAnchor.constraint(equalTo: bottomAnchor, constant: 8), | ||
| 186 | inputField.widthAnchor.constraint(equalToConstant: 1), | ||
| 187 | inputField.heightAnchor.constraint(equalToConstant: 1), | ||
| 188 | ]) | ||
| 189 | } | ||
| 190 | |||
| 191 | deinit { | ||
| 192 | destroySurface() | ||
| 193 | } | ||
| 194 | |||
| 195 | public override func layoutSubviews() { | ||
| 196 | super.layoutSubviews() | ||
| 197 | updateContentScale() | ||
| 198 | |||
| 199 | let viewportSize = terminalViewport.bounds.size | ||
| 200 | if surface == nil { | ||
| 201 | createSurfaceIfPossible() | ||
| 202 | } | ||
| 203 | |||
| 204 | guard viewportSize != lastViewportSize || contentScaleFactor != lastContentScale else { | ||
| 205 | return | ||
| 206 | } | ||
| 207 | |||
| 208 | lastViewportSize = viewportSize | ||
| 209 | lastContentScale = contentScaleFactor | ||
| 210 | resizeSurface() | ||
| 211 | } | ||
| 212 | |||
| 213 | public override func didMoveToWindow() { | ||
| 214 | super.didMoveToWindow() | ||
| 215 | |||
| 216 | guard window != nil else { return } | ||
| 217 | DispatchQueue.main.async { [weak self] in | ||
| 218 | self?.requestKeyboardFocus() | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | ||
| 223 | if !string.isEmpty { | ||
| 224 | emitInput(string) | ||
| 225 | return false | ||
| 226 | } | ||
| 227 | |||
| 228 | return false | ||
| 229 | } | ||
| 230 | |||
| 231 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { | ||
| 232 | emitInput("\n") | ||
| 233 | textField.text = "" | ||
| 234 | return false | ||
| 235 | } | ||
| 236 | |||
| 237 | @objc | ||
| 238 | private func handleViewportTap() { | ||
| 239 | requestKeyboardFocus() | ||
| 240 | } | ||
| 241 | |||
| 242 | @objc | ||
| 243 | private func handleViewportPan(_ gesture: UIPanGestureRecognizer) { | ||
| 244 | guard let surface else { return } | ||
| 245 | |||
| 246 | let location = gesture.location(in: terminalViewport) | ||
| 247 | ghostty_surface_mouse_pos( | ||
| 248 | surface, | ||
| 249 | Double(location.x * contentScaleFactor), | ||
| 250 | Double(location.y * contentScaleFactor), | ||
| 251 | GHOSTTY_MODS_NONE | ||
| 252 | ) | ||
| 253 | |||
| 254 | switch gesture.state { | ||
| 255 | case .began: | ||
| 256 | pendingVerticalScrollPoints = 0 | ||
| 257 | gesture.setTranslation(.zero, in: terminalViewport) | ||
| 258 | case .changed: | ||
| 259 | let translation = gesture.translation(in: terminalViewport) | ||
| 260 | let stepSize = max( | ||
| 261 | fontSize * Self.verticalScrollStepMultiplier, | ||
| 262 | Self.minimumVerticalScrollStepPoints | ||
| 263 | ) | ||
| 264 | let totalVerticalPoints = pendingVerticalScrollPoints + translation.y | ||
| 265 | let verticalSteps = Int(totalVerticalPoints / stepSize) | ||
| 266 | pendingVerticalScrollPoints = totalVerticalPoints - (CGFloat(verticalSteps) * stepSize) | ||
| 267 | |||
| 268 | guard verticalSteps != 0 else { | ||
| 269 | gesture.setTranslation(.zero, in: terminalViewport) | ||
| 270 | return | ||
| 271 | } | ||
| 272 | |||
| 273 | ghostty_surface_mouse_scroll(surface, 0, Double(verticalSteps), 0) | ||
| 274 | redrawSurface() | ||
| 275 | gesture.setTranslation(.zero, in: terminalViewport) | ||
| 276 | default: | ||
| 277 | pendingVerticalScrollPoints = 0 | ||
| 278 | gesture.setTranslation(.zero, in: terminalViewport) | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | @objc | ||
| 283 | private func handleInputEditingDidBegin() { | ||
| 284 | textInputModeDidChange() | ||
| 285 | } | ||
| 286 | |||
| 287 | private func createSurfaceIfPossible() { | ||
| 288 | guard surface == nil, app == nil, !isCreatingSurface, !surfaceCreationFailed else { return } | ||
| 289 | guard terminalViewport.bounds.width > 0, terminalViewport.bounds.height > 0 else { return } | ||
| 290 | guard GhosttyRuntime.ensureInitialized() else { | ||
| 291 | surfaceCreationFailed = true | ||
| 292 | return | ||
| 293 | } | ||
| 294 | |||
| 295 | isCreatingSurface = true | ||
| 296 | defer { isCreatingSurface = false } | ||
| 297 | |||
| 298 | var runtimeConfig = ghostty_runtime_config_s( | ||
| 299 | userdata: Unmanaged.passUnretained(self).toOpaque(), | ||
| 300 | supports_selection_clipboard: false, | ||
| 301 | wakeup_cb: { _ in }, | ||
| 302 | action_cb: { _, _, _ in false }, | ||
| 303 | read_clipboard_cb: { _, _, _ in }, | ||
| 304 | confirm_read_clipboard_cb: { _, _, _, _ in }, | ||
| 305 | write_clipboard_cb: { _, _, _, _, _ in }, | ||
| 306 | close_surface_cb: { _, _ in } | ||
| 307 | ) | ||
| 308 | |||
| 309 | guard let config = ghostty_config_new() else { | ||
| 310 | surfaceCreationFailed = true | ||
| 311 | return | ||
| 312 | } | ||
| 313 | loadThemeConfig(into: config) | ||
| 314 | ghostty_config_finalize(config) | ||
| 315 | defer { ghostty_config_free(config) } | ||
| 316 | |||
| 317 | guard let createdApp = ghostty_app_new(&runtimeConfig, config) else { | ||
| 318 | surfaceCreationFailed = true | ||
| 319 | return | ||
| 320 | } | ||
| 321 | |||
| 322 | var surfaceConfig = ghostty_surface_config_new() | ||
| 323 | surfaceConfig.platform_tag = GHOSTTY_PLATFORM_IOS | ||
| 324 | surfaceConfig.platform.ios.uiview = Unmanaged.passUnretained(terminalViewport).toOpaque() | ||
| 325 | surfaceConfig.userdata = Unmanaged.passUnretained(self).toOpaque() | ||
| 326 | surfaceConfig.scale_factor = Double(contentScaleFactor) | ||
| 327 | surfaceConfig.font_size = Float(fontSize) | ||
| 328 | surfaceConfig.context = GHOSTTY_SURFACE_CONTEXT_WINDOW | ||
| 329 | surfaceConfig.use_custom_io = true | ||
| 330 | |||
| 331 | guard let createdSurface = ghostty_surface_new(createdApp, &surfaceConfig) else { | ||
| 332 | ghostty_app_free(createdApp) | ||
| 333 | surfaceCreationFailed = true | ||
| 334 | return | ||
| 335 | } | ||
| 336 | |||
| 337 | app = createdApp | ||
| 338 | surface = createdSurface | ||
| 339 | ghostty_app_set_color_scheme(createdApp, appearance.ghosttyColorScheme) | ||
| 340 | ghostty_surface_set_color_scheme(createdSurface, appearance.ghosttyColorScheme) | ||
| 341 | setupWriteCallback() | ||
| 342 | resizeSurface() | ||
| 343 | feedBuffer(initialBuffer) | ||
| 344 | } | ||
| 345 | |||
| 346 | private func resetSurface() { | ||
| 347 | destroySurface() | ||
| 348 | lastAppliedBuffer = "" | ||
| 349 | lastViewportSize = .zero | ||
| 350 | lastContentScale = 0 | ||
| 351 | lastReportedGrid = nil | ||
| 352 | surfaceCreationFailed = false | ||
| 353 | setNeedsLayout() | ||
| 354 | } | ||
| 355 | |||
| 356 | private func destroySurface() { | ||
| 357 | if let surface { | ||
| 358 | ghostty_surface_set_write_callback(surface, nil, nil) | ||
| 359 | ghostty_surface_free(surface) | ||
| 360 | } | ||
| 361 | if let app { | ||
| 362 | ghostty_app_free(app) | ||
| 363 | } | ||
| 364 | surface = nil | ||
| 365 | app = nil | ||
| 366 | } | ||
| 367 | |||
| 368 | private func applyRemoteBuffer(_ buffer: String) { | ||
| 369 | guard surface != nil else { | ||
| 370 | createSurfaceIfPossible() | ||
| 371 | return | ||
| 372 | } | ||
| 373 | |||
| 374 | if buffer.hasPrefix(lastAppliedBuffer) { | ||
| 375 | let suffix = String(buffer.dropFirst(lastAppliedBuffer.count)) | ||
| 376 | feedData(Data(suffix.utf8)) | ||
| 377 | lastAppliedBuffer = buffer | ||
| 378 | return | ||
| 379 | } | ||
| 380 | |||
| 381 | resetSurface() | ||
| 382 | createSurfaceIfPossible() | ||
| 383 | } | ||
| 384 | |||
| 385 | private func feedBuffer(_ buffer: String) { | ||
| 386 | guard !buffer.isEmpty else { return } | ||
| 387 | feedData(Data(buffer.utf8)) | ||
| 388 | lastAppliedBuffer = buffer | ||
| 389 | } | ||
| 390 | |||
| 391 | private func feedData(_ data: Data) { | ||
| 392 | guard let surface, !data.isEmpty else { return } | ||
| 393 | |||
| 394 | data.withUnsafeBytes { buffer in | ||
| 395 | guard let pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { | ||
| 396 | return | ||
| 397 | } | ||
| 398 | ghostty_surface_feed_data(surface, pointer, buffer.count) | ||
| 399 | } | ||
| 400 | |||
| 401 | redrawSurface() | ||
| 402 | } | ||
| 403 | |||
| 404 | private func setupWriteCallback() { | ||
| 405 | guard let surface else { return } | ||
| 406 | |||
| 407 | let userdata = Unmanaged.passUnretained(self).toOpaque() | ||
| 408 | ghostty_surface_set_write_callback(surface, { userdata, data, len in | ||
| 409 | guard let userdata, let data, len > 0 else { return } | ||
| 410 | let view = Unmanaged<T3TerminalView>.fromOpaque(userdata).takeUnretainedValue() | ||
| 411 | let bytes = Data(bytes: data, count: len) | ||
| 412 | guard let input = String(data: bytes, encoding: .utf8), !input.isEmpty else { return } | ||
| 413 | |||
| 414 | DispatchQueue.main.async { | ||
| 415 | view.onInput(["data": input]) | ||
| 416 | } | ||
| 417 | }, userdata) | ||
| 418 | } | ||
| 419 | |||
| 420 | private func resizeSurface() { | ||
| 421 | guard let surface else { | ||
| 422 | emitEstimatedResize() | ||
| 423 | return | ||
| 424 | } | ||
| 425 | |||
| 426 | let scale = contentScaleFactor | ||
| 427 | let width = UInt32(max(floor(terminalViewport.bounds.width * scale), 1)) | ||
| 428 | let height = UInt32(max(floor(terminalViewport.bounds.height * scale), 1)) | ||
| 429 | |||
| 430 | terminalViewport.contentScaleFactor = scale | ||
| 431 | ghostty_surface_set_content_scale(surface, Double(scale), Double(scale)) | ||
| 432 | ghostty_surface_set_size(surface, width, height) | ||
| 433 | ghostty_surface_set_occlusion(surface, window != nil) | ||
| 434 | configureIOSurfaceLayers() | ||
| 435 | redrawSurface() | ||
| 436 | emitGhosttyResize() | ||
| 437 | } | ||
| 438 | |||
| 439 | private func redrawSurface() { | ||
| 440 | guard let surface else { return } | ||
| 441 | ghostty_surface_refresh(surface) | ||
| 442 | ghostty_surface_draw(surface) | ||
| 443 | markIOSurfaceLayersForDisplay() | ||
| 444 | emitGhosttyResize() | ||
| 445 | } | ||
| 446 | |||
| 447 | private func emitGhosttyResize() { | ||
| 448 | guard let surface else { | ||
| 449 | emitEstimatedResize() | ||
| 450 | return | ||
| 451 | } | ||
| 452 | |||
| 453 | let size = ghostty_surface_size(surface) | ||
| 454 | let cols = max(1, Int(size.columns)) | ||
| 455 | let rows = max(1, Int(size.rows)) | ||
| 456 | emitResize(cols: cols, rows: rows) | ||
| 457 | } | ||
| 458 | |||
| 459 | private func emitEstimatedResize() { | ||
| 460 | guard bounds.width > 0, bounds.height > 0 else { return } | ||
| 461 | |||
| 462 | let cellWidth = max(fontSize * 0.62, 1) | ||
| 463 | let cellHeight = max(fontSize * 1.35, 1) | ||
| 464 | let cols = max(20, min(400, Int(bounds.width / cellWidth))) | ||
| 465 | let terminalHeight = max(bounds.height, 0) | ||
| 466 | let rows = max(5, min(200, Int(terminalHeight / cellHeight))) | ||
| 467 | emitResize(cols: cols, rows: rows) | ||
| 468 | } | ||
| 469 | |||
| 470 | private func emitResize(cols: Int, rows: Int) { | ||
| 471 | guard lastReportedGrid?.cols != cols || lastReportedGrid?.rows != rows else { | ||
| 472 | return | ||
| 473 | } | ||
| 474 | |||
| 475 | lastReportedGrid = (cols, rows) | ||
| 476 | onResize([ | ||
| 477 | "cols": cols, | ||
| 478 | "rows": rows, | ||
| 479 | ]) | ||
| 480 | } | ||
| 481 | |||
| 482 | private func updateContentScale() { | ||
| 483 | let scale = window?.screen.scale ?? UIScreen.main.scale | ||
| 484 | if contentScaleFactor != scale { | ||
| 485 | contentScaleFactor = scale | ||
| 486 | } | ||
| 487 | } | ||
| 488 | |||
| 489 | private func requestKeyboardFocus() { | ||
| 490 | guard window != nil else { return } | ||
| 491 | inputField.becomeFirstResponder() | ||
| 492 | textInputModeDidChange() | ||
| 493 | } | ||
| 494 | |||
| 495 | private func emitInput(_ data: String) { | ||
| 496 | guard !data.isEmpty else { return } | ||
| 497 | onInput(["data": data]) | ||
| 498 | } | ||
| 499 | |||
| 500 | private func textInputModeDidChange() { | ||
| 501 | guard let app else { return } | ||
| 502 | ghostty_app_keyboard_changed(app) | ||
| 503 | } | ||
| 504 | |||
| 505 | private func configureIOSurfaceLayers() { | ||
| 506 | let targetBounds = CGRect(origin: .zero, size: terminalViewport.bounds.size) | ||
| 507 | CATransaction.begin() | ||
| 508 | CATransaction.setDisableActions(true) | ||
| 509 | terminalViewport.layer.sublayers?.forEach { sublayer in | ||
| 510 | sublayer.frame = targetBounds | ||
| 511 | sublayer.contentsScale = contentScaleFactor | ||
| 512 | } | ||
| 513 | CATransaction.commit() | ||
| 514 | } | ||
| 515 | |||
| 516 | private func markIOSurfaceLayersForDisplay() { | ||
| 517 | terminalViewport.layer.setNeedsDisplay() | ||
| 518 | terminalViewport.layer.sublayers?.forEach { layer in | ||
| 519 | layer.setNeedsDisplay() | ||
| 520 | } | ||
| 521 | } | ||
| 522 | |||
| 523 | private func applyTheme() { | ||
| 524 | backgroundColor = backgroundColorValue | ||
| 525 | terminalViewport.backgroundColor = backgroundColorValue | ||
| 526 | } | ||
| 527 | |||
| 528 | private func updateGhosttyTheme() { | ||
| 529 | guard let app, let surface else { return } | ||
| 530 | guard let config = ghostty_config_new() else { return } | ||
| 531 | loadThemeConfig(into: config) | ||
| 532 | ghostty_config_finalize(config) | ||
| 533 | ghostty_app_update_config(app, config) | ||
| 534 | ghostty_surface_update_config(surface, config) | ||
| 535 | ghostty_app_set_color_scheme(app, appearance.ghosttyColorScheme) | ||
| 536 | ghostty_surface_set_color_scheme(surface, appearance.ghosttyColorScheme) | ||
| 537 | ghostty_config_free(config) | ||
| 538 | redrawSurface() | ||
| 539 | } | ||
| 540 | |||
| 541 | private func loadThemeConfig(into config: ghostty_config_t) { | ||
| 542 | guard let path = writeThemeConfigFile() else { return } | ||
| 543 | path.withCString { cString in | ||
| 544 | ghostty_config_load_file(config, cString) | ||
| 545 | } | ||
| 546 | } | ||
| 547 | |||
| 548 | private func writeThemeConfigFile() -> String? { | ||
| 549 | guard !themeConfig.isEmpty else { return nil } | ||
| 550 | let configContents = themeConfig | ||
| 551 | let url = URL(fileURLWithPath: NSTemporaryDirectory()) | ||
| 552 | .appendingPathComponent("t3-terminal-theme-\(appearance.rawValue).ghostty") | ||
| 553 | |||
| 554 | do { | ||
| 555 | if let existing = try? String(contentsOf: url, encoding: .utf8), existing == configContents { | ||
| 556 | return url.path | ||
| 557 | } | ||
| 558 | |||
| 559 | try configContents.write(to: url, atomically: true, encoding: .utf8) | ||
| 560 | return url.path | ||
| 561 | } catch { | ||
| 562 | return nil | ||
| 563 | } | ||
| 564 | } | ||
| 565 | } | ||
| @@ -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 | } | ||
| @@ -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", |
| @@ -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 | } |
| @@ -0,0 +1,5 @@ | |||
| 1 | import { ThreadTerminalRouteScreen } from "../../../../features/threads/ThreadTerminalRouteScreen"; | ||
| 2 | |||
| 3 | export default function ThreadTerminalRoute() { | ||
| 4 | return <ThreadTerminalRouteScreen />; | ||
| 5 | } | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | import type { GitRunStackedActionResult, GitStatusResult } from "@t3tools/contracts"; | 1 | import type { GitRunStackedActionResult, GitStatusResult, ProjectScript } from "@t3tools/contracts"; |
| 2 | import { | 2 | import { |
| 3 | type GitActionRequestInput, | 3 | type GitActionRequestInput, |
| 4 | requiresDefaultBranchConfirmation, | 4 | requiresDefaultBranchConfirmation, |
| @@ -9,6 +9,14 @@ import Stack from "expo-router/stack"; · 4 unmodified lines | |||
| 9 | import { useCallback, useMemo } from "react"; | 9 | import { useCallback, useMemo } from "react"; |
| 10 | import { Alert, Linking } from "react-native"; | 10 | import { Alert, Linking } from "react-native"; |
| 11 | import { buildThreadReviewRoutePath } from "../../lib/routes"; | 11 | import { buildThreadReviewRoutePath } from "../../lib/routes"; |
| 12 | import { | ||
| 13 | basename, | ||
| 14 | getTerminalLabel, | ||
| 15 | getTerminalStatusLabel, | ||
| 16 | projectScriptMenuIcon, | ||
| 17 | projectScriptMenuLabel, | ||
| 18 | type TerminalMenuSession, | ||
| 19 | } from "./terminalMenu"; | ||
| 12 | 20 | ||
| 13 | function truncateMiddle(value: string, maxLength: number): string { | 21 | function 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.MenuAction | 233 | <Stack.Toolbar.MenuAction |
| 172 | icon="point.topleft.down.curvedto.point.bottomright.up" | 234 | icon="point.topleft.down.curvedto.point.bottomright.up" |
| @@ -3,6 +3,8 @@ import { useCallback, useMemo, useState } from "react"; · 2 unmodified lines | |||
| 3 | import * as Arr from "effect/Array"; | 3 | import * as Arr from "effect/Array"; |
| 4 | import * as Option from "effect/Option"; | 4 | import * as Option from "effect/Option"; |
| 5 | import { pipe } from "effect/Function"; | 5 | import { pipe } from "effect/Function"; |
| 6 | import type { ProjectScript } from "@t3tools/contracts"; | ||
| 7 | import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; | ||
| 6 | import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native"; | 8 | import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native"; |
| 7 | import { useThemeColor } from "../../lib/useThemeColor"; | 9 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 8 | import { useGitStatus, gitStatusManager } from "../../state/use-git-status"; | 10 | import { useGitStatus, gitStatusManager } from "../../state/use-git-status"; |
| @@ -10,24 +12,33 @@ import { dismissGitActionResult, useGitActionProgress } from "../../state/use-gi · 1 unmodified line | |||
| 10 | 12 | ||
| 11 | import { EmptyState } from "../../components/EmptyState"; | 13 | import { EmptyState } from "../../components/EmptyState"; |
| 12 | import { LoadingScreen } from "../../components/LoadingScreen"; | 14 | import { LoadingScreen } from "../../components/LoadingScreen"; |
| 13 | import { buildThreadRoutePath } from "../../lib/routes"; | 15 | import { buildThreadRoutePath, buildThreadTerminalRoutePath } from "../../lib/routes"; |
| 14 | import { scopedThreadKey } from "../../lib/scopedEntities"; | 16 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 15 | import { connectionTone } from "../connection/connectionTone"; | 17 | import { connectionTone } from "../connection/connectionTone"; |
| 16 | 18 | ||
| 17 | import { useRemoteCatalog } from "../../state/use-remote-catalog"; | 19 | import { useRemoteCatalog } from "../../state/use-remote-catalog"; |
| 18 | import { | 20 | import { |
| 21 | getEnvironmentClient, | ||
| 19 | useRemoteConnectionStatus, | 22 | useRemoteConnectionStatus, |
| 20 | useRemoteEnvironmentState, | 23 | useRemoteEnvironmentState, |
| 21 | } from "../../state/use-remote-environment-registry"; | 24 | } from "../../state/use-remote-environment-registry"; |
| 25 | import { terminalSessionManager, useKnownTerminalSessions } from "../../state/use-terminal-session"; | ||
| 22 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; | 26 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; |
| 23 | import { useThreadSelection } from "../../state/use-thread-selection"; | 27 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 24 | import { GitActionProgressOverlay } from "./GitActionProgressOverlay"; | 28 | import { GitActionProgressOverlay } from "./GitActionProgressOverlay"; |
| 29 | import { | ||
| 30 | buildTerminalMenuSessions, | ||
| 31 | nextTerminalId, | ||
| 32 | resolveProjectScriptTerminalId, | ||
| 33 | } from "./terminalMenu"; | ||
| 34 | import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext"; | ||
| 25 | import { ThreadDetailScreen } from "./ThreadDetailScreen"; | 35 | import { ThreadDetailScreen } from "./ThreadDetailScreen"; |
| 26 | import { ThreadGitControls } from "./ThreadGitControls"; | 36 | import { ThreadGitControls } from "./ThreadGitControls"; |
| 27 | import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; | 37 | import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; |
| 28 | import { useSelectedThreadCommands } from "./use-selected-thread-commands"; | 38 | import { useSelectedThreadCommands } from "./use-selected-thread-commands"; |
| 29 | import { useSelectedThreadGitActions } from "./use-selected-thread-git-actions"; | 39 | import { useSelectedThreadGitActions } from "./use-selected-thread-git-actions"; |
| 30 | import { useSelectedThreadGitState } from "./use-selected-thread-git-state"; | 40 | import { useSelectedThreadGitState } from "./use-selected-thread-git-state"; |
| 41 | import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; | ||
| 31 | import { useThreadComposerState } from "./use-thread-composer-state"; | 42 | import { useThreadComposerState } from "./use-thread-composer-state"; |
| 32 | 43 | ||
| 33 | function firstRouteParam(value: string | string[] | undefined): string | null { | 44 | function 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 | /> |
| @@ -0,0 +1,170 @@ | |||
| 1 | import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts"; | ||
| 2 | import { SymbolView } from "expo-symbols"; | ||
| 3 | import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| 4 | import { Pressable, View } from "react-native"; | ||
| 5 | |||
| 6 | import { AppText as Text } from "../../components/AppText"; | ||
| 7 | import { getEnvironmentClient } from "../../state/use-remote-environment-registry"; | ||
| 8 | import { useTerminalSession, useTerminalSessionTarget } from "../../state/use-terminal-session"; | ||
| 9 | import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface"; | ||
| 10 | import { hasNativeTerminalSurface } from "../../native/terminal/nativeTerminalModule"; | ||
| 11 | |||
| 12 | interface ThreadTerminalPanelProps { | ||
| 13 | readonly environmentId: string; | ||
| 14 | readonly threadId: string; | ||
| 15 | readonly cwd: string; | ||
| 16 | readonly worktreePath: string | null; | ||
| 17 | readonly visible: boolean; | ||
| 18 | readonly onClose: () => void; | ||
| 19 | } | ||
| 20 | |||
| 21 | const DEFAULT_TERMINAL_COLS = 80; | ||
| 22 | const DEFAULT_TERMINAL_ROWS = 24; | ||
| 23 | |||
| 24 | export const ThreadTerminalPanel = memo(function ThreadTerminalPanel( | ||
| 25 | props: ThreadTerminalPanelProps, | ||
| 26 | ) { | ||
| 27 | const nativeTerminalAvailable = hasNativeTerminalSurface(); | ||
| 28 | const terminalId = DEFAULT_TERMINAL_ID; | ||
| 29 | const target = useTerminalSessionTarget({ | ||
| 30 | environmentId: props.environmentId, | ||
| 31 | threadId: props.threadId, | ||
| 32 | terminalId, | ||
| 33 | }); | ||
| 34 | const terminal = useTerminalSession(target); | ||
| 35 | const [lastGridSize, setLastGridSize] = useState({ | ||
| 36 | cols: DEFAULT_TERMINAL_COLS, | ||
| 37 | rows: DEFAULT_TERMINAL_ROWS, | ||
| 38 | }); | ||
| 39 | const hasOpenedRef = useRef(false); | ||
| 40 | |||
| 41 | const terminalKey = useMemo( | ||
| 42 | () => `${props.environmentId}:${props.threadId}:${terminalId}`, | ||
| 43 | [props.environmentId, props.threadId, terminalId], | ||
| 44 | ); | ||
| 45 | const isRunning = terminal.status === "running" || terminal.status === "starting"; | ||
| 46 | |||
| 47 | useEffect(() => { | ||
| 48 | hasOpenedRef.current = false; | ||
| 49 | }, [terminalKey]); | ||
| 50 | |||
| 51 | const openTerminal = useCallback(async () => { | ||
| 52 | const client = getEnvironmentClient(props.environmentId); | ||
| 53 | if (!client) { | ||
| 54 | return; | ||
| 55 | } | ||
| 56 | |||
| 57 | await client.terminal.open({ | ||
| 58 | threadId: props.threadId, | ||
| 59 | terminalId, | ||
| 60 | cwd: props.cwd, | ||
| 61 | worktreePath: props.worktreePath, | ||
| 62 | cols: lastGridSize.cols, | ||
| 63 | rows: lastGridSize.rows, | ||
| 64 | }); | ||
| 65 | }, [ | ||
| 66 | lastGridSize.cols, | ||
| 67 | lastGridSize.rows, | ||
| 68 | props.cwd, | ||
| 69 | props.environmentId, | ||
| 70 | props.threadId, | ||
| 71 | props.worktreePath, | ||
| 72 | terminalId, | ||
| 73 | ]); | ||
| 74 | |||
| 75 | useEffect(() => { | ||
| 76 | if (!props.visible || hasOpenedRef.current || terminal.status !== "closed") { | ||
| 77 | return; | ||
| 78 | } | ||
| 79 | |||
| 80 | hasOpenedRef.current = true; | ||
| 81 | void openTerminal().catch(() => { | ||
| 82 | hasOpenedRef.current = false; | ||
| 83 | }); | ||
| 84 | }, [openTerminal, props.visible, terminal.status]); | ||
| 85 | |||
| 86 | const handleInput = useCallback( | ||
| 87 | (data: string) => { | ||
| 88 | const client = getEnvironmentClient(props.environmentId); | ||
| 89 | if (!client || !isRunning) { | ||
| 90 | return; | ||
| 91 | } | ||
| 92 | |||
| 93 | void client.terminal.write({ | ||
| 94 | threadId: props.threadId, | ||
| 95 | terminalId, | ||
| 96 | data, | ||
| 97 | }); | ||
| 98 | }, | ||
| 99 | [isRunning, props.environmentId, props.threadId, terminalId], | ||
| 100 | ); | ||
| 101 | |||
| 102 | const handleResize = useCallback( | ||
| 103 | (size: { readonly cols: number; readonly rows: number }) => { | ||
| 104 | if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) { | ||
| 105 | return; | ||
| 106 | } | ||
| 107 | |||
| 108 | setLastGridSize(size); | ||
| 109 | const client = getEnvironmentClient(props.environmentId); | ||
| 110 | if (!client || !isRunning) { | ||
| 111 | return; | ||
| 112 | } | ||
| 113 | |||
| 114 | void client.terminal.resize({ | ||
| 115 | threadId: props.threadId, | ||
| 116 | terminalId, | ||
| 117 | cols: size.cols, | ||
| 118 | rows: size.rows, | ||
| 119 | }); | ||
| 120 | }, | ||
| 121 | [ | ||
| 122 | isRunning, | ||
| 123 | lastGridSize.cols, | ||
| 124 | lastGridSize.rows, | ||
| 125 | props.environmentId, | ||
| 126 | props.threadId, | ||
| 127 | terminalId, | ||
| 128 | ], | ||
| 129 | ); | ||
| 130 | |||
| 131 | if (!props.visible) { | ||
| 132 | return null; | ||
| 133 | } | ||
| 134 | |||
| 135 | return ( | ||
| 136 | <View className="absolute inset-x-3 bottom-28 top-28 overflow-hidden rounded-[8px] border border-white/10 bg-neutral-950 shadow-2xl"> | ||
| 137 | <View className="flex-row items-center justify-between border-b border-white/10 px-3 py-2"> | ||
| 138 | <View className="min-w-0 flex-1"> | ||
| 139 | <Text className="font-t3-bold text-[13px] text-neutral-100" numberOfLines={1}> | ||
| 140 | Terminal | ||
| 141 | </Text> | ||
| 142 | <Text className="text-[11px] text-neutral-500" numberOfLines={1}> | ||
| 143 | {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"} | ||
| 144 | </Text> | ||
| 145 | </View> | ||
| 146 | <View className="flex-row items-center gap-2"> | ||
| 147 | {terminal.error ? ( | ||
| 148 | <Text className="max-w-44 text-right text-[11px] text-red-300" numberOfLines={1}> | ||
| 149 | {terminal.error} | ||
| 150 | </Text> | ||
| 151 | ) : null} | ||
| 152 | <Pressable | ||
| 153 | className="h-8 w-8 items-center justify-center rounded-[8px] bg-white/10" | ||
| 154 | onPress={props.onClose} | ||
| 155 | > | ||
| 156 | <SymbolView name="xmark" size={13} tintColor="#e5e5e5" type="monochrome" /> | ||
| 157 | </Pressable> | ||
| 158 | </View> | ||
| 159 | </View> | ||
| 160 | <TerminalSurface | ||
| 161 | terminalKey={terminalKey} | ||
| 162 | buffer={terminal.buffer} | ||
| 163 | isRunning={isRunning} | ||
| 164 | onInput={handleInput} | ||
| 165 | onResize={handleResize} | ||
| 166 | style={{ flex: 1 }} | ||
| 167 | /> | ||
| 168 | </View> | ||
| 169 | ); | ||
| 170 | }); | ||
| @@ -0,0 +1,66 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap"; | ||
| 4 | |||
| 5 | describe("resolveTerminalRouteBootstrap", () => { | ||
| 6 | it("redirects bare terminal routes to another already-running terminal for the thread", () => { | ||
| 7 | expect( | ||
| 8 | resolveTerminalRouteBootstrap({ | ||
| 9 | hasThread: true, | ||
| 10 | hasWorkspaceRoot: true, | ||
| 11 | hasOpened: false, | ||
| 12 | requestedTerminalId: null, | ||
| 13 | currentTerminalId: "default", | ||
| 14 | runningTerminalId: "term-2", | ||
| 15 | }), | ||
| 16 | ).toEqual({ | ||
| 17 | kind: "redirect", | ||
| 18 | terminalId: "term-2", | ||
| 19 | }); | ||
| 20 | }); | ||
| 21 | |||
| 22 | it("hydrates the current running terminal instead of skipping open", () => { | ||
| 23 | expect( | ||
| 24 | resolveTerminalRouteBootstrap({ | ||
| 25 | hasThread: true, | ||
| 26 | hasWorkspaceRoot: true, | ||
| 27 | hasOpened: false, | ||
| 28 | requestedTerminalId: null, | ||
| 29 | currentTerminalId: "default", | ||
| 30 | runningTerminalId: "default", | ||
| 31 | }), | ||
| 32 | ).toEqual({ | ||
| 33 | kind: "open", | ||
| 34 | }); | ||
| 35 | }); | ||
| 36 | |||
| 37 | it("opens explicit terminal routes once so they replay existing history", () => { | ||
| 38 | expect( | ||
| 39 | resolveTerminalRouteBootstrap({ | ||
| 40 | hasThread: true, | ||
| 41 | hasWorkspaceRoot: true, | ||
| 42 | hasOpened: false, | ||
| 43 | requestedTerminalId: "term-2", | ||
| 44 | currentTerminalId: "term-2", | ||
| 45 | runningTerminalId: "term-2", | ||
| 46 | }), | ||
| 47 | ).toEqual({ | ||
| 48 | kind: "open", | ||
| 49 | }); | ||
| 50 | }); | ||
| 51 | |||
| 52 | it("stays idle after the route already bootstrapped once", () => { | ||
| 53 | expect( | ||
| 54 | resolveTerminalRouteBootstrap({ | ||
| 55 | hasThread: true, | ||
| 56 | hasWorkspaceRoot: true, | ||
| 57 | hasOpened: true, | ||
| 58 | requestedTerminalId: null, | ||
| 59 | currentTerminalId: "default", | ||
| 60 | runningTerminalId: "default", | ||
| 61 | }), | ||
| 62 | ).toEqual({ | ||
| 63 | kind: "idle", | ||
| 64 | }); | ||
| 65 | }); | ||
| 66 | }); | ||
| @@ -0,0 +1,734 @@ | |||
| 1 | import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts"; | ||
| 2 | import { Stack, useLocalSearchParams, useRouter } from "expo-router"; | ||
| 3 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| 4 | import { Pressable, ScrollView, Text as RNText, View } from "react-native"; | ||
| 5 | import { KeyboardStickyView } from "react-native-keyboard-controller"; | ||
| 6 | import Animated, { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated"; | ||
| 7 | |||
| 8 | import { EmptyState } from "../../components/EmptyState"; | ||
| 9 | import { LoadingScreen } from "../../components/LoadingScreen"; | ||
| 10 | import { buildThreadTerminalRoutePath } from "../../lib/routes"; | ||
| 11 | import { | ||
| 12 | getEnvironmentClient, | ||
| 13 | useRemoteEnvironmentState, | ||
| 14 | } from "../../state/use-remote-environment-registry"; | ||
| 15 | import { | ||
| 16 | terminalSessionManager, | ||
| 17 | useKnownTerminalSessions, | ||
| 18 | useTerminalSession, | ||
| 19 | useTerminalSessionTarget, | ||
| 20 | } from "../../state/use-terminal-session"; | ||
| 21 | import { useThreadSelection } from "../../state/use-thread-selection"; | ||
| 22 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; | ||
| 23 | import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface"; | ||
| 24 | import { loadPreferences, savePreferencesPatch } from "../../lib/storage"; | ||
| 25 | import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap"; | ||
| 26 | import { resolveTerminalOpenLocation } from "./terminalLaunchContext"; | ||
| 27 | import { | ||
| 28 | basename, | ||
| 29 | buildTerminalMenuSessions, | ||
| 30 | getTerminalLabel, | ||
| 31 | getTerminalStatusLabel, | ||
| 32 | nextTerminalId, | ||
| 33 | } from "./terminalMenu"; | ||
| 34 | import { | ||
| 35 | DEFAULT_TERMINAL_FONT_SIZE, | ||
| 36 | MAX_TERMINAL_FONT_SIZE, | ||
| 37 | MIN_TERMINAL_FONT_SIZE, | ||
| 38 | TERMINAL_FONT_SIZE_STEP, | ||
| 39 | normalizeTerminalFontSize, | ||
| 40 | } from "./terminalPreferences"; | ||
| 41 | |||
| 42 | const DEFAULT_TERMINAL_COLS = 80; | ||
| 43 | const DEFAULT_TERMINAL_ROWS = 24; | ||
| 44 | const TERMINAL_ACCESSORY_HEIGHT = 52; | ||
| 45 | |||
| 46 | type PendingModifier = "ctrl" | "meta"; | ||
| 47 | type HostPlatform = "mac" | "linux" | "windows" | "unknown"; | ||
| 48 | |||
| 49 | type TerminalToolbarAction = | ||
| 50 | | { readonly kind: "send"; readonly key: string; readonly label: string; readonly data: string } | ||
| 51 | | { | ||
| 52 | readonly kind: "modifier"; | ||
| 53 | readonly key: string; | ||
| 54 | readonly label: string; | ||
| 55 | readonly modifier: PendingModifier; | ||
| 56 | }; | ||
| 57 | |||
| 58 | interface TerminalMenuSession { | ||
| 59 | readonly terminalId: string; | ||
| 60 | readonly cwd: string | null; | ||
| 61 | readonly status: "starting" | "running" | "exited" | "error" | "closed"; | ||
| 62 | readonly updatedAt: string | null; | ||
| 63 | } | ||
| 64 | |||
| 65 | function getTerminalStatusTone(input: { | ||
| 66 | readonly status: TerminalMenuSession["status"]; | ||
| 67 | readonly hasRunningSubprocess?: boolean; | ||
| 68 | }): { | ||
| 69 | readonly tintColor: string; | ||
| 70 | readonly textColor: string; | ||
| 71 | } { | ||
| 72 | if (input.status === "running") { | ||
| 73 | if (input.hasRunningSubprocess) { | ||
| 74 | return { | ||
| 75 | tintColor: "#fbbf24", | ||
| 76 | textColor: "#a3a3a3", | ||
| 77 | }; | ||
| 78 | } | ||
| 79 | |||
| 80 | return { | ||
| 81 | tintColor: "#34d399", | ||
| 82 | textColor: "#a3a3a3", | ||
| 83 | }; | ||
| 84 | } | ||
| 85 | |||
| 86 | if (input.status === "starting") { | ||
| 87 | return { | ||
| 88 | tintColor: "#f59e0b", | ||
| 89 | textColor: "#a3a3a3", | ||
| 90 | }; | ||
| 91 | } | ||
| 92 | |||
| 93 | if (input.status === "error") { | ||
| 94 | return { | ||
| 95 | tintColor: "#ef4444", | ||
| 96 | textColor: "#fca5a5", | ||
| 97 | }; | ||
| 98 | } | ||
| 99 | |||
| 100 | return { | ||
| 101 | tintColor: "#ef4444", | ||
| 102 | textColor: "#a3a3a3", | ||
| 103 | }; | ||
| 104 | } | ||
| 105 | |||
| 106 | function firstRouteParam(value: string | string[] | undefined): string | null { | ||
| 107 | if (Array.isArray(value)) { | ||
| 108 | return value[0] ?? null; | ||
| 109 | } | ||
| 110 | |||
| 111 | return value ?? null; | ||
| 112 | } | ||
| 113 | |||
| 114 | function inferHostPlatform(environmentLabel: string | null): HostPlatform { | ||
| 115 | const value = environmentLabel?.toLowerCase() ?? ""; | ||
| 116 | if ( | ||
| 117 | value.includes("mac") || | ||
| 118 | value.includes("macbook") || | ||
| 119 | value.includes("mac mini") || | ||
| 120 | value.includes("imac") || | ||
| 121 | value.includes("darwin") | ||
| 122 | ) { | ||
| 123 | return "mac"; | ||
| 124 | } | ||
| 125 | if (value.includes("windows") || value.includes("win")) { | ||
| 126 | return "windows"; | ||
| 127 | } | ||
| 128 | if (value.includes("linux") || value.includes("ubuntu") || value.includes("debian")) { | ||
| 129 | return "linux"; | ||
| 130 | } | ||
| 131 | |||
| 132 | return "unknown"; | ||
| 133 | } | ||
| 134 | |||
| 135 | function applyCtrlModifier(input: string): string { | ||
| 136 | const firstCharacter = input[0]; | ||
| 137 | if (!firstCharacter) { | ||
| 138 | return input; | ||
| 139 | } | ||
| 140 | |||
| 141 | const lowerCharacter = firstCharacter.toLowerCase(); | ||
| 142 | if (lowerCharacter >= "a" && lowerCharacter <= "z") { | ||
| 143 | return String.fromCharCode(lowerCharacter.charCodeAt(0) - 96); | ||
| 144 | } | ||
| 145 | |||
| 146 | if (firstCharacter === "@") return "\u0000"; | ||
| 147 | if (firstCharacter === "[") return "\u001b"; | ||
| 148 | if (firstCharacter === "\\") return "\u001c"; | ||
| 149 | if (firstCharacter === "]") return "\u001d"; | ||
| 150 | if (firstCharacter === "^") return "\u001e"; | ||
| 151 | if (firstCharacter === "_") return "\u001f"; | ||
| 152 | if (firstCharacter === "?") return "\u007f"; | ||
| 153 | |||
| 154 | return input; | ||
| 155 | } | ||
| 156 | |||
| 157 | export function ThreadTerminalRouteScreen() { | ||
| 158 | const router = useRouter(); | ||
| 159 | const keyboard = useAnimatedKeyboard(); | ||
| 160 | const { isLoadingSavedConnection } = useRemoteEnvironmentState(); | ||
| 161 | const params = useLocalSearchParams<{ | ||
| 162 | environmentId?: string | string[]; | ||
| 163 | threadId?: string | string[]; | ||
| 164 | terminalId?: string | string[]; | ||
| 165 | }>(); | ||
| 166 | const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = | ||
| 167 | useThreadSelection(); | ||
| 168 | const selectedThreadDetail = useSelectedThreadDetail(); | ||
| 169 | const requestedTerminalId = firstRouteParam(params.terminalId); | ||
| 170 | const knownSessions = useKnownTerminalSessions({ | ||
| 171 | environmentId: selectedThread?.environmentId ?? null, | ||
| 172 | threadId: selectedThread?.id ?? null, | ||
| 173 | }); | ||
| 174 | const [lastGridSize, setLastGridSize] = useState({ | ||
| 175 | cols: DEFAULT_TERMINAL_COLS, | ||
| 176 | rows: DEFAULT_TERMINAL_ROWS, | ||
| 177 | }); | ||
| 178 | const [pendingModifier, setPendingModifier] = useState<PendingModifier | null>(null); | ||
| 179 | const [fontSize, setFontSize] = useState(DEFAULT_TERMINAL_FONT_SIZE); | ||
| 180 | const hasOpenedRef = useRef(false); | ||
| 181 | const hasLoadedFontPreferenceRef = useRef(false); | ||
| 182 | |||
| 183 | const terminalId = requestedTerminalId ?? DEFAULT_TERMINAL_ID; | ||
| 184 | const target = useTerminalSessionTarget({ | ||
| 185 | environmentId: selectedThread?.environmentId ?? null, | ||
| 186 | threadId: selectedThread?.id ?? null, | ||
| 187 | terminalId, | ||
| 188 | }); | ||
| 189 | const terminal = useTerminalSession(target); | ||
| 190 | const terminalKey = useMemo( | ||
| 191 | () => | ||
| 192 | selectedThread | ||
| 193 | ? `${selectedThread.environmentId}:${selectedThread.id}:${terminalId}` | ||
| 194 | : terminalId, | ||
| 195 | [selectedThread, terminalId], | ||
| 196 | ); | ||
| 197 | const isRunning = terminal.status === "running" || terminal.status === "starting"; | ||
| 198 | const cwd = terminal.snapshot?.cwd ?? selectedThreadProject?.workspaceRoot ?? null; | ||
| 199 | const hostPlatform = useMemo( | ||
| 200 | () => inferHostPlatform(selectedEnvironmentConnection?.environmentLabel ?? null), | ||
| 201 | [selectedEnvironmentConnection?.environmentLabel], | ||
| 202 | ); | ||
| 203 | const runningSession = useMemo( | ||
| 204 | () => | ||
| 205 | knownSessions.find( | ||
| 206 | (session) => session.state.status === "running" || session.state.status === "starting", | ||
| 207 | ) ?? null, | ||
| 208 | [knownSessions], | ||
| 209 | ); | ||
| 210 | const activeKnownSession = useMemo( | ||
| 211 | () => knownSessions.find((session) => session.target.terminalId === terminalId) ?? null, | ||
| 212 | [knownSessions, terminalId], | ||
| 213 | ); | ||
| 214 | const terminalStatusTone = useMemo( | ||
| 215 | () => | ||
| 216 | getTerminalStatusTone({ | ||
| 217 | status: terminal.status, | ||
| 218 | hasRunningSubprocess: terminal.hasRunningSubprocess, | ||
| 219 | }), | ||
| 220 | [terminal.hasRunningSubprocess, terminal.status], | ||
| 221 | ); | ||
| 222 | const headerTitle = useMemo(() => { | ||
| 223 | const topLineParts = [ | ||
| 224 | selectedEnvironmentConnection?.environmentLabel ?? null, | ||
| 225 | selectedThreadProject?.title ?? null, | ||
| 226 | ].filter((value): value is string => Boolean(value)); | ||
| 227 | |||
| 228 | return { | ||
| 229 | topLine: topLineParts.join(" \u00b7 "), | ||
| 230 | bottomLine: cwd ?? selectedThreadProject?.workspaceRoot ?? "", | ||
| 231 | }; | ||
| 232 | }, [ | ||
| 233 | cwd, | ||
| 234 | selectedEnvironmentConnection?.environmentLabel, | ||
| 235 | selectedThreadProject?.title, | ||
| 236 | selectedThreadProject?.workspaceRoot, | ||
| 237 | ]); | ||
| 238 | const terminalToolbarActions = useMemo<ReadonlyArray<TerminalToolbarAction>>(() => { | ||
| 239 | const modifierActions: ReadonlyArray<TerminalToolbarAction> = | ||
| 240 | hostPlatform === "mac" | ||
| 241 | ? [ | ||
| 242 | { kind: "modifier", key: "cmd", label: "cmd", modifier: "meta" }, | ||
| 243 | { kind: "modifier", key: "ctrl", label: "ctrl", modifier: "ctrl" }, | ||
| 244 | ] | ||
| 245 | : [ | ||
| 246 | { kind: "modifier", key: "ctrl", label: "ctrl", modifier: "ctrl" }, | ||
| 247 | { kind: "modifier", key: "alt", label: "alt", modifier: "meta" }, | ||
| 248 | ]; | ||
| 249 | |||
| 250 | return [ | ||
| 251 | { kind: "send", key: "esc", label: "esc", data: "\u001b" }, | ||
| 252 | ...modifierActions, | ||
| 253 | { kind: "send", key: "tab", label: "tab", data: "\t" }, | ||
| 254 | { kind: "send", key: "up", label: "↑", data: "\u001b[A" }, | ||
| 255 | { kind: "send", key: "down", label: "↓", data: "\u001b[B" }, | ||
| 256 | { kind: "send", key: "left", label: "←", data: "\u001b[D" }, | ||
| 257 | { kind: "send", key: "right", label: "→", data: "\u001b[C" }, | ||
| 258 | { kind: "send", key: "tilde", label: "~", data: "~" }, | ||
| 259 | { kind: "send", key: "pipe", label: "|", data: "|" }, | ||
| 260 | { kind: "send", key: "slash", label: "/", data: "/" }, | ||
| 261 | { kind: "send", key: "dash", label: "-", data: "-" }, | ||
| 262 | ]; | ||
| 263 | }, [hostPlatform]); | ||
| 264 | const terminalBottomInset = TERMINAL_ACCESSORY_HEIGHT; | ||
| 265 | const terminalContainerAnimatedStyle = useAnimatedStyle(() => ({ | ||
| 266 | paddingBottom: | ||
| 267 | keyboard.height.value > 0 | ||
| 268 | ? keyboard.height.value + TERMINAL_ACCESSORY_HEIGHT | ||
| 269 | : terminalBottomInset, | ||
| 270 | })); | ||
| 271 | |||
| 272 | const terminalMenuSessions = useMemo<ReadonlyArray<TerminalMenuSession>>( | ||
| 273 | () => | ||
| 274 | buildTerminalMenuSessions({ | ||
| 275 | knownSessions, | ||
| 276 | workspaceRoot: selectedThreadProject?.workspaceRoot ?? null, | ||
| 277 | currentSession: { | ||
| 278 | terminalId, | ||
| 279 | cwd: cwd ?? null, | ||
| 280 | status: terminal.status, | ||
| 281 | updatedAt: terminal.updatedAt, | ||
| 282 | }, | ||
| 283 | }), | ||
| 284 | [ | ||
| 285 | cwd, | ||
| 286 | knownSessions, | ||
| 287 | selectedThreadProject?.workspaceRoot, | ||
| 288 | terminal.status, | ||
| 289 | terminal.updatedAt, | ||
| 290 | terminalId, | ||
| 291 | ], | ||
| 292 | ); | ||
| 293 | |||
| 294 | const openTerminal = useCallback(async () => { | ||
| 295 | if (!selectedThread || !selectedThreadProject?.workspaceRoot) { | ||
| 296 | return; | ||
| 297 | } | ||
| 298 | |||
| 299 | const client = getEnvironmentClient(selectedThread.environmentId); | ||
| 300 | if (!client) { | ||
| 301 | return; | ||
| 302 | } | ||
| 303 | |||
| 304 | const launchLocation = resolveTerminalOpenLocation({ | ||
| 305 | terminalSnapshot: terminal.snapshot, | ||
| 306 | activeSessionSnapshot: activeKnownSession?.state.snapshot ?? null, | ||
| 307 | workspaceRoot: selectedThreadProject.workspaceRoot, | ||
| 308 | threadShellWorktreePath: selectedThread.worktreePath ?? null, | ||
| 309 | threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null, | ||
| 310 | }); | ||
| 311 | |||
| 312 | const snapshot = await client.terminal.open({ | ||
| 313 | threadId: selectedThread.id, | ||
| 314 | terminalId, | ||
| 315 | cwd: launchLocation.cwd, | ||
| 316 | worktreePath: launchLocation.worktreePath, | ||
| 317 | cols: lastGridSize.cols, | ||
| 318 | rows: lastGridSize.rows, | ||
| 319 | }); | ||
| 320 | |||
| 321 | terminalSessionManager.syncSnapshot({ environmentId: selectedThread.environmentId }, snapshot); | ||
| 322 | }, [ | ||
| 323 | lastGridSize.cols, | ||
| 324 | lastGridSize.rows, | ||
| 325 | activeKnownSession?.state.snapshot, | ||
| 326 | selectedThreadDetail?.worktreePath, | ||
| 327 | selectedThread, | ||
| 328 | selectedThreadProject?.workspaceRoot, | ||
| 329 | terminal.snapshot, | ||
| 330 | terminalId, | ||
| 331 | ]); | ||
| 332 | |||
| 333 | useEffect(() => { | ||
| 334 | hasOpenedRef.current = false; | ||
| 335 | }, [terminalKey]); | ||
| 336 | |||
| 337 | useEffect(() => { | ||
| 338 | let cancelled = false; | ||
| 339 | |||
| 340 | void loadPreferences() | ||
| 341 | .then((preferences) => { | ||
| 342 | if (cancelled) { | ||
| 343 | return; | ||
| 344 | } | ||
| 345 | |||
| 346 | setFontSize(normalizeTerminalFontSize(preferences.terminalFontSize)); | ||
| 347 | hasLoadedFontPreferenceRef.current = true; | ||
| 348 | }) | ||
| 349 | .catch(() => { | ||
| 350 | if (cancelled) { | ||
| 351 | return; | ||
| 352 | } | ||
| 353 | |||
| 354 | hasLoadedFontPreferenceRef.current = true; | ||
| 355 | }); | ||
| 356 | |||
| 357 | return () => { | ||
| 358 | cancelled = true; | ||
| 359 | }; | ||
| 360 | }, []); | ||
| 361 | |||
| 362 | useEffect(() => { | ||
| 363 | if (!hasLoadedFontPreferenceRef.current) { | ||
| 364 | return; | ||
| 365 | } | ||
| 366 | |||
| 367 | void savePreferencesPatch({ | ||
| 368 | terminalFontSize: normalizeTerminalFontSize(fontSize), | ||
| 369 | }); | ||
| 370 | }, [fontSize]); | ||
| 371 | |||
| 372 | useEffect(() => { | ||
| 373 | const bootstrapAction = resolveTerminalRouteBootstrap({ | ||
| 374 | hasThread: selectedThread !== null, | ||
| 375 | hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), | ||
| 376 | hasOpened: hasOpenedRef.current, | ||
| 377 | requestedTerminalId, | ||
| 378 | currentTerminalId: terminalId, | ||
| 379 | runningTerminalId: runningSession?.target.terminalId ?? null, | ||
| 380 | }); | ||
| 381 | if (bootstrapAction.kind === "idle" || !selectedThread) { | ||
| 382 | return; | ||
| 383 | } | ||
| 384 | |||
| 385 | if (bootstrapAction.kind === "redirect") { | ||
| 386 | router.replace(buildThreadTerminalRoutePath(selectedThread, bootstrapAction.terminalId)); | ||
| 387 | return; | ||
| 388 | } | ||
| 389 | |||
| 390 | hasOpenedRef.current = true; | ||
| 391 | void openTerminal().catch(() => { | ||
| 392 | hasOpenedRef.current = false; | ||
| 393 | }); | ||
| 394 | }, [ | ||
| 395 | openTerminal, | ||
| 396 | requestedTerminalId, | ||
| 397 | router, | ||
| 398 | runningSession, | ||
| 399 | selectedThread, | ||
| 400 | selectedThreadProject?.workspaceRoot, | ||
| 401 | terminalId, | ||
| 402 | ]); | ||
| 403 | |||
| 404 | useEffect(() => { | ||
| 405 | setPendingModifier(null); | ||
| 406 | }, [terminalId]); | ||
| 407 | |||
| 408 | const writeInput = useCallback( | ||
| 409 | (data: string) => { | ||
| 410 | if (!selectedThread || !isRunning) { | ||
| 411 | return; | ||
| 412 | } | ||
| 413 | |||
| 414 | const client = getEnvironmentClient(selectedThread.environmentId); | ||
| 415 | if (!client) { | ||
| 416 | return; | ||
| 417 | } | ||
| 418 | |||
| 419 | void client.terminal.write({ | ||
| 420 | threadId: selectedThread.id, | ||
| 421 | terminalId, | ||
| 422 | data, | ||
| 423 | }); | ||
| 424 | }, | ||
| 425 | [isRunning, selectedThread, terminalId], | ||
| 426 | ); | ||
| 427 | |||
| 428 | const handleInput = useCallback( | ||
| 429 | (data: string) => { | ||
| 430 | if (data.length === 0) { | ||
| 431 | return; | ||
| 432 | } | ||
| 433 | |||
| 434 | if (pendingModifier === "ctrl") { | ||
| 435 | setPendingModifier(null); | ||
| 436 | writeInput(applyCtrlModifier(data)); | ||
| 437 | } else if (pendingModifier === "meta") { | ||
| 438 | setPendingModifier(null); | ||
| 439 | writeInput(`\u001b${data}`); | ||
| 440 | } else { | ||
| 441 | writeInput(data); | ||
| 442 | } | ||
| 443 | }, | ||
| 444 | [pendingModifier, writeInput], | ||
| 445 | ); | ||
| 446 | |||
| 447 | const handleResize = useCallback( | ||
| 448 | (size: { readonly cols: number; readonly rows: number }) => { | ||
| 449 | if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) { | ||
| 450 | return; | ||
| 451 | } | ||
| 452 | |||
| 453 | setLastGridSize(size); | ||
| 454 | if (!selectedThread || !isRunning) { | ||
| 455 | return; | ||
| 456 | } | ||
| 457 | |||
| 458 | const client = getEnvironmentClient(selectedThread.environmentId); | ||
| 459 | if (!client) { | ||
| 460 | return; | ||
| 461 | } | ||
| 462 | |||
| 463 | void client.terminal.resize({ | ||
| 464 | threadId: selectedThread.id, | ||
| 465 | terminalId, | ||
| 466 | cols: size.cols, | ||
| 467 | rows: size.rows, | ||
| 468 | }); | ||
| 469 | }, | ||
| 470 | [isRunning, lastGridSize.cols, lastGridSize.rows, selectedThread, terminalId], | ||
| 471 | ); | ||
| 472 | |||
| 473 | const handleSelectTerminal = useCallback( | ||
| 474 | (nextTerminalId: string) => { | ||
| 475 | if (!selectedThread || nextTerminalId === terminalId) { | ||
| 476 | return; | ||
| 477 | } | ||
| 478 | |||
| 479 | router.replace(buildThreadTerminalRoutePath(selectedThread, nextTerminalId)); | ||
| 480 | }, | ||
| 481 | [router, selectedThread, terminalId], | ||
| 482 | ); | ||
| 483 | |||
| 484 | const handleOpenNewTerminal = useCallback(() => { | ||
| 485 | if (!selectedThread) { | ||
| 486 | return; | ||
| 487 | } | ||
| 488 | |||
| 489 | router.replace( | ||
| 490 | buildThreadTerminalRoutePath( | ||
| 491 | selectedThread, | ||
| 492 | nextTerminalId(terminalMenuSessions.map((session) => session.terminalId)), | ||
| 493 | ), | ||
| 494 | ); | ||
| 495 | }, [router, selectedThread, terminalMenuSessions]); | ||
| 496 | |||
| 497 | const adjustFontSize = useCallback((delta: number) => { | ||
| 498 | setTimeout(() => { | ||
| 499 | setFontSize((current) => normalizeTerminalFontSize(current + delta)); | ||
| 500 | }, 0); | ||
| 501 | }, []); | ||
| 502 | |||
| 503 | const handleDecreaseFontSize = useCallback(() => { | ||
| 504 | adjustFontSize(-TERMINAL_FONT_SIZE_STEP); | ||
| 505 | }, [adjustFontSize]); | ||
| 506 | |||
| 507 | const handleIncreaseFontSize = useCallback(() => { | ||
| 508 | adjustFontSize(TERMINAL_FONT_SIZE_STEP); | ||
| 509 | }, [adjustFontSize]); | ||
| 510 | |||
| 511 | const handleToolbarActionPress = useCallback( | ||
| 512 | (action: TerminalToolbarAction) => { | ||
| 513 | if (action.kind === "modifier") { | ||
| 514 | setPendingModifier((current) => (current === action.modifier ? null : action.modifier)); | ||
| 515 | return; | ||
| 516 | } | ||
| 517 | |||
| 518 | setPendingModifier(null); | ||
| 519 | if (pendingModifier === "ctrl") { | ||
| 520 | writeInput(applyCtrlModifier(action.data)); | ||
| 521 | } else if (pendingModifier === "meta") { | ||
| 522 | writeInput(`\u001b${action.data}`); | ||
| 523 | } else { | ||
| 524 | writeInput(action.data); | ||
| 525 | } | ||
| 526 | }, | ||
| 527 | [pendingModifier, writeInput], | ||
| 528 | ); | ||
| 529 | |||
| 530 | if (!selectedThread) { | ||
| 531 | if (isLoadingSavedConnection) { | ||
| 532 | return <LoadingScreen message="Opening terminal…" />; | ||
| 533 | } | ||
| 534 | |||
| 535 | return ( | ||
| 536 | <View className="flex-1 bg-screen"> | ||
| 537 | <EmptyState | ||
| 538 | title="Thread unavailable" | ||
| 539 | detail="This terminal route needs an active thread and workspace." | ||
| 540 | /> | ||
| 541 | </View> | ||
| 542 | ); | ||
| 543 | } | ||
| 544 | |||
| 545 | if (!selectedThreadProject?.workspaceRoot) { | ||
| 546 | return ( | ||
| 547 | <View className="flex-1 bg-screen"> | ||
| 548 | <EmptyState | ||
| 549 | title="Terminal unavailable" | ||
| 550 | detail="This thread does not have a workspace root yet, so there is nowhere to open a shell." | ||
| 551 | /> | ||
| 552 | </View> | ||
| 553 | ); | ||
| 554 | } | ||
| 555 | |||
| 556 | return ( | ||
| 557 | <> | ||
| 558 | <Stack.Screen | ||
| 559 | options={{ | ||
| 560 | headerShown: true, | ||
| 561 | headerBackButtonDisplayMode: "minimal", | ||
| 562 | headerBackTitle: "", | ||
| 563 | headerShadowVisible: false, | ||
| 564 | headerStyle: { backgroundColor: "#0a0a0a" }, | ||
| 565 | headerTintColor: "#f5f5f5", | ||
| 566 | headerTitleAlign: "center", | ||
| 567 | title: "", | ||
| 568 | headerTitle: () => ( | ||
| 569 | <View | ||
| 570 | style={{ | ||
| 571 | alignItems: "center", | ||
| 572 | gap: 1, | ||
| 573 | maxWidth: 240, | ||
| 574 | }} | ||
| 575 | > | ||
| 576 | <RNText | ||
| 577 | numberOfLines={1} | ||
| 578 | style={{ | ||
| 579 | color: "#f5f5f5", | ||
| 580 | fontFamily: "DMSans_700Bold", | ||
| 581 | fontSize: 13, | ||
| 582 | lineHeight: 16, | ||
| 583 | }} | ||
| 584 | > | ||
| 585 | {headerTitle.topLine} | ||
| 586 | </RNText> | ||
| 587 | <RNText | ||
| 588 | ellipsizeMode="middle" | ||
| 589 | numberOfLines={1} | ||
| 590 | style={{ | ||
| 591 | color: "#8f8f94", | ||
| 592 | fontFamily: "Menlo", | ||
| 593 | fontSize: 11, | ||
| 594 | lineHeight: 14, | ||
| 595 | }} | ||
| 596 | > | ||
| 597 | {headerTitle.bottomLine} | ||
| 598 | </RNText> | ||
| 599 | </View> | ||
| 600 | ), | ||
| 601 | }} | ||
| 602 | /> | ||
| 603 | |||
| 604 | <Stack.Toolbar placement="right"> | ||
| 605 | <Stack.Toolbar.Menu | ||
| 606 | icon="circle.fill" | ||
| 607 | style={{ | ||
| 608 | color: terminalStatusTone.textColor, | ||
| 609 | fontFamily: "DMSans_700Bold", | ||
| 610 | fontSize: 12, | ||
| 611 | fontWeight: "700", | ||
| 612 | }} | ||
| 613 | tintColor={terminalStatusTone.tintColor} | ||
| 614 | title="Terminal options" | ||
| 615 | > | ||
| 616 | <Stack.Toolbar.Label> | ||
| 617 | {getTerminalStatusLabel({ | ||
| 618 | status: terminal.status, | ||
| 619 | hasRunningSubprocess: terminal.hasRunningSubprocess, | ||
| 620 | })} | ||
| 621 | </Stack.Toolbar.Label> | ||
| 622 | <Stack.Toolbar.Menu icon="textformat.size" inline title="Text size"> | ||
| 623 | <Stack.Toolbar.Label>Text size</Stack.Toolbar.Label> | ||
| 624 | <Stack.Toolbar.MenuAction | ||
| 625 | disabled={fontSize <= MIN_TERMINAL_FONT_SIZE} | ||
| 626 | discoverabilityLabel="Decrease terminal text size" | ||
| 627 | onPress={handleDecreaseFontSize} | ||
| 628 | > | ||
| 629 | <Stack.Toolbar.Label>{`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`}</Stack.Toolbar.Label> | ||
| 630 | </Stack.Toolbar.MenuAction> | ||
| 631 | <Stack.Toolbar.MenuAction | ||
| 632 | disabled={fontSize >= MAX_TERMINAL_FONT_SIZE} | ||
| 633 | discoverabilityLabel="Increase terminal text size" | ||
| 634 | onPress={handleIncreaseFontSize} | ||
| 635 | > | ||
| 636 | <Stack.Toolbar.Label>{`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`}</Stack.Toolbar.Label> | ||
| 637 | </Stack.Toolbar.MenuAction> | ||
| 638 | </Stack.Toolbar.Menu> | ||
| 639 | {terminalMenuSessions.map((session) => ( | ||
| 640 | <Stack.Toolbar.MenuAction | ||
| 641 | key={session.terminalId} | ||
| 642 | icon={session.terminalId === terminalId ? "checkmark" : "terminal"} | ||
| 643 | onPress={() => handleSelectTerminal(session.terminalId)} | ||
| 644 | subtitle={[getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] | ||
| 645 | .filter(Boolean) | ||
| 646 | .join(" · ")} | ||
| 647 | > | ||
| 648 | <Stack.Toolbar.Label>{getTerminalLabel(session.terminalId)}</Stack.Toolbar.Label> | ||
| 649 | </Stack.Toolbar.MenuAction> | ||
| 650 | ))} | ||
| 651 | <Stack.Toolbar.MenuAction | ||
| 652 | icon="plus" | ||
| 653 | onPress={handleOpenNewTerminal} | ||
| 654 | subtitle={`Start another shell in ${basename(selectedThreadProject.workspaceRoot) ?? "this workspace"}`} | ||
| 655 | > | ||
| 656 | <Stack.Toolbar.Label>Open new terminal</Stack.Toolbar.Label> | ||
| 657 | </Stack.Toolbar.MenuAction> | ||
| 658 | </Stack.Toolbar.Menu> | ||
| 659 | </Stack.Toolbar> | ||
| 660 | |||
| 661 | <View style={{ flex: 1, backgroundColor: "#050505" }}> | ||
| 662 | <Animated.View style={[{ flex: 1 }, terminalContainerAnimatedStyle]}> | ||
| 663 | <TerminalSurface | ||
| 664 | buffer={terminal.buffer} | ||
| 665 | fontSize={fontSize} | ||
| 666 | isRunning={isRunning} | ||
| 667 | onInput={handleInput} | ||
| 668 | onResize={handleResize} | ||
| 669 | style={{ flex: 1 }} | ||
| 670 | terminalKey={terminalKey} | ||
| 671 | /> | ||
| 672 | </Animated.View> | ||
| 673 | |||
| 674 | <KeyboardStickyView style={{ position: "absolute", bottom: 0, left: 0, right: 0 }}> | ||
| 675 | <View | ||
| 676 | style={{ | ||
| 677 | backgroundColor: "#0a0a0a", | ||
| 678 | borderTopColor: "rgba(255,255,255,0.08)", | ||
| 679 | borderTopWidth: 1, | ||
| 680 | minHeight: TERMINAL_ACCESSORY_HEIGHT, | ||
| 681 | paddingBottom: 4, | ||
| 682 | paddingHorizontal: 8, | ||
| 683 | paddingTop: 4, | ||
| 684 | }} | ||
| 685 | > | ||
| 686 | <ScrollView | ||
| 687 | horizontal | ||
| 688 | contentContainerStyle={{ alignItems: "center", gap: 6, paddingRight: 2 }} | ||
| 689 | showsHorizontalScrollIndicator={false} | ||
| 690 | > | ||
| 691 | {terminalToolbarActions.map((action) => { | ||
| 692 | const active = action.kind === "modifier" && pendingModifier === action.modifier; | ||
| 693 | |||
| 694 | return ( | ||
| 695 | <Pressable | ||
| 696 | key={action.key} | ||
| 697 | onPress={() => handleToolbarActionPress(action)} | ||
| 698 | style={({ pressed }) => ({ | ||
| 699 | alignItems: "center", | ||
| 700 | backgroundColor: active | ||
| 701 | ? "rgba(74, 222, 128, 0.18)" | ||
| 702 | : pressed | ||
| 703 | ? "rgba(255,255,255,0.12)" | ||
| 704 | : "rgba(255,255,255,0.06)", | ||
| 705 | borderColor: active ? "rgba(74, 222, 128, 0.36)" : "rgba(255,255,255,0.08)", | ||
| 706 | borderRadius: 12, | ||
| 707 | borderWidth: 1, | ||
| 708 | justifyContent: "center", | ||
| 709 | minWidth: action.label.length > 1 ? 46 : 38, | ||
| 710 | paddingHorizontal: 11, | ||
| 711 | paddingVertical: 8, | ||
| 712 | })} | ||
| 713 | > | ||
| 714 | <RNText | ||
| 715 | style={{ | ||
| 716 | color: active ? "#bbf7d0" : "#f5f5f5", | ||
| 717 | fontFamily: "DMSans_700Bold", | ||
| 718 | fontSize: 12, | ||
| 719 | fontWeight: "700", | ||
| 720 | textTransform: action.kind === "modifier" ? "uppercase" : "none", | ||
| 721 | }} | ||
| 722 | > | ||
| 723 | {action.label} | ||
| 724 | </RNText> | ||
| 725 | </Pressable> | ||
| 726 | ); | ||
| 727 | })} | ||
| 728 | </ScrollView> | ||
| 729 | </View> | ||
| 730 | </KeyboardStickyView> | ||
| 731 | </View> | ||
| 732 | </> | ||
| 733 | ); | ||
| 734 | } | ||
| @@ -10,12 +10,14 @@ import { useGitStatus } from "../../../state/use-git-status"; · 9 unmodified lines | |||
| 10 | import { useThreadSelection } from "../../../state/use-thread-selection"; | 10 | import { useThreadSelection } from "../../../state/use-thread-selection"; |
| 11 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; | 11 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; |
| 12 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; | 12 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; |
| 13 | import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | ||
| 13 | import { SheetActionButton } from "./gitSheetComponents"; | 14 | import { SheetActionButton } from "./gitSheetComponents"; |
| 14 | 15 | ||
| 15 | export function GitBranchesSheet() { | 16 | export 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; |
| @@ -9,13 +9,15 @@ import { useGitStatus } from "../../../state/use-git-status"; · 8 unmodified lines | |||
| 9 | import { useThreadSelection } from "../../../state/use-thread-selection"; | 9 | import { useThreadSelection } from "../../../state/use-thread-selection"; |
| 10 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; | 10 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; |
| 11 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; | 11 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; |
| 12 | import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | ||
| 12 | import { SheetActionButton } from "./gitSheetComponents"; | 13 | import { SheetActionButton } from "./gitSheetComponents"; |
| 13 | 14 | ||
| 14 | export function GitCommitSheet() { | 15 | export 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; |
| @@ -17,6 +17,7 @@ import { useGitStatus } from "../../../state/use-git-status"; · 16 unmodified lines | |||
| 17 | import { useThreadSelection } from "../../../state/use-thread-selection"; | 17 | import { useThreadSelection } from "../../../state/use-thread-selection"; |
| 18 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; | 18 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; |
| 19 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; | 19 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; |
| 20 | import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | ||
| 20 | import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents"; | 21 | import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents"; |
| 21 | 22 | ||
| 22 | export function GitOverviewSheet() { | 23 | export 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; |
| @@ -23,8 +23,8 @@ import { cn } from "../../../lib/cn"; · 22 unmodified lines | |||
| 23 | import { useThemeColor } from "../../../lib/useThemeColor"; | 23 | import { useThemeColor } from "../../../lib/useThemeColor"; |
| 24 | import { getEnvironmentClient } from "../../../state/use-remote-environment-registry"; | 24 | import { getEnvironmentClient } from "../../../state/use-remote-environment-registry"; |
| 25 | import { useSelectedThreadDetail } from "../../../state/use-thread-detail"; | 25 | import { useSelectedThreadDetail } from "../../../state/use-thread-detail"; |
| 26 | import { useThreadSelection } from "../../../state/use-thread-selection"; | ||
| 27 | import { useThreadDraftForThread } from "../use-thread-composer-state"; | 26 | import { useThreadDraftForThread } from "../use-thread-composer-state"; |
| 27 | import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | ||
| 28 | import { | 28 | import { |
| 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], |
| @@ -0,0 +1,61 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | resolvePreferredThreadWorktreePath, | ||
| 5 | resolveTerminalOpenLocation, | ||
| 6 | } from "./terminalLaunchContext"; | ||
| 7 | |||
| 8 | describe("resolvePreferredThreadWorktreePath", () => { | ||
| 9 | it("prefers thread detail worktree paths over thread shell paths", () => { | ||
| 10 | expect( | ||
| 11 | resolvePreferredThreadWorktreePath({ | ||
| 12 | threadShellWorktreePath: "/repo/root", | ||
| 13 | threadDetailWorktreePath: "/repo/worktrees/feature", | ||
| 14 | }), | ||
| 15 | ).toBe("/repo/worktrees/feature"); | ||
| 16 | }); | ||
| 17 | |||
| 18 | it("falls back to the thread shell worktree path when detail is unavailable", () => { | ||
| 19 | expect( | ||
| 20 | resolvePreferredThreadWorktreePath({ | ||
| 21 | threadShellWorktreePath: "/repo/worktrees/feature", | ||
| 22 | threadDetailWorktreePath: null, | ||
| 23 | }), | ||
| 24 | ).toBe("/repo/worktrees/feature"); | ||
| 25 | }); | ||
| 26 | }); | ||
| 27 | |||
| 28 | describe("resolveTerminalOpenLocation", () => { | ||
| 29 | it("uses the thread detail worktree path before the workspace root for a fresh mobile open", () => { | ||
| 30 | expect( | ||
| 31 | resolveTerminalOpenLocation({ | ||
| 32 | terminalSnapshot: null, | ||
| 33 | activeSessionSnapshot: null, | ||
| 34 | workspaceRoot: "/repo/root", | ||
| 35 | threadShellWorktreePath: null, | ||
| 36 | threadDetailWorktreePath: "/repo/worktrees/feature", | ||
| 37 | }), | ||
| 38 | ).toEqual({ | ||
| 39 | cwd: "/repo/worktrees/feature", | ||
| 40 | worktreePath: "/repo/worktrees/feature", | ||
| 41 | }); | ||
| 42 | }); | ||
| 43 | |||
| 44 | it("preserves the running terminal snapshot cwd when attaching to an existing session", () => { | ||
| 45 | expect( | ||
| 46 | resolveTerminalOpenLocation({ | ||
| 47 | terminalSnapshot: null, | ||
| 48 | activeSessionSnapshot: { | ||
| 49 | cwd: "/repo/worktrees/feature", | ||
| 50 | worktreePath: "/repo/worktrees/feature", | ||
| 51 | }, | ||
| 52 | workspaceRoot: "/repo/root", | ||
| 53 | threadShellWorktreePath: null, | ||
| 54 | threadDetailWorktreePath: "/repo/worktrees/other", | ||
| 55 | }), | ||
| 56 | ).toEqual({ | ||
| 57 | cwd: "/repo/worktrees/feature", | ||
| 58 | worktreePath: "/repo/worktrees/feature", | ||
| 59 | }); | ||
| 60 | }); | ||
| 61 | }); | ||
| @@ -0,0 +1,39 @@ | |||
| 1 | interface TerminalSnapshotLike { | ||
| 2 | readonly cwd: string; | ||
| 3 | readonly worktreePath: string | null; | ||
| 4 | } | ||
| 5 | |||
| 6 | export function resolvePreferredThreadWorktreePath(input: { | ||
| 7 | readonly threadShellWorktreePath: string | null; | ||
| 8 | readonly threadDetailWorktreePath: string | null; | ||
| 9 | }): string | null { | ||
| 10 | return input.threadDetailWorktreePath ?? input.threadShellWorktreePath ?? null; | ||
| 11 | } | ||
| 12 | |||
| 13 | export function resolveTerminalOpenLocation(input: { | ||
| 14 | readonly terminalSnapshot: TerminalSnapshotLike | null; | ||
| 15 | readonly activeSessionSnapshot: TerminalSnapshotLike | null; | ||
| 16 | readonly workspaceRoot: string; | ||
| 17 | readonly threadShellWorktreePath: string | null; | ||
| 18 | readonly threadDetailWorktreePath: string | null; | ||
| 19 | }): { | ||
| 20 | readonly cwd: string; | ||
| 21 | readonly worktreePath: string | null; | ||
| 22 | } { | ||
| 23 | const preferredThreadWorktreePath = resolvePreferredThreadWorktreePath({ | ||
| 24 | threadShellWorktreePath: input.threadShellWorktreePath, | ||
| 25 | threadDetailWorktreePath: input.threadDetailWorktreePath, | ||
| 26 | }); | ||
| 27 | |||
| 28 | return { | ||
| 29 | cwd: | ||
| 30 | input.terminalSnapshot?.cwd ?? | ||
| 31 | input.activeSessionSnapshot?.cwd ?? | ||
| 32 | preferredThreadWorktreePath ?? | ||
| 33 | input.workspaceRoot, | ||
| 34 | worktreePath: | ||
| 35 | input.terminalSnapshot?.worktreePath ?? | ||
| 36 | input.activeSessionSnapshot?.worktreePath ?? | ||
| 37 | preferredThreadWorktreePath, | ||
| 38 | }; | ||
| 39 | } | ||
| @@ -0,0 +1,128 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import type { KnownTerminalSession } from "@t3tools/client-runtime"; | ||
| 4 | import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts"; | ||
| 5 | |||
| 6 | import { buildTerminalMenuSessions, resolveProjectScriptTerminalId } from "./terminalMenu"; | ||
| 7 | |||
| 8 | function makeKnownSession(input: { | ||
| 9 | readonly terminalId: string; | ||
| 10 | readonly status: KnownTerminalSession["state"]["status"]; | ||
| 11 | readonly cwd?: string | null; | ||
| 12 | readonly updatedAt?: string | null; | ||
| 13 | }): KnownTerminalSession { | ||
| 14 | return { | ||
| 15 | target: { | ||
| 16 | environmentId: "env-1", | ||
| 17 | threadId: "thread-1", | ||
| 18 | terminalId: input.terminalId, | ||
| 19 | }, | ||
| 20 | state: { | ||
| 21 | snapshot: input.cwd | ||
| 22 | ? { | ||
| 23 | threadId: "thread-1", | ||
| 24 | terminalId: input.terminalId, | ||
| 25 | cwd: input.cwd, | ||
| 26 | worktreePath: input.cwd, | ||
| 27 | status: input.status === "closed" ? "error" : input.status, | ||
| 28 | pid: input.status === "running" ? 123 : null, | ||
| 29 | history: "", | ||
| 30 | exitCode: null, | ||
| 31 | exitSignal: null, | ||
| 32 | updatedAt: input.updatedAt ?? "2026-04-15T20:00:00.000Z", | ||
| 33 | } | ||
| 34 | : null, | ||
| 35 | buffer: "", | ||
| 36 | status: input.status, | ||
| 37 | error: null, | ||
| 38 | hasRunningSubprocess: false, | ||
| 39 | updatedAt: input.updatedAt ?? "2026-04-15T20:00:00.000Z", | ||
| 40 | version: 1, | ||
| 41 | }, | ||
| 42 | }; | ||
| 43 | } | ||
| 44 | |||
| 45 | describe("buildTerminalMenuSessions", () => { | ||
| 46 | it("keeps the default shell and only includes running sessions by default", () => { | ||
| 47 | expect( | ||
| 48 | buildTerminalMenuSessions({ | ||
| 49 | knownSessions: [ | ||
| 50 | makeKnownSession({ | ||
| 51 | terminalId: "term-3", | ||
| 52 | status: "running", | ||
| 53 | cwd: "/workspace/feature", | ||
| 54 | updatedAt: "2026-04-15T20:05:00.000Z", | ||
| 55 | }), | ||
| 56 | makeKnownSession({ | ||
| 57 | terminalId: "term-2", | ||
| 58 | status: "exited", | ||
| 59 | cwd: "/workspace/exited", | ||
| 60 | updatedAt: "2026-04-15T20:06:00.000Z", | ||
| 61 | }), | ||
| 62 | ], | ||
| 63 | workspaceRoot: "/workspace/root", | ||
| 64 | }), | ||
| 65 | ).toEqual([ | ||
| 66 | { | ||
| 67 | terminalId: DEFAULT_TERMINAL_ID, | ||
| 68 | cwd: "/workspace/root", | ||
| 69 | status: "closed", | ||
| 70 | updatedAt: null, | ||
| 71 | }, | ||
| 72 | { | ||
| 73 | terminalId: "term-3", | ||
| 74 | cwd: "/workspace/feature", | ||
| 75 | status: "running", | ||
| 76 | updatedAt: "2026-04-15T20:05:00.000Z", | ||
| 77 | }, | ||
| 78 | ]); | ||
| 79 | }); | ||
| 80 | |||
| 81 | it("keeps the current terminal visible even if it is no longer running", () => { | ||
| 82 | expect( | ||
| 83 | buildTerminalMenuSessions({ | ||
| 84 | knownSessions: [], | ||
| 85 | workspaceRoot: "/workspace/root", | ||
| 86 | currentSession: { | ||
| 87 | terminalId: "term-4", | ||
| 88 | cwd: "/workspace/exited", | ||
| 89 | status: "exited", | ||
| 90 | updatedAt: "2026-04-15T20:07:00.000Z", | ||
| 91 | }, | ||
| 92 | }), | ||
| 93 | ).toEqual([ | ||
| 94 | { | ||
| 95 | terminalId: DEFAULT_TERMINAL_ID, | ||
| 96 | cwd: "/workspace/root", | ||
| 97 | status: "closed", | ||
| 98 | updatedAt: null, | ||
| 99 | }, | ||
| 100 | { | ||
| 101 | terminalId: "term-4", | ||
| 102 | cwd: "/workspace/exited", | ||
| 103 | status: "exited", | ||
| 104 | updatedAt: "2026-04-15T20:07:00.000Z", | ||
| 105 | }, | ||
| 106 | ]); | ||
| 107 | }); | ||
| 108 | }); | ||
| 109 | |||
| 110 | describe("resolveProjectScriptTerminalId", () => { | ||
| 111 | it("reuses the default shell when no terminal is running", () => { | ||
| 112 | expect( | ||
| 113 | resolveProjectScriptTerminalId({ | ||
| 114 | existingTerminalIds: [DEFAULT_TERMINAL_ID], | ||
| 115 | hasRunningTerminal: false, | ||
| 116 | }), | ||
| 117 | ).toBe(DEFAULT_TERMINAL_ID); | ||
| 118 | }); | ||
| 119 | |||
| 120 | it("opens a new terminal when a shell is already running", () => { | ||
| 121 | expect( | ||
| 122 | resolveProjectScriptTerminalId({ | ||
| 123 | existingTerminalIds: [DEFAULT_TERMINAL_ID, "term-2", "term-4"], | ||
| 124 | hasRunningTerminal: true, | ||
| 125 | }), | ||
| 126 | ).toBe("term-3"); | ||
| 127 | }); | ||
| 128 | }); | ||
| @@ -0,0 +1,139 @@ | |||
| 1 | import type { KnownTerminalSession } from "@t3tools/client-runtime"; | ||
| 2 | import { DEFAULT_TERMINAL_ID, type ProjectScript } from "@t3tools/contracts"; | ||
| 3 | |||
| 4 | export interface TerminalMenuSession { | ||
| 5 | readonly terminalId: string; | ||
| 6 | readonly cwd: string | null; | ||
| 7 | readonly status: "starting" | "running" | "exited" | "error" | "closed"; | ||
| 8 | readonly updatedAt: string | null; | ||
| 9 | } | ||
| 10 | |||
| 11 | export function basename(input: string | null): string | null { | ||
| 12 | if (!input) { | ||
| 13 | return null; | ||
| 14 | } | ||
| 15 | |||
| 16 | const normalized = input.replace(/\/+$/, ""); | ||
| 17 | if (normalized.length === 0) { | ||
| 18 | return "/"; | ||
| 19 | } | ||
| 20 | |||
| 21 | const segments = normalized.split("/"); | ||
| 22 | return segments[segments.length - 1] ?? normalized; | ||
| 23 | } | ||
| 24 | |||
| 25 | export function getTerminalLabel(terminalId: string): string { | ||
| 26 | if (terminalId === DEFAULT_TERMINAL_ID) { | ||
| 27 | return "Main shell"; | ||
| 28 | } | ||
| 29 | |||
| 30 | const numericSuffix = /^term(?:inal)?-(\d+)$/i.exec(terminalId)?.[1]; | ||
| 31 | if (numericSuffix) { | ||
| 32 | return `Terminal ${numericSuffix}`; | ||
| 33 | } | ||
| 34 | |||
| 35 | return terminalId; | ||
| 36 | } | ||
| 37 | |||
| 38 | export function getTerminalStatusLabel(input: { | ||
| 39 | readonly status: TerminalMenuSession["status"]; | ||
| 40 | readonly hasRunningSubprocess?: boolean; | ||
| 41 | }): string { | ||
| 42 | if (input.status === "running") { | ||
| 43 | return input.hasRunningSubprocess ? "Task running" : "Ready"; | ||
| 44 | } | ||
| 45 | if (input.status === "starting") { | ||
| 46 | return "Starting"; | ||
| 47 | } | ||
| 48 | if (input.status === "exited") { | ||
| 49 | return "Exited"; | ||
| 50 | } | ||
| 51 | if (input.status === "error") { | ||
| 52 | return "Error"; | ||
| 53 | } | ||
| 54 | |||
| 55 | return "Not started"; | ||
| 56 | } | ||
| 57 | |||
| 58 | export function nextTerminalId(existingTerminalIds: ReadonlyArray<string>): string { | ||
| 59 | const usedIds = new Set(existingTerminalIds); | ||
| 60 | let nextIndex = 2; | ||
| 61 | while (usedIds.has(`term-${nextIndex}`)) { | ||
| 62 | nextIndex += 1; | ||
| 63 | } | ||
| 64 | |||
| 65 | return `term-${nextIndex}`; | ||
| 66 | } | ||
| 67 | |||
| 68 | export function buildTerminalMenuSessions(input: { | ||
| 69 | readonly knownSessions: ReadonlyArray<KnownTerminalSession>; | ||
| 70 | readonly workspaceRoot: string | null; | ||
| 71 | readonly currentSession?: TerminalMenuSession | null; | ||
| 72 | }): ReadonlyArray<TerminalMenuSession> { | ||
| 73 | const sessionsById = new Map<string, TerminalMenuSession>(); | ||
| 74 | |||
| 75 | sessionsById.set(DEFAULT_TERMINAL_ID, { | ||
| 76 | terminalId: DEFAULT_TERMINAL_ID, | ||
| 77 | cwd: input.workspaceRoot, | ||
| 78 | status: "closed", | ||
| 79 | updatedAt: null, | ||
| 80 | }); | ||
| 81 | |||
| 82 | for (const session of input.knownSessions) { | ||
| 83 | if ( | ||
| 84 | session.state.status !== "running" && | ||
| 85 | session.state.status !== "starting" && | ||
| 86 | session.target.terminalId !== input.currentSession?.terminalId | ||
| 87 | ) { | ||
| 88 | continue; | ||
| 89 | } | ||
| 90 | |||
| 91 | sessionsById.set(session.target.terminalId, { | ||
| 92 | terminalId: session.target.terminalId, | ||
| 93 | cwd: session.state.snapshot?.cwd ?? input.workspaceRoot, | ||
| 94 | status: session.state.status, | ||
| 95 | updatedAt: session.state.updatedAt, | ||
| 96 | }); | ||
| 97 | } | ||
| 98 | |||
| 99 | if (input.currentSession && !sessionsById.has(input.currentSession.terminalId)) { | ||
| 100 | sessionsById.set(input.currentSession.terminalId, input.currentSession); | ||
| 101 | } | ||
| 102 | |||
| 103 | return Array.from(sessionsById.values()).sort((left, right) => { | ||
| 104 | if (left.terminalId === DEFAULT_TERMINAL_ID) return -1; | ||
| 105 | if (right.terminalId === DEFAULT_TERMINAL_ID) return 1; | ||
| 106 | |||
| 107 | const leftUpdatedAt = left.updatedAt ? Date.parse(left.updatedAt) : 0; | ||
| 108 | const rightUpdatedAt = right.updatedAt ? Date.parse(right.updatedAt) : 0; | ||
| 109 | if (leftUpdatedAt !== rightUpdatedAt) { | ||
| 110 | return rightUpdatedAt - leftUpdatedAt; | ||
| 111 | } | ||
| 112 | |||
| 113 | return left.terminalId.localeCompare(right.terminalId); | ||
| 114 | }); | ||
| 115 | } | ||
| 116 | |||
| 117 | export function resolveProjectScriptTerminalId(input: { | ||
| 118 | readonly existingTerminalIds: ReadonlyArray<string>; | ||
| 119 | readonly hasRunningTerminal: boolean; | ||
| 120 | }): string { | ||
| 121 | if (!input.hasRunningTerminal) { | ||
| 122 | return DEFAULT_TERMINAL_ID; | ||
| 123 | } | ||
| 124 | |||
| 125 | return nextTerminalId(input.existingTerminalIds); | ||
| 126 | } | ||
| 127 | |||
| 128 | export function projectScriptMenuLabel(script: ProjectScript): string { | ||
| 129 | return script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name; | ||
| 130 | } | ||
| 131 | |||
| 132 | export function projectScriptMenuIcon(icon: ProjectScript["icon"]) { | ||
| 133 | if (icon === "test") return "flask"; | ||
| 134 | if (icon === "lint") return "checklist"; | ||
| 135 | if (icon === "configure") return "wrench.and.screwdriver"; | ||
| 136 | if (icon === "build") return "hammer"; | ||
| 137 | if (icon === "debug") return "ladybug"; | ||
| 138 | return "play"; | ||
| 139 | } | ||
| @@ -0,0 +1,28 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | DEFAULT_TERMINAL_FONT_SIZE, | ||
| 5 | MAX_TERMINAL_FONT_SIZE, | ||
| 6 | MIN_TERMINAL_FONT_SIZE, | ||
| 7 | normalizeTerminalFontSize, | ||
| 8 | } from "./terminalPreferences"; | ||
| 9 | |||
| 10 | describe("normalizeTerminalFontSize", () => { | ||
| 11 | it("returns the default size for missing or invalid values", () => { | ||
| 12 | expect(normalizeTerminalFontSize(undefined)).toBe(DEFAULT_TERMINAL_FONT_SIZE); | ||
| 13 | expect(normalizeTerminalFontSize(null)).toBe(DEFAULT_TERMINAL_FONT_SIZE); | ||
| 14 | expect(normalizeTerminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE); | ||
| 15 | }); | ||
| 16 | |||
| 17 | it("clamps below the minimum", () => { | ||
| 18 | expect(normalizeTerminalFontSize(MIN_TERMINAL_FONT_SIZE - 4)).toBe(MIN_TERMINAL_FONT_SIZE); | ||
| 19 | }); | ||
| 20 | |||
| 21 | it("clamps above the maximum", () => { | ||
| 22 | expect(normalizeTerminalFontSize(MAX_TERMINAL_FONT_SIZE + 4)).toBe(MAX_TERMINAL_FONT_SIZE); | ||
| 23 | }); | ||
| 24 | |||
| 25 | it("preserves in-range values", () => { | ||
| 26 | expect(normalizeTerminalFontSize(9.5)).toBe(9.5); | ||
| 27 | }); | ||
| 28 | }); | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | export const DEFAULT_TERMINAL_FONT_SIZE = 10; | ||
| 2 | export const TERMINAL_FONT_SIZE_STEP = 0.5; | ||
| 3 | export const MIN_TERMINAL_FONT_SIZE = 6; | ||
| 4 | export const MAX_TERMINAL_FONT_SIZE = 14; | ||
| 5 | |||
| 6 | export function normalizeTerminalFontSize(value: number | null | undefined): number { | ||
| 7 | if (typeof value !== "number" || !Number.isFinite(value)) { | ||
| 8 | return DEFAULT_TERMINAL_FONT_SIZE; | ||
| 9 | } | ||
| 10 | |||
| 11 | return Math.min(MAX_TERMINAL_FONT_SIZE, Math.max(MIN_TERMINAL_FONT_SIZE, value)); | ||
| 12 | } | ||
| @@ -0,0 +1,25 @@ | |||
| 1 | export function resolveTerminalRouteBootstrap(input: { | ||
| 2 | readonly hasThread: boolean; | ||
| 3 | readonly hasWorkspaceRoot: boolean; | ||
| 4 | readonly hasOpened: boolean; | ||
| 5 | readonly requestedTerminalId: string | null; | ||
| 6 | readonly currentTerminalId: string; | ||
| 7 | readonly runningTerminalId: string | null; | ||
| 8 | }): | ||
| 9 | | { readonly kind: "idle" } | ||
| 10 | | { readonly kind: "redirect"; readonly terminalId: string } | ||
| 11 | | { readonly kind: "open" } { | ||
| 12 | if (!input.hasThread || !input.hasWorkspaceRoot || input.hasOpened) { | ||
| 13 | return { kind: "idle" }; | ||
| 14 | } | ||
| 15 | |||
| 16 | if ( | ||
| 17 | input.requestedTerminalId === null && | ||
| 18 | input.runningTerminalId !== null && | ||
| 19 | input.runningTerminalId !== input.currentTerminalId | ||
| 20 | ) { | ||
| 21 | return { kind: "redirect", terminalId: input.runningTerminalId }; | ||
| 22 | } | ||
| 23 | |||
| 24 | return { kind: "open" }; | ||
| 25 | } | ||
| @@ -25,9 +25,11 @@ import { gitActionManager, showGitActionResult } from "../../state/use-git-actio · 24 unmodified lines | |||
| 25 | import { gitBranchManager } from "../../state/use-git-branches"; | 25 | import { gitBranchManager } from "../../state/use-git-branches"; |
| 26 | import { gitStatusManager } from "../../state/use-git-status"; | 26 | import { gitStatusManager } from "../../state/use-git-status"; |
| 27 | import { useThreadSelection } from "../../state/use-thread-selection"; | 27 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 28 | import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; | ||
| 28 | 29 | ||
| 29 | export function useSelectedThreadGitActions() { | 30 | export 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 { |
| @@ -5,20 +5,18 @@ import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git"; · 4 unmodified lines | |||
| 5 | import { useGitActionState } from "../../state/use-git-action-state"; | 5 | import { useGitActionState } from "../../state/use-git-action-state"; |
| 6 | import { useGitBranches } from "../../state/use-git-branches"; | 6 | import { useGitBranches } from "../../state/use-git-branches"; |
| 7 | import { useThreadSelection } from "../../state/use-thread-selection"; | 7 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 8 | import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; | ||
| 8 | 9 | ||
| 9 | export function useSelectedThreadGitState() { | 10 | export 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 | ||
| @@ -0,0 +1,24 @@ | |||
| 1 | import { useMemo } from "react"; | ||
| 2 | |||
| 3 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; | ||
| 4 | import { useThreadSelection } from "../../state/use-thread-selection"; | ||
| 5 | import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext"; | ||
| 6 | |||
| 7 | export function useSelectedThreadWorktree() { | ||
| 8 | const { selectedThread, selectedThreadProject } = useThreadSelection(); | ||
| 9 | const selectedThreadDetail = useSelectedThreadDetail(); | ||
| 10 | |||
| 11 | const selectedThreadWorktreePath = useMemo( | ||
| 12 | () => | ||
| 13 | resolvePreferredThreadWorktreePath({ | ||
| 14 | threadShellWorktreePath: selectedThread?.worktreePath ?? null, | ||
| 15 | threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null, | ||
| 16 | }), | ||
| 17 | [selectedThread?.worktreePath, selectedThreadDetail?.worktreePath], | ||
| 18 | ); | ||
| 19 | |||
| 20 | return { | ||
| 21 | selectedThreadWorktreePath, | ||
| 22 | selectedThreadCwd: selectedThreadWorktreePath ?? selectedThreadProject?.workspaceRoot ?? null, | ||
| 23 | }; | ||
| 24 | } | ||
| @@ -29,6 +29,18 @@ export function buildThreadReviewRoutePath( · 28 unmodified lines | |||
| 29 | return `${buildThreadRoutePath(input)}/review`; | 29 | return `${buildThreadRoutePath(input)}/review`; |
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | export function buildThreadTerminalRoutePath( | ||
| 33 | input: ThreadRouteInput | PlainThreadRouteInput, | ||
| 34 | terminalId?: string | null, | ||
| 35 | ): string { | ||
| 36 | const basePath = `${buildThreadRoutePath(input)}/terminal`; | ||
| 37 | if (!terminalId) { | ||
| 38 | return basePath; | ||
| 39 | } | ||
| 40 | |||
| 41 | return `${basePath}?terminalId=${encodeURIComponent(terminalId)}`; | ||
| 42 | } | ||
| 43 | |||
| 32 | export function dismissRoute(router: Router) { | 44 | export function dismissRoute(router: Router) { |
| 33 | if (router.canGoBack()) { | 45 | if (router.canGoBack()) { |
| 34 | router.back(); | 46 | router.back(); |
| @@ -5,6 +5,11 @@ import * as SecureStore from "expo-secure-store"; · 4 unmodified lines | |||
| 5 | import type { SavedRemoteConnection } from "./connection"; | 5 | import type { SavedRemoteConnection } from "./connection"; |
| 6 | 6 | ||
| 7 | const CONNECTIONS_KEY = "t3code.connections"; | 7 | const CONNECTIONS_KEY = "t3code.connections"; |
| 8 | const PREFERENCES_KEY = "t3code.preferences"; | ||
| 9 | |||
| 10 | export interface MobilePreferences { | ||
| 11 | readonly terminalFontSize?: number; | ||
| 12 | } | ||
| 8 | 13 | ||
| 9 | async function readStorageItem(key: string): Promise<string | null> { | 14 | async 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 | ||
| 17 | export async function loadSavedConnections(): Promise<ReadonlyArray<SavedRemoteConnection>> { | 22 | async 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 | |||
| 35 | export 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 | ||
| 36 | export async function saveConnection(connection: SavedRemoteConnection): Promise<void> { | 49 | export 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 | |||
| 70 | export async function loadPreferences(): Promise<MobilePreferences> { | ||
| 71 | const parsed = await readJsonStorageItem<MobilePreferences>(PREFERENCES_KEY); | ||
| 72 | if (!parsed || typeof parsed !== "object") { | ||
| 73 | return {}; | ||
| 74 | } | ||
| 75 | |||
| 76 | if (typeof parsed.terminalFontSize === "number") { | ||
| 77 | return { terminalFontSize: parsed.terminalFontSize }; | ||
| 78 | } | ||
| 79 | |||
| 80 | return {}; | ||
| 81 | } | ||
| 82 | |||
| 83 | export async function savePreferencesPatch( | ||
| 84 | patch: Partial<MobilePreferences>, | ||
| 85 | ): Promise<MobilePreferences> { | ||
| 86 | const current = await loadPreferences(); | ||
| 87 | const next: MobilePreferences = { | ||
| 88 | ...current, | ||
| 89 | ...patch, | ||
| 90 | }; | ||
| 91 | await writeStorageItem(PREFERENCES_KEY, JSON.stringify(next)); | ||
| 92 | return next; | ||
| 93 | } | ||
| @@ -0,0 +1,206 @@ | |||
| 1 | import { memo, useCallback } from "react"; | ||
| 2 | import { | ||
| 3 | Pressable, | ||
| 4 | ScrollView, | ||
| 5 | TextInput, | ||
| 6 | View, | ||
| 7 | type LayoutChangeEvent, | ||
| 8 | type NativeSyntheticEvent, | ||
| 9 | type ViewProps, | ||
| 10 | useColorScheme, | ||
| 11 | } from "react-native"; | ||
| 12 | |||
| 13 | import { AppText as Text } from "../../components/AppText"; | ||
| 14 | import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule"; | ||
| 15 | import { | ||
| 16 | buildGhosttyThemeConfig, | ||
| 17 | getGitHubTerminalTheme, | ||
| 18 | type TerminalTheme, | ||
| 19 | } from "./terminalTheme"; | ||
| 20 | |||
| 21 | interface TerminalInputEvent { | ||
| 22 | readonly data: string; | ||
| 23 | } | ||
| 24 | |||
| 25 | interface TerminalResizeEvent { | ||
| 26 | readonly cols: number; | ||
| 27 | readonly rows: number; | ||
| 28 | } | ||
| 29 | |||
| 30 | interface TerminalSurfaceProps extends ViewProps { | ||
| 31 | readonly terminalKey: string; | ||
| 32 | readonly buffer: string; | ||
| 33 | readonly fontSize?: number; | ||
| 34 | readonly isRunning: boolean; | ||
| 35 | readonly theme?: TerminalTheme; | ||
| 36 | readonly onInput: (data: string) => void; | ||
| 37 | readonly onResize: (size: { readonly cols: number; readonly rows: number }) => void; | ||
| 38 | } | ||
| 39 | |||
| 40 | function estimateGridSize(input: { | ||
| 41 | readonly width: number; | ||
| 42 | readonly height: number; | ||
| 43 | readonly fontSize: number; | ||
| 44 | }): { readonly cols: number; readonly rows: number } { | ||
| 45 | const cellWidth = input.fontSize * 0.62; | ||
| 46 | const cellHeight = input.fontSize * 1.35; | ||
| 47 | return { | ||
| 48 | cols: Math.max(20, Math.min(400, Math.floor(input.width / cellWidth))), | ||
| 49 | rows: Math.max(5, Math.min(200, Math.floor(input.height / cellHeight))), | ||
| 50 | }; | ||
| 51 | } | ||
| 52 | |||
| 53 | const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) { | ||
| 54 | const fontSize = props.fontSize ?? 12; | ||
| 55 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; | ||
| 56 | const theme = props.theme ?? getGitHubTerminalTheme(appearanceScheme); | ||
| 57 | const statusLabel = props.isRunning | ||
| 58 | ? "Native terminal unavailable. Using text fallback." | ||
| 59 | : "Open terminal to start a shell."; | ||
| 60 | |||
| 61 | const handleLayout = (event: LayoutChangeEvent) => { | ||
| 62 | const { width, height } = event.nativeEvent.layout; | ||
| 63 | props.onResize(estimateGridSize({ width, height, fontSize })); | ||
| 64 | }; | ||
| 65 | |||
| 66 | return ( | ||
| 67 | <View | ||
| 68 | style={[ | ||
| 69 | { | ||
| 70 | flex: 1, | ||
| 71 | backgroundColor: theme.background, | ||
| 72 | borderRadius: 8, | ||
| 73 | overflow: "hidden", | ||
| 74 | }, | ||
| 75 | props.style, | ||
| 76 | ]} | ||
| 77 | onLayout={handleLayout} | ||
| 78 | > | ||
| 79 | <View style={{ flex: 1, paddingHorizontal: 10, paddingVertical: 8 }}> | ||
| 80 | <Text | ||
| 81 | style={{ | ||
| 82 | color: theme.mutedForeground, | ||
| 83 | fontSize: 11, | ||
| 84 | paddingBottom: 8, | ||
| 85 | }} | ||
| 86 | > | ||
| 87 | {statusLabel} | ||
| 88 | </Text> | ||
| 89 | <ScrollView | ||
| 90 | style={{ flex: 1 }} | ||
| 91 | contentContainerStyle={{ paddingBottom: 12 }} | ||
| 92 | showsVerticalScrollIndicator={false} | ||
| 93 | > | ||
| 94 | <Text | ||
| 95 | selectable | ||
| 96 | style={{ | ||
| 97 | color: theme.foreground, | ||
| 98 | fontFamily: "Menlo", | ||
| 99 | fontSize, | ||
| 100 | lineHeight: Math.round(fontSize * 1.35), | ||
| 101 | }} | ||
| 102 | > | ||
| 103 | {props.buffer || "$ "} | ||
| 104 | </Text> | ||
| 105 | </ScrollView> | ||
| 106 | </View> | ||
| 107 | <View | ||
| 108 | style={{ | ||
| 109 | borderTopWidth: 1, | ||
| 110 | borderTopColor: theme.border, | ||
| 111 | flexDirection: "row", | ||
| 112 | alignItems: "center", | ||
| 113 | gap: 8, | ||
| 114 | padding: 8, | ||
| 115 | }} | ||
| 116 | > | ||
| 117 | <TextInput | ||
| 118 | autoCapitalize="none" | ||
| 119 | autoCorrect={false} | ||
| 120 | blurOnSubmit={false} | ||
| 121 | editable={props.isRunning} | ||
| 122 | placeholder="type and press return" | ||
| 123 | placeholderTextColor={theme.mutedForeground} | ||
| 124 | returnKeyType="send" | ||
| 125 | style={{ | ||
| 126 | color: theme.foreground, | ||
| 127 | flex: 1, | ||
| 128 | fontFamily: "Menlo", | ||
| 129 | fontSize: 13, | ||
| 130 | padding: 0, | ||
| 131 | }} | ||
| 132 | onSubmitEditing={(event) => { | ||
| 133 | const text = event.nativeEvent.text; | ||
| 134 | if (text.length > 0) { | ||
| 135 | props.onInput(`${text}\n`); | ||
| 136 | } | ||
| 137 | }} | ||
| 138 | /> | ||
| 139 | <Pressable | ||
| 140 | disabled={!props.isRunning} | ||
| 141 | style={({ pressed }) => ({ | ||
| 142 | opacity: !props.isRunning ? 0.35 : pressed ? 0.65 : 1, | ||
| 143 | paddingHorizontal: 10, | ||
| 144 | paddingVertical: 6, | ||
| 145 | borderRadius: 8, | ||
| 146 | backgroundColor: theme.border, | ||
| 147 | })} | ||
| 148 | onPress={() => props.onInput("\u0003")} | ||
| 149 | > | ||
| 150 | <Text | ||
| 151 | style={{ | ||
| 152 | color: theme.foreground, | ||
| 153 | fontFamily: "DMSans_700Bold", | ||
| 154 | fontSize: 11, | ||
| 155 | }} | ||
| 156 | > | ||
| 157 | Ctrl-C | ||
| 158 | </Text> | ||
| 159 | </Pressable> | ||
| 160 | </View> | ||
| 161 | </View> | ||
| 162 | ); | ||
| 163 | }); | ||
| 164 | |||
| 165 | export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) { | ||
| 166 | const fontSize = props.fontSize ?? 12; | ||
| 167 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; | ||
| 168 | const theme = props.theme ?? getGitHubTerminalTheme(appearanceScheme); | ||
| 169 | const { onInput, onResize } = props; | ||
| 170 | const NativeTerminalSurfaceView = resolveNativeTerminalSurfaceView(); | ||
| 171 | const handleNativeInput = useCallback( | ||
| 172 | (event: NativeSyntheticEvent<TerminalInputEvent>) => { | ||
| 173 | onInput(event.nativeEvent.data); | ||
| 174 | }, | ||
| 175 | [onInput], | ||
| 176 | ); | ||
| 177 | const handleNativeResize = useCallback( | ||
| 178 | (event: NativeSyntheticEvent<TerminalResizeEvent>) => { | ||
| 179 | onResize({ | ||
| 180 | cols: event.nativeEvent.cols, | ||
| 181 | rows: event.nativeEvent.rows, | ||
| 182 | }); | ||
| 183 | }, | ||
| 184 | [onResize], | ||
| 185 | ); | ||
| 186 | |||
| 187 | if (NativeTerminalSurfaceView) { | ||
| 188 | return ( | ||
| 189 | <NativeTerminalSurfaceView | ||
| 190 | {...props} | ||
| 191 | appearanceScheme={appearanceScheme} | ||
| 192 | backgroundColor={theme.background} | ||
| 193 | foregroundColor={theme.foreground} | ||
| 194 | mutedForegroundColor={theme.mutedForeground} | ||
| 195 | terminalKey={props.terminalKey} | ||
| 196 | initialBuffer={props.buffer} | ||
| 197 | fontSize={fontSize} | ||
| 198 | themeConfig={buildGhosttyThemeConfig(theme)} | ||
| 199 | onInput={handleNativeInput} | ||
| 200 | onResize={handleNativeResize} | ||
| 201 | /> | ||
| 202 | ); | ||
| 203 | } | ||
| 204 | |||
| 205 | return <FallbackTerminalSurface {...props} fontSize={fontSize} theme={theme} />; | ||
| 206 | }); | ||
| @@ -0,0 +1,52 @@ | |||
| 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
| 2 | |||
| 3 | const expoModulesCoreMocks = vi.hoisted(() => ({ | ||
| 4 | requireNativeViewManager: vi.fn(), | ||
| 5 | })); | ||
| 6 | const nativeView = () => null; | ||
| 7 | const originalExpo = globalThis.expo; | ||
| 8 | |||
| 9 | function setExpoViewConfigAvailable() { | ||
| 10 | globalThis.expo = { | ||
| 11 | getViewConfig: vi.fn().mockReturnValue({ validAttributes: {}, directEventTypes: {} }), | ||
| 12 | } as unknown as typeof globalThis.expo; | ||
| 13 | } | ||
| 14 | |||
| 15 | vi.mock("expo-modules-core", () => ({ | ||
| 16 | requireNativeViewManager: expoModulesCoreMocks.requireNativeViewManager, | ||
| 17 | })); | ||
| 18 | |||
| 19 | describe("resolveNativeTerminalSurfaceView", () => { | ||
| 20 | beforeEach(() => { | ||
| 21 | vi.clearAllMocks(); | ||
| 22 | vi.resetModules(); | ||
| 23 | globalThis.expo = undefined as unknown as typeof globalThis.expo; | ||
| 24 | }); | ||
| 25 | |||
| 26 | afterEach(() => { | ||
| 27 | globalThis.expo = originalExpo; | ||
| 28 | }); | ||
| 29 | |||
| 30 | it("returns null when the native terminal view config is unavailable", async () => { | ||
| 31 | const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule"); | ||
| 32 | expect(resolveNativeTerminalSurfaceView()).toBeNull(); | ||
| 33 | expect(expoModulesCoreMocks.requireNativeViewManager).not.toHaveBeenCalled(); | ||
| 34 | }); | ||
| 35 | |||
| 36 | it("returns the native terminal view when the view config is installed", async () => { | ||
| 37 | setExpoViewConfigAvailable(); | ||
| 38 | expoModulesCoreMocks.requireNativeViewManager.mockReturnValue(nativeView); | ||
| 39 | const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule"); | ||
| 40 | expect(resolveNativeTerminalSurfaceView()).toBe(nativeView); | ||
| 41 | expect(expoModulesCoreMocks.requireNativeViewManager).toHaveBeenCalledWith("T3TerminalSurface"); | ||
| 42 | }); | ||
| 43 | |||
| 44 | it("returns null when the view manager cannot be required", async () => { | ||
| 45 | setExpoViewConfigAvailable(); | ||
| 46 | expoModulesCoreMocks.requireNativeViewManager.mockImplementation(() => { | ||
| 47 | throw new Error("boom"); | ||
| 48 | }); | ||
| 49 | const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule"); | ||
| 50 | expect(resolveNativeTerminalSurfaceView()).toBeNull(); | ||
| 51 | }); | ||
| 52 | }); | ||
| @@ -0,0 +1,65 @@ | |||
| 1 | import type { ComponentType } from "react"; | ||
| 2 | import type { NativeSyntheticEvent, ViewProps } from "react-native"; | ||
| 3 | import { requireNativeViewManager } from "expo-modules-core"; | ||
| 4 | |||
| 5 | const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface"; | ||
| 6 | |||
| 7 | interface ExpoGlobalWithViewConfig { | ||
| 8 | readonly expo?: { | ||
| 9 | getViewConfig?: (moduleName: string, viewName?: string) => unknown; | ||
| 10 | }; | ||
| 11 | } | ||
| 12 | |||
| 13 | interface TerminalInputEvent { | ||
| 14 | readonly data: string; | ||
| 15 | } | ||
| 16 | |||
| 17 | interface TerminalResizeEvent { | ||
| 18 | readonly cols: number; | ||
| 19 | readonly rows: number; | ||
| 20 | } | ||
| 21 | |||
| 22 | export interface NativeTerminalSurfaceProps extends ViewProps { | ||
| 23 | readonly appearanceScheme?: "light" | "dark"; | ||
| 24 | readonly themeConfig?: string; | ||
| 25 | readonly backgroundColor?: string; | ||
| 26 | readonly foregroundColor?: string; | ||
| 27 | readonly mutedForegroundColor?: string; | ||
| 28 | readonly terminalKey: string; | ||
| 29 | readonly initialBuffer: string; | ||
| 30 | readonly fontSize: number; | ||
| 31 | readonly onInput?: (event: NativeSyntheticEvent<TerminalInputEvent>) => void; | ||
| 32 | readonly onResize?: (event: NativeSyntheticEvent<TerminalResizeEvent>) => void; | ||
| 33 | } | ||
| 34 | |||
| 35 | let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined; | ||
| 36 | |||
| 37 | function getExpoViewConfig(moduleName: string) { | ||
| 38 | return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.( | ||
| 39 | moduleName, | ||
| 40 | ); | ||
| 41 | } | ||
| 42 | |||
| 43 | export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminalSurfaceProps> | null { | ||
| 44 | if (cachedNativeTerminalSurfaceView) { | ||
| 45 | return cachedNativeTerminalSurfaceView; | ||
| 46 | } | ||
| 47 | |||
| 48 | if (getExpoViewConfig(NATIVE_TERMINAL_MODULE_NAME) == null) { | ||
| 49 | return null; | ||
| 50 | } | ||
| 51 | |||
| 52 | try { | ||
| 53 | cachedNativeTerminalSurfaceView = requireNativeViewManager<NativeTerminalSurfaceProps>( | ||
| 54 | NATIVE_TERMINAL_MODULE_NAME, | ||
| 55 | ); | ||
| 56 | } catch { | ||
| 57 | return null; | ||
| 58 | } | ||
| 59 | |||
| 60 | return cachedNativeTerminalSurfaceView ?? null; | ||
| 61 | } | ||
| 62 | |||
| 63 | export function hasNativeTerminalSurface() { | ||
| 64 | return resolveNativeTerminalSurfaceView() !== null; | ||
| 65 | } | ||
| @@ -0,0 +1,36 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { buildGhosttyThemeConfig, getGitHubTerminalTheme } from "./terminalTheme"; | ||
| 4 | |||
| 5 | describe("getGitHubTerminalTheme", () => { | ||
| 6 | it("returns the GitHub light terminal palette", () => { | ||
| 7 | expect(getGitHubTerminalTheme("light")).toMatchObject({ | ||
| 8 | background: "#ffffff", | ||
| 9 | foreground: "#586069", | ||
| 10 | cursorForeground: "#005cc5", | ||
| 11 | cursorBackground: "#d1d5da", | ||
| 12 | }); | ||
| 13 | }); | ||
| 14 | |||
| 15 | it("returns the GitHub dark terminal palette", () => { | ||
| 16 | expect(getGitHubTerminalTheme("dark")).toMatchObject({ | ||
| 17 | background: "#24292e", | ||
| 18 | foreground: "#d1d5da", | ||
| 19 | cursorForeground: "#79b8ff", | ||
| 20 | cursorBackground: "#586069", | ||
| 21 | }); | ||
| 22 | }); | ||
| 23 | }); | ||
| 24 | |||
| 25 | describe("buildGhosttyThemeConfig", () => { | ||
| 26 | it("serializes theme colors into a ghostty config file", () => { | ||
| 27 | const config = buildGhosttyThemeConfig(getGitHubTerminalTheme("dark")); | ||
| 28 | |||
| 29 | expect(config).toContain("background = #24292e"); | ||
| 30 | expect(config).toContain("foreground = #d1d5da"); | ||
| 31 | expect(config).toContain("cursor-color = #79b8ff"); | ||
| 32 | expect(config).toContain("palette = 0=#586069"); | ||
| 33 | expect(config).toContain("palette = 15=#fafbfc"); | ||
| 34 | expect(config.endsWith("\n")).toBe(true); | ||
| 35 | }); | ||
| 36 | }); | ||
| @@ -0,0 +1,85 @@ | |||
| 1 | export type TerminalAppearanceScheme = "light" | "dark"; | ||
| 2 | |||
| 3 | export interface TerminalTheme { | ||
| 4 | readonly background: string; | ||
| 5 | readonly foreground: string; | ||
| 6 | readonly mutedForeground: string; | ||
| 7 | readonly border: string; | ||
| 8 | readonly cursorForeground: string; | ||
| 9 | readonly cursorBackground: string; | ||
| 10 | readonly palette: readonly string[]; | ||
| 11 | } | ||
| 12 | |||
| 13 | const GITHUB_LIGHT_THEME: TerminalTheme = { | ||
| 14 | // Sourced from GitHub's official VS Code theme terminal palette. | ||
| 15 | background: "#ffffff", | ||
| 16 | foreground: "#586069", | ||
| 17 | mutedForeground: "#6a737d", | ||
| 18 | border: "rgba(27,31,35,0.12)", | ||
| 19 | cursorForeground: "#005cc5", | ||
| 20 | cursorBackground: "#d1d5da", | ||
| 21 | palette: [ | ||
| 22 | "#24292e", | ||
| 23 | "#d73a49", | ||
| 24 | "#28a745", | ||
| 25 | "#dbab09", | ||
| 26 | "#0366d6", | ||
| 27 | "#5a32a3", | ||
| 28 | "#1b7c83", | ||
| 29 | "#6a737d", | ||
| 30 | "#959da5", | ||
| 31 | "#cb2431", | ||
| 32 | "#22863a", | ||
| 33 | "#b08800", | ||
| 34 | "#005cc5", | ||
| 35 | "#5a32a3", | ||
| 36 | "#3192aa", | ||
| 37 | "#d1d5da", | ||
| 38 | ], | ||
| 39 | }; | ||
| 40 | |||
| 41 | const GITHUB_DARK_THEME: TerminalTheme = { | ||
| 42 | background: "#24292e", | ||
| 43 | foreground: "#d1d5da", | ||
| 44 | mutedForeground: "#959da5", | ||
| 45 | border: "rgba(240,246,252,0.12)", | ||
| 46 | cursorForeground: "#79b8ff", | ||
| 47 | cursorBackground: "#586069", | ||
| 48 | palette: [ | ||
| 49 | "#586069", | ||
| 50 | "#ea4a5a", | ||
| 51 | "#34d058", | ||
| 52 | "#ffea7f", | ||
| 53 | "#2188ff", | ||
| 54 | "#b392f0", | ||
| 55 | "#39c5cf", | ||
| 56 | "#d1d5da", | ||
| 57 | "#959da5", | ||
| 58 | "#f97583", | ||
| 59 | "#85e89d", | ||
| 60 | "#ffea7f", | ||
| 61 | "#79b8ff", | ||
| 62 | "#b392f0", | ||
| 63 | "#56d4dd", | ||
| 64 | "#fafbfc", | ||
| 65 | ], | ||
| 66 | }; | ||
| 67 | |||
| 68 | export function getGitHubTerminalTheme(scheme: TerminalAppearanceScheme): TerminalTheme { | ||
| 69 | return scheme === "light" ? GITHUB_LIGHT_THEME : GITHUB_DARK_THEME; | ||
| 70 | } | ||
| 71 | |||
| 72 | export function buildGhosttyThemeConfig(theme: TerminalTheme): string { | ||
| 73 | const lines = [ | ||
| 74 | `background = ${theme.background}`, | ||
| 75 | `foreground = ${theme.foreground}`, | ||
| 76 | `cursor-color = ${theme.cursorForeground}`, | ||
| 77 | `cursor-text = ${theme.cursorBackground}`, | ||
| 78 | ]; | ||
| 79 | |||
| 80 | for (const [index, color] of theme.palette.entries()) { | ||
| 81 | lines.push(`palette = ${index}=${color}`); | ||
| 82 | } | ||
| 83 | |||
| 84 | return `${lines.join("\n")}\n`; | ||
| 85 | } | ||
| @@ -23,6 +23,7 @@ import { appAtomRegistry } from "./atom-registry"; · 22 unmodified lines | |||
| 23 | import { type ConnectedEnvironmentSummary, type EnvironmentSession } from "./remote-runtime-types"; | 23 | import { type ConnectedEnvironmentSummary, type EnvironmentSession } from "./remote-runtime-types"; |
| 24 | import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime"; | 24 | import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime"; |
| 25 | import { shellSnapshotManager } from "./use-shell-snapshot"; | 25 | import { shellSnapshotManager } from "./use-shell-snapshot"; |
| 26 | import { terminalSessionManager } from "./use-terminal-session"; | ||
| 26 | 27 | ||
| 27 | const environmentSessions = new Map<string, EnvironmentSession>(); | 28 | const environmentSessions = new Map<string, EnvironmentSession>(); |
| 28 | const environmentConnectionListeners = new Set<() => void>(); | 29 | const 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 | }, []); |
| @@ -0,0 +1,63 @@ | |||
| 1 | import { useAtomValue } from "@effect/atom-react"; | ||
| 2 | import { | ||
| 3 | createTerminalSessionManager, | ||
| 4 | EMPTY_TERMINAL_SESSION_ATOM, | ||
| 5 | EMPTY_TERMINAL_SESSION_STATE, | ||
| 6 | getTerminalSessionTargetKey, | ||
| 7 | knownTerminalSessionsAtom, | ||
| 8 | terminalSessionStateAtom, | ||
| 9 | type TerminalSessionState, | ||
| 10 | } from "@t3tools/client-runtime"; | ||
| 11 | import { useMemo } from "react"; | ||
| 12 | |||
| 13 | import { appAtomRegistry } from "./atom-registry"; | ||
| 14 | |||
| 15 | export const terminalSessionManager = createTerminalSessionManager({ | ||
| 16 | getRegistry: () => appAtomRegistry, | ||
| 17 | }); | ||
| 18 | |||
| 19 | export function useTerminalSession(input: { | ||
| 20 | readonly environmentId: string | null; | ||
| 21 | readonly threadId: string | null; | ||
| 22 | readonly terminalId: string | null; | ||
| 23 | }): TerminalSessionState { | ||
| 24 | const targetKey = getTerminalSessionTargetKey(input); | ||
| 25 | const state = useAtomValue( | ||
| 26 | targetKey !== null ? terminalSessionStateAtom(targetKey) : EMPTY_TERMINAL_SESSION_ATOM, | ||
| 27 | ); | ||
| 28 | return targetKey === null ? EMPTY_TERMINAL_SESSION_STATE : state; | ||
| 29 | } | ||
| 30 | |||
| 31 | export function useTerminalSessionTarget(input: { | ||
| 32 | readonly environmentId: string | null; | ||
| 33 | readonly threadId: string | null; | ||
| 34 | readonly terminalId: string | null; | ||
| 35 | }) { | ||
| 36 | return useMemo( | ||
| 37 | () => ({ | ||
| 38 | environmentId: input.environmentId, | ||
| 39 | threadId: input.threadId, | ||
| 40 | terminalId: input.terminalId, | ||
| 41 | }), | ||
| 42 | [input.environmentId, input.threadId, input.terminalId], | ||
| 43 | ); | ||
| 44 | } | ||
| 45 | |||
| 46 | export function useKnownTerminalSessions(input: { | ||
| 47 | readonly environmentId: string | null; | ||
| 48 | readonly threadId: string | null; | ||
| 49 | }) { | ||
| 50 | const knownSessionsIndex = useAtomValue(knownTerminalSessionsAtom); | ||
| 51 | |||
| 52 | return useMemo(() => { | ||
| 53 | if (input.environmentId === null || input.threadId === null) { | ||
| 54 | return []; | ||
| 55 | } | ||
| 56 | |||
| 57 | void knownSessionsIndex; | ||
| 58 | return terminalSessionManager.listSessions({ | ||
| 59 | environmentId: input.environmentId, | ||
| 60 | threadId: input.threadId, | ||
| 61 | }); | ||
| 62 | }, [input.environmentId, input.threadId, knownSessionsIndex]); | ||
| 63 | } | ||
| @@ -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, { |
| @@ -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 = []; |
| @@ -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 | } |
| @@ -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; |
| @@ -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 { |
| @@ -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=="], |
| @@ -11,6 +11,7 @@ export * from "./gitActions.ts"; · 10 unmodified lines | |||
| 11 | export * from "./gitActionState.ts"; | 11 | export * from "./gitActionState.ts"; |
| 12 | export * from "./gitBranchState.ts"; | 12 | export * from "./gitBranchState.ts"; |
| 13 | export * from "./gitStatusState.ts"; | 13 | export * from "./gitStatusState.ts"; |
| 14 | export * from "./terminalSessionState.ts"; | ||
| 14 | export * from "./transportError.ts"; | 15 | export * from "./transportError.ts"; |
| 15 | export * from "./wsRpcProtocol.ts"; | 16 | export * from "./wsRpcProtocol.ts"; |
| 16 | export * from "./wsTransport.ts"; | 17 | export * from "./wsTransport.ts"; |
| @@ -0,0 +1,276 @@ | |||
| 1 | import { AtomRegistry } from "effect/unstable/reactivity"; | ||
| 2 | import { afterEach, describe, expect, it } from "vitest"; | ||
| 3 | |||
| 4 | import type { TerminalSessionSnapshot } from "@t3tools/contracts"; | ||
| 5 | |||
| 6 | import { createTerminalSessionManager } from "./terminalSessionState"; | ||
| 7 | |||
| 8 | let atomRegistry = AtomRegistry.make(); | ||
| 9 | |||
| 10 | function resetAtomRegistry() { | ||
| 11 | atomRegistry.dispose(); | ||
| 12 | atomRegistry = AtomRegistry.make(); | ||
| 13 | } | ||
| 14 | |||
| 15 | const TARGET = { | ||
| 16 | environmentId: "env-local", | ||
| 17 | threadId: "thread-1", | ||
| 18 | terminalId: "default", | ||
| 19 | } as const; | ||
| 20 | |||
| 21 | const BASE_SNAPSHOT: TerminalSessionSnapshot = { | ||
| 22 | threadId: TARGET.threadId, | ||
| 23 | terminalId: TARGET.terminalId, | ||
| 24 | cwd: "/repo", | ||
| 25 | worktreePath: null, | ||
| 26 | status: "running", | ||
| 27 | pid: 123, | ||
| 28 | history: "hello", | ||
| 29 | exitCode: null, | ||
| 30 | exitSignal: null, | ||
| 31 | updatedAt: "2026-04-01T00:00:00.000Z", | ||
| 32 | }; | ||
| 33 | |||
| 34 | describe("createTerminalSessionManager", () => { | ||
| 35 | afterEach(() => { | ||
| 36 | resetAtomRegistry(); | ||
| 37 | }); | ||
| 38 | |||
| 39 | it("hydrates from started snapshots and appends output events", () => { | ||
| 40 | const manager = createTerminalSessionManager({ | ||
| 41 | getRegistry: () => atomRegistry, | ||
| 42 | }); | ||
| 43 | |||
| 44 | manager.applyEvent(TARGET, { | ||
| 45 | type: "started", | ||
| 46 | threadId: TARGET.threadId, | ||
| 47 | terminalId: TARGET.terminalId, | ||
| 48 | createdAt: BASE_SNAPSHOT.updatedAt, | ||
| 49 | snapshot: BASE_SNAPSHOT, | ||
| 50 | }); | ||
| 51 | manager.applyEvent(TARGET, { | ||
| 52 | type: "output", | ||
| 53 | threadId: TARGET.threadId, | ||
| 54 | terminalId: TARGET.terminalId, | ||
| 55 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 56 | data: " world", | ||
| 57 | }); | ||
| 58 | |||
| 59 | expect(manager.getSnapshot(TARGET)).toMatchObject({ | ||
| 60 | snapshot: BASE_SNAPSHOT, | ||
| 61 | buffer: "hello world", | ||
| 62 | status: "running", | ||
| 63 | error: null, | ||
| 64 | updatedAt: "2026-04-01T00:00:01.000Z", | ||
| 65 | }); | ||
| 66 | }); | ||
| 67 | |||
| 68 | it("caps retained output", () => { | ||
| 69 | const manager = createTerminalSessionManager({ | ||
| 70 | getRegistry: () => atomRegistry, | ||
| 71 | maxBufferBytes: 5, | ||
| 72 | }); | ||
| 73 | |||
| 74 | manager.applyEvent(TARGET, { | ||
| 75 | type: "output", | ||
| 76 | threadId: TARGET.threadId, | ||
| 77 | terminalId: TARGET.terminalId, | ||
| 78 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 79 | data: "abcdef", | ||
| 80 | }); | ||
| 81 | |||
| 82 | expect(manager.getSnapshot(TARGET).buffer).toBe("bcdef"); | ||
| 83 | }); | ||
| 84 | |||
| 85 | it("caps retained output by utf-8 byte length", () => { | ||
| 86 | const manager = createTerminalSessionManager({ | ||
| 87 | getRegistry: () => atomRegistry, | ||
| 88 | maxBufferBytes: 4, | ||
| 89 | }); | ||
| 90 | |||
| 91 | manager.applyEvent(TARGET, { | ||
| 92 | type: "output", | ||
| 93 | threadId: TARGET.threadId, | ||
| 94 | terminalId: TARGET.terminalId, | ||
| 95 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 96 | data: "🙂🙂", | ||
| 97 | }); | ||
| 98 | |||
| 99 | expect(manager.getSnapshot(TARGET).buffer).toBe("🙂"); | ||
| 100 | }); | ||
| 101 | |||
| 102 | it("invalidates one environment without clearing others", () => { | ||
| 103 | const manager = createTerminalSessionManager({ | ||
| 104 | getRegistry: () => atomRegistry, | ||
| 105 | }); | ||
| 106 | const otherTarget = { | ||
| 107 | environmentId: "env-remote", | ||
| 108 | threadId: "thread-1", | ||
| 109 | terminalId: "default", | ||
| 110 | } as const; | ||
| 111 | |||
| 112 | for (const target of [TARGET, otherTarget]) { | ||
| 113 | manager.applyEvent(target, { | ||
| 114 | type: "output", | ||
| 115 | threadId: target.threadId, | ||
| 116 | terminalId: target.terminalId, | ||
| 117 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 118 | data: target.environmentId, | ||
| 119 | }); | ||
| 120 | } | ||
| 121 | |||
| 122 | manager.invalidateEnvironment(TARGET.environmentId); | ||
| 123 | |||
| 124 | expect(manager.getSnapshot(TARGET).buffer).toBe(""); | ||
| 125 | expect(manager.getSnapshot(otherTarget).buffer).toBe("env-remote"); | ||
| 126 | }); | ||
| 127 | |||
| 128 | it("lists known sessions for a thread ordered by recency", () => { | ||
| 129 | const manager = createTerminalSessionManager({ | ||
| 130 | getRegistry: () => atomRegistry, | ||
| 131 | }); | ||
| 132 | |||
| 133 | manager.applyEvent(TARGET, { | ||
| 134 | type: "started", | ||
| 135 | threadId: TARGET.threadId, | ||
| 136 | terminalId: TARGET.terminalId, | ||
| 137 | createdAt: "2026-04-01T00:00:00.000Z", | ||
| 138 | snapshot: BASE_SNAPSHOT, | ||
| 139 | }); | ||
| 140 | manager.applyEvent( | ||
| 141 | { | ||
| 142 | environmentId: TARGET.environmentId, | ||
| 143 | }, | ||
| 144 | { | ||
| 145 | type: "started", | ||
| 146 | threadId: TARGET.threadId, | ||
| 147 | terminalId: "term-2", | ||
| 148 | createdAt: "2026-04-01T00:00:02.000Z", | ||
| 149 | snapshot: { | ||
| 150 | ...BASE_SNAPSHOT, | ||
| 151 | terminalId: "term-2", | ||
| 152 | updatedAt: "2026-04-01T00:00:02.000Z", | ||
| 153 | }, | ||
| 154 | }, | ||
| 155 | ); | ||
| 156 | |||
| 157 | expect( | ||
| 158 | manager | ||
| 159 | .listSessions({ | ||
| 160 | environmentId: TARGET.environmentId, | ||
| 161 | threadId: TARGET.threadId, | ||
| 162 | }) | ||
| 163 | .map((session) => session.target.terminalId), | ||
| 164 | ).toEqual(["term-2", "default"]); | ||
| 165 | }); | ||
| 166 | |||
| 167 | it("drops known sessions when an environment is invalidated", () => { | ||
| 168 | const manager = createTerminalSessionManager({ | ||
| 169 | getRegistry: () => atomRegistry, | ||
| 170 | }); | ||
| 171 | |||
| 172 | manager.applyEvent(TARGET, { | ||
| 173 | type: "output", | ||
| 174 | threadId: TARGET.threadId, | ||
| 175 | terminalId: TARGET.terminalId, | ||
| 176 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 177 | data: "hello", | ||
| 178 | }); | ||
| 179 | |||
| 180 | manager.invalidateEnvironment(TARGET.environmentId); | ||
| 181 | |||
| 182 | expect( | ||
| 183 | manager.listSessions({ | ||
| 184 | environmentId: TARGET.environmentId, | ||
| 185 | threadId: TARGET.threadId, | ||
| 186 | }), | ||
| 187 | ).toEqual([]); | ||
| 188 | }); | ||
| 189 | |||
| 190 | it("removes closed sessions from the known-session index while keeping local closed state", () => { | ||
| 191 | const manager = createTerminalSessionManager({ | ||
| 192 | getRegistry: () => atomRegistry, | ||
| 193 | }); | ||
| 194 | |||
| 195 | manager.applyEvent(TARGET, { | ||
| 196 | type: "started", | ||
| 197 | threadId: TARGET.threadId, | ||
| 198 | terminalId: TARGET.terminalId, | ||
| 199 | createdAt: BASE_SNAPSHOT.updatedAt, | ||
| 200 | snapshot: BASE_SNAPSHOT, | ||
| 201 | }); | ||
| 202 | |||
| 203 | manager.applyEvent(TARGET, { | ||
| 204 | type: "closed", | ||
| 205 | threadId: TARGET.threadId, | ||
| 206 | terminalId: TARGET.terminalId, | ||
| 207 | createdAt: "2026-04-01T00:00:04.000Z", | ||
| 208 | }); | ||
| 209 | |||
| 210 | expect( | ||
| 211 | manager.listSessions({ | ||
| 212 | environmentId: TARGET.environmentId, | ||
| 213 | threadId: TARGET.threadId, | ||
| 214 | }), | ||
| 215 | ).toEqual([]); | ||
| 216 | expect(manager.getSnapshot(TARGET)).toMatchObject({ | ||
| 217 | buffer: "hello", | ||
| 218 | status: "closed", | ||
| 219 | snapshot: null, | ||
| 220 | updatedAt: "2026-04-01T00:00:04.000Z", | ||
| 221 | }); | ||
| 222 | }); | ||
| 223 | |||
| 224 | it("clears locally retained closed state on reset", () => { | ||
| 225 | const manager = createTerminalSessionManager({ | ||
| 226 | getRegistry: () => atomRegistry, | ||
| 227 | }); | ||
| 228 | |||
| 229 | manager.applyEvent(TARGET, { | ||
| 230 | type: "started", | ||
| 231 | threadId: TARGET.threadId, | ||
| 232 | terminalId: TARGET.terminalId, | ||
| 233 | createdAt: BASE_SNAPSHOT.updatedAt, | ||
| 234 | snapshot: BASE_SNAPSHOT, | ||
| 235 | }); | ||
| 236 | manager.applyEvent(TARGET, { | ||
| 237 | type: "closed", | ||
| 238 | threadId: TARGET.threadId, | ||
| 239 | terminalId: TARGET.terminalId, | ||
| 240 | createdAt: "2026-04-01T00:00:04.000Z", | ||
| 241 | }); | ||
| 242 | |||
| 243 | manager.reset(); | ||
| 244 | |||
| 245 | expect(manager.getSnapshot(TARGET)).toEqual({ | ||
| 246 | snapshot: null, | ||
| 247 | buffer: "", | ||
| 248 | status: "closed", | ||
| 249 | error: null, | ||
| 250 | hasRunningSubprocess: false, | ||
| 251 | updatedAt: null, | ||
| 252 | version: 0, | ||
| 253 | }); | ||
| 254 | }); | ||
| 255 | |||
| 256 | it("syncs snapshots returned from open calls immediately", () => { | ||
| 257 | const manager = createTerminalSessionManager({ | ||
| 258 | getRegistry: () => atomRegistry, | ||
| 259 | }); | ||
| 260 | |||
| 261 | manager.syncSnapshot( | ||
| 262 | { environmentId: TARGET.environmentId }, | ||
| 263 | { | ||
| 264 | ...BASE_SNAPSHOT, | ||
| 265 | history: "prompt$ ", | ||
| 266 | updatedAt: "2026-04-01T00:00:03.000Z", | ||
| 267 | }, | ||
| 268 | ); | ||
| 269 | |||
| 270 | expect(manager.getSnapshot(TARGET)).toMatchObject({ | ||
| 271 | buffer: "prompt$ ", | ||
| 272 | status: "running", | ||
| 273 | updatedAt: "2026-04-01T00:00:03.000Z", | ||
| 274 | }); | ||
| 275 | }); | ||
| 276 | }); | ||
| @@ -0,0 +1,404 @@ | |||
| 1 | import type { TerminalEvent, TerminalSessionSnapshot } from "@t3tools/contracts"; | ||
| 2 | import { Atom, type AtomRegistry } from "effect/unstable/reactivity"; | ||
| 3 | |||
| 4 | export interface TerminalSessionState { | ||
| 5 | readonly snapshot: TerminalSessionSnapshot | null; | ||
| 6 | readonly buffer: string; | ||
| 7 | readonly status: TerminalSessionSnapshot["status"] | "closed"; | ||
| 8 | readonly error: string | null; | ||
| 9 | readonly hasRunningSubprocess: boolean; | ||
| 10 | readonly updatedAt: string | null; | ||
| 11 | readonly version: number; | ||
| 12 | } | ||
| 13 | |||
| 14 | export interface TerminalSessionTarget { | ||
| 15 | readonly environmentId: string | null; | ||
| 16 | readonly threadId: string | null; | ||
| 17 | readonly terminalId: string | null; | ||
| 18 | } | ||
| 19 | |||
| 20 | export interface KnownTerminalSessionTarget { | ||
| 21 | readonly environmentId: string; | ||
| 22 | readonly threadId: string; | ||
| 23 | readonly terminalId: string; | ||
| 24 | } | ||
| 25 | |||
| 26 | export interface KnownTerminalSession { | ||
| 27 | readonly target: KnownTerminalSessionTarget; | ||
| 28 | readonly state: TerminalSessionState; | ||
| 29 | } | ||
| 30 | |||
| 31 | export interface TerminalSessionManagerConfig { | ||
| 32 | readonly getRegistry: () => AtomRegistry.AtomRegistry; | ||
| 33 | readonly maxBufferBytes?: number; | ||
| 34 | } | ||
| 35 | |||
| 36 | export const EMPTY_TERMINAL_SESSION_STATE = Object.freeze<TerminalSessionState>({ | ||
| 37 | snapshot: null, | ||
| 38 | buffer: "", | ||
| 39 | status: "closed", | ||
| 40 | error: null, | ||
| 41 | hasRunningSubprocess: false, | ||
| 42 | updatedAt: null, | ||
| 43 | version: 0, | ||
| 44 | }); | ||
| 45 | |||
| 46 | const DEFAULT_MAX_BUFFER_BYTES = 512 * 1024; | ||
| 47 | const knownTerminalSessionKeys = new Set<string>(); | ||
| 48 | const textEncoder = new TextEncoder(); | ||
| 49 | const textDecoder = new TextDecoder(); | ||
| 50 | |||
| 51 | export const terminalSessionStateAtom = Atom.family((key: string) => { | ||
| 52 | knownTerminalSessionKeys.add(key); | ||
| 53 | return Atom.make(EMPTY_TERMINAL_SESSION_STATE).pipe( | ||
| 54 | Atom.keepAlive, | ||
| 55 | Atom.withLabel(`terminal-session:${key}`), | ||
| 56 | ); | ||
| 57 | }); | ||
| 58 | |||
| 59 | export const EMPTY_TERMINAL_SESSION_ATOM = Atom.make(EMPTY_TERMINAL_SESSION_STATE).pipe( | ||
| 60 | Atom.keepAlive, | ||
| 61 | Atom.withLabel("terminal-session:null"), | ||
| 62 | ); | ||
| 63 | |||
| 64 | export const knownTerminalSessionsAtom = Atom.make<Record<string, KnownTerminalSessionTarget>>( | ||
| 65 | {}, | ||
| 66 | ).pipe(Atom.keepAlive, Atom.withLabel("terminal-session:index")); | ||
| 67 | |||
| 68 | export function getTerminalSessionTargetKey(target: TerminalSessionTarget): string | null { | ||
| 69 | if (target.environmentId === null || target.threadId === null || target.terminalId === null) { | ||
| 70 | return null; | ||
| 71 | } | ||
| 72 | |||
| 73 | return `${target.environmentId}:${target.threadId}:${target.terminalId}`; | ||
| 74 | } | ||
| 75 | |||
| 76 | function toKnownTarget(target: TerminalSessionTarget): KnownTerminalSessionTarget | null { | ||
| 77 | const targetKey = getTerminalSessionTargetKey(target); | ||
| 78 | if (targetKey === null) { | ||
| 79 | return null; | ||
| 80 | } | ||
| 81 | |||
| 82 | const environmentId = target.environmentId; | ||
| 83 | const threadId = target.threadId; | ||
| 84 | const terminalId = target.terminalId; | ||
| 85 | if (environmentId === null || threadId === null || terminalId === null) { | ||
| 86 | return null; | ||
| 87 | } | ||
| 88 | |||
| 89 | return { | ||
| 90 | environmentId, | ||
| 91 | threadId, | ||
| 92 | terminalId, | ||
| 93 | }; | ||
| 94 | } | ||
| 95 | |||
| 96 | function trimBufferToBytes(buffer: string, maxBufferBytes: number): string { | ||
| 97 | if (maxBufferBytes <= 0) { | ||
| 98 | return ""; | ||
| 99 | } | ||
| 100 | |||
| 101 | const encoded = textEncoder.encode(buffer); | ||
| 102 | if (encoded.byteLength <= maxBufferBytes) { | ||
| 103 | return buffer; | ||
| 104 | } | ||
| 105 | |||
| 106 | let start = encoded.byteLength - maxBufferBytes; | ||
| 107 | while (start < encoded.length) { | ||
| 108 | const byte = encoded[start]; | ||
| 109 | if (byte === undefined || (byte & 0b1100_0000) !== 0b1000_0000) { | ||
| 110 | break; | ||
| 111 | } | ||
| 112 | start += 1; | ||
| 113 | } | ||
| 114 | |||
| 115 | return textDecoder.decode(encoded.subarray(start)); | ||
| 116 | } | ||
| 117 | |||
| 118 | function stateFromSnapshot( | ||
| 119 | snapshot: TerminalSessionSnapshot, | ||
| 120 | maxBufferBytes: number, | ||
| 121 | ): TerminalSessionState { | ||
| 122 | return { | ||
| 123 | snapshot, | ||
| 124 | buffer: trimBufferToBytes(snapshot.history, maxBufferBytes), | ||
| 125 | status: snapshot.status, | ||
| 126 | error: null, | ||
| 127 | hasRunningSubprocess: false, | ||
| 128 | updatedAt: snapshot.updatedAt, | ||
| 129 | version: 1, | ||
| 130 | }; | ||
| 131 | } | ||
| 132 | |||
| 133 | export function createTerminalSessionManager(config: TerminalSessionManagerConfig) { | ||
| 134 | const maxBufferBytes = config.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES; | ||
| 135 | const closedSessionStates = new Map<string, TerminalSessionState>(); | ||
| 136 | |||
| 137 | function clearKnownSessionAtomState(targetKey: string): void { | ||
| 138 | knownTerminalSessionKeys.delete(targetKey); | ||
| 139 | config.getRegistry().set(terminalSessionStateAtom(targetKey), EMPTY_TERMINAL_SESSION_STATE); | ||
| 140 | } | ||
| 141 | |||
| 142 | function rememberTarget(target: TerminalSessionTarget): string | null { | ||
| 143 | const targetKey = getTerminalSessionTargetKey(target); | ||
| 144 | const knownTarget = toKnownTarget(target); | ||
| 145 | if (targetKey === null || knownTarget === null) { | ||
| 146 | return null; | ||
| 147 | } | ||
| 148 | |||
| 149 | closedSessionStates.delete(targetKey); | ||
| 150 | |||
| 151 | const current = config.getRegistry().get(knownTerminalSessionsAtom); | ||
| 152 | const existing = current[targetKey]; | ||
| 153 | if ( | ||
| 154 | existing?.environmentId === knownTarget.environmentId && | ||
| 155 | existing.threadId === knownTarget.threadId && | ||
| 156 | existing.terminalId === knownTarget.terminalId | ||
| 157 | ) { | ||
| 158 | return targetKey; | ||
| 159 | } | ||
| 160 | |||
| 161 | config.getRegistry().set(knownTerminalSessionsAtom, { | ||
| 162 | ...current, | ||
| 163 | [targetKey]: knownTarget, | ||
| 164 | }); | ||
| 165 | return targetKey; | ||
| 166 | } | ||
| 167 | |||
| 168 | function removeTargets( | ||
| 169 | match: (target: KnownTerminalSessionTarget) => boolean, | ||
| 170 | ): ReadonlyArray<string> { | ||
| 171 | const current = config.getRegistry().get(knownTerminalSessionsAtom); | ||
| 172 | const removedKeys: string[] = []; | ||
| 173 | const next = Object.fromEntries( | ||
| 174 | Object.entries(current).filter(([key, target]) => { | ||
| 175 | const shouldRemove = match(target); | ||
| 176 | if (shouldRemove) { | ||
| 177 | removedKeys.push(key); | ||
| 178 | } | ||
| 179 | return !shouldRemove; | ||
| 180 | }), | ||
| 181 | ) as Record<string, KnownTerminalSessionTarget>; | ||
| 182 | if (Object.keys(next).length === Object.keys(current).length) { | ||
| 183 | return removedKeys; | ||
| 184 | } | ||
| 185 | |||
| 186 | config.getRegistry().set(knownTerminalSessionsAtom, next); | ||
| 187 | return removedKeys; | ||
| 188 | } | ||
| 189 | |||
| 190 | function getSnapshot(target: TerminalSessionTarget): TerminalSessionState { | ||
| 191 | const targetKey = getTerminalSessionTargetKey(target); | ||
| 192 | if (targetKey === null) { | ||
| 193 | return EMPTY_TERMINAL_SESSION_STATE; | ||
| 194 | } | ||
| 195 | |||
| 196 | const closedState = closedSessionStates.get(targetKey); | ||
| 197 | if (closedState) { | ||
| 198 | return closedState; | ||
| 199 | } | ||
| 200 | |||
| 201 | const rememberedTargetKey = rememberTarget(target); | ||
| 202 | if (rememberedTargetKey === null) { | ||
| 203 | return EMPTY_TERMINAL_SESSION_STATE; | ||
| 204 | } | ||
| 205 | |||
| 206 | return config.getRegistry().get(terminalSessionStateAtom(rememberedTargetKey)); | ||
| 207 | } | ||
| 208 | |||
| 209 | function setState(targetKey: string, nextState: TerminalSessionState): void { | ||
| 210 | config.getRegistry().set(terminalSessionStateAtom(targetKey), nextState); | ||
| 211 | } | ||
| 212 | |||
| 213 | function syncSnapshot( | ||
| 214 | target: Pick<TerminalSessionTarget, "environmentId">, | ||
| 215 | snapshot: TerminalSessionSnapshot, | ||
| 216 | ): void { | ||
| 217 | const targetKey = rememberTarget({ | ||
| 218 | environmentId: target.environmentId, | ||
| 219 | threadId: snapshot.threadId, | ||
| 220 | terminalId: snapshot.terminalId, | ||
| 221 | }); | ||
| 222 | if (targetKey === null) { | ||
| 223 | return; | ||
| 224 | } | ||
| 225 | |||
| 226 | setState(targetKey, stateFromSnapshot(snapshot, maxBufferBytes)); | ||
| 227 | } | ||
| 228 | |||
| 229 | function applyEvent( | ||
| 230 | target: Pick<TerminalSessionTarget, "environmentId">, | ||
| 231 | event: TerminalEvent, | ||
| 232 | ): void { | ||
| 233 | const targetKey = rememberTarget({ | ||
| 234 | environmentId: target.environmentId, | ||
| 235 | threadId: event.threadId, | ||
| 236 | terminalId: event.terminalId, | ||
| 237 | }); | ||
| 238 | if (targetKey === null) { | ||
| 239 | return; | ||
| 240 | } | ||
| 241 | |||
| 242 | const current = config.getRegistry().get(terminalSessionStateAtom(targetKey)); | ||
| 243 | switch (event.type) { | ||
| 244 | case "started": | ||
| 245 | case "restarted": | ||
| 246 | setState(targetKey, stateFromSnapshot(event.snapshot, maxBufferBytes)); | ||
| 247 | return; | ||
| 248 | case "output": | ||
| 249 | setState(targetKey, { | ||
| 250 | ...current, | ||
| 251 | buffer: trimBufferToBytes(`${current.buffer}${event.data}`, maxBufferBytes), | ||
| 252 | status: current.status === "closed" ? "running" : current.status, | ||
| 253 | error: null, | ||
| 254 | updatedAt: event.createdAt, | ||
| 255 | version: current.version + 1, | ||
| 256 | }); | ||
| 257 | return; | ||
| 258 | case "cleared": | ||
| 259 | setState(targetKey, { | ||
| 260 | ...current, | ||
| 261 | buffer: "", | ||
| 262 | error: null, | ||
| 263 | updatedAt: event.createdAt, | ||
| 264 | version: current.version + 1, | ||
| 265 | }); | ||
| 266 | return; | ||
| 267 | case "exited": | ||
| 268 | setState(targetKey, { | ||
| 269 | ...current, | ||
| 270 | snapshot: current.snapshot | ||
| 271 | ? { | ||
| 272 | ...current.snapshot, | ||
| 273 | status: "exited", | ||
| 274 | exitCode: event.exitCode, | ||
| 275 | exitSignal: event.exitSignal, | ||
| 276 | updatedAt: event.createdAt, | ||
| 277 | } | ||
| 278 | : null, | ||
| 279 | status: "exited", | ||
| 280 | hasRunningSubprocess: false, | ||
| 281 | updatedAt: event.createdAt, | ||
| 282 | version: current.version + 1, | ||
| 283 | }); | ||
| 284 | return; | ||
| 285 | case "closed": | ||
| 286 | closedSessionStates.set(targetKey, { | ||
| 287 | ...current, | ||
| 288 | snapshot: null, | ||
| 289 | status: "closed", | ||
| 290 | error: null, | ||
| 291 | hasRunningSubprocess: false, | ||
| 292 | updatedAt: event.createdAt, | ||
| 293 | version: current.version + 1, | ||
| 294 | }); | ||
| 295 | clearKnownSessionAtomState(targetKey); | ||
| 296 | removeTargets( | ||
| 297 | (knownTarget) => | ||
| 298 | knownTarget.environmentId === target.environmentId && | ||
| 299 | knownTarget.threadId === event.threadId && | ||
| 300 | knownTarget.terminalId === event.terminalId, | ||
| 301 | ); | ||
| 302 | return; | ||
| 303 | case "error": | ||
| 304 | setState(targetKey, { | ||
| 305 | ...current, | ||
| 306 | status: "error", | ||
| 307 | error: event.message, | ||
| 308 | hasRunningSubprocess: false, | ||
| 309 | updatedAt: event.createdAt, | ||
| 310 | version: current.version + 1, | ||
| 311 | }); | ||
| 312 | return; | ||
| 313 | case "activity": | ||
| 314 | setState(targetKey, { | ||
| 315 | ...current, | ||
| 316 | hasRunningSubprocess: event.hasRunningSubprocess, | ||
| 317 | updatedAt: event.createdAt, | ||
| 318 | version: current.version + 1, | ||
| 319 | }); | ||
| 320 | return; | ||
| 321 | } | ||
| 322 | } | ||
| 323 | |||
| 324 | function invalidate(target?: TerminalSessionTarget): void { | ||
| 325 | if (target) { | ||
| 326 | const targetKey = getTerminalSessionTargetKey(target); | ||
| 327 | if (targetKey !== null) { | ||
| 328 | closedSessionStates.delete(targetKey); | ||
| 329 | setState(targetKey, EMPTY_TERMINAL_SESSION_STATE); | ||
| 330 | } | ||
| 331 | return; | ||
| 332 | } | ||
| 333 | |||
| 334 | for (const key of knownTerminalSessionKeys) { | ||
| 335 | setState(key, EMPTY_TERMINAL_SESSION_STATE); | ||
| 336 | } | ||
| 337 | closedSessionStates.clear(); | ||
| 338 | knownTerminalSessionKeys.clear(); | ||
| 339 | config.getRegistry().set(knownTerminalSessionsAtom, {}); | ||
| 340 | } | ||
| 341 | |||
| 342 | function invalidateEnvironment(environmentId: string): void { | ||
| 343 | const prefix = `${environmentId}:`; | ||
| 344 | for (const key of knownTerminalSessionKeys) { | ||
| 345 | if (key.startsWith(prefix)) { | ||
| 346 | setState(key, EMPTY_TERMINAL_SESSION_STATE); | ||
| 347 | } | ||
| 348 | } | ||
| 349 | for (const key of closedSessionStates.keys()) { | ||
| 350 | if (key.startsWith(prefix)) { | ||
| 351 | closedSessionStates.delete(key); | ||
| 352 | } | ||
| 353 | } | ||
| 354 | for (const key of removeTargets((target) => target.environmentId === environmentId)) { | ||
| 355 | closedSessionStates.delete(key); | ||
| 356 | clearKnownSessionAtomState(key); | ||
| 357 | } | ||
| 358 | } | ||
| 359 | |||
| 360 | function reset(): void { | ||
| 361 | invalidate(); | ||
| 362 | } | ||
| 363 | |||
| 364 | function listSessions( | ||
| 365 | filter?: Partial<KnownTerminalSessionTarget>, | ||
| 366 | ): ReadonlyArray<KnownTerminalSession> { | ||
| 367 | const knownTargets = Object.values(config.getRegistry().get(knownTerminalSessionsAtom)); | ||
| 368 | return knownTargets | ||
| 369 | .filter((target) => { | ||
| 370 | if (filter?.environmentId && target.environmentId !== filter.environmentId) { | ||
| 371 | return false; | ||
| 372 | } | ||
| 373 | if (filter?.threadId && target.threadId !== filter.threadId) { | ||
| 374 | return false; | ||
| 375 | } | ||
| 376 | if (filter?.terminalId && target.terminalId !== filter.terminalId) { | ||
| 377 | return false; | ||
| 378 | } | ||
| 379 | return true; | ||
| 380 | }) | ||
| 381 | .map((target) => ({ | ||
| 382 | target, | ||
| 383 | state: getSnapshot(target), | ||
| 384 | })) | ||
| 385 | .sort((left, right) => { | ||
| 386 | const leftUpdatedAt = left.state.updatedAt ? Date.parse(left.state.updatedAt) : 0; | ||
| 387 | const rightUpdatedAt = right.state.updatedAt ? Date.parse(right.state.updatedAt) : 0; | ||
| 388 | if (leftUpdatedAt !== rightUpdatedAt) { | ||
| 389 | return rightUpdatedAt - leftUpdatedAt; | ||
| 390 | } | ||
| 391 | return left.target.terminalId.localeCompare(right.target.terminalId); | ||
| 392 | }); | ||
| 393 | } | ||
| 394 | |||
| 395 | return { | ||
| 396 | applyEvent, | ||
| 397 | getSnapshot, | ||
| 398 | invalidate, | ||
| 399 | invalidateEnvironment, | ||
| 400 | listSessions, | ||
| 401 | syncSnapshot, | ||
| 402 | reset, | ||
| 403 | }; | ||
| 404 | } | ||
| @@ -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, { |
| @@ -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 | ||
| 122 | const TerminalClosedEvent = Schema.Struct({ | ||
| 123 | ...TerminalEventBaseSchema.fields, | ||
| 124 | type: Schema.Literal("closed"), | ||
| 125 | }); | ||
| 126 | |||
| 122 | const TerminalErrorEvent = Schema.Struct({ | 127 | const 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, |