summaryrefslogblamecommitdiffstats
path: root/gui/terminal.cpp
blob: 65ad2c026635b740f51b93e64146d6fe23a9f9d9 (plain) (tree)





























                                                                            
                     


                        
 
                               
                  
















































                                                                                               
                                        









































                                                                                                                 



                                                                                                    












                                                                                          
                                                                
                                                
                               




















                                                                               







                                                                                      
                                




                                                                                                                                             

                     
                  














































































































































































































                                                                                                         
                                 



































                                                                                                                                                              







































                                                                                          
                                 







                                             
                                 


























                                                                        
                                                       















































































                                                                                       



                                





                                                                    

                                                    
                                     
                                        



















































































































































                                                                                                                      
                                                                   































                                                                                



                                          







                             
                                 





























                                                                                          
                               













                                                                                                 

                           

































                                                                                            
                                                                                     

                                                                                                       
                                                                                 
















                                                                                                                      
                                                                    




                                                                                         



                                                                                                                                            
                                
         
 
/*
	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>
#include <sys/wait.h>

extern "C" {
#include "../twcommon.h"
}
#include "../minuitwrp/minui.h"
#include "gui.hpp"

#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;
			set_select_fd();
			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) {
			// assume child has died (usual errno when shell exits seems to be EIO == 5)
			if (errno != EIO)
				LOGERR("pty read failed: %d\n", errno);
			stop();
		}
		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) {
			LOGERR("pty write failed: %d\n", errno);
			// assume child has died
			stop();
		}
		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);
	}

	void stop()
	{
		if (!started()) {
			LOGERR("someone tried to stop pty, but it was not started\n");
			return;
		}
		close(fdMaster);
		g_pty_fd = fdMaster = -1;
		set_select_fd();
		int status;
		waitpid(pid, &status, WNOHANG); // avoid zombies but don't hang if the child is still alive and we got here due to some error
		pid = 0;
	}

private:
	int fdMaster;
	pid_t 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);
		linewrap = false;
		++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);
		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;
		linewrap = false;
		++updateCounter;
	}

	void setY(int y)
	{
		//y = min(height, max(y, 0));
		y = max(y, 0);
		cursorY = y;
		linewrap = false;
		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)
	{
		if (linewrap) {
			down();
			setX(0);
		}
		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(); // also bumps updateCounter

		if (cursorX >= width)
			linewrap = true;
	}

	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
	bool linewrap; // true to put next character into next line
	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 (!HasInputFocus)
		return 1;
	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_ttf_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_ttf_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_ttf_measureEx("N", mFont->GetResource());
	engine->setSize(mRenderW / charWidth, GetDisplayItemCount(), mRenderW, mRenderH);
}

void GUITerminal::SetPageFocus(int inFocus)
{
	if (inFocus && isConditionTrue()) {
		// TODO: grab focus correctly, this hack grabs focus and insists that the terminal be the focus regardless of other elements
		// It's highly unlikely that there will be any other visible input elements on the page anyway...
		SetInputFocus(1);
		InitAndResize();
	}
}