FABRIC.md items 4.4v/4.4r/4.4ab: keyboard bridge, and simplify to a

full-screen vt100 terminal

4.4v -- keyboard-to-REPL bridge, real and tested:
Refactored KEY-EVENT's per-arch translation logic (keyboard_words.c) into
a shared C function, sk_key_event_poll(), so the REPL bridge reuses item
4.3.5f's already-converged Linux-keycode-namespace event stream instead
of building separate amd64/aarch64/riscv64 tables. repl.c's sk_kbd_getc()
decodes the standard US-QWERTY printable range plus Enter/Backspace/Shift
against that stream; sk_readline() polls it as a second source alongside
console_getc(). Verified via QEMU monitor sendkey injection, and by
Captain Bob typing directly into the live QEMU window over real emulated
PS/2 hardware mid-session (1 1 + . -> 2 ok, then a clean BYE shutdown).

4.4ab -- simplify to a full-screen terminal:
Captain Bob's call, reverting the 640x480 CANVAS box + independent REPL
strip (4.4o/4.4t/4.4x/4.4z) in favor of the simplest shape: the entire
framebuffer is one vt100 terminal, g_vt.cols/rows = fb_width()/fb_height()
divided by cell size, no origin offset, no box, no strip, no border
drawing. The REPL prompt is just the terminal's last scrolling line.
Scrollback, TTF rendering, and SGR color are all box-agnostic and keep
working unmodified.

4.4r -- reframed as a text/graphics mode toggle:
"Hide/show the scroll box" stopped meaning anything once the box was
removed; the underlying need survives as a whole-screen mode switch.
vt100_toggle_graphics() is a two-state machine (VISIBLE/HIDDEN) -- hidden
mode stops the terminal from touching the framebuffer while its logical
state keeps advancing, so direct framebuffer/TTF-TEXT drawing can use the
whole screen; showing again wipes and reuses scrollback_redraw() to
restore the terminal exactly. Reachable two ways, one transition function:
physically via Alt+TAB (4.4y revised from Ctrl+TAB) and programmatically
via the new ALT+TAB FORTH word.

Verified: three-arch clean QEMU boot + logs; amd64 screendump confirms
full-width text with no box/strip artifacts.

