diff options
| author | user <user@clank> | 2026-07-17 20:22:48 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-17 20:22:48 +0200 |
| commit | c9495d4b52b245951bf4768379b38aeb1843d60f (patch) | |
| tree | 62bc4e2b00d691f5a3c06e4904f198e26bca900c | |
| parent | net: SYS_POLLCON + larger console ring for link-injected input (diff) | |
| download | gbos-c9495d4b52b245951bf4768379b38aeb1843d60f.tar.gz gbos-c9495d4b52b245951bf4768379b38aeb1843d60f.tar.xz gbos-c9495d4b52b245951bf4768379b38aeb1843d60f.zip | |
irc: a bitchx/irssi-style IRC client
irc HOST [NICK] - connects over the kernel TCP stack (DNS-resolves the
host), registers, and runs a live client on the 40x18 LCD.
UI, within the terminal's means (no cursor addressing - just \r + \b):
messages scroll above a fixed irssi-style input line '[#chan] text_'
redrawn in place; long input scrolls horizontally. The elders' formats:
<nick> msg, <nick:#c> off-channel, *nick* private, -nick- notice,
* nick action, >target< outbound, -!- server/status. Keys come from both
the OSK (SELECT) and the console ring (pollcon), so a hub can drive it.
Commands: /join /part /msg /me /nick /quit /raw, plus bare text to the
current channel. Handles PING (PONG + a wink), CTCP ACTION/VERSION,
JOIN/PART/QUIT/KICK/NICK, 332/353 topic+names, and 433 nick-in-use
(auto-appends _). Registers on bank 32 as program id 30.
Note: gbos doesn't zero C statics, so main() inits its state explicitly;
the local TCP port is randomized (DIV) to dodge a stale server-side
half-open from an unclean prior exit. Tested end to end against a small
ircd through gbhub: full MOTD burst, join, channel + private messages,
actions, and bot replies all render correctly.
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | c/irc.c | 375 | ||||
| -rw-r--r-- | include/gbos.inc | 1 | ||||
| -rw-r--r-- | src/programs.asm | 7 |
4 files changed, 384 insertions, 1 deletions
@@ -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/sh.bin c/ps.bin c/kill.bin c/spin.bin c/blk.bin c/mkdir.bin c/count.bin c/ptest.bin c/necho.bin c/wget.bin c/chat.bin c/netd.bin c/ping.bin c/nslookup.bin c/dhcp.bin + c/ls.bin c/save.bin c/rm.bin c/sh.bin c/ps.bin c/kill.bin c/spin.bin c/blk.bin c/mkdir.bin c/count.bin c/ptest.bin c/necho.bin c/wget.bin c/chat.bin c/netd.bin c/ping.bin c/nslookup.bin c/dhcp.bin c/irc.bin $(BUILD)/programs.o: $(CBLOBS) # C programs also depend on the shared libc sources @@ -0,0 +1,375 @@ +#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; + +/* ---- 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 void handle(char *l) { + char *pfx = (char *)"", *cmd, *arg, *txt = (char *)""; + 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 */ + + 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 (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; + + 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) { /* got a segment: drain hard */ + feed(seg, n); + idle = 0; + continue; + } + if (quitting) { say("-!- quit"); break; } + i = pollin(); /* OSK */ + if (!i) i = pollcon(); /* hub-injected console bytes */ + if (i) { key(i); idle = 0; continue; } + if (++idle >= 4) { msleep(20); idle = 4; } /* settle when idle */ + } + net_close(sock); + row_clear(); + sexit(0); +} diff --git a/include/gbos.inc b/include/gbos.inc index 55f0681..92d8732 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -258,5 +258,6 @@ DEF PROG_NETD EQU 26 DEF PROG_PING EQU 27 DEF PROG_NSLOOKUP EQU 28 DEF PROG_DHCP EQU 29 +DEF PROG_IRC EQU 30 ENDC diff --git a/src/programs.asm b/src/programs.asm index e89fe0c..36f9d02 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -130,6 +130,9 @@ ProgNslookup: SECTION "prog_dhcp", ROMX[$4000], BANK[31] ProgDhcp: INCBIN "c/dhcp.bin" +SECTION "prog_irc", ROMX[$4000], BANK[32] +ProgIrc: + INCBIN "c/irc.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (c/sh.c): parses >/</| and @@ -232,6 +235,8 @@ ProgramTable:: dw ProgNslookup db LOW(BANK(ProgDhcp)), HIGH(BANK(ProgDhcp)) dw ProgDhcp + db LOW(BANK(ProgIrc)), HIGH(BANK(ProgIrc)) + dw ProgIrc ; ----------------------------------------------------------------------------- ; NameTable (ROM0) - command name -> program id, searched by sys_lookup. @@ -298,4 +303,6 @@ NameTable:: db PROG_NSLOOKUP db "dhcp", 0 db PROG_DHCP + db "irc", 0 + db PROG_IRC db 0 |
