1907 lines
58 KiB
C
1907 lines
58 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
|
|
|
|
// Attribute byte: bit 7 = blink, bits 6-4 = bg (0-7), bits 3-0 = fg (0-15)
|
|
#define ATTR_BLINK_BIT 0x80
|
|
#define ATTR_BG_MASK 0x70
|
|
#define ATTR_FG_MASK 0x0F
|
|
|
|
// Blink/cursor rates in milliseconds
|
|
#define BLINK_MS 500
|
|
#define CURSOR_MS 250
|
|
|
|
// ============================================================
|
|
// 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 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);
|
|
|
|
|
|
// ============================================================
|
|
// 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++;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermBuildPalette
|
|
// ============================================================
|
|
//
|
|
// Build the packed 16-color palette and cache it in the widget.
|
|
// Only recomputed when paletteValid is false (first use or
|
|
// after a display format change).
|
|
|
|
static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) {
|
|
if (w->as.ansiTerm.paletteValid) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = 0; i < 16; i++) {
|
|
w->as.ansiTerm.packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]);
|
|
}
|
|
|
|
w->as.ansiTerm.paletteValid = true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermClearSelection
|
|
// ============================================================
|
|
|
|
static void ansiTermClearSelection(WidgetT *w) {
|
|
if (ansiTermHasSelection(w)) {
|
|
w->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
|
|
}
|
|
|
|
w->as.ansiTerm.selStartLine = -1;
|
|
w->as.ansiTerm.selStartCol = -1;
|
|
w->as.ansiTerm.selEndLine = -1;
|
|
w->as.ansiTerm.selEndCol = -1;
|
|
w->as.ansiTerm.selecting = false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermCopySelection
|
|
// ============================================================
|
|
|
|
static void ansiTermCopySelection(WidgetT *w) {
|
|
if (!ansiTermHasSelection(w)) {
|
|
return;
|
|
}
|
|
|
|
int32_t sLine;
|
|
int32_t sCol;
|
|
int32_t eLine;
|
|
int32_t eCol;
|
|
ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol);
|
|
|
|
int32_t cols = w->as.ansiTerm.cols;
|
|
|
|
// Build text from selected cells (strip trailing spaces per line)
|
|
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;
|
|
|
|
// Find last non-space character in this line's selection
|
|
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];
|
|
}
|
|
|
|
// Add newline between lines (not after last)
|
|
if (line < eLine && pos < 4095) {
|
|
buf[pos++] = '\n';
|
|
}
|
|
}
|
|
|
|
buf[pos] = '\0';
|
|
|
|
if (pos > 0) {
|
|
clipboardCopy(buf, pos);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 bot = w->as.ansiTerm.scrollBot;
|
|
int32_t row = w->as.ansiTerm.cursorRow;
|
|
|
|
if (count > bot - row + 1) {
|
|
count = bot - row + 1;
|
|
}
|
|
|
|
if (count <= 0) {
|
|
return;
|
|
}
|
|
|
|
uint8_t *cells = w->as.ansiTerm.cells;
|
|
int32_t bytesPerRow = cols * 2;
|
|
|
|
// Shift lines up within region
|
|
if (row + count <= bot) {
|
|
memmove(cells + row * bytesPerRow,
|
|
cells + (row + count) * bytesPerRow,
|
|
(bot - row - count + 1) * bytesPerRow);
|
|
}
|
|
|
|
// Clear the bottom lines of the region
|
|
for (int32_t r = bot - count + 1; r <= bot; r++) {
|
|
ansiTermFillCells(w, r * cols, cols);
|
|
}
|
|
|
|
// Dirty affected rows
|
|
for (int32_t r = row; r <= bot; 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 == 6) {
|
|
w->as.ansiTerm.originMode = true;
|
|
w->as.ansiTerm.cursorRow = w->as.ansiTerm.scrollTop;
|
|
w->as.ansiTerm.cursorCol = 0;
|
|
}
|
|
|
|
if (mode == 7) {
|
|
w->as.ansiTerm.wrapMode = true;
|
|
}
|
|
|
|
if (mode == 25) {
|
|
w->as.ansiTerm.cursorVisible = true;
|
|
}
|
|
} else if (cmd == 'l') {
|
|
if (mode == 6) {
|
|
w->as.ansiTerm.originMode = false;
|
|
w->as.ansiTerm.cursorRow = 0;
|
|
w->as.ansiTerm.cursorCol = 0;
|
|
}
|
|
|
|
if (mode == 7) {
|
|
w->as.ansiTerm.wrapMode = false;
|
|
}
|
|
|
|
if (mode == 25) {
|
|
w->as.ansiTerm.cursorVisible = false;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
switch (cmd) {
|
|
case '@': // ICH - insert character
|
|
{
|
|
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
|
|
int32_t cols = w->as.ansiTerm.cols;
|
|
int32_t row = w->as.ansiTerm.cursorRow;
|
|
int32_t col = w->as.ansiTerm.cursorCol;
|
|
|
|
if (count > cols - col) {
|
|
count = cols - col;
|
|
}
|
|
|
|
if (count > 0) {
|
|
uint8_t *base = w->as.ansiTerm.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] = w->as.ansiTerm.curAttr;
|
|
}
|
|
|
|
ansiTermDirtyRow(w, row);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'A': // CUU - cursor up
|
|
{
|
|
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
|
|
int32_t minRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollTop : 0;
|
|
w->as.ansiTerm.cursorRow -= count;
|
|
|
|
if (w->as.ansiTerm.cursorRow < minRow) {
|
|
w->as.ansiTerm.cursorRow = minRow;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'B': // CUD - cursor down
|
|
{
|
|
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
|
|
int32_t maxRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollBot : w->as.ansiTerm.rows - 1;
|
|
w->as.ansiTerm.cursorRow += count;
|
|
|
|
if (w->as.ansiTerm.cursorRow > maxRow) {
|
|
w->as.ansiTerm.cursorRow = maxRow;
|
|
}
|
|
|
|
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 'c': // DA - device attributes
|
|
{
|
|
if (w->as.ansiTerm.commWrite) {
|
|
// Respond as VT100 with advanced video option
|
|
const uint8_t reply[] = "\033[?1;2c";
|
|
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, reply, 7);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'E': // CNL - cursor next line
|
|
{
|
|
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
ansiTermNewline(w);
|
|
}
|
|
|
|
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;
|
|
|
|
// Origin mode: row is relative to scroll region
|
|
if (w->as.ansiTerm.originMode) {
|
|
row += w->as.ansiTerm.scrollTop;
|
|
|
|
if (row > w->as.ansiTerm.scrollBot) {
|
|
row = w->as.ansiTerm.scrollBot;
|
|
}
|
|
}
|
|
|
|
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 'P': // DCH - delete character
|
|
{
|
|
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
|
|
int32_t cols = w->as.ansiTerm.cols;
|
|
int32_t row = w->as.ansiTerm.cursorRow;
|
|
int32_t col = w->as.ansiTerm.cursorCol;
|
|
|
|
if (count > cols - col) {
|
|
count = cols - col;
|
|
}
|
|
|
|
if (count > 0) {
|
|
uint8_t *base = w->as.ansiTerm.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] = w->as.ansiTerm.curAttr;
|
|
}
|
|
|
|
ansiTermDirtyRow(w, row);
|
|
}
|
|
|
|
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 'Z': // CBT - back tab
|
|
{
|
|
int32_t col = w->as.ansiTerm.cursorCol;
|
|
|
|
if (col > 0) {
|
|
col = ((col - 1) / 8) * 8;
|
|
}
|
|
|
|
w->as.ansiTerm.cursorCol = col;
|
|
break;
|
|
}
|
|
|
|
case 'm': // SGR - select graphic rendition
|
|
ansiTermProcessSgr(w);
|
|
break;
|
|
|
|
case 'n': // DSR - device status report
|
|
{
|
|
int32_t mode = (n >= 1) ? p[0] : 0;
|
|
|
|
if (w->as.ansiTerm.commWrite) {
|
|
if (mode == 6) {
|
|
// CPR — cursor position report: ESC[row;colR (1-based)
|
|
char reply[16];
|
|
int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(w->as.ansiTerm.cursorRow + 1), (long)(w->as.ansiTerm.cursorCol + 1));
|
|
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, (const uint8_t *)reply, len);
|
|
} else if (mode == 255) {
|
|
// Screen size report
|
|
char reply[16];
|
|
int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm.rows, (long)w->as.ansiTerm.cols);
|
|
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, (const uint8_t *)reply, len);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'r': // DECSTBM - set scrolling region
|
|
{
|
|
int32_t rows = w->as.ansiTerm.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) {
|
|
w->as.ansiTerm.scrollTop = top;
|
|
w->as.ansiTerm.scrollBot = bot;
|
|
} else {
|
|
// Invalid or reset — restore full screen
|
|
w->as.ansiTerm.scrollTop = 0;
|
|
w->as.ansiTerm.scrollBot = rows - 1;
|
|
}
|
|
|
|
// Home cursor (relative to region if origin mode)
|
|
w->as.ansiTerm.cursorRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollTop : 0;
|
|
w->as.ansiTerm.cursorCol = 0;
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermHasSelection
|
|
// ============================================================
|
|
|
|
static bool ansiTermHasSelection(const WidgetT *w) {
|
|
if (w->as.ansiTerm.selStartLine < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (w->as.ansiTerm.selStartLine == w->as.ansiTerm.selEndLine &&
|
|
w->as.ansiTerm.selStartCol == w->as.ansiTerm.selEndCol) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermInsertLines
|
|
// ============================================================
|
|
|
|
static void ansiTermInsertLines(WidgetT *w, int32_t count) {
|
|
int32_t cols = w->as.ansiTerm.cols;
|
|
int32_t bot = w->as.ansiTerm.scrollBot;
|
|
int32_t row = w->as.ansiTerm.cursorRow;
|
|
|
|
if (count > bot - row + 1) {
|
|
count = bot - row + 1;
|
|
}
|
|
|
|
if (count <= 0) {
|
|
return;
|
|
}
|
|
|
|
uint8_t *cells = w->as.ansiTerm.cells;
|
|
int32_t bytesPerRow = cols * 2;
|
|
|
|
// Shift lines down within region
|
|
if (row + count <= bot) {
|
|
memmove(cells + (row + count) * bytesPerRow,
|
|
cells + row * bytesPerRow,
|
|
(bot - row - count + 1) * bytesPerRow);
|
|
}
|
|
|
|
// Clear the inserted lines
|
|
for (int32_t r = row; r < row + count; r++) {
|
|
ansiTermFillCells(w, r * cols, cols);
|
|
}
|
|
|
|
// Dirty affected rows
|
|
for (int32_t r = row; r <= bot; r++) {
|
|
ansiTermDirtyRow(w, r);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermNewline
|
|
// ============================================================
|
|
//
|
|
// Move cursor to next line, scrolling if at the bottom.
|
|
|
|
static void ansiTermNewline(WidgetT *w) {
|
|
int32_t bot = w->as.ansiTerm.scrollBot;
|
|
|
|
if (w->as.ansiTerm.cursorRow == bot) {
|
|
ansiTermScrollUp(w);
|
|
} else if (w->as.ansiTerm.cursorRow < w->as.ansiTerm.rows - 1) {
|
|
w->as.ansiTerm.cursorRow++;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 == '\f') {
|
|
// Form feed — clear screen and home cursor
|
|
ansiTermEraseDisplay(w, 2);
|
|
w->as.ansiTerm.cursorRow = 0;
|
|
w->as.ansiTerm.cursorCol = 0;
|
|
} else if (ch == '\a') {
|
|
// Bell — ignored
|
|
} else {
|
|
// CP437 graphic characters (smileys, card suits, etc.)
|
|
// and all printable characters
|
|
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 if (ch == 'D') {
|
|
// IND — scroll up one line
|
|
ansiTermScrollUp(w);
|
|
w->as.ansiTerm.parseState = PARSE_NORMAL;
|
|
} else if (ch == 'M') {
|
|
// RI — scroll down one line
|
|
ansiTermScrollDown(w);
|
|
w->as.ansiTerm.parseState = PARSE_NORMAL;
|
|
} else if (ch == 'c') {
|
|
// RIS — terminal reset
|
|
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.wrapMode = true;
|
|
w->as.ansiTerm.originMode = false;
|
|
w->as.ansiTerm.cursorVisible = true;
|
|
w->as.ansiTerm.scrollTop = 0;
|
|
w->as.ansiTerm.scrollBot = w->as.ansiTerm.rows - 1;
|
|
ansiTermEraseDisplay(w, 2);
|
|
w->as.ansiTerm.parseState = PARSE_NORMAL;
|
|
} 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 — sets bit 7 of attr byte via bg bit 3
|
|
bg |= 8;
|
|
} else if (code == 25) {
|
|
// Blink off
|
|
bg &= 7;
|
|
} else if (code == 7) {
|
|
// Reverse video
|
|
uint8_t tmp = fg;
|
|
fg = bg;
|
|
bg = tmp;
|
|
} else if (code == 8) {
|
|
// Invisible — foreground same as background
|
|
fg = bg & 0x07;
|
|
} 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] | (bg & 8));
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermPasteToComm
|
|
// ============================================================
|
|
|
|
static void ansiTermPasteToComm(WidgetT *w) {
|
|
if (!w->as.ansiTerm.commWrite) {
|
|
return;
|
|
}
|
|
|
|
int32_t clipLen;
|
|
const char *clip = clipboardGet(&clipLen);
|
|
|
|
if (clipLen <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Transmit clipboard contents, converting \n to \r for the terminal
|
|
for (int32_t i = 0; i < clipLen; i++) {
|
|
uint8_t ch = (uint8_t)clip[i];
|
|
|
|
if (ch == '\n') {
|
|
ch = '\r';
|
|
}
|
|
|
|
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, &ch, 1);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 top = w->as.ansiTerm.scrollTop;
|
|
int32_t bot = w->as.ansiTerm.scrollBot;
|
|
uint8_t *cells = w->as.ansiTerm.cells;
|
|
int32_t bytesPerRow = cols * 2;
|
|
|
|
// Shift lines within region down by one
|
|
if (bot > top) {
|
|
memmove(cells + (top + 1) * bytesPerRow,
|
|
cells + top * bytesPerRow,
|
|
(bot - top) * bytesPerRow);
|
|
}
|
|
|
|
// Clear the top line of the region
|
|
ansiTermFillCells(w, top * cols, cols);
|
|
|
|
// Dirty affected rows
|
|
for (int32_t r = top; r <= bot && r < 32; r++) {
|
|
w->as.ansiTerm.dirtyRows |= (1U << r);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 top = w->as.ansiTerm.scrollTop;
|
|
int32_t bot = w->as.ansiTerm.scrollBot;
|
|
uint8_t *cells = w->as.ansiTerm.cells;
|
|
int32_t bytesPerRow = cols * 2;
|
|
|
|
// Only push to scrollback when scrolling the full screen
|
|
if (top == 0 && bot == w->as.ansiTerm.rows - 1) {
|
|
bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount);
|
|
ansiTermAddToScrollback(w, 0);
|
|
|
|
if (wasAtBottom) {
|
|
w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount;
|
|
}
|
|
}
|
|
|
|
// Shift lines within region up by one
|
|
if (bot > top) {
|
|
memmove(cells + top * bytesPerRow,
|
|
cells + (top + 1) * bytesPerRow,
|
|
(bot - top) * bytesPerRow);
|
|
}
|
|
|
|
// Clear the bottom line of the region
|
|
ansiTermFillCells(w, bot * cols, cols);
|
|
|
|
// Dirty affected rows
|
|
for (int32_t r = top; r <= bot && r < 32; r++) {
|
|
w->as.ansiTerm.dirtyRows |= (1U << r);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermSelectionRange
|
|
// ============================================================
|
|
//
|
|
// Return selection start/end in normalized order (start <= end).
|
|
|
|
static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol) {
|
|
int32_t sl = w->as.ansiTerm.selStartLine;
|
|
int32_t sc = w->as.ansiTerm.selStartCol;
|
|
int32_t el = w->as.ansiTerm.selEndLine;
|
|
int32_t ec = w->as.ansiTerm.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;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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.originMode = 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.scrollTop = 0;
|
|
w->as.ansiTerm.scrollBot = rows - 1;
|
|
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;
|
|
w->as.ansiTerm.selStartLine = -1;
|
|
w->as.ansiTerm.selStartCol = -1;
|
|
w->as.ansiTerm.selEndLine = -1;
|
|
w->as.ansiTerm.selEndCol = -1;
|
|
w->as.ansiTerm.selecting = false;
|
|
w->as.ansiTerm.blinkVisible = true;
|
|
w->as.ansiTerm.blinkTime = clock();
|
|
w->as.ansiTerm.cursorOn = true;
|
|
w->as.ansiTerm.cursorTime = clock();
|
|
w->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
|
|
w->as.ansiTerm.lastCursorRow = -1;
|
|
w->as.ansiTerm.lastCursorCol = -1;
|
|
|
|
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) {
|
|
return 0;
|
|
}
|
|
|
|
// Text blink timer — toggle visibility and dirty rows with blinking cells
|
|
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 - w->as.ansiTerm.blinkTime) >= blinkInterval) {
|
|
w->as.ansiTerm.blinkTime = now;
|
|
w->as.ansiTerm.blinkVisible = !w->as.ansiTerm.blinkVisible;
|
|
|
|
// Dirty any rows that contain cells with blink attribute
|
|
int32_t cols = w->as.ansiTerm.cols;
|
|
int32_t rows = w->as.ansiTerm.rows;
|
|
|
|
for (int32_t row = 0; row < rows && row < 32; row++) {
|
|
for (int32_t col = 0; col < cols; col++) {
|
|
uint8_t attr = w->as.ansiTerm.cells[(row * cols + col) * 2 + 1];
|
|
|
|
if (attr & ATTR_BLINK_BIT) {
|
|
w->as.ansiTerm.dirtyRows |= (1U << row);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cursor blink timer
|
|
if ((now - w->as.ansiTerm.cursorTime) >= curInterval) {
|
|
w->as.ansiTerm.cursorTime = now;
|
|
w->as.ansiTerm.cursorOn = !w->as.ansiTerm.cursorOn;
|
|
|
|
int32_t cRow = w->as.ansiTerm.cursorRow;
|
|
|
|
if (cRow >= 0 && cRow < 32) {
|
|
w->as.ansiTerm.dirtyRows |= (1U << cRow);
|
|
}
|
|
}
|
|
|
|
// Read from comm
|
|
if (!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, int32_t *outY, int32_t *outH) {
|
|
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 cellH = font->charHeight;
|
|
int32_t baseX = w->x + ANSI_BORDER;
|
|
int32_t baseY = w->y + ANSI_BORDER;
|
|
|
|
// Use cached palette
|
|
ansiTermBuildPalette(w, &cd);
|
|
const uint32_t *palette = w->as.ansiTerm.packedPalette;
|
|
|
|
bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.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 = w->as.ansiTerm.scrollPos + row;
|
|
const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
|
|
|
|
// Cursor column for this row (-1 if cursor not on this row)
|
|
int32_t curCol2 = -1;
|
|
if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn &&
|
|
row == w->as.ansiTerm.cursorRow) {
|
|
curCol2 = w->as.ansiTerm.cursorCol;
|
|
}
|
|
|
|
drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol2);
|
|
ansiTermPaintSelRow(w, &cd, ops, font, row, baseX, baseY);
|
|
|
|
if (row < minRow) { minRow = row; }
|
|
if (row > maxRow) { maxRow = row; }
|
|
repainted++;
|
|
}
|
|
|
|
w->as.ansiTerm.dirtyRows = 0;
|
|
w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow;
|
|
w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol;
|
|
|
|
if (outY) { *outY = baseY + minRow * cellH; }
|
|
if (outH) { *outH = (maxRow - minRow + 1) * cellH; }
|
|
|
|
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;
|
|
}
|
|
|
|
ansiTermClearSelection(w);
|
|
|
|
for (int32_t i = 0; i < len; i++) {
|
|
ansiTermProcessByte(w, data[i]);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetAnsiTermDestroy
|
|
// ============================================================
|
|
|
|
void widgetAnsiTermDestroy(WidgetT *w) {
|
|
free(w->as.ansiTerm.cells);
|
|
free(w->as.ansiTerm.scrollback);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ansiTermPaintSelRow
|
|
// ============================================================
|
|
//
|
|
// Overlay selection highlighting for a single terminal row.
|
|
// Inverts fg/bg for selected cells.
|
|
|
|
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;
|
|
}
|
|
|
|
int32_t sLine;
|
|
int32_t sCol;
|
|
int32_t eLine;
|
|
int32_t eCol;
|
|
ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol);
|
|
|
|
int32_t cols = w->as.ansiTerm.cols;
|
|
int32_t lineIndex = w->as.ansiTerm.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 = w->as.ansiTerm.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]; // swap: bg becomes fg
|
|
uint32_t bg = palette[attr & 0x0F]; // swap: fg becomes bg
|
|
|
|
drawChar(d, ops, font, baseX + col * cellW, baseY + screenRow * cellH, ch, fg, bg, true);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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, int32_t mod) {
|
|
// Ctrl+C: copy if selection exists, otherwise send ^C
|
|
if (key == 0x03 && (mod & KEY_MOD_CTRL)) {
|
|
if (ansiTermHasSelection(w)) {
|
|
ansiTermCopySelection(w);
|
|
ansiTermClearSelection(w);
|
|
wgtInvalidate(w);
|
|
return;
|
|
}
|
|
|
|
// No selection — fall through to send ^C to terminal
|
|
}
|
|
|
|
// Ctrl+V: paste from clipboard to terminal
|
|
if (key == 0x16 && (mod & KEY_MOD_CTRL)) {
|
|
ansiTermPasteToComm(w);
|
|
wgtInvalidate(w);
|
|
return;
|
|
}
|
|
|
|
if (!w->as.ansiTerm.commWrite) {
|
|
return;
|
|
}
|
|
|
|
// Any keypress clears selection
|
|
if (ansiTermHasSelection(w)) {
|
|
ansiTermClearSelection(w);
|
|
}
|
|
|
|
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 == 0x1B) {
|
|
// Escape
|
|
buf[0] = 0x1B;
|
|
len = 1;
|
|
} else if (key == 0x09) {
|
|
// Tab
|
|
buf[0] = 0x09;
|
|
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;
|
|
} else if (key >= 1 && key < 32) {
|
|
// Control characters (^A=1, ^B=2, ^C=3, etc.)
|
|
buf[0] = (uint8_t)key;
|
|
len = 1;
|
|
}
|
|
|
|
if (len > 0) {
|
|
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len);
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetAnsiTermOnMouse
|
|
// ============================================================
|
|
//
|
|
// Handle mouse clicks: scrollbar interaction and focus.
|
|
|
|
void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
AppContextT *actx = (AppContextT *)root->userData;
|
|
const BitmapFontT *font = &actx->font;
|
|
hit->focused = true;
|
|
clearOtherSelections(hit);
|
|
|
|
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;
|
|
|
|
// Click in text area — start selection
|
|
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 = hit->as.ansiTerm.scrollPos + clickRow;
|
|
|
|
int32_t clicks = multiClickDetect(vx, vy);
|
|
|
|
if (clicks >= 3) {
|
|
// Triple-click: select entire line
|
|
hit->as.ansiTerm.selStartLine = lineIndex;
|
|
hit->as.ansiTerm.selStartCol = 0;
|
|
hit->as.ansiTerm.selEndLine = lineIndex;
|
|
hit->as.ansiTerm.selEndCol = cols;
|
|
hit->as.ansiTerm.selecting = false;
|
|
sDragTextSelect = NULL;
|
|
} else if (clicks == 2) {
|
|
// Double-click: select word
|
|
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++;
|
|
}
|
|
|
|
hit->as.ansiTerm.selStartLine = lineIndex;
|
|
hit->as.ansiTerm.selStartCol = ws;
|
|
hit->as.ansiTerm.selEndLine = lineIndex;
|
|
hit->as.ansiTerm.selEndCol = we;
|
|
hit->as.ansiTerm.selecting = false;
|
|
sDragTextSelect = NULL;
|
|
} else {
|
|
// Single click: start selection anchor
|
|
hit->as.ansiTerm.selStartLine = lineIndex;
|
|
hit->as.ansiTerm.selStartCol = clickCol;
|
|
hit->as.ansiTerm.selEndLine = lineIndex;
|
|
hit->as.ansiTerm.selEndCol = clickCol;
|
|
hit->as.ansiTerm.selecting = true;
|
|
sDragTextSelect = hit;
|
|
}
|
|
|
|
hit->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
|
|
return;
|
|
}
|
|
|
|
int32_t sbCount = hit->as.ansiTerm.scrollbackCount;
|
|
|
|
if (sbCount == 0) {
|
|
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/cache the 16-color packed palette
|
|
ansiTermBuildPalette(w, d);
|
|
const uint32_t *palette = w->as.ansiTerm.packedPalette;
|
|
|
|
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 row by row using bulk renderer
|
|
for (int32_t row = 0; row < rows; row++) {
|
|
int32_t lineIndex = w->as.ansiTerm.scrollPos + row;
|
|
const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
|
|
|
|
// Cursor column for this row (-1 if cursor not on this row)
|
|
int32_t curCol = -1;
|
|
if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn &&
|
|
row == w->as.ansiTerm.cursorRow) {
|
|
curCol = w->as.ansiTerm.cursorCol;
|
|
}
|
|
|
|
drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol);
|
|
ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
if (w->focused) {
|
|
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg);
|
|
}
|
|
}
|