Files
gw-basic-2026/platform/hal_posix.c
Eremey Valetov 70ffd39562 v0.17.0: BIOS-routed TUI on DOS, version banner, compiler PulseAudio link
QA findings from a multi-round review of the FreeDOS submission prep work:

- TUI rendering refactor: src/tui.c emitted ANSI escape sequences via
  printf, which displays as raw text on bare FreeDOS (no ANSI.SYS).
  Add four HAL ops (tui_enter, tui_leave, render_run, set_cursor_shape)
  and route per-cell rendering through them.  POSIX backend keeps the
  ANSI path; DOS backend drives BIOS INT 10h via the existing
  bios_set_cursor / bios_write_char helpers.  The TUI's logical cursor
  goes through the saved orig_locate to avoid recursing through the
  swapped-in gw_hal->locate.

- DOS extended-key mapping: dos_getch returns 0x100 | scancode for
  arrows / F-keys; tui_read_key wasn't translating those to its TK_*
  constants, so the editor never saw arrow keys or F1-F10 on DOS.
  Add a __MSDOS__-conditional translation table in tui_read_key.

- Version banner: GW_VERSION was still 0.16.0 even though the v0.17.0
  release prep was already in CHANGES.TXT.  Bump.

- Compiler PulseAudio link: gwbasic-compile -c hardcoded
  '-lgwrt -lm -lpthread' on the gcc command line.  When libgwrt was
  built against libpulse-simple (the default on any host with the
  PulseAudio dev headers installed), the compile workflow failed with
  'undefined reference to pa_simple_drain'.  CMake now passes
  GWRT_HAS_PULSEAUDIO to gwbasic-compile when libpulse is present, and
  the compiler appends -lpulse-simple to the link line.

- FRE("") garbage collection: the interpreter skipped strpool_gc with a
  comment 'unsafe during expression eval', but that's exactly what real
  GW-BASIC's FRE("") does (and the AOT compiler path already did).  Add
  the GC call; strpool_pin/unpin is the existing escape hatch if a
  caller has live pool pointers on the C stack.  Fixes the string_gc
  compat test.

- Test harness normalization: run_tests.sh stripped trailing whitespace
  on the actual output but not the expected file, causing spurious
  mismatches against golden files captured from real GWBASIC.EXE.
  Normalize both sides identically.  Fixes the peek_gfx mismatch.

- Print_using: snprintf into mantissa[32] with %.*f and an unbounded
  dec triggered a -Wformat-truncation warning.  Clamp dec to 20 (IEEE
  double has at most ~17 significant decimal digits).

- Doc/version consistency: 16-bit binary size reported as 127KB in one
  place and 128KB in three; standardize on 128KB.  HAL backend count
  said '1 file' but is now 2.  CI test count said 'all 66 test
  programs' but is 72.  Add a v0.17.0 row to the development.md table.
  Update getting-started.md DOS section to match the BIOS-rendering
  reality and add a manual TUI verification checklist.

- dos_init now writes back BIOS-reported cols/rows to dos_hal struct
  fields (forward-declared so dos_init can reference it).

After these changes: 72/72 interpreter tests pass, compat 68/68
matched, no warnings on the Linux build.
2026-05-03 12:25:41 -04:00

218 lines
4.8 KiB
C

