#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: text channel message * nick text CTCP ACTION 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; static unsigned char bar_up; /* is the status/input bar currently drawn? */ /* 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; } /* IRC color/format -> ANSI (defined below); used by the say* line printers. */ static void putfmt(const char *s); static void ansi_reset(void); static void esc(const char *s) { putc(27); puts(s); } /* emit a CSI sequence */ #define BARW 39 /* status/input bar width (keep < 40: no wrap) */ /* ---- the input line ------------------------------------------------------- */ /* erase what's drawn on the bottom row, park at col 0 (default colors) */ static void row_clear(void) { unsigned char i; if (!bar_up && !drawn) return; /* nothing on the row: skip the work */ esc("[0m"); /* reset, so we erase to black bg */ putc('\r'); for (i = 0; i < drawn; i++) putc(' '); putc('\r'); drawn = 0; bar_up = 0; } /* Redraw the irssi-style status/input bar on the bottom row: a full-width blue background, "[channel]" prompt (channel in yellow), then the tail of the input line. We paint the whole blue bar first, then overprint the content so the hardware cursor lands right after the text (irssi feel) instead of at the far edge. Total stays < 40 columns so it never wraps. */ static void row_draw(void) { unsigned char pl, vis, i; const char *t = target[0] ? target : "(status)"; row_clear(); esc("[44m"); /* blue background for the whole bar */ for (i = 0; i < BARW; i++) putc(' '); putc('\r'); esc("[37m"); putc('['); /* white bracket */ esc("[33m"); /* channel in yellow */ for (pl = 0; t[pl] && pl < 12; pl++) putc(t[pl]); esc("[37m"); putc(']'); putc(' '); /* white bracket, on to input */ pl += 3; /* prompt width: '[' + name + '] ' */ vis = (unsigned char)(BARW - pl - 1); /* room for text (cursor gets 1) */ i = (inlen > vis) ? (unsigned char)(inlen - vis) : 0; for (; i < inlen; i++) putc(input[i]); drawn = BARW; /* whole bar is dirty next clear */ esc("[0m"); /* back to defaults for messages */ bar_up = 1; } /* Print a message line above the input line. NB: these deliberately do NOT redraw the status bar - the caller redraws it once after a whole batch of lines (see the recv loop), so a flood repaints the 39-cell bar once, not once per line. row_clear() erases the bar on the first line of a batch; later lines short-circuit it (nothing left to clear). */ static void say(const char *s) { row_clear(); putfmt(s); ansi_reset(); nl(); } static void say2(const char *a, const char *b) { row_clear(); putfmt(a); putfmt(b); ansi_reset(); nl(); } static void say3(const char *a, const char *b, const char *c) { row_clear(); putfmt(a); putfmt(b); putfmt(c); ansi_reset(); nl(); } /* ---- 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'); } /* Map an mIRC color index (0..15, plus the 16..98 extended palette) to the nearest of our 7 renderable ANSI colors. We have no black/grey/background, so those fold onto white. Returns an ANSI fg code 31..37. */ static unsigned char mirc2ansi(unsigned char n) { switch (n) { case 2: case 12: return 34; /* blue / light blue */ case 3: case 9: return 32; /* green / light green */ case 4: case 5: return 31; /* red / brown */ case 6: case 13: return 35; /* purple / pink -> magenta */ case 7: case 8: return 33; /* orange / yellow */ case 10: case 11: return 36; /* teal / cyan */ default: return 37; /* white, black, grey, 16+ */ } } static void ansi_reset(void) { /* ESC [ 0 m : back to default */ putc(27); putc('['); putc('0'); putc('m'); } static void ansi_set(unsigned char code) { /* code 31..37 -> ESC [ 3 x m */ putc(27); putc('['); putc('3'); putc((char)('0' + code - 30)); putc('m'); } /* Print an IRC message, translating formatting to what the LCD can show: \x03 N[,BG] -> the mapped ANSI color (background ignored - we have none); \x03 alone and \x0f -> reset to default; \x04 RRGGBB[,RRGGBB] hex color is dropped (no true-color). Bold/underline/italic/reverse/strike/mono and any other control byte (including a raw ESC, so a server can't drive the terminal) are filtered out. Emitted on the fly, so nothing grows in place. */ static void putfmt(const char *s) { unsigned char c, d, n; while ((c = (unsigned char)*s++)) { if (c == 3) { /* mIRC color */ if (*s >= '0' && *s <= '9') { n = (unsigned char)(*s++ - '0'); if (*s >= '0' && *s <= '9') n = (unsigned char)((n << 3) + (n << 1) + (*s++ - '0')); /* n*10+d */ if (*s == ',' && s[1] >= '0' && s[1] <= '9') { /* skip ,background */ s += 2; if (*s >= '0' && *s <= '9') s++; } ansi_set(mirc2ansi(n)); } else { ansi_reset(); /* \x03 with no digits = reset */ } continue; } if (c == 4) { /* \x04 hex color: drop code + args */ d = 0; while (ishex(*s) && d < 6) { s++; d++; } if (*s == ',' && ishex(s[1])) { s++; d = 0; while (ishex(*s) && d < 6) { s++; d++; } } continue; } if (c == 15) { ansi_reset(); continue; } /* \x0f reset all formatting */ if (c < 32) continue; /* bold/underline/... + raw ESC */ putc((char)c); } } /* bitchx-style: give each nick a stable color from a hash of its name, so the same person is always the same color. 7 colors (31..37), no modulo op. */ static unsigned char nick_color(const char *n) { unsigned char h = 0, i; for (i = 0; n[i]; i++) h = (unsigned char)(h + (unsigned char)n[i] * 3); while (h >= 6) h = (unsigned char)(h - 6); /* 6 colors 31..36 (skip white) */ return (unsigned char)(31 + h); } static void put_nick(const char *n) { ansi_set(nick_color(n)); puts(n); ansi_reset(); } 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. Formatting is now translated when printed (putfmt) rather than stripped here, so mIRC colors survive as ANSI while bold/underline/etc. are filtered out. */ 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("* "); put_nick(pfx); putc(' '); } else if (sceq(dest, nick)) { putc('*'); put_nick(pfx); puts("* "); } else if (sceq(dest, target)) { putc('<'); put_nick(pfx); puts("> "); } else { putc('<'); put_nick(pfx); putc(':'); puts(dest); puts("> "); } putfmt(txt); ansi_reset(); nl(); return; } if (sceq(cmd, "NOTICE")) { row_clear(); putc('-'); puts(pfx[0] ? pfx : "server"); puts("- "); putfmt(txt); ansi_reset(); nl(); 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(); 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 */ /* MOTD can be long and the LCD is slow to scroll it, so suppress it client-side (the server still sends it to normal clients). 375 start prints one marker; 372 lines / 376 end / 377-378 spam are dropped. */ if (cmd[0]=='3'&&cmd[1]=='7'&&cmd[2]=='5') { say("-!- motd (suppressed)"); return; } if (cmd[0]=='3'&&cmd[1]=='7'&& (cmd[2]=='2'||cmd[2]=='6'||cmd[2]=='7'||cmd[2]=='8')) return; if (cmd[0]=='4'&&cmd[1]=='2'&&cmd[2]=='2') return; /* 422 no MOTD */ if (txt[0]) say2("-!- ", txt); /* welcome & 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('>'); put_nick(a); puts("< "); puts(m); nl(); 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("* "); put_nick(nick); putc(' '); puts(a); nl(); 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); /* bar redrawn by key() after submit */ 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('<'); put_nick(nick); puts("> "); puts(input); nl(); inlen = 0; } /* width of the "[channel] " prompt, to know where the input scrolls */ static unsigned char prompt_w(void) { const char *t = target[0] ? target : "(status)"; unsigned char n = 0; while (t[n] && n < 12) n++; return (unsigned char)(n + 3); } /* Handle one typed byte. The expensive full-bar row_draw() is avoided for the common cases: appending a char that still fits just prints that one cell (on the blue bg), and backspacing just erases one cell (\b keeps its blue color). Only when the input scrolls horizontally past the visible width, or on a submit that printed something, do we repaint the whole bar. */ static void key(unsigned char c) { unsigned char vis; if (c == '\n' || c == '\r') { submit(); if (!bar_up) row_draw(); return; } vis = (unsigned char)(BARW - prompt_w() - 1); if (c == 8 || c == 127) { /* backspace */ if (!inlen) return; inlen--; if (inlen >= vis) row_draw(); /* window scrolled: full repaint */ else putc(8); /* else drop one cell (stays blue) */ return; } if (c < 32) return; if (inlen >= 139) return; if (inlen < vis) { /* fits: print just the new cell */ input[inlen++] = (char)c; esc("[44m"); esc("[37m"); putc(c); esc("[0m"); } else { /* would scroll: full repaint */ 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; bar_up = 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 whole segment of lines */ if (!bar_up) row_draw(); /* then repaint the bar ONCE */ } 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); }