gui: add terminal emulator
Emulates enough of a VT-100 to run busybox vi. Change-Id: I99c829c6c9de2246194ecb8b8b3cdf4ac34a0606
This commit is contained in:
@@ -29,6 +29,7 @@ LOCAL_SRC_FILES := \
|
||||
scrolllist.cpp \
|
||||
patternpassword.cpp \
|
||||
textbox.cpp \
|
||||
terminal.cpp \
|
||||
twmsg.cpp
|
||||
|
||||
ifneq ($(TWRP_CUSTOM_KEYBOARD),)
|
||||
|
||||
14
gui/gui.cpp
14
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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -372,6 +372,14 @@ bool Page::ProcessNode(xml_node<>* page, std::vector<xml_node<>*> *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);
|
||||
|
||||
888
gui/terminal.cpp
Normal file
888
gui/terminal.cpp
Normal file
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user