#include "hal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
static struct termios orig_termios;
static int raw_mode = 0;
static int termios_saved = 0;
static int cursor_row = 0;
static int cursor_col = 0;
static int pending_char = -1;
static void posix_putch(int ch)
{
putchar(ch);
if (ch == '\n') {
cursor_row++;
cursor_col = 0;
} else if (ch == '\r') {
cursor_col = 0;
} else {
cursor_col++;
}
}
static void posix_puts(const char *s)
{
while (*s)
posix_putch(*s++);
}
static int posix_getch(void)
{
fflush(stdout);
if (pending_char >= 0) {
int ch = pending_char;
pending_char = -1;
return ch;
}
if (raw_mode) {
/* Set VMIN=1 to block until a byte arrives */
struct termios t;
tcgetattr(STDIN_FILENO, &t);
t.c_cc[VMIN] = 1;
tcsetattr(STDIN_FILENO, TCSANOW, &t);
unsigned char ch;
read(STDIN_FILENO, &ch, 1);
t.c_cc[VMIN] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &t);
return ch;
}
return getchar();
}
static bool posix_kbhit(void)
{
if (pending_char >= 0)
return true;
if (!raw_mode)
return false;
unsigned char ch;
ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n == 1) {
pending_char = ch;
return true;
}
return false;
}
static void posix_locate(int row, int col)
{
printf("\033[%d;%dH", row, col);
fflush(stdout);
cursor_row = row - 1;
cursor_col = col - 1;
}
static int posix_get_cursor_row(void) { return cursor_row; }
static int posix_get_cursor_col(void) { return cursor_col; }
static void posix_cls(void)
{
printf("\033[2J\033[H");
fflush(stdout);
cursor_row = 0;
cursor_col = 0;
}
static void posix_set_width(int cols)
{
(void)cols;
}
static void posix_enable_raw(void)
{
if (raw_mode || !isatty(STDIN_FILENO))
return;
if (!termios_saved) {
tcgetattr(STDIN_FILENO, &orig_termios);
termios_saved = 1;
}
struct termios raw = orig_termios;
cfmakeraw(&raw);
raw.c_oflag |= OPOST; /* keep output processing (newline translation) */
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
raw_mode = 1;
}
static void posix_disable_raw(void)
{
if (!raw_mode)
return;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
raw_mode = 0;
}
static void posix_write_raw(const char *data, int len)
{
fflush(stdout);
write(STDOUT_FILENO, data, len);
}
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 posix_tui_enter(void)
{
printf("\033[?1049h\033[2J\033[H");
fflush(stdout);
}
static void posix_tui_leave(void)
{
printf("\033[?1049l\033[0 q\033[?25h");
fflush(stdout);
}
static void posix_render_run(int row, int col,
const uint8_t *chars, const uint8_t *attrs, int len)
{
printf("\033[%d;%dH", row + 1, col + 1);
int prev_attr = -1;
for (int i = 0; i < len; i++) {
int a = attrs[i];
if (a != prev_attr) {
printf("\033[%d;%dm", ansi_fg[a & 0x0F], ansi_bg[(a >> 4) & 0x07]);
prev_attr = a;
}
unsigned char ch = chars[i];
putchar(ch ? ch : ' ');
}
printf("\033[0m");
fflush(stdout);
}
static void posix_set_cursor_shape(int shape)
{
switch (shape) {
case 0: printf("\033[?25l"); break;
case 1: printf("\033[?25h\033[1 q"); break;
case 2: printf("\033[?25h\033[5 q"); break;
}
fflush(stdout);
}
static void posix_init(void)
{
setbuf(stdout, NULL);
}
static void posix_shutdown(void)
{
if (raw_mode) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
raw_mode = 0;
}
}
static hal_ops_t posix_hal = {
.putch = posix_putch,
.puts = posix_puts,
.getch = posix_getch,
.kbhit = posix_kbhit,
.locate = posix_locate,
.get_cursor_row = posix_get_cursor_row,
.get_cursor_col = posix_get_cursor_col,
.cls = posix_cls,
.set_width = posix_set_width,
.enable_raw = posix_enable_raw,
.disable_raw = posix_disable_raw,
.write_raw = posix_write_raw,
.tui_enter = posix_tui_enter,
.tui_leave = posix_tui_leave,
.render_run = posix_render_run,
.set_cursor_shape = posix_set_cursor_shape,
.screen_width = 80,
.screen_height = 25,
.is_tty = false,
.init = posix_init,
.shutdown = posix_shutdown,
};
hal_ops_t *hal_posix_create(void)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
if (ws.ws_col > 0) posix_hal.screen_width = ws.ws_col;
if (ws.ws_row > 0) posix_hal.screen_height = ws.ws_row;
}
posix_hal.is_tty = isatty(STDIN_FILENO);
return &posix_hal;
}