DVX_GUI/dvx/widgets/widgetAnsiTerm.c

1315 lines
39 KiB
C

// 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);
}
}