blob: e12df5c72aecdc431bea7d9af1e9929ddc2f02d7 [file] [log] [blame]
that1964d192016-01-07 00:41:03 +01001/*
2 Copyright 2016 _that/TeamWin
3 This file is part of TWRP/TeamWin Recovery Project.
4
5 TWRP is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 TWRP is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with TWRP. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19// terminal.cpp - GUITerminal object
20
21#include <stdio.h>
22#include <stdlib.h>
23#include <string.h>
24#include <fcntl.h>
25#include <unistd.h>
26#include <termio.h>
27
28#include <string>
29#include <cctype>
30#include <linux/input.h>
thata17f1752016-01-11 01:07:28 +010031#include <sys/wait.h>
that1964d192016-01-07 00:41:03 +010032
33extern "C" {
34#include "../twcommon.h"
that1964d192016-01-07 00:41:03 +010035}
Ethan Yonkerfbb43532015-12-28 21:54:50 +010036#include "../minuitwrp/minui.h"
bigbiffd58ba182020-03-23 10:02:29 -040037#include "../minuitwrp/truetype.hpp"
38
Ethan Yonker6e8c27a2016-12-22 17:55:57 -060039#include "gui.hpp"
that1964d192016-01-07 00:41:03 +010040
41#include "rapidxml.hpp"
42#include "objects.hpp"
43
44#if 0
45#define debug_printf printf
46#else
47#define debug_printf(...)
48#endif
49
50extern int g_pty_fd; // in gui.cpp where the select is
51
52/*
53Pseudoterminal handler.
54*/
55class Pseudoterminal
56{
57public:
58 Pseudoterminal() : fdMaster(0), pid(0)
59 {
60 }
61
62 bool started() const { return pid > 0; }
63
64 bool start()
65 {
66 fdMaster = getpt();
67 if (fdMaster < 0) {
68 LOGERR("Error %d on getpt()\n", errno);
69 return false;
70 }
71
72 if (unlockpt(fdMaster) != 0) {
73 LOGERR("Error %d on unlockpt()\n", errno);
74 return false;
75 }
76
77 pid = fork();
78 if (pid < 0) {
79 LOGERR("fork failed for pty, error %d\n", errno);
80 close(fdMaster);
81 pid = 0;
82 return false;
83 }
84 else if (pid) {
85 // child started, now someone needs to periodically read from fdMaster
86 // and write it to the terminal
87 // this currently works through gui.cpp calling terminal_pty_read below
88 g_pty_fd = fdMaster;
Ethan Yonker6e8c27a2016-12-22 17:55:57 -060089 set_select_fd();
that1964d192016-01-07 00:41:03 +010090 return true;
91 }
92 else {
93 int fdSlave = open(ptsname(fdMaster), O_RDWR);
94 close(fdMaster);
95 runSlave(fdSlave);
96 }
97 // we can't get here
98 LOGERR("impossible error in pty\n");
99 return false;
100 }
101
102 void runSlave(int fdSlave)
103 {
104 dup2(fdSlave, 0); // PTY becomes standard input (0)
105 dup2(fdSlave, 1); // PTY becomes standard output (1)
106 dup2(fdSlave, 2); // PTY becomes standard error (2)
107
108 // Now the original file descriptor is useless
109 close(fdSlave);
110
111 // Make the current process a new session leader
112 if (setsid() == (pid_t)-1)
113 LOGERR("setsid failed: %d\n", errno);
114
115 // As the child is a session leader, set the controlling terminal to be the slave side of the PTY
116 // (Mandatory for programs like the shell to make them manage correctly their outputs)
117 ioctl(0, TIOCSCTTY, 1);
118
119 execl("/sbin/sh", "sh", NULL);
120 _exit(127);
121 }
122
123 int read(char* buffer, size_t size)
124 {
125 if (!started()) {
126 LOGERR("someone tried to read from pty, but it was not started\n");
127 return -1;
128 }
129 int rc = ::read(fdMaster, buffer, size);
130 debug_printf("pty read: %d bytes\n", rc);
131 if (rc < 0) {
thata17f1752016-01-11 01:07:28 +0100132 // assume child has died (usual errno when shell exits seems to be EIO == 5)
133 if (errno != EIO)
134 LOGERR("pty read failed: %d\n", errno);
135 stop();
that1964d192016-01-07 00:41:03 +0100136 }
137 return rc;
138 }
139
140 int write(const char* buffer, size_t size)
141 {
142 if (!started()) {
143 LOGERR("someone tried to write to pty, but it was not started\n");
144 return -1;
145 }
146 int rc = ::write(fdMaster, buffer, size);
147 debug_printf("pty write: %d bytes -> %d\n", size, rc);
148 if (rc < 0) {
thata17f1752016-01-11 01:07:28 +0100149 LOGERR("pty write failed: %d\n", errno);
that1964d192016-01-07 00:41:03 +0100150 // assume child has died
thata17f1752016-01-11 01:07:28 +0100151 stop();
that1964d192016-01-07 00:41:03 +0100152 }
153 return rc;
154 }
155
156 template<size_t n>
157 inline int write(const char (&literal)[n])
158 {
159 return write(literal, n-1);
160 }
161
162 void resize(int xChars, int yChars, int w, int h)
163 {
164 struct winsize ws;
165 ws.ws_row = yChars;
166 ws.ws_col = xChars;
167 ws.ws_xpixel = w;
168 ws.ws_ypixel = h;
169 if (ioctl(fdMaster, TIOCSWINSZ, &ws) < 0)
170 LOGERR("failed to set window size, error %d\n", errno);
171 }
172
thata17f1752016-01-11 01:07:28 +0100173 void stop()
174 {
175 if (!started()) {
176 LOGERR("someone tried to stop pty, but it was not started\n");
177 return;
178 }
179 close(fdMaster);
180 g_pty_fd = fdMaster = -1;
Ethan Yonker6e8c27a2016-12-22 17:55:57 -0600181 set_select_fd();
thata17f1752016-01-11 01:07:28 +0100182 int status;
183 waitpid(pid, &status, WNOHANG); // avoid zombies but don't hang if the child is still alive and we got here due to some error
184 pid = 0;
185 }
186
that1964d192016-01-07 00:41:03 +0100187private:
188 int fdMaster;
thata17f1752016-01-11 01:07:28 +0100189 pid_t pid;
that1964d192016-01-07 00:41:03 +0100190};
191
192// UTF-8 decoder
193// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
194// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
195
196const uint32_t UTF8_ACCEPT = 0;
197const uint32_t UTF8_REJECT = 1;
198
199static const uint8_t utf8d[] = {
200 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
201 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
202 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
203 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
204 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
205 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
206 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
207 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
208 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
209 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
210 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
211 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
212 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
213 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
214};
215
216uint32_t inline utf8decode(uint32_t* state, uint32_t* codep, uint32_t byte)
217{
218 uint32_t type = utf8d[byte];
219
220 *codep = (*state != UTF8_ACCEPT) ?
221 (byte & 0x3fu) | (*codep << 6) :
222 (0xff >> type) & (byte);
223
224 *state = utf8d[256 + *state*16 + type];
225 return *state;
226}
227// end of UTF-8 decoder
228
229// Append a UTF-8 codepoint to string s
230size_t utf8add(std::string& s, uint32_t cp)
231{
232 if (cp < 0x7f) {
233 s += cp;
234 return 1;
235 }
236 else if (cp < 0x7ff) {
237 s += (0xc0 | (cp >> 6));
238 s += (0x80 | (cp & 0x3f));
239 return 2;
240 }
241 else if (cp < 0xffff) {
242 s += (0xe0 | (cp >> 12));
243 s += (0x80 | ((cp >> 6) & 0x3f));
244 s += (0x80 | (cp & 0x3f));
245 return 3;
246 }
247 else if (cp < 0x1fffff) {
248 s += (0xf0 | (cp >> 18));
249 s += (0x80 | ((cp >> 12) & 0x3f));
250 s += (0x80 | ((cp >> 6) & 0x3f));
251 s += (0x80 | (cp & 0x3f));
252 return 4;
253 }
254 return 0;
255}
256
257/*
258TerminalEngine is the terminal back-end, dealing with the text buffer and attributes
259and with communicating with the pty.
260It does not care about visual things like rendering, fonts, windows etc.
261The idea is that 0 to n GUITerminal instances (e.g. on different pages) can connect
262to one TerminalEngine to interact with the terminal, and that the TerminalEngine
263survives things like page changes or even theme reloads.
264*/
265class TerminalEngine
266{
267public:
268#if 0 // later
269 struct Attributes
270 {
271 COLOR fgcolor; // TODO: what about palette?
272 COLOR bgcolor;
273 // could add bold, underline, blink, etc.
274 };
275
276 struct AttributeRange
277 {
278 size_t start; // start position inside text (in bytes)
279 Attributes a;
280 };
281#endif
282 typedef uint32_t CodePoint; // Unicode code point
283
284 // A line of text, optimized for rendering and storage in the buffer
285 struct Line
286 {
287 std::string text; // in UTF-8 format
288// std::vector<AttributeRange> attrs;
289 Line() {}
290 size_t utf8forward(size_t start) const
291 {
292 if (start >= text.size())
293 return start;
294 uint32_t u8state = 0, u8cp = 0;
295 size_t i = start;
296 uint32_t rc;
297 do {
298 rc = utf8decode(&u8state, &u8cp, (unsigned char)text[i]);
299 ++i;
300 } while (rc != UTF8_ACCEPT && rc != UTF8_REJECT && i < text.size());
301 return i;
302 }
303
304 std::string substr(size_t start, size_t n) const
305 {
306 size_t i = 0;
307 for (; start && i < text.size(); i = utf8forward(i))
308 --start;
309 size_t s = i;
310 for (; n && i < text.size(); i = utf8forward(i))
311 --n;
312 return text.substr(s, i - s);
313 }
314 size_t length() const
315 {
316 size_t n = 0;
317 for (size_t i = 0; i < text.size(); i = utf8forward(i))
318 ++n;
319 return n;
320 }
321 };
322
323 // A single character cell with a Unicode code point
324 struct Cell
325 {
326 Cell() : cp(' ') {}
327 Cell(CodePoint cp) : cp(cp) {}
328 CodePoint cp;
329// Attributes a;
330 };
331
332 // A line of text, optimized for editing single characters
333 struct UnpackedLine
334 {
335 std::vector<Cell> cells;
336 void eraseFrom(size_t x)
337 {
338 if (cells.size() > x)
339 cells.erase(cells.begin() + x, cells.end());
340 }
341
342 void eraseTo(size_t x)
343 {
344 if (x > 0)
345 cells.erase(cells.begin(), cells.begin() + x);
346 }
347 };
348
349 TerminalEngine()
350 {
351 // the default size will be overwritten by the GUI window when the size is known
352 width = 40;
353 height = 10;
354
355 clear();
356 updateCounter = 0;
357 state = kStateGround;
358 utf8state = utf8codepoint = 0;
359 }
360
361 void setSize(int xChars, int yChars, int w, int h)
362 {
363 width = xChars;
364 height = yChars;
365 if (pty.started())
366 pty.resize(width, height, w, h);
367 debug_printf("setSize: %d*%d chars, %d*%d pixels\n", xChars, yChars, w, h);
368 }
369
370 void initPty()
371 {
372 if (!pty.started())
373 {
374 pty.start();
375 pty.resize(width, height, 0, 0);
376 }
377 }
378
379 void readPty()
380 {
381 char buffer[1024];
382 int rc = pty.read(buffer, sizeof(buffer));
383 debug_printf("readPty: %d bytes\n", rc);
384 if (rc < 0)
385 output("\r\nChild process exited.\r\n"); // TODO: maybe exit terminal here
386 else
387 for (int i = 0; i < rc; ++i)
388 output(buffer[i]);
389 }
390
391 void clear()
392 {
393 cursorX = cursorY = 0;
394 lines.clear();
395 setY(0);
396 unpackLine(0);
that0d0d5222017-03-14 20:58:08 +0100397 linewrap = false;
that1964d192016-01-07 00:41:03 +0100398 ++updateCounter;
399 }
400
401 void output(const char *buf)
402 {
403 for (const char* p = buf; *p; ++p)
404 output(*p);
405 }
406
407 void output(const char ch)
408 {
409 char debug[2]; debug[0] = ch; debug[1] = 0;
410 debug_printf("output: %d %s\n", (int)ch, (ch >= ' ' && ch < 127) ? debug : ch == 27 ? "esc" : "");
411 if (ch < 32) {
412 // always process control chars, even after incomplete UTF-8 fragments
413 processC0(ch);
414 if (utf8state != UTF8_ACCEPT)
415 {
416 debug_printf("Terminal: incomplete UTF-8 fragment before control char ignored, codepoint=%u ch=%d\n", utf8codepoint, (int)ch);
417 utf8state = UTF8_ACCEPT;
418 }
419 return;
420 }
421 uint32_t rc = utf8decode(&utf8state, &utf8codepoint, (unsigned char)ch);
422 if (rc == UTF8_ACCEPT)
423 processCodePoint(utf8codepoint);
424 else if (rc == UTF8_REJECT) {
425 debug_printf("Terminal: invalid UTF-8 sequence ignored, codepoint=%u ch=%d\n", utf8codepoint, (int)ch);
426 utf8state = UTF8_ACCEPT;
427 }
428 // else we need to read more bytes to assemble a codepoint
429 }
430
431 bool inputChar(int ch)
432 {
433 debug_printf("inputChar: %d\n", ch);
that1964d192016-01-07 00:41:03 +0100434 initPty(); // reinit just in case it died before
435 // encode the char as UTF-8 and send it to the pty
436 std::string c;
437 utf8add(c, (uint32_t)ch);
438 pty.write(c.c_str(), c.size());
439 return true;
440 }
441
442 bool inputKey(int key)
443 {
444 debug_printf("inputKey: %d\n", key);
445 switch (key)
446 {
447 case KEY_UP: pty.write("\e[A"); break;
448 case KEY_DOWN: pty.write("\e[B"); break;
449 case KEY_RIGHT: pty.write("\e[C"); break;
450 case KEY_LEFT: pty.write("\e[D"); break;
451 case KEY_HOME: pty.write("\eOH"); break;
452 case KEY_END: pty.write("\eOF"); break;
453 case KEY_INSERT: pty.write("\e[2~"); break;
454 case KEY_DELETE: pty.write("\e[3~"); break;
455 case KEY_PAGEUP: pty.write("\e[5~"); break;
456 case KEY_PAGEDOWN: pty.write("\e[6~"); break;
457 // TODO: other keys
458 default:
459 return false;
460 }
461 return true;
462 }
463
464 size_t getLinesCount() const { return lines.size(); }
465 const Line& getLine(size_t n) { if (unpackedY == n) packLine(); return lines[n]; }
466 int getCursorX() const { return cursorX; }
467 int getCursorY() const { return cursorY; }
468 int getUpdateCounter() const { return updateCounter; }
469
470 void setX(int x)
471 {
472 x = min(width, max(x, 0));
473 cursorX = x;
that0d0d5222017-03-14 20:58:08 +0100474 linewrap = false;
that1964d192016-01-07 00:41:03 +0100475 ++updateCounter;
476 }
477
478 void setY(int y)
479 {
480 //y = min(height, max(y, 0));
481 y = max(y, 0);
482 cursorY = y;
that0d0d5222017-03-14 20:58:08 +0100483 linewrap = false;
that1964d192016-01-07 00:41:03 +0100484 while (lines.size() <= (size_t) y)
485 lines.push_back(Line());
486 ++updateCounter;
487 }
488
489 void up(int n = 1) { setY(cursorY - n); }
490 void down(int n = 1) { setY(cursorY + n); }
491 void left(int n = 1) { setX(cursorX - n); }
492 void right(int n = 1) { setX(cursorX + n); }
493
494private:
495 void packLine()
496 {
497 std::string& s = lines[unpackedY].text;
498 s.clear();
499 for (size_t i = 0; i < unpackedLine.cells.size(); ++i) {
500 Cell& c = unpackedLine.cells[i];
501 utf8add(s, c.cp);
502 // later: if attributes changed, add attributes
503 }
504 }
505
506 void unpackLine(size_t y)
507 {
508 uint32_t u8state = 0, u8cp = 0;
509 std::string& s = lines[y].text;
510 unpackedLine.cells.clear();
Matt Mowera8a89d12016-12-30 18:10:37 -0600511 for (size_t i = 0; i < s.size(); ++i) {
that1964d192016-01-07 00:41:03 +0100512 uint32_t rc = utf8decode(&u8state, &u8cp, (unsigned char)s[i]);
513 if (rc == UTF8_ACCEPT)
514 unpackedLine.cells.push_back(Cell(u8cp));
515 }
516 if (unpackedLine.cells.size() < (size_t)width)
517 unpackedLine.cells.resize(width);
518 unpackedY = y;
519 }
520
521 void ensureUnpacked(size_t y)
522 {
523 if (unpackedY != y)
524 {
525 packLine();
526 unpackLine(y);
527 }
528 }
529
530 void processC0(char ch)
531 {
532 switch (ch)
533 {
534 case 7: // BEL
bigbiff bigbiff3ed778a2019-03-12 19:28:31 -0400535
536#ifndef TW_NO_HAPTICS
that1964d192016-01-07 00:41:03 +0100537 DataManager::Vibrate("tw_button_vibrate");
bigbiff bigbiff3ed778a2019-03-12 19:28:31 -0400538#endif
539
that1964d192016-01-07 00:41:03 +0100540 break;
541 case 8: // BS
542 left();
543 break;
544 case 9: // HT
545 // TODO: this might be totally wrong
546 right();
547 while (cursorX % 8 != 0 && cursorX < width)
548 right();
549 break;
550 case 10: // LF
551 case 11: // VT
552 case 12: // FF
553 down();
554 break;
555 case 13: // CR
556 setX(0);
557 break;
558 case 24: // CAN
559 case 26: // SUB
560 state = kStateGround;
561 ctlseq.clear();
562 break;
563 case 27: // ESC
564 state = kStateEsc;
565 ctlseq.clear();
566 break;
567 }
568 }
569
570 void processCodePoint(CodePoint cp)
571 {
572 ++updateCounter;
573 debug_printf("codepoint: %u\n", cp);
574 if (cp == 0x9b) // CSI
575 {
576 state = kStateCsi;
577 ctlseq.clear();
578 return;
579 }
580 switch (state)
581 {
582 case kStateGround:
583 processChar(cp);
584 break;
585 case kStateEsc:
586 processEsc(cp);
587 break;
588 case kStateCsi:
589 processControlSequence(cp);
590 break;
591 }
592 }
593
594 void processChar(CodePoint cp)
595 {
that0d0d5222017-03-14 20:58:08 +0100596 if (linewrap) {
597 down();
598 setX(0);
599 }
that1964d192016-01-07 00:41:03 +0100600 ensureUnpacked(cursorY);
601 // extend unpackedLine if needed, write ch into cell
602 if (unpackedLine.cells.size() <= (size_t)cursorX)
603 unpackedLine.cells.resize(cursorX+1);
604 unpackedLine.cells[cursorX].cp = cp;
605
that0d0d5222017-03-14 20:58:08 +0100606 right(); // also bumps updateCounter
607
that1964d192016-01-07 00:41:03 +0100608 if (cursorX >= width)
that0d0d5222017-03-14 20:58:08 +0100609 linewrap = true;
that1964d192016-01-07 00:41:03 +0100610 }
611
612 void processEsc(CodePoint cp)
613 {
614 switch (cp) {
615 case 'c': // TODO: Reset
616 break;
617 case 'D': // Line feed
618 down();
619 break;
620 case 'E': // Newline
621 setX(0);
622 down();
623 break;
624 case '[': // CSI
625 state = kStateCsi;
626 ctlseq.clear();
627 break;
628 case ']': // TODO: OSC state
629 default:
630 state = kStateGround;
631 }
632 }
633
634 void processControlSequence(CodePoint cp)
635 {
636 if (cp >= 0x40 && cp <= 0x7e) {
637 ctlseq += cp;
638 execControlSequence(ctlseq);
639 ctlseq.clear();
640 state = kStateGround;
641 return;
642 }
643 if (isdigit(cp) || cp == ';' /* || (ch >= 0x3c && ch <= 0x3f) */) {
644 ctlseq += cp;
645 // state = kStateCsiParam;
646 return;
647 }
648 }
649
650 static int parseArg(std::string& s, int defaultvalue)
651 {
652 if (s.empty() || !isdigit(s[0]))
653 return defaultvalue;
654 int value = atoi(s.c_str());
655 size_t pos = s.find(';');
656 s.erase(0, pos != std::string::npos ? pos+1 : std::string::npos);
657 return value;
658 }
659
660 void execControlSequence(std::string ctlseq)
661 {
662 // assert(!ctlseq.empty());
663 if (ctlseq == "6n") {
664 // CPR - cursor position report
665 char answer[20];
666 sprintf(answer, "\e[%d;%dR", cursorY, cursorX);
667 pty.write(answer, strlen(answer));
668 return;
669 }
670 char f = *ctlseq.rbegin();
671 // if (f == '?') ... private mode
672 switch (f)
673 {
674 // case '@': // ICH - insert character
675 case 'A': // CUU - cursor up
676 up(parseArg(ctlseq, 1));
677 break;
678 case 'B': // CUD - cursor down
679 case 'e': // VPR - line position forward
680 down(parseArg(ctlseq, 1));
681 break;
682 case 'C': // CUF - cursor right
683 case 'a': // HPR - character position forward
684 right(parseArg(ctlseq, 1));
685 break;
686 case 'D': // CUB - cursor left
687 left(parseArg(ctlseq, 1));
688 break;
689 case 'E': // CNL - cursor next line
690 down(parseArg(ctlseq, 1));
691 setX(0);
692 break;
693 case 'F': // CPL - cursor preceding line
694 up(parseArg(ctlseq, 1));
695 setX(0);
696 break;
697 case 'G': // CHA - cursor character absolute
698 setX(parseArg(ctlseq, 1)-1);
699 break;
700 case 'H': // CUP - cursor position
701 // TODO: consider scrollback area
702 setY(parseArg(ctlseq, 1)-1);
703 setX(parseArg(ctlseq, 1)-1);
704 break;
705 case 'J': // ED - erase in page
706 {
707 int param = parseArg(ctlseq, 0);
708 ensureUnpacked(cursorY);
709 switch (param) {
710 default:
711 case 0:
712 unpackedLine.eraseFrom(cursorX);
713 if (lines.size() > (size_t)cursorY+1)
714 lines.erase(lines.begin() + cursorY+1, lines.end());
715 break;
716 case 1:
717 unpackedLine.eraseTo(cursorX);
718 if (cursorY > 0) {
719 lines.erase(lines.begin(), lines.begin() + cursorY-1);
720 cursorY = 0;
721 }
722 break;
723 case 2: // clear
724 case 3: // clear incl scrollback
725 clear();
726 break;
727 }
728 }
729 break;
730 case 'K': // EL - erase in line
731 {
732 int param = parseArg(ctlseq, 0);
733 ensureUnpacked(cursorY);
734 switch (param) {
735 default:
736 case 0:
737 unpackedLine.eraseFrom(cursorX);
738 break;
739 case 1:
740 unpackedLine.eraseTo(cursorX);
741 break;
742 case 2:
743 unpackedLine.cells.clear();
744 break;
745 }
746 }
747 break;
748 // case 'L': // IL - insert line
749
750 default:
751 debug_printf("unknown ctlseq: '%s'\n", ctlseq.c_str());
752 break;
753 }
754 }
755
756private:
757 int cursorX, cursorY; // 0-based, char based. TODO: decide how to handle scrollback
that0d0d5222017-03-14 20:58:08 +0100758 bool linewrap; // true to put next character into next line
that1964d192016-01-07 00:41:03 +0100759 int width, height; // window size in chars
760 std::vector<Line> lines; // the text buffer
761 UnpackedLine unpackedLine; // current line for editing
762 size_t unpackedY; // number of current line
763 int updateCounter; // changes whenever terminal could require redraw
764
765 Pseudoterminal pty;
766 enum { kStateGround, kStateEsc, kStateCsi } state;
767
768 // for accumulating a full UTF-8 character from individual bytes
769 uint32_t utf8state;
770 uint32_t utf8codepoint;
771
772 // for accumulating a control sequence after receiving CSI
773 std::string ctlseq;
774};
775
776// The one and only terminal engine for now
777TerminalEngine gEngine;
778
779void terminal_pty_read()
780{
781 gEngine.readPty();
782}
783
784
785GUITerminal::GUITerminal(xml_node<>* node) : GUIScrollList(node)
786{
787 allowSelection = false; // terminal doesn't support list item selections
788 lastCondition = false;
789
790 if (!node) {
Matt Mowera8a89d12016-12-30 18:10:37 -0600791 mRenderX = 0;
792 mRenderY = 0;
793 mRenderW = gr_fb_width();
794 mRenderH = gr_fb_height();
that1964d192016-01-07 00:41:03 +0100795 }
796
797 engine = &gEngine;
798 updateCounter = 0;
799}
800
801int GUITerminal::Update(void)
802{
Matt Mowera8a89d12016-12-30 18:10:37 -0600803 if (!isConditionTrue()) {
that1964d192016-01-07 00:41:03 +0100804 lastCondition = false;
805 return 0;
806 }
807
808 if (lastCondition == false) {
809 lastCondition = true;
810 // we're becoming visible, so we might need to resize the terminal content
811 InitAndResize();
812 }
813
814 if (updateCounter != engine->getUpdateCounter()) {
815 // try to keep the cursor in view
816 SetVisibleListLocation(engine->getCursorY());
817 updateCounter = engine->getUpdateCounter();
818 }
819
820 GUIScrollList::Update();
821
822 if (mUpdate) {
823 mUpdate = 0;
824 if (Render() == 0)
825 return 2;
826 }
827 return 0;
828}
829
830// NotifyTouch - Notify of a touch event
831// Return 0 on success, >0 to ignore remainder of touch, and <0 on error
832int GUITerminal::NotifyTouch(TOUCH_STATE state, int x, int y)
833{
Matt Mowera8a89d12016-12-30 18:10:37 -0600834 if (!isConditionTrue())
that1964d192016-01-07 00:41:03 +0100835 return -1;
836
837 // TODO: grab focus correctly
838 // TODO: fix focus handling in PageManager and GUIInput
839 SetInputFocus(1);
840 debug_printf("Terminal: SetInputFocus\n");
841 return GUIScrollList::NotifyTouch(state, x, y);
842 // TODO later: allow cursor positioning by touch (simulate mouse click?)
843 // http://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash
844 // will likely not work with Busybox anyway
845}
846
847int GUITerminal::NotifyKey(int key, bool down)
848{
thatd4725ca2016-01-19 00:15:21 +0100849 if (!HasInputFocus)
850 return 1;
that1964d192016-01-07 00:41:03 +0100851 if (down)
852 if (engine->inputKey(key))
853 mUpdate = 1;
854 return 0;
855}
856
857// character input
858int GUITerminal::NotifyCharInput(int ch)
859{
860 if (engine->inputChar(ch))
861 mUpdate = 1;
862 return 0;
863}
864
865size_t GUITerminal::GetItemCount()
866{
867 return engine->getLinesCount();
868}
869
Ethan Yonker58f21322018-08-24 11:17:36 -0500870void GUITerminal::RenderItem(size_t itemindex, int yPos, bool selected __unused)
that1964d192016-01-07 00:41:03 +0100871{
872 const TerminalEngine::Line& line = engine->getLine(itemindex);
Ethan Yonker58f21322018-08-24 11:17:36 -0500873
874 if (!mFont || !mFont->GetResource())
875 return;
that1964d192016-01-07 00:41:03 +0100876
877 gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha);
878 // later: handle attributes here
879
880 // render text
881 const char* text = line.text.c_str();
882 gr_textEx_scaleW(mRenderX, yPos, text, mFont->GetResource(), mRenderW, TOP_LEFT, 0);
883
884 if (itemindex == (size_t) engine->getCursorY()) {
885 // render cursor
886 int cursorX = engine->getCursorX();
887 std::string leftOfCursor = line.substr(0, cursorX);
bigbiffd58ba182020-03-23 10:02:29 -0400888 int x = twrpTruetype::gr_ttf_measureEx(leftOfCursor.c_str(), mFont->GetResource());
that1964d192016-01-07 00:41:03 +0100889 // note that this single character can be a UTF-8 sequence
890 std::string atCursor = (size_t)cursorX < line.length() ? line.substr(cursorX, 1) : " ";
bigbiffd58ba182020-03-23 10:02:29 -0400891 int w = twrpTruetype::gr_ttf_measureEx(atCursor.c_str(), mFont->GetResource());
that1964d192016-01-07 00:41:03 +0100892 gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha);
893 gr_fill(mRenderX + x, yPos, w, actualItemHeight);
894 gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, mBackgroundColor.alpha);
895 gr_textEx_scaleW(mRenderX + x, yPos, atCursor.c_str(), mFont->GetResource(), mRenderW, TOP_LEFT, 0);
896 }
897}
898
Ethan Yonker58f21322018-08-24 11:17:36 -0500899void GUITerminal::NotifySelect(size_t item_selected __unused)
that1964d192016-01-07 00:41:03 +0100900{
901 // do nothing - terminal ignores selections
902}
903
904void GUITerminal::InitAndResize()
905{
906 // make sure the shell is started
907 engine->initPty();
908 // send window resize
Ethan Yonker58f21322018-08-24 11:17:36 -0500909 if (mFont && mFont->GetResource()) {
bigbiffd58ba182020-03-23 10:02:29 -0400910 int charWidth = twrpTruetype::gr_ttf_measureEx("N", mFont->GetResource());
Ethan Yonker58f21322018-08-24 11:17:36 -0500911 engine->setSize(mRenderW / charWidth, GetDisplayItemCount(), mRenderW, mRenderH);
912 }
that1964d192016-01-07 00:41:03 +0100913}
914
915void GUITerminal::SetPageFocus(int inFocus)
916{
Ethan Yonkerec0b4322016-03-31 14:04:14 -0500917 if (inFocus && isConditionTrue()) {
918 // TODO: grab focus correctly, this hack grabs focus and insists that the terminal be the focus regardless of other elements
919 // It's highly unlikely that there will be any other visible input elements on the page anyway...
920 SetInputFocus(1);
that1964d192016-01-07 00:41:03 +0100921 InitAndResize();
Ethan Yonkerec0b4322016-03-31 14:04:14 -0500922 }
that1964d192016-01-07 00:41:03 +0100923}