aboutsummaryrefslogtreecommitdiffstats
path: root/usr
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-18 00:38:38 +0200
committeruser <user@clank>2026-07-18 00:38:38 +0200
commitba2d7ae1d7b319645b10aaa31a7f664f9f80c25c (patch)
treee01e6c7c710a661defe2515ff9657f08cddcb283 /usr
parenttools: gbtype prints hub errors to stderr and exits 1 (diff)
downloadgbos-ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c.tar.gz
gbos-ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c.tar.xz
gbos-ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c.zip
refactor: c/ -> usr/, compiled program blobs out of the source tree
Userland sources live in usr/ (fits the Unix theme better than 'c'). SDCC output .bin blobs land in build/usr/ with the other build artifacts instead of littering the source dir; programs.asm INCBINs them from there. Byte-identical ROM.
Diffstat (limited to 'usr')
-rw-r--r--usr/args.c12
-rw-r--r--usr/blk.c13
-rwxr-xr-xusr/build.sh21
-rw-r--r--usr/cat.c17
-rw-r--r--usr/chat.c57
-rw-r--r--usr/chello.c9
-rw-r--r--usr/count.c16
-rw-r--r--usr/crt0.s11
-rw-r--r--usr/dhcp.c72
-rw-r--r--usr/echo.c5
-rw-r--r--usr/false.c2
-rw-r--r--usr/gbos.h64
-rw-r--r--usr/head.c28
-rw-r--r--usr/irc.c422
-rw-r--r--usr/kill.c10
-rw-r--r--usr/libc.c67
-rw-r--r--usr/libc.s304
-rw-r--r--usr/ls.c10
-rw-r--r--usr/mkdir.c8
-rw-r--r--usr/necho.c13
-rw-r--r--usr/netd.c12
-rw-r--r--usr/netlib.h39
-rw-r--r--usr/nslookup.c23
-rw-r--r--usr/pid.c2
-rw-r--r--usr/ping.c61
-rw-r--r--usr/ps.c14
-rw-r--r--usr/ptest.c19
-rw-r--r--usr/resolve.h81
-rw-r--r--usr/rm.c7
-rw-r--r--usr/save.c19
-rw-r--r--usr/sh.c200
-rw-r--r--usr/sock.h71
-rw-r--r--usr/spin.c6
-rw-r--r--usr/true.c2
-rw-r--r--usr/uname.c2
-rw-r--r--usr/wc.c32
-rw-r--r--usr/wget.c43
37 files changed, 1794 insertions, 0 deletions
diff --git a/usr/args.c b/usr/args.c
new file mode 100644
index 0000000..c6f4db3
--- /dev/null
+++ b/usr/args.c
@@ -0,0 +1,12 @@
+#include "gbos.h"
+/* args: an argc/argv demo. Splits its argument string into argv[] and prints
+ argc plus each argument. Try: `args one two three` */
+void main(void) {
+ char *argv[8];
+ unsigned char argc = argv_parse(argv, 8);
+ unsigned char i;
+ puts("argc="); putu(argc); nl();
+ for (i = 0; i < argc; i++) {
+ puts("argv["); putu(i); puts("]="); puts(argv[i]); nl();
+ }
+}
diff --git a/usr/blk.c b/usr/blk.c
new file mode 100644
index 0000000..36839c0
--- /dev/null
+++ b/usr/blk.c
@@ -0,0 +1,13 @@
+#include "gbos.h"
+/* blk: exercise the block device. blk fill <n> <v> | blk peek <n> */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ if (argc >= 3 && argv[0][0] == 'f') {
+ bfill(atou(argv[1]), atou(argv[2]));
+ } else if (argc >= 2 && argv[0][0] == 'p') {
+ putu(bpeek2(atou(argv[1]), argc>=3?atou(argv[2]):0)); nl();
+ } else {
+ puts("usage: blk fill <n> <v> | blk peek <n>"); nl();
+ }
+}
diff --git a/usr/build.sh b/usr/build.sh
new file mode 100755
index 0000000..93815d2
--- /dev/null
+++ b/usr/build.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+# Build a gbos C program into a self-contained 16 KiB ROM-bank image.
+# SDCC native toolchain; crt0 linked FIRST so _start is the $4000 entry.
+# usage: usr/build.sh <prog.c> <out.bin> (run from the repo root)
+set -e
+SRC="$1"; OUT="$2"; B=build/usr/obj
+LIBDIR=/usr/share/sdcc/lib/sm83
+mkdir -p "$B" "$(dirname "$OUT")"
+sdasgb -o "$B/crt0.rel" usr/crt0.s
+sdasgb -o "$B/libc.rel" usr/libc.s
+sdcc -msm83 -c -Iusr usr/libc.c -o "$B/libc_c.rel"
+sdcc -msm83 -c -Iusr "$SRC" -o "$B/prog.rel"
+# link: crt0 first (=> _start at 0x4000), then libc, program, and sm83 lib
+sdldgb -n -m -w -j -i "$B/prog.ihx" \
+ -b _CODE=0x4000 -b _DATA=0xa000 \
+ -k "$LIBDIR" -l sm83 \
+ "$B/crt0.rel" "$B/libc.rel" "$B/libc_c.rel" "$B/prog.rel"
+makebin -Z -p "$B/prog.ihx" "$B/prog.bin"
+dd if="$B/prog.bin" of="$OUT" bs=1024 skip=16 count=16 2>/dev/null
+echo "wrote $OUT ($(wc -c < "$OUT") bytes); entry:"
+grep -iE ' _start| _main' "$B/prog.map" | head -2
diff --git a/usr/cat.c b/usr/cat.c
new file mode 100644
index 0000000..790cb88
--- /dev/null
+++ b/usr/cat.c
@@ -0,0 +1,17 @@
+#include "gbos.h"
+/* cat [file]: print a file, or copy stdin to stdout until EOF. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ if (argc > 0) {
+ unsigned char fd = open(argv[0], O_READ);
+ int ch;
+ if (fd == EISDIR) { puts("cat: is a directory"); nl(); return; }
+ if (fd == NOFD) { puts("cat: no such file"); nl(); return; }
+ while ((ch = fgetc(fd)) >= 0) putc((char)ch);
+ close(fd);
+ } else {
+ char c;
+ for (;;) { c = readc(); if (c == EOF) break; putc(c); }
+ }
+}
diff --git a/usr/chat.c b/usr/chat.c
new file mode 100644
index 0000000..987b336
--- /dev/null
+++ b/usr/chat.c
@@ -0,0 +1,57 @@
+#include "netlib.h"
+/* chat: a link-port chat client. Incoming SLIP frames from the host gateway
+ print as messages; SELECT brings up the on-screen keyboard, type a line and
+ press START to send it. A poll loop interleaves receiving and typing so
+ messages arrive while you're not mid-line. */
+
+static char rxbuf[96];
+static unsigned char rxlen, rxin, rxesc;
+
+/* feed one non-blocking byte to the SLIP receiver; returns a frame length when
+ a whole frame has arrived (copied into out), else 0. */
+static unsigned char slip_poll(char *out) {
+ int b = srecv_nb();
+ unsigned char c, i, n;
+ if (b < 0) return 0;
+ c = (unsigned char)b;
+ if (c == SLIP_END) {
+ if (rxin && rxlen) {
+ n = rxlen;
+ for (i = 0; i < n; i++) out[i] = rxbuf[i];
+ rxlen = 0; rxin = 0; rxesc = 0;
+ return n;
+ }
+ rxin = 1; rxlen = 0; rxesc = 0;
+ return 0;
+ }
+ if (!rxin) return 0;
+ if (rxesc) {
+ if (c == SLIP_ESC_END) c = SLIP_END;
+ else if (c == SLIP_ESC_ESC) c = SLIP_ESC;
+ rxesc = 0;
+ } else if (c == SLIP_ESC) { rxesc = 1; return 0; }
+ if (rxlen < 96) rxbuf[rxlen++] = (char)c;
+ return 0;
+}
+
+void main(void) {
+ char line[64], frame[96];
+ unsigned char linelen = 0, n, i, c;
+ puts("chat: SELECT=keyboard START=send"); nl();
+ for (;;) {
+ n = slip_poll(frame); /* incoming message? */
+ if (n) {
+ for (i = 0; i < n; i++) putc(frame[i]);
+ nl();
+ }
+ c = pollin(); /* typed a key? */
+ if (c == '\n') {
+ if (linelen) { slip_send(line, linelen); linelen = 0; }
+ } else if (c == 8) {
+ if (linelen) { linelen--; putc(8); }
+ } else if (c) {
+ if (linelen < 63) { line[linelen++] = (char)c; putc((char)c); }
+ }
+ yield();
+ }
+}
diff --git a/usr/chello.c b/usr/chello.c
new file mode 100644
index 0000000..f570b61
--- /dev/null
+++ b/usr/chello.c
@@ -0,0 +1,9 @@
+#include "gbos.h"
+void main(void) {
+ writes("hello from C on gbos!", 21);
+ nl();
+ writes("my pid is ", 10);
+ /* print pid as a digit */
+ { unsigned char c = getpid() + '0'; char d = c; writes(&d, 1); }
+ nl();
+}
diff --git a/usr/count.c b/usr/count.c
new file mode 100644
index 0000000..37cdcce
--- /dev/null
+++ b/usr/count.c
@@ -0,0 +1,16 @@
+#include "gbos.h"
+/* count [n]: print n numbered lines (default 25) to observe terminal scrolling
+ and the OSK scroll-region. Each line is tagged so you can tell which lines
+ are visible after scrolling. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ unsigned char n = 25, i;
+ if (argc >= 1) n = atou(argv[0]);
+ for (i = 1; i <= n; i++) {
+ puts("line ");
+ putu(i);
+ puts(" ------------------");
+ nl();
+ }
+}
diff --git a/usr/crt0.s b/usr/crt0.s
new file mode 100644
index 0000000..0d8371f
--- /dev/null
+++ b/usr/crt0.s
@@ -0,0 +1,11 @@
+ .module crt0
+ .globl _main
+ ;; gbos C entry. exec() maps our ROM bank and jumps here ($4000).
+ ;; Call main(), then exit(0). (Args are fetched on demand via getargs().)
+ .area _CODE
+_start::
+ call _main
+ ld b, #0
+ ld c, #0 ; SYS_EXIT
+ rst #0x30
+1$: jr 1$
diff --git a/usr/dhcp.c b/usr/dhcp.c
new file mode 100644
index 0000000..e4186f4
--- /dev/null
+++ b/usr/dhcp.c
@@ -0,0 +1,72 @@
+#include "sock.h"
+/* dhcp - a minimal DHCP client. DISCOVER -> OFFER -> REQUEST -> ACK over UDP
+ (kernel owns UDP; this owns BOOTP/DHCP), then hands the leased address to the
+ kernel with net_setip(). Run at boot before the shell. Times out gracefully
+ if there's no server (the shell then comes up unconfigured). */
+
+static unsigned char pkt[288]; /* BOOTP is ~240B + options (frames > 255) */
+static unsigned char ip[4];
+static unsigned char bcast[4];
+
+/* fill the fixed BOOTP request header + magic cookie; options start at 240 */
+static void bootp(void) {
+ unsigned int i;
+ for (i = 0; i < 240; i++) pkt[i] = 0;
+ pkt[0] = 1; pkt[1] = 1; pkt[2] = 6; /* op=request htype=eth hlen=6 */
+ pkt[4] = 0x12; pkt[5] = 0x34; pkt[6] = 0x56; pkt[7] = 0x78; /* xid */
+ pkt[10] = 0x80; /* flags: broadcast reply */
+ pkt[28] = 0x02; pkt[29] = 0x47; pkt[30] = 0x42; pkt[33] = 0x01; /* client MAC */
+ pkt[236] = 0x63; pkt[237] = 0x82; pkt[238] = 0x53; pkt[239] = 0x63; /* magic */
+}
+
+/* DHCP message type from the received packet (option 53), or 0 */
+static unsigned char mtype(void) {
+ unsigned int p = 240;
+ while (p < 280 && pkt[p] != 255) {
+ if (pkt[p] == 0) { p++; continue; } /* pad */
+ if (pkt[p] == 53) return pkt[p + 2];
+ p += 2 + pkt[p + 1];
+ }
+ return 0;
+}
+
+void main(void) {
+ unsigned char s, i, n;
+ unsigned int p;
+ bcast[0] = 255; bcast[1] = 255; bcast[2] = 255; bcast[3] = 255;
+
+ puts("dhcp: discovering"); nl();
+ s = net_socket(SOCK_UDP);
+ if (s == 0xFF) { puts("dhcp: no socket"); nl(); return; }
+ net_bind(s, 68);
+ net_connect(s, bcast, 67);
+
+ /* DISCOVER -> OFFER (retry a few times for startup timing) */
+ bootp();
+ p = 240;
+ pkt[p++] = 53; pkt[p++] = 1; pkt[p++] = 1; /* DHCPDISCOVER */
+ pkt[p++] = 255;
+ n = 0xFF;
+ for (i = 0; i < 3 && n == 0xFF; i++) {
+ net_send(s, pkt, (unsigned char)p);
+ n = net_recv(s, pkt, 255);
+ }
+ if (n == 0xFF || mtype() != 2) { puts("dhcp: no offer"); nl(); net_close(s); return; }
+ ip[0] = pkt[16]; ip[1] = pkt[17]; ip[2] = pkt[18]; ip[3] = pkt[19]; /* yiaddr */
+
+ /* REQUEST -> ACK */
+ bootp();
+ p = 240;
+ pkt[p++] = 53; pkt[p++] = 1; pkt[p++] = 3; /* DHCPREQUEST */
+ pkt[p++] = 50; pkt[p++] = 4; /* requested IP */
+ pkt[p++] = ip[0]; pkt[p++] = ip[1]; pkt[p++] = ip[2]; pkt[p++] = ip[3];
+ pkt[p++] = 255;
+ net_send(s, pkt, (unsigned char)p);
+ n = net_recv(s, pkt, 255);
+ net_close(s);
+ if (n == 0xFF || mtype() != 5) { puts("dhcp: request failed"); nl(); return; }
+ ip[0] = pkt[16]; ip[1] = pkt[17]; ip[2] = pkt[18]; ip[3] = pkt[19];
+
+ net_setip(ip);
+ puts("dhcp: leased "); for (i = 0; i < 4; i++) { putu(ip[i]); if (i < 3) putc('.'); } nl();
+}
diff --git a/usr/echo.c b/usr/echo.c
new file mode 100644
index 0000000..f8d5125
--- /dev/null
+++ b/usr/echo.c
@@ -0,0 +1,5 @@
+#include "gbos.h"
+void main(void) {
+ puts(getargs());
+ nl();
+}
diff --git a/usr/false.c b/usr/false.c
new file mode 100644
index 0000000..a124951
--- /dev/null
+++ b/usr/false.c
@@ -0,0 +1,2 @@
+#include "gbos.h"
+void main(void) { sexit(1); }
diff --git a/usr/gbos.h b/usr/gbos.h
new file mode 100644
index 0000000..2350665
--- /dev/null
+++ b/usr/gbos.h
@@ -0,0 +1,64 @@
+/* gbos userland C API (thin wrappers over rst $30 syscalls; see c/libc.s) */
+#ifndef GBOS_H
+#define GBOS_H
+#define EOF 4 /* readc() returns this at end of input */
+
+void writes(const char *buf, unsigned char len);
+void putc(char c);
+void puts(const char *s); /* write a NUL-terminated string */
+void nl(void); /* newline (CRLF) */
+unsigned char strlen(const char *s);
+char *getargs(void); /* this program's argument string */
+char readc(void); /* one input byte, EOF at end */
+void sexit(unsigned char code);
+unsigned char getpid(void);
+unsigned char pipe(void); /* -> read fd; write end is read+1 */
+void ssend(unsigned char b); /* link-port: transmit one byte */
+unsigned char srecv(void); /* link-port: receive one byte (blocks) */
+int srecv_nb(void); /* link-port: receive, or -1 if none */
+unsigned char pollin(void); /* poll OSK for a typed char, or 0 */
+unsigned char pollcon(void); /* poll console ring (link-injected
+ input, filled by net pumps), or 0 */
+unsigned char sys_net(void *req); /* socket layer trap (see sock.h) */
+void gsleep(unsigned char units); /* delay units*(1/64)s (raw trap) */
+void msleep(unsigned int ms); /* delay ~ms milliseconds */
+
+/* helpers (c/libc.c) */
+void putu(unsigned int n); /* print decimal */
+unsigned char atou(const char *s); /* parse decimal */
+unsigned char argv_parse(char **argv, unsigned char maxv); /* argc/argv */
+unsigned char hasflag(char **argv, unsigned char argc, char f); /* -x flag */
+char *optval(char **argv, unsigned char argc, char opt); /* -x VALUE */
+
+/* filesystem (c/libc.s -> kernel syscalls) */
+unsigned char open(const char *name, unsigned char mode); /* 0=read 1=write */
+void close(unsigned char fd);
+int fgetc(unsigned char fd); /* a byte, or -1 at EOF */
+void fputc(unsigned char fd, char ch);
+unsigned char flist(unsigned char slot, char *namebuf); /* 1 if slot used */
+void fremove(const char *name);
+unsigned char mkdir(const char *path);
+unsigned char chdir(const char *path);
+unsigned char opendir(const char *path);
+void poweroff(void); /* clean emulator shutdown (saves) */
+#define O_READ 0
+#define O_WRITE 1
+#define NOFD 0xFF
+#define EISDIR 0xFE /* open() failed: path is a directory */
+
+/* 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 */
+unsigned char kill(unsigned char pid); /* terminate a pid (1 is immortal)*/
+unsigned char psget(unsigned char slot, unsigned char *buf); /* {pid,state,prog}*/
+void progname(unsigned char id, char *buf); /* id -> name */
+void yield(void);
+unsigned char reap(void); /* reap a finished bg job, or 0 */
+void bfill(unsigned char block, unsigned char byte); /* [debug] disk */
+unsigned char bpeek(unsigned char block); /* [debug] disk */
+unsigned char bpeek2(unsigned char block, unsigned char off);
+#endif
diff --git a/usr/head.c b/usr/head.c
new file mode 100644
index 0000000..3298e54
--- /dev/null
+++ b/usr/head.c
@@ -0,0 +1,28 @@
+#include "gbos.h"
+/* head [-n N] [N] [file]: first N lines of a file or stdin (default 10). */
+void main(void) {
+ char *argv[8];
+ unsigned char argc = argv_parse(argv, 8);
+ char *nv = optval(argv, argc, 'n');
+ unsigned char n = 10, lines = 0, i, fd = NOFD;
+ char *fname = 0;
+ int ch;
+ if (nv) n = atou(nv);
+ for (i = 0; i < argc; i++) {
+ if (argv[i][0] == '-') continue;
+ if (nv == argv[i]) continue;
+ if (!nv && argv[i][0] >= '0' && argv[i][0] <= '9') { n = atou(argv[i]); continue; }
+ fname = argv[i]; break;
+ }
+ if (fname) {
+ fd = open(fname, O_READ);
+ if (fd > 3) { puts("head: no such file"); nl(); return; }
+ }
+ for (;;) {
+ if (fd > 3) { char c = readc(); if (c == EOF) break; ch = c; }
+ else { ch = fgetc(fd); if (ch < 0) break; }
+ if (lines < n) putc((char)ch);
+ if (ch == '\n') lines++;
+ }
+ if (fd != NOFD) close(fd);
+}
diff --git a/usr/irc.c b/usr/irc.c
new file mode 100644
index 0000000..250738a
--- /dev/null
+++ b/usr/irc.c
@@ -0,0 +1,422 @@
+#include "resolve.h"
+/* irc HOST [NICK] - a bitchx/irssi-flavored IRC client on 40x18 columns.
+ The kernel owns TCP; this owns the protocol and the screen.
+
+ UI: messages scroll above; the bottom line is an irssi-style input line
+ "[#chan] text_" redrawn in place with \r (the terminal has no cursor
+ addressing - \r + overprint is the entire trick). Long input scrolls
+ horizontally like irssi. Keys come from the OSK (SELECT toggles it) and
+ from the console ring (pollcon), so a hub can type into the client too.
+
+ Formats, straight from the elders:
+ <nick> text channel message * nick text CTCP ACTION
+ <nick:#c> text msg to a non-current channel
+ *nick* text private msg >target< text outbound /msg
+ -nick- text notice -!- text server/status */
+
+/* seg first + large: the arg string lives at 0xA000 and the first static
+ lands on it; args are copied out in main() before seg is ever written. */
+static unsigned char seg[220]; /* one TCP segment from the kernel */
+static char line[240]; /* server line accumulator */
+static unsigned char llen;
+static char input[140]; /* the input line being typed */
+static unsigned char inlen;
+static unsigned char drawn; /* chars currently drawn on input row */
+static char nick[16];
+static char target[32]; /* current channel/query, "" = none */
+static char sbuf[200]; /* outgoing IRC line builder */
+static unsigned char slen;
+static unsigned char namebuf[64];
+static unsigned char dst[4];
+static unsigned char sock;
+static unsigned char quitting;
+/* A WRITABLE empty string for parse defaults. handle() writes terminators
+ into pfx (bang) and txt (strip_fmt); a read-only "" literal lives in ROM
+ ($4000-$5FFF), where a write is an MBC5 *RAM-bank-select* - it would swap
+ the whole $A000 data bank out from under every static (instant garbage).
+ A prefix-less or empty-trailing server line is enough to trigger it. */
+static char empty[1];
+
+/* ---- tiny string helpers (SDCC libc is us) ------------------------------- */
+static unsigned char sceq(const char *a, const char *b) { /* case-insens. */
+ unsigned char ca, cb;
+ for (;;) {
+ ca = (unsigned char)*a++; cb = (unsigned char)*b++;
+ if (ca >= 'a' && ca <= 'z') ca -= 32;
+ if (cb >= 'a' && cb <= 'z') cb -= 32;
+ if (ca != cb) return 0;
+ if (!ca) return 1;
+ }
+}
+static void scpy(char *d, const char *s, unsigned char max) {
+ unsigned char i;
+ for (i = 0; s[i] && i < (unsigned char)(max - 1); i++) d[i] = s[i];
+ d[i] = 0;
+}
+
+/* ---- outgoing IRC lines --------------------------------------------------- */
+static void sc(const char *s) { /* append to sbuf */
+ while (*s && slen < 197) sbuf[slen++] = *s++;
+}
+static void sfin(void) { /* CRLF + send */
+ sbuf[slen++] = '\r'; sbuf[slen++] = '\n';
+ net_send(sock, sbuf, slen);
+ slen = 0;
+}
+
+/* ---- the input line ------------------------------------------------------- */
+/* erase what's drawn on the bottom row, park at col 0 */
+static void row_clear(void) {
+ unsigned char i;
+ putc('\r');
+ for (i = 0; i < drawn; i++) putc(' ');
+ putc('\r');
+ drawn = 0;
+}
+/* redraw "[target] tail-of-input_"; keep total < 40 so it never wraps */
+static void row_draw(void) {
+ unsigned char pl, vis, i;
+ const char *t = target[0] ? target : "(status)";
+ row_clear();
+ putc('[');
+ for (pl = 0; t[pl] && pl < 12; pl++) putc(t[pl]);
+ putc(']'); putc(' ');
+ pl += 3; /* prompt width */
+ vis = 39 - pl - 1; /* room for text (cursor gets 1) */
+ i = (inlen > vis) ? (unsigned char)(inlen - vis) : 0;
+ for (; i < inlen; i++) putc(input[i]);
+ drawn = pl + ((inlen > vis) ? vis : inlen);
+}
+/* print a message line above the input line */
+static void say(const char *s) {
+ row_clear();
+ puts(s); nl();
+ row_draw();
+}
+static void say2(const char *a, const char *b) {
+ row_clear();
+ puts(a); puts(b); nl();
+ row_draw();
+}
+static void say3(const char *a, const char *b, const char *c) {
+ row_clear();
+ puts(a); puts(b); puts(c); nl();
+ row_draw();
+}
+
+/* ---- server line parsing -------------------------------------------------- */
+/* split "pfx!user@host" -> pfx nick only (in place) */
+static void bang(char *p) {
+ while (*p && *p != '!') p++;
+ *p = 0;
+}
+static unsigned char isnum(const char *c) {
+ return c[0] >= '0' && c[0] <= '9';
+}
+static unsigned char ishex(char c) {
+ return (c>='0'&&c<='9')||(c>='a'&&c<='f')||(c>='A'&&c<='F');
+}
+
+/* Strip mIRC/IRC formatting in place - crucially their *arguments*, or a
+ color logo (a real MOTD!) leaves its \x03 color numbers on screen as digit
+ soup. \x03 = color (up to 2 digits[,up to 2 digits]); \x04 = hex color
+ (up to 6 hex[,6 hex]); \x02 bold \x0f reset \x11 mono \x16 reverse
+ \x1d italic \x1e strike \x1f underline are lone toggles. Leaves \x01
+ (CTCP) intact for the PRIVMSG path. */
+static void strip_fmt(char *s) {
+ char *w = s, *r = s;
+ unsigned char c, d;
+ while (*r) {
+ c = (unsigned char)*r;
+ if (c == 3) {
+ r++; d = 0;
+ while (*r>='0'&&*r<='9'&&d<2) { r++; d++; }
+ if (*r==','&&r[1]>='0'&&r[1]<='9') { r++; d=0; while(*r>='0'&&*r<='9'&&d<2){r++;d++;} }
+ continue;
+ }
+ if (c == 4) {
+ r++; d = 0;
+ while (ishex(*r)&&d<6) { r++; d++; }
+ if (*r==','&&ishex(r[1])) { r++; d=0; while(ishex(*r)&&d<6){r++;d++;} }
+ continue;
+ }
+ if (c==2||c==15||c==17||c==22||c==29||c==30||c==31) { r++; continue; }
+ *w++ = *r++;
+ }
+ *w = 0;
+}
+
+static void handle(char *l) {
+ char *pfx = empty, *cmd, *arg, *txt = empty;
+ char *p = l;
+
+ if (*p == ':') { /* :prefix */
+ pfx = ++p;
+ while (*p && *p != ' ') p++;
+ if (*p) *p++ = 0;
+ }
+ cmd = p; /* command */
+ while (*p && *p != ' ') p++;
+ if (*p) *p++ = 0;
+ arg = p; /* params, then :trailing */
+ while (*p) {
+ if (*p == ':' && (p == arg || p[-1] == 0)) { txt = p + 1; p[-1] = 0; break; }
+ if (*p == ' ') *p = 0;
+ p++;
+ }
+ /* arg = first param (NUL-joined list), txt = trailing */
+ strip_fmt(txt); /* kill color/format codes + args */
+
+ if (sceq(cmd, "PING")) {
+ slen = 0; sc("PONG :"); sc(txt[0] ? txt : arg); sfin();
+ say("-!- Ping? Pong!");
+ return;
+ }
+ bang(pfx);
+ if (sceq(cmd, "PRIVMSG")) {
+ char *dest = arg;
+ unsigned char act = 0;
+ if (txt[0] == 1) { /* CTCP */
+ char *e = txt + 1;
+ while (*e && *e != 1) e++;
+ *e = 0; txt++;
+ if (txt[0]=='A'&&txt[1]=='C'&&txt[2]=='T'&&txt[3]=='I'&&txt[4]=='O'&&txt[5]=='N'&&txt[6]==' ') {
+ act = 1; txt += 7;
+ } else if (txt[0]=='V') { /* VERSION */
+ slen = 0; sc("NOTICE "); sc(pfx); sc(" :\001VERSION gbos:sl0pboy:GameBoyColor\001"); sfin();
+ return;
+ } else return;
+ }
+ row_clear();
+ if (act) { puts("* "); puts(pfx); putc(' '); }
+ else if (sceq(dest, nick)) { putc('*'); puts(pfx); puts("* "); }
+ else if (sceq(dest, target)) { putc('<'); puts(pfx); puts("> "); }
+ else { putc('<'); puts(pfx); putc(':'); puts(dest); puts("> "); }
+ puts(txt); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(cmd, "NOTICE")) {
+ row_clear();
+ putc('-'); puts(pfx[0] ? pfx : "server"); puts("- "); puts(txt); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(cmd, "JOIN")) {
+ char *ch = txt[0] ? txt : arg;
+ if (sceq(pfx, nick)) {
+ scpy(target, ch, sizeof(target));
+ say2("-!- now talking in ", target);
+ } else say3("-!- ", pfx, " has joined");
+ return;
+ }
+ if (sceq(cmd, "PART")) {
+ if (sceq(pfx, nick)) { target[0] = 0; say("-!- you left"); }
+ else say3("-!- ", pfx, " has left");
+ return;
+ }
+ if (sceq(cmd, "QUIT")) { say3("-!- ", pfx, " has quit"); return; }
+ if (sceq(cmd, "KICK")) {
+ char *ch = arg, *who = arg;
+ while (*who) who++;
+ who++; /* param 2: the victim */
+ if (sceq(who, nick)) { target[0] = 0; say2("-!- you were kicked from ", ch); }
+ else say3("-!- ", who, " was kicked");
+ return;
+ }
+ if (sceq(cmd, "NICK")) {
+ char *nn = txt[0] ? txt : arg;
+ if (sceq(pfx, nick)) scpy(nick, nn, sizeof(nick));
+ row_clear();
+ puts("-!- "); puts(pfx); puts(" is now known as "); puts(nn); nl();
+ row_draw();
+ return;
+ }
+ if (isnum(cmd)) {
+ if (cmd[0]=='4'&&cmd[1]=='3'&&cmd[2]=='3') { /* nick in use */
+ unsigned char n = strlen(nick);
+ if (n < 14) { nick[n] = '_'; nick[n+1] = 0; }
+ slen = 0; sc("NICK "); sc(nick); sfin();
+ say2("-!- nick in use, trying ", nick);
+ return;
+ }
+ if (cmd[0]=='3'&&cmd[1]=='3'&&cmd[2]=='2') { /* topic */
+ say2("-!- topic: ", txt);
+ return;
+ }
+ if (cmd[0]=='3'&&cmd[1]=='5'&&cmd[2]=='3') { /* names */
+ say2("-!- users: ", txt);
+ return;
+ }
+ if (cmd[1]=='3'&&cmd[0]=='3') return; /* 333 topic-by: skip */
+ if (cmd[0]=='0'&&cmd[1]=='0'&&(cmd[2]=='4'||cmd[2]=='5')) return; /* MYINFO/ISUPPORT noise */
+ if (txt[0]) say2("-!- ", txt); /* motd & friends */
+ return;
+ }
+ /* anything else: show it raw-ish, bitchx would too */
+ if (txt[0]) say3(cmd, " ", txt);
+}
+
+/* accumulate TCP bytes into lines */
+static void feed(unsigned char *b, unsigned char n) {
+ unsigned char i;
+ for (i = 0; i < n; i++) {
+ char c = (char)b[i];
+ if (c == '\r') continue;
+ if (c == '\n') { line[llen] = 0; if (llen) handle(line); llen = 0; }
+ else if (llen < 239) line[llen++] = c;
+ }
+}
+
+/* ---- typed input ---------------------------------------------------------- */
+static void run_cmd(char *t) {
+ char *a = t;
+ while (*a && *a != ' ') a++;
+ if (*a) *a++ = 0; /* t = /cmd, a = rest */
+
+ if (sceq(t, "/join") || sceq(t, "/j")) {
+ if (!*a) { say("-!- join what?"); return; }
+ slen = 0; sc("JOIN "); sc(a); sfin();
+ return;
+ }
+ if (sceq(t, "/part") || sceq(t, "/p")) {
+ slen = 0; sc("PART "); sc(*a ? a : target); sfin();
+ return;
+ }
+ if (sceq(t, "/msg")) {
+ char *m = a;
+ while (*m && *m != ' ') m++;
+ if (*m) *m++ = 0;
+ if (!*a || !*m) { say("-!- /msg nick text"); return; }
+ slen = 0; sc("PRIVMSG "); sc(a); sc(" :"); sc(m); sfin();
+ row_clear();
+ putc('>'); puts(a); puts("< "); puts(m); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(t, "/me")) {
+ if (!target[0]) { say("-!- no channel"); return; }
+ slen = 0; sc("PRIVMSG "); sc(target); sc(" :\001ACTION "); sc(a); sc("\001"); sfin();
+ row_clear();
+ puts("* "); puts(nick); putc(' '); puts(a); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(t, "/nick")) {
+ if (!*a) { say("-!- /nick newnick"); return; }
+ slen = 0; sc("NICK "); sc(a); sfin();
+ return;
+ }
+ if (sceq(t, "/quit")) {
+ slen = 0; sc("QUIT :"); sc(*a ? a : "sl0pboy out"); sfin();
+ quitting = 1;
+ return;
+ }
+ if (sceq(t, "/raw") || sceq(t, "/quote")) {
+ slen = 0; sc(a); sfin();
+ return;
+ }
+ say("-!- commands: /join /part /msg /me /nick /quit /raw");
+}
+
+static void submit(void) {
+ input[inlen] = 0;
+ if (!inlen) return;
+ if (input[0] == '/') {
+ inlen = 0;
+ run_cmd(input);
+ row_draw();
+ return;
+ }
+ if (!target[0]) { inlen = 0; say("-!- join a channel first (/join #gb)"); return; }
+ slen = 0; sc("PRIVMSG "); sc(target); sc(" :"); sc(input); sfin();
+ row_clear();
+ putc('<'); puts(nick); puts("> "); puts(input); nl();
+ inlen = 0;
+ row_draw();
+}
+
+static void key(unsigned char c) {
+ if (c == '\n' || c == '\r') { submit(); return; }
+ if (c == 8 || c == 127) {
+ if (inlen) { inlen--; row_draw(); }
+ return;
+ }
+ if (c < 32) return;
+ if (inlen < 139) { input[inlen++] = (char)c; row_draw(); }
+}
+
+/* ---- main ----------------------------------------------------------------- */
+void main(void) {
+ char *argp;
+ unsigned char i, n, idle;
+
+ /* gbos does not zero C statics (crt0 zeroes nothing; fork inherits the
+ parent's RAM bank) - initialize every piece of state explicitly */
+ llen = inlen = drawn = slen = quitting = 0;
+ target[0] = 0;
+ empty[0] = 0; /* gbos doesn't zero BSS */
+
+ argp = getargs(); /* copy args before seg is used */
+ for (i = 0; argp[i] && i < 63; i++) namebuf[i] = (unsigned char)argp[i];
+ namebuf[i] = 0;
+
+ /* split "HOST [NICK]" */
+ scpy(nick, "sl0pboy", sizeof(nick));
+ for (i = 0; namebuf[i]; i++)
+ if (namebuf[i] == ' ') {
+ namebuf[i] = 0;
+ if (namebuf[i+1]) scpy(nick, (char *)namebuf + i + 1, sizeof(nick));
+ break;
+ }
+ if (!namebuf[0]) {
+ puts("usage: irc HOST [NICK] (port 6667)"); nl();
+ puts(" then: /join #chan, SELECT = keyboard"); nl();
+ sexit(1);
+ }
+
+ if (!resolve((char *)namebuf, dst)) {
+ puts("irc: cannot resolve "); puts((char *)namebuf); nl(); sexit(1);
+ }
+
+ sock = net_socket(SOCK_TCP);
+ if (sock == 0xFF) { puts("irc: no socket"); nl(); sexit(1); }
+ /* vary the local port per run (DIV timer noise): a fixed port can hit a
+ stale half-dead connection at the server after an unclean exit */
+ net_bind(sock, 41000 + ((unsigned int)*(volatile unsigned char *)0xFF04 << 2) + getpid());
+ puts("-!- connecting to "); puts((char *)namebuf); puts(":6667"); nl();
+ if (net_connect(sock, dst, 6667) == 0xFF) {
+ puts("irc: connect failed"); nl(); net_close(sock); sexit(1);
+ }
+
+ slen = 0; sc("NICK "); sc(nick); sfin();
+ slen = 0; sc("USER gbos 0 * :sl0pboy on gbos"); sfin();
+ puts("-!- registering as "); puts(nick);
+ puts(" (SELECT = keyboard)"); nl();
+ row_draw();
+
+ idle = 0;
+ for (;;) {
+ n = net_recv_nb(sock, seg, (unsigned char)sizeof(seg));
+ if (n == 0) { say("-!- disconnected"); break; }
+ if (n != 0xFE && n != 0xFF) feed(seg, n); /* render a segment if any */
+ if (quitting) { say("-!- quit"); break; }
+
+ /* Poll input EVERY pass, even mid-flood: otherwise a big MOTD (or a
+ * busy channel) starves the keyboard for its whole duration - the OSK
+ * won't even open, and keystrokes are dropped. That's what made
+ * '/join #sl0p right after connect' look broken. */
+ i = pollin(); /* OSK */
+ if (!i) i = pollcon(); /* hub-injected console bytes */
+ if (i) key(i);
+
+ if (n == 0xFE || n == 0xFF) { /* nothing received this pass */
+ if (!i && ++idle >= 4) { msleep(20); idle = 4; }
+ } else {
+ idle = 0; /* actively receiving; don't sleep */
+ }
+ }
+ net_close(sock);
+ row_clear();
+ sexit(0);
+}
diff --git a/usr/kill.c b/usr/kill.c
new file mode 100644
index 0000000..e524627
--- /dev/null
+++ b/usr/kill.c
@@ -0,0 +1,10 @@
+#include "gbos.h"
+/* kill <pid>: terminate a process (pid 1 is immortal). */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ unsigned char pid;
+ if (argc < 1) { puts("usage: kill <pid>"); nl(); return; }
+ pid = atou(argv[0]);
+ if (kill(pid)) { puts("kill: no such pid (or immortal)"); nl(); }
+}
diff --git a/usr/libc.c b/usr/libc.c
new file mode 100644
index 0000000..a48a64c
--- /dev/null
+++ b/usr/libc.c
@@ -0,0 +1,67 @@
+/* gbos libc C helpers (compiled once, linked into every program). */
+#include "gbos.h"
+
+/* sleep for approximately 'ms' milliseconds (cooperative; other procs run).
+ The kernel counts in 1/64-second units (~15.6 ms), so we round ms to that. */
+void msleep(unsigned int ms) {
+ unsigned int u = ms >> 4; /* ~ms/15.6, close enough for a delay */
+ if (u > 255) u = 255;
+ if (u == 0 && ms) u = 1;
+ gsleep((unsigned char)u);
+}
+
+/* print an unsigned int in decimal */
+void putu(unsigned int n) {
+ char buf[5];
+ unsigned char i = 0;
+ if (n == 0) { putc('0'); return; }
+ while (n) { buf[i++] = '0' + (n % 10); n = n / 10; }
+ while (i) putc(buf[--i]);
+}
+
+/* parse a decimal number (stops at the first non-digit) */
+unsigned char atou(const char *s) {
+ unsigned char n = 0;
+ while (*s >= '0' && *s <= '9') { n = n * 10 + (*s - '0'); s++; }
+ return n;
+}
+
+/* tokenize the argument string in place into argv[]; returns argc.
+ (replaces the spaces in the arg string with NULs) */
+unsigned char argv_parse(char **argv, unsigned char maxv) {
+ char *p = getargs();
+ unsigned char argc = 0;
+ for (;;) {
+ while (*p == ' ') p++;
+ if (*p == 0) break;
+ if (argc < maxv) argv[argc] = p;
+ argc++;
+ while (*p != 0 && *p != ' ') p++;
+ if (*p == ' ') *p++ = 0;
+ }
+ return argc;
+}
+
+/* return nonzero if single-char flag `f` appears (as -f or within -abc) */
+unsigned char hasflag(char **argv, unsigned char argc, char f) {
+ unsigned char i, j;
+ for (i = 0; i < argc; i++) {
+ if (argv[i][0] != '-') continue;
+ for (j = 1; argv[i][j]; j++)
+ if (argv[i][j] == f) return 1;
+ }
+ return 0;
+}
+
+/* return the value for option -<opt> ("-n 5" or "-n5"), or 0 if absent */
+char *optval(char **argv, unsigned char argc, char opt) {
+ unsigned char i;
+ for (i = 0; i < argc; i++) {
+ if (argv[i][0] == '-' && argv[i][1] == opt) {
+ if (argv[i][2]) return &argv[i][2];
+ if (i + 1 < argc) return argv[i+1];
+ return 0;
+ }
+ }
+ return 0;
+}
diff --git a/usr/libc.s b/usr/libc.s
new file mode 100644
index 0000000..5b176d9
--- /dev/null
+++ b/usr/libc.s
@@ -0,0 +1,304 @@
+ .module libc
+ .globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs
+ .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv, _srecv_nb, _pollin, _pollcon
+ .globl _sys_net, _gsleep
+ ;; SDCC sm83 sdcccall(1): 1st arg -> A (byte) or DE (pointer); ret A / BC.
+ ;; rst $30 (the trap) clobbers BC/DE/HL; SDCC treats them caller-saved, so
+ ;; wrappers need not preserve them - but we compute cleanly regardless.
+ .area _CODE
+
+ ;; void writes(const char *buf /*DE*/, unsigned char len /*A*/)
+_writes:
+ ld b, a
+ ld c, #3 ; SYS_WRITE
+ rst #0x30
+ ret
+
+ ;; char *getargs(void) -> BC : the arg string (command line past word 1).
+ ;; The shell leaves "cmd\0args\0" at 0xA000, inherited by the child via fork.
+_getargs:
+ ld hl, #0xa000
+1$: ld a, (hl)
+ inc hl
+ or a
+ jr nz, 1$
+ ld b, h
+ ld c, l
+ ret
+
+ ;; void putc(char c /*A*/) -> stdout via SYS_PUTC
+_putc:
+ ld e, a
+ ld c, #16 ; SYS_PUTC
+ rst #0x30
+ ret
+
+ ;; void puts(const char *s /*DE*/) - write until NUL (no newline)
+_puts:
+1$: ld a, (de)
+ or a
+ ret z
+ push de
+ ld e, a
+ ld c, #16 ; SYS_PUTC
+ rst #0x30
+ pop de
+ inc de
+ jr 1$
+
+ ;; void nl(void) - CRLF via stdout
+_nl:
+ 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
+_strlen:
+ ld c, #0
+1$: ld a, (de)
+ or a
+ jr z, 2$
+ inc c
+ inc de
+ jr 1$
+2$: ld a, c
+ ret
+
+ ;; char readc(void) -> A (4 = EOF)
+_readc:
+ ld c, #2 ; SYS_READ
+ rst #0x30
+ ret
+
+ ;; void sexit(unsigned char code /*A*/) - never returns
+_sexit:
+ ld b, a
+ ld c, #0 ; SYS_EXIT
+ rst #0x30
+ ret
+
+ ;; unsigned char getpid(void) -> A
+_getpid:
+ ld c, #8 ; SYS_GETPID
+ rst #0x30
+ ret
+
+ ;; unsigned char pipe(void) -> A (read fd; write end is read+1)
+_pipe:
+ ld c, #28 ; SYS_PIPE
+ rst #0x30
+ ret
+
+ ;; void ssend(unsigned char b /*A*/) -- link-port transmit
+_ssend:
+ ld e, a
+ ld c, #29 ; SYS_SSEND
+ rst #0x30
+ ret
+
+ ;; unsigned char srecv(void) -> A -- link-port receive (blocks)
+_srecv:
+ ld c, #30 ; SYS_SRECV
+ rst #0x30
+ ret
+
+ ;; int srecv_nb(void) -> byte, or -1 if none (non-blocking)
+_srecv_nb:
+ ld c, #31 ; SYS_SRECVNB
+ rst #0x30
+ jr c, 1$
+ ld c, a
+ ld b, #0
+ ret
+1$: ld bc, #0xffff
+ ret
+
+ ;; unsigned char sys_net(void *req /*DE*/) -> A -- socket layer trap
+_sys_net:
+ ld c, #33 ; SYS_NET (DE already = &netreq)
+ rst #0x30
+ ret
+
+ ;; void gsleep(unsigned char units /*A*/) - delay units*(1/64)s
+_gsleep:
+ ld b, a
+ ld c, #34 ; SYS_SLEEP
+ rst #0x30
+ ret
+
+ ;; unsigned char pollin(void) -> A (typed OSK char, or 0)
+_pollin:
+ ld c, #32 ; SYS_POLLIN
+ rst #0x30
+ ret
+
+ ;; unsigned char pollcon(void) -> A (queued console byte, or 0)
+_pollcon:
+ ld c, #35 ; SYS_POLLCON
+ rst #0x30
+ ret
+
+ ;; unsigned char open(const char *name /*DE*/, unsigned char mode /*A*/) -> A(fd)
+_open:
+ ld b, a ; B = mode
+ ld c, #4 ; SYS_OPEN
+ rst #0x30
+ ret
+
+ ;; void close(unsigned char fd /*A*/)
+_close:
+ ld b, a
+ ld c, #5 ; SYS_CLOSE
+ rst #0x30
+ ret
+
+ ;; int fgetc(unsigned char fd /*A*/) -> BC (byte, or -1 at EOF)
+_fgetc:
+ ld b, a
+ ld c, #12 ; SYS_GETB
+ rst #0x30
+ jr c, 1$
+ ld c, a
+ ld b, #0
+ ret
+1$: ld bc, #0xffff
+ ret
+
+ ;; void fputc(unsigned char fd /*A*/, char ch /*E*/)
+ ;; byte goes to the kernel in E (the trap clobbers A during dispatch)
+_fputc:
+ ld b, a ; B = fd (ch stays in E)
+ ld c, #13 ; SYS_PUTB
+ rst #0x30
+ ret
+
+ ;; unsigned char flist(unsigned char slot /*A*/, char *namebuf /*DE*/) -> A
+_flist:
+ ld b, a
+ ld c, #14 ; SYS_LIST
+ rst #0x30
+ ret
+
+ ;; void fremove(const char *name /*DE*/)
+_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
+
+ .globl _kill, _psget, _progname
+ ;; unsigned char kill(unsigned char pid /*A*/) -> A (0 ok, $FF fail)
+_kill:
+ ld b, a
+ ld c, #9 ; SYS_KILL
+ rst #0x30
+ ret
+ ;; unsigned char psget(unsigned char slot /*A*/, unsigned char *buf /*DE*/) -> A
+_psget:
+ ld b, a
+ ld c, #20 ; SYS_PS
+ rst #0x30
+ ret
+ ;; void progname(unsigned char id /*A*/, char *buf /*DE*/)
+_progname:
+ ld b, a
+ ld c, #21 ; SYS_PROGNAME
+ rst #0x30
+ ret
+
+ .globl _yield, _reap
+ ;; void yield(void)
+_yield:
+ ld c, #11 ; SYS_YIELD
+ rst #0x30
+ ret
+ ;; unsigned char reap(void) -> A (reaped pid, or 0)
+_reap:
+ ld c, #22 ; SYS_REAP
+ rst #0x30
+ ret
+
+ .globl _bfill, _bpeek, _bpeek2
+ ;; void bfill(unsigned char block /*A*/, unsigned char byte /*E*/)
+_bfill:
+ ld b, a
+ ld c, #23 ; SYS_BFILL
+ rst #0x30
+ ret
+ ;; unsigned char bpeek(unsigned char block /*A*/) -> A
+_bpeek:
+ ld b, a
+ ld e, #0
+ ld c, #24 ; SYS_BPEEK
+ rst #0x30
+ ret
+ ;; unsigned char bpeek2(unsigned char block /*A*/, unsigned char off /*E*/) -> A
+_bpeek2:
+ ld b, a
+ ld c, #24 ; SYS_BPEEK (off already in E)
+ rst #0x30
+ ret
+
+
+ .globl _mkdir, _chdir
+ ;; unsigned char mkdir(const char *path /*DE*/) -> A (0 ok / $FF)
+_mkdir:
+ ld c, #25 ; SYS_MKDIR
+ rst #0x30
+ ret
+ ;; unsigned char chdir(const char *path /*DE*/) -> A (0 ok / $FF)
+_chdir:
+ ld c, #26 ; SYS_CHDIR
+ rst #0x30
+ ret
+
+ .globl _opendir
+ ;; unsigned char opendir(const char *path /*DE*/) -> A (0 ok / $FF)
+_opendir:
+ ld c, #27 ; SYS_OPENDIR
+ rst #0x30
+ ret
+
+ .globl _poweroff
+ ;; void poweroff(void) - execute the emulator's clean-exit opcode ($ED)
+_poweroff:
+ .db 0xED
+1$: jr 1$
diff --git a/usr/ls.c b/usr/ls.c
new file mode 100644
index 0000000..0e2e01f
--- /dev/null
+++ b/usr/ls.c
@@ -0,0 +1,10 @@
+#include "gbos.h"
+/* ls [dir]: list a directory (default: current). */
+void main(void) {
+ char *argv[4];
+ char name[16];
+ unsigned char argc = argv_parse(argv, 4);
+ unsigned char i;
+ if (opendir(argc > 0 ? argv[0] : ".")) { puts("ls: no such directory"); nl(); return; }
+ for (i = 0; flist(i, name); i++) { name[15] = 0; puts(name); nl(); }
+}
diff --git a/usr/mkdir.c b/usr/mkdir.c
new file mode 100644
index 0000000..c907518
--- /dev/null
+++ b/usr/mkdir.c
@@ -0,0 +1,8 @@
+#include "gbos.h"
+/* mkdir <path>: create a directory. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ if (argc < 1) { puts("usage: mkdir <name>"); nl(); return; }
+ if (mkdir(argv[0])) { puts("mkdir: failed"); nl(); }
+}
diff --git a/usr/necho.c b/usr/necho.c
new file mode 100644
index 0000000..99551d4
--- /dev/null
+++ b/usr/necho.c
@@ -0,0 +1,13 @@
+#include "netlib.h"
+/* necho: send a SLIP-framed packet over the link port and print the reply.
+ Talks to a host gateway (tools/gateway.py) that echoes frames back. */
+
+void main(void) {
+ char buf[64];
+ unsigned char len;
+ puts("net: send 'hello from gbos'"); nl();
+ slip_send("hello from gbos", 15);
+ len = slip_recv(buf, 63);
+ buf[len] = 0;
+ puts("net: recv '"); puts(buf); puts("'"); nl();
+}
diff --git a/usr/netd.c b/usr/netd.c
new file mode 100644
index 0000000..b07abd2
--- /dev/null
+++ b/usr/netd.c
@@ -0,0 +1,12 @@
+#include "sock.h"
+/* netd - the network daemon. It just pumps the kernel RX path; the kernel's
+ socket layer owns SLIP/IP and auto-answers inbound ICMP echo requests, so
+ running netd makes the Game Boy respond to pings. No SLIP/IP here anymore. */
+
+void main(void) {
+ puts("netd: up (10.0.0.2)"); nl();
+ for (;;) {
+ net_poll(); /* drain RX, answer pings */
+ yield();
+ }
+}
diff --git a/usr/netlib.h b/usr/netlib.h
new file mode 100644
index 0000000..ec98eac
--- /dev/null
+++ b/usr/netlib.h
@@ -0,0 +1,39 @@
+#ifndef NETLIB_H
+#define NETLIB_H
+/* SLIP (RFC 1055) framing over the link port. Header-only so each program gets
+ its own copy (the C build links one translation unit per program). */
+#include "gbos.h"
+
+#define SLIP_END 0xC0
+#define SLIP_ESC 0xDB
+#define SLIP_ESC_END 0xDC
+#define SLIP_ESC_ESC 0xDD
+
+/* send a framed packet (len up to 255) */
+static void slip_send(const char *buf, unsigned char len) {
+ unsigned char i, c;
+ ssend(SLIP_END);
+ for (i = 0; i < len; i++) {
+ c = (unsigned char)buf[i];
+ if (c == SLIP_END) { ssend(SLIP_ESC); ssend(SLIP_ESC_END); }
+ else if (c == SLIP_ESC) { ssend(SLIP_ESC); ssend(SLIP_ESC_ESC); }
+ else ssend(c);
+ }
+ ssend(SLIP_END);
+}
+
+/* receive one framed packet into buf (max), returns its length */
+static unsigned char slip_recv(char *buf, unsigned char max) {
+ unsigned char len = 0, c;
+ for (;;) {
+ c = srecv();
+ if (c == SLIP_END) { if (len) return len; continue; }
+ if (c == SLIP_ESC) {
+ c = srecv();
+ if (c == SLIP_ESC_END) c = SLIP_END;
+ else if (c == SLIP_ESC_ESC) c = SLIP_ESC;
+ }
+ if (len < max) buf[len++] = (char)c;
+ }
+}
+#endif
diff --git a/usr/nslookup.c b/usr/nslookup.c
new file mode 100644
index 0000000..6a531b8
--- /dev/null
+++ b/usr/nslookup.c
@@ -0,0 +1,23 @@
+#include "resolve.h"
+/* nslookup HOST - resolve a hostname to an IPv4 address via DNS over UDP.
+ All the DNS logic lives in resolve.h (shared with wget). */
+
+static unsigned char namebuf[64]; /* first so it lands past the arg at 0xA000 */
+static unsigned char ip[4];
+
+void main(void) {
+ char *host;
+ unsigned char i;
+ host = getargs();
+ if (!host || !*host) { puts("usage: nslookup HOST"); nl(); sexit(1); }
+ for (i = 0; host[i] && i < 63; i++) namebuf[i] = (unsigned char)host[i];
+ namebuf[i] = 0;
+
+ if (resolve((char *)namebuf, ip)) {
+ puts((char *)namebuf); puts(" -> ");
+ for (i = 0; i < 4; i++) { putu(ip[i]); if (i < 3) putc('.'); }
+ nl();
+ } else {
+ puts("nslookup: cannot resolve "); puts((char *)namebuf); nl();
+ }
+}
diff --git a/usr/pid.c b/usr/pid.c
new file mode 100644
index 0000000..9e4e792
--- /dev/null
+++ b/usr/pid.c
@@ -0,0 +1,2 @@
+#include "gbos.h"
+void main(void) { putu(getpid()); nl(); }
diff --git a/usr/ping.c b/usr/ping.c
new file mode 100644
index 0000000..8671318
--- /dev/null
+++ b/usr/ping.c
@@ -0,0 +1,61 @@
+#include "resolve.h"
+/* ping HOST|IP - originate ICMP echo requests via the kernel socket layer.
+ Resolves a hostname via DNS first (resolve.h); default target 10.0.0.1. No
+ SLIP/IP/checksums here - the kernel owns all of that. */
+
+/* rbuf is first + large so namebuf/dst land past the arg string at 0xA000 */
+static unsigned char rbuf[96];
+static unsigned char namebuf[64];
+static unsigned char dst[4];
+static unsigned char payload[4];
+
+static void put_ip(unsigned char *ip) {
+ unsigned char i;
+ for (i = 0; i < 4; i++) { putu(ip[i]); if (i < 3) putc('.'); }
+}
+
+/* wait up to ~2s for an echo reply without blocking in the kernel: a lost
+ reply (some hosts rate-limit ICMP) must cost one "timeout" line, not
+ minutes wedged inside net_recv's pump-counted wait. */
+static unsigned char wait_reply(unsigned char s) {
+ unsigned char t, r;
+ for (t = 0; t < 40; t++) {
+ r = net_recv_nb(s, rbuf, (unsigned char)sizeof(rbuf));
+ if (r != 0xFE) return r; /* reply (or error) */
+ msleep(50);
+ }
+ return 0xFF; /* ~2s with no reply */
+}
+
+void main(void) {
+ char *arg;
+ unsigned char s, seq, got = 0, i;
+ arg = getargs();
+ if (!arg || !*arg) { dst[0] = 10; dst[1] = 0; dst[2] = 0; dst[3] = 1; }
+ else {
+ for (i = 0; arg[i] && i < 63; i++) namebuf[i] = (unsigned char)arg[i];
+ namebuf[i] = 0;
+ if (!resolve((char *)namebuf, dst)) {
+ puts("ping: cannot resolve "); puts((char *)namebuf); nl(); sexit(1);
+ }
+ }
+
+ payload[0] = 'g'; payload[1] = 'b'; payload[2] = 'o'; payload[3] = 's';
+ s = net_socket(SOCK_ICMP);
+ if (s == 0xFF) { puts("ping: no socket"); nl(); sexit(1); }
+ net_connect(s, dst, 0);
+
+ puts("PING "); put_ip(dst); nl();
+ for (seq = 1; seq <= 4; seq++) {
+ net_send(s, payload, 4);
+ if (wait_reply(s) != 0xFF) {
+ got++;
+ puts(" reply from "); put_ip(dst); puts(" seq="); putu(seq); nl();
+ } else {
+ puts(" seq="); putu(seq); puts(" timeout"); nl();
+ }
+ if (seq < 4) msleep(800); /* pace like real ping */
+ }
+ puts("-- "); putu(got); puts("/4 received"); nl();
+ net_close(s);
+}
diff --git a/usr/ps.c b/usr/ps.c
new file mode 100644
index 0000000..7bfc0de
--- /dev/null
+++ b/usr/ps.c
@@ -0,0 +1,14 @@
+#include "gbos.h"
+/* ps: list live processes (pid, state, command). */
+void main(void) {
+ unsigned char i, buf[3];
+ char name[9];
+ for (i = 0; i < 8; i++) {
+ if (psget(i, buf)) { /* buf = {pid, state, prog} */
+ putu(buf[0]); putc(' ');
+ putc("-RrBZ"[buf[1]]); putc(' ');
+ progname(buf[2], name);
+ puts(name); nl();
+ }
+ }
+}
diff --git a/usr/ptest.c b/usr/ptest.c
new file mode 100644
index 0000000..88b7d30
--- /dev/null
+++ b/usr/ptest.c
@@ -0,0 +1,19 @@
+#include "gbos.h"
+/* ptest: exercise the kernel pipe in one process - write some bytes, read them
+ back, then close the write end and confirm the reader sees EOF. */
+void main(void) {
+ unsigned char pr = pipe();
+ unsigned char pw = pr + 1;
+ int c;
+ if (pr == NOFD) { puts("pipe: none free"); nl(); return; }
+ fputc(pw, 'H'); fputc(pw, 'i'); fputc(pw, '!');
+ c = fgetc(pr); putc(c);
+ c = fgetc(pr); putc(c);
+ c = fgetc(pr); putc(c);
+ nl();
+ close(pw); /* last writer gone */
+ c = fgetc(pr);
+ if (c < 0) puts("EOF"); else { puts("got "); putc(c); }
+ nl();
+ close(pr);
+}
diff --git a/usr/resolve.h b/usr/resolve.h
new file mode 100644
index 0000000..9aaccc1
--- /dev/null
+++ b/usr/resolve.h
@@ -0,0 +1,81 @@
+#ifndef RESOLVE_H
+#define RESOLVE_H
+/* resolve.h - turn a dotted-quad OR a hostname into an IPv4 address. Hostname
+ lookups go out as a DNS A query over a UDP socket to 1.1.1.1:53; the kernel
+ owns UDP, this owns DNS. Header-only (one copy per program). */
+#include "sock.h"
+
+static unsigned char _dnsbuf[200];
+static unsigned char _dnssrv[4];
+
+/* parse "A.B.C.D" into out[4]; return 1 on success */
+static unsigned char parse_ip(char *s, unsigned char *out) {
+ unsigned char i, v;
+ for (i = 0; i < 4; i++) {
+ if (*s < '0' || *s > '9') return 0;
+ v = 0;
+ while (*s >= '0' && *s <= '9') { v = (unsigned char)(v * 10 + (*s - '0')); s++; }
+ out[i] = v;
+ if (i < 3) { if (*s != '.') return 0; s++; }
+ }
+ return 1;
+}
+
+static unsigned char _dns_query(char *host) {
+ unsigned char i = 0, p = 12, lblpos, len;
+ _dnsbuf[0] = 0x12; _dnsbuf[1] = 0x34; _dnsbuf[2] = 0x01; _dnsbuf[3] = 0x00;
+ _dnsbuf[4] = 0; _dnsbuf[5] = 1;
+ _dnsbuf[6] = 0; _dnsbuf[7] = 0; _dnsbuf[8] = 0; _dnsbuf[9] = 0; _dnsbuf[10] = 0; _dnsbuf[11] = 0;
+ while (host[i]) {
+ lblpos = p++; len = 0;
+ while (host[i] && host[i] != '.') { _dnsbuf[p++] = host[i]; i++; len++; }
+ _dnsbuf[lblpos] = len;
+ if (host[i] == '.') i++;
+ }
+ _dnsbuf[p++] = 0; _dnsbuf[p++] = 0; _dnsbuf[p++] = 1; _dnsbuf[p++] = 0; _dnsbuf[p++] = 1;
+ return p;
+}
+static unsigned int _dns_skip(unsigned int p) {
+ for (;;) {
+ if (_dnsbuf[p] == 0) return p + 1;
+ if ((_dnsbuf[p] & 0xC0) == 0xC0) return p + 2;
+ p += _dnsbuf[p] + 1;
+ }
+}
+static unsigned char _dns_answer(unsigned char *ip) {
+ unsigned int p, a, an, type, rdlen;
+ an = ((unsigned int)_dnsbuf[6] << 8) | _dnsbuf[7];
+ if (an == 0) return 0;
+ p = _dns_skip(12) + 4;
+ for (a = 0; a < an; a++) {
+ p = _dns_skip(p);
+ type = ((unsigned int)_dnsbuf[p] << 8) | _dnsbuf[p + 1];
+ p += 8;
+ rdlen = ((unsigned int)_dnsbuf[p] << 8) | _dnsbuf[p + 1];
+ p += 2;
+ if (type == 1 && rdlen == 4) {
+ ip[0] = _dnsbuf[p]; ip[1] = _dnsbuf[p + 1]; ip[2] = _dnsbuf[p + 2]; ip[3] = _dnsbuf[p + 3];
+ return 1;
+ }
+ p += rdlen;
+ }
+ return 0;
+}
+
+/* resolve a dotted-quad or hostname into ip[4]; return 1 on success */
+static unsigned char resolve(char *host, unsigned char *ip) {
+ unsigned char s, n;
+ if (parse_ip(host, ip)) return 1; /* already an address */
+ _dnssrv[0] = 1; _dnssrv[1] = 1; _dnssrv[2] = 1; _dnssrv[3] = 1;
+ s = net_socket(SOCK_UDP);
+ if (s == 0xFF) return 0;
+ net_bind(s, 40000);
+ net_connect(s, _dnssrv, 53);
+ n = _dns_query(host);
+ net_send(s, _dnsbuf, n);
+ n = net_recv(s, _dnsbuf, 200);
+ net_close(s);
+ if (n == 0xFF) return 0;
+ return _dns_answer(ip);
+}
+#endif
diff --git a/usr/rm.c b/usr/rm.c
new file mode 100644
index 0000000..25f3004
--- /dev/null
+++ b/usr/rm.c
@@ -0,0 +1,7 @@
+#include "gbos.h"
+/* rm <name>: delete a file. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ if (argc > 0) fremove(argv[0]);
+}
diff --git a/usr/save.c b/usr/save.c
new file mode 100644
index 0000000..1755fea
--- /dev/null
+++ b/usr/save.c
@@ -0,0 +1,19 @@
+#include "gbos.h"
+/* save <name>: read one line from stdin and store it as a file. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ unsigned char fd;
+ char c;
+ if (argc < 1) { puts("usage: save <name>"); nl(); return; }
+ fd = open(argv[0], O_WRITE);
+ if (fd > 3) { puts("save: cannot create"); nl(); return; }
+ for (;;) {
+ c = readc();
+ if (c == EOF) break;
+ putc(c); /* echo */
+ fputc(fd, c);
+ if (c == '\n') break;
+ }
+ close(fd);
+}
diff --git a/usr/sh.c b/usr/sh.c
new file mode 100644
index 0000000..3178894
--- /dev/null
+++ b/usr/sh.c
@@ -0,0 +1,200 @@
+#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; }
+static unsigned char iscmd(char *t, char a, char b) {
+ return t[0] == a && t[1] == b && t[2] == 0;
+}
+
+/* keep the prompt's path string roughly in sync with chdir(). */
+static void set_cwd(char *cwd, char *arg) {
+ char *d, *s, *last;
+ if (arg[0] == '/') { /* absolute */
+ d = cwd; s = arg;
+ while (*s) *d++ = *s++;
+ *d = 0;
+ } else if (arg[0] == '.' && arg[1] == '.' && arg[2] == 0) {
+ last = cwd; d = cwd;
+ while (*d) { if (*d == '/') last = d; d++; }
+ if (last == cwd) { cwd[0] = '/'; cwd[1] = 0; } else *last = 0;
+ } else if (arg[0] == '.' && arg[1] == 0) {
+ /* stay */
+ } else { /* relative: append */
+ d = cwd; while (*d) d++;
+ if (!(cwd[0] == '/' && cwd[1] == 0)) *d++ = '/';
+ s = arg; while (*s) *d++ = *s++;
+ *d = 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.
+ If bg, launch in the background (don't wait; leave fds to the child). */
+static void run(char **tok, unsigned char start, unsigned char end,
+ unsigned char infd, unsigned char outfd, unsigned char bg) {
+ 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);
+ }
+ if (bg) { putc('['); putu(pid); puts("]"); nl(); return; } /* background */
+ /* wait for THIS child; report any background jobs that finish meanwhile */
+ for (;;) {
+ unsigned char r = wait();
+ if (r == pid || r == NOFD) break;
+ putc('['); putu(r); puts(" done]"); nl();
+ }
+done:
+ if (myin != NOFD) close(myin);
+ if (myout != NOFD) close(myout);
+}
+
+/* run a multi-stage pipeline (a | b | c): wire stages together with real
+ kernel pipes, launch them ALL concurrently, then wait for them. */
+static void run_pipeline(char **tok, unsigned char nt) {
+ unsigned char pids[8], npid = 0;
+ unsigned char cfd[8], ncfd = 0; /* file-redirect fds; closed after all exit */
+ unsigned char start = 0, prev_r = NOFD;
+ for (;;) {
+ unsigned char end = start, last, i, cmdend, fin = NOFD, fout = NOFD;
+ unsigned char my_out = NOFD, next_r = NOFD, id, pid;
+ while (end < nt && !isop(tok[end], '|')) end++;
+ last = (end >= nt);
+ if (!last) { /* pipe from this stage to the next */
+ unsigned char pr = pipe();
+ if (pr != NOFD) { next_r = pr; my_out = pr + 1; }
+ }
+ cmdend = end; /* this stage's own >/< redirects */
+ for (i = start; i < end; i++) {
+ if (isop(tok[i], '>')) { if (i + 1 < end) fout = open(tok[i+1], O_WRITE); if (i < cmdend) cmdend = i; }
+ else if (isop(tok[i], '<')) { if (i + 1 < end) fin = 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();
+ if (prev_r != NOFD) close(prev_r); /* orphaned pipe ends -> EOF/EPIPE */
+ if (my_out != NOFD) close(my_out);
+ if (fin != NOFD) close(fin);
+ if (fout != NOFD) close(fout);
+ } else {
+ set_cmdline(tok, start, cmdend);
+ pid = fork();
+ if (pid == 0) {
+ if (fin != NOFD) setin(fin); else if (prev_r != NOFD) setin(prev_r);
+ if (fout != NOFD) setout(fout); else if (my_out != NOFD) setout(my_out);
+ exec(id);
+ }
+ if (npid < 8) pids[npid++] = pid;
+ if (fin != NOFD && ncfd < 8) cfd[ncfd++] = fin;
+ if (fout != NOFD && ncfd < 8) cfd[ncfd++] = fout;
+ }
+ prev_r = next_r;
+ if (last) break;
+ start = end + 1;
+ }
+ { unsigned char got = 0; /* wait for all stages */
+ while (got < npid) {
+ unsigned char r = wait(), j, mine = 0;
+ if (r == NOFD) break;
+ for (j = 0; j < npid; j++) if (pids[j] == r) { mine = 1; break; }
+ if (mine) got++;
+ else { putc('['); putu(r); puts(" done]"); nl(); }
+ }
+ }
+ { unsigned char k; for (k = 0; k < ncfd; k++) close(cfd[k]); }
+}
+
+void main(void) {
+ char line[80];
+ char *tok[16];
+ char cwd[40];
+ cwd[0] = '/'; cwd[1] = 0;
+ /* bring up networking first: run the DHCP client and wait for it to finish
+ (got a lease, or gave up). Only then drop to the prompt. */
+ {
+ unsigned char d = lookup("dhcp");
+ if (d != NOFD) { if (fork() == 0) exec(d); else wait(); }
+ }
+ for (;;) {
+ unsigned char nt, i, pipepos, c, bg;
+ /* reap finished background jobs */
+ { unsigned char p; while ((p = reap())) { putc('['); putu(p); puts(" done]"); nl(); } }
+ puts(cwd); puts("# ");
+ /* read a line (echoing), stop at newline; exit on EOF */
+ i = 0;
+ for (;;) {
+ c = readc();
+ if (c == EOF) poweroff();
+ if (c == '\r' || c == '\n') { nl(); break; }
+ if (c == 8 || c == 127) { /* backspace: drop last char */
+ if (i > 0) { i--; putc(8); }
+ continue;
+ }
+ if (i < 79) { line[i++] = c; putc(c); }
+ }
+ line[i] = 0;
+ nt = tokenize(line, tok, 16);
+ if (nt == 0) continue;
+ if (iscmd(tok[0], 'c', 'd')) { /* cd builtin */
+ char *arg = (nt >= 2) ? tok[1] : "/";
+ if (chdir(arg) == 0) set_cwd(cwd, arg);
+ else { puts("cd: no such directory"); nl(); }
+ continue;
+ }
+ if ((tok[0][0] == 'e' && tok[0][1] == 'x') ||
+ (tok[0][0] == 'q' && tok[0][1] == 'u')) poweroff();
+ bg = 0;
+ if (isop(tok[nt - 1], '&')) { bg = 1; nt--; if (nt == 0) continue; }
+ pipepos = 0;
+ for (i = 1; i < nt; i++)
+ if (isop(tok[i], '|')) { pipepos = i; break; }
+ if (pipepos) run_pipeline(tok, nt);
+ else run(tok, 0, nt, NOFD, NOFD, bg);
+ }
+}
diff --git a/usr/sock.h b/usr/sock.h
new file mode 100644
index 0000000..876e85c
--- /dev/null
+++ b/usr/sock.h
@@ -0,0 +1,71 @@
+#ifndef SOCK_H
+#define SOCK_H
+/* gbos socket API - the kernel owns SLIP/IP/ICMP/UDP/TCP; programs just do
+ socket()/connect()/send()/recv()/close(). Header-only: one copy per program.
+ The netreq layout must match include/gbos.inc (NR_* / NET_* / SOCK_*). */
+#include "gbos.h"
+
+#define SOCK_ICMP 1
+#define SOCK_UDP 2
+#define SOCK_TCP 3
+
+struct netreq {
+ unsigned char op;
+ unsigned char sock;
+ unsigned char *buf;
+ unsigned char len;
+ unsigned char ip[4];
+ unsigned char port_hi, port_lo;
+};
+static struct netreq _nr;
+
+/* -> socket id, or 0xFF if the table is full */
+static unsigned char net_socket(unsigned char type) {
+ _nr.op = 0; _nr.sock = type;
+ return sys_net(&_nr);
+}
+/* set the remote address/port for send()/recv() */
+static unsigned char net_connect(unsigned char s, const unsigned char *ip, unsigned int port) {
+ unsigned char i;
+ _nr.op = 1; _nr.sock = s;
+ for (i = 0; i < 4; i++) _nr.ip[i] = ip[i];
+ _nr.port_hi = (unsigned char)(port >> 8); _nr.port_lo = (unsigned char)port;
+ return sys_net(&_nr);
+}
+/* send 'len' payload bytes to the connected peer -> 0 ok / 0xFF error */
+static unsigned char net_send(unsigned char s, void *buf, unsigned char len) {
+ _nr.op = 2; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = len;
+ return sys_net(&_nr);
+}
+/* receive up to 'max' payload bytes -> length, or 0xFF on timeout */
+static unsigned char net_recv(unsigned char s, void *buf, unsigned char max) {
+ _nr.op = 3; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = max;
+ return sys_net(&_nr);
+}
+/* non-blocking recv: one RX pump -> length, 0xFE if nothing buffered yet,
+ 0 on TCP EOF. Loop it with msleep() to own your timeout (see ping). */
+static unsigned char net_recv_nb(unsigned char s, void *buf, unsigned char max) {
+ _nr.op = 8; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = max;
+ return sys_net(&_nr);
+}
+/* set the local (source) port - needed so UDP replies demux back to us */
+static unsigned char net_bind(unsigned char s, unsigned int port) {
+ _nr.op = 5; _nr.sock = s;
+ _nr.port_hi = (unsigned char)(port >> 8); _nr.port_lo = (unsigned char)port;
+ return sys_net(&_nr);
+}
+static void net_close(unsigned char s) {
+ _nr.op = 4; _nr.sock = s; sys_net(&_nr);
+}
+/* pump the RX path once (answers inbound pings) without blocking */
+static void net_poll(void) {
+ _nr.op = 6; sys_net(&_nr);
+}
+/* set our IPv4 address (the DHCP client calls this once it has a lease) */
+static void net_setip(const unsigned char *ip) {
+ unsigned char i;
+ _nr.op = 7;
+ for (i = 0; i < 4; i++) _nr.ip[i] = ip[i];
+ sys_net(&_nr);
+}
+#endif
diff --git a/usr/spin.c b/usr/spin.c
new file mode 100644
index 0000000..2a3d4e8
--- /dev/null
+++ b/usr/spin.c
@@ -0,0 +1,6 @@
+#include "gbos.h"
+/* spin: a harmless long-running process that just yields forever.
+ Useful as a target for `ps` and `kill`. */
+void main(void) {
+ for (;;) yield();
+}
diff --git a/usr/true.c b/usr/true.c
new file mode 100644
index 0000000..4cf437e
--- /dev/null
+++ b/usr/true.c
@@ -0,0 +1,2 @@
+#include "gbos.h"
+void main(void) { sexit(0); }
diff --git a/usr/uname.c b/usr/uname.c
new file mode 100644
index 0000000..511501d
--- /dev/null
+++ b/usr/uname.c
@@ -0,0 +1,2 @@
+#include "gbos.h"
+void main(void) { puts("gbos sm83 (Game Boy Color)"); nl(); }
diff --git a/usr/wc.c b/usr/wc.c
new file mode 100644
index 0000000..4fa48ee
--- /dev/null
+++ b/usr/wc.c
@@ -0,0 +1,32 @@
+#include "gbos.h"
+/* wc [-l][-w][-c] [file]: count lines/words/chars of a file or stdin. */
+void main(void) {
+ char *argv[8];
+ unsigned char argc = argv_parse(argv, 8);
+ unsigned char wl = hasflag(argv, argc, 'l');
+ unsigned char ww = hasflag(argv, argc, 'w');
+ unsigned char wch = hasflag(argv, argc, 'c');
+ unsigned int lines = 0, words = 0, chars = 0;
+ unsigned char inword = 0, first = 1, i, fd = NOFD;
+ char *fname = 0;
+ int ch;
+ if (!wl && !ww && !wch) { wl = ww = wch = 1; }
+ for (i = 0; i < argc; i++) if (argv[i][0] != '-') { fname = argv[i]; break; }
+ if (fname) {
+ fd = open(fname, O_READ);
+ if (fd > 3) { puts("wc: no such file"); nl(); return; }
+ }
+ for (;;) {
+ if (fd > 3) { char c = readc(); if (c == EOF) break; ch = c; }
+ else { ch = fgetc(fd); if (ch < 0) break; }
+ chars++;
+ if (ch == '\n') lines++;
+ if (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') inword = 0;
+ else if (!inword) { inword = 1; words++; }
+ }
+ if (fd != NOFD) close(fd);
+ if (wl) { putu(lines); first = 0; }
+ if (ww) { if (!first) putc(' '); putu(words); first = 0; }
+ if (wch) { if (!first) putc(' '); putu(chars); }
+ nl();
+}
diff --git a/usr/wget.c b/usr/wget.c
new file mode 100644
index 0000000..aafbae0
--- /dev/null
+++ b/usr/wget.c
@@ -0,0 +1,43 @@
+#include "resolve.h"
+/* wget HOST|IP - fetch http://HOST/ over the kernel TCP socket layer and print
+ it. Resolves a hostname via DNS first (resolve.h), then connect()/send()/
+ recv() over TCP - all the SLIP/IP/UDP/TCP lives in the kernel. */
+
+static unsigned char buf[220]; /* first + large so namebuf/dst land arg-safe */
+static unsigned char namebuf[64];
+static unsigned char dst[4];
+
+void main(void) {
+ char *arg, *r;
+ unsigned char s, n, i;
+ arg = getargs();
+ if (!arg || !*arg) { puts("usage: wget HOST|IP"); nl(); sexit(1); }
+ for (i = 0; arg[i] && i < 63; i++) namebuf[i] = (unsigned char)arg[i];
+ namebuf[i] = 0;
+
+ if (!resolve((char *)namebuf, dst)) {
+ puts("wget: cannot resolve "); puts((char *)namebuf); nl(); sexit(1);
+ }
+ puts("connecting "); for (i = 0; i < 4; i++) { putu(dst[i]); if (i < 3) putc('.'); } nl();
+
+ s = net_socket(SOCK_TCP);
+ if (s == 0xFF) { puts("wget: no socket"); nl(); sexit(1); }
+ net_bind(s, 40001);
+ if (net_connect(s, dst, 80) == 0xFF) { puts("wget: connect failed"); nl(); net_close(s); sexit(1); }
+
+ /* GET / HTTP/1.0 with the requested host as the Host header */
+ r = "GET / HTTP/1.0\r\nHost: ";
+ n = 0;
+ while (r[n]) { buf[n] = (unsigned char)r[n]; n++; }
+ for (i = 0; namebuf[i]; i++) buf[n++] = namebuf[i];
+ buf[n++] = '\r'; buf[n++] = '\n'; buf[n++] = '\r'; buf[n++] = '\n';
+ net_send(s, buf, n);
+
+ for (;;) {
+ n = net_recv(s, buf, (unsigned char)sizeof(buf));
+ if (n == 0xFF) { nl(); puts("[timeout]"); nl(); break; }
+ if (n == 0) { nl(); puts("[eof]"); nl(); break; }
+ for (i = 0; i < n; i++) putc((char)buf[i]);
+ }
+ net_close(s);
+}