Files
gw-basic-2026/src/tui.c
Eremey Valetov 71ff44828d Add 16-bit real-mode DOS target -- 127KB standalone, no extender
New Makefile.dos16 builds with OpenWatcom wcc (16-bit, MEDIUM model)
producing a standard MZ executable that runs on any DOS without DOS/4GW.
All 24 source files compile clean; tested on FreeDOS 1.4 via QEMU.

Changes for 16-bit compatibility:
- hal_dos.c: INTX macro selects int86() vs int386() based on _M_I86
- sound.c: reduce stack buffer from 8192 to 512 samples on 16-bit
- tui.c: gracefully disable TUI if screen buffer allocation fails
  (near heap exhaustion common on 16-bit), batch mode still works
- .gitignore: add .obj/.exe/.err/.lib for OpenWatcom build artifacts

Size comparison:
- 32-bit DOS/4GW: 175KB + 265KB extender = 440KB total
- 16-bit real-mode: 127KB standalone

The 32-bit build (Makefile.dos) and Linux build are unaffected.
72/72 interpreter tests pass.
2026-04-10 06:32:47 -04:00

737 lines
20 KiB
C

#include "tui.h"
#include "hal.h"
#include "gwbasic.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#ifndef __MSDOS__
#include <unistd.h>
#include <sys/ioctl.h>
#endif
tui_state_t tui;
/* Saved original HAL pointers for passthrough in non-TUI mode */
static void (*orig_putch)(int);
static void (*orig_puts)(const char *);
static void (*orig_cls)(void);
static void (*orig_locate)(int, int);
static int (*orig_get_cursor_row)(void);
static int (*orig_get_cursor_col)(void);
/* Default F-key definitions matching GW-BASIC */
static const char *default_fkeys[10] = {
"LIST ", /* F1 */
"RUN\r", /* F2 */
"LOAD\"", /* F3 */
"SAVE\"", /* F4 */
"CONT\r", /* F5 */
",\"LPT1:\"\r", /* F6 */
"TRON\r", /* F7 */
"TROFF\r", /* F8 */
"KEY ", /* F9 */
"SCREEN 0,0,0\r", /* F10 */
};
static void scroll_up(void)
{
int bottom = tui.view_bottom;
memmove(&TUI_CELL(0, 0), &TUI_CELL(1, 0),
bottom * tui.cols * sizeof(tui_cell_t));
for (int c = 0; c < tui.cols; c++) {
TUI_CELL(bottom, c).ch = ' ';
TUI_CELL(bottom, c).attr = tui.current_attr;
}
}
static void advance_cursor(void)
{
tui.cursor_col++;
if (tui.cursor_col >= tui.cols) {
tui.cursor_col = 0;
tui.cursor_row++;
if (tui.cursor_row > tui.view_bottom) {
scroll_up();
tui.cursor_row = tui.view_bottom;
}
}
}
void tui_putch(int ch)
{
if (ch == '\n') {
tui.cursor_col = 0;
tui.cursor_row++;
if (tui.cursor_row > tui.view_bottom) {
scroll_up();
tui.cursor_row = tui.view_bottom;
tui_refresh();
}
return;
}
if (ch == '\r') {
tui.cursor_col = 0;
return;
}
if (ch == '\b') {
if (tui.cursor_col > 0) {
tui.cursor_col--;
TUI_CELL(tui.cursor_row, tui.cursor_col).ch = ' ';
TUI_CELL(tui.cursor_row, tui.cursor_col).attr = tui.current_attr;
}
return;
}
if (ch == '\a') return;
if (ch == '\t') {
int target = (tui.cursor_col + 8) & ~7;
while (tui.cursor_col < target && tui.cursor_col < tui.cols)
tui_putch(' ');
return;
}
TUI_CELL(tui.cursor_row, tui.cursor_col).ch = (uint8_t)ch;
TUI_CELL(tui.cursor_row, tui.cursor_col).attr = tui.current_attr;
advance_cursor();
}
void tui_puts(const char *s)
{
while (*s)
tui_putch((unsigned char)*s++);
tui_refresh();
tui_update_cursor();
}
void tui_cls(void)
{
for (int r = 0; r < tui.rows; r++)
for (int c = 0; c < tui.cols; c++) {
TUI_CELL(r, c).ch = ' ';
TUI_CELL(r, c).attr = tui.current_attr;
}
tui.cursor_row = 0;
tui.cursor_col = 0;
tui_refresh();
tui_update_cursor();
}
void tui_locate(int row, int col)
{
tui.cursor_row = row - 1;
tui.cursor_col = col - 1;
if (tui.cursor_row < 0) tui.cursor_row = 0;
if (tui.cursor_col < 0) tui.cursor_col = 0;
if (tui.cursor_row >= tui.rows) tui.cursor_row = tui.rows - 1;
if (tui.cursor_col >= tui.cols) tui.cursor_col = tui.cols - 1;
tui_update_cursor();
}
int tui_get_cursor_row(void) { return tui.cursor_row; }
int tui_get_cursor_col(void) { return tui.cursor_col; }
/* CGA-to-ANSI color mapping */
static const int ansi_fg[16] = {30,34,32,36,31,35,33,37,90,94,92,96,91,95,93,97};
static const int ansi_bg[8] = {40,44,42,46,41,45,43,47};
static void emit_attr(uint8_t attr)
{
int fg = attr & 0x0F;
int bg = (attr >> 4) & 0x07;
printf("\033[%d;%dm", ansi_fg[fg], ansi_bg[bg]);
}
void tui_refresh(void)
{
printf("\033[H");
int bottom = tui.key_bar_visible ? tui.rows : tui.view_bottom + 1;
uint8_t prev_attr = 0xFF;
for (int r = 0; r < bottom; r++) {
printf("\033[%d;1H", r + 1);
for (int c = 0; c < tui.cols; c++) {
uint8_t attr = TUI_CELL(r, c).attr;
if (attr != prev_attr) {
emit_attr(attr);
prev_attr = attr;
}
uint8_t ch = TUI_CELL(r, c).ch;
putchar(ch ? ch : ' ');
}
}
printf("\033[0m");
if (tui.key_bar_visible)
tui_refresh_row(tui.rows - 1);
fflush(stdout);
}
void tui_refresh_row(int row)
{
if (row < 0 || row >= tui.rows) return;
printf("\033[%d;1H", row + 1);
uint8_t prev_attr = 0xFF;
for (int c = 0; c < tui.cols; c++) {
uint8_t attr = TUI_CELL(row, c).attr;
if (attr != prev_attr) {
emit_attr(attr);
prev_attr = attr;
}
uint8_t ch = TUI_CELL(row, c).ch;
putchar(ch ? ch : ' ');
}
printf("\033[0m");
fflush(stdout);
}
void tui_update_cursor(void)
{
printf("\033[%d;%dH", tui.cursor_row + 1, tui.cursor_col + 1);
fflush(stdout);
}
int tui_read_key(void)
{
/* Drain key buffer first (keys pushed back by event trapping) */
int buffered = tui_pop_key();
if (buffered >= 0)
return buffered;
gw_hal->enable_raw();
int ch = gw_hal->getch();
if (ch < 0) return -1;
if (ch == 3) return TK_CTRL_C;
if (ch != 27) return ch;
if (!gw_hal->kbhit())
return TK_ESCAPE;
int seq1 = gw_hal->getch();
if (seq1 < 0) return TK_ESCAPE;
if (seq1 == '[') {
int seq2 = gw_hal->getch();
if (seq2 < 0) return TK_ESCAPE;
switch (seq2) {
case 'A': return TK_UP;
case 'B': return TK_DOWN;
case 'C': return TK_RIGHT;
case 'D': return TK_LEFT;
case 'H': return TK_HOME;
case 'F': return TK_END;
default:
if (seq2 >= '0' && seq2 <= '9') {
int seq3 = gw_hal->getch();
if (seq3 == '~') {
switch (seq2) {
case '2': return TK_INSERT;
case '3': return TK_DELETE;
case '5': return TK_PGUP;
case '6': return TK_PGDN;
}
} else if (seq2 == '1' && seq3 >= '0' && seq3 <= '9') {
int seq4 = gw_hal->getch();
if (seq4 == '~') {
int code = (seq2 - '0') * 10 + (seq3 - '0');
switch (code) {
case 15: return TK_F5;
case 17: return TK_F6;
case 18: return TK_F7;
case 19: return TK_F8;
case 20: return TK_F9;
case 21: return TK_F10;
}
}
} else if (seq2 == '2' && seq3 >= '0' && seq3 <= '9') {
int seq4 = gw_hal->getch();
(void)seq4;
}
}
break;
}
} else if (seq1 == 'O') {
int seq2 = gw_hal->getch();
switch (seq2) {
case 'P': return TK_F1;
case 'Q': return TK_F2;
case 'R': return TK_F3;
case 'S': return TK_F4;
case 'H': return TK_HOME;
case 'F': return TK_END;
}
}
return TK_ESCAPE;
}
static int extract_screen_line(int row, char *buf, int bufsz)
{
int len = 0;
for (int c = 0; c < tui.cols && len < bufsz - 1; c++) {
buf[len++] = TUI_CELL(row, c).ch ? TUI_CELL(row, c).ch : ' ';
}
while (len > 0 && buf[len - 1] == ' ')
len--;
buf[len] = '\0';
return len;
}
char *tui_read_line(void)
{
static char line_buf[TUI_MAX_LINE + 1];
int enter_row = tui.cursor_row;
tui_update_cursor();
for (;;) {
int key = tui_read_key();
if (key < 0) return NULL;
if (key == TK_CTRL_C || tui.break_flag) {
tui.break_flag = false;
line_buf[0] = '\0';
return NULL;
}
if (key >= TK_F1 && key <= TK_F10) {
int fk = key - TK_F1;
const char *def = tui.fkey_defs[fk];
for (const char *p = def; *p; p++) {
if (*p == '\r') {
tui_putch('\n');
tui_refresh();
extract_screen_line(enter_row, line_buf, sizeof(line_buf));
tui_update_cursor();
return line_buf;
}
TUI_CELL(tui.cursor_row, tui.cursor_col).ch = (uint8_t)*p;
TUI_CELL(tui.cursor_row, tui.cursor_col).attr = tui.current_attr;
advance_cursor();
}
tui_refresh_row(tui.cursor_row);
tui_update_cursor();
continue;
}
switch (key) {
case TK_ENTER:
extract_screen_line(enter_row, line_buf, sizeof(line_buf));
tui.cursor_col = 0;
tui.cursor_row++;
if (tui.cursor_row > tui.view_bottom) {
scroll_up();
tui.cursor_row = tui.view_bottom;
}
tui_refresh();
tui_update_cursor();
return line_buf;
case TK_BACKSPACE:
case 127:
if (tui.cursor_col > 0) {
tui.cursor_col--;
for (int c = tui.cursor_col; c < tui.cols - 1; c++)
TUI_CELL(tui.cursor_row, c) = TUI_CELL(tui.cursor_row, c + 1);
TUI_CELL(tui.cursor_row, tui.cols - 1).ch = ' ';
TUI_CELL(tui.cursor_row, tui.cols - 1).attr = tui.current_attr;
tui_refresh_row(tui.cursor_row);
}
tui_update_cursor();
break;
case TK_DELETE:
for (int c = tui.cursor_col; c < tui.cols - 1; c++)
TUI_CELL(tui.cursor_row, c) = TUI_CELL(tui.cursor_row, c + 1);
TUI_CELL(tui.cursor_row, tui.cols - 1).ch = ' ';
TUI_CELL(tui.cursor_row, tui.cols - 1).attr = tui.current_attr;
tui_refresh_row(tui.cursor_row);
tui_update_cursor();
break;
case TK_LEFT:
if (tui.cursor_col > 0)
tui.cursor_col--;
else if (tui.cursor_row > 0) {
tui.cursor_row--;
tui.cursor_col = tui.cols - 1;
}
tui_update_cursor();
break;
case TK_RIGHT:
if (tui.cursor_col < tui.cols - 1)
tui.cursor_col++;
else if (tui.cursor_row < tui.view_bottom) {
tui.cursor_row++;
tui.cursor_col = 0;
}
tui_update_cursor();
break;
case TK_UP:
if (tui.cursor_row > 0)
tui.cursor_row--;
tui_update_cursor();
break;
case TK_DOWN:
if (tui.cursor_row < tui.view_bottom)
tui.cursor_row++;
tui_update_cursor();
break;
case TK_HOME:
tui.cursor_col = 0;
tui_update_cursor();
break;
case TK_END: {
int c = tui.cols - 1;
while (c > 0 && TUI_CELL(tui.cursor_row, c).ch == ' ')
c--;
if (TUI_CELL(tui.cursor_row, c).ch != ' ')
c++;
if (c >= tui.cols) c = tui.cols - 1;
tui.cursor_col = c;
tui_update_cursor();
break;
}
case TK_CTRL_HOME:
tui_cls();
enter_row = 0;
break;
case TK_INSERT:
tui.insert_mode = !tui.insert_mode;
if (tui.insert_mode)
tui_set_cursor_block();
else
tui_set_cursor_line();
break;
case TK_ESCAPE:
for (int c = tui.cursor_col; c < tui.cols; c++) {
TUI_CELL(tui.cursor_row, c).ch = ' ';
TUI_CELL(tui.cursor_row, c).attr = tui.current_attr;
}
tui_refresh_row(tui.cursor_row);
tui_update_cursor();
break;
default:
if (key >= 32 && key < 127) {
if (tui.insert_mode && tui.cursor_col < tui.cols - 1) {
for (int c = tui.cols - 1; c > tui.cursor_col; c--)
TUI_CELL(tui.cursor_row, c) = TUI_CELL(tui.cursor_row, c - 1);
}
TUI_CELL(tui.cursor_row, tui.cursor_col).ch = (uint8_t)key;
TUI_CELL(tui.cursor_row, tui.cursor_col).attr = tui.current_attr;
if (tui.cursor_row != enter_row && tui.cursor_col == 0)
enter_row = tui.cursor_row;
advance_cursor();
tui_refresh_row(tui.cursor_row);
tui_update_cursor();
}
break;
}
}
}
static void render_key_bar(void)
{
int row = tui.rows - 1;
for (int c = 0; c < tui.cols; c++) {
TUI_CELL(row, c).ch = ' ';
TUI_CELL(row, c).attr = 0x70;
}
int col = 0;
for (int i = 0; i < 10 && col < tui.cols; i++) {
char label[4];
snprintf(label, sizeof(label), "%d", i + 1);
for (const char *p = label; *p && col < tui.cols; p++) {
TUI_CELL(row, col).ch = *p;
TUI_CELL(row, col).attr = 0x07;
col++;
}
const char *def = tui.fkey_defs[i];
int maxw = 6;
for (int j = 0; j < maxw && def[j] && def[j] != '\r' && col < tui.cols; j++) {
TUI_CELL(row, col).ch = (uint8_t)def[j];
TUI_CELL(row, col).attr = 0x70;
col++;
}
if (col < tui.cols) {
TUI_CELL(row, col).ch = ' ';
TUI_CELL(row, col).attr = 0x07;
col++;
}
}
tui_refresh_row(row);
}
void tui_key_on(void)
{
tui.key_bar_visible = true;
tui.view_bottom = tui.rows - 2;
render_key_bar();
tui_update_cursor();
}
void tui_key_off(void)
{
tui.key_bar_visible = false;
tui.view_bottom = tui.rows - 1;
for (int c = 0; c < tui.cols; c++) {
TUI_CELL(tui.rows - 1, c).ch = ' ';
TUI_CELL(tui.rows - 1, c).attr = tui.current_attr;
}
tui_refresh_row(tui.rows - 1);
tui_update_cursor();
}
void tui_key_list(void)
{
for (int i = 0; i < 10; i++) {
char buf[32];
snprintf(buf, sizeof(buf), "F%d ", i + 1);
tui_puts(buf);
for (const char *p = tui.fkey_defs[i]; *p; p++) {
if (*p == '\r')
tui_putch(0x0D);
else
tui_putch(*p);
}
tui_putch('\n');
}
tui_refresh();
tui_update_cursor();
}
/* Key buffer ring buffer operations */
void tui_push_key(int key)
{
int next = (tui.keybuf_head + 1) % TUI_KEYBUF_SIZE;
if (next == tui.keybuf_tail)
return; /* buffer full, drop key */
tui.keybuf[tui.keybuf_head] = key;
tui.keybuf_head = next;
}
int tui_pop_key(void)
{
if (tui.keybuf_head == tui.keybuf_tail)
return -1;
int key = tui.keybuf[tui.keybuf_tail];
tui.keybuf_tail = (tui.keybuf_tail + 1) % TUI_KEYBUF_SIZE;
return key;
}
bool tui_keybuf_empty(void)
{
return tui.keybuf_head == tui.keybuf_tail;
}
int tui_key_to_scancode(int key)
{
/* Map internal TK_* key codes to IBM PC scan codes for INKEY$.
Returns -1 for regular ASCII keys (no scan code). */
switch (key) {
case TK_F1: return 59;
case TK_F2: return 60;
case TK_F3: return 61;
case TK_F4: return 62;
case TK_F5: return 63;
case TK_F6: return 64;
case TK_F7: return 65;
case TK_F8: return 66;
case TK_F9: return 67;
case TK_F10: return 68;
case TK_HOME: return 71;
case TK_UP: return 72;
case TK_PGUP: return 73;
case TK_LEFT: return 75;
case TK_RIGHT: return 77;
case TK_END: return 79;
case TK_DOWN: return 80;
case TK_PGDN: return 81;
case TK_INSERT: return 82;
case TK_DELETE: return 83;
default: return -1;
}
}
void tui_edit_line(const char *prefill)
{
/* Advance to new line */
tui.cursor_col = 0;
tui.cursor_row++;
if (tui.cursor_row > tui.view_bottom) {
scroll_up();
tui.cursor_row = tui.view_bottom;
}
/* Write prefill text into screen buffer */
for (int i = 0; prefill[i] && i < tui.cols; i++) {
TUI_CELL(tui.cursor_row, i).ch = (uint8_t)prefill[i];
TUI_CELL(tui.cursor_row, i).attr = tui.current_attr;
}
tui_refresh_row(tui.cursor_row);
tui_update_cursor();
char *result = tui_read_line();
if (result && result[0])
gw_exec_direct(result);
}
void tui_set_cursor_block(void)
{
printf("\033[1 q");
fflush(stdout);
}
void tui_set_cursor_line(void)
{
printf("\033[5 q");
fflush(stdout);
}
static void sigint_handler(int sig)
{
(void)sig;
tui.break_flag = true;
}
void tui_install_break_handler(void)
{
#ifdef __MSDOS__
signal(SIGINT, sigint_handler);
#else
struct sigaction sa;
sa.sa_handler = sigint_handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
#endif
}
void tui_check_break(void)
{
if (tui.break_flag) {
tui.break_flag = false;
tui_puts("\nBreak\n");
gw.running = false;
longjmp(gw_error_jmp, -1);
}
}
void tui_init(bool fullscreen)
{
memset(&tui, 0, sizeof(tui));
tui.current_attr = 0x07;
tui.insert_mode = false;
tui.active = true;
/* Determine screen dimensions */
tui.rows = TUI_DEFAULT_ROWS;
tui.cols = TUI_DEFAULT_COLS;
if (fullscreen) {
#ifndef __MSDOS__
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0 && ws.ws_col > 0) {
tui.rows = ws.ws_row;
tui.cols = ws.ws_col;
if (tui.rows > TUI_MAX_ROWS) tui.rows = TUI_MAX_ROWS;
if (tui.cols > TUI_MAX_COLS) tui.cols = TUI_MAX_COLS;
}
#else
tui.rows = gw_hal ? gw_hal->screen_height : TUI_DEFAULT_ROWS;
tui.cols = gw_hal ? gw_hal->screen_width : TUI_DEFAULT_COLS;
#endif
}
tui.view_bottom = tui.rows - 1;
/* Allocate screen buffer */
tui.screen = calloc(tui.rows * tui.cols, sizeof(tui_cell_t));
if (!tui.screen) {
/* Not enough near heap (common on 16-bit DOS).
* Disable TUI — batch mode still works via HAL. */
tui.active = false;
return;
}
/* Set default F-key definitions */
for (int i = 0; i < 10; i++)
strncpy(tui.fkey_defs[i], default_fkeys[i], sizeof(tui.fkey_defs[i]) - 1);
/* Clear screen buffer */
for (int r = 0; r < tui.rows; r++)
for (int c = 0; c < tui.cols; c++) {
TUI_CELL(r, c).ch = ' ';
TUI_CELL(r, c).attr = tui.current_attr;
}
/* Save original HAL pointers */
orig_putch = gw_hal->putch;
orig_puts = gw_hal->puts;
orig_cls = gw_hal->cls;
orig_locate = gw_hal->locate;
orig_get_cursor_row = gw_hal->get_cursor_row;
orig_get_cursor_col = gw_hal->get_cursor_col;
/* Swap in TUI handlers */
gw_hal->putch = tui_putch;
gw_hal->puts = tui_puts;
gw_hal->cls = tui_cls;
gw_hal->locate = tui_locate;
gw_hal->get_cursor_row = tui_get_cursor_row;
gw_hal->get_cursor_col = tui_get_cursor_col;
/* Install break handler */
tui_install_break_handler();
/* Enter alternate screen buffer, clear */
printf("\033[?1049h");
printf("\033[2J\033[H");
tui_set_cursor_line();
fflush(stdout);
/* Show KEY bar by default (matches real GW-BASIC) */
tui_key_on();
}
void tui_shutdown(void)
{
if (!tui.active) return;
tui.active = false;
/* Restore original HAL pointers */
gw_hal->putch = orig_putch;
gw_hal->puts = orig_puts;
gw_hal->cls = orig_cls;
gw_hal->locate = orig_locate;
gw_hal->get_cursor_row = orig_get_cursor_row;
gw_hal->get_cursor_col = orig_get_cursor_col;
/* Leave alternate screen buffer, restore cursor */
printf("\033[?1049l");
printf("\033[0 q");
fflush(stdout);
free(tui.screen);
tui.screen = NULL;
signal(SIGINT, SIG_DFL);
}