all repos — xyw @ 1772e5a384ed8c89b967a3b87126e946237adab2

A minimal virtual machine and assembler for terminals.

Implemented a way to have keypress and timers co-exist without blocking each other.
h3rald h3rald@h3rald.com
Fri, 24 Jul 2026 22:45:23 +0200
commit

1772e5a384ed8c89b967a3b87126e946237adab2

parent

4546ecda19fa588cd56b5e7d3a084876d6619d96

5 files changed, 199 insertions(+), 27 deletions(-)

jump to
M devices/clock.cdevices/clock.c

@@ -218,3 +218,14 @@ clock_timer_elapsed_pending = 0;

} } } + +int clock_check_elapsed(void) +{ + clock_get_timer_value(); // triggers pending flag if target time has passed + if (clock_timer_elapsed_pending) + { + clock_timer_elapsed_pending = 0; + return 1; + } + return 0; +}
M devices/clock.hdevices/clock.h

@@ -3,3 +3,4 @@

void clock_setup(xyw_byte *data); xyw_byte clock_input(xyw_byte *data, xyw_byte addr, xyw_byte *error); void clock_output(xyw_byte *data, xyw_byte addr, xyw_byte *error); +int clock_check_elapsed(void);
M devices/terminal.cdevices/terminal.c

@@ -35,9 +35,9 @@ }

raw = terminal_orig_termios; raw.c_lflag &= ~(ICANON | ECHO | ISIG); - // Read returns as soon as 1 byte is available, no inter-byte timeout - raw.c_cc[VMIN] = 1; - raw.c_cc[VTIME] = 0; + // Non-blocking read: return after 100ms even if no input (for polling) + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) {
A examples/clock-repl.xyw

@@ -0,0 +1,140 @@

