FTXUI  5.0.0
C++ functional terminal UI.
terminal_input_parser.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. All rights reserved.
2// Use of this source code is governed by the MIT license that can be found in
3// the LICENSE file.
5
6#include <cstdint> // for uint32_t
7#include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion
8#include <ftxui/component/receiver.hpp> // for SenderImpl, Sender
9#include <map>
10#include <memory> // for unique_ptr, allocator
11#include <utility> // for move
12
13#include "ftxui/component/event.hpp" // for Event
14#include "ftxui/component/task.hpp" // for Task
15
16namespace ftxui {
17
18// NOLINTNEXTLINE
19const std::map<std::string, std::string> g_uniformize = {
20 // Microsoft's terminal uses a different new line character for the return
21 // key. This also happens with linux with the `bind` command:
22 // See https://github.com/ArthurSonzogni/FTXUI/issues/337
23 // Here, we uniformize the new line character to `\n`.
24 {"\r", "\n"},
25
26 // See: https://github.com/ArthurSonzogni/FTXUI/issues/508
27 {std::string({8}), std::string({127})},
28
29 // See: https://github.com/ArthurSonzogni/FTXUI/issues/626
30 //
31 // Depending on the Cursor Key Mode (DECCKM), the terminal sends different
32 // escape sequences:
33 //
34 // Key Normal Application
35 // ----- -------- -----------
36 // Up ESC [ A ESC O A
37 // Down ESC [ B ESC O B
38 // Right ESC [ C ESC O C
39 // Left ESC [ D ESC O D
40 // Home ESC [ H ESC O H
41 // End ESC [ F ESC O F
42 //
43 {"\x1BOA", "\x1B[A"}, // UP
44 {"\x1BOB", "\x1B[B"}, // DOWN
45 {"\x1BOC", "\x1B[C"}, // RIGHT
46 {"\x1BOD", "\x1B[D"}, // LEFT
47 {"\x1BOH", "\x1B[H"}, // HOME
48 {"\x1BOF", "\x1B[F"}, // END
49
50 // Variations around the FN keys.
51 // Internally, we are using:
52 // vt220, xterm-vt200, xterm-xf86-v44, xterm-new, mgt, screen
53 // See: https://invisible-island.net/xterm/xterm-function-keys.html
54
55 // For linux OS console (CTRL+ALT+FN), who do not belong to any
56 // real standard.
57 // See: https://github.com/ArthurSonzogni/FTXUI/issues/685
58 {"\x1B[[A", "\x1BOP"}, // F1
59 {"\x1B[[B", "\x1BOQ"}, // F2
60 {"\x1B[[C", "\x1BOR"}, // F3
61 {"\x1B[[D", "\x1BOS"}, // F4
62 {"\x1B[[E", "\x1B[15~"}, // F5
63
64 // xterm-r5, xterm-r6, rxvt
65 {"\x1B[11~", "\x1BOP"}, // F1
66 {"\x1B[12~", "\x1BOQ"}, // F2
67 {"\x1B[13~", "\x1BOR"}, // F3
68 {"\x1B[14~", "\x1BOS"}, // F4
69
70 // vt100
71 {"\x1BOt", "\x1B[15~"}, // F5
72 {"\x1BOu", "\x1B[17~"}, // F6
73 {"\x1BOv", "\x1B[18~"}, // F7
74 {"\x1BOl", "\x1B[19~"}, // F8
75 {"\x1BOw", "\x1B[20~"}, // F9
76 {"\x1BOx", "\x1B[21~"}, // F10
77
78 // scoansi
79 {"\x1B[M", "\x1BOP"}, // F1
80 {"\x1B[N", "\x1BOQ"}, // F2
81 {"\x1B[O", "\x1BOR"}, // F3
82 {"\x1B[P", "\x1BOS"}, // F4
83 {"\x1B[Q", "\x1B[15~"}, // F5
84 {"\x1B[R", "\x1B[17~"}, // F6
85 {"\x1B[S", "\x1B[18~"}, // F7
86 {"\x1B[T", "\x1B[19~"}, // F8
87 {"\x1B[U", "\x1B[20~"}, // F9
88 {"\x1B[V", "\x1B[21~"}, // F10
89 {"\x1B[W", "\x1B[23~"}, // F11
90 {"\x1B[X", "\x1B[24~"}, // F12
91};
92
94 : out_(std::move(out)) {}
95
97 timeout_ += time;
98 const int timeout_threshold = 50;
99 if (timeout_ < timeout_threshold) {
100 return;
101 }
102 timeout_ = 0;
103 if (!pending_.empty()) {
104 Send(SPECIAL);
105 }
106}
107
109 pending_ += c;
110 timeout_ = 0;
111 position_ = -1;
112 Send(Parse());
113}
114
115unsigned char TerminalInputParser::Current() {
116 return pending_[position_];
117}
118
119bool TerminalInputParser::Eat() {
120 position_++;
121 return position_ < static_cast<int>(pending_.size());
122}
123
124void TerminalInputParser::Send(TerminalInputParser::Output output) {
125 switch (output.type) {
126 case UNCOMPLETED:
127 return;
128
129 case DROP:
130 pending_.clear();
131 return;
132
133 case CHARACTER:
134 out_->Send(Event::Character(std::move(pending_)));
135 pending_.clear();
136 return;
137
138 case SPECIAL: {
139 auto it = g_uniformize.find(pending_);
140 if (it != g_uniformize.end()) {
141 pending_ = it->second;
142 }
143 out_->Send(Event::Special(std::move(pending_)));
144 pending_.clear();
145 }
146 return;
147
148 case MOUSE:
149 out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
150 pending_.clear();
151 return;
152
153 case CURSOR_POSITION:
154 out_->Send(Event::CursorPosition(std::move(pending_), // NOLINT
155 output.cursor.x, // NOLINT
156 output.cursor.y)); // NOLINT
157 pending_.clear();
158 return;
159
160 case CURSOR_SHAPE:
161 out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape));
162 pending_.clear();
163 return;
164 }
165 // NOT_REACHED().
166}
167
168TerminalInputParser::Output TerminalInputParser::Parse() {
169 if (!Eat()) {
170 return UNCOMPLETED;
171 }
172
173 switch (Current()) {
174 case 24: // CAN NOLINT
175 case 26: // SUB NOLINT
176 return DROP;
177
178 case '\x1B':
179 return ParseESC();
180 default:
181 break;
182 }
183
184 if (Current() < 32) { // C0 NOLINT
185 return SPECIAL;
186 }
187
188 if (Current() == 127) { // Delete // NOLINT
189 return SPECIAL;
190 }
191
192 return ParseUTF8();
193}
194
195// Code point <-> UTF-8 conversion
196//
197// ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
198// ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃
199// ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
200// │0xxxxxxx│ │ │ │
201// ├────────┼────────┼────────┼────────┤
202// │110xxxxx│10xxxxxx│ │ │
203// ├────────┼────────┼────────┼────────┤
204// │1110xxxx│10xxxxxx│10xxxxxx│ │
205// ├────────┼────────┼────────┼────────┤
206// │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│
207// └────────┴────────┴────────┴────────┘
208//
209// Then some sequences are illegal if it exist a shorter representation of the
210// same codepoint.
211TerminalInputParser::Output TerminalInputParser::ParseUTF8() {
212 auto head = Current();
213 unsigned char selector = 0b1000'0000; // NOLINT
214
215 // The non code-point part of the first byte.
216 unsigned char mask = selector;
217
218 // Find the first zero in the first byte.
219 unsigned int first_zero = 8; // NOLINT
220 for (unsigned int i = 0; i < 8; ++i) { // NOLINT
221 mask |= selector;
222 if (!(head & selector)) {
223 first_zero = i;
224 break;
225 }
226 selector >>= 1U;
227 }
228
229 // Accumulate the value of the first byte.
230 auto value = uint32_t(head & ~mask); // NOLINT
231
232 // Invalid UTF8, with more than 5 bytes.
233 const unsigned int max_utf8_bytes = 5;
234 if (first_zero == 1 || first_zero >= max_utf8_bytes) {
235 return DROP;
236 }
237
238 // Multi byte UTF-8.
239 for (unsigned int i = 2; i <= first_zero; ++i) {
240 if (!Eat()) {
241 return UNCOMPLETED;
242 }
243
244 // Invalid continuation byte.
245 head = Current();
246 if ((head & 0b1100'0000) != 0b1000'0000) { // NOLINT
247 return DROP;
248 }
249 value <<= 6; // NOLINT
250 value += head & 0b0011'1111; // NOLINT
251 }
252
253 // Check for overlong UTF8 encoding.
254 int extra_byte = 0;
255 if (value <= 0b000'0000'0111'1111) { // NOLINT
256 extra_byte = 0; // NOLINT
257 } else if (value <= 0b000'0111'1111'1111) { // NOLINT
258 extra_byte = 1; // NOLINT
259 } else if (value <= 0b1111'1111'1111'1111) { // NOLINT
260 extra_byte = 2; // NOLINT
261 } else if (value <= 0b1'0000'1111'1111'1111'1111) { // NOLINT
262 extra_byte = 3; // NOLINT
263 } else { // NOLINT
264 return DROP;
265 }
266
267 if (extra_byte != position_) {
268 return DROP;
269 }
270
271 return CHARACTER;
272}
273
274TerminalInputParser::Output TerminalInputParser::ParseESC() {
275 if (!Eat()) {
276 return UNCOMPLETED;
277 }
278 switch (Current()) {
279 case 'P':
280 return ParseDCS();
281 case '[':
282 return ParseCSI();
283 case ']':
284 return ParseOSC();
285 default:
286 if (!Eat()) {
287 return UNCOMPLETED;
288 } else {
289 return SPECIAL;
290 }
291 }
292}
293
294// ESC P ... ESC BACKSLASH
295TerminalInputParser::Output TerminalInputParser::ParseDCS() {
296 // Parse until the string terminator ST.
297 while (true) {
298 if (!Eat()) {
299 return UNCOMPLETED;
300 }
301
302 if (Current() != '\x1B') {
303 continue;
304 }
305
306 if (!Eat()) {
307 return UNCOMPLETED;
308 }
309
310 if (Current() != '\\') {
311 continue;
312 }
313
314 if (pending_.size() == 10 && //
315 pending_[2] == '1' && //
316 pending_[3] == '$' && //
317 pending_[4] == 'r' && //
318 true) {
319 Output output(CURSOR_SHAPE);
320 output.cursor_shape = pending_[5] - '0';
321 return output;
322 }
323
324 return SPECIAL;
325 }
326}
327
328TerminalInputParser::Output TerminalInputParser::ParseCSI() {
329 bool altered = false;
330 int argument = 0;
331 std::vector<int> arguments;
332 while (true) {
333 if (!Eat()) {
334 return UNCOMPLETED;
335 }
336
337 if (Current() == '<') {
338 altered = true;
339 continue;
340 }
341
342 if (Current() >= '0' && Current() <= '9') {
343 argument *= 10; // NOLINT
344 argument += Current() - '0';
345 continue;
346 }
347
348 if (Current() == ';') {
349 arguments.push_back(argument);
350 argument = 0;
351 continue;
352 }
353
354 // CSI is terminated by a character in the range 0x40–0x7E
355 // (ASCII @A–Z[\]^_`a–z{|}~),
356 if (Current() >= '@' && Current() <= '~' &&
357 // Note: I don't remember why we exclude '<'
358 Current() != '<' &&
359 // To handle F1-F4, we exclude '['.
360 Current() != '[') {
361 arguments.push_back(argument);
362 argument = 0; // NOLINT
363
364 switch (Current()) {
365 case 'M':
366 return ParseMouse(altered, true, std::move(arguments));
367 case 'm':
368 return ParseMouse(altered, false, std::move(arguments));
369 case 'R':
370 return ParseCursorPosition(std::move(arguments));
371 default:
372 return SPECIAL;
373 }
374 }
375
376 // Invalid ESC in CSI.
377 if (Current() == '\x1B') {
378 return SPECIAL;
379 }
380 }
381}
382
383TerminalInputParser::Output TerminalInputParser::ParseOSC() {
384 // Parse until the string terminator ST.
385 while (true) {
386 if (!Eat()) {
387 return UNCOMPLETED;
388 }
389 if (Current() != '\x1B') {
390 continue;
391 }
392 if (!Eat()) {
393 return UNCOMPLETED;
394 }
395 if (Current() != '\\') {
396 continue;
397 }
398 return SPECIAL;
399 }
400}
401
402TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT
403 bool altered,
404 bool pressed,
405 std::vector<int> arguments) {
406 if (arguments.size() != 3) {
407 return SPECIAL;
408 }
409
410 (void)altered;
411
412 Output output(MOUSE);
413 output.mouse.button = Mouse::Button((arguments[0] & 3) + // NOLINT
414 ((arguments[0] & 64) >> 4)); // NOLINT
415 output.mouse.motion = Mouse::Motion(pressed); // NOLINT
416 output.mouse.shift = bool(arguments[0] & 4); // NOLINT
417 output.mouse.meta = bool(arguments[0] & 8); // NOLINT
418 output.mouse.x = arguments[1]; // NOLINT
419 output.mouse.y = arguments[2]; // NOLINT
420 return output;
421}
422
423// NOLINTNEXTLINE
424TerminalInputParser::Output TerminalInputParser::ParseCursorPosition(
425 std::vector<int> arguments) {
426 if (arguments.size() != 2) {
427 return SPECIAL;
428 }
429 Output output(CURSOR_POSITION);
430 output.cursor.y = arguments[0]; // NOLINT
431 output.cursor.x = arguments[1]; // NOLINT
432 return output;
433}
434
435} // namespace ftxui
TerminalInputParser(Sender< Task > out)
std::unique_ptr< SenderImpl< T > > Sender
Definition: receiver.hpp:47
const std::map< std::string, std::string > g_uniformize
static Event CursorShape(std::string, int shape)
An event corresponding to a terminal DCS (Device Control String).
Definition: event.cpp:54
static Event Mouse(std::string, Mouse mouse)
An event corresponding to a given typed character.
Definition: event.cpp:44
static Event Character(std::string)
An event corresponding to a given typed character.
Definition: event.cpp:16
static Event CursorPosition(std::string, int x, int y)
Definition: event.cpp:74
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Definition: event.cpp:66