/* * 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 . * */ // 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; ycols; 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; pparameters); 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; xcols; 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; yrows; y++) { for (x=0; xcols; 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; xcells[x] = (CellT *)malloc(sizeof(CellT) * rows); if (!t->cells[x]) { for (y=0; ycells[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; ycells[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; yrows; y++) { xp = pos.x; for (x=0; xcols; 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; yrows; y++) { for (x=0; xcols; 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) { terminal->escFound = 0; terminal->inEscape = 0; terminal->number[0] = 0; terminal->numberIndex = 0; if (terminal->parameters != NULL) { while (arrlen(terminal->parameters) > 0) { free(arrpop(terminal->parameters)); } terminal->parameters = NULL; } } void terminalStringPrint(TerminalT *terminal, char *string) { uint16_t i; for (i=0; i