// widgetAnsiTerm.c — ANSI BBS terminal emulator widget #include "widgetInternal.h" // ============================================================ // Constants // ============================================================ #define ANSI_BORDER 2 #define ANSI_MAX_PARAMS 8 #define ANSI_SB_W 14 #define ANSI_DEFAULT_SCROLLBACK 500 #define PARSE_NORMAL 0 #define PARSE_ESC 1 #define PARSE_CSI 2 // Default attribute: light gray on black #define ANSI_DEFAULT_ATTR 0x07 // ============================================================ // CGA palette (RGB values for 16 standard colors) // ============================================================ static const uint8_t sCgaPalette[16][3] = { { 0, 0, 0}, // 0: black { 0, 0, 170}, // 1: blue { 0, 170, 0}, // 2: green { 0, 170, 170}, // 3: cyan {170, 0, 0}, // 4: red {170, 0, 170}, // 5: magenta {170, 85, 0}, // 6: brown {170, 170, 170}, // 7: light gray { 85, 85, 85}, // 8: dark gray { 85, 85, 255}, // 9: bright blue { 85, 255, 85}, // 10: bright green { 85, 255, 255}, // 11: bright cyan {255, 85, 85}, // 12: bright red {255, 85, 255}, // 13: bright magenta {255, 255, 85}, // 14: bright yellow {255, 255, 255}, // 15: bright white }; // ANSI SGR color index to CGA color index static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 }; // ============================================================ // Prototypes // ============================================================ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow); static void ansiTermDeleteLines(WidgetT *w, int32_t count); static void ansiTermDirtyAll(WidgetT *w); static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); static void ansiTermDirtyRow(WidgetT *w, int32_t row); static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd); static void ansiTermEraseDisplay(WidgetT *w, int32_t mode); static void ansiTermEraseLine(WidgetT *w, int32_t mode); static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count); static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex); static void ansiTermInsertLines(WidgetT *w, int32_t count); static void ansiTermNewline(WidgetT *w); static void ansiTermProcessByte(WidgetT *w, uint8_t ch); static void ansiTermProcessSgr(WidgetT *w); static void ansiTermPutChar(WidgetT *w, uint8_t ch); static void ansiTermScrollDown(WidgetT *w); static void ansiTermScrollUp(WidgetT *w); // ============================================================ // ansiTermAddToScrollback // ============================================================ // // Copy a screen row into the scrollback circular buffer. static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { if (!w->as.ansiTerm.scrollback || w->as.ansiTerm.scrollbackMax <= 0) { return; } int32_t cols = w->as.ansiTerm.cols; int32_t bytesPerRow = cols * 2; int32_t head = w->as.ansiTerm.scrollbackHead; memcpy(w->as.ansiTerm.scrollback + head * bytesPerRow, w->as.ansiTerm.cells + screenRow * bytesPerRow, bytesPerRow); w->as.ansiTerm.scrollbackHead = (head + 1) % w->as.ansiTerm.scrollbackMax; if (w->as.ansiTerm.scrollbackCount < w->as.ansiTerm.scrollbackMax) { w->as.ansiTerm.scrollbackCount++; } } // ============================================================ // ansiTermDirtyAll // ============================================================ static void ansiTermDirtyAll(WidgetT *w) { w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; } // ============================================================ // ansiTermDirtyRange // ============================================================ // // Mark rows dirty that are touched by a cell range [startCell, startCell+count). static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { int32_t cols = w->as.ansiTerm.cols; int32_t startRow = startCell / cols; int32_t endRow = (startCell + count - 1) / cols; for (int32_t r = startRow; r <= endRow && r < 32; r++) { w->as.ansiTerm.dirtyRows |= (1U << r); } } // ============================================================ // ansiTermDirtyRow // ============================================================ static void ansiTermDirtyRow(WidgetT *w, int32_t row) { if (row >= 0 && row < 32) { w->as.ansiTerm.dirtyRows |= (1U << row); } } // ============================================================ // ansiTermDeleteLines // ============================================================ static void ansiTermDeleteLines(WidgetT *w, int32_t count) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t row = w->as.ansiTerm.cursorRow; if (count > rows - row) { count = rows - row; } if (count <= 0) { return; } uint8_t *cells = w->as.ansiTerm.cells; // Shift lines up int32_t bytesPerRow = cols * 2; memmove(cells + row * bytesPerRow, cells + (row + count) * bytesPerRow, (rows - row - count) * bytesPerRow); // Clear the bottom lines for (int32_t r = rows - count; r < rows; r++) { ansiTermFillCells(w, r * cols, cols); } // All rows from cursorRow down are affected for (int32_t r = row; r < rows; r++) { ansiTermDirtyRow(w, r); } } // ============================================================ // ansiTermDispatchCsi // ============================================================ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { int32_t *p = w->as.ansiTerm.params; int32_t n = w->as.ansiTerm.paramCount; // DEC private modes (ESC[?...) if (w->as.ansiTerm.csiPrivate) { int32_t mode = (n >= 1) ? p[0] : 0; if (cmd == 'h') { if (mode == 7) { w->as.ansiTerm.wrapMode = true; } if (mode == 25) { w->as.ansiTerm.cursorVisible = true; } } else if (cmd == 'l') { if (mode == 7) { w->as.ansiTerm.wrapMode = false; } if (mode == 25) { w->as.ansiTerm.cursorVisible = false; } } return; } switch (cmd) { case 'A': // CUU - cursor up { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; w->as.ansiTerm.cursorRow -= count; if (w->as.ansiTerm.cursorRow < 0) { w->as.ansiTerm.cursorRow = 0; } break; } case 'B': // CUD - cursor down { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; w->as.ansiTerm.cursorRow += count; if (w->as.ansiTerm.cursorRow >= w->as.ansiTerm.rows) { w->as.ansiTerm.cursorRow = w->as.ansiTerm.rows - 1; } break; } case 'C': // CUF - cursor forward { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; w->as.ansiTerm.cursorCol += count; if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) { w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1; } break; } case 'D': // CUB - cursor back { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; w->as.ansiTerm.cursorCol -= count; if (w->as.ansiTerm.cursorCol < 0) { w->as.ansiTerm.cursorCol = 0; } break; } case 'H': // CUP - cursor position case 'f': // HVP - same { int32_t row = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t col = (n >= 2 && p[1]) ? p[1] - 1 : 0; if (row < 0) { row = 0; } if (row >= w->as.ansiTerm.rows) { row = w->as.ansiTerm.rows - 1; } if (col < 0) { col = 0; } if (col >= w->as.ansiTerm.cols) { col = w->as.ansiTerm.cols - 1; } w->as.ansiTerm.cursorRow = row; w->as.ansiTerm.cursorCol = col; break; } case 'J': // ED - erase display { int32_t mode = (n >= 1) ? p[0] : 0; ansiTermEraseDisplay(w, mode); break; } case 'K': // EL - erase line { int32_t mode = (n >= 1) ? p[0] : 0; ansiTermEraseLine(w, mode); break; } case 'L': // IL - insert lines { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; ansiTermInsertLines(w, count); break; } case 'M': // DL - delete lines { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; ansiTermDeleteLines(w, count); break; } case 'S': // SU - scroll up { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; for (int32_t i = 0; i < count; i++) { ansiTermScrollUp(w); } break; } case 'T': // SD - scroll down { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; for (int32_t i = 0; i < count; i++) { ansiTermScrollDown(w); } break; } case 'm': // SGR - select graphic rendition ansiTermProcessSgr(w); break; case 's': // SCP - save cursor position w->as.ansiTerm.savedRow = w->as.ansiTerm.cursorRow; w->as.ansiTerm.savedCol = w->as.ansiTerm.cursorCol; break; case 'u': // RCP - restore cursor position w->as.ansiTerm.cursorRow = w->as.ansiTerm.savedRow; w->as.ansiTerm.cursorCol = w->as.ansiTerm.savedCol; break; default: break; } } // ============================================================ // ansiTermEraseDisplay // ============================================================ static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t cur = w->as.ansiTerm.cursorRow * cols + w->as.ansiTerm.cursorCol; if (mode == 0) { // Erase from cursor to end of screen ansiTermFillCells(w, cur, cols * rows - cur); } else if (mode == 1) { // Erase from start to cursor ansiTermFillCells(w, 0, cur + 1); } else if (mode == 2) { // Erase entire screen — push all lines to scrollback first bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } if (wasAtBottom) { w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; } ansiTermFillCells(w, 0, cols * rows); } } // ============================================================ // ansiTermEraseLine // ============================================================ static void ansiTermEraseLine(WidgetT *w, int32_t mode) { int32_t cols = w->as.ansiTerm.cols; int32_t row = w->as.ansiTerm.cursorRow; int32_t col = w->as.ansiTerm.cursorCol; int32_t base = row * cols; if (mode == 0) { // Erase from cursor to end of line ansiTermFillCells(w, base + col, cols - col); } else if (mode == 1) { // Erase from start of line to cursor ansiTermFillCells(w, base, col + 1); } else if (mode == 2) { // Erase entire line ansiTermFillCells(w, base, cols); } } // ============================================================ // ansiTermFillCells // ============================================================ // // Fill a range of cells with space + current attribute. static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { uint8_t *cells = w->as.ansiTerm.cells; uint8_t attr = w->as.ansiTerm.curAttr; int32_t total = w->as.ansiTerm.cols * w->as.ansiTerm.rows; if (start < 0) { count += start; start = 0; } if (start + count > total) { count = total - start; } for (int32_t i = 0; i < count; i++) { cells[(start + i) * 2] = ' '; cells[(start + i) * 2 + 1] = attr; } ansiTermDirtyRange(w, start, count); } // ============================================================ // ansiTermGetLine // ============================================================ // // Get a pointer to the cell data for a given line index in the // combined scrollback+screen view. // lineIndex < scrollbackCount → scrollback line // lineIndex >= scrollbackCount → screen line static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { int32_t cols = w->as.ansiTerm.cols; int32_t bytesPerRow = cols * 2; int32_t sbCount = w->as.ansiTerm.scrollbackCount; if (lineIndex < sbCount) { // Scrollback line (circular buffer) int32_t sbMax = w->as.ansiTerm.scrollbackMax; int32_t actual = (w->as.ansiTerm.scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; return w->as.ansiTerm.scrollback + actual * bytesPerRow; } // Screen line int32_t screenRow = lineIndex - sbCount; return w->as.ansiTerm.cells + screenRow * bytesPerRow; } // ============================================================ // ansiTermInsertLines // ============================================================ static void ansiTermInsertLines(WidgetT *w, int32_t count) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t row = w->as.ansiTerm.cursorRow; if (count > rows - row) { count = rows - row; } if (count <= 0) { return; } uint8_t *cells = w->as.ansiTerm.cells; // Shift lines down int32_t bytesPerRow = cols * 2; memmove(cells + (row + count) * bytesPerRow, cells + row * bytesPerRow, (rows - row - count) * bytesPerRow); // Clear the inserted lines for (int32_t r = row; r < row + count; r++) { ansiTermFillCells(w, r * cols, cols); } // All rows from cursorRow down are affected for (int32_t r = row; r < rows; r++) { ansiTermDirtyRow(w, r); } } // ============================================================ // ansiTermNewline // ============================================================ // // Move cursor to next line, scrolling if at the bottom. static void ansiTermNewline(WidgetT *w) { w->as.ansiTerm.cursorRow++; if (w->as.ansiTerm.cursorRow >= w->as.ansiTerm.rows) { w->as.ansiTerm.cursorRow = w->as.ansiTerm.rows - 1; ansiTermScrollUp(w); } } // ============================================================ // ansiTermProcessByte // ============================================================ // // Feed one byte through the ANSI parser state machine. static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { switch (w->as.ansiTerm.parseState) { case PARSE_NORMAL: if (ch == 0x1B) { w->as.ansiTerm.parseState = PARSE_ESC; } else if (ch == '\r') { w->as.ansiTerm.cursorCol = 0; } else if (ch == '\n') { ansiTermNewline(w); } else if (ch == '\b') { if (w->as.ansiTerm.cursorCol > 0) { w->as.ansiTerm.cursorCol--; } } else if (ch == '\t') { w->as.ansiTerm.cursorCol = (w->as.ansiTerm.cursorCol + 8) & ~7; if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) { w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1; } } else if (ch == '\a') { // Bell — ignored } else if (ch >= 32 || ch >= 128) { ansiTermPutChar(w, ch); } break; case PARSE_ESC: if (ch == '[') { w->as.ansiTerm.parseState = PARSE_CSI; w->as.ansiTerm.paramCount = 0; w->as.ansiTerm.csiPrivate = false; memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params)); } else { // Unknown escape — return to normal w->as.ansiTerm.parseState = PARSE_NORMAL; } break; case PARSE_CSI: if (ch == '?') { w->as.ansiTerm.csiPrivate = true; } else if (ch >= '0' && ch <= '9') { if (w->as.ansiTerm.paramCount == 0) { w->as.ansiTerm.paramCount = 1; } int32_t idx = w->as.ansiTerm.paramCount - 1; if (idx < ANSI_MAX_PARAMS) { w->as.ansiTerm.params[idx] = w->as.ansiTerm.params[idx] * 10 + (ch - '0'); } } else if (ch == ';') { if (w->as.ansiTerm.paramCount < ANSI_MAX_PARAMS) { w->as.ansiTerm.paramCount++; } } else if (ch >= 0x40 && ch <= 0x7E) { // Final byte — dispatch the CSI sequence ansiTermDispatchCsi(w, ch); w->as.ansiTerm.parseState = PARSE_NORMAL; } else { // Unexpected byte — abort sequence w->as.ansiTerm.parseState = PARSE_NORMAL; } break; } } // ============================================================ // ansiTermProcessSgr // ============================================================ // // Handle SGR (Select Graphic Rendition) escape sequence. static void ansiTermProcessSgr(WidgetT *w) { if (w->as.ansiTerm.paramCount == 0) { // ESC[m with no params = reset w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; w->as.ansiTerm.bold = false; return; } for (int32_t i = 0; i < w->as.ansiTerm.paramCount; i++) { int32_t code = w->as.ansiTerm.params[i]; uint8_t fg = w->as.ansiTerm.curAttr & 0x0F; uint8_t bg = (w->as.ansiTerm.curAttr >> 4) & 0x0F; if (code == 0) { fg = 7; bg = 0; w->as.ansiTerm.bold = false; } else if (code == 1) { w->as.ansiTerm.bold = true; fg |= 8; } else if (code == 5) { // Blink — use bright background bg |= 8; } else if (code == 7) { // Reverse video uint8_t tmp = fg; fg = bg; bg = tmp; } else if (code == 22) { // Normal intensity w->as.ansiTerm.bold = false; fg &= 7; } else if (code >= 30 && code <= 37) { fg = (uint8_t)sAnsiToCga[code - 30]; if (w->as.ansiTerm.bold) { fg |= 8; } } else if (code >= 40 && code <= 47) { bg = (uint8_t)sAnsiToCga[code - 40]; } else if (code >= 90 && code <= 97) { // Bright foreground fg = (uint8_t)(sAnsiToCga[code - 90] | 8); } else if (code >= 100 && code <= 107) { // Bright background bg = (uint8_t)(sAnsiToCga[code - 100] | 8); } w->as.ansiTerm.curAttr = (uint8_t)((bg << 4) | fg); } } // ============================================================ // ansiTermPutChar // ============================================================ // // Place a character at the cursor and advance. static void ansiTermPutChar(WidgetT *w, uint8_t ch) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t row = w->as.ansiTerm.cursorRow; int32_t col = w->as.ansiTerm.cursorCol; if (row >= 0 && row < rows && col >= 0 && col < cols) { int32_t idx = (row * cols + col) * 2; w->as.ansiTerm.cells[idx] = ch; w->as.ansiTerm.cells[idx + 1] = w->as.ansiTerm.curAttr; ansiTermDirtyRow(w, row); } w->as.ansiTerm.cursorCol++; if (w->as.ansiTerm.cursorCol >= cols) { if (w->as.ansiTerm.wrapMode) { w->as.ansiTerm.cursorCol = 0; ansiTermNewline(w); } else { w->as.ansiTerm.cursorCol = cols - 1; } } } // ============================================================ // ansiTermScrollDown // ============================================================ static void ansiTermScrollDown(WidgetT *w) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; uint8_t *cells = w->as.ansiTerm.cells; int32_t bytesPerRow = cols * 2; // Shift all lines down by one memmove(cells + bytesPerRow, cells, (rows - 1) * bytesPerRow); // Clear the top line ansiTermFillCells(w, 0, cols); ansiTermDirtyAll(w); } // ============================================================ // ansiTermScrollUp // ============================================================ // // Scroll the screen up by one line. The top line is pushed into // the scrollback buffer before being discarded. static void ansiTermScrollUp(WidgetT *w) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; uint8_t *cells = w->as.ansiTerm.cells; int32_t bytesPerRow = cols * 2; // Track whether the view was following live output bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); // Push top line to scrollback ansiTermAddToScrollback(w, 0); // Shift all lines up by one memmove(cells, cells + bytesPerRow, (rows - 1) * bytesPerRow); // Clear the bottom line ansiTermFillCells(w, (rows - 1) * cols, cols); // Keep view at bottom if it was following live output if (wasAtBottom) { w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; } ansiTermDirtyAll(w); } // ============================================================ // wgtAnsiTerm // ============================================================ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { if (!parent) { return NULL; } if (cols <= 0) { cols = 80; } if (rows <= 0) { rows = 25; } WidgetT *w = widgetAlloc(parent, WidgetAnsiTermE); if (!w) { return NULL; } int32_t cellCount = cols * rows; w->as.ansiTerm.cells = (uint8_t *)malloc(cellCount * 2); if (!w->as.ansiTerm.cells) { free(w); return NULL; } // Allocate scrollback buffer int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; w->as.ansiTerm.scrollback = (uint8_t *)malloc(sbMax * cols * 2); if (!w->as.ansiTerm.scrollback) { free(w->as.ansiTerm.cells); free(w); return NULL; } w->as.ansiTerm.cols = cols; w->as.ansiTerm.rows = rows; w->as.ansiTerm.cursorRow = 0; w->as.ansiTerm.cursorCol = 0; w->as.ansiTerm.cursorVisible = true; w->as.ansiTerm.wrapMode = true; w->as.ansiTerm.bold = false; w->as.ansiTerm.csiPrivate = false; w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; w->as.ansiTerm.parseState = PARSE_NORMAL; w->as.ansiTerm.paramCount = 0; w->as.ansiTerm.savedRow = 0; w->as.ansiTerm.savedCol = 0; w->as.ansiTerm.scrollbackMax = sbMax; w->as.ansiTerm.scrollbackCount = 0; w->as.ansiTerm.scrollbackHead = 0; w->as.ansiTerm.scrollPos = 0; w->as.ansiTerm.commCtx = NULL; w->as.ansiTerm.commRead = NULL; w->as.ansiTerm.commWrite = NULL; memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params)); // Initialize all cells to space with default attribute for (int32_t i = 0; i < cellCount; i++) { w->as.ansiTerm.cells[i * 2] = ' '; w->as.ansiTerm.cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } return w; } // ============================================================ // wgtAnsiTermClear // ============================================================ void wgtAnsiTermClear(WidgetT *w) { if (!w || w->type != WidgetAnsiTermE) { return; } int32_t rows = w->as.ansiTerm.rows; int32_t cols = w->as.ansiTerm.cols; // Push all visible lines to scrollback bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } if (wasAtBottom) { w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; } // Clear the screen int32_t cellCount = cols * rows; for (int32_t i = 0; i < cellCount; i++) { w->as.ansiTerm.cells[i * 2] = ' '; w->as.ansiTerm.cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } w->as.ansiTerm.cursorRow = 0; w->as.ansiTerm.cursorCol = 0; w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; w->as.ansiTerm.bold = false; w->as.ansiTerm.parseState = PARSE_NORMAL; } // ============================================================ // wgtAnsiTermSetScrollback // ============================================================ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { if (!w || w->type != WidgetAnsiTermE || maxLines <= 0) { return; } int32_t cols = w->as.ansiTerm.cols; uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); if (!newBuf) { return; } free(w->as.ansiTerm.scrollback); w->as.ansiTerm.scrollback = newBuf; w->as.ansiTerm.scrollbackMax = maxLines; w->as.ansiTerm.scrollbackCount = 0; w->as.ansiTerm.scrollbackHead = 0; w->as.ansiTerm.scrollPos = 0; } // ============================================================ // wgtAnsiTermPoll // ============================================================ int32_t wgtAnsiTermPoll(WidgetT *w) { if (!w || w->type != WidgetAnsiTermE || !w->as.ansiTerm.commRead) { return 0; } uint8_t buf[256]; int32_t n = w->as.ansiTerm.commRead(w->as.ansiTerm.commCtx, buf, (int32_t)sizeof(buf)); if (n > 0) { wgtAnsiTermWrite(w, buf, n); } return n > 0 ? n : 0; } // ============================================================ // wgtAnsiTermRepaint // ============================================================ // // Fast repaint: renders only dirty rows directly into the window's // content buffer, bypassing the full widget paint pipeline (no clear, // no relayout, no other widgets). This keeps ACK turnaround fast // for the serial link. int32_t wgtAnsiTermRepaint(WidgetT *w) { if (!w || w->type != WidgetAnsiTermE || !w->window) { return 0; } // Always repaint the row the cursor was on last time (to erase it) // and the row it's on now (to draw it) int32_t prevRow = w->as.ansiTerm.lastCursorRow; int32_t curRow = w->as.ansiTerm.cursorRow; int32_t prevCol = w->as.ansiTerm.lastCursorCol; int32_t curCol = w->as.ansiTerm.cursorCol; if (prevRow != curRow || prevCol != curCol) { if (prevRow >= 0 && prevRow < w->as.ansiTerm.rows) { w->as.ansiTerm.dirtyRows |= (1U << prevRow); } if (curRow >= 0 && curRow < w->as.ansiTerm.rows) { w->as.ansiTerm.dirtyRows |= (1U << curRow); } } uint32_t dirty = w->as.ansiTerm.dirtyRows; if (dirty == 0) { return 0; } WindowT *win = w->window; if (!win->contentBuf || !win->widgetRoot) { return 0; } AppContextT *ctx = (AppContextT *)win->widgetRoot->userData; if (!ctx) { return 0; } // Set up display context pointing at the content buffer DisplayT cd = ctx->display; cd.backBuf = win->contentBuf; cd.width = win->contentW; cd.height = win->contentH; cd.pitch = win->contentPitch; cd.clipX = 0; cd.clipY = 0; cd.clipW = win->contentW; cd.clipH = win->contentH; const BlitOpsT *ops = &ctx->blitOps; const BitmapFontT *font = &ctx->font; int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t cellW = font->charWidth; int32_t cellH = font->charHeight; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; // Build palette uint32_t palette[16]; for (int32_t i = 0; i < 16; i++) { palette[i] = packColor(&cd, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); } bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); int32_t repainted = 0; for (int32_t row = 0; row < rows; row++) { if (!(dirty & (1U << row))) { continue; } int32_t lineIndex = w->as.ansiTerm.scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); for (int32_t col = 0; col < cols; col++) { uint8_t ch = lineData[col * 2]; uint8_t attr = lineData[col * 2 + 1]; uint32_t fg = palette[attr & 0x0F]; uint32_t bg = palette[(attr >> 4) & 0x0F]; if (viewingLive && w->as.ansiTerm.cursorVisible && w->focused && row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { uint32_t tmp = fg; fg = bg; bg = tmp; } int32_t cx = baseX + col * cellW; int32_t cy = baseY + row * cellH; drawChar(&cd, ops, font, cx, cy, (char)ch, fg, bg, true); } repainted++; } w->as.ansiTerm.dirtyRows = 0; w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow; w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol; return repainted; } // ============================================================ // wgtAnsiTermSetComm // ============================================================ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { if (!w || w->type != WidgetAnsiTermE) { return; } w->as.ansiTerm.commCtx = ctx; w->as.ansiTerm.commRead = readFn; w->as.ansiTerm.commWrite = writeFn; } // ============================================================ // wgtAnsiTermWrite // ============================================================ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) { return; } for (int32_t i = 0; i < len; i++) { ansiTermProcessByte(w, data[i]); } } // ============================================================ // widgetAnsiTermCalcMinSize // ============================================================ void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = w->as.ansiTerm.cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; w->calcMinH = w->as.ansiTerm.rows * font->charHeight + ANSI_BORDER * 2; } // ============================================================ // widgetAnsiTermOnKey // ============================================================ // // Translate keyboard input to ANSI escape sequences and send // via the comm interface. Does nothing if commWrite is NULL. void widgetAnsiTermOnKey(WidgetT *w, int32_t key) { if (!w->as.ansiTerm.commWrite) { return; } uint8_t buf[8]; int32_t len = 0; if (key >= 32 && key < 127) { // Printable ASCII buf[0] = (uint8_t)key; len = 1; } else if (key == 13 || key == 10) { // Enter buf[0] = '\r'; len = 1; } else if (key == 8) { // Backspace buf[0] = 0x08; len = 1; } else if (key == (0x48 | 0x100)) { // Up arrow → ESC[A buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; len = 3; } else if (key == (0x50 | 0x100)) { // Down arrow → ESC[B buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; len = 3; } else if (key == (0x4D | 0x100)) { // Right arrow → ESC[C buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; len = 3; } else if (key == (0x4B | 0x100)) { // Left arrow → ESC[D buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; len = 3; } else if (key == (0x47 | 0x100)) { // Home → ESC[H buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; len = 3; } else if (key == (0x4F | 0x100)) { // End → ESC[F buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; len = 3; } else if (key == (0x49 | 0x100)) { // PgUp → ESC[5~ buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; len = 4; } else if (key == (0x51 | 0x100)) { // PgDn → ESC[6~ buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; len = 4; } else if (key == (0x53 | 0x100)) { // Delete → ESC[3~ buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; len = 4; } if (len > 0) { w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len); } } // ============================================================ // widgetAnsiTermOnMouse // ============================================================ // // Handle mouse clicks: scrollbar interaction and focus. void widgetAnsiTermOnMouse(WidgetT *hit, int32_t vx, int32_t vy, const BitmapFontT *font) { hit->focused = true; int32_t sbCount = hit->as.ansiTerm.scrollbackCount; if (sbCount == 0) { return; } // Scrollbar geometry int32_t cols = hit->as.ansiTerm.cols; int32_t rows = hit->as.ansiTerm.rows; int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; int32_t sbY = hit->y + ANSI_BORDER; int32_t sbH = rows * font->charHeight; int32_t arrowH = ANSI_SB_W; if (vx < sbX || vx >= sbX + ANSI_SB_W) { return; } int32_t maxScroll = sbCount; if (vy >= sbY && vy < sbY + arrowH) { // Up arrow hit->as.ansiTerm.scrollPos--; } else if (vy >= sbY + sbH - arrowH && vy < sbY + sbH) { // Down arrow hit->as.ansiTerm.scrollPos++; } else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { // Track area — compute thumb position to determine page direction int32_t trackY = sbY + arrowH; int32_t trackH = sbH - arrowH * 2; if (trackH <= 0) { return; } int32_t totalLines = sbCount + rows; int32_t thumbH = (rows * trackH) / totalLines; if (thumbH < 8) { thumbH = 8; } int32_t thumbRange = trackH - thumbH; int32_t thumbY = trackY; if (maxScroll > 0 && thumbRange > 0) { thumbY = trackY + (hit->as.ansiTerm.scrollPos * thumbRange) / maxScroll; } if (vy < thumbY) { // Page up hit->as.ansiTerm.scrollPos -= rows; } else if (vy >= thumbY + thumbH) { // Page down hit->as.ansiTerm.scrollPos += rows; } } // Clamp if (hit->as.ansiTerm.scrollPos < 0) { hit->as.ansiTerm.scrollPos = 0; } if (hit->as.ansiTerm.scrollPos > maxScroll) { hit->as.ansiTerm.scrollPos = maxScroll; } } // ============================================================ // widgetAnsiTermPaint // ============================================================ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { // Draw sunken bevel border BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = 0; bevel.width = ANSI_BORDER; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Build the 16-color packed palette for this display format uint32_t palette[16]; for (int32_t i = 0; i < 16; i++) { palette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); } int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t cellW = font->charWidth; int32_t cellH = font->charHeight; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; int32_t sbCount = w->as.ansiTerm.scrollbackCount; // Determine if viewing live terminal or scrollback bool viewingLive = (w->as.ansiTerm.scrollPos == sbCount); // Render character cells for (int32_t row = 0; row < rows; row++) { int32_t lineIndex = w->as.ansiTerm.scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); for (int32_t col = 0; col < cols; col++) { uint8_t ch = lineData[col * 2]; uint8_t attr = lineData[col * 2 + 1]; uint32_t fg = palette[attr & 0x0F]; uint32_t bg = palette[(attr >> 4) & 0x0F]; // Draw cursor as inverse block (only when viewing live terminal) if (viewingLive && w->as.ansiTerm.cursorVisible && w->focused && row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { uint32_t tmp = fg; fg = bg; bg = tmp; } int32_t cx = baseX + col * cellW; int32_t cy = baseY + row * cellH; drawChar(d, ops, font, cx, cy, (char)ch, fg, bg, true); } } // Draw scrollbar int32_t sbX = baseX + cols * cellW; int32_t sbY = baseY; int32_t sbW = ANSI_SB_W; int32_t sbH = rows * cellH; int32_t arrowH = ANSI_SB_W; if (sbCount == 0) { // No scrollback — fill scrollbar area with trough color rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough); return; } // Track background int32_t trackY = sbY + arrowH; int32_t trackH = sbH - arrowH * 2; rectFill(d, ops, sbX, trackY, sbW, trackH, colors->scrollbarTrough); // Up arrow button BevelStyleT btnBevel; btnBevel.highlight = colors->windowHighlight; btnBevel.shadow = colors->windowShadow; btnBevel.face = colors->scrollbarBg; btnBevel.width = 1; drawBevel(d, ops, sbX, sbY, sbW, arrowH, &btnBevel); // Up arrow glyph (small triangle) int32_t arrowMidX = sbX + sbW / 2; int32_t arrowMidY = sbY + arrowH / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, arrowMidX - i, arrowMidY - 1 + i, i * 2 + 1, colors->scrollbarFg); } // Down arrow button drawBevel(d, ops, sbX, sbY + sbH - arrowH, sbW, arrowH, &btnBevel); arrowMidY = sbY + sbH - arrowH + arrowH / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, arrowMidX - i, arrowMidY + 1 - i, i * 2 + 1, colors->scrollbarFg); } // Thumb if (trackH > 0) { int32_t totalLines = sbCount + rows; int32_t thumbH = (rows * trackH) / totalLines; if (thumbH < 8) { thumbH = 8; } int32_t thumbRange = trackH - thumbH; int32_t maxScroll = sbCount; int32_t thumbY = trackY; if (maxScroll > 0 && thumbRange > 0) { thumbY = trackY + (w->as.ansiTerm.scrollPos * thumbRange) / maxScroll; } drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); } }