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
11namespace {
12
13constexpr uint32_t kSnapshotMagic = 0x54563354; // "T3VT" in little endian.
14constexpr uint16_t kSnapshotVersion = 1;
15constexpr size_t kMaxScrollbackRows = 10000;
16
17enum 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
29struct 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
38class 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
64Session* FromHandle(jlong handle) {
65 return reinterpret_cast<Session*>(static_cast<intptr_t>(handle));
66}
67
68jbyteArray 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
77GhosttyColorRgb 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
86uint32_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
91GhosttyColorRgb 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
104void 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
122void 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
128void 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
159void 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
168std::vector<uint8_t> DrainResponses(Session* session) {
169 std::vector<uint8_t> responses;
170 responses.swap(session->responses);
171 return responses;
172}
173
174bool 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
184uint16_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
200extern "C" JNIEXPORT jlong JNICALL
201Java_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
228extern "C" JNIEXPORT void JNICALL
229Java_expo_modules_t3terminal_GhosttyBridge_nativeDestroy(JNIEnv*, jclass, jlong handle) {
230 FreeSession(FromHandle(handle));
231}
232
233extern "C" JNIEXPORT jbyteArray JNICALL
234Java_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
248extern "C" JNIEXPORT jbyteArray JNICALL
249Java_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
262extern "C" JNIEXPORT void JNICALL
263Java_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
275extern "C" JNIEXPORT void JNICALL
276Java_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
285extern "C" JNIEXPORT jboolean JNICALL
286Java_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
307extern "C" JNIEXPORT void JNICALL
308Java_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
320extern "C" JNIEXPORT jboolean JNICALL
321Java_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
337extern "C" JNIEXPORT void JNICALL
338Java_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.
348extern "C" JNIEXPORT jbyteArray JNICALL
349Java_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
375extern "C" JNIEXPORT jbyteArray JNICALL
376Java_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}