Punch list §25 items 4.4v/4.4r/4.4ab complete; 4.4y revised.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-12 15:41:26 -04:00
co-authored by Claude Sonnet 5
parent b21aa50a14
commit af20efaa15
19 changed files with 42854 additions and 274 deletions
+2 -2
View File
@@ -317,10 +317,10 @@ void console_fb_scroll_fwd(uint32_t n)
}
}
void console_fb_strip_draw(const char *text)
void console_fb_toggle_graphics(void)
{
if (fb_is_available()) {
vt100_strip_draw(text);
vt100_toggle_graphics();
}
}
+76 -176
View File
@@ -81,47 +81,28 @@ 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.4t: confine REPL text (TTF mode only) to the 640x480
* CANVAS box computed in 4.4o and pixel-verified in 4.4p. Boot/POST
* (bitmap mode) is untouched -- g_origin_x/g_origin_y default to (0,0)
* and only change once vt100_enable_ttf() runs.
*
* FABRIC.md item 4.4x: origin is now DERIVED from fb_width()/fb_height()
* at vt100_enable_ttf() time rather than hardcoded per-arch literals --
* both the old per-arch numbers (320/104 amd64, 80/4 aarch64+riscv64) and
* the new ones below check out against the same formula, so this is a
* strict generalization, not a behavior change for X. Y changes because
* 4.4u/4.4w replaced 4.4m's 96px/4-line REPL strip with a single-line
* strip pinned to the bottom of the screen (see VT100_STRIP_* below) --
* the box now sits directly above that strip instead of assuming 96px.
* origin_x = (fb_width() - VT100_BOX_W) / 2 -- horizontal center
* origin_y = fb_height() - VT100_BOX_H -- box bottom edge
* - VT100_STRIP_BOX_GAP_PX -- gap above strip
* - (2*VT100_STRIP_GAP_PX + 1) -- strip height
* --------------------------------------------------------------------- */
#define VT100_BOX_W 640u
#define VT100_BOX_H 480u
#define VT100_BOX_COLS 53u /* 640 / VT100_TTF_CELL_W_PX, exact */
#define VT100_BOX_ROWS 20u /* 480 / VT100_TTF_CELL_H_PX, exact */
/* 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.4u/4.4w/4.4x: the REPL prompt/input strip -- a
* single-line region pinned to the bottom of the screen, independent of
* the scrollback box's own grid (see vt100_strip_draw() below). Numbers
* pinned by Captain Bob directly, 2026-08-12 (4.4w).
* 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.
* --------------------------------------------------------------------- */
#define VT100_STRIP_GAP_PX 15u /* baseline-to-border-line gap, both sides, mirrored */
#define VT100_STRIP_BOX_GAP_PX 8u /* gap between the strip's top border and the box's bottom edge */
#define VT100_BORDER_GRAY FB_RGB(0xAA, 0xAA, 0xAA) /* FB_ANSI_PALETTE[7]; shared by the
strip's border lines (4.4x) and
the box's own border (4.4z) --
same pinned decision (4.4w) */
#define VT100_STRIP_TEXT_WHITE FB_RGB(0xFF, 0xFF, 0xFF)
#define VT100_STRIP_LEFT_MARGIN_PX(fbw) (((fbw) - VT100_BOX_W) / 2u) /* aligned under the box */
static uint32_t g_origin_x = 0; /* box top-left, TTF mode only; 0 in bitmap mode */
static uint32_t g_origin_y = 0;
/* -----------------------------------------------------------------------
* FABRIC.md item 4.4q: REPL scrollback. Text-only (not pixel snapshots --
@@ -239,11 +220,9 @@ static void apply_sgr(void);
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),
* offset by the box origin (4.4t) -- (0,0) in bitmap mode, so boot/POST
* is unaffected. */
static uint32_t px_of(uint32_t col) { return g_origin_x + col * cell_w(); }
static uint32_t py_of(uint32_t row) { return g_origin_y + row * 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; }
@@ -289,68 +268,6 @@ static void ttf_draw_glyph_cell(uint32_t cell_px, uint32_t cell_py, uint8_t ch,
}
}
/* FABRIC.md items 4.4u/4.4w/4.4x: draw the REPL prompt/input strip -- a
* single-line region pinned to the bottom of the screen, independent of
* the scrollback box's own grid and cursor state (this function never
* touches g_vt, and nothing in the box's own put_char()/scroll_up() path
* touches the strip). Reuses ttf_draw_glyph_cell() for the actual glyph
* blit -- same rasterizer, same cache, as the box's own grid -- rather
* than a second text-drawing path.
*
* Horizontal scroll (4.4u step 4): @p text is drawn tail-anchored once it
* exceeds the strip's width. That is equivalent to cursor-anchored
* scrolling because the caller (repl.c) only ever appends at, or
* backspaces from, the end of the line -- there is no mid-line cursor
* movement in this REPL, so "keep the cursor visible" and "keep the tail
* visible" are the same window.
*
* Border lines are drawn LAST, after the glyph loop, not first: a glyph
* cell's own opaque background fill (cell_h()=24px) can extend a few
* pixels past the strip's 15px baseline-to-line gap for full-ascender
* characters -- 4.4u's own record already flags these gap numbers as
* "informal... need to be re-checked once actually built." Drawing the
* borders last means an oversized cell can never erase them; it can only
* be visually clipped by them, which is the correct failure direction.
*
* No-op before TTF mode is active. */
void vt100_strip_draw(const char *text)
{
uint32_t fbw, fbh, bottom_line_y, top_line_y, baseline_y, cell_py;
uint32_t margin_x, avail_px, avail_cols, len, start, i;
if (!g_ttf_ready || g_glyph_mode != VT_GLYPH_TTF) return;
if (!text) text = "";
fbw = fb_width();
fbh = fb_height();
bottom_line_y = fbh - 1u;
top_line_y = bottom_line_y - (2u * VT100_STRIP_GAP_PX);
baseline_y = bottom_line_y - VT100_STRIP_GAP_PX;
cell_py = baseline_y - VT100_TTF_SIZE_PX;
margin_x = VT100_STRIP_LEFT_MARGIN_PX(fbw);
/* Clear the whole strip first -- both border lines get redrawn every
* call regardless, so clearing border-to-border is simpler than
* clearing just the interior and no less correct. */
fb_fill_rect(0, top_line_y, fbw, bottom_line_y - top_line_y + 1u, VT100_DEFAULT_BG);
avail_px = (fbw > 2u * margin_x) ? (fbw - 2u * margin_x) : 0u;
avail_cols = cell_w() ? (avail_px / cell_w()) : 0u;
len = (uint32_t)strlen(text);
start = (len > avail_cols) ? (len - avail_cols) : 0u;
for (i = start; i < len; i++) {
uint32_t col = i - start;
ttf_draw_glyph_cell(margin_x + col * cell_w(), cell_py,
(uint8_t)text[i],
VT100_STRIP_TEXT_WHITE, VT100_DEFAULT_BG);
}
fb_fill_rect(0, top_line_y, fbw, 1u, VT100_BORDER_GRAY);
fb_fill_rect(0, bottom_line_y, fbw, 1u, VT100_BORDER_GRAY);
}
static void draw_cursor_glyph(uint8_t ch)
{
uint32_t f = eff_fg();
@@ -373,6 +290,9 @@ static void draw_cursor_glyph(uint8_t ch)
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 {
@@ -469,28 +389,18 @@ void vt100_enable_ttf(void)
g_glyph_mode = VT_GLYPH_TTF;
/* FABRIC.md item 4.4t: switch to the box-confined coordinate system
* before anything below derives cols/rows/stride from it -- the
* scrollback allocation block just below depends on g_vt.cols via
* g_line_stride, so origin/cols/rows MUST be set first or every
* shadow/ring index silently corrupts instead of crashing. */
/* FABRIC.md item 4.4x: derived from the real framebuffer size, not a
* hardcoded per-arch literal -- see the formula in this file's header
* comment above VT100_BOX_W. */
g_origin_x = VT100_STRIP_LEFT_MARGIN_PX(fb_width());
g_origin_y = fb_height() - VT100_BOX_H - VT100_STRIP_BOX_GAP_PX
- (2u * VT100_STRIP_GAP_PX + 1u);
g_vt.cols = VT100_BOX_COLS;
g_vt.rows = VT100_BOX_ROWS;
/* 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, and
* from here on erase_display(2) is box-scoped (see its mode-aware
* branch below) -- without this, that debris would sit frozen outside
* the box forever instead of leaving clean CANVAS around it. This is
* the one and only whole-framebuffer wipe in TTF mode; every later
* ESC[2J stays box-scoped. */
* 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);
@@ -502,17 +412,6 @@ void vt100_enable_ttf(void)
{
uint32_t i;
g_line_stride = g_vt.cols + 1u;
/* 4.4t: cols is now the fixed box width (53) -- verify the stride
* came out right rather than assuming the ordering above landed
* correctly (advisor-flagged failure mode: silent corruption, not
* a crash, if this ever drifts). */
if (g_line_stride != VT100_BOX_COLS + 1u) {
log_message(LOG_ERROR, "vt100: unexpected line_stride=%u (want %u), scrollback unavailable",
g_line_stride, VT100_BOX_COLS + 1u);
g_shadow = (void *)0;
g_ring = (void *)0;
return;
}
g_shadow = (char *)kmalloc((size_t)g_vt.rows * g_line_stride);
g_ring = (char *)kmalloc((size_t)VT100_SCROLLBACK_MAX_LINES * g_line_stride);
if (!g_shadow || !g_ring) {
@@ -572,6 +471,40 @@ static void scrollback_redraw(void)
}
}
/* 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)
{
if (!g_shadow || !g_ring) return;
@@ -654,13 +587,9 @@ static void scroll_up(uint32_t lines)
}
}
/* 4.4t: box-scoped scroll in TTF mode (fb_scroll_rows() is a
* whole-framebuffer blit and would drag pixels from outside the box);
* bitmap mode (boot/POST) keeps the original full-framebuffer path. */
if (g_glyph_mode == VT_GLYPH_TTF) {
fb_scroll_rect(g_origin_x, g_origin_y, VT100_BOX_W, VT100_BOX_H,
lines * cell_h(), g_vt.def_bg);
} else {
/* 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);
}
}
@@ -679,29 +608,10 @@ static void scroll_up(uint32_t lines)
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);
}
/* FABRIC.md item 4.4z: stroke the scroll box's own visible border. 4.4t
* confined text to the box's coordinate space but never drew a rectangle
* outline on it -- the edges were implicit (where text stops), not
* drawn. Four thin edges, 1px each, same gray as the strip's border
* lines (4.4x/4.4w -- one shared color, one design decision). Called
* from erase_display()'s box-scoped branch below, so the border survives
* every box clear (the initial one at vt100_enable_ttf() time and any
* later ESC[2J), not just the first. */
static void draw_box_border(void)
{
fb_fill_rect(g_origin_x, g_origin_y,
VT100_BOX_W, 1u, VT100_BORDER_GRAY);
fb_fill_rect(g_origin_x, g_origin_y + VT100_BOX_H - 1u,
VT100_BOX_W, 1u, VT100_BORDER_GRAY);
fb_fill_rect(g_origin_x, g_origin_y,
1u, VT100_BOX_H, VT100_BORDER_GRAY);
fb_fill_rect(g_origin_x + VT100_BOX_W - 1u, g_origin_y,
1u, VT100_BOX_H, VT100_BORDER_GRAY);
}
static void erase_display(int mode)
{
if (mode == 0) {
@@ -719,13 +629,8 @@ static void erase_display(int mode)
}
erase_line_range(g_vt.cy, 0, g_vt.cx + 1);
} else {
/* erase entire screen -- 4.4t: box-scoped in TTF mode (a
* full-framebuffer wipe here would blank CANVAS outside the box),
* full-screen in bitmap mode as before (boot/POST unaffected). */
if (g_glyph_mode == VT_GLYPH_TTF) {
fb_fill_rect(g_origin_x, g_origin_y, VT100_BOX_W, VT100_BOX_H, g_vt.def_bg);
draw_box_border();
} 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;
@@ -972,13 +877,8 @@ void vt100_putc(char raw)
/* 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.
* 4.4t: box-scoped in TTF mode, full-width in bitmap mode. */
if (g_glyph_mode == VT_GLYPH_TTF) {
fb_fill_rect(g_origin_x, g_origin_y, VT100_BOX_W, cell_h(), g_vt.def_bg);
} else {
fb_fill_rect(0, 0, fb_width(), cell_h(), g_vt.def_bg);
}
* 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--;
}