; ============================================================================= ; term.asm - a 40x18 text terminal on the CGB LCD, with cursor and fast scroll. ; ; 4x8 font: each 8x8 background tile holds TWO 4-px characters (left+right), ; giving 40 columns. Tiles are LINE-BOUND: line-slot L owns the 20 VRAM tiles ; at tile-positions [L*20 .. L*20+19] (index = LOW(pos), VRAM bank = HIGH(pos); ; positions 256-359 live in bank 1 - hence CGB-only). assign[sr] maps each of ; the 18 screen rows to a line-slot, and the tilemap points each screen row at ; that slot's tiles. Scrolling just rotates assign[], rebuilds the one new ; bottom line, and rewrites the tilemap - O(1 line), not O(screen). ; ; The cursor is an underline OR'd into the current cell's tile (wCurMask). ; ============================================================================= INCLUDE "include/gbos.inc" DEF rSCY EQU $FF42 DEF rSCX EQU $FF43 DEF rVBK EQU $FF4F ; CGB VRAM bank select DEF rBCPS EQU $FF68 ; CGB BG palette index DEF rBCPD EQU $FF69 ; CGB BG palette data 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 DEF ACACHE EQU 40 ; attr cache in COL_BUF's spare stride bytes: the tile ; attribute (palette bits) for [slot][tcol] lives at ; COL_BUF + slot*64 + ACACHE + tcol (tcol 0..19). ; render_tile writes it; term_write_tilemap and ; update_cursor_attr read it - so the tilemap attr ; pass never re-runs the Fano color math (O(1) lookup ; per tile instead of color_setup per tile per scroll). ; terminal state (WRAMX gap $D580-$D5FF) DEF wVram EQU $D584 ; 2 DEF wCurRow EQU $D586 ; cursor screen row 0..17 DEF wCurCol EQU $D587 ; cursor screen col 0..39 DEF wCursorOn EQU $D588 ; draw the cursor? DEF wRT_sr EQU $D58A ; render_tile scratch DEF wRT_tcol EQU $D58B DEF wRT_cL EQU $D58C DEF wRT_cR EQU $D58D DEF wScratch EQU $D58E DEF wViewTop EQU $D58F ; view offset: screen row maps to logical row + this DEF wAssign EQU $D590 ; assign[18]: screen row -> line-slot ($D590-$D5A1) DEF CUR_L EQU $E0 ; cursor underline, left cell (3px) DEF CUR_R EQU $0E ; cursor underline, right cell SECTION "term", ROM0 ; ----------------------------------------------------------------------------- ; term_init - CGB palette, identity assign[], clear buffer, build, LCD on. ; ----------------------------------------------------------------------------- term_init:: xor a ld [rLCDC], a ; LCD off ld [rSCX], a ld [rSCY], a ; 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 ld hl, PalData ld b, 8 * 4 * 2 ; 8 palettes * 4 colors * 2 bytes .palcopy ld a, [hl+] ld [rBCPD], a dec b jr nz, .palcopy ; state: assign[sr]=sr, cursor at 0,0 (on) ld hl, wAssign ld b, TROWS xor a .mkid ld [hl+], a inc a dec b jr nz, .mkid xor a ld [wCurRow], a 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, $FF ld [wCSPL], a ; invalidate the color_setup memo (no packed ; color is $FF: bg/fg nibbles are 0..7) 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 ld de, TROWS * TSTRIDE .clr ld a, $20 ld [hl+], a dec de ld a, d 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_redraw ; build all tiles (also fills the attr cache) call term_write_tilemap ; ...which the tilemap attr pass reads ld a, %10010001 ; LCD on, BG on, tile data $8000, map $9800 ld [rLCDC], a ret ; ----------------------------------------------------------------------------- ; term_write_tilemap - point each screen row's tilemap entries at assign[]'s ; line-slot tiles. Pass 1 = indices (bank 0), pass 2 = attributes (bank 1). ; ----------------------------------------------------------------------------- term_write_tilemap:: call compute_offset xor a ld [rVBK], a ld c, 0 ; screen row .i_row call .slot20 ; DE = assign[c]*20 (pos start), HL = tilemap row ld b, TCOLS/2 .i_col ld a, e ld [hl+], a ; index = LOW(pos) inc de dec b jr nz, .i_col inc c ld a, c cp TROWS jr c, .i_row ld a, 1 ld [rVBK], a xor a ld [wRT_sr], a ; screen row (render_tile scratch, free here) .a_row ld a, [wRT_sr] ld c, a call .slot20 ; DE = pos start, HL = tilemap addr, wTMSlot set ; Split the row at the pos-256 VRAM-bank boundary: the first k tiles are ; bank 0 (attr = cache byte), the rest bank 1 (attr | $08). Each run is ; then a tight cache->tilemap copy: no per-tile addressing or bank test. ld a, d or a jr nz, .k0 ; pos >= 256: whole row is bank 1 xor a sub e ; 256 - LOW(pos) (mod 256; 0 means 256) jr z, .k20 cp TCOLS/2 jr c, .khave ; boundary lands inside this row .k20 ld a, TCOLS/2 jr .khave .k0 xor a .khave ld [wRT_tcol], a ; k = bank-0 tiles in this row ; DE = &cache[slot][0] (pos isn't needed beyond the split above) push hl 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 de, COL_BUF + ACACHE add hl, de ld d, h ld e, l pop hl ; run 1: k tiles in bank 0 ld a, [wRT_tcol] ld b, a or a jr z, .run2 .r1 ld a, [de] inc de ld [hl+], a dec b jr nz, .r1 .run2 ; run 2: the remaining tiles in bank 1 ld a, [wRT_tcol] ld b, a ld a, TCOLS/2 sub b jr z, .arnext ld b, a .r2 ld a, [de] inc de or $08 ld [hl+], a dec b jr nz, .r2 .arnext ld a, [wRT_sr] inc a ld [wRT_sr], a cp TROWS jr c, .a_row xor a ld [rVBK], a ret ; helper: C = screen row -> DE = assign[C]*20, HL = $9800 + C*32 .slot20 ld a, [wViewTop] add c ; logical row = screen row + view offset cp TROWS jr c, .lok ld a, TROWS-1 ; rows the OSK covers clamp to the cursor line .lok add LOW(wAssign) ld l, a ld a, HIGH(wAssign) 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 add hl, hl ; L*4 ld d, h ld e, l add hl, hl add hl, hl ; L*16 add hl, de ; L*20 ld d, h ld e, l ; DE = pos start ld a, c ld l, a ld h, 0 add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl ; row*32 ld a, h add $98 ld h, a ; HL = $9800 + row*32 ret ; ----------------------------------------------------------------------------- ; slot_addr - A = screen row -> HL = TERM_BUF + assign[row]*64 ; ----------------------------------------------------------------------------- slot_addr: add LOW(wAssign) ld l, a ld a, HIGH(wAssign) adc 0 ld h, a ld a, [hl] ; line-slot 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, TERM_BUF add hl, de ret ; ----------------------------------------------------------------------------- ; glyph_ptr - A = char -> HL = FontData + (clamp(A,32,127)-32)*8 ; ----------------------------------------------------------------------------- glyph_ptr: cp 32 jr nc, .ok ld a, 32 .ok cp 128 jr c, .ok2 ld a, 32 .ok2 sub 32 ld l, a ld h, 0 add hl, hl add hl, hl add hl, hl ; *8 ld de, FontData add hl, de 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: ; memo: same (colL,colR) pair as the previous call? wPalAttr and all four ; plane masks are still valid - skip the whole Fano walk. Runs of same- ; colored cells (= almost all text) hit this. ld a, [wCSPL] cp d jr nz, .miss ld a, [wCSPR] cp e ret z .miss ld a, d ld [wCSPL], a ld [wCSL], a ld a, e ld [wCSPR], a 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). ; ----------------------------------------------------------------------------- build_tile:: ld a, d ld [rVBK], a ; VRAM bank for this tile ld h, 0 ld l, e add hl, hl add hl, hl add hl, hl add hl, hl ; E*16 ld a, h add $80 ld h, a ; HL = $8000 + E*16 ld a, l ld [wVram], a ld a, h ld [wVram+1], a ; ---- fast path: two spaces = bg-only planes, 8 constant rows ---- ; (the case term_scroll's cleared line and every blank region hits) ld a, b cp $20 jr nz, .glyphs ld a, c cp $20 jr nz, .glyphs ld a, [wBGP0] ld d, a ; plane 0 = bg mask (g = 0) ld a, [wBGP1] ld e, a ; plane 1 = bg mask ld b, 7 .blank ld a, d ld [hl+], a ld a, e ld [hl+], a dec b jr nz, .blank ld a, [wCurMask] ; last row: cursor underline or d ld [hl+], a ld [hl], e ret .glyphs ; ---- phase 1: combine both glyphs into wRowBuf (8 nibble-pair rows). ; Sequential pointers live in HL/DE registers; the old per-row 16-bit ; pointer walk through WRAM was most of the blit cost. ld a, c call glyph_ptr ; (clobbers DE, so right glyph first...) push hl ld a, b call glyph_ptr ; HL = left glyph pop de ; DE = right glyph FOR I, 8 ld a, [hl+] swap a and $F0 ld c, a ; left -> high nibble ld a, [de] inc de and $0F or c ; g = left | right ld [wRowBuf + I], a ENDR ; ---- phase 2: compose the bitplanes, unrolled, plane-0 masks in B/C ---- ld a, [wFGP0] ld b, a ld a, [wBGP0] ld c, a ld a, [wVram] ld l, a ld a, [wVram+1] ld h, a ; HL = VRAM dest FOR I, 8 ld a, [wRowBuf + I] ld d, a ; D = g and b ; g & FGP0 ld e, a ld a, d cpl and c ; ~g & BGP0 or e ; plane 0 IF I == 7 ld e, a ld a, [wCurMask] ; cursor underline on the last row or e ENDC ld [hl+], a ld a, d cpl ld e, a ; ~g ld a, [wBGP1] and e ld e, a ld a, [wFGP1] and d or e ; plane 1 ld [hl+], a ENDR ret ; ----------------------------------------------------------------------------- ; render_tile - B = screen row, C = tile column: rebuild that one tile from the ; buffer, adding the cursor underline if the cursor is in it. ; ----------------------------------------------------------------------------- render_tile: ld a, b ld [wRT_sr], a ld a, c ld [wRT_tcol], a ; chars: &buffer[assign[sr]][tcol*2] ld a, b call slot_addr ; HL = &buffer[slot][0] ld a, [wRT_tcol] add a ; tcol*2 add l ld l, a ld a, h adc 0 ld h, a ld a, [hl+] ld [wRT_cL], a ld a, [hl] 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 push hl ; &colL = COL_BUF + slot*64 + tcol*2 call color_setup ; refresh the attr cache: cache[slot][tcol] = wPalAttr. ; addr = &colL - tcol + ACACHE (tcol*2 back off, tcol forward) pop hl ld a, [wRT_tcol] ld e, a ld a, ACACHE sub e ; ACACHE - tcol (always positive: 21..40) add l ld l, a ld a, h adc 0 ld h, a ld a, [wPalAttr] ld [hl], a ; cursor mask xor a ld [wCurMask], a ld a, [wCursorOn] or a jr z, .pos ld a, [wCurRow] ld b, a ld a, [wRT_sr] cp b jr nz, .pos ld a, [wRT_tcol] add a ; left cell col ld b, a ld a, [wCurCol] cp b jr nz, .cr ld a, CUR_L ld [wCurMask], a jr .pos .cr inc b ; right cell col cp b jr nz, .pos ld a, CUR_R ld [wCurMask], a .pos ; pos = assign[sr]*20 + tcol -> DE (D=bank, E=index) ld a, [wRT_sr] add LOW(wAssign) ld l, a ld a, HIGH(wAssign) adc 0 ld h, a ld a, [hl] ld l, a ld h, 0 add hl, hl add hl, hl ld d, h ld e, l ; L*4 add hl, hl add hl, hl add hl, de ; L*20 ld a, [wRT_tcol] add l ld l, a ld a, h adc 0 ld h, a ; HL = pos ld d, h ld e, l ; DE = pos ld a, [wRT_cL] ld b, a ld a, [wRT_cR] ld c, a call build_tile xor a ld [rVBK], a ret ; render_cursor_tile - rebuild the tile at the cursor position. render_cursor_tile: ld a, [wCurRow] ld b, a ld a, [wCurCol] srl a ; /2 = tile col ld c, a 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 ; attr = cache[slot][wCurCol/2] - the cursor tile was just rendered ; (render_cursor_tile falls through to here), so the cache is fresh. 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 de, COL_BUF + ACACHE add hl, de ld a, [wCurCol] srl a ; tile col add l ld l, a ld a, h adc 0 ld h, a ld a, [hl] ld [wPalAttr], a ; downstream bank-bit code reads this ; 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). ; ----------------------------------------------------------------------------- term_redraw:: ld b, 0 ; screen row .row ld c, 0 ; tile col .col push bc call render_tile pop bc inc c ld a, c cp TCOLS/2 jr c, .col inc b ld a, b cp TROWS jr c, .row ret ; ----------------------------------------------------------------------------- ; term_scroll - scroll up one line: rotate assign[], blank+build the new bottom ; line, rewrite the tilemap. ; ----------------------------------------------------------------------------- ; term_scroll - scroll all 18 logical rows up one; the new line becomes logical ; row 17. The view offset decides where that lands on screen, so toggling the ; OSK never destroys backlog. term_scroll:: ld a, [wAssign] ld [wScratch], a ; freed line-slot ld hl, wAssign+1 ld de, wAssign ld b, TROWS-1 .rot ld a, [hl+] ld [de], a inc de dec b jr nz, .rot ld a, [wScratch] ld [wAssign + TROWS-1], a ; clear buffer[freed] to spaces 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, TERM_BUF add hl, de ld b, TCOLS .clr ld a, $20 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 push bc ld b, TROWS-1 call render_tile pop bc inc c ld a, c cp TCOLS/2 jr c, .build call term_write_tilemap ret ; cursor_down - advance the cursor row, scrolling when it runs past the bottom. cursor_down: ld a, [wCurRow] inc a cp TROWS jr c, .ok call term_scroll ; rewrites the tilemap (offset recomputed) ld a, TROWS-1 ld [wCurRow], a ret .ok ld [wCurRow], a ; the cursor changed rows; if the OSK is up the view offset may need to move ld a, [wOskVisible] or a ret z jp term_write_tilemap ; compute_offset - set wViewTop so the cursor line is always on screen: 0 when ; the OSK is hidden; otherwise max(0, wCurRow - 14) so the cursor sits at the ; bottom visible row (14) once the terminal has filled past it, but stays put ; while there is little content (no shifting the cursor off the top). compute_offset:: ld a, [wOskVisible] or a jr nz, .osk xor a ld [wViewTop], a ret .osk ld a, [wCurRow] sub TROWS - OSK_DOCK - 1 ; wCurRow - 14 jr nc, .store xor a ; cursor above row 14 -> no shift .store ld [wViewTop], a ret ; ----------------------------------------------------------------------------- ; term_putc - A = character. Print at the cursor, advance, handle \n \r \b. ; ----------------------------------------------------------------------------- ; term_putc guarantees SVBK=1 so the buffer/state at $D5xx/$D6xx are the ones ; we built, no matter which process context KPutc calls us from. The stack is ; in non-banked WRAM ($Cxxx/$Axxx), so saving SVBK across the switch is safe. term_putc:: ld d, a ld a, [rSVBK] push af ld a, 1 ld [rSVBK], a ld a, d call term_putc_body pop af 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 jr z, .cret cp 8 jr z, .bs cp 32 ret c ; ignore other control chars ; printable: buffer[assign[cur]][cc] = char ld [wRT_cL], a ld a, [wCurRow] call slot_addr ld a, [wCurCol] add l ld l, a ld a, h adc 0 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 call render_cursor_tile ; advance ld a, [wCurCol] inc a cp TCOLS jr c, .adv xor a ld [wCurCol], a call cursor_down jr .show .adv ld [wCurCol], a .show ld a, 1 ld [wCursorOn], a call render_cursor_tile ret .newline xor a ld [wCursorOn], a call render_cursor_tile xor a ld [wCurCol], a call cursor_down ld a, 1 ld [wCursorOn], a call render_cursor_tile ret .cret xor a ld [wCursorOn], a call render_cursor_tile xor a ld [wCurCol], a ld a, 1 ld [wCursorOn], a call render_cursor_tile ret .bs ld a, [wCurCol] or a ret z xor a ld [wCursorOn], a call render_cursor_tile ; clear cursor at old ld a, [wCurCol] dec a ld [wCurCol], a ; blank the cell we backed into ld a, [wCurRow] call slot_addr ld a, [wCurCol] add l ld l, a ld a, h adc 0 ld h, a ld a, $20 ld [hl], a ld a, 1 ld [wCursorOn], a 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. ; ----------------------------------------------------------------------------- term_puts:: ld a, [hl+] or a ret z push hl call term_putc pop hl jr term_puts ; ----------------------------------------------------------------------------- ; term_demo - drive the terminal: print enough lines to scroll, end at a prompt. ; ----------------------------------------------------------------------------- term_demo:: ld hl, .msg call term_puts ret .msg: db "GBOS terminal: cursor + fast scroll", 10 db "01 the quick brown fox jumps", 10 db "02 over the lazy dog.", 10 db "03 line three of the scroll test", 10 db "04 line four", 10 db "05 line five", 10 db "06 line six", 10 db "07 line seven", 10 db "08 line eight", 10 db "09 line nine", 10 db "10 line ten", 10 db "11 line eleven", 10 db "12 line twelve", 10 db "13 line thirteen", 10 db "14 line fourteen", 10 db "15 line fifteen", 10 db "16 line sixteen", 10 db "17 line seventeen", 10 db "18 line eighteen", 10 db "19 line nineteen", 10 db "20 line twenty", 10 db "21 line twenty-one", 10 db "22 line twenty-two", 10 db "ready$ ", 0