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 | 125 | "expo-secure-store", |
| 118 | 126 | "expo-router", |
| 119 | 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 | 30 | "config:dev": "APP_VARIANT=development expo config", |
| 31 | 31 | "config:preview": "APP_VARIANT=preview expo config", |
| 32 | 32 | "config:prod": "APP_VARIANT=production expo config", |
| 33 | + "test": "vitest run", | |
| 33 | 34 | "typecheck": "tsc --noEmit" |
| 34 | 35 | }, |
| 35 | 36 | "dependencies": { |
| @@ -45,11 +46,13 @@ · 9 unmodified lines | ||
| 45 | 46 | "@shikijs/themes": "3.23.0", |
| 46 | 47 | "@t3tools/client-runtime": "workspace:*", |
| 47 | 48 | "@t3tools/contracts": "workspace:*", |
| 49 | + "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal", | |
| 48 | 50 | "@t3tools/shared": "workspace:*", |
| 49 | 51 | "clsx": "^2.1.1", |
| 50 | 52 | "diff": "8.0.3", |
| 51 | 53 | "effect": "catalog:", |
| 52 | 54 | "expo": "~55.0.14", |
| 55 | + "expo-build-properties": "~55.0.13", | |
| 53 | 56 | "expo-camera": "~55.0.15", |
| 54 | 57 | "expo-clipboard": "~55.0.9", |
| 55 | 58 | "expo-constants": "^55.0.9", |
| @@ -60,6 +63,7 @@ · 4 unmodified lines | ||
| 60 | 63 | "expo-haptics": "^55.0.9", |
| 61 | 64 | "expo-image-picker": "~55.0.14", |
| 62 | 65 | "expo-linking": "~55.0.12", |
| 66 | + "expo-modules-core": "~55.0.22", | |
| 63 | 67 | "expo-paste-input": "^0.1.15", |
| 64 | 68 | "expo-router": "~55.0.12", |
| 65 | 69 | "expo-secure-store": "~55.0.9", |
| @@ -65,6 +65,16 @@ export default function ThreadLayout() { · 64 unmodified lines | ||
| 65 | 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 | 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 | 2 | import { |
| 3 | 3 | type GitActionRequestInput, |
| 4 | 4 | requiresDefaultBranchConfirmation, |
| @@ -9,6 +9,14 @@ import Stack from "expo-router/stack"; · 4 unmodified lines | ||
| 9 | 9 | import { useCallback, useMemo } from "react"; |
| 10 | 10 | import { Alert, Linking } from "react-native"; |
| 11 | 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 | 21 | function truncateMiddle(value: string, maxLength: number): string { |
| 14 | 22 | if (value.length <= maxLength) { |
| @@ -55,6 +63,12 @@ export function ThreadGitControls(props: { · 40 unmodified lines | ||
| 55 | 63 | readonly currentBranch: string | null; |
| 56 | 64 | readonly gitStatus: GitStatusResult | null; |
| 57 | 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 | 72 | readonly onPull: () => Promise<void>; |
| 59 | 73 | readonly onRunAction: (input: GitActionRequestInput) => Promise<GitRunStackedActionResult | null>; |
| 60 | 74 | }) { |
| @@ -167,6 +181,54 @@ export function ThreadGitControls(props: { · 106 unmodified lines | ||
| 167 | 181 | |
| 168 | 182 | return ( |
| 169 | 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 | 232 | <Stack.Toolbar.Menu icon="point.topleft.down.curvedto.point.bottomright.up"> |
| 171 | 233 | <Stack.Toolbar.MenuAction |
| 172 | 234 | icon="point.topleft.down.curvedto.point.bottomright.up" |
| @@ -3,6 +3,8 @@ import { useCallback, useMemo, useState } from "react"; · 2 unmodified lines | ||
| 3 | 3 | import * as Arr from "effect/Array"; |
| 4 | 4 | import * as Option from "effect/Option"; |
| 5 | 5 | import { pipe } from "effect/Function"; |
| 6 | +import type { ProjectScript } from "@t3tools/contracts"; | |
| 7 | +import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; | |
| 6 | 8 | import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native"; |
| 7 | 9 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 8 | 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 | 13 | import { EmptyState } from "../../components/EmptyState"; |
| 12 | 14 | import { LoadingScreen } from "../../components/LoadingScreen"; |
| 13 | −import { buildThreadRoutePath } from "../../lib/routes"; | |
| 15 | +import { buildThreadRoutePath, buildThreadTerminalRoutePath } from "../../lib/routes"; | |
| 14 | 16 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 15 | 17 | import { connectionTone } from "../connection/connectionTone"; |
| 16 | 18 | |
| 17 | 19 | import { useRemoteCatalog } from "../../state/use-remote-catalog"; |
| 18 | 20 | import { |
| 21 | + getEnvironmentClient, | |
| 19 | 22 | useRemoteConnectionStatus, |
| 20 | 23 | useRemoteEnvironmentState, |
| 21 | 24 | } from "../../state/use-remote-environment-registry"; |
| 25 | +import { terminalSessionManager, useKnownTerminalSessions } from "../../state/use-terminal-session"; | |
| 22 | 26 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; |
| 23 | 27 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 24 | 28 | import { GitActionProgressOverlay } from "./GitActionProgressOverlay"; |
| 29 | +import { | |
| 30 | + buildTerminalMenuSessions, | |
| 31 | + nextTerminalId, | |
| 32 | + resolveProjectScriptTerminalId, | |
| 33 | +} from "./terminalMenu"; | |
| 34 | +import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext"; | |
| 25 | 35 | import { ThreadDetailScreen } from "./ThreadDetailScreen"; |
| 26 | 36 | import { ThreadGitControls } from "./ThreadGitControls"; |
| 27 | 37 | import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; |
| 28 | 38 | import { useSelectedThreadCommands } from "./use-selected-thread-commands"; |
| 29 | 39 | import { useSelectedThreadGitActions } from "./use-selected-thread-git-actions"; |
| 30 | 40 | import { useSelectedThreadGitState } from "./use-selected-thread-git-state"; |
| 41 | +import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; | |
| 31 | 42 | import { useThreadComposerState } from "./use-thread-composer-state"; |
| 32 | 43 | |
| 33 | 44 | function firstRouteParam(value: string | string[] | undefined): string | null { |
| @@ -47,6 +58,7 @@ export function ThreadRouteScreen() { · 13 unmodified lines | ||
| 47 | 58 | const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = |
| 48 | 59 | useThreadSelection(); |
| 49 | 60 | const selectedThreadDetail = useSelectedThreadDetail(); |
| 61 | + const { selectedThreadCwd } = useSelectedThreadWorktree(); | |
| 50 | 62 | const composer = useThreadComposerState(); |
| 51 | 63 | const gitState = useSelectedThreadGitState(); |
| 52 | 64 | const gitActions = useSelectedThreadGitActions(); |
| @@ -80,20 +92,29 @@ export function ThreadRouteScreen() { · 27 unmodified lines | ||
| 80 | 92 | /* ─── Git status for native header trigger ───────────────────────── */ |
| 81 | 93 | const gitStatus = useGitStatus({ |
| 82 | 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 | 111 | /* ─── Git action progress (for overlay banner) ──────────────────── */ |
| 87 | 112 | const gitActionProgressTarget = useMemo( |
| 88 | 113 | () => ({ |
| 89 | 114 | environmentId: selectedThread?.environmentId ?? null, |
| 90 | − cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null, | |
| 115 | + cwd: selectedThreadCwd, | |
| 91 | 116 | }), |
| 92 | − [ | |
| 93 | − selectedThread?.environmentId, | |
| 94 | − selectedThread?.worktreePath, | |
| 95 | − selectedThreadProject?.workspaceRoot, | |
| 96 | − ], | |
| 117 | + [selectedThread?.environmentId, selectedThreadCwd], | |
| 97 | 118 | ); |
| 98 | 119 | const gitActionProgress = useGitActionProgress(gitActionProgressTarget); |
| 99 | 120 | |
| @@ -101,9 +122,9 @@ export function ThreadRouteScreen() { · 1 unmodified line | ||
| 101 | 122 | if (!selectedThread) return; |
| 102 | 123 | await gitStatusManager.refresh({ |
| 103 | 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 | 129 | /** Wraps thread refresh + git status refresh for pull-to-refresh */ |
| 109 | 130 | const handleRefreshAll = useCallback(async () => { |
| @@ -119,6 +140,83 @@ export function ThreadRouteScreen() { · 9 unmodified lines | ||
| 119 | 140 | void router.push("/connections"); |
| 120 | 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 | 220 | if (!environmentId || !threadId) { |
| 123 | 221 | return <LoadingScreen message="Opening thread…" />; |
| 124 | 222 | } |
| @@ -223,6 +321,12 @@ export function ThreadRouteScreen() { · 98 unmodified lines | ||
| 223 | 321 | currentBranch={selectedThreadDetail.branch} |
| 224 | 322 | gitStatus={gitStatus.data} |
| 225 | 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 | 330 | onPull={gitActions.onPullSelectedThreadBranch} |
| 227 | 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 | 10 | import { useThreadSelection } from "../../../state/use-thread-selection"; |
| 11 | 11 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; |
| 12 | 12 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; |
| 13 | +import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | |
| 13 | 14 | import { SheetActionButton } from "./gitSheetComponents"; |
| 14 | 15 | |
| 15 | 16 | export function GitBranchesSheet() { |
| 16 | 17 | const router = useRouter(); |
| 17 | 18 | const insets = useSafeAreaInsets(); |
| 18 | − const { selectedThread, selectedThreadProject } = useThreadSelection(); | |
| 19 | + const { selectedThread } = useThreadSelection(); | |
| 20 | + const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); | |
| 19 | 21 | const gitState = useSelectedThreadGitState(); |
| 20 | 22 | const gitActions = useSelectedThreadGitActions(); |
| 21 | 23 | |
| @@ -27,11 +29,11 @@ export function GitBranchesSheet() { · 5 unmodified lines | ||
| 27 | 29 | |
| 28 | 30 | const gitStatus = useGitStatus({ |
| 29 | 31 | environmentId: selectedThread?.environmentId ?? "", |
| 30 | − cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null, | |
| 32 | + cwd: selectedThreadCwd, | |
| 31 | 33 | }); |
| 32 | 34 | |
| 33 | 35 | const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD"; |
| 34 | − const currentWorktreePath = selectedThread?.worktreePath ?? null; | |
| 36 | + const currentWorktreePath = selectedThreadWorktreePath; | |
| 35 | 37 | const availableBranches = gitState.selectedThreadBranches; |
| 36 | 38 | const branchesLoading = gitState.selectedThreadBranchesLoading; |
| 37 | 39 | const busy = gitState.gitOperationLabel !== null; |
| @@ -9,13 +9,15 @@ import { useGitStatus } from "../../../state/use-git-status"; · 8 unmodified lines | ||
| 9 | 9 | import { useThreadSelection } from "../../../state/use-thread-selection"; |
| 10 | 10 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; |
| 11 | 11 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; |
| 12 | +import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | |
| 12 | 13 | import { SheetActionButton } from "./gitSheetComponents"; |
| 13 | 14 | |
| 14 | 15 | export function GitCommitSheet() { |
| 15 | 16 | const router = useRouter(); |
| 16 | 17 | const insets = useSafeAreaInsets(); |
| 17 | 18 | const isDarkMode = useColorScheme() === "dark"; |
| 18 | − const { selectedThread, selectedThreadProject } = useThreadSelection(); | |
| 19 | + const { selectedThread } = useThreadSelection(); | |
| 20 | + const { selectedThreadCwd } = useSelectedThreadWorktree(); | |
| 19 | 21 | const gitState = useSelectedThreadGitState(); |
| 20 | 22 | const gitActions = useSelectedThreadGitActions(); |
| 21 | 23 | |
| @@ -27,7 +29,7 @@ export function GitCommitSheet() { · 5 unmodified lines | ||
| 27 | 29 | |
| 28 | 30 | const gitStatus = useGitStatus({ |
| 29 | 31 | environmentId: selectedThread?.environmentId ?? "", |
| 30 | − cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null, | |
| 32 | + cwd: selectedThreadCwd, | |
| 31 | 33 | }); |
| 32 | 34 | |
| 33 | 35 | const busy = gitState.gitOperationLabel !== null; |
| @@ -17,6 +17,7 @@ import { useGitStatus } from "../../../state/use-git-status"; · 16 unmodified lines | ||
| 17 | 17 | import { useThreadSelection } from "../../../state/use-thread-selection"; |
| 18 | 18 | import { useSelectedThreadGitActions } from "../use-selected-thread-git-actions"; |
| 19 | 19 | import { useSelectedThreadGitState } from "../use-selected-thread-git-state"; |
| 20 | +import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | |
| 20 | 21 | import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents"; |
| 21 | 22 | |
| 22 | 23 | export function GitOverviewSheet() { |
| @@ -26,7 +27,8 @@ export function GitOverviewSheet() { · 3 unmodified lines | ||
| 26 | 27 | environmentId: string; |
| 27 | 28 | threadId: string; |
| 28 | 29 | }>(); |
| 29 | − const { selectedThread, selectedThreadProject } = useThreadSelection(); | |
| 30 | + const { selectedThread } = useThreadSelection(); | |
| 31 | + const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); | |
| 30 | 32 | const gitState = useSelectedThreadGitState(); |
| 31 | 33 | const gitActions = useSelectedThreadGitActions(); |
| 32 | 34 | |
| @@ -35,11 +37,11 @@ export function GitOverviewSheet() { · 2 unmodified lines | ||
| 35 | 37 | |
| 36 | 38 | const gitStatus = useGitStatus({ |
| 37 | 39 | environmentId: selectedThread?.environmentId ?? "", |
| 38 | − cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null, | |
| 40 | + cwd: selectedThreadCwd, | |
| 39 | 41 | }); |
| 40 | 42 | |
| 41 | 43 | const currentBranchLabel = gitStatus.data?.branch ?? selectedThread?.branch ?? "Detached HEAD"; |
| 42 | − const currentWorktreePath = selectedThread?.worktreePath ?? null; | |
| 44 | + const currentWorktreePath = selectedThreadWorktreePath; | |
| 43 | 45 | const gitOperationLabel = gitState.gitOperationLabel; |
| 44 | 46 | const busy = gitOperationLabel !== null; |
| 45 | 47 | const isRepo = gitStatus.data?.isRepo ?? true; |
| @@ -23,8 +23,8 @@ import { cn } from "../../../lib/cn"; · 22 unmodified lines | ||
| 23 | 23 | import { useThemeColor } from "../../../lib/useThemeColor"; |
| 24 | 24 | import { getEnvironmentClient } from "../../../state/use-remote-environment-registry"; |
| 25 | 25 | import { useSelectedThreadDetail } from "../../../state/use-thread-detail"; |
| 26 | −import { useThreadSelection } from "../../../state/use-thread-selection"; | |
| 27 | 26 | import { useThreadDraftForThread } from "../use-thread-composer-state"; |
| 27 | +import { useSelectedThreadWorktree } from "../use-selected-thread-worktree"; | |
| 28 | 28 | import { |
| 29 | 29 | getCachedReviewParsedDiff, |
| 30 | 30 | setReviewGitSections, |
| @@ -445,7 +445,6 @@ export function ReviewSheet() { · 414 unmodified lines | ||
| 445 | 445 | }>(); |
| 446 | 446 | const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); |
| 447 | 447 | const reviewCache = useReviewCacheForThread({ environmentId, threadId }); |
| 448 | − const { selectedThreadProject } = useThreadSelection(); | |
| 449 | 448 | const selectedThread = useSelectedThreadDetail(); |
| 450 | 449 | const [loadingTurnIds, setLoadingTurnIds] = useState<Record<string, boolean>>({}); |
| 451 | 450 | const [loadingGitDiffs, setLoadingGitDiffs] = useState(false); |
| @@ -464,8 +463,9 @@ export function ReviewSheet() { · 12 unmodified lines | ||
| 464 | 463 | const highlightQueueActiveRef = useRef(false); |
| 465 | 464 | const highlightableFilesByIdRef = useRef<ReadonlyMap<string, ReviewRenderableFile>>(new Map()); |
| 466 | 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 | 469 | const readyCheckpoints = useMemo( |
| 470 | 470 | () => getReadyReviewCheckpoints(selectedThread?.checkpoints ?? []), |
| 471 | 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 | 25 | import { gitBranchManager } from "../../state/use-git-branches"; |
| 26 | 26 | import { gitStatusManager } from "../../state/use-git-status"; |
| 27 | 27 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 28 | +import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; | |
| 28 | 29 | |
| 29 | 30 | export function useSelectedThreadGitActions() { |
| 30 | 31 | const { selectedThread, selectedThreadProject } = useThreadSelection(); |
| 32 | + const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); | |
| 31 | 33 | |
| 32 | 34 | const selectedThreadGitRootCwd = selectedThreadProject?.workspaceRoot ?? null; |
| 33 | 35 | |
| @@ -61,8 +63,7 @@ export function useSelectedThreadGitActions() { · 27 unmodified lines | ||
| 61 | 63 | return null; |
| 62 | 64 | } |
| 63 | 65 | |
| 64 | − const cwd = | |
| 65 | − options?.cwd ?? selectedThread.worktreePath ?? selectedThreadProject.workspaceRoot; | |
| 66 | + const cwd = options?.cwd ?? selectedThreadCwd; | |
| 66 | 67 | if (!cwd) { |
| 67 | 68 | return null; |
| 68 | 69 | } |
| @@ -86,7 +87,7 @@ export function useSelectedThreadGitActions() { · 17 unmodified lines | ||
| 86 | 87 | return null; |
| 87 | 88 | } |
| 88 | 89 | }, |
| 89 | − [selectedThread, selectedThreadProject], | |
| 90 | + [selectedThread, selectedThreadCwd, selectedThreadProject], | |
| 90 | 91 | ); |
| 91 | 92 | |
| 92 | 93 | useEffect(() => { |
| @@ -109,7 +110,7 @@ export function useSelectedThreadGitActions() { · 16 unmodified lines | ||
| 109 | 110 | return null; |
| 110 | 111 | } |
| 111 | 112 | |
| 112 | − const cwd = selectedThread.worktreePath ?? selectedThreadProject.workspaceRoot; | |
| 113 | + const cwd = selectedThreadCwd; | |
| 113 | 114 | if (!cwd) { |
| 114 | 115 | return null; |
| 115 | 116 | } |
| @@ -128,7 +129,7 @@ export function useSelectedThreadGitActions() { · 12 unmodified lines | ||
| 128 | 129 | return null; |
| 129 | 130 | } |
| 130 | 131 | }, |
| 131 | − [selectedThread, selectedThreadProject], | |
| 132 | + [selectedThread, selectedThreadCwd, selectedThreadProject], | |
| 132 | 133 | ); |
| 133 | 134 | |
| 134 | 135 | const refreshSelectedThreadBranches = useCallback(async (): Promise<ReadonlyArray<GitBranch>> => { |
| @@ -204,12 +205,12 @@ export function useSelectedThreadGitActions() { · 69 unmodified lines | ||
| 204 | 205 | cwd, |
| 205 | 206 | nextThreadState: { |
| 206 | 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 | 216 | const onCreateSelectedThreadBranch = useCallback( |
| @@ -227,12 +228,12 @@ export function useSelectedThreadGitActions() { · 11 unmodified lines | ||
| 227 | 228 | cwd, |
| 228 | 229 | nextThreadState: { |
| 229 | 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 | 239 | const onCreateSelectedThreadWorktree = useCallback( |
| @@ -316,7 +317,7 @@ export function useSelectedThreadGitActions() { · 77 unmodified lines | ||
| 316 | 317 | cwd, |
| 317 | 318 | nextThreadState: { |
| 318 | 319 | branch: result.branch.name, |
| 319 | − worktreePath: thread.worktreePath, | |
| 320 | + worktreePath: selectedThreadWorktreePath, | |
| 320 | 321 | }, |
| 321 | 322 | }); |
| 322 | 323 | return result; |
| @@ -326,7 +327,12 @@ export function useSelectedThreadGitActions() { · 3 unmodified lines | ||
| 326 | 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 | 338 | return { |
| @@ -5,20 +5,18 @@ import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git"; · 4 unmodified lines | ||
| 5 | 5 | import { useGitActionState } from "../../state/use-git-action-state"; |
| 6 | 6 | import { useGitBranches } from "../../state/use-git-branches"; |
| 7 | 7 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 8 | +import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; | |
| 8 | 9 | |
| 9 | 10 | export function useSelectedThreadGitState() { |
| 10 | 11 | const { selectedThread, selectedThreadProject } = useThreadSelection(); |
| 12 | + const { selectedThreadCwd } = useSelectedThreadWorktree(); | |
| 11 | 13 | |
| 12 | 14 | const selectedThreadGitTarget = useMemo( |
| 13 | 15 | () => ({ |
| 14 | 16 | environmentId: selectedThread?.environmentId ?? null, |
| 15 | − cwd: selectedThread?.worktreePath ?? selectedThreadProject?.workspaceRoot ?? null, | |
| 17 | + cwd: selectedThreadCwd, | |
| 16 | 18 | }), |
| 17 | − [ | |
| 18 | − selectedThread?.environmentId, | |
| 19 | − selectedThread?.worktreePath, | |
| 20 | − selectedThreadProject?.workspaceRoot, | |
| 21 | − ], | |
| 19 | + [selectedThread?.environmentId, selectedThreadCwd], | |
| 22 | 20 | ); |
| 23 | 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 | 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 | 44 | export function dismissRoute(router: Router) { |
| 33 | 45 | if (router.canGoBack()) { |
| 34 | 46 | router.back(); |
| @@ -5,6 +5,11 @@ import * as SecureStore from "expo-secure-store"; · 4 unmodified lines | ||
| 5 | 5 | import type { SavedRemoteConnection } from "./connection"; |
| 6 | 6 | |
| 7 | 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 | 14 | async function readStorageItem(key: string): Promise<string | null> { |
| 10 | 15 | return await SecureStore.getItemAsync(key); |
| @@ -14,23 +19,31 @@ async function writeStorageItem(key: string, value: string): Promise<void> { · 3 unmodified lines | ||
| 14 | 19 | await SecureStore.setItemAsync(key, value); |
| 15 | 20 | } |
| 16 | 21 | |
| 17 | −export async function loadSavedConnections(): Promise<ReadonlyArray<SavedRemoteConnection>> { | |
| 18 | − const raw = (await readStorageItem(CONNECTIONS_KEY)) ?? ""; | |
| 22 | +async function readJsonStorageItem<T>(key: string): Promise<T | null> { | |
| 23 | + const raw = (await readStorageItem(key)) ?? ""; | |
| 19 | 24 | if (!raw.trim()) { |
| 20 | − return []; | |
| 25 | + return null; | |
| 21 | 26 | } |
| 22 | 27 | |
| 23 | 28 | try { |
| 24 | − const parsed = JSON.parse(raw) as { | |
| 25 | − readonly connections?: ReadonlyArray<SavedRemoteConnection>; | |
| 26 | − }; | |
| 27 | − return pipe( | |
| 28 | − parsed.connections ?? [], | |
| 29 | − Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()), | |
| 30 | − ); | |
| 29 | + return JSON.parse(raw) as T; | |
| 31 | 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 | 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 | 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 | 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 | 23 | import { type ConnectedEnvironmentSummary, type EnvironmentSession } from "./remote-runtime-types"; |
| 24 | 24 | import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime"; |
| 25 | 25 | import { shellSnapshotManager } from "./use-shell-snapshot"; |
| 26 | +import { terminalSessionManager } from "./use-terminal-session"; | |
| 26 | 27 | |
| 27 | 28 | const environmentSessions = new Map<string, EnvironmentSession>(); |
| 28 | 29 | const environmentConnectionListeners = new Set<() => void>(); |
| @@ -154,6 +155,7 @@ export async function disconnectEnvironment( · 125 unmodified lines | ||
| 154 | 155 | notifyEnvironmentConnectionListeners(); |
| 155 | 156 | await session?.connection.dispose(); |
| 156 | 157 | shellSnapshotManager.invalidate({ environmentId }); |
| 158 | + terminalSessionManager.invalidateEnvironment(environmentId); | |
| 157 | 159 | environmentRuntimeManager.invalidate({ environmentId }); |
| 158 | 160 | |
| 159 | 161 | if (options?.removeSaved) { |
| @@ -244,7 +246,9 @@ export async function connectSavedEnvironment( · 84 unmodified lines | ||
| 244 | 246 | onShellResubscribe: (environmentId) => { |
| 245 | 247 | shellSnapshotManager.markPending({ environmentId }); |
| 246 | 248 | }, |
| 247 | − applyTerminalEvent: () => undefined, | |
| 249 | + applyTerminalEvent: (event, environmentId) => { | |
| 250 | + terminalSessionManager.applyEvent({ environmentId }, event); | |
| 251 | + }, | |
| 248 | 252 | onConfigSnapshot: (serverConfig) => { |
| 249 | 253 | environmentRuntimeManager.patch({ environmentId: connection.environmentId }, (runtime) => ({ |
| 250 | 254 | ...runtime, |
| @@ -333,6 +337,7 @@ export function useRemoteEnvironmentBootstrap() { · 82 unmodified lines | ||
| 333 | 337 | environmentSessions.clear(); |
| 334 | 338 | environmentRuntimeManager.invalidate(); |
| 335 | 339 | shellSnapshotManager.invalidate(); |
| 340 | + terminalSessionManager.invalidate(); | |
| 336 | 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 | 469 | it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () => |
| 444 | 470 | Effect.gen(function* () { |
| 445 | 471 | const { manager, getEvents, baseDir } = yield* createManager(); |
| @@ -778,6 +804,21 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", ( · 332 unmodified lines | ||
| 778 | 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 | 822 | it.effect("evicts oldest inactive terminal sessions when retention limit is exceeded", () => |
| 782 | 823 | Effect.gen(function* () { |
| 783 | 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 | 1485 | yield* flushPersist(threadId, terminalId); |
| 1486 | 1486 | |
| 1487 | − yield* modifyManagerState((state) => { | |
| 1487 | + const removed = yield* modifyManagerState((state) => { | |
| 1488 | 1488 | if (!state.sessions.has(key)) { |
| 1489 | − return [undefined, state] as const; | |
| 1489 | + return [false, state] as const; | |
| 1490 | 1490 | } |
| 1491 | 1491 | const sessions = new Map(state.sessions); |
| 1492 | 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 | 1505 | if (deleteHistoryOnClose) { |
| 1497 | 1506 | yield* deleteHistory(threadId, terminalId); |
| 1498 | 1507 | } |
| @@ -1683,11 +1692,19 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 184 unmodified lines | ||
| 1683 | 1692 | const targetCols = input.cols ?? liveSession.cols; |
| 1684 | 1693 | const targetRows = input.rows ?? liveSession.rows; |
| 1685 | 1694 | const runtimeEnvChanged = !Equal.equals(currentRuntimeEnv, nextRuntimeEnv); |
| 1686 | − | |
| 1687 | − if (liveSession.cwd !== input.cwd || runtimeEnvChanged) { | |
| 1695 | + const nextWorktreePath = | |
| 1696 | + input.worktreePath !== undefined | |
| 1697 | + ? (input.worktreePath ?? null) | |
| 1698 | + : liveSession.worktreePath; | |
| 1699 | + const launchContextChanged = | |
| 1700 | + liveSession.cwd !== input.cwd || | |
| 1701 | + runtimeEnvChanged || | |
| 1702 | + liveSession.worktreePath !== nextWorktreePath; | |
| 1703 | + | |
| 1704 | + if (launchContextChanged) { | |
| 1688 | 1705 | yield* stopProcess(liveSession); |
| 1689 | 1706 | liveSession.cwd = input.cwd; |
| 1690 | − liveSession.worktreePath = input.worktreePath ?? null; | |
| 1707 | + liveSession.worktreePath = nextWorktreePath; | |
| 1691 | 1708 | liveSession.runtimeEnv = nextRuntimeEnv; |
| 1692 | 1709 | liveSession.history = ""; |
| 1693 | 1710 | liveSession.pendingHistoryControlSequence = ""; |
| @@ -1701,7 +1718,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith · 7 unmodified lines | ||
| 1701 | 1718 | ); |
| 1702 | 1719 | } else if (liveSession.status === "exited" || liveSession.status === "error") { |
| 1703 | 1720 | liveSession.runtimeEnv = nextRuntimeEnv; |
| 1704 | − liveSession.worktreePath = input.worktreePath ?? null; | |
| 1721 | + liveSession.worktreePath = nextWorktreePath; | |
| 1705 | 1722 | liveSession.history = ""; |
| 1706 | 1723 | liveSession.pendingHistoryControlSequence = ""; |
| 1707 | 1724 | liveSession.pendingProcessEvents = []; |
| @@ -602,16 +602,21 @@ export function TerminalViewport({ · 601 unmodified lines | ||
| 602 | 602 | return; |
| 603 | 603 | } |
| 604 | 604 | |
| 605 | − const details = [ | |
| 606 | − typeof event.exitCode === "number" ? `code ${event.exitCode}` : null, | |
| 607 | − typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null, | |
| 608 | − ] | |
| 609 | − .filter((value): value is string => value !== null) | |
| 610 | − .join(", "); | |
| 611 | − writeSystemMessage( | |
| 612 | − activeTerminal, | |
| 613 | − details.length > 0 ? `Process exited (${details})` : "Process exited", | |
| 614 | − ); | |
| 605 | + if (event.type === "closed") { | |
| 606 | + writeSystemMessage(activeTerminal, "Terminal closed"); | |
| 607 | + } else { | |
| 608 | + const details = [ | |
| 609 | + typeof event.exitCode === "number" ? `code ${event.exitCode}` : null, | |
| 610 | + typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null, | |
| 611 | + ] | |
| 612 | + .filter((value): value is string => value !== null) | |
| 613 | + .join(", "); | |
| 614 | + writeSystemMessage( | |
| 615 | + activeTerminal, | |
| 616 | + details.length > 0 ? `Process exited (${details})` : "Process exited", | |
| 617 | + ); | |
| 618 | + } | |
| 619 | + | |
| 615 | 620 | if (hasHandledExitRef.current) { |
| 616 | 621 | return; |
| 617 | 622 | } |
| @@ -7,6 +7,7 @@ export function terminalRunningSubprocessFromEvent(event: TerminalEvent): boolea · 6 unmodified lines | ||
| 7 | 7 | case "started": |
| 8 | 8 | case "restarted": |
| 9 | 9 | case "exited": |
| 10 | + case "closed": | |
| 10 | 11 | return false; |
| 11 | 12 | default: |
| 12 | 13 | return null; |
| @@ -34,6 +34,8 @@ function makeTerminalEvent( · 33 unmodified lines | ||
| 34 | 34 | return { ...base, type, ...overrides } as TerminalEvent; |
| 35 | 35 | case "exited": |
| 36 | 36 | return { ...base, type, exitCode: 0, exitSignal: null, ...overrides } as TerminalEvent; |
| 37 | + case "closed": | |
| 38 | + return { ...base, type, ...overrides } as TerminalEvent; | |
| 37 | 39 | case "started": |
| 38 | 40 | case "restarted": |
| 39 | 41 | return { |
| @@ -57,11 +57,13 @@ · 56 unmodified lines | ||
| 57 | 57 | "@shikijs/themes": "3.23.0", |
| 58 | 58 | "@t3tools/client-runtime": "workspace:*", |
| 59 | 59 | "@t3tools/contracts": "workspace:*", |
| 60 | + "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal", | |
| 60 | 61 | "@t3tools/shared": "workspace:*", |
| 61 | 62 | "clsx": "^2.1.1", |
| 62 | 63 | "diff": "8.0.3", |
| 63 | 64 | "effect": "catalog:", |
| 64 | 65 | "expo": "~55.0.14", |
| 66 | + "expo-build-properties": "~55.0.13", | |
| 65 | 67 | "expo-camera": "~55.0.15", |
| 66 | 68 | "expo-clipboard": "~55.0.9", |
| 67 | 69 | "expo-constants": "^55.0.9", |
| @@ -72,6 +74,7 @@ · 4 unmodified lines | ||
| 72 | 74 | "expo-haptics": "^55.0.9", |
| 73 | 75 | "expo-image-picker": "~55.0.14", |
| 74 | 76 | "expo-linking": "~55.0.12", |
| 77 | + "expo-modules-core": "~55.0.22", | |
| 75 | 78 | "expo-paste-input": "^0.1.15", |
| 76 | 79 | "expo-router": "~55.0.12", |
| 77 | 80 | "expo-secure-store": "~55.0.9", |
| @@ -1662,6 +1665,8 @@ · 1584 unmodified lines | ||
| 1662 | 1665 | |
| 1663 | 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 | 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 | 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 | 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 | 3042 | "@tailwindcss/node/tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], |
| 3036 | 3043 | |
| 3037 | 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 | 11 | export * from "./gitActionState.ts"; |
| 12 | 12 | export * from "./gitBranchState.ts"; |
| 13 | 13 | export * from "./gitStatusState.ts"; |
| 14 | +export * from "./terminalSessionState.ts"; | |
| 14 | 15 | export * from "./transportError.ts"; |
| 15 | 16 | export * from "./wsRpcProtocol.ts"; |
| 16 | 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 | 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 | 211 | it("accepts activity events", () => { |
| 201 | 212 | expect( |
| 202 | 213 | decodes(TerminalEvent, { |
| @@ -119,6 +119,11 @@ const TerminalExitedEvent = Schema.Struct({ · 118 unmodified lines | ||
| 119 | 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 | 127 | const TerminalErrorEvent = Schema.Struct({ |
| 123 | 128 | ...TerminalEventBaseSchema.fields, |
| 124 | 129 | type: Schema.Literal("error"), |
| @@ -146,6 +151,7 @@ export const TerminalEvent = Schema.Union([ · 21 unmodified lines | ||
| 146 | 151 | TerminalStartedEvent, |
| 147 | 152 | TerminalOutputEvent, |
| 148 | 153 | TerminalExitedEvent, |
| 154 | + TerminalClosedEvent, | |
| 149 | 155 | TerminalErrorEvent, |
| 150 | 156 | TerminalClearedEvent, |
| 151 | 157 | TerminalRestartedEvent, |