reflex-terminal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.doc/ext/reflex-terminal/native.cpp +19 -0
  3. data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
  4. data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
  5. data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
  6. data/.github/workflows/release-gem.yml +60 -0
  7. data/.github/workflows/tag.yml +35 -0
  8. data/.github/workflows/test.yml +46 -0
  9. data/.github/workflows/utils.rb +127 -0
  10. data/CONTRIBUTING.md +7 -0
  11. data/ChangeLog.md +6 -0
  12. data/Gemfile +5 -0
  13. data/LICENSE +21 -0
  14. data/README.md +100 -0
  15. data/Rakefile +44 -0
  16. data/VERSION +1 -0
  17. data/examples/terminal.rb +101 -0
  18. data/ext/reflex-terminal/defs.h +17 -0
  19. data/ext/reflex-terminal/extconf.rb +29 -0
  20. data/ext/reflex-terminal/native.cpp +19 -0
  21. data/ext/reflex-terminal/renderer.cpp +118 -0
  22. data/ext/reflex-terminal/terminal.cpp +572 -0
  23. data/include/reflex/terminal.h +233 -0
  24. data/include/reflex-terminal/defs.h +36 -0
  25. data/include/reflex-terminal/renderer.h +57 -0
  26. data/include/reflex-terminal/ruby/renderer.h +41 -0
  27. data/include/reflex-terminal/ruby/terminal.h +43 -0
  28. data/include/reflex-terminal/ruby.h +11 -0
  29. data/include/reflex-terminal.h +13 -0
  30. data/lib/reflex/bell_event.rb +15 -0
  31. data/lib/reflex/terminal.rb +168 -0
  32. data/lib/reflex/terminal_view.rb +268 -0
  33. data/lib/reflex-terminal/ext.rb +2 -0
  34. data/lib/reflex-terminal/extension.rb +41 -0
  35. data/lib/reflex-terminal.rb +7 -0
  36. data/reflex-terminal.gemspec +39 -0
  37. data/src/pty.cpp +236 -0
  38. data/src/renderer.cpp +365 -0
  39. data/src/terminal.cpp +1547 -0
  40. data/src/terminal.h +66 -0
  41. data/src/win32/pty.cpp +387 -0
  42. data/test/helper.rb +11 -0
  43. data/test/test_renderer.rb +113 -0
  44. data/test/test_terminal.rb +683 -0
  45. data/test/test_terminal_view.rb +182 -0
  46. metadata +152 -0
