From 9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 18 Jul 2026 20:41:27 +0200 Subject: 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. --- usr/ansi.c | 67 +++++++++++++++++++ usr/irc.c | 215 +++++++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 221 insertions(+), 61 deletions(-) create mode 100644 usr/ansi.c (limited to 'usr') 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 -- cgit v1.3.1-sl0p