+.include "lib/macros.xyw" + +; Clock REPL - demonstrates simultaneous keypress + timer handling. +; The prompt shows a live clock [HH:MM:SS] that updates every second +; while accepting keyboard input. Type a line and press Enter to echo it. + +; Main program: show initial prompt, register both handlers, halt +print-prompt JSRw +setup-handlers JSRw +HLT + +.include "lib/putd.xyw" +.include "lib/puts.xyw" + +; Register keypress and timer handlers, start 1-second timer +; -- +.label setup-handlers + on-keypress terminal.on_keypress STW + on-timer clock.on_timer STW + $0100 clock.timer STW + RTS + +; Timer handler: redraw the prompt line with updated clock +; -- +.label on-timer + redraw-line JSRw + $0100 clock.timer STW + RTS + +; Keypress handler +; -- +.label on-keypress + terminal.input LDB + ; Enter + DUP $0D EQU on-enter JCNw + DUP $0A EQU on-enter JCNw + ; Backspace (DEL or BS) + DUP $7F EQU on-backspace JCNw + DUP $08 EQU on-backspace JCNw + ; Ignore non-printable + DUP $20 LTH on-ignore JCNw + ; Ignore if buffer full (48 chars) + buf-len LDBw $30 EQU on-ignore JCNw + ; Store char in buffer at buf[len] + DUP + buf-len LDBw $00 SWP buf ADDw STBw + ; Increment length + buf-len LDBw INC buf-len STBw + ; Echo the character + PUTC + RTS + +.label on-ignore + POP RTS + +; Enter: echo back the line +; -- +.label on-enter + POP + NL + echo-prefix puts JSRw + print-buffer JSRw + NL + ; Clear buffer + $00 buf-len STBw + ; Show fresh prompt + print-prompt JSRw + RTS + +; Backspace: remove last character +; -- +.label on-backspace + POP + buf-len LDBw $00 EQU on-backspace.done JCNw + buf-len LDBw DEC buf-len STBw + $08 PUTC $20 PUTC $08 PUTC + .label on-backspace.done + RTS + +; Print the clock prompt: [HH:MM:SS] > +; -- +.label print-prompt + $5B PUTC + clock.hour LDB ZEROPADx JSRw + $3A PUTC + clock.minute LDB ZEROPADx JSRw + $3A PUTC + clock.second LDB ZEROPADx JSRw + $5D PUTC SP $3E PUTC SP + RTS + +; Redraw current line in-place: CR + prompt + buffer + clear-to-EOL +; -- +.label redraw-line + $0D PUTC + print-prompt JSRw + print-buffer JSRw + ; ANSI escape: ESC[K (clear to end of line) + $1B PUTC $5B PUTC $4B PUTC + RTS + +; Print buffer contents (buf[0..len-1]) +; -[xw, y]- +.label print-buffer + buf POPxw + buf-len LDBw POPy + .label print-buffer.loop + PSHy $00 EQU print-buffer.done JCNw + LDBxw PUTC + INCxw DECy + print-buffer.loop JMPw + .label print-buffer.done + RTS + +; Print 8-bit value with leading zero if < 10 +; a8 -[x]- +.label ZEROPADx + POPx PSHx $0A LTH zeropad.zero JCNw + .label zeropad.digit + PSHx putd JSRw + RTS + .label zeropad.zero + $30 PUTC + zeropad.digit JMPw + +; --- Data --- + +.label echo-prefix + .string "< " + +.label buf-len + .byte $00 + +.label buf + .byte $00 $00 $00 $00 $00 $00 $00 $00 + .byte $00 $00 $00 $00 $00 $00 $00 $00 + .byte $00 $00 $00 $00 $00 $00 $00 $00 + .byte $00 $00 $00 $00 $00 $00 $00 $00 + .byte $00 $00 $00 $00 $00 $00 $00 $00 + .byte $00 $00 $00 $00 $00 $00 $00 $00
M xywrun.cxywrun.c

@@ -21,6 +21,8 @@

#define MAX_IMAGE_EXEC_DEPTH 8 #define MAX_IMAGE_EXEC_PATH_LENGTH 256 +#define POLL_INTERVAL_MS 100 + /* system device addresses */ #define SYSTEM_STATE 0xff00 #define SYSTEM_ERROR 0xff01

@@ -40,15 +42,20 @@

#ifdef _WIN32 #include <conio.h> #include <windows.h> -int getch() +int getch_timeout(int timeout_ms) { - return _getch(); + HANDLE h = GetStdHandle(STD_INPUT_HANDLE); + if (WaitForSingleObject(h, timeout_ms) == WAIT_OBJECT_0 && _kbhit()) + return _getch(); + return 0; } #else #include <unistd.h> -int getch() +int getch_timeout(int timeout_ms) { - return getchar(); + (void)timeout_ms; // bounded by VMIN=0/VTIME in terminal_init() + int c = getchar(); + return (c == EOF) ? 0 : c; } #endif typedef enum

@@ -673,31 +680,44 @@ *pc = on_arg_handler;

return 1; } - // Keypress handling + // Keypress handling (non-blocking: returns 0 on timeout) xyw_word on_keypress_handler = XYW_PEEKW(&xyw_memory[TERMINAL_ON_KEYPRESS]); - if (on_keypress_handler) - { - int c = getch(); - if (c == 3 || c == 4) return 0; // CTRL+C or CTRL+D - xyw_memory[TERMINAL_INPUT] = (xyw_byte)c; - xyw_memory[SYSTEM_STATE] |= SYSTEM_STATE_RUNNING; - // Sentinel return: handler's RTS returns to device space, cleanly ending dispatch - ss_push_frame(DEVICE_AREA_START); - *pc = on_keypress_handler; - return 1; - } - // Timer handling (readb blocks until the timer elapses if active) + // Timer handling xyw_word on_timer_handler = XYW_PEEKW(&xyw_memory[CLOCK_ON_TIMER_ELAPSED]); - if (on_timer_handler) + + // Polling loop: keep checking until an event fires or exit is requested + while (on_keypress_handler || on_timer_handler) { - if (readb(CLOCK_ON_TIMER_ELAPSED)) + if (on_keypress_handler) + { + int c = getch_timeout(POLL_INTERVAL_MS); + if (c > 0) + { + if (c == 3 || c == 4) return 0; // CTRL+C or CTRL+D + xyw_memory[TERMINAL_INPUT] = (xyw_byte)c; + xyw_memory[SYSTEM_STATE] |= SYSTEM_STATE_RUNNING; + // Sentinel return: handler's RTS returns to device space, cleanly ending dispatch + ss_push_frame(DEVICE_AREA_START); + *pc = on_keypress_handler; + return 1; + } + // c == 0: no key this cycle — fall through to timer check + } + + if (on_timer_handler) { - xyw_memory[SYSTEM_STATE] |= SYSTEM_STATE_RUNNING; - // Sentinel return: handler's RTS returns to device space, cleanly ending dispatch - ss_push_frame(DEVICE_AREA_START); - *pc = on_timer_handler; - return 1; + int elapsed = on_keypress_handler + ? clock_check_elapsed() // non-blocking: keypress handler also armed + : readb(CLOCK_ON_TIMER_ELAPSED); // blocking: timer-only program + if (elapsed) + { + xyw_memory[SYSTEM_STATE] |= SYSTEM_STATE_RUNNING; + // Sentinel return: handler's RTS returns to device space, cleanly ending dispatch + ss_push_frame(DEVICE_AREA_START); + *pc = on_timer_handler; + return 1; + } } }