From 3fa8c6f034089ef02f9211bece6e14be1bb4f8eb Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Fri, 27 Feb 2026 17:29:09 -0500 Subject: [PATCH] Implement EDIT statement and ON TIMER/ON KEY event trapping, update to v0.9.0 Add event-driven programming: ON TIMER(n) GOSUB with TIMER ON/OFF/STOP, ON KEY(n) GOSUB with KEY(n) ON/OFF/STOP for F1-F10. Fix F-key escape sequence parser (F9/F10 detection, push back consumed bytes on unmatched sequences). Add EDIT statement for TUI line editing. Guard key trap polling so keystrokes aren't consumed when no traps are configured. --- README.md | 7 +- docs/development.md | 5 +- docs/getting-started.md | 2 +- docs/language-reference.md | 34 +++- docs/roadmap.md | 2 - include/gwbasic.h | 2 +- include/interp.h | 4 + include/tui.h | 14 ++ include/types.h | 19 +++ src/eval.c | 8 +- src/interp.c | 286 ++++++++++++++++++++++++++++++++++ src/tui.c | 53 +++++++ tests/programs/on_timer.bas | 12 ++ tests/programs/timer_stop.bas | 16 ++ 14 files changed, 453 insertions(+), 11 deletions(-) create mode 100644 tests/programs/on_timer.bas create mode 100644 tests/programs/timer_stop.bas diff --git a/README.md b/README.md index 79e1704..425a635 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.8.0 +GW-BASIC 2026 0.9.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok @@ -67,10 +67,11 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$ | Variables | LET, DIM, ERASE, SWAP, DEFINT/SNG/DBL/STR | | Control flow | GOTO, GOSUB/RETURN, FOR/NEXT, IF/THEN/ELSE, WHILE/WEND, ON...GOTO/GOSUB | | Input | INPUT, LINE INPUT, DATA/READ/RESTORE, INKEY$ | -| Program | RUN, RUN "file", CONT, STOP, END, NEW, LIST, CLEAR, AUTO, RENUM, DELETE | +| Program | RUN, RUN "file", CONT, STOP, END, NEW, LIST, CLEAR, AUTO, RENUM, DELETE, EDIT | | Sequential I/O | OPEN, CLOSE, PRINT#, WRITE#, INPUT#, LINE INPUT# | | Random-access I/O | FIELD, LSET, RSET, PUT, GET, CVI/CVS/CVD, MKI$/MKS$/MKD$ | | Program I/O | SAVE, LOAD, MERGE, CHAIN, COMMON | +| Event trapping | ON TIMER(n) GOSUB, TIMER ON/OFF/STOP, ON KEY(n) GOSUB, KEY(n) ON/OFF/STOP | | Error handling | ON ERROR GOTO, RESUME, ERROR, ERR, ERL | | User functions | DEF FN, RANDOMIZE | | File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL | @@ -160,7 +161,7 @@ Key design differences from the original: ## Tests -54 test programs in `tests/programs/`, with CI via GitHub Actions: +56 test programs in `tests/programs/`, with CI via GitHub Actions: ```bash bash tests/run_tests.sh diff --git a/docs/development.md b/docs/development.md index 5424fe4..fcbe115 100644 --- a/docs/development.md +++ b/docs/development.md @@ -12,10 +12,11 @@ | 0.6.0 | `ece018d` | DATE$/TIME$/TIMER, FILES, SHELL, CHDIR, MKDIR, RMDIR | | 0.7.0 | `da6b513` | AUTO, RENUM (with GOTO/GOSUB patching), DELETE, COMMON, LIST range fix | | 0.8.0 | `c68167c` | Dynamic TUI screen buffer, `--full` flag, LPRINT/LLIST with `--lpt` | +| 0.9.0 | | EDIT statement, ON TIMER/ON KEY event trapping, F-key escape parser fixes | ## Tests -54 test programs in `tests/programs/`. Run the full suite: +56 test programs in `tests/programs/`. Run the full suite: ```bash bash tests/run_tests.sh @@ -40,6 +41,6 @@ bash tests/run_compat.sh ## CI GitHub Actions runs on every push to `main` and on pull requests. The workflow -builds the project with PulseAudio support and runs all 54 test programs. +builds the project with PulseAudio support and runs all 56 test programs. See [`.github/workflows/ci.yml`](https://github.com/evvaletov/gw-basic-2026/blob/main/.github/workflows/ci.yml). diff --git a/docs/getting-started.md b/docs/getting-started.md index d3d8c8f..ec528ae 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -37,7 +37,7 @@ Running `./gwbasic` with no arguments launches the full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.8.0 +GW-BASIC 2026 0.9.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok diff --git a/docs/language-reference.md b/docs/language-reference.md index bc31ff4..ef4d6fc 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -47,10 +47,11 @@ type suffixes (`%`, `!`, `#`) | Variables | `LET`, `DIM`, `ERASE`, `SWAP`, `DEFINT`, `DEFSNG`, `DEFDBL`, `DEFSTR` | | Control flow | `GOTO`, `GOSUB`/`RETURN`, `FOR`/`NEXT`, `IF`/`THEN`/`ELSE`, `WHILE`/`WEND`, `ON...GOTO`, `ON...GOSUB` | | Input | `INPUT`, `LINE INPUT`, `DATA`/`READ`/`RESTORE`, `INKEY$` | -| Program control | `RUN`, `RUN "file"`, `CONT`, `STOP`, `END`, `NEW`, `LIST`, `CLEAR`, `AUTO`, `RENUM`, `DELETE` | +| Program control | `RUN`, `RUN "file"`, `CONT`, `STOP`, `END`, `NEW`, `LIST`, `CLEAR`, `AUTO`, `RENUM`, `DELETE`, `EDIT` | | Sequential I/O | `OPEN`, `CLOSE`, `PRINT#`, `WRITE#`, `INPUT#`, `LINE INPUT#` | | Random-access I/O | `FIELD`, `LSET`, `RSET`, `PUT`, `GET`, `CVI`/`CVS`/`CVD`, `MKI$`/`MKS$`/`MKD$` | | Program I/O | `SAVE`, `LOAD`, `MERGE`, `CHAIN`, `COMMON` | +| Event trapping | `ON TIMER(n) GOSUB`, `TIMER ON`/`OFF`/`STOP`, `ON KEY(n) GOSUB`, `KEY(n) ON`/`OFF`/`STOP` | | Error handling | `ON ERROR GOTO`, `RESUME`, `RESUME NEXT`, `RESUME n`, `ERROR`, `ERR`, `ERL` | | User functions | `DEF FN`, `RANDOMIZE` | | File management | `KILL`, `NAME`, `FILES`, `MKDIR`, `RMDIR`, `CHDIR`, `SHELL` | @@ -145,6 +146,37 @@ suitable for scripting and test harnesses. ### Program Editing +- `EDIT [linenum]` — display a program line for editing in the TUI; press Enter to re-store it - `AUTO [start][,increment]` — automatic line numbering mode - `RENUM [new][,[old][,increment]]` — renumber program lines (patches all GOTO/GOSUB references) - `DELETE range` — delete program lines (`DELETE 10-50`, `DELETE -100`, `DELETE 200-`) + +## Event Trapping + +GW-BASIC supports event-driven programming through trap handlers that fire +between statements during program execution. + +### Timer Events + +``` +ON TIMER(n) GOSUB line ' register handler (fires every n seconds) +TIMER ON ' enable timer trapping +TIMER STOP ' suspend trapping (events are queued) +TIMER OFF ' disable trapping (events are discarded) +``` + +### Function Key Events + +``` +ON KEY(n) GOSUB line ' register handler for F-key n (1-10) +KEY(n) ON ' enable trapping for key n +KEY(n) STOP ' suspend trapping (events are queued) +KEY(n) OFF ' disable trapping +``` + +Event handlers execute as implicit GOSUBs. The `RETURN` statement returns +to the interrupted code and clears the handler's in-progress flag. Events do +not fire inside their own handler (re-entrant protection). + +`TIMER STOP` / `KEY(n) STOP` queue events while stopped; switching to +`TIMER ON` / `KEY(n) ON` fires the pending event immediately. diff --git a/docs/roadmap.md b/docs/roadmap.md index 30f4d2f..72c208e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,8 +2,6 @@ ## Planned Features -- **EDIT statement** — edit a specific program line in the TUI -- **ON KEY / ON TIMER event trapping** — periodic event handlers for interactive programs - **BSAVE / BLOAD** — binary file save/load for screen buffers and data - **DEF SEG** — memory segment declaration for PEEK/POKE/BSAVE/BLOAD - **PRINT USING edge cases** — `**` asterisk fill, `**$` combined diff --git a/include/gwbasic.h b/include/gwbasic.h index f0ccdde..b9225b1 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.8.0" +#define GW_VERSION "0.9.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ diff --git a/include/interp.h b/include/interp.h index 7b17daa..b31b4c0 100644 --- a/include/interp.h +++ b/include/interp.h @@ -76,6 +76,10 @@ typedef struct { /* COMMON variable list for CHAIN */ int common_count; struct { char name[2]; gw_valtype_t type; } common_vars[64]; + + /* Event trapping */ + timer_trap_t timer_trap; + event_trap_t key_traps[10]; /* KEY(1)-KEY(10) */ } interp_state_t; extern interp_state_t gw; diff --git a/include/tui.h b/include/tui.h index 827239e..14262de 100644 --- a/include/tui.h +++ b/include/tui.h @@ -45,6 +45,9 @@ typedef struct { #define TK_CTRL_C 0x03 #define TK_CTRL_BREAK 0x200 +/* Key buffer for event trapping (keys consumed from HAL but not yet processed) */ +#define TUI_KEYBUF_SIZE 16 + /* TUI state */ typedef struct { tui_cell_t *screen; /* dynamically allocated [rows * cols] */ @@ -60,6 +63,9 @@ typedef struct { volatile bool break_flag; /* set by SIGINT handler */ int view_top; /* top of scrollable area */ int view_bottom; /* bottom row (rows-1 normally, rows-2 with key bar) */ + int keybuf[TUI_KEYBUF_SIZE]; /* ring buffer for pushed-back keys */ + int keybuf_head; + int keybuf_tail; } tui_state_t; extern tui_state_t tui; @@ -93,6 +99,14 @@ void tui_key_on(void); void tui_key_off(void); void tui_key_list(void); +/* EDIT statement */ +void tui_edit_line(const char *prefill); + +/* Key buffer for event trapping */ +void tui_push_key(int key); +int tui_pop_key(void); +bool tui_keybuf_empty(void); + /* Ctrl+Break */ void tui_check_break(void); void tui_install_break_handler(void); diff --git a/include/types.h b/include/types.h index ef69516..82a7ada 100644 --- a/include/types.h +++ b/include/types.h @@ -64,11 +64,30 @@ typedef struct { uint16_t line_num; } for_entry_t; +/* Event trap mode */ +typedef enum { TRAP_OFF = 0, TRAP_ON = 1, TRAP_STOP = 2 } trap_mode_t; + +/* Event trap state (shared by TIMER/KEY traps) */ +typedef struct event_trap { + trap_mode_t mode; + uint16_t gosub_line; + bool pending; + bool in_handler; +} event_trap_t; + +/* ON TIMER trap */ +typedef struct { + event_trap_t trap; + float interval; /* seconds */ + double last_fire; /* monotonic timestamp */ +} timer_trap_t; + /* GOSUB stack entry */ typedef struct { uint8_t *ret_text; struct program_line *ret_line; uint16_t line_num; + struct event_trap *event_source; /* non-NULL for event handler GOSUB */ } gosub_entry_t; /* WHILE stack entry */ diff --git a/src/eval.c b/src/eval.c index af53e65..cbb2f64 100644 --- a/src/eval.c +++ b/src/eval.c @@ -1,4 +1,5 @@ #include "gwbasic.h" +#include "tui.h" #include "graphics.h" #include #include @@ -879,7 +880,12 @@ static gw_value_t eval_atom(void) gw_chrget(); gw_value_t v; v.type = VT_STR; - if (gw_hal && gw_hal->kbhit()) { + /* Check key buffer first (keys pushed back by event trapping) */ + if (!tui_keybuf_empty()) { + int ch = tui_pop_key(); + v.sval = gw_str_alloc(1); + v.sval.data[0] = (char)ch; + } else if (gw_hal && gw_hal->kbhit()) { int ch = gw_hal->getch(); v.sval = gw_str_alloc(1); v.sval.data[0] = ch; diff --git a/src/interp.c b/src/interp.c index ae81451..09ab33e 100644 --- a/src/interp.c +++ b/src/interp.c @@ -11,6 +11,7 @@ #include #include #include +#include /* * Execution loop and control flow - the heart of the interpreter. @@ -1087,6 +1088,28 @@ void gw_exec_stmt(void) free(path); return; } + /* TIMER ON/OFF/STOP */ + if (xstmt == XSTMT_TIMER) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == TOK_ON) { + gw_chrget(); + gw.timer_trap.trap.mode = TRAP_ON; + return; + } + if (gw_chrgot() == TOK_OFF) { + gw_chrget(); + gw.timer_trap.trap.mode = TRAP_OFF; + gw.timer_trap.trap.pending = false; + return; + } + if (gw_chrgot() == TOK_STOP) { + gw_chrget(); + gw.timer_trap.trap.mode = TRAP_STOP; + return; + } + gw_error(ERR_SN); + } /* Stubs: VIEW, WINDOW, PALETTE */ if (xstmt == XSTMT_VIEW || xstmt == XSTMT_WINDOW || xstmt == XSTMT_PALETTE) { @@ -1216,6 +1239,8 @@ void gw_exec_stmt(void) gw.on_error_line = 0; gw.in_error_handler = false; gw.option_base = 0; + memset(&gw.timer_trap, 0, sizeof(gw.timer_trap)); + memset(gw.key_traps, 0, sizeof(gw.key_traps)); gw.cur_line = start; gw.text_ptr = start->tokens; @@ -1287,6 +1312,40 @@ void gw_exec_stmt(void) return; } + /* EDIT [linenum] */ + if (tok == TOK_EDIT) { + gw_chrget(); + gw_skip_spaces(); + uint16_t num; + if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) { + num = gw_eval_uint16(); + } else if (gw.cont_line) { + num = gw.cont_line->num; + } else if (gw.err_line_num) { + num = gw.err_line_num; + } else { + gw_error(ERR_SN); + } + program_line_t *line = gw_find_line(num); + if (!line) gw_error(ERR_UL); + char listbuf[512]; + gw_list_line(line->tokens, line->len, listbuf, sizeof(listbuf)); + char formatted[560]; + snprintf(formatted, sizeof(formatted), "%u %s", line->num, listbuf); + if (tui.active) { + tui_edit_line(formatted); + } else { + /* Non-interactive: just display the line */ + if (gw_hal) { + gw_hal->puts(formatted); + gw_hal->puts("\n"); + } else { + printf("%s\n", formatted); + } + } + return; + } + /* AUTO [start[,increment]] */ if (tok == TOK_AUTO) { gw_chrget(); @@ -1521,6 +1580,7 @@ void gw_exec_stmt(void) gw.gosub_stack[gw.gosub_sp].ret_text = gw.text_ptr; gw.gosub_stack[gw.gosub_sp].ret_line = gw.cur_line; gw.gosub_stack[gw.gosub_sp].line_num = gw.cur_line_num; + gw.gosub_stack[gw.gosub_sp].event_source = NULL; gw.gosub_sp++; gw.cur_line = target; @@ -1539,6 +1599,10 @@ void gw_exec_stmt(void) gw.cur_line = gw.gosub_stack[gw.gosub_sp].ret_line; gw.cur_line_num = gw.gosub_stack[gw.gosub_sp].line_num; + /* Clear event handler flag if returning from event trap */ + if (gw.gosub_stack[gw.gosub_sp].event_source) + gw.gosub_stack[gw.gosub_sp].event_source->in_handler = false; + /* Optional line number: RETURN */ gw_skip_spaces(); if (gw_chrgot() >= TOK_INT2 && gw_chrgot() <= TOK_CONST_DBL) { @@ -1859,6 +1923,49 @@ void gw_exec_stmt(void) return; } + /* ON TIMER(n) GOSUB line */ + if (gw_chrgot() == TOK_PREFIX_FE && gw.text_ptr[1] == XSTMT_TIMER) { + gw.text_ptr += 2; + gw_expect('('); + gw_value_t v = gw_eval_num(); + float interval; + if (v.type == VT_INT) interval = (float)v.ival; + else if (v.type == VT_SNG) interval = v.fval; + else interval = (float)v.dval; + if (interval <= 0) gw_error(ERR_FC); + gw_expect(')'); + gw_skip_spaces(); + if (gw_chrgot() != TOK_GOSUB) gw_error(ERR_SN); + gw_chrget(); + uint16_t line = gw_eval_uint16(); + gw.timer_trap.interval = interval; + gw.timer_trap.trap.gosub_line = line; + gw.timer_trap.trap.pending = false; + gw.timer_trap.trap.in_handler = false; + /* Reset the clock */ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + gw.timer_trap.last_fire = ts.tv_sec + ts.tv_nsec / 1e9; + return; + } + + /* ON KEY(n) GOSUB line */ + if (gw_chrgot() == TOK_KEY) { + gw_chrget(); + gw_expect('('); + int n = gw_eval_int(); + if (n < 1 || n > 10) gw_error(ERR_FC); + gw_expect(')'); + gw_skip_spaces(); + if (gw_chrgot() != TOK_GOSUB) gw_error(ERR_SN); + gw_chrget(); + uint16_t line = gw_eval_uint16(); + gw.key_traps[n - 1].gosub_line = line; + gw.key_traps[n - 1].pending = false; + gw.key_traps[n - 1].in_handler = false; + return; + } + int idx = gw_eval_int(); gw_skip_spaces(); @@ -1899,6 +2006,7 @@ void gw_exec_stmt(void) gw.gosub_stack[gw.gosub_sp].ret_text = gw.text_ptr; gw.gosub_stack[gw.gosub_sp].ret_line = gw.cur_line; gw.gosub_stack[gw.gosub_sp].line_num = gw.cur_line_num; + gw.gosub_stack[gw.gosub_sp].event_source = NULL; gw.gosub_sp++; } @@ -2277,6 +2385,31 @@ void gw_exec_stmt(void) if (tok == TOK_KEY) { gw_chrget(); gw_skip_spaces(); + /* KEY(n) ON/OFF/STOP — event trapping */ + if (gw_chrgot() == '(') { + gw_chrget(); + int n = gw_eval_int(); + if (n < 1 || n > 10) gw_error(ERR_FC); + gw_expect(')'); + gw_skip_spaces(); + if (gw_chrgot() == TOK_ON) { + gw_chrget(); + gw.key_traps[n - 1].mode = TRAP_ON; + return; + } + if (gw_chrgot() == TOK_OFF) { + gw_chrget(); + gw.key_traps[n - 1].mode = TRAP_OFF; + gw.key_traps[n - 1].pending = false; + return; + } + if (gw_chrgot() == TOK_STOP) { + gw_chrget(); + gw.key_traps[n - 1].mode = TRAP_STOP; + return; + } + gw_error(ERR_SN); + } if (gw_chrgot() == TOK_ON) { gw_chrget(); tui_key_on(); @@ -2450,6 +2583,156 @@ void gw_exec_stmt(void) gw_error(ERR_SN); } +/* ================================================================ + * Event Trapping + * ================================================================ */ + +static void fire_event_trap(event_trap_t *trap) +{ + trap->in_handler = true; + trap->pending = false; + + program_line_t *target = gw_find_line(trap->gosub_line); + if (!target) return; + + if (gw.gosub_sp >= MAX_GOSUB_DEPTH) + gw_error(ERR_OM); + gw.gosub_stack[gw.gosub_sp].ret_text = gw.text_ptr; + gw.gosub_stack[gw.gosub_sp].ret_line = gw.cur_line; + gw.gosub_stack[gw.gosub_sp].line_num = gw.cur_line_num; + gw.gosub_stack[gw.gosub_sp].event_source = trap; + gw.gosub_sp++; + + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; +} + +static void gw_check_events(void) +{ + /* Timer trap */ + if (gw.timer_trap.trap.gosub_line && !gw.timer_trap.trap.in_handler) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + double now = ts.tv_sec + ts.tv_nsec / 1e9; + double elapsed = now - gw.timer_trap.last_fire; + + if (elapsed >= gw.timer_trap.interval) { + if (gw.timer_trap.trap.mode == TRAP_ON) { + gw.timer_trap.last_fire = now; + fire_event_trap(&gw.timer_trap.trap); + return; + } else if (gw.timer_trap.trap.mode == TRAP_STOP) { + gw.timer_trap.trap.pending = true; + } + } + } + + /* Check for pending timer event after TIMER ON */ + if (gw.timer_trap.trap.pending && gw.timer_trap.trap.mode == TRAP_ON && + !gw.timer_trap.trap.in_handler) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + gw.timer_trap.last_fire = ts.tv_sec + ts.tv_nsec / 1e9; + fire_event_trap(&gw.timer_trap.trap); + return; + } + + /* Key traps: only consume keystrokes when at least one trap is configured */ + bool any_key_trap = false; + for (int i = 0; i < 10; i++) { + if (gw.key_traps[i].gosub_line) { any_key_trap = true; break; } + } + + if (any_key_trap && gw_hal && gw_hal->kbhit()) { + int ch = gw_hal->getch(); + int fkey = -1; + if (ch == 27 && gw_hal->kbhit()) { + int seq1 = gw_hal->getch(); + if (seq1 == 'O') { + int seq2 = gw_hal->getch(); + switch (seq2) { + case 'P': fkey = 0; break; /* F1 */ + case 'Q': fkey = 1; break; /* F2 */ + case 'R': fkey = 2; break; /* F3 */ + case 'S': fkey = 3; break; /* F4 */ + default: + tui_push_key(27); + tui_push_key('O'); + tui_push_key(seq2); + return; + } + } else if (seq1 == '[') { + int seq2 = gw_hal->getch(); + if ((seq2 == '1' || seq2 == '2') && gw_hal->kbhit()) { + int seq3 = gw_hal->getch(); + if (gw_hal->kbhit()) { + int seq4 = gw_hal->getch(); + if (seq4 == '~') { + int code = (seq2 - '0') * 10 + (seq3 - '0'); + switch (code) { + case 15: fkey = 4; break; /* F5 */ + case 17: fkey = 5; break; /* F6 */ + case 18: fkey = 6; break; /* F7 */ + case 19: fkey = 7; break; /* F8 */ + case 20: fkey = 8; break; /* F9 */ + case 21: fkey = 9; break; /* F10 */ + } + } + if (fkey < 0) { + tui_push_key(27); + tui_push_key('['); + tui_push_key(seq2); + tui_push_key(seq3); + tui_push_key(seq4); + return; + } + } else { + tui_push_key(27); + tui_push_key('['); + tui_push_key(seq2); + tui_push_key(seq3); + return; + } + } else { + tui_push_key(27); + tui_push_key('['); + tui_push_key(seq2); + return; + } + } else { + tui_push_key(27); + tui_push_key(seq1); + return; + } + } + + if (fkey >= 0 && fkey < 10) { + event_trap_t *kt = &gw.key_traps[fkey]; + if (kt->gosub_line && kt->mode == TRAP_ON && !kt->in_handler) { + fire_event_trap(kt); + return; + } else if (kt->gosub_line && kt->mode == TRAP_STOP) { + kt->pending = true; + return; + } + return; + } + + /* Not an F-key: push into key buffer for INKEY$/input */ + tui_push_key(ch); + } + + /* Check for pending key traps after KEY(n) ON */ + for (int i = 0; i < 10; i++) { + event_trap_t *kt = &gw.key_traps[i]; + if (kt->pending && kt->mode == TRAP_ON && !kt->in_handler) { + fire_event_trap(kt); + return; + } + } +} + /* ================================================================ * NEWSTT Loop (Run Loop) * ================================================================ */ @@ -2471,6 +2754,9 @@ void gw_run_loop(void) if (tui.active) tui_check_break(); + /* Check event traps (ON TIMER, ON KEY) */ + gw_check_events(); + gw_skip_spaces(); uint8_t ch = gw_chrgot(); diff --git a/src/tui.c b/src/tui.c index a32f938..0a9a584 100644 --- a/src/tui.c +++ b/src/tui.c @@ -167,6 +167,11 @@ void tui_update_cursor(void) 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(); @@ -484,6 +489,54 @@ void tui_key_list(void) 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; +} + +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"); diff --git a/tests/programs/on_timer.bas b/tests/programs/on_timer.bas new file mode 100644 index 0000000..05e1d46 --- /dev/null +++ b/tests/programs/on_timer.bas @@ -0,0 +1,12 @@ +10 REM ON TIMER event trapping test +20 COUNT = 0 +30 ON TIMER(1) GOSUB 100 +40 TIMER ON +50 T0 = TIMER +60 IF TIMER - T0 < 3 THEN 60 +70 TIMER OFF +80 PRINT "Timer fired"; COUNT; "times" +90 IF COUNT >= 2 THEN PRINT "PASS" ELSE PRINT "FAIL" +95 END +100 COUNT = COUNT + 1 +110 RETURN diff --git a/tests/programs/timer_stop.bas b/tests/programs/timer_stop.bas new file mode 100644 index 0000000..e17dddd --- /dev/null +++ b/tests/programs/timer_stop.bas @@ -0,0 +1,16 @@ +10 REM TIMER STOP queues events, TIMER ON fires pending +20 COUNT = 0 +30 ON TIMER(1) GOSUB 200 +40 TIMER STOP +50 T0 = TIMER +60 IF TIMER - T0 < 2 THEN 60 +70 PRINT "Before TIMER ON: count ="; COUNT +80 TIMER ON +90 REM Small busy-wait for pending event to fire +95 FOR I = 1 TO 10000: NEXT I +100 PRINT "After TIMER ON: count ="; COUNT +110 TIMER OFF +120 IF COUNT >= 1 THEN PRINT "PASS" ELSE PRINT "FAIL" +130 END +200 COUNT = COUNT + 1 +210 RETURN