| 1 | #include <jni.h> |
| 2 | |
| 3 | #include <algorithm> |
| 4 | #include <cstdint> |
| 5 | #include <mutex> |
| 6 | #include <utility> |
| 7 | #include <vector> |
| 8 | |
| 9 | #include <ghostty/vt.h> |
| 10 | |
| 11 | namespace { |
| 12 | |
| 13 | constexpr uint32_t kSnapshotMagic = 0x54563354; // "T3VT" in little endian. |
| 14 | constexpr uint16_t kSnapshotVersion = 1; |
| 15 | constexpr size_t kMaxScrollbackRows = 10000; |
| 16 | |
| 17 | enum CellFlag : uint16_t { |
| 18 | kBold = 1 << 0, |
| 19 | kItalic = 1 << 1, |
| 20 | kFaint = 1 << 2, |
| 21 | kInverse = 1 << 3, |
| 22 | kInvisible = 1 << 4, |
| 23 | kStrikethrough = 1 << 5, |
| 24 | kOverline = 1 << 6, |
| 25 | kUnderline = 1 << 7, |
| 26 | kSelected = 1 << 8, |
| 27 | }; |
| 28 | |
| 29 | struct Session { |
| 30 | GhosttyTerminal terminal = nullptr; |
| 31 | GhosttyRenderState render_state = nullptr; |
| 32 | GhosttyRenderStateRowIterator row_iterator = nullptr; |
| 33 | GhosttyRenderStateRowCells row_cells = nullptr; |
| 34 | std::vector<uint8_t> responses; |
| 35 | std::mutex mutex; |
| 36 | }; |
| 37 | |
| 38 | class ByteWriter { |
| 39 | public: |
| 40 | explicit ByteWriter(size_t capacity) { bytes_.reserve(capacity); } |
| 41 | |
| 42 | void U8(uint8_t value) { bytes_.push_back(value); } |
| 43 | |
| 44 | void U16(uint16_t value) { |
| 45 | U8(static_cast<uint8_t>(value)); |
| 46 | U8(static_cast<uint8_t>(value >> 8)); |
| 47 | } |
| 48 | |
| 49 | void U32(uint32_t value) { |
| 50 | U16(static_cast<uint16_t>(value)); |
| 51 | U16(static_cast<uint16_t>(value >> 16)); |
| 52 | } |
| 53 | |
| 54 | void Bytes(const std::vector<uint8_t>& value) { |
| 55 | bytes_.insert(bytes_.end(), value.begin(), value.end()); |
| 56 | } |
| 57 | |
| 58 | std::vector<uint8_t> Take() { return std::move(bytes_); } |
| 59 | |
| 60 | private: |
| 61 | std::vector<uint8_t> bytes_; |
| 62 | }; |
| 63 | |
| 64 | Session* FromHandle(jlong handle) { |
| 65 | return reinterpret_cast<Session*>(static_cast<intptr_t>(handle)); |
| 66 | } |
| 67 | |
| 68 | jbyteArray ToJavaBytes(JNIEnv* env, const std::vector<uint8_t>& bytes) { |
| 69 | auto result = env->NewByteArray(static_cast<jsize>(bytes.size())); |
| 70 | if (result != nullptr && !bytes.empty()) { |
| 71 | env->SetByteArrayRegion(result, 0, static_cast<jsize>(bytes.size()), |
| 72 | reinterpret_cast<const jbyte*>(bytes.data())); |
| 73 | } |
| 74 | return result; |
| 75 | } |
| 76 | |
| 77 | GhosttyColorRgb RgbFromArgb(jint color) { |
| 78 | const auto value = static_cast<uint32_t>(color); |
| 79 | return { |
| 80 | .r = static_cast<uint8_t>(value >> 16), |
| 81 | .g = static_cast<uint8_t>(value >> 8), |
| 82 | .b = static_cast<uint8_t>(value), |
| 83 | }; |
| 84 | } |
| 85 | |
| 86 | uint32_t ArgbFromRgb(GhosttyColorRgb color) { |
| 87 | return 0xFF000000U | (static_cast<uint32_t>(color.r) << 16U) | |
| 88 | (static_cast<uint32_t>(color.g) << 8U) | color.b; |
| 89 | } |
| 90 | |
| 91 | GhosttyColorRgb Blend(GhosttyColorRgb foreground, GhosttyColorRgb background, |
| 92 | uint8_t foreground_weight) { |
| 93 | const auto blend = [foreground_weight](uint8_t front, uint8_t back) { |
| 94 | const uint16_t back_weight = 255 - foreground_weight; |
| 95 | return static_cast<uint8_t>((front * foreground_weight + back * back_weight) / 255); |
| 96 | }; |
| 97 | return { |
| 98 | .r = blend(foreground.r, background.r), |
| 99 | .g = blend(foreground.g, background.g), |
| 100 | .b = blend(foreground.b, background.b), |
| 101 | }; |
| 102 | } |
| 103 | |
| 104 | void AppendUtf8(std::vector<uint8_t>* output, uint32_t codepoint) { |
| 105 | if (codepoint <= 0x7F) { |
| 106 | output->push_back(static_cast<uint8_t>(codepoint)); |
| 107 | } else if (codepoint <= 0x7FF) { |
| 108 | output->push_back(static_cast<uint8_t>(0xC0 | (codepoint >> 6))); |
| 109 | output->push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3F))); |
| 110 | } else if (codepoint <= 0xFFFF && !(codepoint >= 0xD800 && codepoint <= 0xDFFF)) { |
| 111 | output->push_back(static_cast<uint8_t>(0xE0 | (codepoint >> 12))); |
| 112 | output->push_back(static_cast<uint8_t>(0x80 | ((codepoint >> 6) & 0x3F))); |
| 113 | output->push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3F))); |
| 114 | } else if (codepoint <= 0x10FFFF) { |
| 115 | output->push_back(static_cast<uint8_t>(0xF0 | (codepoint >> 18))); |
| 116 | output->push_back(static_cast<uint8_t>(0x80 | ((codepoint >> 12) & 0x3F))); |
| 117 | output->push_back(static_cast<uint8_t>(0x80 | ((codepoint >> 6) & 0x3F))); |
| 118 | output->push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3F))); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | void OnWritePty(GhosttyTerminal, void* userdata, const uint8_t* data, size_t len) { |
| 123 | auto* session = static_cast<Session*>(userdata); |
| 124 | if (session == nullptr || data == nullptr || len == 0) return; |
| 125 | session->responses.insert(session->responses.end(), data, data + len); |
| 126 | } |
| 127 | |
| 128 | void ApplyTheme(Session* session, jint foreground, jint background, jint cursor, |
| 129 | JNIEnv* env, jintArray palette_array) { |
| 130 | auto foreground_rgb = RgbFromArgb(foreground); |
| 131 | auto background_rgb = RgbFromArgb(background); |
| 132 | auto cursor_rgb = RgbFromArgb(cursor); |
| 133 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, |
| 134 | &foreground_rgb); |
| 135 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, |
| 136 | &background_rgb); |
| 137 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, &cursor_rgb); |
| 138 | |
| 139 | if (palette_array == nullptr) return; |
| 140 | const auto palette_length = env->GetArrayLength(palette_array); |
| 141 | if (palette_length <= 0) return; |
| 142 | |
| 143 | GhosttyColorRgb palette[256]; |
| 144 | if (ghostty_terminal_get(session->terminal, |
| 145 | GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT, |
| 146 | palette) != GHOSTTY_SUCCESS) { |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | const auto copied_length = std::min<jsize>(palette_length, 256); |
| 151 | std::vector<jint> colors(static_cast<size_t>(copied_length)); |
| 152 | env->GetIntArrayRegion(palette_array, 0, copied_length, colors.data()); |
| 153 | for (jsize index = 0; index < copied_length; ++index) { |
| 154 | palette[index] = RgbFromArgb(colors[index]); |
| 155 | } |
| 156 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, palette); |
| 157 | } |
| 158 | |
| 159 | void FreeSession(Session* session) { |
| 160 | if (session == nullptr) return; |
| 161 | ghostty_render_state_row_cells_free(session->row_cells); |
| 162 | ghostty_render_state_row_iterator_free(session->row_iterator); |
| 163 | ghostty_render_state_free(session->render_state); |
| 164 | ghostty_terminal_free(session->terminal); |
| 165 | delete session; |
| 166 | } |
| 167 | |
| 168 | std::vector<uint8_t> DrainResponses(Session* session) { |
| 169 | std::vector<uint8_t> responses; |
| 170 | responses.swap(session->responses); |
| 171 | return responses; |
| 172 | } |
| 173 | |
| 174 | bool ViewportGridRef(Session* session, jint x, jint y, GhosttyGridRef* out) { |
| 175 | *out = GhosttyGridRef{}; |
| 176 | out->size = sizeof(*out); |
| 177 | GhosttyPoint point{}; |
| 178 | point.tag = GHOSTTY_POINT_TAG_VIEWPORT; |
| 179 | point.value.coordinate.x = static_cast<uint16_t>(std::max<jint>(x, 0)); |
| 180 | point.value.coordinate.y = static_cast<uint32_t>(std::max<jint>(y, 0)); |
| 181 | return ghostty_terminal_grid_ref(session->terminal, point, out) == GHOSTTY_SUCCESS; |
| 182 | } |
| 183 | |
| 184 | uint16_t StyleFlags(const GhosttyStyle& style, bool selected) { |
| 185 | uint16_t flags = 0; |
| 186 | if (style.bold) flags |= kBold; |
| 187 | if (style.italic) flags |= kItalic; |
| 188 | if (style.faint) flags |= kFaint; |
| 189 | if (style.inverse) flags |= kInverse; |
| 190 | if (style.invisible) flags |= kInvisible; |
| 191 | if (style.strikethrough) flags |= kStrikethrough; |
| 192 | if (style.overline) flags |= kOverline; |
| 193 | if (style.underline != 0) flags |= kUnderline; |
| 194 | if (selected) flags |= kSelected; |
| 195 | return flags; |
| 196 | } |
| 197 | |
| 198 | } // namespace |
| 199 | |
| 200 | extern "C" JNIEXPORT jlong JNICALL |
| 201 | Java_expo_modules_t3terminal_GhosttyBridge_nativeCreate( |
| 202 | JNIEnv* env, jclass, jint cols, jint rows, jint cell_width, jint cell_height, |
| 203 | jint foreground, jint background, jint cursor, jintArray palette) { |
| 204 | auto* session = new Session(); |
| 205 | GhosttyTerminalOptions options = { |
| 206 | .cols = static_cast<uint16_t>(std::clamp(cols, 1, 65535)), |
| 207 | .rows = static_cast<uint16_t>(std::clamp(rows, 1, 65535)), |
| 208 | .max_scrollback = kMaxScrollbackRows, |
| 209 | }; |
| 210 | if (ghostty_terminal_new(nullptr, &session->terminal, options) != GHOSTTY_SUCCESS || |
| 211 | ghostty_render_state_new(nullptr, &session->render_state) != GHOSTTY_SUCCESS || |
| 212 | ghostty_render_state_row_iterator_new(nullptr, &session->row_iterator) != GHOSTTY_SUCCESS || |
| 213 | ghostty_render_state_row_cells_new(nullptr, &session->row_cells) != GHOSTTY_SUCCESS) { |
| 214 | FreeSession(session); |
| 215 | return 0; |
| 216 | } |
| 217 | |
| 218 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_USERDATA, session); |
| 219 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, |
| 220 | reinterpret_cast<const void*>(OnWritePty)); |
| 221 | ApplyTheme(session, foreground, background, cursor, env, palette); |
| 222 | ghostty_terminal_resize(session->terminal, options.cols, options.rows, |
| 223 | static_cast<uint32_t>(std::max(cell_width, 1)), |
| 224 | static_cast<uint32_t>(std::max(cell_height, 1))); |
| 225 | return static_cast<jlong>(reinterpret_cast<intptr_t>(session)); |
| 226 | } |
| 227 | |
| 228 | extern "C" JNIEXPORT void JNICALL |
| 229 | Java_expo_modules_t3terminal_GhosttyBridge_nativeDestroy(JNIEnv*, jclass, jlong handle) { |
| 230 | FreeSession(FromHandle(handle)); |
| 231 | } |
| 232 | |
| 233 | extern "C" JNIEXPORT jbyteArray JNICALL |
| 234 | Java_expo_modules_t3terminal_GhosttyBridge_nativeFeed(JNIEnv* env, jclass, jlong handle, |
| 235 | jbyteArray data) { |
| 236 | auto* session = FromHandle(handle); |
| 237 | if (session == nullptr || data == nullptr) return env->NewByteArray(0); |
| 238 | std::lock_guard<std::mutex> lock(session->mutex); |
| 239 | const auto length = env->GetArrayLength(data); |
| 240 | std::vector<uint8_t> bytes(static_cast<size_t>(length)); |
| 241 | if (length > 0) { |
| 242 | env->GetByteArrayRegion(data, 0, length, reinterpret_cast<jbyte*>(bytes.data())); |
| 243 | ghostty_terminal_vt_write(session->terminal, bytes.data(), bytes.size()); |
| 244 | } |
| 245 | return ToJavaBytes(env, DrainResponses(session)); |
| 246 | } |
| 247 | |
| 248 | extern "C" JNIEXPORT jbyteArray JNICALL |
| 249 | Java_expo_modules_t3terminal_GhosttyBridge_nativeResize( |
| 250 | JNIEnv* env, jclass, jlong handle, jint cols, jint rows, jint cell_width, jint cell_height) { |
| 251 | auto* session = FromHandle(handle); |
| 252 | if (session == nullptr) return env->NewByteArray(0); |
| 253 | std::lock_guard<std::mutex> lock(session->mutex); |
| 254 | ghostty_terminal_resize(session->terminal, |
| 255 | static_cast<uint16_t>(std::clamp(cols, 1, 65535)), |
| 256 | static_cast<uint16_t>(std::clamp(rows, 1, 65535)), |
| 257 | static_cast<uint32_t>(std::max(cell_width, 1)), |
| 258 | static_cast<uint32_t>(std::max(cell_height, 1))); |
| 259 | return ToJavaBytes(env, DrainResponses(session)); |
| 260 | } |
| 261 | |
| 262 | extern "C" JNIEXPORT void JNICALL |
| 263 | Java_expo_modules_t3terminal_GhosttyBridge_nativeScroll(JNIEnv*, jclass, jlong handle, |
| 264 | jint rows) { |
| 265 | auto* session = FromHandle(handle); |
| 266 | if (session == nullptr || rows == 0) return; |
| 267 | std::lock_guard<std::mutex> lock(session->mutex); |
| 268 | GhosttyTerminalScrollViewport scroll = { |
| 269 | .tag = GHOSTTY_SCROLL_VIEWPORT_DELTA, |
| 270 | .value = {.delta = rows}, |
| 271 | }; |
| 272 | ghostty_terminal_scroll_viewport(session->terminal, scroll); |
| 273 | } |
| 274 | |
| 275 | extern "C" JNIEXPORT void JNICALL |
| 276 | Java_expo_modules_t3terminal_GhosttyBridge_nativeSetTheme( |
| 277 | JNIEnv* env, jclass, jlong handle, jint foreground, jint background, jint cursor, |
| 278 | jintArray palette) { |
| 279 | auto* session = FromHandle(handle); |
| 280 | if (session == nullptr) return; |
| 281 | std::lock_guard<std::mutex> lock(session->mutex); |
| 282 | ApplyTheme(session, foreground, background, cursor, env, palette); |
| 283 | } |
| 284 | |
| 285 | extern "C" JNIEXPORT jboolean JNICALL |
| 286 | Java_expo_modules_t3terminal_GhosttyBridge_nativeSelectWordAt(JNIEnv*, jclass, |
| 287 | jlong handle, jint x, |
| 288 | jint y) { |
| 289 | auto* session = FromHandle(handle); |
| 290 | if (session == nullptr) return JNI_FALSE; |
| 291 | std::lock_guard<std::mutex> lock(session->mutex); |
| 292 | GhosttyTerminalSelectWordOptions options{}; |
| 293 | options.size = sizeof(options); |
| 294 | if (!ViewportGridRef(session, x, y, &options.ref)) return JNI_FALSE; |
| 295 | GhosttySelection selection{}; |
| 296 | selection.size = sizeof(selection); |
| 297 | if (ghostty_terminal_select_word(session->terminal, &options, &selection) != |
| 298 | GHOSTTY_SUCCESS) { |
| 299 | return JNI_FALSE; |
| 300 | } |
| 301 | return ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_SELECTION, |
| 302 | &selection) == GHOSTTY_SUCCESS |
| 303 | ? JNI_TRUE |
| 304 | : JNI_FALSE; |
| 305 | } |
| 306 | |
| 307 | extern "C" JNIEXPORT void JNICALL |
| 308 | Java_expo_modules_t3terminal_GhosttyBridge_nativeExtendSelection( |
| 309 | JNIEnv*, jclass, jlong handle, jint anchor_x, jint anchor_y, jint x, jint y) { |
| 310 | auto* session = FromHandle(handle); |
| 311 | if (session == nullptr) return; |
| 312 | std::lock_guard<std::mutex> lock(session->mutex); |
| 313 | GhosttySelection selection{}; |
| 314 | selection.size = sizeof(selection); |
| 315 | if (!ViewportGridRef(session, anchor_x, anchor_y, &selection.start)) return; |
| 316 | if (!ViewportGridRef(session, x, y, &selection.end)) return; |
| 317 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_SELECTION, &selection); |
| 318 | } |
| 319 | |
| 320 | extern "C" JNIEXPORT jboolean JNICALL |
| 321 | Java_expo_modules_t3terminal_GhosttyBridge_nativeSelectAll(JNIEnv*, jclass, |
| 322 | jlong handle) { |
| 323 | auto* session = FromHandle(handle); |
| 324 | if (session == nullptr) return JNI_FALSE; |
| 325 | std::lock_guard<std::mutex> lock(session->mutex); |
| 326 | GhosttySelection selection{}; |
| 327 | selection.size = sizeof(selection); |
| 328 | if (ghostty_terminal_select_all(session->terminal, &selection) != GHOSTTY_SUCCESS) { |
| 329 | return JNI_FALSE; |
| 330 | } |
| 331 | return ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_SELECTION, |
| 332 | &selection) == GHOSTTY_SUCCESS |
| 333 | ? JNI_TRUE |
| 334 | : JNI_FALSE; |
| 335 | } |
| 336 | |
| 337 | extern "C" JNIEXPORT void JNICALL |
| 338 | Java_expo_modules_t3terminal_GhosttyBridge_nativeClearSelection(JNIEnv*, jclass, |
| 339 | jlong handle) { |
| 340 | auto* session = FromHandle(handle); |
| 341 | if (session == nullptr) return; |
| 342 | std::lock_guard<std::mutex> lock(session->mutex); |
| 343 | ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_SELECTION, nullptr); |
| 344 | } |
| 345 | |
| 346 | // Returns the active selection as UTF-8 bytes (soft-wrapped lines unwrapped, |
| 347 | // trailing whitespace trimmed), or null when there is no selection. |
| 348 | extern "C" JNIEXPORT jbyteArray JNICALL |
| 349 | Java_expo_modules_t3terminal_GhosttyBridge_nativeGetSelectionText(JNIEnv* env, jclass, |
| 350 | jlong handle) { |
| 351 | auto* session = FromHandle(handle); |
| 352 | if (session == nullptr) return nullptr; |
| 353 | std::lock_guard<std::mutex> lock(session->mutex); |
| 354 | GhosttyTerminalSelectionFormatOptions options{}; |
| 355 | options.size = sizeof(options); |
| 356 | options.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; |
| 357 | options.unwrap = true; |
| 358 | options.trim = true; |
| 359 | uint8_t* bytes = nullptr; |
| 360 | size_t len = 0; |
| 361 | if (ghostty_terminal_selection_format_alloc(session->terminal, nullptr, options, |
| 362 | &bytes, &len) != GHOSTTY_SUCCESS || |
| 363 | bytes == nullptr) { |
| 364 | return nullptr; |
| 365 | } |
| 366 | auto result = env->NewByteArray(static_cast<jsize>(len)); |
| 367 | if (result != nullptr && len > 0) { |
| 368 | env->SetByteArrayRegion(result, 0, static_cast<jsize>(len), |
| 369 | reinterpret_cast<const jbyte*>(bytes)); |
| 370 | } |
| 371 | ghostty_free(nullptr, bytes, len); |
| 372 | return result; |
| 373 | } |
| 374 | |
| 375 | extern "C" JNIEXPORT jbyteArray JNICALL |
| 376 | Java_expo_modules_t3terminal_GhosttyBridge_nativeSnapshot(JNIEnv* env, jclass, |
| 377 | jlong handle) { |
| 378 | auto* session = FromHandle(handle); |
| 379 | if (session == nullptr) return env->NewByteArray(0); |
| 380 | std::lock_guard<std::mutex> lock(session->mutex); |
| 381 | |
| 382 | if (ghostty_render_state_update(session->render_state, session->terminal) != GHOSTTY_SUCCESS) { |
| 383 | return env->NewByteArray(0); |
| 384 | } |
| 385 | |
| 386 | uint16_t cols = 0; |
| 387 | uint16_t rows = 0; |
| 388 | bool cursor_visible = false; |
| 389 | bool cursor_in_viewport = false; |
| 390 | bool cursor_blinking = false; |
| 391 | uint16_t cursor_x = 0xFFFF; |
| 392 | uint16_t cursor_y = 0xFFFF; |
| 393 | GhosttyRenderStateCursorVisualStyle cursor_style = |
| 394 | GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK; |
| 395 | ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_COLS, &cols); |
| 396 | ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_ROWS, &rows); |
| 397 | ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, |
| 398 | &cursor_visible); |
| 399 | ghostty_render_state_get(session->render_state, |
| 400 | GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, |
| 401 | &cursor_in_viewport); |
| 402 | ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING, |
| 403 | &cursor_blinking); |
| 404 | ghostty_render_state_get(session->render_state, |
| 405 | GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, &cursor_style); |
| 406 | if (cursor_in_viewport) { |
| 407 | ghostty_render_state_get(session->render_state, |
| 408 | GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cursor_x); |
| 409 | ghostty_render_state_get(session->render_state, |
| 410 | GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cursor_y); |
| 411 | } |
| 412 | |
| 413 | GhosttyRenderStateColors colors{}; |
| 414 | colors.size = sizeof(colors); |
| 415 | if (ghostty_render_state_colors_get(session->render_state, &colors) != GHOSTTY_SUCCESS) { |
| 416 | return env->NewByteArray(0); |
| 417 | } |
| 418 | const auto cursor_color = colors.cursor_has_value ? colors.cursor : colors.foreground; |
| 419 | |
| 420 | ByteWriter writer(32 + static_cast<size_t>(cols) * rows * 14); |
| 421 | writer.U32(kSnapshotMagic); |
| 422 | writer.U16(kSnapshotVersion); |
| 423 | writer.U16(cols); |
| 424 | writer.U16(rows); |
| 425 | writer.U16(cursor_x); |
| 426 | writer.U16(cursor_y); |
| 427 | writer.U8(cursor_visible && cursor_in_viewport ? 1 : 0); |
| 428 | writer.U8(static_cast<uint8_t>(cursor_style)); |
| 429 | writer.U8(cursor_blinking ? 1 : 0); |
| 430 | writer.U8(0); |
| 431 | writer.U32(ArgbFromRgb(colors.foreground)); |
| 432 | writer.U32(ArgbFromRgb(colors.background)); |
| 433 | writer.U32(ArgbFromRgb(cursor_color)); |
| 434 | |
| 435 | if (ghostty_render_state_get(session->render_state, |
| 436 | GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, |
| 437 | &session->row_iterator) != GHOSTTY_SUCCESS) { |
| 438 | return env->NewByteArray(0); |
| 439 | } |
| 440 | |
| 441 | uint16_t written_rows = 0; |
| 442 | while (written_rows < rows && |
| 443 | ghostty_render_state_row_iterator_next(session->row_iterator)) { |
| 444 | if (ghostty_render_state_row_get(session->row_iterator, |
| 445 | GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, |
| 446 | &session->row_cells) != GHOSTTY_SUCCESS) { |
| 447 | break; |
| 448 | } |
| 449 | |
| 450 | uint16_t written_cols = 0; |
| 451 | while (written_cols < cols && ghostty_render_state_row_cells_next(session->row_cells)) { |
| 452 | GhosttyStyle style{}; |
| 453 | style.size = sizeof(style); |
| 454 | bool selected = false; |
| 455 | GhosttyColorRgb foreground = colors.foreground; |
| 456 | GhosttyColorRgb background = colors.background; |
| 457 | ghostty_render_state_row_cells_get(session->row_cells, |
| 458 | GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, |
| 459 | &style); |
| 460 | ghostty_render_state_row_cells_get(session->row_cells, |
| 461 | GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_SELECTED, |
| 462 | &selected); |
| 463 | ghostty_render_state_row_cells_get(session->row_cells, |
| 464 | GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, |
| 465 | &foreground); |
| 466 | ghostty_render_state_row_cells_get(session->row_cells, |
| 467 | GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, |
| 468 | &background); |
| 469 | if (style.inverse) std::swap(foreground, background); |
| 470 | if (style.faint) foreground = Blend(foreground, background, 155); |
| 471 | |
| 472 | uint32_t grapheme_count = 0; |
| 473 | ghostty_render_state_row_cells_get( |
| 474 | session->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, |
| 475 | &grapheme_count); |
| 476 | std::vector<uint8_t> utf8; |
| 477 | if (grapheme_count > 0) { |
| 478 | std::vector<uint32_t> codepoints(grapheme_count); |
| 479 | if (ghostty_render_state_row_cells_get( |
| 480 | session->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, |
| 481 | codepoints.data()) == GHOSTTY_SUCCESS) { |
| 482 | utf8.reserve(grapheme_count * 4); |
| 483 | for (const auto codepoint : codepoints) AppendUtf8(&utf8, codepoint); |
| 484 | } |
| 485 | } |
| 486 | |
| 487 | const auto text_length = static_cast<uint16_t>(std::min<size_t>(utf8.size(), 65535)); |
| 488 | writer.U32(ArgbFromRgb(foreground)); |
| 489 | writer.U32(ArgbFromRgb(background)); |
| 490 | writer.U16(StyleFlags(style, selected)); |
| 491 | writer.U16(text_length); |
| 492 | if (text_length != utf8.size()) utf8.resize(text_length); |
| 493 | writer.Bytes(utf8); |
| 494 | ++written_cols; |
| 495 | } |
| 496 | |
| 497 | while (written_cols++ < cols) { |
| 498 | writer.U32(ArgbFromRgb(colors.foreground)); |
| 499 | writer.U32(ArgbFromRgb(colors.background)); |
| 500 | writer.U16(0); |
| 501 | writer.U16(0); |
| 502 | } |
| 503 | ++written_rows; |
| 504 | } |
| 505 | |
| 506 | while (written_rows++ < rows) { |
| 507 | for (uint16_t column = 0; column < cols; ++column) { |
| 508 | writer.U32(ArgbFromRgb(colors.foreground)); |
| 509 | writer.U32(ArgbFromRgb(colors.background)); |
| 510 | writer.U16(0); |
| 511 | writer.U16(0); |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | return ToJavaBytes(env, writer.Take()); |
| 516 | } |