/*
Terminal
by John Ryland
Copyright (c) 2023
*/
////////////////////////////////////////////////////////////////////////////////////
// Terminal Emulator
#include "TerminalEmulator.h"
// C++
#include <chrono>
#include <cstdlib>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <array>
/*
VT100 and ANSI escape code references:
- https://en.wikipedia.org/wiki/ANSI_escape_code
- https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
- https://www.xfree86.org/current/ctlseqs.html
- https://www.ascii-code.com/
- https://vt100.net/docs/vt100-ug/chapter3.html
- https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Functions-using-CSI-_-ordered-by-the-final-character_s_
*/
/*
Things to fix:
- vim - unhandled character
- resize window to 0 height
- resize in vim height, can expand and it doesn't fill with new lines of text
- man command doesn't work as intended
- play ding
- select colors - color scheme
- character attributes (bold, italics etc)
- clipboard support
- osc - besides change title
- i18n input / test
- utf-8 output
- fix todos
- can't use enter on numpad
- abstract terminal emulation so not tied to ImGui directly in this file
*/
/*
Done:
- general scrolling of window
- improved resize handling like other terms
- background colors
- vim - no page up, page down
- vim - scroll window not working
- scrolling within apps like vim
- can't Ctrl-C
- neofetch doesn't detect term
*/
namespace details {
uint32_t Color(int r, int g, int b)
{
return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
}
template <typename T>
T Clamp(T val, T min, T max)
{
return std::min(std::max(val, min), max);
}
} // details namespace
namespace Terminal {
TerminalEmulator::TerminalEmulator()
{
strncpy(m_title, "Terminal", std::size(m_title));
for (int i = 0; i < MAX_SCROLL_BACK_LINES; ++i)
m_mainWindow[i] = nullptr;
for (int i = 0; i < MAX_SCROLL_BACK_LINES; ++i)
m_altWindow[i] = nullptr;
m_topMargin[0] = 0;
m_topMargin[1] = 0;
m_bottomMargin[0] = MAX_SCROLL_BACK_LINES - 1;
m_bottomMargin[1] = MAX_SCROLL_BACK_LINES - 1;
}
// virtual
TerminalEmulator::~TerminalEmulator()
{
//
}
enum class TerminalEmulator::CharAttrib : uint8_t
{
Normal = 0,
Bold = 1 << 0,
Italics = 1 << 1,
Underlined = 1 << 2,
Blink = 1 << 3,
FastBlink = 1 << 4,
Reverse = 1 << 5,
StrikeOut = 1 << 6
};
TerminalEmulator::CharAttrib operator|=(const TerminalEmulator::CharAttrib& a, const TerminalEmulator::CharAttrib& b)
{
return (TerminalEmulator::CharAttrib)((int)a | (int)b);
}
TerminalEmulator::CharAttrib operator~(const TerminalEmulator::CharAttrib& a)
{
return (TerminalEmulator::CharAttrib)(~(int)a);
}
TerminalEmulator::CharAttrib operator&=(const TerminalEmulator::CharAttrib& a, const TerminalEmulator::CharAttrib& b)
{
return (TerminalEmulator::CharAttrib)((int)a & (int)b);
}
// virtual
void TerminalEmulator::Initialize()
{
m_cursorX = 0;
m_cursorY = 0;
m_fontWidth = 6;
m_fontHeight = 12;
m_currentColumns = 0;
m_currentLines = 0;
m_currentAttributes = { 0, CharAttrib::Normal, 0, 0 };
m_windowMemory = (TerminalEmulator::Char*)malloc(MAX_SCROLL_BACK_LINES * MAX_LINE_COLUMNS * BUFFER_COUNT * sizeof(Char));
memset(m_windowMemory, 0, MAX_SCROLL_BACK_LINES * MAX_LINE_COLUMNS * BUFFER_COUNT * sizeof(Char));
for (int i = 0; i < MAX_SCROLL_BACK_LINES; ++i)
m_mainWindow[i] = &m_windowMemory[i * MAX_LINE_COLUMNS];
for (int i = 0; i < MAX_SCROLL_BACK_LINES; ++i)
m_altWindow[i] = &m_windowMemory[MAX_SCROLL_BACK_LINES * MAX_LINE_COLUMNS + i * MAX_LINE_COLUMNS];
m_textWindow = m_mainWindow;
BuildColorLookupTable();
m_decodeState1 = 0;
m_decodeState2 = 0;
m_paramCount = 0;
m_paramOffset = 0;
for (int i = 0; i < 10; ++i)
m_params[i][0] = 0;
m_process.Initialize();
}
// virtual
void TerminalEmulator::Shutdown()
{
m_process.Shutdown();
if (m_windowMemory)
free(m_windowMemory);
}
// virtual
void TerminalEmulator::Update()
{
if (!m_process.HasExited())
{
ReadChars();
HandleInput();
DrawCursor();
}
Display();
}
// virtual
void TerminalEmulator::HandleInput()
{
// A subclass needs to implement this to hook this up to a UI system
}
// virtual
void TerminalEmulator::DrawCursor()
{
// A subclass needs to implement this to hook this up to a UI system
}
// virtual
void TerminalEmulator::Display()
{
// A subclass needs to implement this to hook this up to a UI system
}
void TerminalEmulator::BuildColorLookupTable()
{
uint8_t retroColors[16][3] = {
{ 0, 0, 0 }, { 128, 0, 0 }, { 0,128, 0 }, { 128,128, 0 },
{ 0, 0,128 }, { 128, 0,128 }, { 0,128,128 }, { 192,192,192 },
{ 128,128,128 }, { 255, 0, 0 }, { 0,255, 0 }, { 255,255, 0 },
{ 0, 0,255 }, { 255, 0,255 }, { 0,255,255 }, { 255,255,255 }
};
uint8_t modernColors[16][3] = {
{ 1, 1, 1 }, { 222, 56, 43 }, { 57,181, 74 }, { 255,199, 6 },
{ 0,111,184 }, { 118, 38,113 }, { 44,181,233 }, { 204,204,204 },
{ 129,131,131 }, { 252, 57, 31 }, { 49,231, 34 }, { 234,236, 35 },
{ 88, 51,255 }, { 249, 53,248 }, { 20,240,240 }, { 233,235,235 }
};
auto colors = modernColors;
// 16 palette colors (8 dark and 8 bright)
for (int i = 0; i < 16; ++i)
m_colorLookupTable[i]= details::Color(colors[i][0], colors[i][1], colors[i][2]);
// 6x6x6 color cube
for (int i = 17; i < 232; ++i)
{
int col = i - 17;
int r = 3 + ((col / 36) % 6) * (255/6);
int g = 3 + ((col / 6) % 6) * (255/6);
int b = 3 + ((col / 1) % 6) * (255/6);
m_colorLookupTable[i]= details::Color(r, g, b);
}
// 24 greys (between black and white which are already included in the 16 color palette)
for (int i = 232; i < 256; ++i)
{
int col = 8 + (i - 232) * 10; // 256/24 = 10 rem 16. 16/2 = 8
m_colorLookupTable[i]= details::Color(col, col, col);
}
}
TerminalEmulator::Char* TerminalEmulator::Line(int line)
{
//line = details::Clamp(line, 0, m_currentLines - 1);
return m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + line];
}
void TerminalEmulator::NewLine()
{
m_cursorY++;
int top = m_currentLines + 1 - MAX_SCROLL_BACK_LINES; // ????+1?????
int bottom = m_currentLines;
if (m_altScreenBuffer)
{
top = m_topMargin[1];
bottom = m_bottomMargin[1];
}
if (m_cursorY >= bottom)
{
int line0 = MAX_SCROLL_BACK_LINES - m_currentLines + top - 1;
int lineN = MAX_SCROLL_BACK_LINES - m_currentLines + bottom - 1;
// scrolling
Char* firstLine = m_textWindow[line0];
for (int i = line0+1; i <= lineN; ++i)
m_textWindow[i-1] = m_textWindow[i];
m_textWindow[lineN] = firstLine;
memset(m_textWindow[lineN], 0, MAX_LINE_COLUMNS * sizeof(Char));
m_cursorY = bottom - 1;
}
m_needScrollToY = true;
}
void TerminalEmulator::SetCursorPosition(int x, int y)
{
m_needScrollToY = (y != m_cursorY);
m_cursorY = y;
// This clamping interfers with save/restore cursor and changing alt->main buffers
//m_cursorY = details::Clamp(m_cursorY, 0, m_currentLines - 1);
m_cursorX = x;
//m_cursorX = details::Clamp(m_cursorX, 0, m_currentColumns - 1);
}
void TerminalEmulator::SendChar(char ch)
{
m_process.SendBytes(&ch, 1);
}
void TerminalEmulator::PushChar(char ch)
{
if (m_cursorX < 0)
m_cursorX = 0;
if (m_cursorX >= m_currentColumns)
{
m_cursorX = 0;
NewLine();
}
if (m_cursorY < 0)
m_cursorY = 0;
Line(m_cursorY)[m_cursorX] = m_currentAttributes;
Line(m_cursorY)[m_cursorX].ch = ch;
m_cursorX++;
}
void TerminalEmulator::ResetDecoder()
{
m_paramCount = 0;
m_paramOffset = 0;
m_decodeState1 = 0;
}
int TerminalEmulator::GetParam(int defaultVal, int param)
{
if (param < m_paramCount || ((param == m_paramCount) && m_paramOffset))
return ::atoi(m_params[param]);
return defaultVal;
}
void TerminalEmulator::DecodeRawChar(char ch)
{
if (ch == 27) // ESC
m_decodeState1 = 1;
else if (ch == '\0')
; // 'clear' command sends this - TODO: perhaps clear the scroll-back contents?
else if (ch == '\t') // TAB
m_cursorX += 8 - (m_cursorX % 8); // 'ls' sends this to make output in to columns - TODO: fixed to multiples of 8, but is there a setting for that?
else if (ch == '\r') // LF - line feed
m_cursorX = 0;
else if (ch == '\n') // CR - carriage return
NewLine();
else if (ch == '\a') // BEL - bell / alert
/* TODO: code which plays a ding */; // ding
else if (ch == '\b') // BS - backspace
m_cursorX--;
else if ( ::isalnum(ch) || ::ispunct(ch) || ::isspace(ch) )
PushChar(ch); // regular char
else
printf("special ch: \\0%o\n", (int)(uint8_t)ch); // special char - TODO: I see 226, 128 and 144 as characters being emitted which aren't handled - this is UTF-8 encoding for '-'
// have also seen: /0302/0251 is (c) symbol in unicode
}
void TerminalEmulator::DecodeEscapeSequence(char ch)
{
if (ch == '[')
{
m_decodeState1 = 2;
return;
}
else if (ch == '7')
printf("mode 7\n"); // TODO: save cursor and attributes
else if (ch == '8')
printf("mode 8\n"); // TODO: restore cursor and attributes
else if (ch == '>')
printf("mode >\n"); // TODO: The auxiliary keypad keys will send ASCII codes (num lock on)
else if (ch == '<')
printf("mode <\n"); // TODO: 'reset' command sends this - enter ANSI mode
else if (ch == '=')
printf("mode =\n"); // TODO: The auxiliary keypad keys will transmit control sequences (num lock off)
else if (ch == 'M')
{
printf("mode M\n"); // TODO: reverse index ?? - man pages send this multiple times to scroll back up
// This is the reverse of \n
// This can scroll the viewport within the margins in opposite direction that \n does
}
else if (ch == 'P')
{
printf("mode DCS\n"); // DCS command - DCS = device control string
m_decodeState1 = 5; // TODO: start of sending device control string
return;
}
else if (ch == '\\')
printf("mode \\ <-- shouldn't see this\n"); // TODO: end of device control string
else if (ch == ']')
{
m_decodeState1 = 4; // operating system command - things like setting the titlebar title
return;
}
else if (ch == 'c')
printf("terminal reset\n"); // TODO: Full tty terminal reset
else
printf("unknown ch in state 1: %d %c\n", ch, ch);
ResetDecoder();
}
void TerminalEmulator::DecodeControlSequence(char ch)
{
if (ch == '\077')
{
m_decodeState1 = 3;
return;
}
else if (ch >= '0' && ch <= '9')
{
m_params[m_paramCount][m_paramOffset] = ch;
m_paramOffset++;
m_params[m_paramCount][m_paramOffset] = 0;
return;
}
else if (ch == ';')
{
m_paramCount++;
m_paramOffset = 0;
return;
}
else if (ch == 'J')
{
// clear
// 0 - from cursor to end, 1 - from start to cursor, 2- entire screen
int mode = GetParam(0, 0);
switch (mode)
{
case 0:
memset(&Line(m_cursorY)[m_cursorX], 0, (m_currentColumns - m_cursorX) * sizeof(Char));
for (int line = m_cursorY + 1; line < m_currentLines; ++line)
memset(Line(line), 0, m_currentColumns * sizeof(Char));
break;
case 1:
for (int line = 0; line < m_cursorY; ++line)
memset(Line(line), 0, m_currentColumns * sizeof(Char));
memset(&Line(m_cursorY)[0], 0, m_cursorX * sizeof(Char));
break;
case 2:
for (int line = 0; line < m_currentLines; ++line) // ???? <= ????
memset(Line(line), 0, m_currentColumns * sizeof(Char));
break;
default:
printf("unknown clear code: %d\n", mode);
break;
}
}
else if (ch == '@')
{
// shift n charactes right (fill with spaces)
int n = GetParam(1);
memmove(&Line(m_cursorY)[m_cursorX + n], &Line(m_cursorY)[m_cursorX], (MAX_LINE_COLUMNS - (m_cursorX + n)) * sizeof(Char));
for (int i = 0; i < n; ++i)
Line(m_cursorY)[m_cursorX + i].ch = ' ';
}
else if (ch == 'P')
{
// delete n charactes (shifting left and filling with spaces)
int n = GetParam(1);
memmove(&Line(m_cursorY)[m_cursorX], &Line(m_cursorY)[m_cursorX + n], (MAX_LINE_COLUMNS - (m_cursorX + n)) * sizeof(Char));
for (int i = 0; i < n; ++i)
Line(m_cursorY)[MAX_LINE_COLUMNS - 1 - i].ch = ' ';
}
else if (ch == 'X')
{
// erase n charactes with spaces
int n = GetParam(1);
for (int i = 0; i < n; ++i)
Line(m_cursorY)[m_cursorX].ch = ' ';
}
else if (ch == 'S')
{
// TODO: scroll up
int n = GetParam(1);
// between top and bottom margin
// seems not common
}
else if (ch == 'T')
{
// TODO: scroll down
int n = GetParam(1);
// between top and bottom margin
// seems not common
}
else if (ch == 'L')
{
// TODO: scroll inside vim - insert lines (when scrolling up)
// between top and bottom margin
// seems not common - seems to work for alt-buffer - not tested for main buffer
int n = GetParam(1);
for (int i = 0; i < n; ++i)
{
if (m_altScreenBuffer)
{
int bottom = m_bottomMargin[1];
int line0 = MAX_SCROLL_BACK_LINES - m_currentLines + m_topMargin[1] - 1;
int lineN = MAX_SCROLL_BACK_LINES - m_currentLines + bottom - 1;
// scrolling
Char* push = m_textWindow[lineN];
for (int i = lineN-1; i >= line0; --i)
m_textWindow[i+1] = m_textWindow[i];
m_textWindow[line0] = push;
memset(push, 0, MAX_LINE_COLUMNS * sizeof(Char));
}
else
{
//auto curLine = &m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n];
//auto nextLine = &m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n + 1];
//auto lastLine = &m_textWindow[MAX_SCROLL_BACK_LINES - 1];
auto push = m_textWindow[MAX_SCROLL_BACK_LINES - 1];
//for (int j = 0; j < (m_cursorY - n - 1); ++j)
memmove(&m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n + 1], &m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n], (m_cursorY - n - 1)*sizeof(Char*));
m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n] = push;
memset(push, 0, sizeof(Char) * MAX_LINE_COLUMNS);
}
}
ResetDecoder();
return;
}
else if (ch == 'M')
{
// TODO: scroll inside vim - delete lines (not called when scrolling down)
// between top and bottom margin
int n = GetParam(1);
for (int i = 0; i < n; ++i)
{
//auto curLine = &m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n];
//auto nextLine = &m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n + 1];
//auto lastLine = &m_textWindow[MAX_SCROLL_BACK_LINES - 1];
auto push = m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n];
for (int j = 0; j < (m_cursorY - n - 1); ++j)
m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n] = m_textWindow[MAX_SCROLL_BACK_LINES - 1 - m_currentLines + m_cursorY + n + 1];
m_textWindow[MAX_SCROLL_BACK_LINES - 1] = push;
memset(push, 0, sizeof(Char) * MAX_LINE_COLUMNS);
}
//ResetDecoder();
//return;
}
else if (ch == 'r')
{
// Scrolling Margins
int idx = m_altScreenBuffer ? 1 : 0; // Scrolling margins are per-buffer
m_topMargin[idx] = GetParam(1, 0) - 1; // set top and bottom margins - defines a scroll section
m_bottomMargin[idx] = GetParam(m_currentLines + 1, 1) - 1;
//ResetDecoder();
//return;
}
else if (ch == '>')
{
m_decodeState1 = 7;
return;
}
else if (ch == 'l')
{
// TODO: currently ignoring - not sure - might be error in the program
ResetDecoder();
return;
}
else if (ch == '%')
{
// TODO: currently ignoring - not sure - might be error in the program
return;
}
else if (ch == '!') // ESC[!p - soft reset
{
// TODO: currently ignoring - not sure - might be error in the program
return;
}
else if (ch == 't')
{
// icon, title save / restore etc - lots of sub-commands here - Window manipulation (XTWINOPS)
printf("win manip: %d,%d,%d\n", GetParam(0,0), GetParam(0,1), GetParam(0,2));
/*
Ps = 2 2 ; 0 ⇒ Save xterm icon and window title on stack.
Ps = 2 2 ; 1 ⇒ Save xterm icon title on stack.
Ps = 2 2 ; 2 ⇒ Save xterm window title on stack.
Ps = 2 3 ; 0 ⇒ Restore xterm icon and window title from stack.
Ps = 2 3 ; 1 ⇒ Restore xterm icon title from stack.
Ps = 2 3 ; 2 ⇒ Restore xterm window title from stack.
*/
}
else if (ch == 'n')
{
// report status
int mode = GetParam();
char tmpBuf[1024];
int chars = 0;
switch (mode)
{
case 6:
// report cursor position
chars = sprintf(tmpBuf, "\033[%d;%dR", m_cursorY + 1, m_cursorX + 1); // row,col CSI ? r ; c R
if (chars > 0)
m_process.SendBytes(tmpBuf, chars);
break;
default:
printf("unknown report status code: %d\n", mode);
break;
}
}
else if (ch == 'K')
{
// clear line
// 0 - cur to end, 1 - start to cur, 2 - all of line
//m_cursorX = details::Clamp(m_cursorX, 0, m_currentColumns - 1);
//m_cursorY = details::Clamp(m_cursorY, 0, m_currentLines - 1);
int mode = GetParam(0, 0);
switch (mode)
{
case 0:
memset(&Line(m_cursorY)[m_cursorX], 0, (m_currentColumns - m_cursorX) * sizeof(Char));
break;
case 1:
memset(Line(m_cursorY), 0, m_cursorX * sizeof(Char));
break;
case 2:
memset(Line(m_cursorY), 0, m_currentColumns * sizeof(Char));
break;
default:
printf("unknown clear code: %d\n", mode);
break;
}
}
else if (ch == 'A')
SetCursorPosition(m_cursorX, m_cursorY - GetParam()); // Move cursor up
else if (ch == 'B')
SetCursorPosition(m_cursorX, m_cursorY + GetParam()); // Move cursor down
else if (ch == 'C')
SetCursorPosition(m_cursorX + GetParam(), m_cursorY); // Move cursor right
else if (ch == 'D')
SetCursorPosition(m_cursorX - GetParam(), m_cursorY); // Move cursor left
else if (ch == 'H')
SetCursorPosition(GetParam(1, 1) - 1, GetParam(1, 0) - 1); // Move cursor absolute position
else if (ch == 'm')
{
for (int i = 0; i <= m_paramCount; ++i)
{
int fmtParam = GetParam(0, i);
switch (fmtParam)
{
case 0:
m_currentAttributes.attrib = CharAttrib::Normal; // does it clear colors too?
m_currentAttributes.foregroundColor = 0; // 0 means default
m_currentAttributes.backgroundColor = 0;
break;
case 1: m_currentAttributes.attrib |= CharAttrib::Bold; break;
case 3: m_currentAttributes.attrib |= CharAttrib::Italics; break;
case 4: m_currentAttributes.attrib |= CharAttrib::Underlined; break;
case 5: m_currentAttributes.attrib |= CharAttrib::Blink; break;
case 7: m_currentAttributes.attrib |= CharAttrib::Reverse; break;
case 9: m_currentAttributes.attrib |= CharAttrib::StrikeOut; break;
case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: break; // TODO: Alternative fonts, fonts 0-9
case 22: m_currentAttributes.attrib &= ~CharAttrib::Bold; break;
case 23: m_currentAttributes.attrib &= ~CharAttrib::Italics; break;
case 24: m_currentAttributes.attrib &= ~CharAttrib::Underlined; break;
case 25: m_currentAttributes.attrib &= ~CharAttrib::Blink; break;
case 27: m_currentAttributes.attrib &= ~CharAttrib::Reverse; break;
case 29: m_currentAttributes.attrib &= ~CharAttrib::StrikeOut; break;
case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37:
m_currentAttributes.foregroundColor = m_colorLookupTable[fmtParam - 30]; break;
case 38:
if (GetParam(0, i+1) == 5)
{
m_currentAttributes.foregroundColor = m_colorLookupTable[GetParam(0, i+2)];
i += 2;
}
else if (GetParam(0, i+1) == 2)
{
m_currentAttributes.foregroundColor = (GetParam(0, i+2) << 0) | (GetParam(0, i+3) << 8) | (GetParam(0, i+4) << 16) | 0xFF000000;
i += 4;
}
break;
case 39:
m_currentAttributes.foregroundColor = 0; break;
case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47:
m_currentAttributes.backgroundColor = m_colorLookupTable[fmtParam - 40]; break;
case 48:
if (GetParam(0, i+1) == 5)
{
m_currentAttributes.backgroundColor = m_colorLookupTable[GetParam(0, i+2)];
i += 2;
}
else if (GetParam(0, i+1) == 2)
{
m_currentAttributes.backgroundColor = (GetParam(0, i+2) << 0) | (GetParam(0, i+3) << 8) | (GetParam(0, i+4) << 16) | 0xFF000000;
i += 4;
}
break;
case 49:
m_currentAttributes.backgroundColor = 0; break;
case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97:
m_currentAttributes.foregroundColor = m_colorLookupTable[8 + fmtParam - 90]; break;
case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107:
m_currentAttributes.backgroundColor = m_colorLookupTable[8 + fmtParam - 90]; break;
default:
printf("unhandled attrib case\n");
break;
}
}
}
else
printf("unknown ch in state 2: %d %c\n", ch, (ch==27)?'E':ch);
m_cursorX = details::Clamp(m_cursorX, 0, m_currentColumns - 1);
m_cursorY = details::Clamp(m_cursorY, 0, m_currentLines - 1);
ResetDecoder();
}
void TerminalEmulator::DecodeEscapeSpecial(char ch)
{
if (ch >= '0' && ch <= '9')
{
m_params[m_paramCount][m_paramOffset] = ch;
m_paramOffset++;
m_params[m_paramCount][m_paramOffset] = 0;
return;
}
else if (ch == 'h')
{
m_paramCount++;
switch (GetParam())
{
case 1: m_appArrowKeys = true; break;
case 7: m_wrap = true; break;
case 12: m_blinkCursor = true; break;
case 25: m_showCursor = true; break;
case 47: m_saveScreen = true; break;
// 69
case 1004: m_reportFocus = true; break;
case 1049:
{
if (!m_altScreenBuffer)
{
m_topMargin[1] = 0; // set top and bottom margins to defaults for alt screen
m_bottomMargin[1] = m_currentLines;// - 1;
m_savedCursorX = m_cursorX;
m_savedCursorY = m_cursorY;
m_cursorX = 0;
m_cursorY = 0;//m_currentLines - 1;
m_altScreenBuffer = true;
m_textWindow = m_altWindow;
UpdateSize(m_viewWidth, m_viewHeight);
}
break;
}
case 2004: m_bracketedPaste = true; break;
default: printf("cmd on: %s\n", m_params[0]); break;
}
}
else if (ch == 'l')
{
m_paramCount++;
switch (GetParam())
{
case 1: m_appArrowKeys = false; break;
case 7: m_wrap = false; break;
case 12: m_blinkCursor = false; break;
case 25: m_showCursor = false; break;
case 47: m_saveScreen = false; break;
// 69
case 1004: m_reportFocus = false; break;
case 1049:
{
if (m_altScreenBuffer)
{
SetCursorPosition(m_savedCursorX, m_savedCursorY);
m_altScreenBuffer = false;
m_textWindow = m_mainWindow;
UpdateSize(m_viewWidth, m_viewHeight);
}
break;
}
case 2004: m_bracketedPaste = false; break;
default: printf("cmd off: %s\n", m_params[0]); break;
}
}
else
printf("unknown special state: %s\n", m_params[0]);
ResetDecoder();
}
void TerminalEmulator::DecodeCommandSequence(char ch)
{
if (ch >= '0' && ch <= '9')
{
m_params[m_paramCount][m_paramOffset] = ch;
m_paramOffset++;
m_params[m_paramCount][m_paramOffset] = 0;
return;
}
else if (ch == ';')
{
m_paramCount++;
m_paramOffset = 0;
m_osc = GetParam();
m_decodeState1 = 8;
return;
}
ResetDecoder();
}
void TerminalEmulator::DecodeChar(char ch)
{
// An aide to debugging by keeping a history in a circular buffer of last 256 characters
m_charHistory[m_charHistoryIdx] = ch;
++m_charHistoryIdx;
if (m_decodeState1 == 0)
DecodeRawChar(ch);
else if (m_decodeState1 == 1)
DecodeEscapeSequence(ch);
else if (m_decodeState1 == 2)
DecodeControlSequence(ch);
else if (m_decodeState1 == 3)
DecodeEscapeSpecial(ch);
else if (m_decodeState1 == 4)
DecodeCommandSequence(ch);
else if (m_decodeState1 == 5)
{
if (ch == 27)
m_decodeState1 = 6;
else
PushChar(ch);
}
else if (m_decodeState1 == 6)
{
if (ch == '\\')
ResetDecoder(); // End of DCS
else
{
if (ch != 27)
m_decodeState1 = 5;
PushChar(27);
PushChar(ch);
}
}
else if (m_decodeState1 == 7)
{
static int st = 0;
if (ch >= '0' && ch <= '9')
{
st = ch - '0';
}
else if (ch == ';')
{
st = ch - '0';
}
else if (ch == 't')
{
printf("title mode: %d\n", st);
ResetDecoder(); // End
}
else if (ch == 'm')
{
printf("reset keyboard modifiers: %d\n", st);
ResetDecoder(); // End
}
else if (ch == 'c')
{
printf("send device attributes: %d\n", st);
ResetDecoder(); // End
}
else
{
ResetDecoder(); // End - unknown
}
}
else if (m_decodeState1 == 8)
{
if (ch == '\a') // End of OSC
{
if (m_osc == 0)
memcpy(m_title, m_oscText, std::min(std::size(m_title), std::size(m_oscText)));
m_oscText[0] = 0;
m_oscTextLen = 0;
ResetDecoder();
}
else
{
static_assert((sizeof(m_oscText)/sizeof(m_oscText[0])) >= 1);
if (m_oscTextLen < (std::size(m_oscText) - 1))
{
m_oscText[m_oscTextLen] = ch;
++m_oscTextLen;
m_oscText[m_oscTextLen] = 0;
}
}
}
else
{
printf("back decoder state\n");
ResetDecoder();
}
}
void TerminalEmulator::ReadChars()
{
for (int i = 0; i < 10000; ++i)
{
uint8_t* buffer;
uint32_t bytesAvailable;
if (!m_process.GetAvailableData(buffer, bytesAvailable))
break;
for (int x = 0; x < bytesAvailable; ++x)
DecodeChar(buffer[x]);
}
}
void TerminalEmulator::UpdateSize(int availableX, int availableY)
{
m_viewWidth = availableX;
m_viewHeight = availableY;
if (!m_altScreenBuffer)
availableY = MAX_SCROLL_BACK_LINES * m_fontHeight;
availableY = details::Clamp(availableY, m_fontHeight, (MAX_SCROLL_BACK_LINES - 1) * m_fontHeight);
int cols = (availableX / m_fontWidth) - (m_leftMargin + m_rightMargin);
int lines = (availableY /* + m_fontHeight */) / m_fontHeight;
if (cols != m_currentColumns || lines != m_currentLines)
{
//m_cursorY += lines - m_currentLines;
m_currentColumns = cols;
m_currentLines = lines;
m_process.UpdateSize(lines, cols);
}
}
} // Terminal namespace