gui: add terminal emulator

Emulates enough of a VT-100 to run busybox vi.

Change-Id: I99c829c6c9de2246194ecb8b8b3cdf4ac34a0606
diff --git a/gui/Android.mk b/gui/Android.mk
index 40e0cd9..77fd52a 100644
--- a/gui/Android.mk
+++ b/gui/Android.mk
@@ -29,6 +29,7 @@
     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 63baeee..08178fc 100644
--- a/gui/gui.cpp
+++ b/gui/gui.cpp
@@ -80,6 +80,9 @@
 // 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 @@
 	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 5e09607..15ad1e6 100644
--- a/gui/objects.hpp
+++ b/gui/objects.hpp
@@ -81,6 +81,7 @@
 	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 @@
 	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 c097c39..bd7c799 100644
--- a/gui/pages.cpp
+++ b/gui/pages.cpp
@@ -372,6 +372,14 @@
 			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 0000000..00424eb
--- /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 <http://www.gnu.org/licenses/>.
+*/
+
+// terminal.cpp - GUITerminal object
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <termio.h>
+
+#include <string>
+#include <cctype>
+#include <linux/input.h>
+
+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<size_t n>
+	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 <bjoern@hoehrmann.de>
+// 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<AttributeRange> 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<Cell> 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<Line> 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();
+}