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.
- checksums.yaml +7 -0
- data/.doc/ext/reflex-terminal/native.cpp +19 -0
- data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
- data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
- data/.github/workflows/release-gem.yml +60 -0
- data/.github/workflows/tag.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.github/workflows/utils.rb +127 -0
- data/CONTRIBUTING.md +7 -0
- data/ChangeLog.md +6 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/examples/terminal.rb +101 -0
- data/ext/reflex-terminal/defs.h +17 -0
- data/ext/reflex-terminal/extconf.rb +29 -0
- data/ext/reflex-terminal/native.cpp +19 -0
- data/ext/reflex-terminal/renderer.cpp +118 -0
- data/ext/reflex-terminal/terminal.cpp +572 -0
- data/include/reflex/terminal.h +233 -0
- data/include/reflex-terminal/defs.h +36 -0
- data/include/reflex-terminal/renderer.h +57 -0
- data/include/reflex-terminal/ruby/renderer.h +41 -0
- data/include/reflex-terminal/ruby/terminal.h +43 -0
- data/include/reflex-terminal/ruby.h +11 -0
- data/include/reflex-terminal.h +13 -0
- data/lib/reflex/bell_event.rb +15 -0
- data/lib/reflex/terminal.rb +168 -0
- data/lib/reflex/terminal_view.rb +268 -0
- data/lib/reflex-terminal/ext.rb +2 -0
- data/lib/reflex-terminal/extension.rb +41 -0
- data/lib/reflex-terminal.rb +7 -0
- data/reflex-terminal.gemspec +39 -0
- data/src/pty.cpp +236 -0
- data/src/renderer.cpp +365 -0
- data/src/terminal.cpp +1547 -0
- data/src/terminal.h +66 -0
- data/src/win32/pty.cpp +387 -0
- data/test/helper.rb +11 -0
- data/test/test_renderer.rb +113 -0
- data/test/test_terminal.rb +683 -0
- data/test/test_terminal_view.rb +182 -0
- 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
|