data/src/terminal.cpp ADDED
@@ -0,0 +1,1547 @@
1
+ #include "terminal.h"
2
+
3
+
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+ #include <string>
7
+ #include <ghostty/vt.h>
8
+ #include <rays/color.h>
9
+ #include <reflex/exception.h>
10
+
11
+
12
+ namespace Reflex
13
+ {
14
+
15
+
16
+ template <typename T>
17
+ static T
18
+ init_sized ()
19
+ {
20
+ // GHOSTTY_INIT_SIZED() is a C compound literal, so this is the C++
21
+ // equivalent for the sized-struct ABI pattern
22
+
23
+ T t = {};
24
+ t.size = sizeof(T);
25
+ return t;
26
+ }
27
+
28
+
29
+ struct Terminal::Data
30
+ {
31
+
32
+ GhosttyTerminal terminal = NULL;
33
+
34
+ GhosttyRenderState render_state = NULL;
35
+
36
+ GhosttyRenderStateRowIterator row_iterator = NULL;
37
+
38
+ GhosttyRenderStateRowCells row_cells = NULL;
39
+
40
+ GhosttyKeyEncoder key_encoder = NULL;
41
+
42
+ GhosttyKeyEvent key_event = NULL;
43
+
44
+ GhosttyMouseEncoder mouse_encoder = NULL;
45
+
46
+ GhosttyMouseEvent mouse_event = NULL;
47
+
48
+ GhosttyOptionAsAlt option_as_alt = GHOSTTY_OPTION_AS_ALT_TRUE;
49
+
50
+ bool any_button_pressed = false;
51
+
52
+ int columns = 0, rows = 0;
53
+
54
+ int cell_width = 8, cell_height = 16;
55
+
56
+ int screen_width = 0, screen_height = 0;
57
+
58
+ PTY pty;
59
+
60
+ String pending_input;
61
+
62
+ String title;
63
+
64
+ longlong bells = 0;
65
+
66
+ RowList spans;
67
+
68
+ std::vector<uint> cell_offsets;
69
+
70
+ ~Data ()
71
+ {
72
+ if (mouse_event) ghostty_mouse_event_free(mouse_event);
73
+ if (mouse_encoder) ghostty_mouse_encoder_free(mouse_encoder);
74
+ if (key_event) ghostty_key_event_free(key_event);
75
+ if (key_encoder) ghostty_key_encoder_free(key_encoder);
76
+ if (row_cells) ghostty_render_state_row_cells_free(row_cells);
77
+ if (row_iterator) ghostty_render_state_row_iterator_free(row_iterator);
78
+ if (render_state) ghostty_render_state_free(render_state);
79
+ if (terminal) ghostty_terminal_free(terminal);
80
+ }
81
+
82
+ bool is_valid () const
83
+ {
84
+ return terminal;
85
+ }
86
+
87
+ };// Terminal::Data
88
+
89
+
90
+ static void
91
+ write_input (Terminal::Data* self, const char* bytes, size_t size)
92
+ {
93
+ // sends bytes to the child process, or accumulates them for
94
+ // read_pending_input() while no child process is attached
95
+
96
+ if (size == 0) return;
97
+
98
+ if (self->pty)
99
+ self->pty.write(bytes, size);
100
+ else
101
+ self->pending_input.append(bytes, size);
102
+ }
103
+
104
+ static void
105
+ write_pty (GhosttyTerminal terminal, void* userdata, const uint8_t* data, size_t len)
106
+ {
107
+ auto* self = (Terminal::Data*) userdata;
108
+ if (!self) return;
109
+
110
+ write_input(self, (const char*) data, len);
111
+ }
112
+
113
+ static void
114
+ set_default_modes (GhosttyTerminal terminal)
115
+ {
116
+ // ghostty turns this on itself (grapheme-width-method defaults to
117
+ // unicode), and it is what makes a cell hold a whole grapheme
118
+ // cluster. without it a flag arrives as two regional indicators in
119
+ // two cells, and the renderer that composes them into one glyph
120
+ // and the child that was told they are four columns wide disagree
121
+ // about where the next character goes.
122
+ // a reset clears the modes, so this has to be said again after one
123
+ ghostty_terminal_mode_set(terminal, GHOSTTY_MODE_GRAPHEME_CLUSTER, true);
124
+ }
125
+
126
+ static void
127
+ bell_rang (GhosttyTerminal terminal, void* userdata)
128
+ {
129
+ auto* self = (Terminal::Data*) userdata;
130
+ if (!self) return;
131
+
132
+ self->bells += 1;
133
+ }
134
+
135
+ static void
136
+ title_changed (GhosttyTerminal terminal, void* userdata)
137
+ {
138
+ auto* self = (Terminal::Data*) userdata;
139
+ if (!self) return;
140
+
141
+ GhosttyString str = {NULL, 0};
142
+ if (ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_TITLE, &str) != GHOSTTY_SUCCESS)
143
+ return;
144
+
145
+ self->title.assign((const char*) str.ptr, str.len);
146
+ }
147
+
148
+
149
+ static uint
150
+ to_attribs (const GhosttyStyle& style)
151
+ {
152
+ // INVERSE is reported rather than applied: swapping fg/bg is left to
153
+ // the renderer, which is the one that knows the theme's default colors.
154
+ // UNDERLINE_MASK is a 3-bit field holding ghostty's style number --
155
+ // 0: none, 1: single, 2: double, 3: curly, 4: dotted, 5: dashed
156
+
157
+ uint attribs = 0;
158
+ if (style.bold) attribs |= Terminal::Span::BOLD;
159
+ if (style.italic) attribs |= Terminal::Span::ITALIC;
160
+ if (style.faint) attribs |= Terminal::Span::FAINT;
161
+ if (style.blink) attribs |= Terminal::Span::BLINK;
162
+ if (style.inverse) attribs |= Terminal::Span::INVERSE;
163
+ if (style.invisible) attribs |= Terminal::Span::INVISIBLE;
164
+ if (style.strikethrough) attribs |= Terminal::Span::STRIKETHROUGH;
165
+ if (style.overline) attribs |= Terminal::Span::OVERLINE;
166
+ attribs |=
167
+ (style.underline << Terminal::Span::UNDERLINE_SHIFT) & Terminal::Span::UNDERLINE_MASK;
168
+ return attribs;
169
+ }
170
+
171
+ static int
172
+ to_rgb (const GhosttyColorRgb& color)
173
+ {
174
+ return (color.r << 16) | (color.g << 8) | color.b;
175
+ }
176
+
177
+ static GhosttyKey
178
+ to_ghostty_key (int code)
179
+ {
180
+ // Reflex::KeyCode constants resolve to the platform native keycodes at
181
+ // compile time (NATIVE_VK), so comparing KeyEvent#code against KEY_*
182
+ // keeps this table platform-independent.
183
+ // Some KEY_* values alias each other on some platforms (e.g.
184
+ // KEY_SHIFT == KEY_LSHIFT on macOS), so use an if-chain instead of a
185
+ // switch to avoid duplicate case errors; aliases map to the same
186
+ // GhosttyKey anyway.
187
+
188
+ if (code < 0) return GHOSTTY_KEY_UNIDENTIFIED;
189
+
190
+ #define KEY(key, ghostty_key) \
191
+ if (code == KEY_##key) return GHOSTTY_KEY_##ghostty_key
192
+
193
+ KEY(A, A); KEY(B, B); KEY(C, C); KEY(D, D); KEY(E, E); KEY(F, F);
194
+ KEY(G, G); KEY(H, H); KEY(I, I); KEY(J, J); KEY(K, K); KEY(L, L);
195
+ KEY(M, M); KEY(N, N); KEY(O, O); KEY(P, P); KEY(Q, Q); KEY(R, R);
196
+ KEY(S, S); KEY(T, T); KEY(U, U); KEY(V, V); KEY(W, W); KEY(X, X);
197
+ KEY(Y, Y); KEY(Z, Z);
198
+
199
+ KEY(0, DIGIT_0); KEY(1, DIGIT_1); KEY(2, DIGIT_2); KEY(3, DIGIT_3);
200
+ KEY(4, DIGIT_4); KEY(5, DIGIT_5); KEY(6, DIGIT_6); KEY(7, DIGIT_7);
201
+ KEY(8, DIGIT_8); KEY(9, DIGIT_9);
202
+
203
+ KEY(MINUS, MINUS);
204
+ KEY(EQUAL, EQUAL);
205
+ KEY(COMMA, COMMA);
206
+ KEY(PERIOD, PERIOD);
207
+ KEY(SEMICOLON, SEMICOLON);
208
+ KEY(QUOTE, QUOTE);
209
+ KEY(SLASH, SLASH);
210
+ KEY(BACKSLASH, BACKSLASH);
211
+ KEY(GRAVE, BACKQUOTE);
212
+ KEY(LBRACKET, BRACKET_LEFT);
213
+ KEY(RBRACKET, BRACKET_RIGHT);
214
+ KEY(UNDERSCORE, INTL_RO);// JIS
215
+ KEY(YEN, INTL_YEN);// JIS
216
+ KEY(SECTION, INTL_BACKSLASH);
217
+
218
+ KEY(SPACE, SPACE);
219
+ KEY(TAB, TAB);
220
+ KEY(ENTER, ENTER);
221
+ KEY(BACKSPACE, BACKSPACE);
222
+ KEY(DELETE, DELETE);
223
+ KEY(INSERT, INSERT);
224
+ KEY(ESCAPE, ESCAPE);
225
+
226
+ KEY(LEFT, ARROW_LEFT);
227
+ KEY(RIGHT, ARROW_RIGHT);
228
+ KEY(UP, ARROW_UP);
229
+ KEY(DOWN, ARROW_DOWN);
230
+ KEY(HOME, HOME);
231
+ KEY(END, END);
232
+ KEY(PAGEUP, PAGE_UP);
233
+ KEY(PAGEDOWN, PAGE_DOWN);
234
+
235
+ KEY(CAPSLOCK, CAPS_LOCK);
236
+ KEY(SHIFT, SHIFT_LEFT);
237
+ KEY(LSHIFT, SHIFT_LEFT);
238
+ KEY(RSHIFT, SHIFT_RIGHT);
239
+ KEY(CONTROL, CONTROL_LEFT);
240
+ KEY(LCONTROL, CONTROL_LEFT);
241
+ KEY(RCONTROL, CONTROL_RIGHT);
242
+ KEY(ALT, ALT_LEFT);
243
+ KEY(LALT, ALT_LEFT);
244
+ KEY(RALT, ALT_RIGHT);
245
+ KEY(OPTION, ALT_LEFT);
246
+ KEY(LOPTION, ALT_LEFT);
247
+ KEY(ROPTION, ALT_RIGHT);
248
+ KEY(COMMAND, META_LEFT);
249
+ KEY(LCOMMAND, META_LEFT);
250
+ KEY(RCOMMAND, META_RIGHT);
251
+ KEY(LWIN, META_LEFT);
252
+ KEY(RWIN, META_RIGHT);
253
+
254
+ KEY(F1, F1); KEY(F2, F2); KEY(F3, F3); KEY(F4, F4);
255
+ KEY(F5, F5); KEY(F6, F6); KEY(F7, F7); KEY(F8, F8);
256
+ KEY(F9, F9); KEY(F10, F10); KEY(F11, F11); KEY(F12, F12);
257
+ KEY(F13, F13); KEY(F14, F14); KEY(F15, F15); KEY(F16, F16);
258
+ KEY(F17, F17); KEY(F18, F18); KEY(F19, F19); KEY(F20, F20);
259
+ KEY(F21, F21); KEY(F22, F22); KEY(F23, F23); KEY(F24, F24);
260
+
261
+ KEY(NUM_0, NUMPAD_0); KEY(NUM_1, NUMPAD_1); KEY(NUM_2, NUMPAD_2);
262
+ KEY(NUM_3, NUMPAD_3); KEY(NUM_4, NUMPAD_4); KEY(NUM_5, NUMPAD_5);
263
+ KEY(NUM_6, NUMPAD_6); KEY(NUM_7, NUMPAD_7); KEY(NUM_8, NUMPAD_8);
264
+ KEY(NUM_9, NUMPAD_9);
265
+
266
+ KEY(NUM_PLUS, NUMPAD_ADD);
267
+ KEY(NUM_MINUS, NUMPAD_SUBTRACT);
268
+ KEY(NUM_MULTIPLY, NUMPAD_MULTIPLY);
269
+ KEY(NUM_DIVIDE, NUMPAD_DIVIDE);
270
+ KEY(NUM_EQUAL, NUMPAD_EQUAL);
271
+ KEY(NUM_PERIOD, NUMPAD_DECIMAL);
272
+ KEY(NUM_DECIMAL, NUMPAD_DECIMAL);
273
+ KEY(NUM_COMMA, NUMPAD_COMMA);
274
+ KEY(NUM_CLEAR, NUMPAD_CLEAR);
275
+ KEY(NUM_ENTER, NUMPAD_ENTER);
276
+ KEY(NUMLOCK, NUM_LOCK);
277
+
278
+ KEY(EISU, NON_CONVERT);// JIS
279
+ KEY(KANA, KANA_MODE);// JIS
280
+
281
+ KEY(PRINTSCREEN, PRINT_SCREEN);
282
+ KEY(SCROLLLOCK, SCROLL_LOCK);
283
+ KEY(PAUSE, PAUSE);
284
+ KEY(HELP, HELP);
285
+ KEY(CONTEXT_MENU, CONTEXT_MENU);
286
+ KEY(COPY, COPY);
287
+ KEY(CUT, CUT);
288
+ KEY(PASTE, PASTE);
289
+
290
+ #undef KEY
291
+
292
+ return GHOSTTY_KEY_UNIDENTIFIED;
293
+ }
294
+
295
+ static GhosttyMods
296
+ to_ghostty_mods (uint modifiers)
297
+ {
298
+ GhosttyMods mods = 0;
299
+ if (modifiers & MOD_SHIFT) mods |= GHOSTTY_MODS_SHIFT;
300
+ if (modifiers & MOD_CONTROL) mods |= GHOSTTY_MODS_CTRL;
301
+ if (modifiers & (MOD_ALT | MOD_OPTION)) mods |= GHOSTTY_MODS_ALT;
302
+ if (modifiers & (MOD_WIN | MOD_COMMAND)) mods |= GHOSTTY_MODS_SUPER;
303
+ if (modifiers & MOD_CAPS) mods |= GHOSTTY_MODS_CAPS_LOCK;
304
+ return mods;
305
+ }
306
+
307
+ static uint32_t
308
+ to_unshifted_codepoint (GhosttyKey key)
309
+ {
310
+ // US-layout approximation, used only by the kitty keyboard protocol's
311
+ // alternate key reporting
312
+
313
+ if (GHOSTTY_KEY_A <= key && key <= GHOSTTY_KEY_Z)
314
+ return 'a' + (key - GHOSTTY_KEY_A);
315
+ if (GHOSTTY_KEY_DIGIT_0 <= key && key <= GHOSTTY_KEY_DIGIT_9)
316
+ return '0' + (key - GHOSTTY_KEY_DIGIT_0);
317
+
318
+ switch (key)
319
+ {
320
+ case GHOSTTY_KEY_BACKQUOTE: return '`';
321
+ case GHOSTTY_KEY_MINUS: return '-';
322
+ case GHOSTTY_KEY_EQUAL: return '=';
323
+ case GHOSTTY_KEY_BRACKET_LEFT: return '[';
324
+ case GHOSTTY_KEY_BRACKET_RIGHT: return ']';
325
+ case GHOSTTY_KEY_BACKSLASH: return '\\';
326
+ case GHOSTTY_KEY_SEMICOLON: return ';';
327
+ case GHOSTTY_KEY_QUOTE: return '\'';
328
+ case GHOSTTY_KEY_COMMA: return ',';
329
+ case GHOSTTY_KEY_PERIOD: return '.';
330
+ case GHOSTTY_KEY_SLASH: return '/';
331
+ case GHOSTTY_KEY_SPACE: return ' ';
332
+ case GHOSTTY_KEY_INTL_YEN: return 0xA5;// '¥'
333
+ case GHOSTTY_KEY_INTL_RO: return '_';
334
+ default: return 0;
335
+ }
336
+ }
337
+
338
+ static bool
339
+ is_printable (const char* chars)
340
+ {
341
+ if (!chars || !*chars) return false;
342
+
343
+ for (const char* p = chars; *p; ++p)
344
+ {
345
+ unsigned char c = (unsigned char) *p;
346
+ if (c < 0x20 || c == 0x7f) return false;
347
+ }
348
+ return true;
349
+ }
350
+
351
+ static char
352
+ to_c0 (GhosttyKey key, GhosttyMods mods, const char* chars)
353
+ {
354
+ // what to send when the encoder produced nothing at all, which happens
355
+ // for a few ctrl combinations that have no legacy encoding
356
+
357
+ // the platform resolves some of these itself using the real
358
+ // keyboard layout (macOS turns ctrl+- into 0x1f), which beats
359
+ // guessing from the key, so prefer it whenever it did
360
+ if (chars && chars[0] && !chars[1] && (unsigned char) chars[0] < 0x20)
361
+ return chars[0];
362
+
363
+ // ghostty leaves ctrl+i/m/[ to the kitty keyboard protocol so that
364
+ // they stay distinct from tab/enter/escape (the fixterms
365
+ // convention). Legacy mode cannot express that distinction, so an
366
+ // app that has not asked for the protocol would just lose these
367
+ // keys: send the C0 byte every other terminal sends.
368
+ if (mods != GHOSTTY_MODS_CTRL) return 0;
369
+
370
+ switch (key)
371
+ {
372
+ case GHOSTTY_KEY_I: return 0x09;// tab
373
+ case GHOSTTY_KEY_M: return 0x0d;// return
374
+ case GHOSTTY_KEY_BRACKET_LEFT: return 0x1b;// escape
375
+ default: return 0;
376
+ }
377
+ }
378
+
379
+ static void
380
+ encode_key (Terminal::Data* self, const KeyEvent& event, GhosttyKeyAction action)
381
+ {
382
+ ghostty_key_encoder_setopt_from_terminal(self->key_encoder, self->terminal);
383
+ ghostty_key_encoder_setopt(
384
+ self->key_encoder, GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT,
385
+ &self->option_as_alt);
386
+
387
+ GhosttyKey key = to_ghostty_key(event.code());
388
+ GhosttyMods mods = to_ghostty_mods(event.modifiers());
389
+
390
+ // send the composed text only when it is printable and no
391
+ // command/meta modifier is in effect
392
+ const char* chars = event.chars();
393
+ bool use_utf8 =
394
+ is_printable(chars) &&
395
+ !(mods & GHOSTTY_MODS_SUPER) &&
396
+ !((mods & GHOSTTY_MODS_ALT) && self->option_as_alt != GHOSTTY_OPTION_AS_ALT_FALSE);
397
+
398
+ GhosttyMods consumed = 0;
399
+ if (use_utf8 && (mods & GHOSTTY_MODS_SHIFT))
400
+ consumed |= GHOSTTY_MODS_SHIFT;
401
+
402
+ GhosttyKeyEvent e = self->key_event;
403
+ ghostty_key_event_set_action(e, action);
404
+ ghostty_key_event_set_key(e, key);
405
+ ghostty_key_event_set_mods(e, mods);
406
+ ghostty_key_event_set_consumed_mods(e, consumed);
407
+ ghostty_key_event_set_composing(e, false);
408
+ ghostty_key_event_set_utf8(e, use_utf8 ? chars : "", use_utf8 ? strlen(chars) : 0);
409
+ ghostty_key_event_set_unshifted_codepoint(e, to_unshifted_codepoint(key));
410
+
411
+ char buffer[256];
412
+ size_t size = 0;
413
+ GhosttyResult result =
414
+ ghostty_key_encoder_encode(self->key_encoder, e, buffer, sizeof(buffer), &size);
415
+ if (result == GHOSTTY_SUCCESS && size == 0 && action == GHOSTTY_KEY_ACTION_PRESS)
416
+ {
417
+ char c0 = to_c0(key, mods, chars);
418
+ if (c0 != 0) write_input(self, &c0, 1);
419
+ }
420
+ else if (result == GHOSTTY_SUCCESS)
421
+ write_input(self, buffer, size);
422
+ else if (result == GHOSTTY_OUT_OF_SPACE)
423
+ {
424
+ std::string big(size, '\0');
425
+ result = ghostty_key_encoder_encode(self->key_encoder, e, &big[0], big.size(), &size);
426
+ if (result == GHOSTTY_SUCCESS)
427
+ write_input(self, big.data(), size);
428
+ }
429
+ }
430
+
431
+ static void
432
+ encode_mouse (
433
+ Terminal::Data* self, GhosttyMouseAction action, int button,
434
+ GhosttyMods mods, float x, float y)
435
+ {
436
+ ghostty_mouse_encoder_setopt_from_terminal(self->mouse_encoder, self->terminal);
437
+
438
+ GhosttyMouseEncoderSize size = init_sized<GhosttyMouseEncoderSize>();
439
+ size.screen_width = self->screen_width > 0
440
+ ? self->screen_width : self->columns * self->cell_width;
441
+ size.screen_height = self->screen_height > 0
442
+ ? self->screen_height : self->rows * self->cell_height;
443
+ size.cell_width = self->cell_width;
444
+ size.cell_height = self->cell_height;
445
+ ghostty_mouse_encoder_setopt(self->mouse_encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE, &size);
446
+ ghostty_mouse_encoder_setopt(
447
+ self->mouse_encoder, GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED, &self->any_button_pressed);
448
+
449
+ GhosttyMouseEvent e = self->mouse_event;
450
+ ghostty_mouse_event_set_action(e, action);
451
+ if (button > 0)
452
+ ghostty_mouse_event_set_button(e, (GhosttyMouseButton) button);
453
+ else
454
+ ghostty_mouse_event_clear_button(e);
455
+ ghostty_mouse_event_set_mods(e, mods);
456
+
457
+ GhosttyMousePosition position = {x, y};
458
+ ghostty_mouse_event_set_position(e, position);
459
+
460
+ char buffer[64];
461
+ size_t written = 0;
462
+ GhosttyResult result =
463
+ ghostty_mouse_encoder_encode(self->mouse_encoder, e, buffer, sizeof(buffer), &written);
464
+ // written == 0 is normal while mouse tracking is off
465
+ if (result == GHOSTTY_SUCCESS && written > 0)
466
+ write_input(self, buffer, written);
467
+ }
468
+
469
+ static void
470
+ rebuild_spans (Terminal::Data* self)
471
+ {
472
+ self->spans.clear();
473
+ self->cell_offsets.clear();
474
+
475
+ GhosttyResult result = ghostty_render_state_get(
476
+ self->render_state, GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, &self->row_iterator);
477
+ if (result != GHOSTTY_SUCCESS) return;
478
+
479
+ String utf8;
480
+ while (ghostty_render_state_row_iterator_next(self->row_iterator))
481
+ {
482
+ self->spans.emplace_back();
483
+ Terminal::SpanList& row = self->spans.back();
484
+
485
+ result = ghostty_render_state_row_get(
486
+ self->row_iterator, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &self->row_cells);
487
+ if (result != GHOSTTY_SUCCESS) continue;
488
+
489
+ // the range once per row, rather than the selected flag once per
490
+ // cell, as ghostty asks of a renderer that draws in spans
491
+ auto selection = init_sized<GhosttyRenderStateRowSelection>();
492
+ result = ghostty_render_state_row_get(
493
+ self->row_iterator, GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION, &selection);
494
+ bool selected = result == GHOSTTY_SUCCESS;
495
+
496
+ Terminal::Span* span = NULL;
497
+ bool span_is_wide = false;
498
+ int x = -1;
499
+
500
+ while (ghostty_render_state_row_cells_next(self->row_cells))
501
+ {
502
+ ++x;
503
+
504
+ GhosttyCell raw = 0;
505
+ ghostty_render_state_row_cells_get(
506
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, &raw);
507
+
508
+ GhosttyCellWide wide = GHOSTTY_CELL_WIDE_NARROW;
509
+ ghostty_cell_get(raw, GHOSTTY_CELL_DATA_WIDE, &wide);
510
+ if (wide == GHOSTTY_CELL_WIDE_SPACER_TAIL)
511
+ continue;// occupied by the previous wide cell
512
+
513
+ uint32_t nchars = 0;
514
+ ghostty_render_state_row_cells_get(
515
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, &nchars);
516
+
517
+ int fg = Terminal::Span::COLOR_NONE, bg = Terminal::Span::COLOR_NONE;
518
+ GhosttyColorRgb color;
519
+ result = ghostty_render_state_row_cells_get(
520
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, &color);
521
+ if (result == GHOSTTY_SUCCESS) fg = to_rgb(color);
522
+ result = ghostty_render_state_row_cells_get(
523
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, &color);
524
+ if (result == GHOSTTY_SUCCESS) bg = to_rgb(color);
525
+
526
+ uint attribs = 0;
527
+ bool has_styling = false;
528
+ ghostty_render_state_row_cells_get(
529
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING, &has_styling);
530
+ if (has_styling)
531
+ {
532
+ GhosttyStyle style = init_sized<GhosttyStyle>();
533
+ result = ghostty_render_state_row_cells_get(
534
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, &style);
535
+ if (result == GHOSTTY_SUCCESS)
536
+ attribs = to_attribs(style);
537
+ }
538
+
539
+ // a wide cell answers for the spacer that follows it, which
540
+ // is skipped above: a selection covering only the spacer
541
+ // still has to show on the character standing there
542
+ int right = x + (wide == GHOSTTY_CELL_WIDE_WIDE ? 1 : 0);
543
+ if (selected && selection.start_x <= right && x <= selection.end_x)
544
+ attribs |= Terminal::Span::SELECTED;
545
+
546
+ bool empty = nchars == 0;
547
+ if (empty && bg == Terminal::Span::COLOR_NONE && attribs == 0)
548
+ {
549
+ span = NULL;// blank cell without style: leave a gap
550
+ continue;
551
+ }
552
+
553
+ utf8.clear();
554
+ if (!empty)
555
+ {
556
+ char stack_buf[64];
557
+ GhosttyBuffer buf = {(uint8_t*) stack_buf, sizeof(stack_buf), 0};
558
+ result = ghostty_render_state_row_cells_get(
559
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8, &buf);
560
+ if (result == GHOSTTY_OUT_OF_SPACE)
561
+ {
562
+ utf8.resize(buf.len);
563
+ buf.ptr = (uint8_t*) &utf8[0];
564
+ buf.cap = utf8.size();
565
+ result = ghostty_render_state_row_cells_get(
566
+ self->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8, &buf);
567
+ }
568
+ else if (result == GHOSTTY_SUCCESS)
569
+ utf8.assign(stack_buf, buf.len);
570
+ if (result != GHOSTTY_SUCCESS) utf8.clear();
571
+ }
572
+ if (utf8.empty()) utf8 = " ";
573
+
574
+ bool is_wide = wide == GHOSTTY_CELL_WIDE_WIDE;
575
+ int cell_width = is_wide ? 2 : 1;
576
+
577
+ if (
578
+ !span ||
579
+ span->fg != fg || span->bg != bg || span->attribs != attribs ||
580
+ span_is_wide != is_wide)
581
+ {
582
+ row.emplace_back();
583
+ span = &row.back();
584
+ span->x = x;
585
+ span->width = 0;
586
+ span->cell_offset = (uint) self->cell_offsets.size();
587
+ span->cell_size = 0;
588
+ span->fg = fg;
589
+ span->bg = bg;
590
+ span->attribs = attribs;
591
+ span_is_wide = is_wide;
592
+ }
593
+
594
+ self->cell_offsets.push_back((uint) span->text.size());
595
+ span->cell_size += 1;
596
+ span->text += utf8;
597
+ span->width += cell_width;
598
+ }
599
+
600
+ bool clean = false;
601
+ ghostty_render_state_row_set(
602
+ self->row_iterator, GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, &clean);
603
+ }
604
+ }
605
+
606
+ Terminal::EnvMap
607
+ Terminal_make_child_envs (const Terminal::EnvMap& envs)
608
+ {
609
+ Terminal::EnvMap map;
610
+ map["TERM"] = "xterm-256color";
611
+ map["COLORTERM"] = "truecolor";
612
+ map["TERM_PROGRAM"] = "reflex-terminal";
613
+ map["LINES"] = std::nullopt;
614
+ map["COLUMNS"] = std::nullopt;
615
+
616
+ // drop what would contradict the terminal we just claimed to be: a
617
+ // version for someone else's TERM_PROGRAM, and terminfo pointing at
618
+ // another app. Session markers left by a terminal or multiplexer we
619
+ // happen to run inside are its own business, so the application
620
+ // removes those it cares about (env => nil)
621
+ map["TERM_PROGRAM_VERSION"] = std::nullopt;
622
+ map["TERMINFO"] = std::nullopt;
623
+
624
+ for (const auto& it : envs)
625
+ map[it.first] = it.second;
626
+
627
+ return map;
628
+ }
629
+
630
+ const std::vector<uint>&
631
+ Terminal_get_cell_offsets (const Terminal& terminal)
632
+ {
633
+ return terminal.self->cell_offsets;
634
+ }
635
+
636
+
637
+ Terminal::Terminal ()
638
+ {
639
+ }
640
+
641
+ Terminal::Terminal (int columns, int rows, size_t scrollback_bytes)
642
+ {
643
+ // scrollback_bytes is a memory budget rather than a line count, since
644
+ // how many lines fit depends on how wide the terminal is. 0 keeps no
645
+ // scrollback at all
646
+
647
+ if (
648
+ columns <= 0 || UINT16_MAX < columns ||
649
+ rows <= 0 || UINT16_MAX < rows)
650
+ {
651
+ argument_error(
652
+ __FILE__, __LINE__, "invalid terminal size: %dx%d", columns, rows);
653
+ }
654
+
655
+ GhosttyTerminalOptions options = {};
656
+ options.cols = (uint16_t) columns;
657
+ options.rows = (uint16_t) rows;
658
+ options.max_scrollback = scrollback_bytes;
659
+ if (ghostty_terminal_new(NULL, &self->terminal, options) != GHOSTTY_SUCCESS)
660
+ system_error(__FILE__, __LINE__, "failed to create a terminal");
661
+
662
+ // register the cell pixel size (required right after creation)
663
+ ghostty_terminal_resize(
664
+ self->terminal, options.cols, options.rows, self->cell_width, self->cell_height);
665
+
666
+ set_default_modes(self->terminal);
667
+
668
+ ghostty_terminal_set(self->terminal, GHOSTTY_TERMINAL_OPT_USERDATA, self.get());
669
+ ghostty_terminal_set(self->terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, (const void*) write_pty);
670
+ ghostty_terminal_set(self->terminal, GHOSTTY_TERMINAL_OPT_BELL, (const void*) bell_rang);
671
+ ghostty_terminal_set(
672
+ self->terminal, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void*) title_changed);
673
+ if (
674
+ ghostty_render_state_new(NULL, &self->render_state) != GHOSTTY_SUCCESS ||
675
+ ghostty_render_state_row_iterator_new(NULL, &self->row_iterator) != GHOSTTY_SUCCESS ||
676
+ ghostty_render_state_row_cells_new(NULL, &self->row_cells) != GHOSTTY_SUCCESS)
677
+ {
678
+ system_error(__FILE__, __LINE__, "failed to create a render state");
679
+ }
680
+
681
+ if (
682
+ ghostty_key_encoder_new(NULL, &self->key_encoder) != GHOSTTY_SUCCESS ||
683
+ ghostty_key_event_new(NULL, &self->key_event) != GHOSTTY_SUCCESS ||
684
+ ghostty_mouse_encoder_new(NULL, &self->mouse_encoder) != GHOSTTY_SUCCESS ||
685
+ ghostty_mouse_event_new(NULL, &self->mouse_event) != GHOSTTY_SUCCESS)
686
+ {
687
+ system_error(__FILE__, __LINE__, "failed to create input encoders");
688
+ }
689
+
690
+ self->columns = columns;
691
+ self->rows = rows;
692
+
693
+ update();
694
+ }
695
+
696
+ Terminal::~Terminal ()
697
+ {
698
+ }
699
+
700
+ bool
701
+ Terminal::update ()
702
+ {
703
+ if (!*this)
704
+ invalid_state_error(__FILE__, __LINE__);
705
+
706
+ if (self->pty)
707
+ {
708
+ // pump the pty: read output from the child process and
709
+ // write back accumulated query responses
710
+ char buffer[16 * 1024];
711
+ size_t total = 0;
712
+ enum {MAX_BYTES_PER_UPDATE = 1024 * 1024};// avoid UI freeze
713
+ while (total < MAX_BYTES_PER_UPDATE)
714
+ {
715
+ size_t size = self->pty.read(buffer, sizeof(buffer));
716
+ if (size == 0)
717
+ {
718
+ // the kernel pty buffer holds only ~1kb, so a screenful
719
+ // arrives as a burst of small chunks: once one has
720
+ // started, wait briefly for the rest. Leaving each chunk
721
+ // to the next frame would spread it over hundreds of
722
+ // frames, visible as the screen filling in row by row.
723
+ if (total > 0 && self->pty.wait_readable(1)) continue;
724
+
725
+ break;
726
+ }
727
+
728
+ ghostty_terminal_vt_write(self->terminal, (const uint8_t*) buffer, size);
729
+ total += size;
730
+ }
731
+ }
732
+
733
+ GhosttyResult result = ghostty_render_state_update(self->render_state, self->terminal);
734
+ if (result != GHOSTTY_SUCCESS)
735
+ system_error(__FILE__, __LINE__, "failed to update a render state");
736
+
737
+ uint16_t columns = 0, rows = 0;
738
+ ghostty_render_state_get(self->render_state, GHOSTTY_RENDER_STATE_DATA_COLS, &columns);
739
+ ghostty_render_state_get(self->render_state, GHOSTTY_RENDER_STATE_DATA_ROWS, &rows);
740
+ if (columns > 0) self->columns = columns;
741
+ if (rows > 0) self->rows = rows;
742
+
743
+ GhosttyRenderStateDirty dirty = GHOSTTY_RENDER_STATE_DIRTY_FALSE;
744
+ ghostty_render_state_get(self->render_state, GHOSTTY_RENDER_STATE_DATA_DIRTY, &dirty);
745
+ if (dirty == GHOSTTY_RENDER_STATE_DIRTY_FALSE && !self->spans.empty())
746
+ return false;
747
+
748
+ rebuild_spans(self.get());
749
+
750
+ GhosttyRenderStateDirty clean = GHOSTTY_RENDER_STATE_DIRTY_FALSE;
751
+ ghostty_render_state_set(self->render_state, GHOSTTY_RENDER_STATE_OPTION_DIRTY, &clean);
752
+
753
+ return true;
754
+ }
755
+
756
+ void
757
+ Terminal::reset ()
758
+ {
759
+ if (!*this)
760
+ invalid_state_error(__FILE__, __LINE__);
761
+
762
+ ghostty_terminal_reset(self->terminal);
763
+ set_default_modes(self->terminal);
764
+ }
765
+
766
+ void
767
+ Terminal::resize (
768
+ int columns, int rows,
769
+ int cell_width, int cell_height,
770
+ int screen_width, int screen_height)
771
+ {
772
+ if (
773
+ columns <= 0 || UINT16_MAX < columns ||
774
+ rows <= 0 || UINT16_MAX < rows)
775
+ {
776
+ argument_error(__FILE__, __LINE__, "invalid terminal size: %dx%d", columns, rows);
777
+ }
778
+ if (cell_width <= 0 || cell_height <= 0)
779
+ argument_error(__FILE__, __LINE__, "invalid cell size: %dx%d", cell_width, cell_height);
780
+ if (!*this)
781
+ invalid_state_error(__FILE__, __LINE__);
782
+
783
+ self->cell_width = cell_width;
784
+ self->cell_height = cell_height;
785
+ self->screen_width = screen_width;
786
+ self->screen_height = screen_height;
787
+
788
+ GhosttyResult result = ghostty_terminal_resize(
789
+ self->terminal,
790
+ (uint16_t) columns, (uint16_t) rows,
791
+ (uint32_t) cell_width, (uint32_t) cell_height);
792
+ if (result != GHOSTTY_SUCCESS)
793
+ system_error(__FILE__, __LINE__, "failed to resize a terminal");
794
+
795
+ self->columns = columns;
796
+ self->rows = rows;
797
+
798
+ // sends SIGWINCH to the child process
799
+ self->pty.set_size(columns, rows, cell_width, cell_height);
800
+ }
801
+
802
+ void
803
+ Terminal::feed (const char* bytes, size_t size)
804
+ {
805
+ if (!bytes)
806
+ argument_error(__FILE__, __LINE__, "bytes is NULL");
807
+ if (!*this)
808
+ invalid_state_error(__FILE__, __LINE__);
809
+
810
+ ghostty_terminal_vt_write(self->terminal, (const uint8_t*) bytes, size);
811
+ }
812
+
813
+ String
814
+ Terminal::read_pending_input ()
815
+ {
816
+ // the bytes to be sent to the child process -- query responses and
817
+ // encoded input events, in the order they were generated -- accumulated
818
+ // while no child process is attached
819
+
820
+ if (!*this)
821
+ invalid_state_error(__FILE__, __LINE__);
822
+
823
+ String input;
824
+ input.swap(self->pending_input);// takes the bytes and leaves it empty
825
+ return input;
826
+ }
827
+
828
+ void
829
+ Terminal::spawn (const StringList& args, const EnvMap& envs)
830
+ {
831
+ if (!*this)
832
+ invalid_state_error(__FILE__, __LINE__);
833
+
834
+ // a child that has run its course is in nobody's way, and on windows
835
+ // nothing else notices it has gone: the pseudo console holds the pipe
836
+ // open, so read() never sees the end that would close it on posix
837
+ if (!self->pty.is_child_alive()) self->pty.close();
838
+
839
+ StringList list = args;
840
+ bool login_shell = list.empty();
841
+ if (login_shell)
842
+ {
843
+ #ifdef WIN32
844
+ const char* shell = getenv("COMSPEC");
845
+ list.emplace_back(shell ? shell : "cmd.exe");
846
+ #else
847
+ const char* shell = getenv("SHELL");
848
+ list.emplace_back(shell ? shell : "/bin/sh");
849
+ #endif
850
+ }
851
+
852
+ self->pty.spawn(
853
+ list, envs,
854
+ self->columns, self->rows, self->cell_width, self->cell_height,
855
+ login_shell);
856
+ }
857
+
858
+ void
859
+ Terminal::close ()
860
+ {
861
+ if (!*this)
862
+ invalid_state_error(__FILE__, __LINE__);
863
+
864
+ self->pty.close();
865
+ }
866
+
867
+ void
868
+ Terminal::write (const char* bytes, size_t size)
869
+ {
870
+ if (!bytes)
871
+ argument_error(__FILE__, __LINE__, "bytes is NULL");
872
+ if (!*this)
873
+ invalid_state_error(__FILE__, __LINE__);
874
+
875
+ write_input(self.get(), bytes, size);
876
+ }
877
+
878
+ void
879
+ Terminal::write_key (const KeyEvent& event)
880
+ {
881
+ if (!*this)
882
+ invalid_state_error(__FILE__, __LINE__);
883
+
884
+ GhosttyKeyAction action;
885
+ if (event.action() == KeyEvent::DOWN)
886
+ action = event.repeat() >= 1 ? GHOSTTY_KEY_ACTION_REPEAT : GHOSTTY_KEY_ACTION_PRESS;
887
+ else if (event.action() == KeyEvent::UP)
888
+ action = GHOSTTY_KEY_ACTION_RELEASE;
889
+ else
890
+ return;
891
+
892
+ encode_key(self.get(), event, action);
893
+ }
894
+
895
+ void
896
+ Terminal::write_pointer (const PointerEvent& event)
897
+ {
898
+ if (!*this)
899
+ invalid_state_error(__FILE__, __LINE__);
900
+ if (event.empty()) return;
901
+
902
+ const Pointer& pointer = event[0];
903
+ uint types = pointer.types();
904
+ if (!(types & Pointer::MOUSE)) return;// touch/pen: not supported yet
905
+
906
+ int button = 0;
907
+ if (types & Pointer::MOUSE_LEFT) button = GHOSTTY_MOUSE_BUTTON_LEFT;
908
+ else if (types & Pointer::MOUSE_RIGHT) button = GHOSTTY_MOUSE_BUTTON_RIGHT;
909
+ else if (types & Pointer::MOUSE_MIDDLE) button = GHOSTTY_MOUSE_BUTTON_MIDDLE;
910
+
911
+ GhosttyMouseAction action;
912
+ switch (pointer.action())
913
+ {
914
+ case Pointer::DOWN:
915
+ action = GHOSTTY_MOUSE_ACTION_PRESS;
916
+ self->any_button_pressed = true;
917
+ break;
918
+
919
+ case Pointer::UP:
920
+ action = GHOSTTY_MOUSE_ACTION_RELEASE;
921
+ break;
922
+
923
+ case Pointer::MOVE:
924
+ action = GHOSTTY_MOUSE_ACTION_MOTION;
925
+ break;
926
+
927
+ default: return;
928
+ }
929
+
930
+ encode_mouse(
931
+ self.get(), action, button,
932
+ to_ghostty_mods(pointer.modifiers()),
933
+ pointer.position().x, pointer.position().y);
934
+
935
+ if (pointer.action() == Pointer::UP)
936
+ self->any_button_pressed = false;
937
+ }
938
+
939
+ void
940
+ Terminal::write_wheel (const WheelEvent& event)
941
+ {
942
+ if (!*this)
943
+ invalid_state_error(__FILE__, __LINE__);
944
+
945
+ int dy = (int) event.dposition().y;
946
+ if (dy == 0) return;
947
+
948
+ int button = dy > 0 ? GHOSTTY_MOUSE_BUTTON_FIVE : GHOSTTY_MOUSE_BUTTON_FOUR;
949
+ GhosttyMods mods = to_ghostty_mods(event.modifiers());
950
+ float x = event.position().x;
951
+ float y = event.position().y;
952
+
953
+ enum {MAX_STEPS = 8};
954
+ int steps = dy > 0 ? dy : -dy;
955
+ if (steps > MAX_STEPS) steps = MAX_STEPS;
956
+
957
+ for (int i = 0; i < steps; ++i)
958
+ {
959
+ encode_mouse(self.get(), GHOSTTY_MOUSE_ACTION_PRESS, button, mods, x, y);
960
+ encode_mouse(self.get(), GHOSTTY_MOUSE_ACTION_RELEASE, button, mods, x, y);
961
+ }
962
+ }
963
+
964
+ void
965
+ Terminal::paste (const char* text, size_t size)
966
+ {
967
+ if (!text)
968
+ argument_error(__FILE__, __LINE__, "text is NULL");
969
+ if (!*this)
970
+ invalid_state_error(__FILE__, __LINE__);
971
+
972
+ bool bracketed = false;
973
+ ghostty_terminal_mode_get(self->terminal, GHOSTTY_MODE_BRACKETED_PASTE, &bracketed);
974
+
975
+ // ghostty sanitizes the text in place
976
+ String input(text, size);
977
+
978
+ std::string buffer(size + 16, '\0');
979
+ size_t written = 0;
980
+ GhosttyResult result = ghostty_paste_encode(
981
+ &input[0], size, bracketed, &buffer[0], buffer.size(), &written);
982
+ if (result == GHOSTTY_OUT_OF_SPACE)
983
+ {
984
+ buffer.resize(written);
985
+ input.assign(text, size);
986
+ result = ghostty_paste_encode(
987
+ &input[0], size, bracketed, &buffer[0], buffer.size(), &written);
988
+ }
989
+ if (result == GHOSTTY_SUCCESS)
990
+ write_input(self.get(), buffer.data(), written);
991
+ }
992
+
993
+ bool
994
+ Terminal::is_alive () const
995
+ {
996
+ return self && self->pty.is_child_alive();
997
+ }
998
+
999
+ bool
1000
+ Terminal::is_mouse_tracking () const
1001
+ {
1002
+ if (!*this) return false;
1003
+
1004
+ bool tracking = false;
1005
+ ghostty_terminal_get(self->terminal, GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING, &tracking);
1006
+ return tracking;
1007
+ }
1008
+
1009
+ static GhosttyTerminalScrollbar
1010
+ get_scrollbar (const Terminal::Data* self)
1011
+ {
1012
+ GhosttyTerminalScrollbar bar = {};
1013
+ ghostty_terminal_get(self->terminal, GHOSTTY_TERMINAL_DATA_SCROLLBAR, &bar);
1014
+ return bar;
1015
+ }
1016
+
1017
+ static bool
1018
+ to_grid_ref (GhosttyGridRef* ref, const Terminal::Data* self, int x, int y)
1019
+ {
1020
+ // rows are counted from the top of the viewport, so a negative one
1021
+ // names a row of the history above it. the screen coordinates run
1022
+ // through both, with the viewport starting at the scrollbar offset
1023
+ int64_t row = (int64_t) get_scrollbar(self).offset + y;
1024
+ if (row < 0 || x < 0 || x > UINT16_MAX) return false;
1025
+
1026
+ GhosttyPoint point = {};
1027
+ point.tag = GHOSTTY_POINT_TAG_SCREEN;
1028
+ point.value.coordinate.x = (uint16_t) x;
1029
+ point.value.coordinate.y = (uint32_t) row;
1030
+ return ghostty_terminal_grid_ref(self->terminal, point, ref) == GHOSTTY_SUCCESS;
1031
+ }
1032
+
1033
+ static void
1034
+ set_selection (Terminal::Data* self, const GhosttySelection* selection)
1035
+ {
1036
+ // the terminal copies it and takes to tracking the text it covers,
1037
+ // so the snapshot is ours to drop once this returns
1038
+ ghostty_terminal_set(self->terminal, GHOSTTY_TERMINAL_OPT_SELECTION, selection);
1039
+ }
1040
+
1041
+ static void
1042
+ select_between (Terminal::Data* self, int x1, int y1, int x2, int y2, bool rectangle)
1043
+ {
1044
+ GhosttySelection selection = init_sized<GhosttySelection>();
1045
+ selection.rectangle = rectangle;
1046
+ if (
1047
+ !to_grid_ref(&selection.start, self, x1, y1) ||
1048
+ !to_grid_ref(&selection.end, self, x2, y2))
1049
+ {
1050
+ return;
1051
+ }
1052
+
1053
+ set_selection(self, &selection);
1054
+ }
1055
+
1056
+ void
1057
+ Terminal::select (int x1, int y1, int x2, int y2)
1058
+ {
1059
+ if (!*this)
1060
+ invalid_state_error(__FILE__, __LINE__);
1061
+
1062
+ select_between(self.get(), x1, y1, x2, y2, false);
1063
+ }
1064
+
1065
+ void
1066
+ Terminal::select_rect (int x1, int y1, int x2, int y2)
1067
+ {
1068
+ if (!*this)
1069
+ invalid_state_error(__FILE__, __LINE__);
1070
+
1071
+ select_between(self.get(), x1, y1, x2, y2, true);
1072
+ }
1073
+
1074
+ void
1075
+ Terminal::select_word (int x, int y)
1076
+ {
1077
+ if (!*this)
1078
+ invalid_state_error(__FILE__, __LINE__);
1079
+
1080
+ auto options = init_sized<GhosttyTerminalSelectWordOptions>();
1081
+ if (!to_grid_ref(&options.ref, self.get(), x, y)) return;
1082
+
1083
+ GhosttySelection selection = init_sized<GhosttySelection>();
1084
+ GhosttyResult result =
1085
+ ghostty_terminal_select_word(self->terminal, &options, &selection);
1086
+ // no word under the cell leaves the selection as it was, so that a
1087
+ // drag through a gap does not flicker
1088
+ if (result != GHOSTTY_SUCCESS)
1089
+ return;
1090
+
1091
+ set_selection(self.get(), &selection);
1092
+ }
1093
+
1094
+ void
1095
+ Terminal::select_line (int y)
1096
+ {
1097
+ if (!*this)
1098
+ invalid_state_error(__FILE__, __LINE__);
1099
+
1100
+ auto options = init_sized<GhosttyTerminalSelectLineOptions>();
1101
+ if (!to_grid_ref(&options.ref, self.get(), 0, y)) return;
1102
+
1103
+ GhosttySelection selection = init_sized<GhosttySelection>();
1104
+ GhosttyResult result =
1105
+ ghostty_terminal_select_line(self->terminal, &options, &selection);
1106
+ if (result != GHOSTTY_SUCCESS)
1107
+ return;
1108
+
1109
+ set_selection(self.get(), &selection);
1110
+ }
1111
+
1112
+ void
1113
+ Terminal::deselect ()
1114
+ {
1115
+ if (!*this)
1116
+ invalid_state_error(__FILE__, __LINE__);
1117
+
1118
+ set_selection(self.get(), NULL);
1119
+ }
1120
+
1121
+ bool
1122
+ Terminal::has_selection () const
1123
+ {
1124
+ if (!*this) return false;
1125
+
1126
+ GhosttySelection selection = init_sized<GhosttySelection>();
1127
+ GhosttyResult result =
1128
+ ghostty_terminal_get(self->terminal, GHOSTTY_TERMINAL_DATA_SELECTION, &selection);
1129
+ return result == GHOSTTY_SUCCESS;
1130
+ }
1131
+
1132
+ String
1133
+ Terminal::selected_text () const
1134
+ {
1135
+ if (!*this) return "";
1136
+
1137
+ auto options = init_sized<GhosttyTerminalSelectionFormatOptions>();
1138
+ options.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
1139
+ options.unwrap = true;
1140
+ options.trim = true;
1141
+ options.selection = NULL;// the terminal's own selection
1142
+ uint8_t* buffer = NULL;
1143
+ size_t length = 0;
1144
+ GhosttyResult result =
1145
+ ghostty_terminal_selection_format_alloc(self->terminal, NULL, options, &buffer, &length);
1146
+ if (result != GHOSTTY_SUCCESS)
1147
+ return "";
1148
+
1149
+ String text((const char*) buffer, length);
1150
+ ghostty_free(NULL, buffer, length);
1151
+ return text;
1152
+ }
1153
+
1154
+ static uint64_t
1155
+ bottom_offset (const GhosttyTerminalScrollbar& bar)
1156
+ {
1157
+ // the viewport offset ghostty reports while the viewport is at the bottom
1158
+
1159
+ return bar.total > bar.len ? bar.total - bar.len : 0;
1160
+ }
1161
+
1162
+ void
1163
+ Terminal::scroll_to (int row)
1164
+ {
1165
+ if (!*this)
1166
+ invalid_state_error(__FILE__, __LINE__);
1167
+
1168
+ GhosttyTerminalScrollViewport behavior = {};
1169
+ if (row >= 0)
1170
+ behavior.tag = GHOSTTY_SCROLL_VIEWPORT_BOTTOM;
1171
+ else
1172
+ {
1173
+ uint64_t bottom = bottom_offset(get_scrollbar(self.get()));
1174
+ uint64_t back = (uint64_t) -(int64_t) row;
1175
+ behavior.tag = GHOSTTY_SCROLL_VIEWPORT_ROW;
1176
+ behavior.value.row = back < bottom ? bottom - back : 0;
1177
+ }
1178
+ ghostty_terminal_scroll_viewport(self->terminal, behavior);
1179
+ }
1180
+
1181
+ void
1182
+ Terminal::scroll_by (int rows)
1183
+ {
1184
+ if (!*this)
1185
+ invalid_state_error(__FILE__, __LINE__);
1186
+ if (rows == 0) return;
1187
+
1188
+ GhosttyTerminalScrollViewport behavior = {};
1189
+ behavior.tag = GHOSTTY_SCROLL_VIEWPORT_DELTA;
1190
+ behavior.value.delta = rows;
1191
+ ghostty_terminal_scroll_viewport(self->terminal, behavior);
1192
+ }
1193
+
1194
+ int
1195
+ Terminal::scroll () const
1196
+ {
1197
+ if (!*this) return 0;
1198
+
1199
+ GhosttyTerminalScrollbar bar = get_scrollbar(self.get());
1200
+ uint64_t bottom = bottom_offset(bar);
1201
+ return bar.offset < bottom ? -(int) (bottom - bar.offset) : 0;
1202
+ }
1203
+
1204
+ void
1205
+ Terminal::set_option_as_alt (OptionAsAlt state)
1206
+ {
1207
+ self->option_as_alt = (GhosttyOptionAsAlt) state;
1208
+ }
1209
+
1210
+ Terminal::OptionAsAlt
1211
+ Terminal::option_as_alt () const
1212
+ {
1213
+ return (OptionAsAlt) self->option_as_alt;
1214
+ }
1215
+
1216
+ int
1217
+ Terminal::columns () const
1218
+ {
1219
+ return self->columns;
1220
+ }
1221
+
1222
+ int
1223
+ Terminal::rows () const
1224
+ {
1225
+ return self->rows;
1226
+ }
1227
+
1228
+ Terminal::Cursor
1229
+ Terminal::cursor () const
1230
+ {
1231
+ Cursor cursor = {0, 0, Cursor::BLOCK, false};
1232
+ if (!*this) return cursor;
1233
+
1234
+ bool has_value = false;
1235
+ ghostty_render_state_get(
1236
+ self->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, &has_value);
1237
+
1238
+ bool visible = false;
1239
+ ghostty_render_state_get(
1240
+ self->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, &visible);
1241
+
1242
+ cursor.visible = has_value && visible;
1243
+ if (!has_value) return cursor;
1244
+
1245
+ uint16_t x = 0, y = 0;
1246
+ ghostty_render_state_get(self->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &x);
1247
+ ghostty_render_state_get(self->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &y);
1248
+ cursor.x = x;
1249
+ cursor.y = y;
1250
+
1251
+ GhosttyRenderStateCursorVisualStyle style = GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK;
1252
+ ghostty_render_state_get(
1253
+ self->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, &style);
1254
+ cursor.style = (Cursor::Style) style;
1255
+
1256
+ return cursor;
1257
+ }
1258
+
1259
+ enum {PALETTE_SIZE = Terminal::COLOR_PALETTE_LAST - Terminal::COLOR_PALETTE_FIRST + 1};
1260
+
1261
+ static bool
1262
+ is_palette (Terminal::ColorIndex index)
1263
+ {
1264
+ return Terminal::COLOR_PALETTE_FIRST <= index && index <= Terminal::COLOR_PALETTE_LAST;
1265
+ }
1266
+
1267
+ static GhosttyColorRgb
1268
+ to_ghostty_color (const Color& color)
1269
+ {
1270
+ GhosttyColorRgb rgb;
1271
+ rgb.r = Color::float2uchar(color.red);
1272
+ rgb.g = Color::float2uchar(color.green);
1273
+ rgb.b = Color::float2uchar(color.blue);
1274
+ return rgb;
1275
+ }
1276
+
1277
+ static GhosttyTerminalOption
1278
+ to_color_option (Terminal::ColorIndex index)
1279
+ {
1280
+ switch (index)
1281
+ {
1282
+ case Terminal::COLOR_FOREGROUND: return GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND;
1283
+ case Terminal::COLOR_BACKGROUND: return GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND;
1284
+ case Terminal::COLOR_CURSOR: return GHOSTTY_TERMINAL_OPT_COLOR_CURSOR;
1285
+ default:
1286
+ argument_error(__FILE__, __LINE__, "invalid color index: %d", index);
1287
+ }
1288
+ return GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND;
1289
+ }
1290
+
1291
+ static GhosttyTerminalData
1292
+ to_color_data (Terminal::ColorIndex index, bool default_)
1293
+ {
1294
+ switch (index)
1295
+ {
1296
+ case Terminal::COLOR_FOREGROUND:
1297
+ return default_
1298
+ ? GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT
1299
+ : GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND;
1300
+
1301
+ case Terminal::COLOR_BACKGROUND:
1302
+ return default_
1303
+ ? GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT
1304
+ : GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND;
1305
+
1306
+ case Terminal::COLOR_CURSOR:
1307
+ return default_
1308
+ ? GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT
1309
+ : GHOSTTY_TERMINAL_DATA_COLOR_CURSOR;
1310
+
1311
+ default:
1312
+ argument_error(__FILE__, __LINE__, "invalid color index: %d", index);
1313
+ }
1314
+ return GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND;
1315
+ }
1316
+
1317
+ static void
1318
+ get_palette (GhosttyTerminal terminal, GhosttyColorRgb* palette, bool default_)
1319
+ {
1320
+ GhosttyTerminalData data = default_
1321
+ ? GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT
1322
+ : GHOSTTY_TERMINAL_DATA_COLOR_PALETTE;
1323
+
1324
+ if (ghostty_terminal_get(terminal, data, palette) != GHOSTTY_SUCCESS)
1325
+ system_error(__FILE__, __LINE__, "failed to get a terminal palette");
1326
+ }
1327
+
1328
+ static void
1329
+ set_palette (GhosttyTerminal terminal, const GhosttyColorRgb* palette)
1330
+ {
1331
+ ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, palette);
1332
+ }
1333
+
1334
+ static bool
1335
+ get_terminal_color (
1336
+ const Terminal& terminal, Terminal::ColorIndex index, Color* color, bool default_)
1337
+ {
1338
+ if (!terminal) return false;
1339
+
1340
+ GhosttyColorRgb rgb;
1341
+ if (is_palette(index))
1342
+ {
1343
+ GhosttyColorRgb palette[PALETTE_SIZE];
1344
+ get_palette(terminal.self->terminal, palette, default_);
1345
+ rgb = palette[index - Terminal::COLOR_PALETTE_FIRST];
1346
+ }
1347
+ else
1348
+ {
1349
+ GhosttyTerminalData data = to_color_data(index, default_);
1350
+ if (ghostty_terminal_get(terminal.self->terminal, data, &rgb) != GHOSTTY_SUCCESS)
1351
+ return false;
1352
+ }
1353
+
1354
+ if (color) color->reset8(rgb.r, rgb.g, rgb.b);
1355
+ return true;
1356
+ }
1357
+
1358
+ void
1359
+ Terminal::set_default_color (ColorIndex index, const Color& color)
1360
+ {
1361
+ if (color.alpha != 1)
1362
+ argument_error(__FILE__, __LINE__, "color alpha must be 1");
1363
+ if (!*this)
1364
+ invalid_state_error(__FILE__, __LINE__);
1365
+
1366
+ GhosttyColorRgb rgb = to_ghostty_color(color);
1367
+
1368
+ if (is_palette(index))
1369
+ {
1370
+ GhosttyColorRgb palette[PALETTE_SIZE];
1371
+ get_palette(self->terminal, palette, true);
1372
+ palette[index - COLOR_PALETTE_FIRST] = rgb;
1373
+ set_palette(self->terminal, palette);
1374
+ }
1375
+ else
1376
+ ghostty_terminal_set(self->terminal, to_color_option(index), &rgb);
1377
+ }
1378
+
1379
+ void
1380
+ Terminal::clear_default_color (ColorIndex index)
1381
+ {
1382
+ if (!*this)
1383
+ invalid_state_error(__FILE__, __LINE__);
1384
+
1385
+ if (is_palette(index))
1386
+ {
1387
+ GhosttyColorRgb palette[PALETTE_SIZE], builtin[PALETTE_SIZE];
1388
+ get_palette(self->terminal, palette, true);
1389
+ set_palette(self->terminal, NULL);
1390
+ get_palette(self->terminal, builtin, true);
1391
+
1392
+ int i = index - COLOR_PALETTE_FIRST;
1393
+ palette[i] = builtin[i];
1394
+ set_palette(self->terminal, palette);
1395
+ }
1396
+ else
1397
+ ghostty_terminal_set(self->terminal, to_color_option(index), NULL);
1398
+ }
1399
+
1400
+ bool
1401
+ Terminal::get_default_color (ColorIndex index, Color* color) const
1402
+ {
1403
+ return get_terminal_color(*this, index, color, true);
1404
+ }
1405
+
1406
+ bool
1407
+ Terminal::get_color (ColorIndex index, Color* color) const
1408
+ {
1409
+ return get_terminal_color(*this, index, color, false);
1410
+ }
1411
+
1412
+ const char*
1413
+ Terminal::title () const
1414
+ {
1415
+ return self->title.c_str();
1416
+ }
1417
+
1418
+ StringList
1419
+ Terminal::lines () const
1420
+ {
1421
+ StringList result;
1422
+ result.reserve(self->spans.size());
1423
+
1424
+ for (const SpanList& spans : self->spans)
1425
+ {
1426
+ String line;
1427
+ int width = 0;
1428
+ for (const Span& span : spans)
1429
+ {
1430
+ if (span.x > width) line.append(span.x - width, ' ');
1431
+ line += span.text;
1432
+ width = span.x + span.width;
1433
+ }
1434
+
1435
+ size_t end = line.find_last_not_of(' ');
1436
+ result.push_back(end == String::npos ? String() : line.substr(0, end + 1));
1437
+ }
1438
+ return result;
1439
+ }
1440
+
1441
+ int
1442
+ Terminal::history_rows () const
1443
+ {
1444
+ if (!*this) return 0;
1445
+
1446
+ return (int) bottom_offset(get_scrollbar(self.get()));
1447
+ }
1448
+
1449
+ StringList
1450
+ Terminal::get_history_lines (int offset, int size) const
1451
+ {
1452
+ if (offset < 0)
1453
+ argument_error(__FILE__, __LINE__, "offset is negative");
1454
+ if (size < 0)
1455
+ argument_error(__FILE__, __LINE__, "size is negative");
1456
+ if (!*this)
1457
+ invalid_state_error(__FILE__, __LINE__);
1458
+
1459
+ StringList lines;
1460
+ if (size == 0) return lines;
1461
+
1462
+ int rows = history_rows();
1463
+ if (offset >= rows)
1464
+ return lines;
1465
+
1466
+ if (size > rows - offset)
1467
+ size = rows - offset;
1468
+
1469
+ GhosttyResult result;
1470
+ GhosttyPoint point = {};
1471
+ point.tag = GHOSTTY_POINT_TAG_HISTORY;
1472
+
1473
+ GhosttySelection selection = init_sized<GhosttySelection>();
1474
+ point.value.coordinate.x = 0;
1475
+ point.value.coordinate.y = (uint32_t) offset;
1476
+ result = ghostty_terminal_grid_ref(self->terminal, point, &selection.start);
1477
+ if (result != GHOSTTY_SUCCESS)
1478
+ return lines;
1479
+
1480
+ point.value.coordinate.x = (uint16_t) (self->columns - 1);
1481
+ point.value.coordinate.y = (uint32_t) (offset + size - 1);
1482
+ result = ghostty_terminal_grid_ref(self->terminal, point, &selection.end);
1483
+ if (result != GHOSTTY_SUCCESS)
1484
+ return lines;
1485
+
1486
+ GhosttyFormatter formatter = NULL;
1487
+ auto options = init_sized<GhosttyFormatterTerminalOptions>();
1488
+ options.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
1489
+ options.trim = true;
1490
+ options.selection = &selection;
1491
+ result = ghostty_formatter_terminal_new(NULL, &formatter, self->terminal, options);
1492
+ if (result != GHOSTTY_SUCCESS)
1493
+ return lines;
1494
+
1495
+ uint8_t* buffer = NULL;
1496
+ size_t length = 0;
1497
+ result = ghostty_formatter_format_alloc(formatter, NULL, &buffer, &length);
1498
+ if (result == GHOSTTY_SUCCESS)
1499
+ {
1500
+ const char* text = (const char*) buffer;
1501
+ size_t start = 0;
1502
+ while (start <= length)
1503
+ {
1504
+ const char* end = (const char*) memchr(text + start, '\n', length - start);
1505
+ size_t stop = end ? (size_t) (end - text) : length;
1506
+ lines.emplace_back(text + start, stop - start);
1507
+ if (!end) break;
1508
+
1509
+ start = stop + 1;
1510
+ }
1511
+ ghostty_free(NULL, buffer, length);
1512
+ }
1513
+ ghostty_formatter_free(formatter);
1514
+
1515
+ return lines;
1516
+ }
1517
+
1518
+ longlong
1519
+ Terminal::bells () const
1520
+ {
1521
+ // how many BEL characters (0x07) have arrived so far. it only ever
1522
+ // grows, so a reader tells the new ones from the ones it has already
1523
+ // answered by remembering the last count it saw, and neither feed()
1524
+ // nor update() can drop one on the way
1525
+
1526
+ return self->bells;
1527
+ }
1528
+
1529
+ const Terminal::RowList&
1530
+ Terminal::spans () const
1531
+ {
1532
+ return self->spans;
1533
+ }
1534
+
1535
+ Terminal::operator bool () const
1536
+ {
1537
+ return self && self->is_valid();
1538
+ }
1539
+
1540
+ bool
1541
+ Terminal::operator ! () const
1542
+ {
1543
+ return !operator bool();
1544
+ }
1545
+
1546
+
1547
+ }// Reflex