aboutsummaryrefslogtreecommitdiffstats
path: root/usr
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-18 20:41:27 +0200
committeruser <user@clank>2026-07-18 20:41:27 +0200
commit9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045 (patch)
tree5dcde624f3b366cc9244e08b0d2ca4ab35b338e2 /usr
parentkernel: a real timer source (64 Hz tick IRQ) + uptime(1) (diff)
downloadgbos-9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045.tar.gz
gbos-9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045.tar.xz
gbos-9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045.zip
term: 7-color terminal (per-glyph fg+bg) + ANSI escapes; colorize irc
Fano-plane palette scheme: the 7 colors map onto 7 CGB BG palettes so any color pair shares one palette; a tile's palette is picked from the set of colors its two cells need (tables generated by tools/gencolor.py). Per cell a packed (bg<<4)|fg byte lives alongside the char shadow, and the glyph blitter steers glyph/empty pixels to each cell's fg/bg color slots via plane masks. An ANSI-ish CSI parser (ESC [ .. m) drives it; usr/ansi.c demos it and the irc client now renders hashed nick colors, status dimming and a channel-activity bar.
Diffstat (limited to '')
-rw-r--r--usr/ansi.c67
-rw-r--r--usr/irc.c215
2 files changed, 221 insertions, 61 deletions
diff --git a/usr/ansi.c b/usr/ansi.c
new file mode 100644
index 0000000..0e73ff8
--- /dev/null
+++ b/usr/ansi.c
@@ -0,0 +1,67 @@
+#include "gbos.h"
+/* ansi - show the terminal's 7-color ANSI support. Also stress-tests per-glyph
+ * coloring: adjacent characters (which share an 8x8 tile) get different colors.
+ *
+ * NB: no initialized statics / pointer arrays here - SDCC would place them in
+ * the _INITIALIZED area at $A000 (the RAM window), overflowing the 16K image. */
+
+static void sgr(unsigned char n) { /* emit ESC [ n m */
+ putc(27);
+ putc('[');
+ putu(n);
+ putc('m');
+}
+
+void main(void) {
+ unsigned char i, c;
+ const char *s;
+
+ puts("ANSI 7-color test"); nl();
+
+ /* each color name printed in its own color (31..37) */
+ sgr(31); puts("red ");
+ sgr(32); puts("green ");
+ sgr(33); puts("yellow ");
+ sgr(34); puts("blue ");
+ sgr(35); puts("magenta ");
+ sgr(36); puts("cyan ");
+ sgr(37); puts("white");
+ sgr(0); nl();
+
+ /* per-glyph stress: 40 bars, color changes every single character, so the
+ * left/right halves of one tile differ - impossible with a per-tile palette */
+ c = 31;
+ for (i = 0; i < 40; i++) {
+ sgr(c);
+ putc('#');
+ if (++c > 37) c = 31;
+ }
+ sgr(0); nl();
+
+ /* one word, one color per letter */
+ c = 31;
+ s = "RAINBOW!";
+ for (i = 0; s[i]; i++) {
+ sgr(c);
+ putc(s[i]);
+ if (++c > 37) c = 31;
+ }
+ sgr(0); nl();
+
+ /* background colors: a block of each (SGR 41..47), default white text */
+ puts("backgrounds:"); nl();
+ c = 41;
+ for (i = 0; i < 7; i++) {
+ putc(27); putc('['); putc('4'); putc((char)('0' + (c - 40))); putc('m');
+ puts(" bg ");
+ if (++c > 47) c = 41;
+ }
+ sgr(0); nl();
+
+ /* foreground-on-background combos (one CSI carries both: ESC[fg;bg m) */
+ puts("\033[37;41m wht/red \033[33;44m yel/blu \033[34;43m blu/yel "
+ "\033[32;45m grn/mag \033[36;41m cyn/red \033[0m"); nl();
+
+ /* per-glyph fg over a shared bg: adjacent letters differ, bg stays green */
+ puts("\033[42m\033[31mR\033[33mA\033[34mI\033[35mN\033[37mBOW\033[0m"); nl();
+}
diff --git a/usr/irc.c b/usr/irc.c
index 250738a..441d98a 100644
--- a/usr/irc.c
+++ b/usr/irc.c
@@ -30,6 +30,7 @@ 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
@@ -64,44 +65,65 @@ static void sfin(void) { /* CRLF + send */
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 */
+/* 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 "[target] tail-of-input_"; keep total < 40 so it never wraps */
+/* 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();
- putc('[');
+ 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]);
- putc(']'); putc(' ');
- pl += 3; /* prompt width */
- vis = 39 - pl - 1; /* room for text (cursor gets 1) */
+ 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 = pl + ((inlen > vis) ? vis : inlen);
+ 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 */
+/* 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();
- puts(s); nl();
- row_draw();
+ putfmt(s); ansi_reset(); nl();
}
static void say2(const char *a, const char *b) {
row_clear();
- puts(a); puts(b); nl();
- row_draw();
+ putfmt(a); putfmt(b); ansi_reset(); nl();
}
static void say3(const char *a, const char *b, const char *c) {
row_clear();
- puts(a); puts(b); puts(c); nl();
- row_draw();
+ putfmt(a); putfmt(b); putfmt(c); ansi_reset(); nl();
}
/* ---- server line parsing -------------------------------------------------- */
@@ -117,33 +139,74 @@ 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++;} }
+/* 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) {
- 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++;} }
+ 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==2||c==15||c==17||c==22||c==29||c==30||c==31) { r++; continue; }
- *w++ = *r++;
+ if (c == 15) { ansi_reset(); continue; } /* \x0f reset all formatting */
+ if (c < 32) continue; /* bold/underline/... + raw ESC */
+ putc((char)c);
}
- *w = 0;
+}
+
+/* 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) {
@@ -164,8 +227,9 @@ static void handle(char *l) {
if (*p == ' ') *p = 0;
p++;
}
- /* arg = first param (NUL-joined list), txt = trailing */
- strip_fmt(txt); /* kill color/format codes + args */
+ /* 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();
@@ -188,18 +252,16 @@ static void handle(char *l) {
} 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();
+ 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("- "); puts(txt); nl();
- row_draw();
+ putc('-'); puts(pfx[0] ? pfx : "server"); puts("- "); putfmt(txt); ansi_reset(); nl();
return;
}
if (sceq(cmd, "JOIN")) {
@@ -229,7 +291,6 @@ static void handle(char *l) {
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)) {
@@ -250,7 +311,14 @@ static void handle(char *l) {
}
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 */
+ /* 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 */
@@ -290,16 +358,14 @@ static void run_cmd(char *t) {
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();
+ 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("* "); puts(nick); putc(' '); puts(a); nl();
- row_draw();
+ puts("* "); put_nick(nick); putc(' '); puts(a); nl();
return;
}
if (sceq(t, "/nick")) {
@@ -324,26 +390,49 @@ static void submit(void) {
if (!inlen) return;
if (input[0] == '/') {
inlen = 0;
- run_cmd(input);
- row_draw();
+ 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('<'); puts(nick); puts("> "); puts(input); nl();
+ putc('<'); put_nick(nick); puts("> "); puts(input); nl();
inlen = 0;
- row_draw();
}
+/* 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) {
- if (c == '\n' || c == '\r') { submit(); return; }
- if (c == 8 || c == 127) {
- if (inlen) { inlen--; row_draw(); }
+ 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) { input[inlen++] = (char)c; row_draw(); }
+ 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 ----------------------------------------------------------------- */
@@ -354,6 +443,7 @@ void main(void) {
/* 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 */
@@ -399,7 +489,10 @@ void main(void) {
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 (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