#define DVX_WIDGET_IMPL // widgetAnsiTerm.c -- ANSI BBS terminal emulator widget // // Implements a VT100/ANSI-compatible terminal emulator widget designed for // connecting to BBS systems over the serial link. The terminal uses a // traditional text-mode cell buffer (character + attribute byte pairs) that // mirrors the layout of CGA/VGA text mode memory. This representation was // chosen because: // 1. It maps directly to the BBS/ANSI art paradigm (CP437 character set, // 16-color CGA palette, blink attribute) // 2. Cell-based storage is extremely compact -- 2 bytes per cell means an // 80x25 screen is only 4000 bytes, fitting in L1 cache on a 486 // 3. Dirty-row tracking via a 32-bit bitmask allows sub-millisecond // incremental repaints without scanning the entire buffer // // The ANSI parser is a 3-state machine (NORMAL -> ESC -> CSI) that handles // the subset of sequences commonly used by DOS BBS software: cursor movement, // screen/line erase, scrolling regions, SGR colors, and a few DEC private modes. // Full VT100 conformance is explicitly NOT a goal -- only sequences actually // emitted by real BBS systems are implemented. // // Scrollback is implemented as a circular buffer of row snapshots. Only // full-screen scroll operations push lines into scrollback; scroll-region // operations (used by split-screen chat, status bars) do not, matching // the behavior users expect from DOS terminal programs. // // The widget supports two paint paths: // - Full paint (widgetAnsiTermPaint): used during normal widget repaints // - Fast repaint (wgtAnsiTermRepaint): bypasses the widget pipeline // entirely, rendering dirty rows directly into the window's content // buffer. This is critical for serial communication where ACK turnaround // time matters -- the fewer milliseconds between receiving data and // displaying it, the higher the effective throughput. // // Communication is abstracted through read/write function pointers, allowing // the terminal to work with raw serial ports, the secLink encrypted channel, // or any other byte-oriented transport. #include "dvxWidgetPlugin.h" #include "../texthelp/textHelp.h" static int32_t sTypeId = -1; #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 #define ANSI_DEFAULT_ATTR 0x07 #define ATTR_BLINK_BIT 0x80 #define ATTR_FG_MASK 0x0F #define BLINK_MS 500 #define CURSOR_MS 250 typedef struct { uint8_t *cells; int32_t cols; int32_t rows; int32_t cursorRow; int32_t cursorCol; bool cursorVisible; bool wrapMode; bool bold; bool originMode; bool csiPrivate; uint8_t curAttr; uint8_t parseState; int32_t params[8]; int32_t paramCount; int32_t savedRow; int32_t savedCol; int32_t scrollTop; int32_t scrollBot; uint8_t *scrollback; int32_t scrollbackMax; int32_t scrollbackCount; int32_t scrollbackHead; int32_t scrollPos; bool blinkVisible; clock_t blinkTime; bool cursorOn; clock_t cursorTime; uint32_t dirtyRows; int32_t lastCursorRow; int32_t lastCursorCol; uint32_t packedPalette[16]; bool paletteValid; int32_t selStartLine; int32_t selStartCol; int32_t selEndLine; int32_t selEndCol; bool selecting; void *commCtx; int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); } AnsiTermDataT; static const uint8_t sCgaPalette[16][3] = { { 0, 0, 0}, { 0, 0, 170}, { 0, 170, 0}, { 0, 170, 170}, {170, 0, 0}, {170, 0, 170}, {170, 85, 0}, {170, 170, 170}, { 85, 85, 85}, { 85, 85, 255}, { 85, 255, 85}, { 85, 255, 255}, {255, 85, 85}, {255, 85, 255}, {255, 255, 85}, {255, 255, 255}, }; 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 ansiTermBuildPalette(WidgetT *w, const DisplayT *d); static void ansiTermClearSelection(WidgetT *w); static void ansiTermCopySelection(WidgetT *w); static void ansiTermDeleteLines(WidgetT *w, int32_t count); 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 bool ansiTermHasSelection(const WidgetT *w); static void ansiTermInsertLines(WidgetT *w, int32_t count); static void ansiTermNewline(WidgetT *w); static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY); static void ansiTermPasteToComm(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); static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol); void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (!at->scrollback || at->scrollbackMax <= 0) { return; } int32_t cols = at->cols; int32_t bytesPerRow = cols * 2; int32_t head = at->scrollbackHead; memcpy(at->scrollback + head * bytesPerRow, at->cells + screenRow * bytesPerRow, bytesPerRow); at->scrollbackHead = (head + 1) % at->scrollbackMax; if (at->scrollbackCount < at->scrollbackMax) { at->scrollbackCount++; } } static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (at->paletteValid) { return; } for (int32_t i = 0; i < 16; i++) { at->packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); } at->paletteValid = true; } static void ansiTermClearSelection(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (ansiTermHasSelection(w)) { at->dirtyRows = 0xFFFFFFFF; } at->selStartLine = -1; at->selStartCol = -1; at->selEndLine = -1; at->selEndCol = -1; at->selecting = false; } static void ansiTermCopySelection(WidgetT *w) { if (!ansiTermHasSelection(w)) { return; } AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t sLine, sCol, eLine, eCol; ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); int32_t cols = at->cols; char buf[4096]; int32_t pos = 0; for (int32_t line = sLine; line <= eLine && pos < 4095; line++) { const uint8_t *lineData = ansiTermGetLine(w, line); int32_t colStart = (line == sLine) ? sCol : 0; int32_t colEnd = (line == eLine) ? eCol : cols; int32_t lastNonSpace = colStart - 1; for (int32_t c = colStart; c < colEnd; c++) { if (lineData[c * 2] != ' ') { lastNonSpace = c; } } for (int32_t c = colStart; c <= lastNonSpace && pos < 4095; c++) { buf[pos++] = (char)lineData[c * 2]; } if (line < eLine && pos < 4095) { buf[pos++] = '\n'; } } buf[pos] = '\0'; if (pos > 0) { clipboardCopy(buf, pos); } } static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t startRow = startCell / at->cols; int32_t endRow = (startCell + count - 1) / at->cols; for (int32_t r = startRow; r <= endRow && r < 32; r++) { at->dirtyRows |= (1U << r); } } static void ansiTermDirtyRow(WidgetT *w, int32_t row) { if (row >= 0 && row < 32) { ((AnsiTermDataT *)w->data)->dirtyRows |= (1U << row); } } static void ansiTermDeleteLines(WidgetT *w, int32_t count) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t bot = at->scrollBot; int32_t row = at->cursorRow; if (count > bot - row + 1) { count = bot - row + 1; } if (count <= 0) { return; } int32_t bytesPerRow = cols * 2; if (row + count <= bot) { memmove(at->cells + row * bytesPerRow, at->cells + (row + count) * bytesPerRow, (bot - row - count + 1) * bytesPerRow); } for (int32_t r = bot - count + 1; r <= bot; r++) { ansiTermFillCells(w, r * cols, cols); } for (int32_t r = row; r <= bot; r++) { ansiTermDirtyRow(w, r); } } static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t *p = at->params; int32_t n = at->paramCount; if (at->csiPrivate) { int32_t mode = (n >= 1) ? p[0] : 0; if (cmd == 'h') { if (mode == 6) { at->originMode = true; at->cursorRow = at->scrollTop; at->cursorCol = 0; } if (mode == 7) { at->wrapMode = true; } if (mode == 25) { at->cursorVisible = true; } } else if (cmd == 'l') { if (mode == 6) { at->originMode = false; at->cursorRow = 0; at->cursorCol = 0; } if (mode == 7) { at->wrapMode = false; } if (mode == 25) { at->cursorVisible = false; } } return; } switch (cmd) { case '@': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; int32_t cols = at->cols; int32_t row = at->cursorRow; int32_t col = at->cursorCol; if (count > cols - col) { count = cols - col; } if (count > 0) { uint8_t *base = at->cells + row * cols * 2; memmove(base + (col + count) * 2, base + col * 2, (cols - col - count) * 2); for (int32_t i = col; i < col + count; i++) { base[i * 2] = ' '; base[i * 2 + 1] = at->curAttr; } ansiTermDirtyRow(w, row); } break; } case 'A': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; int32_t minRow = at->originMode ? at->scrollTop : 0; at->cursorRow -= count; if (at->cursorRow < minRow) { at->cursorRow = minRow; } break; } case 'B': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; int32_t maxRow = at->originMode ? at->scrollBot : at->rows - 1; at->cursorRow += count; if (at->cursorRow > maxRow) { at->cursorRow = maxRow; } break; } case 'C': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; at->cursorCol += count; if (at->cursorCol >= at->cols) { at->cursorCol = at->cols - 1; } break; } case 'D': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; at->cursorCol -= count; if (at->cursorCol < 0) { at->cursorCol = 0; } break; } case 'c': { if (at->commWrite) { const uint8_t reply[] = "\033[?1;2c"; at->commWrite(at->commCtx, reply, 7); } break; } case 'E': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; for (int32_t i = 0; i < count; i++) { ansiTermNewline(w); } at->cursorCol = 0; break; } case 'H': case 'f': { int32_t row = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t col = (n >= 2 && p[1]) ? p[1] - 1 : 0; if (at->originMode) { row += at->scrollTop; if (row > at->scrollBot) { row = at->scrollBot; } } if (row < 0) { row = 0; } if (row >= at->rows) { row = at->rows - 1; } if (col < 0) { col = 0; } if (col >= at->cols) { col = at->cols - 1; } at->cursorRow = row; at->cursorCol = col; break; } case 'J': { ansiTermEraseDisplay(w, (n >= 1) ? p[0] : 0); break; } case 'K': { ansiTermEraseLine(w, (n >= 1) ? p[0] : 0); break; } case 'L': { ansiTermInsertLines(w, (n >= 1 && p[0]) ? p[0] : 1); break; } case 'M': { ansiTermDeleteLines(w, (n >= 1 && p[0]) ? p[0] : 1); break; } case 'P': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; int32_t cols = at->cols; int32_t row = at->cursorRow; int32_t col = at->cursorCol; if (count > cols - col) { count = cols - col; } if (count > 0) { uint8_t *base = at->cells + row * cols * 2; memmove(base + col * 2, base + (col + count) * 2, (cols - col - count) * 2); for (int32_t i = cols - count; i < cols; i++) { base[i * 2] = ' '; base[i * 2 + 1] = at->curAttr; } ansiTermDirtyRow(w, row); } break; } case 'S': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; for (int32_t i = 0; i < count; i++) { ansiTermScrollUp(w); } break; } case 'T': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; for (int32_t i = 0; i < count; i++) { ansiTermScrollDown(w); } break; } case 'Z': { int32_t col = at->cursorCol; if (col > 0) { col = ((col - 1) / 8) * 8; } at->cursorCol = col; break; } case 'm': ansiTermProcessSgr(w); break; case 'n': { int32_t mode = (n >= 1) ? p[0] : 0; if (at->commWrite) { if (mode == 6) { char reply[16]; int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(at->cursorRow + 1), (long)(at->cursorCol + 1)); at->commWrite(at->commCtx, (const uint8_t *)reply, len); } else if (mode == 255) { char reply[16]; int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->rows, (long)at->cols); at->commWrite(at->commCtx, (const uint8_t *)reply, len); } } break; } case 'r': { int32_t rows = at->rows; int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t bot = (n >= 2 && p[1]) ? p[1] - 1 : rows - 1; if (top < 0) { top = 0; } if (bot >= rows) { bot = rows - 1; } if (top < bot) { at->scrollTop = top; at->scrollBot = bot; } else { at->scrollTop = 0; at->scrollBot = rows - 1; } at->cursorRow = at->originMode ? at->scrollTop : 0; at->cursorCol = 0; break; } case 's': at->savedRow = at->cursorRow; at->savedCol = at->cursorCol; break; case 'u': at->cursorRow = at->savedRow; at->cursorCol = at->savedCol; break; default: break; } } static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t rows = at->rows; int32_t cur = at->cursorRow * cols + at->cursorCol; if (mode == 0) { ansiTermFillCells(w, cur, cols * rows - cur); } else if (mode == 1) { ansiTermFillCells(w, 0, cur + 1); } else if (mode == 2) { bool wasAtBottom = (at->scrollPos == at->scrollbackCount); for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } if (wasAtBottom) { at->scrollPos = at->scrollbackCount; } ansiTermFillCells(w, 0, cols * rows); } } static void ansiTermEraseLine(WidgetT *w, int32_t mode) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t row = at->cursorRow; int32_t col = at->cursorCol; int32_t base = row * cols; if (mode == 0) { ansiTermFillCells(w, base + col, cols - col); } else if (mode == 1) { ansiTermFillCells(w, base, col + 1); } else if (mode == 2) { ansiTermFillCells(w, base, cols); } } static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; uint8_t *cells = at->cells; uint8_t attr = at->curAttr; int32_t total = at->cols * at->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); } static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t bytesPerRow = cols * 2; int32_t sbCount = at->scrollbackCount; if (lineIndex < sbCount) { int32_t sbMax = at->scrollbackMax; int32_t actual = (at->scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; return at->scrollback + actual * bytesPerRow; } return at->cells + (lineIndex - sbCount) * bytesPerRow; } static bool ansiTermHasSelection(const WidgetT *w) { const AnsiTermDataT *at = (const AnsiTermDataT *)w->data; if (at->selStartLine < 0) { return false; } if (at->selStartLine == at->selEndLine && at->selStartCol == at->selEndCol) { return false; } return true; } static void ansiTermInsertLines(WidgetT *w, int32_t count) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t bot = at->scrollBot; int32_t row = at->cursorRow; if (count > bot - row + 1) { count = bot - row + 1; } if (count <= 0) { return; } int32_t bytesPerRow = cols * 2; if (row + count <= bot) { memmove(at->cells + (row + count) * bytesPerRow, at->cells + row * bytesPerRow, (bot - row - count + 1) * bytesPerRow); } for (int32_t r = row; r < row + count; r++) { ansiTermFillCells(w, r * cols, cols); } for (int32_t r = row; r <= bot; r++) { ansiTermDirtyRow(w, r); } } static void ansiTermNewline(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (at->cursorRow == at->scrollBot) { ansiTermScrollUp(w); } else if (at->cursorRow < at->rows - 1) { at->cursorRow++; } } static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; switch (at->parseState) { case PARSE_NORMAL: if (ch == 0x1B) { at->parseState = PARSE_ESC; } else if (ch == '\r') { at->cursorCol = 0; } else if (ch == '\n') { ansiTermNewline(w); } else if (ch == '\b') { if (at->cursorCol > 0) { at->cursorCol--; } } else if (ch == '\t') { at->cursorCol = (at->cursorCol + 8) & ~7; if (at->cursorCol >= at->cols) { at->cursorCol = at->cols - 1; } } else if (ch == '\f') { ansiTermEraseDisplay(w, 2); at->cursorRow = 0; at->cursorCol = 0; } else if (ch == '\a') { /* Bell -- ignored */ } else { ansiTermPutChar(w, ch); } break; case PARSE_ESC: if (ch == '[') { at->parseState = PARSE_CSI; at->paramCount = 0; at->csiPrivate = false; memset(at->params, 0, sizeof(at->params)); } else if (ch == 'D') { ansiTermScrollUp(w); at->parseState = PARSE_NORMAL; } else if (ch == 'M') { ansiTermScrollDown(w); at->parseState = PARSE_NORMAL; } else if (ch == 'c') { at->cursorRow = 0; at->cursorCol = 0; at->curAttr = ANSI_DEFAULT_ATTR; at->bold = false; at->wrapMode = true; at->originMode = false; at->cursorVisible = true; at->scrollTop = 0; at->scrollBot = at->rows - 1; ansiTermEraseDisplay(w, 2); at->parseState = PARSE_NORMAL; } else { at->parseState = PARSE_NORMAL; } break; case PARSE_CSI: if (ch == '?') { at->csiPrivate = true; } else if (ch >= '0' && ch <= '9') { if (at->paramCount == 0) { at->paramCount = 1; } int32_t idx = at->paramCount - 1; if (idx < ANSI_MAX_PARAMS) { at->params[idx] = at->params[idx] * 10 + (ch - '0'); } } else if (ch == ';') { if (at->paramCount < ANSI_MAX_PARAMS) { at->paramCount++; } } else if (ch >= 0x40 && ch <= 0x7E) { ansiTermDispatchCsi(w, ch); at->parseState = PARSE_NORMAL; } else { at->parseState = PARSE_NORMAL; } break; } } static void ansiTermProcessSgr(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (at->paramCount == 0) { at->curAttr = ANSI_DEFAULT_ATTR; at->bold = false; return; } for (int32_t i = 0; i < at->paramCount; i++) { int32_t code = at->params[i]; uint8_t fg = at->curAttr & 0x0F; uint8_t bg = (at->curAttr >> 4) & 0x0F; if (code == 0) { fg = 7; bg = 0; at->bold = false; } else if (code == 1) { at->bold = true; fg |= 8; } else if (code == 5) { bg |= 8; } else if (code == 25) { bg &= 7; } else if (code == 7) { uint8_t tmp = fg; fg = bg; bg = tmp; } else if (code == 8) { fg = bg & 0x07; } else if (code == 22) { at->bold = false; fg &= 7; } else if (code >= 30 && code <= 37) { fg = (uint8_t)sAnsiToCga[code - 30]; if (at->bold) { fg |= 8; } } else if (code >= 40 && code <= 47) { bg = (uint8_t)(sAnsiToCga[code - 40] | (bg & 8)); } else if (code >= 90 && code <= 97) { fg = (uint8_t)(sAnsiToCga[code - 90] | 8); } else if (code >= 100 && code <= 107) { bg = (uint8_t)(sAnsiToCga[code - 100] | 8); } at->curAttr = (uint8_t)((bg << 4) | fg); } } static void ansiTermPasteToComm(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (!at->commWrite) { return; } int32_t clipLen; const char *clip = clipboardGet(&clipLen); if (clipLen <= 0) { return; } for (int32_t i = 0; i < clipLen; i++) { uint8_t ch = (uint8_t)clip[i]; if (ch == '\n') { ch = '\r'; } at->commWrite(at->commCtx, &ch, 1); } } static void ansiTermPutChar(WidgetT *w, uint8_t ch) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t rows = at->rows; int32_t row = at->cursorRow; int32_t col = at->cursorCol; if (row >= 0 && row < rows && col >= 0 && col < cols) { int32_t idx = (row * cols + col) * 2; at->cells[idx] = ch; at->cells[idx + 1] = at->curAttr; ansiTermDirtyRow(w, row); } at->cursorCol++; if (at->cursorCol >= cols) { if (at->wrapMode) { at->cursorCol = 0; ansiTermNewline(w); } else { at->cursorCol = cols - 1; } } } static void ansiTermScrollDown(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t top = at->scrollTop; int32_t bot = at->scrollBot; int32_t bytesPerRow = cols * 2; if (bot > top) { memmove(at->cells + (top + 1) * bytesPerRow, at->cells + top * bytesPerRow, (bot - top) * bytesPerRow); } ansiTermFillCells(w, top * cols, cols); for (int32_t r = top; r <= bot && r < 32; r++) { at->dirtyRows |= (1U << r); } } static void ansiTermScrollUp(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; int32_t top = at->scrollTop; int32_t bot = at->scrollBot; int32_t bytesPerRow = cols * 2; if (top == 0 && bot == at->rows - 1) { bool wasAtBottom = (at->scrollPos == at->scrollbackCount); ansiTermAddToScrollback(w, 0); if (wasAtBottom) { at->scrollPos = at->scrollbackCount; } } if (bot > top) { memmove(at->cells + top * bytesPerRow, at->cells + (top + 1) * bytesPerRow, (bot - top) * bytesPerRow); } ansiTermFillCells(w, bot * cols, cols); for (int32_t r = top; r <= bot && r < 32; r++) { at->dirtyRows |= (1U << r); } } static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol) { const AnsiTermDataT *at = (const AnsiTermDataT *)w->data; int32_t sl = at->selStartLine, sc = at->selStartCol; int32_t el = at->selEndLine, ec = at->selEndCol; if (sl > el || (sl == el && sc > ec)) { *startLine = el; *startCol = ec; *endLine = sl; *endCol = sc; } else { *startLine = sl; *startCol = sc; *endLine = el; *endCol = ec; } } void wgtAnsiTermClear(WidgetT *w) { VALIDATE_WIDGET_VOID(w, sTypeId); AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t rows = at->rows; int32_t cols = at->cols; bool wasAtBottom = (at->scrollPos == at->scrollbackCount); for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } if (wasAtBottom) { at->scrollPos = at->scrollbackCount; } int32_t cellCount = cols * rows; for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } at->cursorRow = 0; at->cursorCol = 0; at->curAttr = ANSI_DEFAULT_ATTR; at->bold = false; at->parseState = PARSE_NORMAL; } int32_t wgtAnsiTermPoll(WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); AnsiTermDataT *at = (AnsiTermDataT *)w->data; clock_t now = clock(); clock_t blinkInterval = (clock_t)BLINK_MS * CLOCKS_PER_SEC / 1000; clock_t curInterval = (clock_t)CURSOR_MS * CLOCKS_PER_SEC / 1000; if ((now - at->blinkTime) >= blinkInterval) { at->blinkTime = now; at->blinkVisible = !at->blinkVisible; int32_t cols = at->cols; int32_t rows = at->rows; for (int32_t row = 0; row < rows && row < 32; row++) { for (int32_t col = 0; col < cols; col++) { if (at->cells[(row * cols + col) * 2 + 1] & ATTR_BLINK_BIT) { at->dirtyRows |= (1U << row); break; } } } } if ((now - at->cursorTime) >= curInterval) { at->cursorTime = now; at->cursorOn = !at->cursorOn; int32_t cRow = at->cursorRow; if (cRow >= 0 && cRow < 32) { at->dirtyRows |= (1U << cRow); } } if (!at->commRead) { return 0; } uint8_t buf[256]; int32_t n = at->commRead(at->commCtx, buf, (int32_t)sizeof(buf)); if (n > 0) { wgtAnsiTermWrite(w, buf, n); } return n > 0 ? n : 0; } int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { if (!w || w->type != sTypeId || !w->window) { return 0; } AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t prevRow = at->lastCursorRow; int32_t curRow = at->cursorRow; if (prevRow != curRow || at->lastCursorCol != at->cursorCol) { if (prevRow >= 0 && prevRow < at->rows) { at->dirtyRows |= (1U << prevRow); } if (curRow >= 0 && curRow < at->rows) { at->dirtyRows |= (1U << curRow); } } uint32_t dirty = at->dirtyRows; if (dirty == 0) { return 0; } WindowT *win = w->window; if (!win->contentBuf || !win->widgetRoot) { return 0; } AppContextT *ctx = wgtGetContext(win->widgetRoot); if (!ctx) { return 0; } 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 = at->cols; int32_t rows = at->rows; int32_t cellH = font->charHeight; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; ansiTermBuildPalette(w, &cd); const uint32_t *palette = at->packedPalette; bool viewingLive = (at->scrollPos == at->scrollbackCount); int32_t repainted = 0; int32_t minRow = rows; int32_t maxRow = -1; for (int32_t row = 0; row < rows; row++) { if (!(dirty & (1U << row))) { continue; } int32_t lineIndex = at->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); int32_t curCol2 = -1; if (viewingLive && at->cursorVisible && at->cursorOn && row == at->cursorRow) { curCol2 = at->cursorCol; } drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, at->blinkVisible, curCol2); ansiTermPaintSelRow(w, &cd, ops, font, row, baseX, baseY); if (row < minRow) { minRow = row; } if (row > maxRow) { maxRow = row; } repainted++; } at->dirtyRows = 0; at->lastCursorRow = at->cursorRow; at->lastCursorCol = at->cursorCol; if (outY) { *outY = baseY + minRow * cellH; } if (outH) { *outH = (maxRow - minRow + 1) * cellH; } return repainted; } void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { if (!w || w->type != sTypeId || !data || len <= 0) { return; } ansiTermClearSelection(w); for (int32_t i = 0; i < len; i++) { ansiTermProcessByte(w, data[i]); } } void widgetAnsiTermDestroy(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (at) { free(at->cells); free(at->scrollback); free(at); w->data = NULL; } } void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; w->calcMinW = at->cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; w->calcMinH = at->rows * font->charHeight + ANSI_BORDER * 2; } static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY) { if (!ansiTermHasSelection(w)) { return; } AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t sLine, sCol, eLine, eCol; ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); int32_t cols = at->cols; int32_t lineIndex = at->scrollPos + screenRow; if (lineIndex < sLine || lineIndex > eLine) { return; } int32_t colStart = (lineIndex == sLine) ? sCol : 0; int32_t colEnd = (lineIndex == eLine) ? eCol : cols; if (colStart >= colEnd) { return; } const uint8_t *lineData = ansiTermGetLine(w, lineIndex); const uint32_t *palette = at->packedPalette; int32_t cellH = font->charHeight; int32_t cellW = font->charWidth; for (int32_t col = colStart; col < colEnd; col++) { uint8_t ch = lineData[col * 2]; uint8_t attr = lineData[col * 2 + 1]; uint32_t fg = palette[(attr >> 4) & 0x07]; uint32_t bg = palette[attr & 0x0F]; drawChar(d, ops, font, baseX + col * cellW, baseY + screenRow * cellH, ch, fg, bg, true); } } void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (key == 0x03 && (mod & KEY_MOD_CTRL)) { if (ansiTermHasSelection(w)) { ansiTermCopySelection(w); ansiTermClearSelection(w); wgtInvalidatePaint(w); return; } } if (key == 0x16 && (mod & KEY_MOD_CTRL)) { ansiTermPasteToComm(w); wgtInvalidatePaint(w); return; } if (!at->commWrite) { return; } if (ansiTermHasSelection(w)) { ansiTermClearSelection(w); } uint8_t buf[8]; int32_t len = 0; if (key >= 32 && key < 127) { buf[0] = (uint8_t)key; len = 1; } else if (key == 0x1B) { buf[0] = 0x1B; len = 1; } else if (key == 0x09) { buf[0] = 0x09; len = 1; } else if (key == 13 || key == 10) { buf[0] = '\r'; len = 1; } else if (key == 8) { buf[0] = 0x08; len = 1; } else if (key == (0x48 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; len = 3; } else if (key == (0x50 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; len = 3; } else if (key == (0x4D | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; len = 3; } else if (key == (0x4B | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; len = 3; } else if (key == (0x47 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; len = 3; } else if (key == (0x4F | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; len = 3; } else if (key == (0x49 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; len = 4; } else if (key == (0x51 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; len = 4; } else if (key == (0x53 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; len = 4; } else if (key >= 1 && key < 32) { buf[0] = (uint8_t)key; len = 1; } if (len > 0) { at->commWrite(at->commCtx, buf, len); } wgtInvalidatePaint(w); } void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { AnsiTermDataT *at = (AnsiTermDataT *)hit->data; AppContextT *actx = (AppContextT *)root->userData; const BitmapFontT *font = &actx->font; hit->focused = true; clearOtherSelections(hit); int32_t cols = at->cols; int32_t rows = at->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) { int32_t baseX = hit->x + ANSI_BORDER; int32_t baseY = hit->y + ANSI_BORDER; int32_t clickRow = (vy - baseY) / font->charHeight; int32_t clickCol = (vx - baseX) / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= rows) { clickRow = rows - 1; } if (clickCol < 0) { clickCol = 0; } if (clickCol >= cols) { clickCol = cols - 1; } int32_t lineIndex = at->scrollPos + clickRow; int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 3) { at->selStartLine = lineIndex; at->selStartCol = 0; at->selEndLine = lineIndex; at->selEndCol = cols; at->selecting = false; sDragWidget = NULL; } else if (clicks == 2) { const uint8_t *lineData = ansiTermGetLine(hit, lineIndex); int32_t ws = clickCol; int32_t we = clickCol; while (ws > 0 && isWordChar((char)lineData[(ws - 1) * 2])) { ws--; } while (we < cols && isWordChar((char)lineData[we * 2])) { we++; } at->selStartLine = lineIndex; at->selStartCol = ws; at->selEndLine = lineIndex; at->selEndCol = we; at->selecting = false; sDragWidget = NULL; } else { at->selStartLine = lineIndex; at->selStartCol = clickCol; at->selEndLine = lineIndex; at->selEndCol = clickCol; at->selecting = true; sDragWidget = hit; } at->dirtyRows = 0xFFFFFFFF; return; } int32_t sbCount = at->scrollbackCount; if (sbCount == 0) { return; } int32_t maxScroll = sbCount; if (vy >= sbY && vy < sbY + arrowH) { at->scrollPos--; } else if (vy >= sbY + sbH - arrowH && vy < sbY + sbH) { at->scrollPos++; } else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { 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 + (at->scrollPos * thumbRange) / maxScroll; } if (vy < thumbY) { at->scrollPos -= rows; } else if (vy >= thumbY + thumbH) { at->scrollPos += rows; } } if (at->scrollPos < 0) { at->scrollPos = 0; } if (at->scrollPos > maxScroll) { at->scrollPos = maxScroll; } } void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; 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); ansiTermBuildPalette(w, d); const uint32_t *palette = at->packedPalette; int32_t cols = at->cols; int32_t rows = at->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 = at->scrollbackCount; bool viewingLive = (at->scrollPos == sbCount); uint32_t dirty = at->dirtyRows; if (dirty == 0) { dirty = 0xFFFFFFFF; } for (int32_t row = 0; row < rows; row++) { if (row < 32 && !(dirty & (1U << row))) { continue; } int32_t lineIndex = at->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); int32_t curCol = -1; if (viewingLive && at->cursorVisible && at->cursorOn && row == at->cursorRow) { curCol = at->cursorCol; } drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, at->blinkVisible, curCol); ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY); } at->dirtyRows = 0; // 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) { rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough); return; } int32_t trackY = sbY + arrowH; int32_t trackH = sbH - arrowH * 2; rectFill(d, ops, sbX, trackY, sbW, trackH, colors->scrollbarTrough); 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); 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); } 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); } 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 + (at->scrollPos * thumbRange) / maxScroll; } drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); } if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); } } void widgetAnsiTermPollVtable(WidgetT *w, WindowT *win) { (void)win; wgtAnsiTermPoll(w); } static bool widgetAnsiTermClearSelection(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (at->selStartLine >= 0) { at->selStartLine = -1; at->selStartCol = -1; at->selEndLine = -1; at->selEndCol = -1; at->selecting = false; at->dirtyRows = 0xFFFFFFFF; return true; } return false; } static void widgetAnsiTermOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; AnsiTermDataT *at = (AnsiTermDataT *)w->data; AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t innerX = w->x + ANSI_BORDER; int32_t innerY = w->y + ANSI_BORDER; int32_t col = (vx - innerX) / font->charWidth; int32_t row = (vy - innerY) / font->charHeight; if (col < 0) { col = 0; } if (col >= at->cols) { col = at->cols - 1; } if (row < 0) { row = 0; } if (row >= at->rows) { row = at->rows - 1; } int32_t lineIndex = at->scrollPos + row; at->selEndLine = lineIndex; at->selEndCol = col; at->dirtyRows = 0xFFFFFFFF; } static const WidgetClassT sClassAnsiTerm = { .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB, .paint = widgetAnsiTermPaint, .paintOverlay = NULL, .calcMinSize = widgetAnsiTermCalcMinSize, .layout = NULL, .onMouse = widgetAnsiTermOnMouse, .onKey = widgetAnsiTermOnKey, .destroy = widgetAnsiTermDestroy, .getText = NULL, .setText = NULL, .clearSelection = widgetAnsiTermClearSelection, .onDragUpdate = widgetAnsiTermOnDragUpdate, .poll = widgetAnsiTermPollVtable, .quickRepaint = wgtAnsiTermRepaint }; 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, sTypeId); if (!w) { return NULL; } AnsiTermDataT *at = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT)); if (!at) { free(w); return NULL; } w->data = at; int32_t cellCount = cols * rows; at->cells = (uint8_t *)malloc(cellCount * 2); if (!at->cells) { free(at); free(w); return NULL; } int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; at->scrollback = (uint8_t *)malloc(sbMax * cols * 2); if (!at->scrollback) { free(at->cells); free(at); free(w); return NULL; } at->cols = cols; at->rows = rows; at->cursorVisible = true; at->wrapMode = true; at->curAttr = ANSI_DEFAULT_ATTR; at->parseState = PARSE_NORMAL; at->scrollBot = rows - 1; at->scrollbackMax = sbMax; at->selStartLine = -1; at->selStartCol = -1; at->selEndLine = -1; at->selEndCol = -1; at->blinkVisible = true; at->blinkTime = clock(); at->cursorOn = true; at->cursorTime = clock(); at->dirtyRows = 0xFFFFFFFF; at->lastCursorRow = -1; at->lastCursorCol = -1; for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } return w; } void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { VALIDATE_WIDGET_VOID(w, sTypeId); AnsiTermDataT *at = (AnsiTermDataT *)w->data; at->commCtx = ctx; at->commRead = readFn; at->commWrite = writeFn; } void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { if (!w || w->type != sTypeId || maxLines <= 0) { return; } AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); if (!newBuf) { return; } free(at->scrollback); at->scrollback = newBuf; at->scrollbackMax = maxLines; at->scrollbackCount = 0; at->scrollbackHead = 0; at->scrollPos = 0; } static const struct { WidgetT *(*create)(WidgetT *parent, int32_t cols, int32_t rows); void (*write)(WidgetT *w, const uint8_t *data, int32_t len); void (*clear)(WidgetT *w); void (*setComm)(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)); void (*setScrollback)(WidgetT *w, int32_t maxLines); int32_t (*poll)(WidgetT *w); int32_t (*repaint)(WidgetT *w, int32_t *outY, int32_t *outH); } sApi = { .create = wgtAnsiTerm, .write = wgtAnsiTermWrite, .clear = wgtAnsiTermClear, .setComm = wgtAnsiTermSetComm, .setScrollback = wgtAnsiTermSetScrollback, .poll = wgtAnsiTermPoll, .repaint = wgtAnsiTermRepaint }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassAnsiTerm); wgtRegisterApi("ansiterm", &sApi); }