aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-16 08:25:35 +0200
committeruser <user@clank>2026-07-16 08:25:35 +0200
commit6c64d97e70984c77be3cd8d4e03e9adf717e02fb (patch)
treefd55f0ef943fef12a833ab526ebfe6bc5740f728
parentfilesystem: a RAM FS (WRAMX) + ls/cat/save/rm; wc/head read files (diff)
downloadgbos-6c64d97e70984c77be3cd8d4e03e9adf717e02fb.tar.gz
gbos-6c64d97e70984c77be3cd8d4e03e9adf717e02fb.tar.xz
gbos-6c64d97e70984c77be3cd8d4e03e9adf717e02fb.zip
shell: I/O redirection (>/<) and pipes (|); rewrite shell in C
- kernel I/O routing: PCB gains PROC_STDIN/PROC_STDOUT (default console $FF), inherited across fork. KGetc/KPutc route read/write/putc to the console or a file fd. New syscalls: putc(16), setin(17), setout(18), lookup(19). read/write now go through the routing; putc/puts/nl use SYS_PUTC. - sys_lookup + NameTable move command-name resolution into the kernel. - shell rewritten in C (c/sh.c): tokenizes a line, parses > / < / |, and drives fork+setin/setout+exec+wait. Pipes run as 'a > __pipe ; b < __pipe' (temp file). asm shell + cmdtab removed; StrEqual/SkipName kept for sys_lookup. - libc: fork/exec/wait/setin/setout/lookup wrappers. - verified: echo>file, cat file, wc<file, cat readme|wc -l, echo ...|wc.
-rw-r--r--Makefile2
-rw-r--r--README.md37
-rw-r--r--c/gbos.h8
-rw-r--r--c/libc.s67
-rw-r--r--c/sh.c99
-rw-r--r--include/gbos.inc10
-rw-r--r--src/kdata.asm1
-rw-r--r--src/proc.asm19
-rw-r--r--src/programs.asm181
-rw-r--r--src/syscall.asm146
10 files changed, 386 insertions, 184 deletions
diff --git a/Makefile b/Makefile
index 8c38b3c..9b871fa 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ $(BUILD)/%.o: src/%.asm | $(BUILD)
# C programs: compiled with SDCC into ROM-bank blobs and INCBIN'd by programs.asm
CBLOBS := c/chello.bin c/echo.bin c/true.bin c/false.bin c/uname.bin \
c/pid.bin c/cat.bin c/wc.bin c/head.bin c/args.bin \
- c/ls.bin c/save.bin c/rm.bin
+ c/ls.bin c/save.bin c/rm.bin c/sh.bin
$(BUILD)/programs.o: $(CBLOBS)
# C programs also depend on the shared libc sources
diff --git a/README.md b/README.md
index 85a01d2..b0a2edf 100644
--- a/README.md
+++ b/README.md
@@ -58,22 +58,33 @@ across a syscall (`push hl` / `pop hl`). Libc syscall stubs handle this for C.
| 2 | read | ✅ blocking console input (1 byte) via external-clock serial |
| 4,5,9,10 | open/close/kill/brk | ⛔ `ENOSYS` |
-### The shell (PROG_SH)
+### The shell (`c/sh.c`)
-init `exec`s a tiny shell: **prompt → read a line (with echo) → match a command
-→ `fork`+`exec`+`wait`**. Commands live in a table (`worker`, `hello`). Runs
-entirely over the headless serial console:
+init `exec`s the shell (written in C). It reads a line, tokenizes it, and runs
+commands with **I/O redirection and pipes**:
```
-$ hello
-hello world!
-$ worker
-worker!
-$ foo
-?
-$
+$ echo hello > note # redirect stdout to a file
+$ cat note
+hello
+$ wc < note # redirect stdin from a file
+1 1 6
+$ cat readme | wc -l # pipe (via a temp file)
+2
+$ echo one two three | wc
+1 3 14
```
+Operators must be space-separated (`cat readme > out`). Command names are
+resolved by the kernel (`sys_lookup` + `NameTable`).
+
+**How I/O routing works.** Each process has a `stdin`/`stdout` fd in its PCB
+(default `$FF` = console). `read`/`write`/`putc` route through the kernel
+(`KGetc`/`KPutc`): console when the fd is `$FF`, otherwise the filesystem
+(`getb`/`putb`). The shell forks a child, the child `setin`/`setout`s to open
+files, then `exec`s — so the program's I/O lands in the file. A pipe
+`a | b` is run as `a > __pipe ; b < __pipe ; rm __pipe`.
+
### Process lifecycle (exit / wait / reaping)
The classic Unix zombie/reap dance, adapted to banked memory:
@@ -295,7 +306,9 @@ $ rm notes
- [x] `wc`, `head`, and an `argc/argv` demo (`args`) + `argv_parse` in libc
- [x] option parsing (`hasflag`/`optval`): `wc -l/-w/-c`, `head -n N`
- [x] a RAM filesystem (WRAMX) + `ls`/`cat`/`save`/`rm`; `wc`/`head` read files
-- [ ] more tools (`rev`, `grep`, `tail`), shell redirection (`>`/`|`)
+- [x] shell in C with redirection (`>`/`<`) and pipes (`|`, via a temp file)
+- [x] per-process stdin/stdout routing (`KGetc`/`KPutc`, `setin`/`setout`)
+- [ ] more tools (`rev`, `grep`, `tail`), true concurrent pipes
- [ ] battery-backed files (move the FS to cart SRAM), directories
- [ ] Preemptive scheduling: real context save in `TimerISR` → `hSwitchTo`
- [ ] Use the `$D000-$DFFF` u-area (SVBK) for per-process kernel state / kstack
diff --git a/c/gbos.h b/c/gbos.h
index d8c8eb3..fa926fd 100644
--- a/c/gbos.h
+++ b/c/gbos.h
@@ -30,4 +30,12 @@ void fremove(const char *name);
#define O_READ 0
#define O_WRITE 1
#define NOFD 0xFF
+
+/* process control (c/libc.s) */
+unsigned char fork(void);
+void exec(unsigned char id); /* replace image; never returns */
+unsigned char wait(void);
+void setin(unsigned char fd); /* redirect stdin ($FF=console) */
+void setout(unsigned char fd); /* redirect stdout ($FF=console) */
+unsigned char lookup(const char *name); /* command name -> program id */
#endif
diff --git a/c/libc.s b/c/libc.s
index a26042e..857a16d 100644
--- a/c/libc.s
+++ b/c/libc.s
@@ -25,11 +25,11 @@ _getargs:
ld c, l
ret
- ;; void putc(char c /*A*/)
+ ;; void putc(char c /*A*/) -> stdout via SYS_PUTC
_putc:
- ld (0xff01), a
- ld a, #0x81
- ld (0xff02), a
+ ld e, a
+ ld c, #16 ; SYS_PUTC
+ rst #0x30
ret
;; void puts(const char *s /*DE*/) - write until NUL (no newline)
@@ -37,22 +37,22 @@ _puts:
1$: ld a, (de)
or a
ret z
- ld (0xff01), a
- ld a, #0x81
- ld (0xff02), a
+ push de
+ ld e, a
+ ld c, #16 ; SYS_PUTC
+ rst #0x30
+ pop de
inc de
jr 1$
- ;; void nl(void) - CRLF
+ ;; void nl(void) - CRLF via stdout
_nl:
- ld a, #0x0d
- ld (0xff01), a
- ld a, #0x81
- ld (0xff02), a
- ld a, #0x0a
- ld (0xff01), a
- ld a, #0x81
- ld (0xff02), a
+ ld e, #0x0d
+ ld c, #16 ; SYS_PUTC
+ rst #0x30
+ ld e, #0x0a
+ ld c, #16
+ rst #0x30
ret
;; unsigned char strlen(const char *s /*DE*/) -> A
@@ -132,3 +132,38 @@ _fremove:
ld c, #15 ; SYS_REMOVE
rst #0x30
ret
+
+ .globl _fork, _exec, _wait, _setin, _setout, _lookup
+ ;; unsigned char fork(void) -> A (child pid in parent, 0 in child)
+_fork:
+ ld c, #1 ; SYS_FORK
+ rst #0x30
+ ret
+ ;; void exec(unsigned char id /*A*/) - never returns
+_exec:
+ ld b, a
+ ld c, #6 ; SYS_EXEC
+ rst #0x30
+ ret
+ ;; unsigned char wait(void) -> A (reaped pid)
+_wait:
+ ld c, #7 ; SYS_WAIT
+ rst #0x30
+ ret
+ ;; void setin(unsigned char fd /*A*/)
+_setin:
+ ld b, a
+ ld c, #17 ; SYS_SETIN
+ rst #0x30
+ ret
+ ;; void setout(unsigned char fd /*A*/)
+_setout:
+ ld b, a
+ ld c, #18 ; SYS_SETOUT
+ rst #0x30
+ ret
+ ;; unsigned char lookup(const char *name /*DE*/) -> A (prog id or $FF)
+_lookup:
+ ld c, #19 ; SYS_LOOKUP
+ rst #0x30
+ ret
diff --git a/c/sh.c b/c/sh.c
new file mode 100644
index 0000000..c24ed9f
--- /dev/null
+++ b/c/sh.c
@@ -0,0 +1,99 @@
+#include "gbos.h"
+/* gbos shell: prompt -> read a line -> parse -> run, with I/O redirection
+ ( > file, < file ) and pipes ( cmd1 | cmd2, via a temp file ).
+ Operators must be space-separated (e.g. cat readme > out ). */
+
+#define ARGV0 ((char *)0xA000) /* where a child reads its command line */
+
+/* split s into space-separated tokens (NUL-terminated in place). */
+static unsigned char tokenize(char *s, char **tok, unsigned char max) {
+ unsigned char n = 0;
+ for (;;) {
+ while (*s == ' ') *s++ = 0;
+ if (*s == 0) break;
+ if (n < max) tok[n++] = s;
+ while (*s && *s != ' ') s++;
+ }
+ return n;
+}
+
+static unsigned char isop(char *t, char c) { return t[0] == c && t[1] == 0; }
+
+/* build the child command line "name\0arg1 arg2\0" at 0xA000 */
+static void set_cmdline(char **tok, unsigned char start, unsigned char end) {
+ char *p = ARGV0, *t;
+ unsigned char i;
+ t = tok[start];
+ while (*t) *p++ = *t++;
+ *p++ = 0; /* NUL after the command word */
+ for (i = start + 1; i < end; i++) {
+ if (i > start + 1) *p++ = ' ';
+ t = tok[i];
+ while (*t) *p++ = *t++;
+ }
+ *p = 0;
+}
+
+/* run tok[start..end): parse its own >/< redirects, honor forced in/out. */
+static void run(char **tok, unsigned char start, unsigned char end,
+ unsigned char infd, unsigned char outfd) {
+ unsigned char i, cmdend = end, myin = NOFD, myout = NOFD, id, pid;
+ for (i = start; i < end; i++) {
+ if (isop(tok[i], '>')) {
+ if (i + 1 < end) myout = open(tok[i + 1], O_WRITE);
+ if (i < cmdend) cmdend = i;
+ } else if (isop(tok[i], '<')) {
+ if (i + 1 < end) myin = open(tok[i + 1], O_READ);
+ if (i < cmdend) cmdend = i;
+ }
+ }
+ id = lookup(tok[start]);
+ if (id == NOFD) { puts(tok[start]); puts(": not found"); nl(); goto done; }
+ set_cmdline(tok, start, cmdend);
+ pid = fork();
+ if (pid == 0) {
+ if (myin != NOFD) setin(myin);
+ else if (infd != NOFD) setin(infd);
+ if (myout != NOFD) setout(myout);
+ else if (outfd != NOFD) setout(outfd);
+ exec(id);
+ }
+ wait();
+done:
+ if (myin != NOFD) close(myin);
+ if (myout != NOFD) close(myout);
+}
+
+void main(void) {
+ char line[80];
+ char *tok[16];
+ for (;;) {
+ unsigned char nt, i, pipepos, c;
+ puts("$ ");
+ /* read a line (echoing), stop at newline; exit on EOF */
+ i = 0;
+ for (;;) {
+ c = readc();
+ if (c == EOF) return;
+ if (c == '\r' || c == '\n') { nl(); break; }
+ if (i < 79) { line[i++] = c; putc(c); }
+ }
+ line[i] = 0;
+ nt = tokenize(line, tok, 16);
+ if (nt == 0) continue;
+ pipepos = 0;
+ for (i = 1; i < nt; i++)
+ if (isop(tok[i], '|')) { pipepos = i; break; }
+ if (pipepos) {
+ unsigned char pw = open("__pipe", O_WRITE);
+ run(tok, 0, pipepos, NOFD, pw);
+ close(pw);
+ { unsigned char pr = open("__pipe", O_READ);
+ run(tok, pipepos + 1, nt, pr, NOFD);
+ close(pr); }
+ fremove("__pipe");
+ } else {
+ run(tok, 0, nt, NOFD, NOFD);
+ }
+ }
+}
diff --git a/include/gbos.inc b/include/gbos.inc
index 7c3f325..6aaaf73 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -67,8 +67,12 @@ DEF PROC_RAMB RB 1 ; data/stack cart-RAM bank
DEF PROC_WRAMB RB 1 ; u-area SVBK bank (1..7)
DEF PROC_PARENT RB 1
DEF PROC_EXIT RB 1 ; exit code (valid when PS_ZOMBIE)
+DEF PROC_STDIN RB 1 ; $FF = console, else an open-file fd
+DEF PROC_STDOUT RB 1 ; $FF = console, else an open-file fd
DEF PROC_SIZE RB 0
+DEF STD_CONSOLE EQU $FF
+
; -----------------------------------------------------------------------------
; Syscall numbers (passed in C; args in DE/HL/B per call, ret in A)
; -----------------------------------------------------------------------------
@@ -88,7 +92,11 @@ DEF SYS_GETB EQU 12 ; read a byte from an open file (B=fd -> A, CF=EOF)
DEF SYS_PUTB EQU 13 ; append a byte to an open file (B=fd, E=byte)
DEF SYS_LIST EQU 14 ; list a directory slot (B=slot, DE=namebuf)
DEF SYS_REMOVE EQU 15 ; delete a file (DE=name)
-DEF SYS_MAX EQU 16
+DEF SYS_PUTC EQU 16 ; write a byte to stdout (E=byte)
+DEF SYS_SETIN EQU 17 ; set current stdin fd (B=fd, $FF=console)
+DEF SYS_SETOUT EQU 18 ; set current stdout fd (B=fd, $FF=console)
+DEF SYS_LOOKUP EQU 19 ; program id for a command name (DE=name -> A=id/$FF)
+DEF SYS_MAX EQU 20
; (SYS_OPEN=4 / SYS_CLOSE=5 are now implemented by the filesystem)
; -----------------------------------------------------------------------------
diff --git a/src/kdata.asm b/src/kdata.asm
index 9aca466..ea18756 100644
--- a/src/kdata.asm
+++ b/src/kdata.asm
@@ -45,3 +45,4 @@ wFsMode:: DS 1
wFsSlot:: DS 1
wFsByte:: DS 1
wFsOFPtr:: DS 2
+wKChar:: DS 1 ; scratch for KPutc
diff --git a/src/proc.asm b/src/proc.asm
index a2b1368..ab1c807 100644
--- a/src/proc.asm
+++ b/src/proc.asm
@@ -221,7 +221,10 @@ ProcCreate::
ld [hl+], a ; PROC_WRAMB
xor a
ld [hl+], a ; PROC_PARENT
- ld [hl], a ; PROC_EXIT
+ ld [hl+], a ; PROC_EXIT
+ ld a, STD_CONSOLE
+ ld [hl+], a ; PROC_STDIN = console
+ ld [hl], a ; PROC_STDOUT = console
; build the initial stack frame in the task's RAM bank
ld a, [wTmpEntry]
@@ -438,7 +441,19 @@ sys_fork::
ld [hl+], a
; PROC_EXIT = 0
xor a
- ld [hl], a
+ ld [hl+], a
+ ; PROC_STDIN / PROC_STDOUT = inherit from parent
+ ld a, [wCurProc]
+ add PROC_STDIN
+ ld e, a
+ ld a, [wCurProc+1]
+ adc 0
+ ld d, a
+ ld a, [de]
+ ld [hl+], a ; child STDIN = parent STDIN
+ inc de
+ ld a, [de]
+ ld [hl], a ; child STDOUT = parent STDOUT
; ---- return to the parent with child pid ----
ld a, [wForkUserSP]
diff --git a/src/programs.asm b/src/programs.asm
index 161ee2f..1a7092e 100644
--- a/src/programs.asm
+++ b/src/programs.asm
@@ -90,149 +90,13 @@ ProgRm:
INCBIN "c/rm.bin"
; -----------------------------------------------------------------------------
-; PROG_SH (bank 3) - a tiny shell.
-; prompt -> read a line (with echo) -> match a command -> fork+exec+wait.
+; PROG_SH (bank 3) - the shell, now written in C (c/sh.c): parses >/</| and
+; drives fork+exec+wait with I/O redirection. Command lookup + StrEqual/SkipName
+; live in the kernel now (sys_lookup + NameTable + shell_lib).
; -----------------------------------------------------------------------------
SECTION "prog_sh", ROMX[$4000], BANK[3]
ProgSh:
-.prompt
- ld de, .ps1
- ld b, 2
- ld c, SYS_WRITE
- rst $30
-
- ; read a line into SH_LINEBUF, echoing as we go
- ld hl, SH_LINEBUF
-.rd
- push hl ; syscalls clobber HL (trap uses it to dispatch)
- ld c, SYS_READ
- rst $30 ; A = input byte
- pop hl
- cp $04
- jr z, .eof ; Ctrl-D / EOF: exit the shell
- cp $0D
- jr z, .eol
- cp $0A
- jr z, .eol
- ld [hl], a ; store in the line buffer (A preserved)
- inc hl
- ld [$FF01], a ; echo the char straight to serial
- ld a, $81
- ld [$FF02], a
- ld a, l
- cp LOW(SH_LINEBUF) + SH_MAXLINE - 1
- jr c, .rd ; keep reading unless the buffer is full
-
-.eof
- ld b, 0
- ld c, SYS_EXIT ; exit init/shell -> system idle
- rst $30
-
-.eol
- ld [hl], 0 ; NUL-terminate
- ld de, .crlf
- ld b, 2
- ld c, SYS_WRITE
- rst $30
- ld a, [SH_LINEBUF]
- or a
- jp z, .prompt ; empty line
-
- ; split off the first word: replace the first space with a NUL so command
- ; matching sees just the command, and the args survive at SH_LINEBUF+n
- ; (inherited by the child through fork, read back via main(char *args)).
- ld hl, SH_LINEBUF
-.split
- ld a, [hl]
- or a
- jr z, .lookup ; no space -> no args
- cp $20
- jr z, .cut
- inc hl
- jr .split
-.cut
- ld [hl], 0 ; isolate the command word
-
-.lookup
- ; look the command up in the table
- ld de, .cmdtab
-.scan
- ld a, [de]
- or a
- jr z, .unknown ; empty name = end of table
- push de
- ld hl, SH_LINEBUF
- call StrEqual ; Z set if equal
- pop de
- jr z, .match
- call SkipName ; DE -> NUL after this name
- inc de ; -> program id
- inc de ; -> next entry
- jr .scan
-
-.match
- call SkipName
- inc de ; DE -> program id
- ld a, [de]
- ld [SH_PROGID], a
- ld c, SYS_FORK
- rst $30
- or a
- jr z, .exec
- ld c, SYS_WAIT ; parent: reap the command
- rst $30
- jp .prompt
-.exec
- ld a, [SH_PROGID]
- ld b, a
- ld c, SYS_EXEC
- rst $30 ; child becomes the command
-
-.unknown
- ld de, .huh
- ld b, 3
- ld c, SYS_WRITE
- rst $30
- jp .prompt
-
-.ps1
- db "$ "
-.crlf
- db $0D, $0A
-.huh
- db "?", $0D, $0A
-.cmdtab
- db "worker", 0
- db PROG_WORKER
- db "hello", 0
- db PROG_HELLO
- db "chello", 0
- db PROG_CHELLO
- db "echo", 0
- db PROG_ECHO
- db "true", 0
- db PROG_TRUE
- db "false", 0
- db PROG_FALSE
- db "uname", 0
- db PROG_UNAME
- db "pid", 0
- db PROG_PID
- db "cat", 0
- db PROG_CAT
- db "wc", 0
- db PROG_WC
- db "head", 0
- db PROG_HEAD
- db "args", 0
- db PROG_ARGS
- db "ls", 0
- db PROG_LS
- db "save", 0
- db PROG_SAVE
- db "rm", 0
- db PROG_RM
- db 0 ; terminator
+ INCBIN "c/sh.bin"
; -----------------------------------------------------------------------------
; Shell string helpers (ROM0, always mapped).
@@ -298,3 +162,40 @@ ProgramTable::
dw ProgSave
db LOW(BANK(ProgRm)), HIGH(BANK(ProgRm))
dw ProgRm
+
+; -----------------------------------------------------------------------------
+; NameTable (ROM0) - command name -> program id, searched by sys_lookup.
+; -----------------------------------------------------------------------------
+SECTION "nametab", ROM0
+NameTable::
+ db "worker", 0
+ db PROG_WORKER
+ db "hello", 0
+ db PROG_HELLO
+ db "chello", 0
+ db PROG_CHELLO
+ db "echo", 0
+ db PROG_ECHO
+ db "true", 0
+ db PROG_TRUE
+ db "false", 0
+ db PROG_FALSE
+ db "uname", 0
+ db PROG_UNAME
+ db "pid", 0
+ db PROG_PID
+ db "cat", 0
+ db PROG_CAT
+ db "wc", 0
+ db PROG_WC
+ db "head", 0
+ db PROG_HEAD
+ db "args", 0
+ db PROG_ARGS
+ db "ls", 0
+ db PROG_LS
+ db "save", 0
+ db PROG_SAVE
+ db "rm", 0
+ db PROG_RM
+ db 0
diff --git a/src/syscall.asm b/src/syscall.asm
index 5ed3d6f..f149a76 100644
--- a/src/syscall.asm
+++ b/src/syscall.asm
@@ -50,6 +50,10 @@ SyscallTable:
dw sys_putb ; 13 PUTB
dw sys_list ; 14 LIST
dw sys_remove ; 15 REMOVE
+ dw sys_putc ; 16 PUTC
+ dw sys_setin ; 17 SETIN
+ dw sys_setout ; 18 SETOUT
+ dw sys_lookup ; 19 LOOKUP
; -----------------------------------------------------------------------------
sys_nosys:
@@ -64,33 +68,151 @@ sys_write:
ld a, b
or a
ret z ; len 0
- ld c, b ; C = remaining
.loop
ld a, [de]
- call PutChar
+ push de
+ push bc
+ ld e, a
+ call KPutc ; route the byte to this process's stdout
+ pop bc
+ pop de
inc de
- dec c
+ dec b
jr nz, .loop
- ld a, b ; return len
ret
; -----------------------------------------------------------------------------
-; sys_read() -> A = one input byte from the console (blocking).
-; Uses an external-clock serial transfer as a clean RX: it completes only when
-; the host has a byte (no stdout echo). While waiting we yield so other procs
-; run. Preserves BC/DE/HL (the context switch saves/restores them).
+; CurStdin / CurStdout -> A = current process PROC_STDIN / PROC_STDOUT
; -----------------------------------------------------------------------------
-sys_read:
+CurStdin:
+ ld a, [wCurProc]
+ add PROC_STDIN
+ ld l, a
+ ld a, [wCurProc+1]
+ adc 0
+ ld h, a
+ ld a, [hl]
+ ret
+CurStdout:
+ ld a, [wCurProc]
+ add PROC_STDOUT
+ ld l, a
+ ld a, [wCurProc+1]
+ adc 0
+ ld h, a
+ ld a, [hl]
+ ret
+
+; -----------------------------------------------------------------------------
+; KGetc -> A = byte, CF set on EOF. Reads this process's stdin (console/file).
+; -----------------------------------------------------------------------------
+KGetc::
+ call CurStdin
+ cp STD_CONSOLE
+ jr z, .console
+ ld b, a
+ jp sys_getb ; B=fd -> A=byte, CF=EOF
+.console
.poll
ld a, $80
- ld [rSC], a ; request a byte (external clock)
+ ld [rSC], a
ld a, [rSC]
bit 7, a
- jr z, .got ; bit7 clear => transfer completed
- call SchedYield ; nothing yet: let others run, then retry
+ jr z, .got
+ call SchedYield
jr .poll
.got
ld a, [rSB]
+ and a ; CF = 0
+ ret
+
+; -----------------------------------------------------------------------------
+; KPutc(E = byte) - write to this process's stdout (console/file).
+; -----------------------------------------------------------------------------
+KPutc::
+ ld a, e
+ ld [wKChar], a
+ call CurStdout
+ cp STD_CONSOLE
+ jr z, .console
+ ld b, a ; fd
+ ld a, [wKChar]
+ ld e, a
+ jp sys_putb ; B=fd, E=byte
+.console
+ ld a, [wKChar]
+ ld [rSB], a
+ ld a, $81
+ ld [rSC], a
+ ret
+
+; -----------------------------------------------------------------------------
+; sys_putc(E = byte), sys_setin/sys_setout(B = fd)
+; -----------------------------------------------------------------------------
+sys_putc:
+ call KPutc
+ ret
+sys_setin:
+ ld a, [wCurProc]
+ add PROC_STDIN
+ ld l, a
+ ld a, [wCurProc+1]
+ adc 0
+ ld h, a
+ ld a, b
+ ld [hl], a
+ ret
+sys_setout:
+ ld a, [wCurProc]
+ add PROC_STDOUT
+ ld l, a
+ ld a, [wCurProc+1]
+ adc 0
+ ld h, a
+ ld a, b
+ ld [hl], a
+ ret
+
+; -----------------------------------------------------------------------------
+; sys_lookup(DE = name) -> A = program id, or $FF if not a known command.
+; -----------------------------------------------------------------------------
+sys_lookup:
+ ld h, d
+ ld l, e ; HL = query name
+ ld de, NameTable
+.l
+ ld a, [de]
+ or a
+ jr z, .none
+ push hl
+ push de
+ call StrEqual ; HL=query vs DE=table name -> Z if equal
+ pop de
+ pop hl
+ jr z, .match
+ call SkipName ; DE -> NUL
+ inc de ; -> id
+ inc de ; -> next entry
+ jr .l
+.match
+ call SkipName
+ inc de ; -> id
+ ld a, [de]
+ ret
+.none
+ ld a, $FF
+ ret
+
+; -----------------------------------------------------------------------------
+; sys_read() -> A = one input byte from the console (blocking).
+; Uses an external-clock serial transfer as a clean RX: it completes only when
+; the host has a byte (no stdout echo). While waiting we yield so other procs
+; run. Preserves BC/DE/HL (the context switch saves/restores them).
+; -----------------------------------------------------------------------------
+sys_read:
+ call KGetc ; A=byte, CF=EOF (file); console EOF is $04
+ ret nc
+ ld a, $04 ; file EOF -> EOT
ret
; -----------------------------------------------------------------------------