aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--include/gbos.inc23
-rw-r--r--src/color.asm40
-rw-r--r--src/osk.asm8
-rw-r--r--src/programs.asm7
-rw-r--r--src/term.asm523
-rw-r--r--tools/gencolor.py113
-rw-r--r--usr/ansi.c67
-rw-r--r--usr/irc.c215
9 files changed, 907 insertions, 91 deletions
diff --git a/Makefile b/Makefile
index fbdf10a..19de12e 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ $(BUILD)/%.o: src/%.asm | $(BUILD)
# $(BUILD)/usr and INCBIN'd by programs.asm
PROGS := chello echo true false uname pid cat wc head args \
ls save rm sh ps kill spin blk mkdir count ptest necho \
- wget chat netd ping nslookup dhcp irc uptime
+ wget chat netd ping nslookup dhcp irc uptime ansi
CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS))
$(BUILD)/programs.o: $(CBLOBS)
diff --git a/include/gbos.inc b/include/gbos.inc
index 246628c..e63d53f 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -17,6 +17,28 @@ DEF IEF_TIMER EQU %00000100
DEF rDIV EQU $FF04 ; divider register: free-running 8-bit @ 16384 Hz
DEF rSVBK EQU $FF70 ; CGB WRAM bank select ($D000-$DFFF): 1..7
DEF wCurMask EQU $D589 ; term/osk: cursor underline bits OR'd into a tile's last row
+; --- color renderer (term.asm) ---------------------------------------------
+; Per-glyph foreground AND background color. Each pixel of a tile gets a CGB
+; color-id: glyph pixels use the fg color's slot, empty pixels the bg color's
+; slot. The 7 colors form a Fano plane across 7 BG palettes so any pair shares
+; one palette; a tile's palette is chosen from the set of colors it needs
+; (tools/gencolor.py builds PalData/BitTab/BestPal/SlotOf). Per cell we store a
+; packed color byte = (bg<<4)|fg (each 0..7; 0=black/default).
+DEF wFGP0 EQU $D5A2 ; foreground plane-0 mask (glyph pixels -> id bit0)
+DEF wFGP1 EQU $D5A3 ; foreground plane-1 mask (glyph pixels -> id bit1)
+DEF wCurColor EQU $D5A4 ; current packed color (bg<<4)|fg for chars printed next
+DEF wAnsiState EQU $D5A5 ; ANSI parser: 0=normal 1=after-ESC 2=in-CSI
+DEF wAnsiParam EQU $D5A6 ; ANSI parser: accumulating numeric parameter
+DEF wPalAttr EQU $D5A7 ; chosen palette (bits 0..2) for a tile's tilemap attr
+DEF wTMSlot EQU $D5A8 ; line-slot stashed by term_write_tilemap's attr pass
+DEF wBGP0 EQU $D5A9 ; background plane-0 mask (empty pixels -> id bit0)
+DEF wBGP1 EQU $D5AA ; background plane-1 mask (empty pixels -> id bit1)
+DEF wCSL EQU $D5AB ; color_setup scratch: left cell packed color
+DEF wCSR EQU $D5AC ; color_setup scratch: right cell packed color
+DEF wCSset EQU $D5AD ; color_setup scratch: 7-bit set of colors present
+DEF wCSpb EQU $D5AE ; color_setup scratch: palette*8 (SlotOf base)
+DEF wCStmp0 EQU $D5AF ; color_setup scratch: assembled plane-0 mask
+DEF wCStmp1 EQU $D5B0 ; color_setup scratch: assembled plane-1 mask
DEF OSK_DOCK EQU 3 ; on-screen keyboard height in rows (dock at the bottom)
DEF rKEY1 EQU $FF4D ; CGB speed switch
DEF rSB EQU $FF01 ; serial data
@@ -265,5 +287,6 @@ DEF PROG_NSLOOKUP EQU 28
DEF PROG_DHCP EQU 29
DEF PROG_IRC EQU 30
DEF PROG_UPTIME EQU 31
+DEF PROG_ANSI EQU 32
ENDC
diff --git a/src/color.asm b/src/color.asm
new file mode 100644
index 0000000..5c12569
--- /dev/null
+++ b/src/color.asm
@@ -0,0 +1,40 @@
+; auto-generated by tools/gencolor.py - Fano-plane color tables
+; (foreground + background). DO NOT EDIT.
+SECTION "color", ROM0
+
+; 8 CGB BG palettes, 4 colors each (c0=black). Loaded at BCPS $80.
+PalData::
+ db $00,$00,$FF,$7F,$1F,$00,$E0,$7F ; palette 0
+ db $FF,$7F,$00,$00,$00,$00,$00,$00 ; palette 1
+ db $00,$00,$1F,$00,$E0,$03,$FF,$03 ; palette 2
+ db $00,$00,$1F,$00,$00,$7C,$1F,$7C ; palette 3
+ db $00,$00,$E0,$03,$00,$7C,$E0,$7F ; palette 4
+ db $00,$00,$E0,$03,$1F,$7C,$FF,$7F ; palette 5
+ db $00,$00,$FF,$03,$00,$7C,$FF,$7F ; palette 6
+ db $00,$00,$FF,$03,$1F,$7C,$E0,$7F ; palette 7
+
+; BitTab[color] = bit for that color in the 'colors present' set (black=0)
+BitTab::
+ db $00,$01,$02,$04,$08,$10,$20,$40
+
+; BestPal[set] = palette (0..7) covering the most colors in a 7-bit set
+BestPal::
+ db $00,$00,$02,$02,$02,$02,$02,$02,$03,$03,$04,$02,$06,$02,$02,$02 ; 0..15
+ db $03,$03,$05,$02,$07,$02,$02,$02,$03,$03,$03,$03,$03,$03,$02,$02 ; 16..31
+ db $00,$00,$04,$00,$07,$00,$02,$02,$04,$00,$04,$04,$04,$00,$04,$02 ; 32..47
+ db $07,$00,$04,$00,$07,$07,$07,$02,$03,$03,$04,$03,$07,$03,$04,$02 ; 48..63
+ db $00,$00,$05,$00,$06,$00,$02,$02,$06,$00,$04,$00,$06,$06,$06,$02 ; 64..79
+ db $05,$00,$05,$05,$05,$00,$05,$02,$03,$03,$05,$03,$06,$03,$05,$02 ; 80..95
+ db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$04,$00,$06,$00,$04,$00 ; 96..111
+ db $00,$00,$05,$00,$07,$00,$05,$00,$00,$00,$04,$00,$06,$00,$04,$00 ; 112..127
+
+; SlotOf[palette*8 + color] = that color's slot/id (0..3) in the palette
+SlotOf::
+ db $00,$02,$01,$01,$01,$01,$03,$01 ; palette 0
+ db $00,$01,$01,$01,$01,$01,$01,$01 ; palette 1
+ db $00,$01,$02,$03,$01,$01,$01,$01 ; palette 2
+ db $00,$01,$01,$01,$02,$03,$01,$01 ; palette 3
+ db $00,$01,$01,$01,$02,$01,$03,$01 ; palette 4
+ db $00,$01,$01,$01,$01,$02,$01,$03 ; palette 5
+ db $00,$01,$01,$01,$02,$01,$01,$03 ; palette 6
+ db $00,$01,$01,$01,$01,$02,$03,$01 ; palette 7
diff --git a/src/osk.asm b/src/osk.asm
index f5d99ac..d5edad7 100644
--- a/src/osk.asm
+++ b/src/osk.asm
@@ -80,10 +80,16 @@ osk_init::
ld [rBCPD], a
ld [rBCPD], a ; c1 = $0000 black
- ; build the key tiles (no cursor underline)
+ ; build the key tiles (no cursor underline). Glyph pixels -> color-id 1
+ ; (palette slot 1: white normal / black inverted); empty pixels -> id 0.
xor a
ld [wCurMask], a
ld [wOskBuild], a
+ ld [wFGP1], a
+ ld [wBGP0], a
+ ld [wBGP1], a
+ ld a, $FF
+ ld [wFGP0], a
.build
ld a, [wOskBuild]
ld e, a
diff --git a/src/programs.asm b/src/programs.asm
index 7c17fb5..c6228f3 100644
--- a/src/programs.asm
+++ b/src/programs.asm
@@ -136,6 +136,9 @@ ProgIrc:
SECTION "prog_uptime", ROMX[$4000], BANK[33]
ProgUptime:
INCBIN "build/usr/uptime.bin"
+SECTION "prog_ansi", ROMX[$4000], BANK[34]
+ProgAnsi:
+ INCBIN "build/usr/ansi.bin"
; -----------------------------------------------------------------------------
; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and
@@ -242,6 +245,8 @@ ProgramTable::
dw ProgIrc
db LOW(BANK(ProgUptime)), HIGH(BANK(ProgUptime))
dw ProgUptime
+ db LOW(BANK(ProgAnsi)), HIGH(BANK(ProgAnsi))
+ dw ProgAnsi
ProgramTableEnd::
; -----------------------------------------------------------------------------
@@ -313,4 +318,6 @@ NameTable::
db PROG_IRC
db "uptime", 0
db PROG_UPTIME
+ db "ansi", 0
+ db PROG_ANSI
db 0
diff --git a/src/term.asm b/src/term.asm
index 099ca0a..d62310e 100644
--- a/src/term.asm
+++ b/src/term.asm
@@ -23,6 +23,8 @@ DEF TCOLS EQU 40 ; text columns
DEF TROWS EQU 18 ; text rows
DEF TSTRIDE EQU 64 ; bytes per line-slot in the buffer (power of 2)
DEF TERM_BUF EQU $D600 ; 18*64 = 1152 B (WRAMX, above the FS caches)
+DEF COL_BUF EQU $DA80 ; per-cell fg color, parallel to TERM_BUF (18*64 = 1152 B)
+DEF COL_OFF EQU COL_BUF - TERM_BUF ; $0480: add to a char addr -> its color addr
; terminal state (WRAMX gap $D580-$D5FF)
DEF wGlyphL EQU $D580 ; 2
@@ -53,24 +55,20 @@ term_init::
ld [rSCX], a
ld [rSCY], a
- ; BG palette 0: color0 = black (bg), color1 = white (text) - dark terminal
- ld a, $80
+ ; Load all 8 CGB BG palettes from PalData (see tools/gencolor.py). Palettes
+ ; 0 and 2..7 are the 7 Fano-plane text palettes (each = bg + 3 of our 7
+ ; colors); palette 1 is the OSK's white-on-black inverse. Per-glyph color
+ ; works because each tile picks the one palette that holds both its glyphs'
+ ; colors, and the two bitplanes steer each glyph to its color's slot.
+ ld a, $80 ; BCPS index 0, auto-increment
ld [rBCPS], a
- xor a
- ld [rBCPD], a
- ld [rBCPD], a ; c0 = $0000 black (background)
- ld a, $FF
- ld [rBCPD], a
- ld a, $7F
- ld [rBCPD], a ; c1 = $7FFF white (text)
- ld a, $FF
- ld [rBCPD], a
- ld a, $7F
- ld [rBCPD], a ; c2 = white
- ld a, $FF
+ ld hl, PalData
+ ld b, 8 * 4 * 2 ; 8 palettes * 4 colors * 2 bytes
+.palcopy
+ ld a, [hl+]
ld [rBCPD], a
- ld a, $7F
- ld [rBCPD], a ; c3 = white
+ dec b
+ jr nz, .palcopy
; state: assign[sr]=sr, cursor at 0,0 (on)
ld hl, wAssign
@@ -86,8 +84,11 @@ term_init::
ld [wCurCol], a
ld [wViewTop], a ; view starts unshifted (all 18 rows)
ld [wOskVisible], a ; OSK hidden until toggled
+ ld [wAnsiState], a ; ANSI parser idle
ld a, 1
ld [wCursorOn], a
+ ld a, 7
+ ld [wCurColor], a ; default fg color = 7 (white)
; clear the buffer (all slots) to spaces
ld hl, TERM_BUF
@@ -100,6 +101,17 @@ term_init::
or e
jr nz, .clr
+ ; clear the color buffer to the default fg color (7 = white)
+ ld hl, COL_BUF
+ ld de, TROWS * TSTRIDE
+.clrc
+ ld a, 7
+ ld [hl+], a
+ dec de
+ ld a, d
+ or e
+ jr nz, .clrc
+
call term_write_tilemap
call term_redraw ; build all tiles (blank + cursor)
@@ -134,16 +146,47 @@ term_write_tilemap::
ld [rVBK], a
ld c, 0
.a_row
- call .slot20
+ call .slot20 ; DE = pos, HL = tilemap addr, wTMSlot = slot
ld b, TCOLS/2
.a_col
- ld a, d ; HIGH(pos): 0 or 1
+ ; attribute = palette(from this tile's two cell colors) | bank bit(pos hi).
+ push hl ; save tilemap write ptr
+ push de ; save pos
+ ; color addr = COL_BUF + slot*64 + tcol*2 ; tcol = (TCOLS/2) - b
+ ld a, [wTMSlot]
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl ; slot*64
+ ld a, TCOLS/2
+ sub b
+ add a ; tcol*2
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld de, COL_BUF
+ add hl, de
+ ld a, [hl+]
+ ld d, a ; colL
+ ld a, [hl]
+ ld e, a ; colR
+ call color_setup ; -> wPalAttr (preserves B, C)
+ pop de ; restore pos
+ pop hl ; restore tilemap write ptr
+ ld a, d ; HIGH(pos): 0 or 1 -> VRAM bank bit
or a
jr z, .a0
- ld a, $08 ; VRAM bank 1 attribute
+ ld a, [wPalAttr]
+ or $08 ; VRAM bank 1 tile
jr .aw
.a0
- xor a
+ ld a, [wPalAttr]
.aw
ld [hl+], a
inc de
@@ -170,6 +213,7 @@ term_write_tilemap::
adc 0
ld h, a
ld a, [hl] ; L = assign[logical row]
+ ld [wTMSlot], a ; stash slot for the color-aware attr pass
ld l, a
ld h, 0
add hl, hl
@@ -239,6 +283,135 @@ glyph_ptr:
ret
; -----------------------------------------------------------------------------
+; color_setup - D = left cell packed color, E = right cell packed color (each
+; (bg<<4)|fg, 0..7 per nibble). Picks the tile's palette (wPalAttr) and the four
+; bitplane masks: wFGP0/wFGP1 steer GLYPH pixels to each cell's fg slot, and
+; wBGP0/wBGP1 steer EMPTY pixels to each cell's bg slot. Clobbers A, DE, HL;
+; preserves B, C (so it runs inside the tilemap column loop).
+; -----------------------------------------------------------------------------
+color_setup:
+ ld a, d
+ ld [wCSL], a
+ ld a, e
+ ld [wCSR], a
+ ; set = union of the (non-black) colors of both cells
+ xor a
+ ld [wCSset], a
+ ld a, [wCSL]
+ and $0F
+ call cs_add ; fg left
+ ld a, [wCSL]
+ swap a
+ and $0F
+ call cs_add ; bg left
+ ld a, [wCSR]
+ and $0F
+ call cs_add ; fg right
+ ld a, [wCSR]
+ swap a
+ and $0F
+ call cs_add ; bg right
+ ; palette = BestPal[set]
+ ld a, [wCSset]
+ ld l, a
+ ld h, 0
+ ld de, BestPal
+ add hl, de
+ ld a, [hl]
+ ld [wPalAttr], a
+ add a
+ add a
+ add a ; palette*8 = SlotOf base
+ ld [wCSpb], a
+ ; foreground masks from the two fg slots (left in high nibble, right in low)
+ ld a, [wCSL]
+ and $0F
+ call cs_slot
+ ld d, a
+ ld a, [wCSR]
+ and $0F
+ call cs_slot
+ ld e, a
+ call cs_planes
+ ld a, [wCStmp0]
+ ld [wFGP0], a
+ ld a, [wCStmp1]
+ ld [wFGP1], a
+ ; background masks from the two bg slots
+ ld a, [wCSL]
+ swap a
+ and $0F
+ call cs_slot
+ ld d, a
+ ld a, [wCSR]
+ swap a
+ and $0F
+ call cs_slot
+ ld e, a
+ call cs_planes
+ ld a, [wCStmp0]
+ ld [wBGP0], a
+ ld a, [wCStmp1]
+ ld [wBGP1], a
+ ret
+
+; cs_add - A = color (0..7); OR its set-bit into wCSset (black contributes none)
+cs_add:
+ or a
+ ret z
+ ld l, a
+ ld h, 0
+ ld de, BitTab
+ add hl, de
+ ld a, [hl]
+ ld hl, wCSset
+ or [hl]
+ ld [hl], a
+ ret
+
+; cs_slot - A = color (0..7) -> A = its id (0..3) in the chosen palette.
+; Addresses with A/HL only so it preserves DE (the caller holds the left id in D
+; across the second call). SlotOf index = wCSpb + color <= 63, so no page carry
+; issue beyond the single adc below.
+cs_slot:
+ ld l, a
+ ld a, [wCSpb]
+ add l
+ add LOW(SlotOf)
+ ld l, a
+ ld a, 0
+ adc HIGH(SlotOf)
+ ld h, a
+ ld a, [hl]
+ ret
+
+; cs_planes - D = id for the high (left) nibble, E = id for the low (right)
+; nibble -> wCStmp0 = plane-0 mask, wCStmp1 = plane-1 mask. An id's bit0 lights
+; plane 0, bit1 lights plane 1; the left cell owns $F0, the right owns $0F.
+cs_planes:
+ xor a
+ bit 0, d
+ jr z, .p0r
+ or $F0
+.p0r
+ bit 0, e
+ jr z, .p0done
+ or $0F
+.p0done
+ ld [wCStmp0], a
+ xor a
+ bit 1, d
+ jr z, .p1r
+ or $F0
+.p1r
+ bit 1, e
+ jr z, .p1done
+ or $0F
+.p1done
+ ld [wCStmp1], a
+ ret
+
+; -----------------------------------------------------------------------------
; build_tile - build tile for tile-position DE (E=index, D=bank) from chars
; B (left) and C (right). ORs wCurMask into the last pixel row (cursor).
; -----------------------------------------------------------------------------
@@ -291,21 +464,39 @@ build_tile::
ld d, a
ld a, [de]
and $0F
- or c ; A = combined pixel row
- ld e, a ; save (DE reloaded next iter)
+ or c ; A = glyph row g (left=hi nibble, right=lo)
+ ld c, a ; C = g
+ cpl
+ ld d, a ; D = ~g (the empty pixels = background)
+ ; plane1 = (g & wFGP1) | (~g & wBGP1) -> hold in E for the write below
+ ld a, [wFGP1]
+ and c
+ ld e, a
+ ld a, [wBGP1]
+ and d
+ or e
+ ld e, a ; E = plane 1
+ ; plane0 = (g & wFGP0) | (~g & wBGP0) (+cursor underline on the last row)
+ ld a, [wFGP0]
+ and c
+ ld c, a ; C = g & wFGP0
+ ld a, [wBGP0]
+ and d
+ or c ; A = plane 0 base
+ ld c, a
ld a, b
dec a
jr nz, .nocur ; last row (b==1)? add cursor underline
ld a, [wCurMask]
- or e
+ or c
jr .wr
.nocur
- ld a, e
+ ld a, c
.wr
pop hl
ld [hl+], a ; plane 0
- xor a
- ld [hl+], a ; plane 1 = 0
+ ld a, e
+ ld [hl+], a ; plane 1
; advance glyph pointers - MUST NOT touch HL (it holds the VRAM dest!)
ld a, [wGlyphL]
inc a
@@ -349,7 +540,16 @@ render_tile:
ld a, [hl+]
ld [wRT_cL], a
ld a, [hl]
- ld [wRT_cR], a
+ ld [wRT_cR], a ; HL = &cellR
+ ; per-glyph color: read colL/colR from the parallel color buffer, then pick
+ ; the tile's palette + bitplane masks (D=colL, E=colR) via the Fano table.
+ ld de, COL_OFF
+ add hl, de ; HL = &colR
+ ld a, [hl-]
+ ld e, a ; E = colR
+ ld a, [hl]
+ ld d, a ; D = colL
+ call color_setup
; cursor mask
xor a
ld [wCurMask], a
@@ -418,7 +618,102 @@ render_cursor_tile:
ld a, [wCurCol]
srl a ; /2 = tile col
ld c, a
- jp render_tile
+ call render_tile
+ ; fall through: keep the cursor tile's palette attribute in sync with its
+ ; (possibly just-recolored) cells.
+
+; -----------------------------------------------------------------------------
+; update_cursor_attr - rewrite the tilemap palette attribute for the tile under
+; the cursor, from that tile's two cell colors. The cursor line is always fully
+; on-screen (compute_offset guarantees it), so its physical row = wCurRow minus
+; the view offset with no clamping. Bank bit follows the tile's VRAM position.
+; -----------------------------------------------------------------------------
+update_cursor_attr:
+ ; slot = assign[wCurRow]
+ ld a, [wCurRow]
+ add LOW(wAssign)
+ ld l, a
+ ld a, HIGH(wAssign)
+ adc 0
+ ld h, a
+ ld a, [hl]
+ ld [wTMSlot], a
+ ; color addr = COL_BUF + slot*64 + (wCurCol & $FE)
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl ; slot*64
+ ld a, [wCurCol]
+ and $FE ; left cell of the tile
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld de, COL_BUF
+ add hl, de
+ ld a, [hl+]
+ ld d, a ; colL
+ ld a, [hl]
+ ld e, a ; colR
+ call color_setup ; -> wPalAttr (masks recomputed, harmless)
+ ; bank bit: pos = slot*20 + (wCurCol/2)
+ ld a, [wTMSlot]
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl ; *4
+ ld d, h
+ ld e, l
+ add hl, hl
+ add hl, hl ; *16
+ add hl, de ; *20
+ ld a, [wCurCol]
+ srl a
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = pos
+ ld a, [wPalAttr]
+ bit 0, h ; pos >= 256 -> VRAM bank 1
+ jr z, .noattr_bank
+ or $08
+.noattr_bank
+ ld c, a ; C = attribute byte
+ ; physical row = wCurRow - wViewTop
+ ld a, [wCurRow]
+ ld hl, wViewTop
+ sub [hl]
+ ; tilemap attr addr = $9800 + physical*32 + (wCurCol/2), VRAM bank 1
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl ; *32
+ ld a, h
+ add $98
+ ld h, a
+ ld a, [wCurCol]
+ srl a
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, 1
+ ld [rVBK], a
+ ld a, c
+ ld [hl], a
+ xor a
+ ld [rVBK], a
+ ret
; -----------------------------------------------------------------------------
; term_redraw - rebuild every tile from the buffer (init / full refresh).
@@ -479,6 +774,24 @@ term_scroll::
ld [hl+], a
dec b
jr nz, .clr
+ ; reset the freed line's colors to the default (1 = white)
+ ld a, [wScratch]
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl ; *64
+ ld de, COL_BUF
+ add hl, de
+ ld b, TCOLS
+.clrcol
+ ld a, 7
+ ld [hl+], a
+ dec b
+ jr nz, .clrcol
; rebuild logical row 17
ld c, 0
.build
@@ -549,6 +862,18 @@ term_putc::
ld [rSVBK], a
ret
term_putc_body:
+ ld e, a ; save the incoming byte
+ ld a, [wAnsiState]
+ or a
+ jp nz, .ansi_active
+ ld a, e
+ cp 27 ; ESC begins an ANSI sequence
+ jr nz, .not_esc
+ ld a, 1
+ ld [wAnsiState], a
+ ret
+.not_esc:
+ ld a, e
cp 10
jr z, .newline
cp 13
@@ -569,6 +894,13 @@ term_putc_body:
ld h, a
ld a, [wRT_cL]
ld [hl], a
+ ; record this cell's color in the parallel color buffer
+ push hl
+ ld de, COL_OFF
+ add hl, de
+ ld a, [wCurColor]
+ ld [hl], a
+ pop hl
; redraw current cell WITHOUT cursor (shows the char)
xor a
ld [wCursorOn], a
@@ -636,6 +968,141 @@ term_putc_body:
call render_cursor_tile
ret
+; --- ANSI CSI parser (reached when wAnsiState != 0) --------------------------
+.ansi_active:
+ dec a ; state 1 (just saw ESC)?
+ jr nz, .ansi_csi ; else state 2 (inside CSI)
+ ld a, e
+ cp 91 ; '[' : ESC '[' opens a CSI; anything else aborts
+ jr nz, .ansi_off
+ ld a, 2
+ ld [wAnsiState], a
+ xor a
+ ld [wAnsiParam], a
+ ret
+.ansi_off:
+ xor a
+ ld [wAnsiState], a
+ ret
+.ansi_csi:
+ ld a, e
+ cp 48 ; '0'
+ jr c, .csi_final
+ cp 58 ; '9'+1
+ jr nc, .csi_final
+ sub 48 ; '0' : accumulate decimal parameter
+ ld d, a
+ ld a, [wAnsiParam]
+ add a, a ; *2
+ ld c, a
+ add a, a
+ add a, a ; *8
+ add c ; *10
+ add d
+ ld [wAnsiParam], a
+ ret
+.csi_final:
+ ld a, e
+ cp 109 ; 'm' : set graphics rendition
+ jr z, .csi_sgr
+ cp 59 ; ';' : parameter separator
+ jr z, .csi_sep
+ xor a ; unknown final byte: end the sequence
+ ld [wAnsiState], a
+ ret
+.csi_sep:
+ call apply_sgr
+ xor a
+ ld [wAnsiParam], a
+ ret
+.csi_sgr:
+ call apply_sgr
+ xor a
+ ld [wAnsiState], a
+ ret
+
+; -----------------------------------------------------------------------------
+; apply_sgr - act on the accumulated SGR parameter in wAnsiParam, updating the
+; packed current color (bg<<4)|fg. Foreground 30..37 / 90..97 map through
+; SgrColorMap (black-fg -> white so it stays visible on black); background
+; 40..47 / 100..107 map straight (0=black..7=white, black bg = our default).
+; 0 = reset both; 39 = default fg; 49 = default bg. Others are ignored.
+; -----------------------------------------------------------------------------
+apply_sgr:
+ ld a, [wAnsiParam]
+ or a
+ jr z, .reset ; 0 = reset fg+bg
+ cp 30
+ jr c, .done
+ cp 38
+ jr c, .fg ; 30..37
+ cp 39
+ jr z, .fgdef ; 39 = default fg
+ cp 40
+ jr c, .done ; 38 (256-color intro): ignore
+ cp 48
+ jr c, .bg ; 40..47
+ cp 49
+ jr z, .bgdef ; 49 = default bg
+ cp 90
+ jr c, .done ; 48, 50..89: ignore
+ cp 98
+ jr c, .fgb ; 90..97
+ cp 100
+ jr c, .done
+ cp 108
+ jr c, .bgb ; 100..107
+.done:
+ ret
+.fg:
+ sub 30
+ jr .setfg
+.fgb:
+ sub 90
+.setfg:
+ ld e, a
+ ld d, 0
+ ld hl, SgrColorMap
+ add hl, de
+ ld a, [hl] ; A = fg color 1..7
+ ld e, a
+ ld a, [wCurColor]
+ and $F0
+ or e
+ ld [wCurColor], a
+ ret
+.fgdef:
+ ld a, [wCurColor]
+ and $F0
+ or 7
+ ld [wCurColor], a
+ ret
+.bg:
+ sub 40
+ jr .setbg
+.bgb:
+ sub 100
+.setbg:
+ swap a ; bg color 0..7 -> high nibble
+ ld e, a
+ ld a, [wCurColor]
+ and $0F
+ or e
+ ld [wCurColor], a
+ ret
+.bgdef:
+ ld a, [wCurColor]
+ and $0F ; bg -> 0 (black/default)
+ ld [wCurColor], a
+ ret
+.reset:
+ ld a, 7 ; fg=7 (white), bg=0 (black)
+ ld [wCurColor], a
+ ret
+SgrColorMap:
+ ; blk red grn yel blu mag cyn wht (ANSI 30..37 -> our 1..7; black->white)
+ db 7, 1, 2, 3, 4, 5, 6, 7
+
; -----------------------------------------------------------------------------
; term_puts - HL = NUL-terminated string: print via term_putc.
; -----------------------------------------------------------------------------
diff --git a/tools/gencolor.py b/tools/gencolor.py
new file mode 100644
index 0000000..711fa78
--- /dev/null
+++ b/tools/gencolor.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# Generate src/color.asm: CGB palette data + the lookup tables the terminal
+# uses to render per-glyph FOREGROUND *and* BACKGROUND color.
+#
+# The 7 colors are the points of a FANO PLANE (Steiner triple system S(2,3,7)):
+# 7 palettes = 7 lines, and every PAIR of colors lies on exactly one line, so a
+# tile that needs at most two distinct (non-black) colors always has a palette
+# holding both. slot0 of every palette is black, so a black background costs no
+# color id; a colored background is drawn by giving the cell's empty pixels the
+# background color's slot instead of slot0.
+#
+# Because a tile packs two independent cells (each with fg+bg) it can need up to
+# 3 non-black colors. Every color pair is covered exactly; a 3-color tile is
+# covered only if that triple happens to be a line - otherwise BestPal picks the
+# line covering the most of them and the odd one out is approximated.
+#
+# Color index: 0=black(bg/none) 1=red 2=green 3=yellow 4=blue 5=magenta 6=cyan
+# 7=white. BG palette 1 is reserved for the on-screen keyboard.
+
+def gbc15(r, g, b):
+ return r | (g << 5) | (b << 10)
+
+COLOR_RGB = {
+ 1: (31, 0, 0), # red
+ 2: ( 0, 31, 0), # green
+ 3: (31, 31, 0), # yellow
+ 4: ( 0, 0, 31), # blue
+ 5: (31, 0, 31), # magenta
+ 6: ( 0, 31, 31), # cyan
+ 7: (31, 31, 31), # white
+}
+
+# Hardware BG palette -> its 3 colors in slot1,slot2,slot3 order (slot0=black).
+# The 7 triples form a Fano plane over colors 1..7. Palette 0 keeps white at
+# slot1 (the OSK and plain white text draw with color-id 1).
+PALETTES = {
+ 0: [7, 1, 6],
+ 2: [1, 2, 3],
+ 3: [1, 4, 5],
+ 4: [2, 4, 6],
+ 5: [2, 5, 7],
+ 6: [3, 4, 7],
+ 7: [3, 5, 6],
+}
+PAL_ORDER = [0, 2, 3, 4, 5, 6, 7]
+
+# --- verify Fano: every pair of 1..7 lies on exactly one line ------------------
+seen = {}
+for pal, line in PALETTES.items():
+ assert len(set(line)) == 3
+ for i in range(3):
+ for j in range(i + 1, 3):
+ k = frozenset((line[i], line[j]))
+ assert k not in seen, ("pair on two lines", k)
+ seen[k] = pal
+assert set(seen) == {frozenset((a, b)) for a in range(1, 8)
+ for b in range(1, 8) if a != b}
+
+def slot_of(pal, color):
+ """id (0..3) of color in palette pal. black->0; absent->1 (visible fallback)."""
+ if color == 0:
+ return 0
+ line = PALETTES.get(pal, [])
+ return (line.index(color) + 1) if color in line else 1
+
+def best_pal(mask):
+ """Palette covering the most colors present in the 7-bit set 'mask'."""
+ if mask == 0:
+ return 0
+ best, bestn = 0, -1
+ for pal in PAL_ORDER:
+ n = sum(1 for c in PALETTES[pal] if mask & (1 << (c - 1)))
+ if n > bestn:
+ best, bestn = pal, n
+ return best
+
+# --- emit ----------------------------------------------------------------------
+def pal_colors(hw):
+ if hw == 1: # OSK: white bg, black ink (inverse)
+ return [(31,31,31), (0,0,0), (0,0,0), (0,0,0)]
+ return [(0,0,0)] + [COLOR_RGB[c] for c in PALETTES[hw]]
+
+L = []
+L.append("; auto-generated by tools/gencolor.py - Fano-plane color tables")
+L.append("; (foreground + background). DO NOT EDIT.")
+L.append('SECTION "color", ROM0')
+L.append("")
+L.append("; 8 CGB BG palettes, 4 colors each (c0=black). Loaded at BCPS $80.")
+L.append("PalData::")
+for hw in range(8):
+ words = []
+ for (r, g, b) in pal_colors(hw):
+ v = gbc15(r, g, b)
+ words.append("$%02X,$%02X" % (v & 0xFF, (v >> 8) & 0xFF))
+ L.append(" db " + ",".join(words) + " ; palette %d" % hw)
+L.append("")
+L.append("; BitTab[color] = bit for that color in the 'colors present' set (black=0)")
+L.append("BitTab::")
+L.append(" db " + ",".join("$%02X" % (0 if c == 0 else (1 << (c - 1))) for c in range(8)))
+L.append("")
+L.append("; BestPal[set] = palette (0..7) covering the most colors in a 7-bit set")
+L.append("BestPal::")
+for base in range(0, 128, 16):
+ L.append(" db " + ",".join("$%02X" % best_pal(base + j) for j in range(16))
+ + " ; %d..%d" % (base, base + 15))
+L.append("")
+L.append("; SlotOf[palette*8 + color] = that color's slot/id (0..3) in the palette")
+L.append("SlotOf::")
+for pal in range(8):
+ L.append(" db " + ",".join("$%02X" % slot_of(pal, c) for c in range(8))
+ + " ; palette %d" % pal)
+open("/home/user/dev/gbos/src/color.asm", "w").write("\n".join(L) + "\n")
+print("wrote src/color.asm")
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