Newer
Older
GameEngine / src / Terminal / TerminalEmulator.cpp
@John Ryland John Ryland on 22 Aug 30 KB save more of the WIP
/*
	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