diff options
Diffstat (limited to '')
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | include/gbos.inc | 23 | ||||
| -rw-r--r-- | src/color.asm | 40 | ||||
| -rw-r--r-- | src/osk.asm | 8 | ||||
| -rw-r--r-- | src/programs.asm | 7 | ||||
| -rw-r--r-- | src/term.asm | 523 | ||||
| -rw-r--r-- | tools/gencolor.py | 113 | ||||
| -rw-r--r-- | usr/ansi.c | 67 | ||||
| -rw-r--r-- | usr/irc.c | 215 |
9 files changed, 907 insertions, 91 deletions
@@ -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(); +} @@ -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 |
