vt100.c: font_8x16/bitmap-mode boot output previously had no scrollback at all -- g_shadow/g_ring were only allocated in vt100_enable_ttf(), so POST/self-test/heartbeat text was gone the instant it scrolled off, recoverable only from the serial log. Gives boot mode its own ring/ shadow pair (bitmap cell geometry, 4096-line capacity), frozen as a snapshot the moment vt100_enable_ttf() switches to the TTF-geometry pair, per the two-independent-rings design scoped with Captain Bob. scrollback_line_at()/scrollback_redraw()/vt100_scroll_back() now walk all four segments (boot ring, boot shadow, TTF ring, TTF shadow) as one continuous history, so PgUp from the REPL reaches back through POST. Three-arch QEMU boot + logs clean (amd64/aarch64/riscv64, no faults, no dictionary/parity regressions). Visual verification that PgUp actually recalls POST text still needs an interactive GTK screendump -- noted as open in FABRIC.md, same pattern as 4.4ab's screendump. Also includes BLOCK_MAP.md/artemis.img regenerated by these builds, and the acceptance-boot logs (plus stray logs from an earlier QEMU-instance collision during testing -- kept per repo convention, logs are audit artifacts, not deleted). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1085 lines
42 KiB
C
1085 lines
42 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
Licensed under the StarForth License, Version 1.0.
|
||
*/
|
||
|
||
/**
|
||
* vt100.c — Full-color ANSI / VT100 terminal state machine
|
||
*
|
||
* Parser states
|
||
* ─────────────
|
||
* NORMAL plain text characters
|
||
* ESC received 0x1B, waiting for next byte
|
||
* CSI received ESC [ , accumulating parameter digits / intermediates
|
||
* ESC_HASH received ESC # (line attributes — we ignore, eat next byte)
|
||
* ESC_CHAR received ESC ( or ESC ) (charset designation — ignored)
|
||
*
|
||
* Supported CSI sequences
|
||
* ───────────────────────
|
||
* CUU A cursor up
|
||
* CUD B cursor down
|
||
* CUF C cursor forward
|
||
* CUB D cursor back
|
||
* CUP H cursor position (row ; col)
|
||
* ED J erase display (0=below 1=above 2=all)
|
||
* EL K erase line (0=right 1=left 2=all)
|
||
* SGR m select graphic rendition (see header)
|
||
* SCP s save cursor position
|
||
* RCP u restore cursor position
|
||
*
|
||
* ESC sequences (2-char)
|
||
* ESC 7 save cursor (DEC)
|
||
* ESC 8 restore cursor (DEC)
|
||
* ESC M reverse index (scroll down)
|
||
* ESC c full reset
|
||
*/
|
||
|
||
#include "vt100.h"
|
||
#include "framebuffer.h"
|
||
#include "starkernel/ttf.h"
|
||
#include "starkernel/capsule_generated.h"
|
||
#include "starkernel/kmalloc.h"
|
||
#include "log.h"
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
|
||
/* Maximum CSI parameters */
|
||
#define VT100_MAX_PARAMS 16
|
||
|
||
/* Underline scanline (row 14 of 16) -- font_8x16 mode only; see
|
||
* draw_cursor_glyph()'s mode split. */
|
||
#define UNDERLINE_ROW 14u
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* FABRIC.md item 4.4j: TTF glyph backend, active only from
|
||
* vt100_enable_ttf() onward (boot/POST stays font_8x16.c).
|
||
*
|
||
* Point size and cell geometry, decided final by item 4.4m (20px text,
|
||
* 96px REPL strip = 4 lines). Chosen so 4.4i's confirmed hmtx result (all
|
||
* printable ASCII share one 600/1000-em advance width) divides evenly:
|
||
* 600/1000 * 20 = 12px exactly, no fractional-pixel cell width. Cell
|
||
* height is size + a fixed descender/leading allowance, not derived from
|
||
* hhea (not read by this parser -- see ttf.h).
|
||
* --------------------------------------------------------------------- */
|
||
#define VT100_TTF_SIZE_PX 20u
|
||
#define VT100_TTF_CELL_W_PX 12u /* 600/1000 * VT100_TTF_SIZE_PX */
|
||
#define VT100_TTF_CELL_H_PX 24u /* provisional: size + 4px leading/descender */
|
||
#define VT100_TTF_CACHE_SLOTS 96u /* covers all 95 printable ASCII, no thrash */
|
||
#define VT100_TTF_FONT_CAPSULE "fonts:JetBrainsMono-Regular.ttf"
|
||
|
||
typedef enum {
|
||
VT_GLYPH_BITMAP = 0, /* font_8x16.c, via fb_draw_glyph() */
|
||
VT_GLYPH_TTF = 1, /* TTF-TEXT's rasterizer, via ttf_draw_glyph_cell() */
|
||
} vt_glyph_mode_t;
|
||
|
||
static vt_glyph_mode_t g_glyph_mode = VT_GLYPH_BITMAP;
|
||
static ttf_font_t g_ttf_font;
|
||
static int g_ttf_ready = 0;
|
||
static ttf_raster_cache_t g_ttf_cache;
|
||
|
||
/* FABRIC.md item 4.4y-revised: Alt+TAB graphics/text toggle. When 0, every
|
||
* terminal draw call (put_char's glyph blit, erase, scroll) becomes a
|
||
* pure logical update -- g_vt's cursor/attributes and the scrollback
|
||
* shadow still advance normally, only the actual framebuffer write is
|
||
* skipped -- so direct framebuffer/TTF-TEXT drawing shows through
|
||
* undisturbed, and vt100_toggle_graphics() can restore the terminal
|
||
* exactly as it logically stands once toggled back. Always 1 in bitmap
|
||
* mode (boot/POST never hides). */
|
||
static int g_terminal_visible = 1;
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* FABRIC.md items 4.4t/4.4o/4.4u/4.4w/4.4x/4.4z tried a 640x480 CANVAS
|
||
* box with a separate single-line REPL strip pinned below it -- a
|
||
* confined region plus independent border-drawing/geometry bookkeeping
|
||
* for both. Reverted 2026-08-12, simplified back to what this comment
|
||
* block now describes: the entire framebuffer is one vt100 terminal,
|
||
* full width and full height, in both bitmap and TTF glyph modes. No
|
||
* origin offset, no box, no strip, no border drawing -- cols/rows are
|
||
* simply fb_width()/cell_w() and fb_height()/cell_h(). The REPL prompt is
|
||
* just the terminal's last line, exactly like any other scrolling
|
||
* terminal, not a separately-rendered region.
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* FABRIC.md item 4.4q: REPL scrollback. Text-only (not pixel snapshots --
|
||
* at 1000 lines x ~cols bytes this is tens of KB; a pixel-snapshot ring
|
||
* would be roughly three orders of magnitude larger for no benefit, since
|
||
* the only consumer is a redraw). Only active in TTF mode (allocated in
|
||
* vt100_enable_ttf(), never during boot/POST) -- font_8x16-mode boot
|
||
* output has no scrollback and none is scoped for it.
|
||
*
|
||
* Scope, decided with Captain Bob 2026-08-11: this is scrollback on
|
||
* today's full-screen terminal grid, NOT scoped to confine REPL text to
|
||
* the 4.4o 640x480 box -- that confinement (vt100's grid still spans the
|
||
* whole screen, not just the box) is a real, separate gap, observed by
|
||
* 4.4p and still open; deliberately not fixed here, per that explicit
|
||
* scoping decision rather than a silent deferral.
|
||
*
|
||
* Model: `g_shadow` mirrors exactly what's currently on screen (rows x
|
||
* cols characters, updated by every draw_cursor_glyph() call and shifted
|
||
* by every scroll_up()); `g_ring` holds every line scroll_up() has ever
|
||
* evicted from the top of `g_shadow`, oldest-first, circular once full.
|
||
* Conceptually one continuous history: ring (oldest..newest evicted) then
|
||
* shadow (oldest..newest on-screen) back-to-back; g_scroll_offset is how
|
||
* many lines back from the live (shadow) tail the current view sits.
|
||
* Scrolling is always "pick a window into that history and redraw it" --
|
||
* no separate pixel-scroll code path for scrollback, so recall is
|
||
* trivially consistent with what was actually printed instead of a second
|
||
* mechanism bolted onto fb_scroll_rows().
|
||
*
|
||
* Known limitations, not fixed here: redraw uses the terminal's current
|
||
* default fg/bg for every recalled line -- per-cell historical SGR color
|
||
* isn't stored, so scrollback recovers text exactly but not color.
|
||
* Drawing new live output while g_scroll_offset > 0 is unverified and not
|
||
* specially handled -- draw_cursor_glyph() always draws at the live
|
||
* cursor position regardless of scroll offset, so it would visually
|
||
* corrupt a scrolled-back view; out of scope for this item's verification
|
||
* path (sequential FORTH-word injection over serial, never concurrent
|
||
* with a live typist -- that's M8's keyboard-input problem, unstarted).
|
||
* --------------------------------------------------------------------- */
|
||
#define VT100_SCROLLBACK_MAX_LINES 1000u
|
||
|
||
static char *g_shadow = (void *)0; /* g_vt.rows * g_line_stride bytes */
|
||
static char *g_ring = (void *)0; /* g_ring_cap * g_line_stride bytes */
|
||
static uint32_t g_line_stride = 0; /* cols + 1 (NUL terminator) */
|
||
static uint32_t g_ring_count = 0; /* valid stored lines, <= g_ring_cap */
|
||
static uint32_t g_ring_head = 0; /* ring slot index of the oldest stored line */
|
||
static uint32_t g_ring_cap = 0; /* active ring capacity -- VT100_BOOT_SCROLLBACK_MAX_LINES
|
||
* pre-TTF, VT100_SCROLLBACK_MAX_LINES from vt100_enable_ttf()
|
||
* onward; see 4.4ac below */
|
||
static uint32_t g_scroll_offset = 0; /* 0 = live view */
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* FABRIC.md item 4.4ac: boot-mode scrollback. font_8x16/bitmap-mode boot
|
||
* output (POST, capsule birth, self-tests, heartbeat) previously had no
|
||
* scrollback at all -- g_shadow/g_ring above were only ever allocated in
|
||
* vt100_enable_ttf(), so anything printed before the REPL took over was
|
||
* gone the moment it scrolled off-screen. This block gives boot mode its
|
||
* own ring/shadow pair, allocated in vt100_init() at the bitmap cell
|
||
* geometry (8x16), separate from the TTF-geometry pair above because the
|
||
* two modes have different cell sizes and therefore different
|
||
* g_line_stride -- reflowing one buffer's stored lines into the other's
|
||
* width was rejected as unnecessary complexity for a text-only recall
|
||
* (decided with Captain Bob 2026-08-12).
|
||
*
|
||
* g_boot_shadow/g_boot_ring are frozen (snapshotted, not appended to)
|
||
* the moment vt100_enable_ttf() runs: it points g_shadow/g_ring at fresh
|
||
* TTF-geometry buffers, so this file's four scrollback segments become,
|
||
* oldest to newest: boot ring, boot shadow (the last screenful of boot
|
||
* output, frozen as of the transition), TTF ring, TTF shadow (live).
|
||
* scrollback_line_at()/scrollback_redraw() walk all four as one
|
||
* continuous history, per Captain Bob's call that PgUp from the REPL
|
||
* should reach all the way back through POST, not stop at a seam.
|
||
*
|
||
* Capacity guessed, not measured: 4096 lines vs. the TTF ring's 1000.
|
||
* A real qemu-esp boot transcript observed 2026-08-12 ran to ~8,500
|
||
* lines of POST/self-test/heartbeat output -- 4096 covers roughly half
|
||
* of that on its own; closing the rest of the gap is expected to come
|
||
* from trimming that verbosity (a separate, still-open item), not from
|
||
* growing this buffer indefinitely. Revisit if that trim doesn't happen. */
|
||
#define VT100_BOOT_SCROLLBACK_MAX_LINES 4096u
|
||
|
||
static char *g_boot_shadow = (void *)0; /* frozen boot g_shadow, snapshotted at vt100_enable_ttf() */
|
||
static char *g_boot_ring = (void *)0; /* frozen boot g_ring */
|
||
static uint32_t g_boot_line_stride = 0;
|
||
static uint32_t g_boot_ring_count = 0;
|
||
static uint32_t g_boot_ring_head = 0;
|
||
static uint32_t g_boot_ring_cap = 0;
|
||
static uint32_t g_boot_rows = 0; /* g_vt.rows at snapshot time; 0 if boot alloc failed */
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Parser state enum
|
||
* --------------------------------------------------------------------- */
|
||
|
||
typedef enum {
|
||
ST_NORMAL = 0,
|
||
ST_ESC,
|
||
ST_CSI,
|
||
ST_ESC_HASH,
|
||
ST_ESC_CHAR,
|
||
} ParseState;
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Terminal state
|
||
* --------------------------------------------------------------------- */
|
||
|
||
typedef struct {
|
||
/* Geometry */
|
||
uint32_t cols;
|
||
uint32_t rows;
|
||
|
||
/* Cursor */
|
||
uint32_t cx; /* column (0-based) */
|
||
uint32_t cy; /* row (0-based) */
|
||
uint32_t saved_cx;
|
||
uint32_t saved_cy;
|
||
|
||
/* Colors & attributes */
|
||
uint32_t fg; /* packed 0x00RRGGBB */
|
||
uint32_t bg;
|
||
uint32_t def_fg;
|
||
uint32_t def_bg;
|
||
int bold;
|
||
int underline;
|
||
int reverse;
|
||
|
||
/* Escape parser */
|
||
ParseState state;
|
||
int32_t params[VT100_MAX_PARAMS];
|
||
uint32_t param_count;
|
||
int param_has_value; /* 1 if current param digit has been seen */
|
||
|
||
int initialized;
|
||
} VT100State;
|
||
|
||
static VT100State g_vt;
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Forward declarations
|
||
* --------------------------------------------------------------------- */
|
||
static void put_char(uint8_t ch);
|
||
static void advance_cursor(void);
|
||
static void scroll_up(uint32_t lines);
|
||
static void erase_display(int mode);
|
||
static void erase_line(int mode);
|
||
static void set_cursor(uint32_t row, uint32_t col);
|
||
static void apply_sgr(void);
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Internal helpers
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/* Cell dimensions (pixels) — font_8x16.c's fixed 8x16 grid in bitmap mode,
|
||
* or the provisional TTF cell (4.4j) once vt100_enable_ttf() has switched
|
||
* modes. Mode-aware per 4.4i's finding that the grid was hardcoded to
|
||
* font_8x16.c specifically; this is the generalization that item called
|
||
* for. */
|
||
static uint32_t cell_w(void) { return g_glyph_mode == VT_GLYPH_TTF ? VT100_TTF_CELL_W_PX : fb_cell_w(); }
|
||
static uint32_t cell_h(void) { return g_glyph_mode == VT_GLYPH_TTF ? VT100_TTF_CELL_H_PX : fb_cell_h(); }
|
||
|
||
/* Pixel position of the top-left corner of character cell (cx, cy). */
|
||
static uint32_t px_of(uint32_t col) { return col * cell_w(); }
|
||
static uint32_t py_of(uint32_t row) { return row * cell_h(); }
|
||
|
||
/* Effective fg / bg accounting for reverse video */
|
||
static uint32_t eff_fg(void) { return g_vt.reverse ? g_vt.bg : g_vt.fg; }
|
||
static uint32_t eff_bg(void) { return g_vt.reverse ? g_vt.fg : g_vt.bg; }
|
||
|
||
/* Draw one glyph via the TTF rasterizer into the cell at pixel (cell_px,
|
||
* cell_py). Mirrors fb_draw_glyph()'s opaque-cell contract (every pixel in
|
||
* the cell gets either fg or bg, not just the glyph's "ink") by filling
|
||
* the whole cell with bg first, then stamping covered bitmap pixels with
|
||
* fg. Baseline sits VT100_TTF_SIZE_PX down from the cell's top, matching
|
||
* TTF-TEXT's own bitmap-origin convention (ttf.h's
|
||
* "glyph baseline sits at pixel (MARGIN, size_px + MARGIN)"). Bitmap
|
||
* pixels are clipped to the cell rect -- a glyph's rasterized bounds can
|
||
* slightly exceed the nominal advance width, and cells must stay opaque
|
||
* neighbors, not bleed into each other. */
|
||
static void ttf_draw_glyph_cell(uint32_t cell_px, uint32_t cell_py, uint8_t ch,
|
||
uint32_t fg, uint32_t bg)
|
||
{
|
||
ttf_bitmap_t bmp;
|
||
int rc, was_hit;
|
||
uint32_t bx, by;
|
||
uint32_t baseline_y = cell_py + VT100_TTF_SIZE_PX;
|
||
|
||
fb_fill_rect(cell_px, cell_py, cell_w(), cell_h(), bg);
|
||
|
||
if (!g_ttf_ready) return; /* shouldn't happen once VT_GLYPH_TTF is set, defensive only */
|
||
|
||
rc = ttf_raster_cache_get(&g_ttf_cache, &g_ttf_font, (uint32_t)ch,
|
||
VT100_TTF_SIZE_PX, &bmp, &was_hit);
|
||
if (rc != TTF_OK) return; /* missing/unrasterizable glyph -- leave cell blank (bg only) */
|
||
|
||
for (by = 0; by < bmp.height; by++) {
|
||
int32_t fb_y = (int32_t)baseline_y + (int32_t)by
|
||
- (int32_t)VT100_TTF_SIZE_PX - TTF_CACHE_MARGIN;
|
||
if (fb_y < (int32_t)cell_py || fb_y >= (int32_t)(cell_py + cell_h())) continue;
|
||
for (bx = 0; bx < bmp.width; bx++) {
|
||
int32_t fb_x = (int32_t)cell_px + (int32_t)bx - TTF_CACHE_MARGIN;
|
||
if (fb_x < (int32_t)cell_px || fb_x >= (int32_t)(cell_px + cell_w())) continue;
|
||
if (bmp.pixels[by * bmp.width + bx]) {
|
||
fb_put_pixel((uint32_t)fb_x, (uint32_t)fb_y, fg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static void draw_cursor_glyph(uint8_t ch)
|
||
{
|
||
uint32_t f = eff_fg();
|
||
uint32_t b = eff_bg();
|
||
|
||
/* FABRIC.md item 4.4q: mirror every drawn character into the
|
||
* scrollback shadow buffer, when active. */
|
||
if (g_shadow && g_vt.cy < g_vt.rows && g_vt.cx < g_vt.cols) {
|
||
g_shadow[(size_t)g_vt.cy * g_line_stride + g_vt.cx] = (char)ch;
|
||
}
|
||
|
||
if (g_vt.bold) {
|
||
/* Lighten fg for bold */
|
||
uint32_t r = FB_R(f);
|
||
uint32_t g = FB_G(f);
|
||
uint32_t bv= FB_B(f);
|
||
r = r > 0xAAu ? 0xFFu : (r + 0x55u > 0xFFu ? 0xFFu : r + 0x55u);
|
||
g = g > 0xAAu ? 0xFFu : (g + 0x55u > 0xFFu ? 0xFFu : g + 0x55u);
|
||
bv= bv> 0xAAu ? 0xFFu : (bv+ 0x55u > 0xFFu ? 0xFFu : bv+ 0x55u);
|
||
f = FB_RGB(r, g, bv);
|
||
}
|
||
|
||
if (!g_terminal_visible) return; /* 4.4y-revised: graphics mode -- logical state
|
||
* only (already updated above), no framebuffer write */
|
||
|
||
if (g_glyph_mode == VT_GLYPH_TTF) {
|
||
ttf_draw_glyph_cell(px_of(g_vt.cx), py_of(g_vt.cy), ch, f, b);
|
||
} else {
|
||
fb_draw_glyph(px_of(g_vt.cx), py_of(g_vt.cy), ch, f, b);
|
||
}
|
||
|
||
if (g_vt.underline) {
|
||
/* font_8x16 mode: underline sits at UNDERLINE_ROW (row 14 of 16)
|
||
* of that font's fixed grid, scaled. TTF mode: no equivalent fixed
|
||
* row exists (this parser doesn't read hhea underline metrics --
|
||
* see ttf.h) -- placed 2px below the baseline instead, a
|
||
* provisional choice like the rest of 4.4j's TTF cell geometry. */
|
||
uint32_t scale = g_glyph_mode == VT_GLYPH_TTF ? 1u : cell_h() / 16u;
|
||
uint32_t ul_y = g_glyph_mode == VT_GLYPH_TTF
|
||
? py_of(g_vt.cy) + VT100_TTF_SIZE_PX + 2u
|
||
: py_of(g_vt.cy) + UNDERLINE_ROW * scale;
|
||
fb_fill_rect(px_of(g_vt.cx), ul_y, cell_w(), scale, f);
|
||
}
|
||
}
|
||
|
||
/* Solid block cursor at the current position (g_vt.cx, g_vt.cy). Not
|
||
* blinking -- a static indicator, simplest thing that actually shows
|
||
* where typed input lands, callable as often as the caller likes since
|
||
* it is idempotent (always draws the same thing at the current position).
|
||
* A character typed at this position naturally overwrites it --
|
||
* draw_cursor_glyph() always fills the whole cell with bg before drawing
|
||
* a glyph -- but *leaving* this position without drawing a character
|
||
* (Enter/newline) does not, which is what vt100_erase_cursor() below is
|
||
* for. No-op outside TTF mode or graphics mode (4.4ab's
|
||
* g_terminal_visible), matching every other draw call in this file. */
|
||
void vt100_draw_cursor(void)
|
||
{
|
||
if (!g_terminal_visible || g_glyph_mode != VT_GLYPH_TTF) return;
|
||
fb_fill_rect(px_of(g_vt.cx), py_of(g_vt.cy), cell_w(), cell_h(), eff_fg());
|
||
}
|
||
|
||
/* Erases whatever vt100_draw_cursor() last drew at the current position,
|
||
* restoring it to plain background -- call this before moving away from
|
||
* a cursor-drawn cell without also drawing a character there (the
|
||
* newline case in sk_readline: Enter moves to a new line, and nothing
|
||
* else would ever overwrite the stray block left behind). */
|
||
void vt100_erase_cursor(void)
|
||
{
|
||
if (!g_terminal_visible || g_glyph_mode != VT_GLYPH_TTF) return;
|
||
fb_fill_rect(px_of(g_vt.cx), py_of(g_vt.cy), cell_w(), cell_h(), eff_bg());
|
||
}
|
||
|
||
static void reset_attrs(void)
|
||
{
|
||
g_vt.fg = g_vt.def_fg;
|
||
g_vt.bg = g_vt.def_bg;
|
||
g_vt.bold = 0;
|
||
g_vt.underline = 0;
|
||
g_vt.reverse = 0;
|
||
}
|
||
|
||
static void reset_parser(void)
|
||
{
|
||
uint32_t i;
|
||
g_vt.state = ST_NORMAL;
|
||
g_vt.param_count = 0;
|
||
g_vt.param_has_value = 0;
|
||
for (i = 0; i < VT100_MAX_PARAMS; i++) {
|
||
g_vt.params[i] = -1; /* -1 = not set (use default) */
|
||
}
|
||
}
|
||
|
||
/* Return parameter N, substituting `def` when not set */
|
||
static int32_t param(uint32_t n, int32_t def)
|
||
{
|
||
if (n >= g_vt.param_count) return def;
|
||
return (g_vt.params[n] < 0) ? def : g_vt.params[n];
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Public API
|
||
* --------------------------------------------------------------------- */
|
||
|
||
void vt100_init(void)
|
||
{
|
||
if (!fb_is_available()) return;
|
||
|
||
g_vt.cols = fb_width() / cell_w();
|
||
g_vt.rows = fb_height() / cell_h();
|
||
g_vt.def_fg = VT100_DEFAULT_FG;
|
||
g_vt.def_bg = VT100_DEFAULT_BG;
|
||
g_vt.cx = g_vt.cy = 0;
|
||
g_vt.saved_cx = g_vt.saved_cy = 0;
|
||
reset_attrs();
|
||
reset_parser();
|
||
g_vt.initialized = 1;
|
||
|
||
erase_display(2);
|
||
|
||
/* FABRIC.md item 4.4ac: allocate boot-mode scrollback now, at the
|
||
* bitmap cell geometry just computed above. Non-fatal on failure, same
|
||
* pattern as vt100_enable_ttf()'s TTF allocation below: g_shadow/g_ring
|
||
* stay NULL, draw_cursor_glyph()/scroll_up() already gate on that and
|
||
* skip scrollback tracking, so boot text just isn't recoverable --
|
||
* exactly the pre-4.4ac behavior, not a new failure mode. */
|
||
{
|
||
uint32_t i;
|
||
g_line_stride = g_vt.cols + 1u;
|
||
g_ring_cap = VT100_BOOT_SCROLLBACK_MAX_LINES;
|
||
g_shadow = (char *)kmalloc((size_t)g_vt.rows * g_line_stride);
|
||
g_ring = (char *)kmalloc((size_t)g_ring_cap * g_line_stride);
|
||
if (!g_shadow || !g_ring) {
|
||
log_message(LOG_ERROR, "vt100: boot scrollback buffer allocation failed, unavailable");
|
||
if (g_shadow) { kfree(g_shadow); g_shadow = (void *)0; }
|
||
if (g_ring) { kfree(g_ring); g_ring = (void *)0; }
|
||
} else {
|
||
for (i = 0; i < g_vt.rows; i++) {
|
||
uint32_t c;
|
||
char *row = g_shadow + (size_t)i * g_line_stride;
|
||
for (c = 0; c < g_vt.cols; c++) row[c] = ' ';
|
||
row[g_vt.cols] = '\0';
|
||
}
|
||
}
|
||
g_ring_count = 0;
|
||
g_ring_head = 0;
|
||
}
|
||
}
|
||
|
||
void vt100_enable_ttf(void)
|
||
{
|
||
if (g_glyph_mode == VT_GLYPH_TTF) return; /* already switched */
|
||
if (!g_vt.initialized) return;
|
||
|
||
if (!g_ttf_ready) {
|
||
ttf_raster_cache_slot_t *slots;
|
||
int rc;
|
||
|
||
rc = ttf_load_from_capsule(capsule_get_directory(), capsule_get_descriptors(),
|
||
capsule_get_names(), capsule_get_arena(),
|
||
VT100_TTF_FONT_CAPSULE, &g_ttf_font);
|
||
if (rc != TTF_OK) {
|
||
log_message(LOG_ERROR, "vt100: TTF font capsule load failed (rc=%d), staying on font_8x16", rc);
|
||
return;
|
||
}
|
||
|
||
slots = (ttf_raster_cache_slot_t *)kmalloc(sizeof(ttf_raster_cache_slot_t) * VT100_TTF_CACHE_SLOTS);
|
||
if (!slots) {
|
||
log_message(LOG_ERROR, "vt100: TTF cache allocation failed, staying on font_8x16");
|
||
return;
|
||
}
|
||
ttf_raster_cache_init(&g_ttf_cache, slots, VT100_TTF_CACHE_SLOTS);
|
||
g_ttf_ready = 1;
|
||
}
|
||
|
||
/* FABRIC.md item 4.4ac: freeze the boot-mode scrollback buffers (if
|
||
* any -- vt100_init() may have failed to allocate them) before they
|
||
* get overwritten below. g_vt.cols/rows/g_shadow/g_ring/g_line_stride/
|
||
* g_ring_count/g_ring_head/g_ring_cap still hold boot-mode values at
|
||
* this point, nothing below has touched them yet. g_boot_rows is only
|
||
* set non-zero when the snapshot is actually usable -- if boot alloc
|
||
* failed, g_shadow is already NULL and g_ring_count is already 0 (
|
||
* scroll_up() never touches the ring when g_shadow/g_ring are NULL),
|
||
* so leaving g_boot_rows at 0 keeps scrollback_line_at()'s combined
|
||
* index space consistent instead of pointing into a NULL buffer. */
|
||
g_boot_shadow = g_shadow;
|
||
g_boot_ring = g_ring;
|
||
g_boot_line_stride = g_line_stride;
|
||
g_boot_ring_count = g_ring_count;
|
||
g_boot_ring_head = g_ring_head;
|
||
g_boot_ring_cap = g_ring_cap;
|
||
g_boot_rows = g_shadow ? g_vt.rows : 0;
|
||
|
||
g_glyph_mode = VT_GLYPH_TTF;
|
||
|
||
/* Full-screen grid, TTF cell size -- see this file's header comment.
|
||
* cols/rows MUST be set before anything below derives stride from
|
||
* them via g_line_stride. */
|
||
g_vt.cols = fb_width() / cell_w();
|
||
g_vt.rows = fb_height() / cell_h();
|
||
g_vt.cx = g_vt.cy = 0;
|
||
|
||
/* One-time full-screen clear at the bitmap-to-TTF transition: the boot/
|
||
* POST scroll left the whole framebuffer covered in font_8x16 text.
|
||
* erase_display(2) below would cover this on its own now that it's
|
||
* always full-screen, but the explicit fill stays as the "clean slate
|
||
* before anything else runs" step it always was. */
|
||
fb_fill_rect(0, 0, fb_width(), fb_height(), g_vt.def_bg);
|
||
erase_display(2);
|
||
|
||
/* FABRIC.md item 4.4q: allocate the scrollback shadow + ring buffers
|
||
* now that cols/rows are fixed for the rest of this session. Failure
|
||
* is non-fatal -- scrollback just stays unavailable (g_shadow/g_ring
|
||
* stay NULL, every scrollback call site already checks), REPL text
|
||
* output itself is unaffected either way. */
|
||
{
|
||
uint32_t i;
|
||
g_line_stride = g_vt.cols + 1u;
|
||
g_ring_cap = VT100_SCROLLBACK_MAX_LINES;
|
||
g_shadow = (char *)kmalloc((size_t)g_vt.rows * g_line_stride);
|
||
g_ring = (char *)kmalloc((size_t)g_ring_cap * g_line_stride);
|
||
if (!g_shadow || !g_ring) {
|
||
log_message(LOG_ERROR, "vt100: scrollback buffer allocation failed, scrollback unavailable");
|
||
if (g_shadow) { kfree(g_shadow); g_shadow = (void *)0; }
|
||
if (g_ring) { kfree(g_ring); g_ring = (void *)0; }
|
||
} else {
|
||
for (i = 0; i < g_vt.rows; i++) {
|
||
uint32_t c;
|
||
char *row = g_shadow + (size_t)i * g_line_stride;
|
||
for (c = 0; c < g_vt.cols; c++) row[c] = ' ';
|
||
row[g_vt.cols] = '\0';
|
||
}
|
||
}
|
||
g_ring_count = 0;
|
||
g_ring_head = 0;
|
||
g_scroll_offset = 0;
|
||
}
|
||
}
|
||
|
||
/* Fetch the text of combined-history index `idx` (0 = oldest boot-ring
|
||
* entry, ... newest TTF-shadow row = live) -- four segments back to back:
|
||
* boot ring, boot shadow (frozen at the boot->TTF transition), TTF ring,
|
||
* TTF shadow (live). See 4.4ac's block above for why boot and TTF each
|
||
* keep their own ring/shadow pair (different cell geometry, different
|
||
* g_line_stride) instead of one reflowed buffer, and 4.4q's block for the
|
||
* ring-then-shadow model each pair follows individually. Only ever called
|
||
* once TTF mode is active (scrollback is wired to a keyboard command that
|
||
* doesn't exist before the REPL starts -- console.c's
|
||
* console_fb_scroll_back()/_fwd()), so g_ring/g_shadow here always mean
|
||
* the TTF pair, never the boot one. */
|
||
static const char *scrollback_line_at(uint32_t idx)
|
||
{
|
||
if (idx < g_boot_ring_count) {
|
||
uint32_t slot = (g_boot_ring_head + idx) % g_boot_ring_cap;
|
||
return g_boot_ring + (size_t)slot * g_boot_line_stride;
|
||
}
|
||
idx -= g_boot_ring_count;
|
||
|
||
if (idx < g_boot_rows) {
|
||
return g_boot_shadow + (size_t)idx * g_boot_line_stride;
|
||
}
|
||
idx -= g_boot_rows;
|
||
|
||
if (idx < g_ring_count) {
|
||
uint32_t slot = (g_ring_head + idx) % g_ring_cap;
|
||
return g_ring + (size_t)slot * g_line_stride;
|
||
} else {
|
||
uint32_t row = idx - g_ring_count;
|
||
if (row >= g_vt.rows) row = g_vt.rows - 1; /* defensive clamp */
|
||
return g_shadow + (size_t)row * g_line_stride;
|
||
}
|
||
}
|
||
|
||
/* Redraw every row from the combined history at the current
|
||
* g_scroll_offset. Uses the terminal's current default colors, not each
|
||
* line's original SGR state -- see this file's 4.4q block. `total` now
|
||
* spans all four 4.4ac segments, not just the TTF ring + shadow, so
|
||
* scrolling back from the REPL reaches all the way through POST. */
|
||
static void scrollback_redraw(void)
|
||
{
|
||
uint32_t total = g_boot_ring_count + g_boot_rows + g_ring_count + g_vt.rows;
|
||
uint32_t row;
|
||
|
||
for (row = 0; row < g_vt.rows; row++) {
|
||
uint32_t idx = total - g_vt.rows - g_scroll_offset + row;
|
||
const char *text = scrollback_line_at(idx);
|
||
uint32_t col;
|
||
|
||
for (col = 0; col < g_vt.cols; col++) {
|
||
uint8_t ch = (uint8_t)(text[col] ? text[col] : ' ');
|
||
if (g_glyph_mode == VT_GLYPH_TTF) {
|
||
ttf_draw_glyph_cell(px_of(col), py_of(row), ch, g_vt.def_fg, g_vt.def_bg);
|
||
} else {
|
||
fb_draw_glyph(px_of(col), py_of(row), ch, g_vt.def_fg, g_vt.def_bg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* FABRIC.md item 4.4y-revised: Alt+TAB graphics/text toggle, a two-state
|
||
* machine (VISIBLE <-> HIDDEN) with exactly one transition function --
|
||
* both the physical Alt+TAB interception (repl.c) and the ALT+TAB FORTH
|
||
* word (keyboard_words.c) call this same function, so there is exactly
|
||
* one place the state actually flips, not two call sites each doing it
|
||
* independently.
|
||
*
|
||
* VISIBLE -> HIDDEN: flip the flag only. draw_cursor_glyph()/
|
||
* erase_line_range()/erase_display()/scroll_up() all check
|
||
* g_terminal_visible and skip their framebuffer writes from this point
|
||
* on, while still advancing g_vt's cursor/attributes and the g_shadow/
|
||
* g_ring scrollback state normally -- graphics-mode drawing (direct
|
||
* framebuffer or TTF-TEXT calls) is then free to use the whole screen,
|
||
* and the terminal's logical state stays perfectly consistent with what
|
||
* it would have been had it stayed visible.
|
||
*
|
||
* HIDDEN -> VISIBLE: flip the flag, then wipe whatever graphics drew and
|
||
* reuse scrollback_redraw() to repaint the terminal's current on-screen
|
||
* content from that untouched logical state -- restored exactly as it
|
||
* logically stood, not merely as it last looked on screen. */
|
||
void vt100_toggle_graphics(void)
|
||
{
|
||
if (!g_ttf_ready || g_glyph_mode != VT_GLYPH_TTF) return;
|
||
|
||
if (g_terminal_visible) {
|
||
g_terminal_visible = 0;
|
||
return;
|
||
}
|
||
|
||
g_terminal_visible = 1;
|
||
fb_fill_rect(0, 0, fb_width(), fb_height(), g_vt.def_bg);
|
||
scrollback_redraw();
|
||
}
|
||
|
||
void vt100_scroll_back(uint32_t n)
|
||
{
|
||
uint32_t history; /* 4.4ac: boot ring + boot shadow + TTF ring, not just the TTF ring */
|
||
if (!g_shadow || !g_ring) return;
|
||
history = g_boot_ring_count + g_boot_rows + g_ring_count;
|
||
if (n > history - g_scroll_offset) n = history - g_scroll_offset;
|
||
if (n == 0) return;
|
||
g_scroll_offset += n;
|
||
scrollback_redraw();
|
||
}
|
||
|
||
void vt100_scroll_fwd(uint32_t n)
|
||
{
|
||
if (!g_shadow || !g_ring) return;
|
||
if (n > g_scroll_offset) n = g_scroll_offset;
|
||
if (n == 0) return;
|
||
g_scroll_offset -= n;
|
||
scrollback_redraw();
|
||
}
|
||
|
||
uint32_t vt100_cols(void) { return g_vt.cols; }
|
||
uint32_t vt100_rows(void) { return g_vt.rows; }
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Cursor movement
|
||
* --------------------------------------------------------------------- */
|
||
|
||
static void set_cursor(uint32_t row, uint32_t col)
|
||
{
|
||
if (row >= g_vt.rows) row = g_vt.rows - 1;
|
||
if (col >= g_vt.cols) col = g_vt.cols - 1;
|
||
g_vt.cy = row;
|
||
g_vt.cx = col;
|
||
}
|
||
|
||
static void advance_cursor(void)
|
||
{
|
||
g_vt.cx++;
|
||
if (g_vt.cx >= g_vt.cols) {
|
||
g_vt.cx = 0;
|
||
g_vt.cy++;
|
||
if (g_vt.cy >= g_vt.rows) {
|
||
scroll_up(1);
|
||
g_vt.cy = g_vt.rows - 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* FABRIC.md item 4.4q: push the rows about to be discarded into the
|
||
* scrollback ring before the pixel scroll happens, then shift the shadow
|
||
* buffer to match. `n` is clamped to g_vt.rows -- scrolling further than
|
||
* a full screen at once cannot happen through this code path (advance_cursor
|
||
* only ever calls scroll_up(1)), but the clamp keeps this correct even if
|
||
* a future caller passes more. */
|
||
static void scroll_up(uint32_t lines)
|
||
{
|
||
if (g_shadow && g_ring && lines > 0) {
|
||
uint32_t n = lines > g_vt.rows ? g_vt.rows : lines;
|
||
uint32_t i, r;
|
||
|
||
for (r = 0; r < n; r++) {
|
||
uint32_t slot = (g_ring_head + g_ring_count) % g_ring_cap;
|
||
char *dst = g_ring + (size_t)slot * g_line_stride;
|
||
char *src = g_shadow + (size_t)r * g_line_stride;
|
||
for (i = 0; i < g_line_stride; i++) dst[i] = src[i];
|
||
if (g_ring_count < g_ring_cap) {
|
||
g_ring_count++;
|
||
} else {
|
||
g_ring_head = (g_ring_head + 1) % g_ring_cap;
|
||
}
|
||
}
|
||
|
||
for (r = 0; r + n < g_vt.rows; r++) {
|
||
char *dst = g_shadow + (size_t)r * g_line_stride;
|
||
char *src = g_shadow + (size_t)(r + n) * g_line_stride;
|
||
for (i = 0; i < g_line_stride; i++) dst[i] = src[i];
|
||
}
|
||
for (r = g_vt.rows - n; r < g_vt.rows; r++) {
|
||
char *row = g_shadow + (size_t)r * g_line_stride;
|
||
for (i = 0; i < g_vt.cols; i++) row[i] = ' ';
|
||
row[g_vt.cols] = '\0';
|
||
}
|
||
}
|
||
|
||
/* Full-framebuffer scroll -- both glyph modes cover the whole screen
|
||
* now, so there is no box to scope this to. */
|
||
if (g_terminal_visible) {
|
||
fb_scroll_rows(lines, g_vt.def_bg);
|
||
}
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Erase operations
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/* FABRIC.md item 4.4i found this drawing a per-cell blank glyph
|
||
* (font_8x16.c-specific, and inconsistent with erase_display(2)'s
|
||
* full-screen case just below, which already used a plain rect fill).
|
||
* 4.4j fixes both problems at once: a blank cell is visually identical to
|
||
* a filled bg rect regardless of glyph backend, so this needs no font
|
||
* involvement at all -- one fb_fill_rect() for the whole range instead of
|
||
* a per-column glyph draw. */
|
||
static void erase_line_range(uint32_t row, uint32_t c0, uint32_t c1)
|
||
{
|
||
if (c1 <= c0) return;
|
||
if (!g_terminal_visible) return; /* 4.4y-revised: graphics mode */
|
||
fb_fill_rect(px_of(c0), py_of(row), (c1 - c0) * cell_w(), cell_h(), g_vt.bg);
|
||
}
|
||
|
||
static void erase_display(int mode)
|
||
{
|
||
if (mode == 0) {
|
||
/* erase from cursor to end */
|
||
erase_line_range(g_vt.cy, g_vt.cx, g_vt.cols);
|
||
uint32_t r;
|
||
for (r = g_vt.cy + 1; r < g_vt.rows; r++) {
|
||
erase_line_range(r, 0, g_vt.cols);
|
||
}
|
||
} else if (mode == 1) {
|
||
/* erase from start to cursor */
|
||
uint32_t r;
|
||
for (r = 0; r < g_vt.cy; r++) {
|
||
erase_line_range(r, 0, g_vt.cols);
|
||
}
|
||
erase_line_range(g_vt.cy, 0, g_vt.cx + 1);
|
||
} else {
|
||
/* erase entire screen -- full framebuffer, both glyph modes. */
|
||
if (g_terminal_visible) {
|
||
fb_fill_rect(0, 0, fb_width(), fb_height(), g_vt.def_bg);
|
||
}
|
||
g_vt.cx = g_vt.cy = 0;
|
||
}
|
||
}
|
||
|
||
static void erase_line(int mode)
|
||
{
|
||
if (mode == 0) {
|
||
erase_line_range(g_vt.cy, g_vt.cx, g_vt.cols);
|
||
} else if (mode == 1) {
|
||
erase_line_range(g_vt.cy, 0, g_vt.cx + 1);
|
||
} else {
|
||
erase_line_range(g_vt.cy, 0, g_vt.cols);
|
||
}
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* SGR (Select Graphic Rendition) handler
|
||
*
|
||
* The parameter list may contain sub-sequences like:
|
||
* 38 ; 2 ; r ; g ; b truecolor fg
|
||
* 38 ; 5 ; n 256-color fg
|
||
* 48 ; 2 ; r ; g ; b truecolor bg
|
||
* 48 ; 5 ; n 256-color bg
|
||
* These are walked in a sub-loop.
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/* 6×6×6 colour cube + grayscale ramp for 256-color mode */
|
||
static uint32_t ansi256_to_rgb(uint32_t idx)
|
||
{
|
||
if (idx < 16u) {
|
||
return FB_ANSI_PALETTE[idx];
|
||
}
|
||
if (idx >= 232u) {
|
||
uint32_t v = 8u + (idx - 232u) * 10u;
|
||
return FB_RGB(v, v, v);
|
||
}
|
||
/* 6×6×6 colour cube: indices 16–231 */
|
||
idx -= 16u;
|
||
uint32_t r6 = idx / 36u;
|
||
uint32_t g6 = (idx % 36u) / 6u;
|
||
uint32_t b6 = idx % 6u;
|
||
uint32_t r = r6 ? (55u + r6 * 40u) : 0u;
|
||
uint32_t g = g6 ? (55u + g6 * 40u) : 0u;
|
||
uint32_t b = b6 ? (55u + b6 * 40u) : 0u;
|
||
return FB_RGB(r, g, b);
|
||
}
|
||
|
||
static void apply_sgr(void)
|
||
{
|
||
uint32_t i = 0;
|
||
|
||
if (g_vt.param_count == 0) {
|
||
reset_attrs();
|
||
return;
|
||
}
|
||
|
||
while (i < g_vt.param_count) {
|
||
int32_t p = param(i, 0);
|
||
|
||
if (p == 0) {
|
||
reset_attrs();
|
||
} else if (p == 1) {
|
||
g_vt.bold = 1;
|
||
} else if (p == 4) {
|
||
g_vt.underline = 1;
|
||
} else if (p == 7) {
|
||
g_vt.reverse = 1;
|
||
} else if (p == 21 || p == 22) {
|
||
g_vt.bold = 0;
|
||
} else if (p == 24) {
|
||
g_vt.underline = 0;
|
||
} else if (p == 27) {
|
||
g_vt.reverse = 0;
|
||
} else if (p >= 30 && p <= 37) {
|
||
g_vt.fg = FB_ANSI_PALETTE[p - 30];
|
||
} else if (p == 38) {
|
||
int32_t mode = param(i + 1, -1);
|
||
if (mode == 2 && i + 4 < g_vt.param_count) {
|
||
uint32_t r = (uint32_t)param(i + 2, 0);
|
||
uint32_t g = (uint32_t)param(i + 3, 0);
|
||
uint32_t b = (uint32_t)param(i + 4, 0);
|
||
g_vt.fg = FB_RGB(r, g, b);
|
||
i += 4;
|
||
} else if (mode == 5 && i + 2 < g_vt.param_count) {
|
||
g_vt.fg = ansi256_to_rgb((uint32_t)param(i + 2, 0));
|
||
i += 2;
|
||
}
|
||
} else if (p == 39) {
|
||
g_vt.fg = g_vt.def_fg;
|
||
} else if (p >= 40 && p <= 47) {
|
||
g_vt.bg = FB_ANSI_PALETTE[p - 40];
|
||
} else if (p == 48) {
|
||
int32_t mode = param(i + 1, -1);
|
||
if (mode == 2 && i + 4 < g_vt.param_count) {
|
||
uint32_t r = (uint32_t)param(i + 2, 0);
|
||
uint32_t g = (uint32_t)param(i + 3, 0);
|
||
uint32_t b = (uint32_t)param(i + 4, 0);
|
||
g_vt.bg = FB_RGB(r, g, b);
|
||
i += 4;
|
||
} else if (mode == 5 && i + 2 < g_vt.param_count) {
|
||
g_vt.bg = ansi256_to_rgb((uint32_t)param(i + 2, 0));
|
||
i += 2;
|
||
}
|
||
} else if (p == 49) {
|
||
g_vt.bg = g_vt.def_bg;
|
||
} else if (p >= 90 && p <= 97) {
|
||
g_vt.fg = FB_ANSI_PALETTE[p - 90 + 8];
|
||
} else if (p >= 100 && p <= 107) {
|
||
g_vt.bg = FB_ANSI_PALETTE[p - 100 + 8];
|
||
}
|
||
|
||
i++;
|
||
}
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* CSI dispatch
|
||
* --------------------------------------------------------------------- */
|
||
|
||
static void dispatch_csi(char final)
|
||
{
|
||
int32_t p0 = param(0, -1);
|
||
int32_t p1 = param(1, -1);
|
||
int32_t n;
|
||
|
||
switch (final) {
|
||
case 'A': /* CUU cursor up */
|
||
n = (p0 < 1) ? 1 : p0;
|
||
g_vt.cy = (g_vt.cy >= (uint32_t)n) ? g_vt.cy - (uint32_t)n : 0;
|
||
break;
|
||
case 'B': /* CUD cursor down */
|
||
n = (p0 < 1) ? 1 : p0;
|
||
g_vt.cy += (uint32_t)n;
|
||
if (g_vt.cy >= g_vt.rows) g_vt.cy = g_vt.rows - 1;
|
||
break;
|
||
case 'C': /* CUF cursor forward */
|
||
n = (p0 < 1) ? 1 : p0;
|
||
g_vt.cx += (uint32_t)n;
|
||
if (g_vt.cx >= g_vt.cols) g_vt.cx = g_vt.cols - 1;
|
||
break;
|
||
case 'D': /* CUB cursor back */
|
||
n = (p0 < 1) ? 1 : p0;
|
||
g_vt.cx = (g_vt.cx >= (uint32_t)n) ? g_vt.cx - (uint32_t)n : 0;
|
||
break;
|
||
case 'H': /* CUP cursor position ESC[row;colH (1-based) */
|
||
case 'f':
|
||
{
|
||
uint32_t row = (p0 <= 0) ? 0 : (uint32_t)(p0 - 1);
|
||
uint32_t col = (p1 <= 0) ? 0 : (uint32_t)(p1 - 1);
|
||
set_cursor(row, col);
|
||
}
|
||
break;
|
||
case 'J': /* ED erase in display */
|
||
erase_display((p0 < 0) ? 0 : p0);
|
||
break;
|
||
case 'K': /* EL erase in line */
|
||
erase_line((p0 < 0) ? 0 : p0);
|
||
break;
|
||
case 'm': /* SGR */
|
||
apply_sgr();
|
||
break;
|
||
case 's': /* SCP save cursor */
|
||
g_vt.saved_cx = g_vt.cx;
|
||
g_vt.saved_cy = g_vt.cy;
|
||
break;
|
||
case 'u': /* RCP restore cursor */
|
||
set_cursor(g_vt.saved_cy, g_vt.saved_cx);
|
||
break;
|
||
/* unrecognised sequences are silently ignored */
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Printable character output
|
||
* --------------------------------------------------------------------- */
|
||
|
||
static void put_char(uint8_t ch)
|
||
{
|
||
draw_cursor_glyph(ch);
|
||
advance_cursor();
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* vt100_putc — the main entry point
|
||
* --------------------------------------------------------------------- */
|
||
|
||
void vt100_putc(char raw)
|
||
{
|
||
uint8_t c = (uint8_t)raw;
|
||
|
||
if (!g_vt.initialized) return;
|
||
|
||
switch (g_vt.state) {
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
case ST_NORMAL:
|
||
if (c == 0x1B) {
|
||
g_vt.state = ST_ESC;
|
||
} else if (c == '\r') {
|
||
g_vt.cx = 0;
|
||
} else if (c == '\n') {
|
||
g_vt.cy++;
|
||
if (g_vt.cy >= g_vt.rows) {
|
||
scroll_up(1);
|
||
g_vt.cy = g_vt.rows - 1;
|
||
}
|
||
} else if (c == '\t') {
|
||
/* advance to next 8-column tab stop */
|
||
g_vt.cx = (g_vt.cx + 8u) & ~7u;
|
||
if (g_vt.cx >= g_vt.cols) {
|
||
g_vt.cx = 0;
|
||
g_vt.cy++;
|
||
if (g_vt.cy >= g_vt.rows) {
|
||
scroll_up(1);
|
||
g_vt.cy = g_vt.rows - 1;
|
||
}
|
||
}
|
||
} else if (c == '\b') {
|
||
if (g_vt.cx > 0) g_vt.cx--;
|
||
} else if (c == 0x07) {
|
||
/* BEL — ignored */
|
||
} else if (c >= 0x20u) {
|
||
put_char(c);
|
||
}
|
||
break;
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
case ST_ESC:
|
||
if (c == '[') {
|
||
reset_parser();
|
||
g_vt.state = ST_CSI;
|
||
} else if (c == '7') {
|
||
g_vt.saved_cx = g_vt.cx;
|
||
g_vt.saved_cy = g_vt.cy;
|
||
g_vt.state = ST_NORMAL;
|
||
} else if (c == '8') {
|
||
set_cursor(g_vt.saved_cy, g_vt.saved_cx);
|
||
g_vt.state = ST_NORMAL;
|
||
} else if (c == 'M') {
|
||
/* RI reverse index: scroll down (insert line at top) */
|
||
if (g_vt.cy == 0) {
|
||
/* Scroll display down: not trivially done without a
|
||
* full back-buffer; fill top row with bg instead. */
|
||
fb_fill_rect(0, 0, fb_width(), cell_h(), g_vt.def_bg);
|
||
} else {
|
||
g_vt.cy--;
|
||
}
|
||
g_vt.state = ST_NORMAL;
|
||
} else if (c == 'c') {
|
||
/* RIS full reset */
|
||
reset_attrs();
|
||
g_vt.cx = g_vt.cy = 0;
|
||
erase_display(2);
|
||
g_vt.state = ST_NORMAL;
|
||
} else if (c == '#') {
|
||
g_vt.state = ST_ESC_HASH;
|
||
} else if (c == '(' || c == ')') {
|
||
g_vt.state = ST_ESC_CHAR;
|
||
} else {
|
||
/* Unknown 2-char ESC sequence — absorb and reset */
|
||
g_vt.state = ST_NORMAL;
|
||
}
|
||
break;
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
case ST_ESC_HASH:
|
||
case ST_ESC_CHAR:
|
||
/* Eat the designator byte and return to normal */
|
||
g_vt.state = ST_NORMAL;
|
||
break;
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
case ST_CSI:
|
||
if (c >= '0' && c <= '9') {
|
||
uint32_t i = g_vt.param_count;
|
||
if (i < VT100_MAX_PARAMS) {
|
||
if (g_vt.params[i] < 0) g_vt.params[i] = 0;
|
||
g_vt.params[i] = g_vt.params[i] * 10 + (int32_t)(c - '0');
|
||
g_vt.param_has_value = 1;
|
||
}
|
||
} else if (c == ';') {
|
||
if (g_vt.param_count < VT100_MAX_PARAMS - 1u) {
|
||
g_vt.param_count++;
|
||
}
|
||
g_vt.param_has_value = 0;
|
||
} else if (c == '?' || c == '>' || c == '<' || c == '!') {
|
||
/* Private / intermediate bytes — swallow, keep parsing */
|
||
} else if (c >= 0x40 && c <= 0x7E) {
|
||
/* Final byte — dispatch and return to normal */
|
||
if (g_vt.param_has_value || g_vt.param_count > 0) {
|
||
g_vt.param_count++;
|
||
}
|
||
dispatch_csi((char)c);
|
||
g_vt.state = ST_NORMAL;
|
||
} else {
|
||
/* Unexpected — abort */
|
||
g_vt.state = ST_NORMAL;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
void vt100_puts(const char *s)
|
||
{
|
||
if (!s) return;
|
||
while (*s) {
|
||
vt100_putc(*s++);
|
||
}
|
||
}
|