kpmpgsmkii/client/src/gui/terminal.c
2022-03-05 19:06:31 -06:00

734 lines
20 KiB
C

/*
* Kangaroo Punch MultiPlayer Game Server Mark II
* Copyright (C) 2020-2021 Scott Duensing
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
// http://www.ansi-bbs.org/BANSI/bansi.txt
// http://ansi-bbs.org/ansi-bbs-core-server.html
#include "terminal.h"
//#define termWrite(...) logWrite(__VA_ARGS__)
#define termWrite(...) while(0){}
#define TERMINAL_CELL_GET_FLAG(t,x,y,f) (((t)->cells[(x)][(y)].flags & (1 << (f))) != 0)
#define TERMINAL_CELL_SET_FLAG(t,x,y,f) ((t)->cells[(x)][(y)].flags |= (1 << (f)))
#define TERMINAL_CELL_CLEAR_FLAG(t,x,y,f) ((t)->cells[(x)][(y)].flags &= (~(1 << (f))))
static void terminalDel(WidgetT **widget);
static void terminalFocusEvent(WidgetT *widget, uint8_t focused);
static int16_t terminalIntConvert(char *number, int16_t defaultValue);
static void terminalMouseEvent(WidgetT *widget, MouseT *mouse, uint16_t x, uint16_t y, uint8_t event);
static void terminalKeyboardEvent(WidgetT *widget, uint8_t ascii, uint8_t extended, uint8_t scancode, uint8_t shift, uint8_t control, uint8_t alt);
static void terminalPaint(WidgetT *widget, uint8_t enabled, RectT pos);
static void terminalSequenceReset(TerminalT *terminal);
void terminalBackgroundSet(TerminalT *terminal, uint8_t color) {
terminal->background = color;
}
void terminalCharPrint(TerminalT *terminal, uint8_t c) {
int16_t x;
int16_t y;
uint16_t p;
// Find leading ESC.
if (c == 27 && !terminal->escFound && !terminal->inEscape) {
terminal->escFound = 1;
return;
}
// Find [.
if (c == '[' && terminal->escFound && !terminal->inEscape) {
terminal->inEscape = 1;
return;
}
// Is this a sequence that is ESC and then not [ ?
if (terminal->escFound && !terminal->inEscape) {
termWrite("Unexpected character: '%c' %d\n", c, c);
terminalSequenceReset(terminal);
return;
}
if (terminal->inEscape) {
switch (c) {
case '[':
case 27:
termWrite("Escape found inside sequence\n");
// ESC sequence inside sequence. Invalid ANSIBBS.
//***TODO*** Can stick custom sequences here.
break;
// End of a parameter
case ';':
arrput(terminal->parameters, strdup(terminal->number));
terminal->number[0] = 0;
terminal->numberIndex = 0;
break;
// Cursor up.
case 'A':
y = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor up %d\n", y);
terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY - y);
terminalSequenceReset(terminal);
break;
// Cursor down.
case 'B':
y = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor down %d\n", y);
terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY + y);
terminalSequenceReset(terminal);
break;
// Cursor forward.
case 'C':
x = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor forward %d\n", x);
terminalCursorMove(terminal, terminal->cursorX + x, terminal->cursorY);
terminalSequenceReset(terminal);
break;
// Cursor backward.
case 'D':
x = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor backward %d\n", x);
terminalCursorMove(terminal, terminal->cursorX - x, terminal->cursorY);
terminalSequenceReset(terminal);
break;
// Cursor line down.
case 'E':
y = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor down %d lines\n", y);
terminalCursorMove(terminal, 1, terminal->cursorY + y);
//***TODO*** This should allow scrolling.
terminalSequenceReset(terminal);
break;
// Cursor line up.
case 'F':
y = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor up %d lines\n", y);
terminalCursorMove(terminal, 1, terminal->cursorY - y);
//***TODO*** This should allow scrolling.
terminalSequenceReset(terminal);
break;
// Cursor horizontal absolute.
case 'G':
x = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor horizontal absolute %d\n", x);
terminalCursorMove(terminal, x, terminal->cursorY);
terminalSequenceReset(terminal);
break;
// Cursor move.
case 'H':
case 'f':
switch (arrlen(terminal->parameters)) {
// Cursor vertical absolute. Kinda. Moves X to 1.
case 0:
y = terminalIntConvert(terminal->number, 1);
termWrite("Moving cursor kinda vertictal absolute %d\n", y);
terminalCursorMove(terminal, 1, y);
break;
// Absolute position.
case 1:
x = terminalIntConvert(terminal->number, 1);
y = terminalIntConvert(terminal->parameters[0], 1);
termWrite("Moving cursor absolute %d %d\n", x, y);
terminalCursorMove(terminal, x, y);
break;
// Invalid nonsense.
default:
termWrite("Unknown cursor move: '%c' %d\n", c, c);
break;
}
terminalSequenceReset(terminal);
break;
// Clear display.
case 'J':
if (arrlen(terminal->parameters) == 0 && terminalIntConvert(terminal->number, 1) == 2) {
termWrite("Clear display\n");
terminalScreenClear(terminal);
terminalCursorMove(terminal, 1, 1);
} else {
//***TODO*** Unimplemented screen clear.
termWrite("Unimplemented clear display\n");
}
terminalSequenceReset(terminal);
break;
// Clear from cursor to end of line.
case 'K':
x = terminalIntConvert(terminal->number, 0);
if (x == 0) {
termWrite("Clear to end of line\n");
for (y=terminal->cursorX-1; y<terminal->cols; y++) {
terminal->cells[y][terminal->cursorY - 1].character = ' ';
TERMINAL_CELL_SET_FLAG(terminal, y, terminal->cursorY - 1, TERMINAL_FLAG_DIRTY);
GUI_SET_FLAG((WidgetT *)terminal, WIDGET_FLAG_DIRTY);
}
} else {
//***TODO*** Unimplemented line clear.
termWrite("Unimplemented line clear\n");
}
terminalSequenceReset(terminal);
break;
// Insert lines.
case 'L':
//***TODO*** Unimplemented.
termWrite("Unimplemented insert lines\n");
terminalSequenceReset(terminal);
break;
// Delete lines.
case 'M':
//***TODO*** Unimplemented.
termWrite("Unimplemented delete lines\n");
terminalSequenceReset(terminal);
break;
// Delete characters.
case 'P':
//***TODO*** Unimplemented.
termWrite("Unimplemented delete characters\n");
terminalSequenceReset(terminal);
break;
// Scroll up.
case 'S':
//***TODO*** Unimplemented.
termWrite("Unimplemented scroll up\n");
terminalSequenceReset(terminal);
break;
// Scroll down.
case 'T':
//***TODO*** Unimplemented.
termWrite("Unimplemented scroll down\n");
terminalSequenceReset(terminal);
break;
// Clear screen with normal attribute.
case 'U':
termWrite("Clear screen with normal attribute\n");
x = terminal->background;
terminal->background = TERMINAL_COLOR_BLACK;
terminalScreenClear(terminal);
terminal->background = x;
terminalCursorMove(terminal, 1, 1);
terminalSequenceReset(terminal);
break;
// Back-TAB.
case 'Z':
//***TODO*** Unimplemented.
termWrite("Unimplemented back-TAB\n");
terminalSequenceReset(terminal);
break;
// Set attributes.
case 'm':
arrput(terminal->parameters, strdup(terminal->number));
for (p=0; p<arrlen(terminal->parameters); p++) {
x = terminalIntConvert(terminal->parameters[p], 0);
switch (x) {
// Reset.
case 0:
termWrite("Attribute reset\n");
terminal->bold = 0;
terminal->blink = 0;
terminal->reverse = 0;
terminal->foreground = TERMINAL_COLOR_LIGHT_GRAY;
terminal->background = TERMINAL_COLOR_BLACK;
break;
// Bold.
case 1:
termWrite("Bold\n");
terminal->bold = 1;
break;
// Blink slow.
case 5:
termWrite("Blink slow\n");
terminal->blink = TERMINAL_FLAG_BLINK_SLOW;
break;
// Blink fast.
case 6:
termWrite("Blink fast\n");
terminal->blink = TERMINAL_FLAG_BLINK_FAST;
break;
// Reverse.
case 7:
if (!terminal->reverse) {
termWrite("Reverse\n");
x = terminal->foreground;
terminal->foreground = terminal->background;
terminal->background = x;
terminal->reverse = 1;
} else {
termWrite("Already reversed\n");
}
break;
// Normal intensity. (22 is the actual code, unsure exactly what 21 is.)
case 21:
case 22:
termWrite("Normal intensity\n");
terminal->bold = 0;
break;
// Steady.
case 25:
termWrite("Steady\n");
terminal->blink = 0;
break;
// Normal
case 27:
if (terminal->reverse) {
termWrite("Normal\n");
x = terminal->foreground;
terminal->foreground = terminal->background;
terminal->background = x;
terminal->reverse = 0;
} else {
termWrite("Already normal\n");
}
break;
// Color change.
default:
if (x > 29 && x < 38) {
terminal->foreground = x - 30;
termWrite("Foreground %d\n", terminal->foreground);
}
if (x > 39 && x < 48) {
terminal->background = x - 40;
termWrite("Background %d\n", terminal->background);
}
break;
} // switch (x)
}
terminalSequenceReset(terminal);
break;
// Define scroll region.
case 'r':
//***TODO*** Unimplemented.
termWrite("Unimplemented define scroll region\n");
terminalSequenceReset(terminal);
break;
// Cursor save.
case 's':
terminal->saveX = terminal->cursorX;
terminal->saveY = terminal->cursorY;
termWrite("Cursor save %d %d\n", terminal->saveX, terminal->saveX);
terminalSequenceReset(terminal);
break;
// Cursor restore.
case 'u':
termWrite("Cursor restore %d %d\n", terminal->saveX, terminal->saveX);
terminal->cursorX = terminal->saveX;
terminal->cursorY = terminal->saveY;
terminalSequenceReset(terminal);
break;
// Something else.
default:
// Number?
if (c >= '0' && c <= '9') {
terminal->number[terminal->numberIndex++] = c;
terminal->number[terminal->numberIndex] = 0;
} else {
// Unknown sequence.
termWrite("Unknown sequence: '%c' %d\n", c, c);
terminalSequenceReset(terminal);
}
} // switch (c)
} else { // inEscape
switch (c) {
// Bell.
case 7:
//***TODO*** Unimplemented.
termWrite("Unimplemented bell\n");
break;
// Backspace.
case 8:
case 128:
//***TODO*** Unimplemented.
termWrite("Unimplemented backspace\n");
break;
// TAB
case 9:
//***TODO*** Unimplemented.
termWrite("Unimplemented TAB\n");
break;
// Line feed.
case 10:
terminal->cursorY++;
termWrite("Line feed\n");
if (terminal->cursorY > terminal->rows) {
terminal->cursorY--;
//***TODO*** Scroll.
termWrite("Unimplemented line feed scroll\n");
}
terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY);
break;
// Clear screen (form feed).
case 12:
termWrite("Form feed\n");
x = terminal->background;
terminal->background = TERMINAL_COLOR_BLACK;
terminalScreenClear(terminal);
terminal->background = x;
terminalCursorMove(terminal, 1, 1);
break;
// Carrage return.
case 13:
termWrite("Carrage return\n");
terminalCursorMove(terminal, 1, terminal->cursorY);
break;
// Non-special character.
default:
// Render character.
terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].character = c;
terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].background = terminal->background;
terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].foreground = terminal->foreground + (terminal->bold ? 8 : 0);
terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].flags = 0;
if (terminal->blink == TERMINAL_FLAG_BLINK_SLOW) TERMINAL_CELL_SET_FLAG(terminal, terminal->cursorX - 1, terminal->cursorY - 1, TERMINAL_FLAG_BLINK_SLOW);
if (terminal->blink == TERMINAL_FLAG_BLINK_FAST) TERMINAL_CELL_SET_FLAG(terminal, terminal->cursorX - 1, terminal->cursorY - 1, TERMINAL_FLAG_BLINK_FAST);
TERMINAL_CELL_SET_FLAG(terminal, terminal->cursorX - 1, terminal->cursorY - 1, TERMINAL_FLAG_DIRTY);
GUI_SET_FLAG((WidgetT *)terminal, WIDGET_FLAG_DIRTY);
// Move cursor.
terminal->cursorX++;
if (terminal->cursorX > terminal->cols) {
terminal->cursorX = 1;
terminal->cursorY++;
if (terminal->cursorY > terminal->rows) {
terminal->cursorY--;
//***TODO*** Scroll.
termWrite("Unimplemented render scroll\n");
}
}
terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY);
break;
} // switch (c)
} // inEscape
}
void terminalCursorMove(TerminalT *terminal, uint16_t col, uint16_t row) {
// Clamp X.
if (col < 1) {
terminal->cursorX = 1;
} else {
if (col > terminal->cols) {
terminal->cursorX = terminal->cols;
} else {
terminal->cursorX = col;
}
}
// Clamp Y.
if (row < 1) {
terminal->cursorY = 1;
} else {
if (row > terminal->rows) {
terminal->cursorY = terminal->rows;
} else {
terminal->cursorY = row;
}
}
}
static void terminalDel(WidgetT **widget) {
TerminalT *t = (TerminalT *)*widget;
uint16_t x;
terminalSequenceReset(t);
for (x=0; x<t->cols; x++) {
free(t->cells[x]);
}
free(t->cells);
}
static void terminalFocusEvent(WidgetT *widget, uint8_t focused) {
TerminalT *t = (TerminalT *)widget;
uint16_t x;
uint16_t y;
(void)focused;
// When focus changes, we need to mark ALL the cells dirty so they get redrawn when the window is redrawn.
for (y=0; y<t->rows; y++) {
for (x=0; x<t->cols; x++) {
TERMINAL_CELL_SET_FLAG(t, x, y, TERMINAL_FLAG_DIRTY);
}
}
GUI_SET_FLAG(widget, WIDGET_FLAG_DIRTY);
}
void terminalForegroundSet(TerminalT *terminal, uint8_t color) {
terminal->foreground = color;
terminal->bold = (color > 7);
}
WidgetT *terminalInit(WidgetT *widget, uint16_t cols, uint16_t rows) {
TerminalT *t = (TerminalT *)widget;
uint16_t x;
uint16_t y;
t->base.delMethod = terminalDel;
t->base.focusMethod = terminalFocusEvent;
t->base.paintMethod = terminalPaint;
t->base.keyboardEventMethod = terminalKeyboardEvent;
t->base.mouseEventMethod = terminalMouseEvent;
t->font = __guiFont; // Default font. Also see terminalNew.
t->cols = cols;
t->rows = rows;
t->cursorX = 1;
t->cursorY = 1;
t->saveX = 1;
t->saveY = 1;
t->bold = 0;
t->blink = 0;
t->reverse = 0;
t->background = TERMINAL_COLOR_BLACK;
t->foreground = TERMINAL_COLOR_LIGHT_GRAY;
t->escFound = 0;
t->inEscape = 0;
t->numberIndex = 0;
t->number[0] = 0;
t->parameters = NULL;
// Allocate space for column data.
t->cells = (CellT **)malloc(sizeof(CellT *) * cols);
if (!t->cells) return NULL;
// Allocate space for row data.
for (x=0; x<cols; x++) {
t->cells[x] = (CellT *)malloc(sizeof(CellT) * rows);
if (!t->cells[x]) {
for (y=0; y<x; y++) {
free(t->cells[y]);
}
free(t->cells);
return NULL;
}
}
// Set up default palette.
t->palette[TERMINAL_COLOR_BLACK] = vbeColorMake( 0, 0, 0);
t->palette[TERMINAL_COLOR_BLUE] = vbeColorMake(170, 0, 0);
t->palette[TERMINAL_COLOR_GREEN] = vbeColorMake( 0, 170, 0);
t->palette[TERMINAL_COLOR_CYAN] = vbeColorMake(170, 85, 0);
t->palette[TERMINAL_COLOR_RED] = vbeColorMake( 0, 0, 170);
t->palette[TERMINAL_COLOR_MAGENTA] = vbeColorMake(170, 0, 170);
t->palette[TERMINAL_COLOR_BROWN] = vbeColorMake( 0, 170, 170);
t->palette[TERMINAL_COLOR_LIGHT_GRAY] = vbeColorMake(170, 170, 170);
t->palette[TERMINAL_COLOR_DARK_GRAY] = vbeColorMake( 85, 85, 85);
t->palette[TERMINAL_COLOR_BRIGHT_BLUE] = vbeColorMake(255, 85, 85);
t->palette[TERMINAL_COLOR_BRIGHT_GREEN] = vbeColorMake( 85, 255, 85);
t->palette[TERMINAL_COLOR_BRIGHT_CYAN] = vbeColorMake(255, 255, 85);
t->palette[TERMINAL_COLOR_BRIGHT_RED] = vbeColorMake( 85, 85, 255);
t->palette[TERMINAL_COLOR_BRIGHT_MAGENTA] = vbeColorMake(255, 85, 255);
t->palette[TERMINAL_COLOR_YELLOW] = vbeColorMake( 85, 255, 255);
t->palette[TERMINAL_COLOR_WHITE] = vbeColorMake(255, 255, 255);
// Default attributes is gray on black, no bold, no blink, and dirty.
for (y=0; y<rows; y++) {
for (x=0; x<cols; x++) {
t->cells[x][y].character = ' ';
t->cells[x][y].background = TERMINAL_COLOR_BLACK;
t->cells[x][y].foreground = TERMINAL_COLOR_LIGHT_GRAY;
t->cells[x][y].flags = 0;
TERMINAL_CELL_SET_FLAG(t, x, y, TERMINAL_FLAG_DIRTY);
}
}
return widget;
}
static int16_t terminalIntConvert(char *number, int16_t defaultValue) {
char *end = NULL;
int16_t result = (int16_t)strtol(number, &end, 10);
if (end == NULL || number[0] == 0) result = defaultValue;
return result;
}
static void terminalKeyboardEvent(WidgetT *widget, uint8_t ascii, uint8_t extended, uint8_t scancode, uint8_t shift, uint8_t control, uint8_t alt) {
}
static void terminalMouseEvent(WidgetT *widget, MouseT *mouse, uint16_t x, uint16_t y, uint8_t event) {
TerminalT *t = (TerminalT *)widget;
(void)mouse;
// Fire callback on mouse up.
if (event == MOUSE_EVENT_LEFT_UP) {
}
}
TerminalT *terminalNew(uint16_t x, uint16_t y, uint16_t cols, uint16_t rows) {
TerminalT *terminal = (TerminalT *)malloc(sizeof(TerminalT));
WidgetT *widget = NULL;
uint16_t w;
uint16_t h;
if (!terminal) return NULL;
// Default font. Also see terminalInit.
w = fontWidthGet(__guiFont) * cols;
h = fontHeightGet(__guiFont) * rows;
widget = widgetInit(W(terminal), MAGIC_TERMINAL, x, y, w, h, 0, 0, 0, 0);
if (!widget) {
free(terminal);
return NULL;
}
terminal = (TerminalT *)terminalInit((WidgetT *)terminal, cols, rows);
if (!terminal) {
free(terminal);
return NULL;
}
return terminal;
}
static void terminalPaint(WidgetT *widget, uint8_t enabled, RectT pos) {
TerminalT *t = (TerminalT *)widget;
uint16_t x;
uint16_t y;
uint16_t xp;
uint16_t yp;
char c[2];
c[1] = 0;
yp = pos.y;
for (y=0; y<t->rows; y++) {
xp = pos.x;
for (x=0; x<t->cols; x++) {
if (TERMINAL_CELL_GET_FLAG(t, x, y, TERMINAL_FLAG_DIRTY)) {
c[0] = t->cells[x][y].character;
fontRender(t->font, c, t->palette[t->cells[x][y].foreground], t->palette[t->cells[x][y].background], xp, yp);
TERMINAL_CELL_CLEAR_FLAG(t, x, y, TERMINAL_FLAG_DIRTY);
}
xp += fontWidthGet(t->font);
}
yp += fontHeightGet(t->font);
}
}
void terminalScreenClear(TerminalT *terminal) {
uint16_t x;
uint16_t y;
// Does not move cursor.
for (y=0; y<terminal->rows; y++) {
for (x=0; x<terminal->cols; x++) {
terminal->cells[x][y].character = ' ';
TERMINAL_CELL_CLEAR_FLAG(terminal, x, y, TERMINAL_FLAG_BLINK_SLOW);
TERMINAL_CELL_CLEAR_FLAG(terminal, x, y, TERMINAL_FLAG_BLINK_FAST);
TERMINAL_CELL_SET_FLAG(terminal, x, y, TERMINAL_FLAG_DIRTY);
}
}
GUI_SET_FLAG((WidgetT *)terminal, WIDGET_FLAG_DIRTY);
}
static void terminalSequenceReset(TerminalT *terminal) {
char *parms = NULL;
terminal->escFound = 0;
terminal->inEscape = 0;
terminal->number[0] = 0;
terminal->numberIndex = 0;
if (terminal->parameters != NULL) {
while (arrlen(terminal->parameters) > 0) {
parms = arrpop(terminal->parameters);
free(parms);
}
terminal->parameters = NULL;
}
}
void terminalStringPrint(TerminalT *terminal, char *string) {
uint16_t i;
for (i=0; i<strlen(string); i++) {
terminalCharPrint(terminal, string[i]);
}
}