From 1964d19c464d25fb1898a8ea38579f97b36fafd5 Mon Sep 17 00:00:00 2001 From: that Date: Thu, 7 Jan 2016 00:41:03 +0100 Subject: gui: add terminal emulator Emulates enough of a VT-100 to run busybox vi. Change-Id: I99c829c6c9de2246194ecb8b8b3cdf4ac34a0606 --- gui/Android.mk | 1 + gui/gui.cpp | 14 + gui/objects.hpp | 38 +++ gui/pages.cpp | 8 + gui/terminal.cpp | 888 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 949 insertions(+) create mode 100644 gui/terminal.cpp diff --git a/gui/Android.mk b/gui/Android.mk index 40e0cd936..77fd52abc 100644 --- a/gui/Android.mk +++ b/gui/Android.mk @@ -29,6 +29,7 @@ LOCAL_SRC_FILES := \ scrolllist.cpp \ patternpassword.cpp \ textbox.cpp \ + terminal.cpp \ twmsg.cpp ifneq ($(TWRP_CUSTOM_KEYBOARD),) diff --git a/gui/gui.cpp b/gui/gui.cpp index 63baeee92..08178fc17 100644 --- a/gui/gui.cpp +++ b/gui/gui.cpp @@ -80,6 +80,9 @@ static float scale_theme_h = 1; // Needed by pages.cpp too int gGuiRunning = 0; +int g_pty_fd = -1; // set by terminal on init +void terminal_pty_read(); + static int gRecorder = -1; extern "C" void gr_write_frame_to_file(int fd); @@ -640,6 +643,17 @@ static int runPages(const char *page_name, const int stop_on_page_done) for (;;) { loopTimer(input_timeout_ms); + if (g_pty_fd > 0) { + // TODO: this is not nice, we should have one central select for input, pty, and ors + FD_ZERO(&fdset); + FD_SET(g_pty_fd, &fdset); + timeout.tv_sec = 0; + timeout.tv_usec = 1; + has_data = select(g_pty_fd+1, &fdset, NULL, NULL, &timeout); + if (has_data > 0) { + terminal_pty_read(); + } + } #ifndef TW_OEM_BUILD if (ors_read_fd > 0 && !orsout) { // orsout is non-NULL if a command is still running FD_ZERO(&fdset); diff --git a/gui/objects.hpp b/gui/objects.hpp index 5e0960777..15ad1e6c2 100644 --- a/gui/objects.hpp +++ b/gui/objects.hpp @@ -81,6 +81,7 @@ public: virtual int SetPlacement(Placement placement) { mPlacement = placement; return 0; } // SetPageFocus - Notify when a page gains or loses focus + // TODO: This should be named NotifyPageFocus for consistency virtual void SetPageFocus(int inFocus __unused) { return; } protected: @@ -767,6 +768,43 @@ protected: int RenderConsole(void); }; +class TerminalEngine; +class GUITerminal : public GUIScrollList, public InputObject +{ +public: + GUITerminal(xml_node<>* node); + +public: + // Update - Update any UI component animations (called <= 30 FPS) + // Return 0 if nothing to update, 1 on success and contiue, >1 if full render required, and <0 on error + virtual int Update(void); + + // NotifyTouch - Notify of a touch event + // Return 0 on success, >0 to ignore remainder of touch, and <0 on error (Return error to allow other handlers) + virtual int NotifyTouch(TOUCH_STATE state, int x, int y); + + // NotifyKey - Notify of a key press + // Return 0 on success (and consume key), >0 to pass key to next handler, and <0 on error + virtual int NotifyKey(int key, bool down); + + // character input + virtual int NotifyCharInput(int ch); + + // SetPageFocus - Notify when a page gains or loses focus + virtual void SetPageFocus(int inFocus); + + // ScrollList interface + virtual size_t GetItemCount(); + virtual void RenderItem(size_t itemindex, int yPos, bool selected); + virtual void NotifySelect(size_t item_selected); +protected: + void InitAndResize(); + + TerminalEngine* engine; // non-visual parts of the terminal (text buffer etc.), not owned + int updateCounter; // to track if anything changed in the back-end + bool lastCondition; // to track if the condition became true and we might need to resize the terminal engine +}; + // GUIAnimation - Used for animations class GUIAnimation : public GUIObject, public RenderObject { diff --git a/gui/pages.cpp b/gui/pages.cpp index c097c39bd..bd7c79959 100644 --- a/gui/pages.cpp +++ b/gui/pages.cpp @@ -372,6 +372,14 @@ bool Page::ProcessNode(xml_node<>* page, std::vector*> *templates, in mRenders.push_back(element); mActions.push_back(element); } + else if (type == "terminal") + { + GUITerminal* element = new GUITerminal(child); + mObjects.push_back(element); + mRenders.push_back(element); + mActions.push_back(element); + mInputs.push_back(element); + } else if (type == "button") { GUIButton* element = new GUIButton(child); diff --git a/gui/terminal.cpp b/gui/terminal.cpp new file mode 100644 index 000000000..00424eb90 --- /dev/null +++ b/gui/terminal.cpp @@ -0,0 +1,888 @@ +/* + Copyright 2016 _that/TeamWin + This file is part of TWRP/TeamWin Recovery Project. + + TWRP is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + TWRP is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with TWRP. If not, see . +*/ + +// terminal.cpp - GUITerminal object + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +extern "C" { +#include "../twcommon.h" +#include "../minuitwrp/minui.h" +} + +#include "rapidxml.hpp" +#include "objects.hpp" + +#if 0 +#define debug_printf printf +#else +#define debug_printf(...) +#endif + +extern int g_pty_fd; // in gui.cpp where the select is + +/* +Pseudoterminal handler. +*/ +class Pseudoterminal +{ +public: + Pseudoterminal() : fdMaster(0), pid(0) + { + } + + bool started() const { return pid > 0; } + + bool start() + { + fdMaster = getpt(); + if (fdMaster < 0) { + LOGERR("Error %d on getpt()\n", errno); + return false; + } + + if (unlockpt(fdMaster) != 0) { + LOGERR("Error %d on unlockpt()\n", errno); + return false; + } + + pid = fork(); + if (pid < 0) { + LOGERR("fork failed for pty, error %d\n", errno); + close(fdMaster); + pid = 0; + return false; + } + else if (pid) { + // child started, now someone needs to periodically read from fdMaster + // and write it to the terminal + // this currently works through gui.cpp calling terminal_pty_read below + g_pty_fd = fdMaster; + return true; + } + else { + int fdSlave = open(ptsname(fdMaster), O_RDWR); + close(fdMaster); + runSlave(fdSlave); + } + // we can't get here + LOGERR("impossible error in pty\n"); + return false; + } + + void runSlave(int fdSlave) + { + dup2(fdSlave, 0); // PTY becomes standard input (0) + dup2(fdSlave, 1); // PTY becomes standard output (1) + dup2(fdSlave, 2); // PTY becomes standard error (2) + + // Now the original file descriptor is useless + close(fdSlave); + + // Make the current process a new session leader + if (setsid() == (pid_t)-1) + LOGERR("setsid failed: %d\n", errno); + + // As the child is a session leader, set the controlling terminal to be the slave side of the PTY + // (Mandatory for programs like the shell to make them manage correctly their outputs) + ioctl(0, TIOCSCTTY, 1); + + execl("/sbin/sh", "sh", NULL); + _exit(127); + } + + int read(char* buffer, size_t size) + { + if (!started()) { + LOGERR("someone tried to read from pty, but it was not started\n"); + return -1; + } + int rc = ::read(fdMaster, buffer, size); + debug_printf("pty read: %d bytes\n", rc); + if (rc < 0) { + LOGINFO("pty read failed: %d\n", errno); + // assume child has died + close(fdMaster); + g_pty_fd = fdMaster = -1; + pid = 0; + } + return rc; + } + + int write(const char* buffer, size_t size) + { + if (!started()) { + LOGERR("someone tried to write to pty, but it was not started\n"); + return -1; + } + int rc = ::write(fdMaster, buffer, size); + debug_printf("pty write: %d bytes -> %d\n", size, rc); + if (rc < 0) { + LOGINFO("pty write failed: %d\n", errno); + // assume child has died + close(fdMaster); + g_pty_fd = fdMaster = -1; + pid = 0; + } + return rc; + } + + template + inline int write(const char (&literal)[n]) + { + return write(literal, n-1); + } + + void resize(int xChars, int yChars, int w, int h) + { + struct winsize ws; + ws.ws_row = yChars; + ws.ws_col = xChars; + ws.ws_xpixel = w; + ws.ws_ypixel = h; + if (ioctl(fdMaster, TIOCSWINSZ, &ws) < 0) + LOGERR("failed to set window size, error %d\n", errno); + } + +private: + int fdMaster; + int pid; +}; + +// UTF-8 decoder +// Copyright (c) 2008-2009 Bjoern Hoehrmann +// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. + +const uint32_t UTF8_ACCEPT = 0; +const uint32_t UTF8_REJECT = 1; + +static const uint8_t utf8d[] = { + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef + 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff + 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0 + 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 + 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 + 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 + 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 +}; + +uint32_t inline utf8decode(uint32_t* state, uint32_t* codep, uint32_t byte) +{ + uint32_t type = utf8d[byte]; + + *codep = (*state != UTF8_ACCEPT) ? + (byte & 0x3fu) | (*codep << 6) : + (0xff >> type) & (byte); + + *state = utf8d[256 + *state*16 + type]; + return *state; +} +// end of UTF-8 decoder + +// Append a UTF-8 codepoint to string s +size_t utf8add(std::string& s, uint32_t cp) +{ + if (cp < 0x7f) { + s += cp; + return 1; + } + else if (cp < 0x7ff) { + s += (0xc0 | (cp >> 6)); + s += (0x80 | (cp & 0x3f)); + return 2; + } + else if (cp < 0xffff) { + s += (0xe0 | (cp >> 12)); + s += (0x80 | ((cp >> 6) & 0x3f)); + s += (0x80 | (cp & 0x3f)); + return 3; + } + else if (cp < 0x1fffff) { + s += (0xf0 | (cp >> 18)); + s += (0x80 | ((cp >> 12) & 0x3f)); + s += (0x80 | ((cp >> 6) & 0x3f)); + s += (0x80 | (cp & 0x3f)); + return 4; + } + return 0; +} + +/* +TerminalEngine is the terminal back-end, dealing with the text buffer and attributes +and with communicating with the pty. +It does not care about visual things like rendering, fonts, windows etc. +The idea is that 0 to n GUITerminal instances (e.g. on different pages) can connect +to one TerminalEngine to interact with the terminal, and that the TerminalEngine +survives things like page changes or even theme reloads. +*/ +class TerminalEngine +{ +public: +#if 0 // later + struct Attributes + { + COLOR fgcolor; // TODO: what about palette? + COLOR bgcolor; + // could add bold, underline, blink, etc. + }; + + struct AttributeRange + { + size_t start; // start position inside text (in bytes) + Attributes a; + }; +#endif + typedef uint32_t CodePoint; // Unicode code point + + // A line of text, optimized for rendering and storage in the buffer + struct Line + { + std::string text; // in UTF-8 format +// std::vector attrs; + Line() {} + size_t utf8forward(size_t start) const + { + if (start >= text.size()) + return start; + uint32_t u8state = 0, u8cp = 0; + size_t i = start; + uint32_t rc; + do { + rc = utf8decode(&u8state, &u8cp, (unsigned char)text[i]); + ++i; + } while (rc != UTF8_ACCEPT && rc != UTF8_REJECT && i < text.size()); + return i; + } + + std::string substr(size_t start, size_t n) const + { + size_t i = 0; + for (; start && i < text.size(); i = utf8forward(i)) + --start; + size_t s = i; + for (; n && i < text.size(); i = utf8forward(i)) + --n; + return text.substr(s, i - s); + } + size_t length() const + { + size_t n = 0; + for (size_t i = 0; i < text.size(); i = utf8forward(i)) + ++n; + return n; + } + }; + + // A single character cell with a Unicode code point + struct Cell + { + Cell() : cp(' ') {} + Cell(CodePoint cp) : cp(cp) {} + CodePoint cp; +// Attributes a; + }; + + // A line of text, optimized for editing single characters + struct UnpackedLine + { + std::vector cells; + void eraseFrom(size_t x) + { + if (cells.size() > x) + cells.erase(cells.begin() + x, cells.end()); + } + + void eraseTo(size_t x) + { + if (x > 0) + cells.erase(cells.begin(), cells.begin() + x); + } + }; + + TerminalEngine() + { + // the default size will be overwritten by the GUI window when the size is known + width = 40; + height = 10; + + clear(); + updateCounter = 0; + state = kStateGround; + utf8state = utf8codepoint = 0; + } + + void setSize(int xChars, int yChars, int w, int h) + { + width = xChars; + height = yChars; + if (pty.started()) + pty.resize(width, height, w, h); + debug_printf("setSize: %d*%d chars, %d*%d pixels\n", xChars, yChars, w, h); + } + + void initPty() + { + if (!pty.started()) + { + pty.start(); + pty.resize(width, height, 0, 0); + } + } + + void readPty() + { + char buffer[1024]; + int rc = pty.read(buffer, sizeof(buffer)); + debug_printf("readPty: %d bytes\n", rc); + if (rc < 0) + output("\r\nChild process exited.\r\n"); // TODO: maybe exit terminal here + else + for (int i = 0; i < rc; ++i) + output(buffer[i]); + } + + void clear() + { + cursorX = cursorY = 0; + lines.clear(); + setY(0); + unpackLine(0); + ++updateCounter; + } + + void output(const char *buf) + { + for (const char* p = buf; *p; ++p) + output(*p); + } + + void output(const char ch) + { + char debug[2]; debug[0] = ch; debug[1] = 0; + debug_printf("output: %d %s\n", (int)ch, (ch >= ' ' && ch < 127) ? debug : ch == 27 ? "esc" : ""); + if (ch < 32) { + // always process control chars, even after incomplete UTF-8 fragments + processC0(ch); + if (utf8state != UTF8_ACCEPT) + { + debug_printf("Terminal: incomplete UTF-8 fragment before control char ignored, codepoint=%u ch=%d\n", utf8codepoint, (int)ch); + utf8state = UTF8_ACCEPT; + } + return; + } + uint32_t rc = utf8decode(&utf8state, &utf8codepoint, (unsigned char)ch); + if (rc == UTF8_ACCEPT) + processCodePoint(utf8codepoint); + else if (rc == UTF8_REJECT) { + debug_printf("Terminal: invalid UTF-8 sequence ignored, codepoint=%u ch=%d\n", utf8codepoint, (int)ch); + utf8state = UTF8_ACCEPT; + } + // else we need to read more bytes to assemble a codepoint + } + + bool inputChar(int ch) + { + debug_printf("inputChar: %d\n", ch); + if (ch == 13) + ch = 10; + initPty(); // reinit just in case it died before + // encode the char as UTF-8 and send it to the pty + std::string c; + utf8add(c, (uint32_t)ch); + pty.write(c.c_str(), c.size()); + return true; + } + + bool inputKey(int key) + { + debug_printf("inputKey: %d\n", key); + switch (key) + { + case KEY_UP: pty.write("\e[A"); break; + case KEY_DOWN: pty.write("\e[B"); break; + case KEY_RIGHT: pty.write("\e[C"); break; + case KEY_LEFT: pty.write("\e[D"); break; + case KEY_HOME: pty.write("\eOH"); break; + case KEY_END: pty.write("\eOF"); break; + case KEY_INSERT: pty.write("\e[2~"); break; + case KEY_DELETE: pty.write("\e[3~"); break; + case KEY_PAGEUP: pty.write("\e[5~"); break; + case KEY_PAGEDOWN: pty.write("\e[6~"); break; + // TODO: other keys + default: + return false; + } + return true; + } + + size_t getLinesCount() const { return lines.size(); } + const Line& getLine(size_t n) { if (unpackedY == n) packLine(); return lines[n]; } + int getCursorX() const { return cursorX; } + int getCursorY() const { return cursorY; } + int getUpdateCounter() const { return updateCounter; } + + void setX(int x) + { + x = min(width, max(x, 0)); + cursorX = x; + ++updateCounter; + } + + void setY(int y) + { + //y = min(height, max(y, 0)); + y = max(y, 0); + cursorY = y; + while (lines.size() <= (size_t) y) + lines.push_back(Line()); + ++updateCounter; + } + + void up(int n = 1) { setY(cursorY - n); } + void down(int n = 1) { setY(cursorY + n); } + void left(int n = 1) { setX(cursorX - n); } + void right(int n = 1) { setX(cursorX + n); } + +private: + void packLine() + { + std::string& s = lines[unpackedY].text; + s.clear(); + for (size_t i = 0; i < unpackedLine.cells.size(); ++i) { + Cell& c = unpackedLine.cells[i]; + utf8add(s, c.cp); + // later: if attributes changed, add attributes + } + } + + void unpackLine(size_t y) + { + uint32_t u8state = 0, u8cp = 0; + std::string& s = lines[y].text; + unpackedLine.cells.clear(); + for(size_t i = 0; i < s.size(); ++i) { + uint32_t rc = utf8decode(&u8state, &u8cp, (unsigned char)s[i]); + if (rc == UTF8_ACCEPT) + unpackedLine.cells.push_back(Cell(u8cp)); + } + if (unpackedLine.cells.size() < (size_t)width) + unpackedLine.cells.resize(width); + unpackedY = y; + } + + void ensureUnpacked(size_t y) + { + if (unpackedY != y) + { + packLine(); + unpackLine(y); + } + } + + void processC0(char ch) + { + switch (ch) + { + case 7: // BEL + DataManager::Vibrate("tw_button_vibrate"); + break; + case 8: // BS + left(); + break; + case 9: // HT + // TODO: this might be totally wrong + right(); + while (cursorX % 8 != 0 && cursorX < width) + right(); + break; + case 10: // LF + case 11: // VT + case 12: // FF + down(); + break; + case 13: // CR + setX(0); + break; + case 24: // CAN + case 26: // SUB + state = kStateGround; + ctlseq.clear(); + break; + case 27: // ESC + state = kStateEsc; + ctlseq.clear(); + break; + } + } + + void processCodePoint(CodePoint cp) + { + ++updateCounter; + debug_printf("codepoint: %u\n", cp); + if (cp == 0x9b) // CSI + { + state = kStateCsi; + ctlseq.clear(); + return; + } + switch (state) + { + case kStateGround: + processChar(cp); + break; + case kStateEsc: + processEsc(cp); + break; + case kStateCsi: + processControlSequence(cp); + break; + } + } + + void processChar(CodePoint cp) + { + ensureUnpacked(cursorY); + // extend unpackedLine if needed, write ch into cell + if (unpackedLine.cells.size() <= (size_t)cursorX) + unpackedLine.cells.resize(cursorX+1); + unpackedLine.cells[cursorX].cp = cp; + + right(); + if (cursorX >= width) + { + // TODO: configurable line wrapping + // TODO: don't go down immediately but only on next char? + down(); + setX(0); + } + // TODO: update all GUI objects that display this terminal engine + } + + void processEsc(CodePoint cp) + { + switch (cp) { + case 'c': // TODO: Reset + break; + case 'D': // Line feed + down(); + break; + case 'E': // Newline + setX(0); + down(); + break; + case '[': // CSI + state = kStateCsi; + ctlseq.clear(); + break; + case ']': // TODO: OSC state + default: + state = kStateGround; + } + } + + void processControlSequence(CodePoint cp) + { + if (cp >= 0x40 && cp <= 0x7e) { + ctlseq += cp; + execControlSequence(ctlseq); + ctlseq.clear(); + state = kStateGround; + return; + } + if (isdigit(cp) || cp == ';' /* || (ch >= 0x3c && ch <= 0x3f) */) { + ctlseq += cp; + // state = kStateCsiParam; + return; + } + } + + static int parseArg(std::string& s, int defaultvalue) + { + if (s.empty() || !isdigit(s[0])) + return defaultvalue; + int value = atoi(s.c_str()); + size_t pos = s.find(';'); + s.erase(0, pos != std::string::npos ? pos+1 : std::string::npos); + return value; + } + + void execControlSequence(std::string ctlseq) + { + // assert(!ctlseq.empty()); + if (ctlseq == "6n") { + // CPR - cursor position report + char answer[20]; + sprintf(answer, "\e[%d;%dR", cursorY, cursorX); + pty.write(answer, strlen(answer)); + return; + } + char f = *ctlseq.rbegin(); + // if (f == '?') ... private mode + switch (f) + { + // case '@': // ICH - insert character + case 'A': // CUU - cursor up + up(parseArg(ctlseq, 1)); + break; + case 'B': // CUD - cursor down + case 'e': // VPR - line position forward + down(parseArg(ctlseq, 1)); + break; + case 'C': // CUF - cursor right + case 'a': // HPR - character position forward + right(parseArg(ctlseq, 1)); + break; + case 'D': // CUB - cursor left + left(parseArg(ctlseq, 1)); + break; + case 'E': // CNL - cursor next line + down(parseArg(ctlseq, 1)); + setX(0); + break; + case 'F': // CPL - cursor preceding line + up(parseArg(ctlseq, 1)); + setX(0); + break; + case 'G': // CHA - cursor character absolute + setX(parseArg(ctlseq, 1)-1); + break; + case 'H': // CUP - cursor position + // TODO: consider scrollback area + setY(parseArg(ctlseq, 1)-1); + setX(parseArg(ctlseq, 1)-1); + break; + case 'J': // ED - erase in page + { + int param = parseArg(ctlseq, 0); + ensureUnpacked(cursorY); + switch (param) { + default: + case 0: + unpackedLine.eraseFrom(cursorX); + if (lines.size() > (size_t)cursorY+1) + lines.erase(lines.begin() + cursorY+1, lines.end()); + break; + case 1: + unpackedLine.eraseTo(cursorX); + if (cursorY > 0) { + lines.erase(lines.begin(), lines.begin() + cursorY-1); + cursorY = 0; + } + break; + case 2: // clear + case 3: // clear incl scrollback + clear(); + break; + } + } + break; + case 'K': // EL - erase in line + { + int param = parseArg(ctlseq, 0); + ensureUnpacked(cursorY); + switch (param) { + default: + case 0: + unpackedLine.eraseFrom(cursorX); + break; + case 1: + unpackedLine.eraseTo(cursorX); + break; + case 2: + unpackedLine.cells.clear(); + break; + } + } + break; + // case 'L': // IL - insert line + + default: + debug_printf("unknown ctlseq: '%s'\n", ctlseq.c_str()); + break; + } + } + +private: + int cursorX, cursorY; // 0-based, char based. TODO: decide how to handle scrollback + int width, height; // window size in chars + std::vector lines; // the text buffer + UnpackedLine unpackedLine; // current line for editing + size_t unpackedY; // number of current line + int updateCounter; // changes whenever terminal could require redraw + + Pseudoterminal pty; + enum { kStateGround, kStateEsc, kStateCsi } state; + + // for accumulating a full UTF-8 character from individual bytes + uint32_t utf8state; + uint32_t utf8codepoint; + + // for accumulating a control sequence after receiving CSI + std::string ctlseq; +}; + +// The one and only terminal engine for now +TerminalEngine gEngine; + +void terminal_pty_read() +{ + gEngine.readPty(); +} + + +GUITerminal::GUITerminal(xml_node<>* node) : GUIScrollList(node) +{ + allowSelection = false; // terminal doesn't support list item selections + lastCondition = false; + + if (!node) { + mRenderX = 0; mRenderY = 0; mRenderW = gr_fb_width(); mRenderH = gr_fb_height(); + } + + engine = &gEngine; + updateCounter = 0; +} + +int GUITerminal::Update(void) +{ + if(!isConditionTrue()) { + lastCondition = false; + return 0; + } + + if (lastCondition == false) { + lastCondition = true; + // we're becoming visible, so we might need to resize the terminal content + InitAndResize(); + } + + if (updateCounter != engine->getUpdateCounter()) { + // try to keep the cursor in view + SetVisibleListLocation(engine->getCursorY()); + updateCounter = engine->getUpdateCounter(); + } + + GUIScrollList::Update(); + + if (mUpdate) { + mUpdate = 0; + if (Render() == 0) + return 2; + } + return 0; +} + +// NotifyTouch - Notify of a touch event +// Return 0 on success, >0 to ignore remainder of touch, and <0 on error +int GUITerminal::NotifyTouch(TOUCH_STATE state, int x, int y) +{ + if(!isConditionTrue()) + return -1; + + // TODO: grab focus correctly + // TODO: fix focus handling in PageManager and GUIInput + SetInputFocus(1); + debug_printf("Terminal: SetInputFocus\n"); + return GUIScrollList::NotifyTouch(state, x, y); + // TODO later: allow cursor positioning by touch (simulate mouse click?) + // http://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash + // will likely not work with Busybox anyway +} + +int GUITerminal::NotifyKey(int key, bool down) +{ + if (down) + if (engine->inputKey(key)) + mUpdate = 1; + return 0; +} + +// character input +int GUITerminal::NotifyCharInput(int ch) +{ + if (engine->inputChar(ch)) + mUpdate = 1; + return 0; +} + +size_t GUITerminal::GetItemCount() +{ + return engine->getLinesCount(); +} + +void GUITerminal::RenderItem(size_t itemindex, int yPos, bool selected) +{ + const TerminalEngine::Line& line = engine->getLine(itemindex); + + gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha); + // later: handle attributes here + + // render text + const char* text = line.text.c_str(); + gr_textEx_scaleW(mRenderX, yPos, text, mFont->GetResource(), mRenderW, TOP_LEFT, 0); + + if (itemindex == (size_t) engine->getCursorY()) { + // render cursor + int cursorX = engine->getCursorX(); + std::string leftOfCursor = line.substr(0, cursorX); + int x = gr_measureEx(leftOfCursor.c_str(), mFont->GetResource()); + // note that this single character can be a UTF-8 sequence + std::string atCursor = (size_t)cursorX < line.length() ? line.substr(cursorX, 1) : " "; + int w = gr_measureEx(atCursor.c_str(), mFont->GetResource()); + gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha); + gr_fill(mRenderX + x, yPos, w, actualItemHeight); + gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, mBackgroundColor.alpha); + gr_textEx_scaleW(mRenderX + x, yPos, atCursor.c_str(), mFont->GetResource(), mRenderW, TOP_LEFT, 0); + } +} + +void GUITerminal::NotifySelect(size_t item_selected) +{ + // do nothing - terminal ignores selections +} + +void GUITerminal::InitAndResize() +{ + // make sure the shell is started + engine->initPty(); + // send window resize + int charWidth = gr_measureEx("N", mFont->GetResource()); + engine->setSize(mRenderW / charWidth, GetDisplayItemCount(), mRenderW, mRenderH); +} + +void GUITerminal::SetPageFocus(int inFocus) +{ + if (inFocus && isConditionTrue()) + InitAndResize(); +} -- cgit v1.2.3