From 7e999db4aeb6c76d5146c59f406e62bbbcdeacea Mon Sep 17 00:00:00 2001 From: Logan Joy-Koer Date: Fri, 19 Jun 2026 00:22:56 -0700 Subject: [PATCH] I asked claude to completely replace my ansi parser attempt since it knows how to make an ansi parser and I don't everything hardware specific is still my own code, as well as the actual renderer --- src/main.c | 285 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 269 insertions(+), 16 deletions(-) diff --git a/src/main.c b/src/main.c index cba010f..3f2b2ed 100644 --- a/src/main.c +++ b/src/main.c @@ -96,6 +96,53 @@ uint8_t colour_gray = 0b1000; uint8_t keyboardState = 0x00; +/* --------------------------------------------------------------------- + * ANSI / VT100 escape sequence parser state + * + * Supports a practical subset of CSI ("ESC [ ... letter") sequences: + * CUU/CUD/CUF/CUB - cursor up/down/forward/back (A/B/C/D) + * CUP/HVP - absolute cursor position (H/f) + * ED - erase in display, modes 0/1/2 (J) + * EL - erase in line, modes 0/1/2 (K) + * SGR - colours + bold/reverse, see below (m) + * + * SGR uses the existing 4-bit (RGBI) palette: bit0=red, bit1=green, + * bit2=blue, bit3=intensity ("gray"/bold bit). Standard ANSI codes + * 30-37/40-47 map straight onto that 3-bit RGB value, and 90-97/100-107 + * add the intensity bit for the "bright" variants. 256-colour / truecolour + * sequences (38;5;n / 38;2;r;g;b) aren't supported by a 16-colour display, + * so they're parsed-but-ignored rather than crashing or printing garbage. + * + * Anything else we don't recognise (DEC private modes via "ESC[?", OSC + * strings like "ESC]0;title BEL" used for window titles, single-byte + * escapes like ESC c) is consumed and discarded rather than dumped to + * the screen as literal characters. + * --------------------------------------------------------------------- */ + +typedef enum { + TERM_NORMAL, + TERM_ESC, + TERM_CSI, + TERM_OSC +} term_state_t; + +term_state_t termState = TERM_NORMAL; + +#define MAX_CSI_PARAMS 8 +int csiParams[MAX_CSI_PARAMS]; +uint8_t csiParamCount = 0; +bool csiPrivate = false; + +uint8_t baseFgColour = 0x0F; /* default: full white text */ +uint8_t baseBgColour = 0x00; /* default: black background */ +bool reverseVideo = false; + +/* Tracks whether the previous byte was a CR, so a CRLF pair only advances one line */ +bool sawCR = false; + +/* Set by Ctrl+V (0x16): the next byte is drawn as a raw glyph, bypassing all parsing */ +bool literalNext = false; + int scrollAmount = 0; bool newTiles = true; @@ -172,35 +219,242 @@ int end() { return exit_Code; } +/* Current colour byte (high nibble = fg, low nibble = bg), honouring SGR reverse video */ +uint8_t colourByte(void) { + uint8_t fg = reverseVideo ? baseBgColour : baseFgColour; + uint8_t bg = reverseVideo ? baseFgColour : baseBgColour; + return (uint8_t)((fg << 4) | bg); +} + +/* Blank out renderGoal tiles [startIdx, endIdx] (inclusive) using the current colour */ +void eraseRange(int startIdx, int endIdx) { + uint16_t blank = (uint16_t)((0x20 << 8) | colourByte()); + if (startIdx < 0) startIdx = 0; + if (endIdx > 1199) endIdx = 1199; + for (int i = startIdx; i <= endIdx; i++) { + renderGoal[i] = blank; + } + newTiles = true; +} + +/* Execute a fully-parsed CSI sequence ("ESC [ params finalByte") */ +void handleEscapeCommand(char finalByte) { + int p0 = csiParams[0]; + int n = (p0 == 0) ? 1 : p0; + int tmp; + + switch (finalByte) { + case 'A': /* CUU - cursor up */ + tmp = (int)cursorY - n; + cursorY = (uint8_t)(tmp < 0 ? 0 : tmp); + break; + + case 'B': /* CUD - cursor down */ + tmp = (int)cursorY + n; + cursorY = (uint8_t)(tmp > 25 ? 25 : tmp); + break; + + case 'C': /* CUF - cursor forward */ + tmp = (int)cursorX + n; + cursorX = (uint8_t)(tmp > 39 ? 39 : tmp); + break; + + case 'D': /* CUB - cursor back */ + tmp = (int)cursorX - n; + cursorX = (uint8_t)(tmp < 0 ? 0 : tmp); + break; + + case 'H': /* CUP */ + case 'f': { /* HVP - treated the same here */ + int row = csiParams[0]; + int col = csiParams[1]; + if (row <= 0) row = 1; + if (col <= 0) col = 1; + row--; + col--; + if (row > 25) row = 25; + if (col > 39) col = 39; + cursorY = (uint8_t)row; + cursorX = (uint8_t)col; + break; + } + + case 'J': { /* ED - erase in display */ + int mode = csiParams[0]; + int cur = (40 * (cursorY + 2)) + cursorX; + int scrStart = 80; + int scrEnd = 1119; + if (mode == 1) { + eraseRange(scrStart, cur); + } else if (mode == 2 || mode == 3) { + eraseRange(scrStart, scrEnd); + } else { + eraseRange(cur, scrEnd); + } + break; + } + + case 'K': { /* EL - erase in line */ + int mode = csiParams[0]; + int rowStart = 40 * (cursorY + 2); + int rowEnd = rowStart + 39; + int cur = rowStart + cursorX; + if (mode == 1) { + eraseRange(rowStart, cur); + } else if (mode == 2) { + eraseRange(rowStart, rowEnd); + } else { + eraseRange(cur, rowEnd); + } + break; + } + + case 'm': { /* SGR - colours / attributes */ + int count = csiParamCount + 1; + for (int i = 0; i < count; i++) { + int code = csiParams[i]; + if (code == 0) { + baseFgColour = (uint8_t)(colour_red | colour_green | colour_blue | colour_gray); + baseBgColour = 0x00; + reverseVideo = false; + } else if (code == 1) { + baseFgColour |= colour_gray; + } else if (code == 2 || code == 22) { + baseFgColour &= (uint8_t)~colour_gray; + } else if (code == 7) { + reverseVideo = true; + } else if (code == 27) { + reverseVideo = false; + } else if (code >= 30 && code <= 37) { + baseFgColour = (uint8_t)((baseFgColour & colour_gray) | (code - 30)); + } else if (code == 39) { + baseFgColour = (uint8_t)(colour_red | colour_green | colour_blue | colour_gray); + } else if (code >= 40 && code <= 47) { + baseBgColour = (uint8_t)((baseBgColour & colour_gray) | (code - 40)); + } else if (code == 49) { + baseBgColour = 0x00; + } else if (code >= 90 && code <= 97) { + baseFgColour = (uint8_t)(colour_gray | (code - 90)); + } else if (code >= 100 && code <= 107) { + baseBgColour = (uint8_t)(colour_gray | (code - 100)); + } + /* anything else (italics, underline, 256-colour, etc.) is silently ignored */ + } + break; + } + + default: + /* Unimplemented CSI command - ignore it */ + break; + } +} + bool step() { usb_HandleEvents(); if(has_srl_device) { char in_buf[1024]; /* Read up to 1024 bytes from the serial buffer */ size_t bytes_read = srl_Read(&srl, in_buf, sizeof in_buf); - idx = 0; - char symbol; - char escape_code[16]; - bool bs = false; - /* Check for an error (e.g. device disconneced) */ - if(bytes_read < 0) { - dbg_printf("error %d on srl_Read\n", bytes_read); - has_srl_device = false; - } else if(bytes_read > 0) { - while (idx < bytes_read) { - symbol = in_buf[idx]; + if (bytes_read > 0) { + for (idx = 0; idx < bytes_read; idx++) { + char symbol = in_buf[idx]; + + /* --- Ctrl+V literal-next: bypass all parsing for exactly one byte --- */ + if (literalNext) { + literalNext = false; + if (cursorX >= 40) { + cursorX = 0; + cursorY += 1; + } + if (cursorY > 25) { + scroll(); + } + renderGoal[(40 * (cursorY+2)) + cursorX] = (uint16_t)((symbol << 8) | colourByte()); + cursorX += 1; + continue; + } + + /* --- escape sequence state machine --- */ + if (termState == TERM_ESC) { + if (symbol == '[') { + termState = TERM_CSI; + csiParamCount = 0; + memset(csiParams, 0, sizeof csiParams); + csiPrivate = false; + } else if (symbol == ']') { + /* OSC string (e.g. window title) - swallow until terminator */ + termState = TERM_OSC; + } else { + /* Unimplemented single-byte escape (ESC c, ESC 7, etc.) - discard */ + termState = TERM_NORMAL; + } + continue; + } + + if (termState == TERM_OSC) { + if (symbol == 0x07 || symbol == 0x1B) { /* BEL or ESC terminates OSC */ + termState = TERM_NORMAL; + } + continue; + } + + if (termState == TERM_CSI) { + if (symbol >= '0' && symbol <= '9') { + if (csiParamCount < MAX_CSI_PARAMS) { + int v = (csiParams[csiParamCount] * 10) + (symbol - '0'); + csiParams[csiParamCount] = (v > 9999) ? 9999 : v; + } + } else if (symbol == ';') { + if (csiParamCount < MAX_CSI_PARAMS - 1) csiParamCount++; + } else if (symbol == '?' || symbol == '>' || symbol == '=') { + /* DEC private mode marker - parse but don't act on it */ + csiPrivate = true; + } else if (symbol >= 0x40 && symbol <= 0x7E) { + if (!csiPrivate) { + handleEscapeCommand(symbol); + } + termState = TERM_NORMAL; + } else { + /* Stray byte we don't expect inside a CSI sequence - bail out */ + termState = TERM_NORMAL; + } + continue; + } + + if (symbol == 0x1B) { /* ESC */ + termState = TERM_ESC; + continue; + } + + if (symbol == 0x16) { /* Ctrl+V - next byte is drawn literally */ + literalNext = true; + continue; + } + + /* --- plain character / legacy control-character handling --- */ int increment_X = 1; - if (symbol == 0x27) {}; + bool bs = false; + bool wasPrevCR = sawCR; + sawCR = false; if (symbol == 0x7F) { symbol = 0x20; cursorX -= (cursorX>0); increment_X = 0; } - if (symbol == 0x0D) { + if (symbol == 0x0D) { /* CR - also treated as a full newline on its own */ cursorX = 0; cursorY += 1; symbol = 0x20; increment_X = 0; + sawCR = true; + } + if (symbol == 0x0A) { /* LF - next line; skip the move if a CR just did it */ + if (!wasPrevCR) { + cursorX = 0; + cursorY += 1; + } + symbol = 0x20; + increment_X = 0; } if (symbol == 0x05) { srl_Write(&srl, "TerminalCE", 10); @@ -221,11 +475,10 @@ bool step() { if (cursorY > 25) { scroll(); } - renderGoal[(40 * (cursorY+2)) + cursorX] = ((symbol << 8)|0xF0); + renderGoal[(40 * (cursorY+2)) + cursorX] = (uint16_t)((symbol << 8) | colourByte()); if (!bs) { cursorX += increment_X; } - idx += 1; } newTiles = true; } @@ -350,4 +603,4 @@ int main() { cycleCounter++; } return end(); -} +} \ No newline at end of file