FABRIC.md item 4.4x: split the REPL prompt into its own bottom strip

Scope expanded from pure CANVAS-rectangle arithmetic (as originally
scoped) to also splitting the REPL prompt/input line out of the
scrollback box into an independent single-line strip, per Captain Bob's
explicit fold-in after the gap was reported (§25.0 rule 3) rather than
silently expanded.

vt100.c: VT100_BOX_ORIGIN_X/Y are no longer hardcoded per-arch literals --
both are now derived from fb_width()/fb_height() at vt100_enable_ttf()
time. New vt100_strip_draw() renders the bottom strip (gray border lines,
bright-white text) directly via the existing ttf_draw_glyph_cell()
rasterizer, independent of the box's own grid/cursor state. Border lines
are drawn after the glyph loop so an oversized cell can only be clipped
by them, never erase them.

console.c/console.h: console_fb_strip_draw() thin wrapper, matching the
existing console_fb_enable_ttf()/console_fb_scroll_*() pattern.

repl.c: builds a plain-text "[VMName] ok> <input>" mirror in
g_strip_prompt/strip_refresh(), refreshed on every keystroke (including
backspace) from sk_readline() -- already wired for item 4.4v, since
keyboard-typed characters will flow through the same console_getc() path
once that lands. Also widened sk_repl_step()/sk_repl_run()'s local input
buffer from a second, smaller 256-byte buffer to INPUT_BUFFER_SIZE
(1025), per 4.4w's decision.

Verified: three-arch clean QEMU boot + logs, amd64 screendump showing
the box and strip as two visually distinct regions with no visible
glyph/border clipping.

Punch list §25 item 4.4x complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-12 14:59:07 -04:00
co-authored by Claude Sonnet 5
parent 763c6f2cd5
commit 91742e02f4
21 changed files with 42690 additions and 23 deletions
+7
View File
@@ -317,6 +317,13 @@ void console_fb_scroll_fwd(uint32_t n)
}
}
void console_fb_strip_draw(const char *text)
{
if (fb_is_available()) {
vt100_strip_draw(text);
}
}
/**
* Read a single character from serial console (non-blocking)
* Returns -1 if no character available
+97 -13
View File
@@ -45,6 +45,7 @@
#include "starkernel/kmalloc.h"
#include "log.h"
#include <stdint.h>
#include <string.h>
/* Maximum CSI parameters */
#define VT100_MAX_PARAMS 16
@@ -82,23 +83,40 @@ 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. Per-arch raster
* top-left origin, from those items' measurements. Boot/POST (bitmap mode)
* is untouched -- g_origin_x/g_origin_y default to (0,0) and only change
* once vt100_enable_ttf() runs.
* 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
* --------------------------------------------------------------------- */
#if defined(__x86_64__) || defined(__i386__)
#define VT100_BOX_ORIGIN_X 320u
#define VT100_BOX_ORIGIN_Y 104u
#else
#define VT100_BOX_ORIGIN_X 80u
#define VT100_BOX_ORIGIN_Y 4u
#endif
#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 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).
* --------------------------------------------------------------------- */
#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_STRIP_BORDER_GRAY FB_RGB(0xAA, 0xAA, 0xAA) /* FB_ANSI_PALETTE[7] */
#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;
@@ -268,6 +286,68 @@ 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_STRIP_BORDER_GRAY);
fb_fill_rect(0, bottom_line_y, fbw, 1u, VT100_STRIP_BORDER_GRAY);
}
static void draw_cursor_glyph(uint8_t ch)
{
uint32_t f = eff_fg();
@@ -391,8 +471,12 @@ void vt100_enable_ttf(void)
* 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. */
g_origin_x = VT100_BOX_ORIGIN_X;
g_origin_y = VT100_BOX_ORIGIN_Y;
/* 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;
g_vt.cx = g_vt.cy = 0;
+78 -2
View File
@@ -35,6 +35,7 @@
#include "starkernel/timer.h"
#include "starkernel/arch.h"
#include <stdint.h>
#include <string.h>
/* FABRIC.md 4.4: "ok>" (including its trailing space) renders in bright
* cyan, 0x55FFFF -- reuses FB_ANSI_PALETTE[14]. Sent as a real SGR escape
@@ -44,6 +45,72 @@
const char lithos_version[64] = LITHOS_VERSION_STR;
/*===========================================================================
* FABRIC.md items 4.4u/4.4w/4.4x: the bottom REPL strip -- a plain-text
* (no ANSI/SGR) mirror of "[VMName] ok> <input so far>", refreshed into
* console_fb_strip_draw() on every keystroke. Built separately from
* SK_PROMPT_TEXT rather than derived from it, since the strip renders in
* one fixed color (4.4u) and has no SGR parser of its own -- the ANSI
* version keeps going to console_puts() exactly as before, unaffected,
* for serial and for the scrollback box's own history.
*===========================================================================*/
#define SK_STRIP_PROMPT_MAX 80u
static char g_strip_prompt[SK_STRIP_PROMPT_MAX] = "ok> ";
/* Builds "[VMName] ok> " or "[VMName] zuse)ok> " (no VM name -> just the
* suffix) into g_strip_prompt, bounded, no strcat -- mirrors emit_prefix()'s
* bracket format (console.c) so the strip and the box agree on what a
* prompt looks like, minus the ANSI color codes the strip doesn't use. */
static void set_strip_prompt(VM *vm)
{
const char *vn = console_get_vm_name();
char *p = g_strip_prompt;
size_t room = sizeof(g_strip_prompt) - 1u;
size_t n;
const char *suffix;
size_t used, left, sn;
if (vn && vn[0]) {
n = strlen(vn);
if (n > room) n = room;
*p++ = '[';
memcpy(p, vn, n);
p += n;
*p++ = ']';
*p++ = ' ';
}
suffix = (vm && vm->zuse_session) ? "zuse)ok> " : "ok> ";
used = (size_t)(p - g_strip_prompt);
left = (used < room) ? (room - used) : 0u;
sn = strlen(suffix);
if (sn > left) sn = left;
memcpy(p, suffix, sn);
p += sn;
*p = '\0';
}
/* Concatenates g_strip_prompt + input_so_far (bounded, no strcat) and
* hands the result to the strip renderer. Called once with an empty
* input_so_far right after set_strip_prompt(), then again on every
* keystroke from sk_readline() below. */
static void strip_refresh(const char *input_so_far)
{
char combined[INPUT_BUFFER_SIZE + SK_STRIP_PROMPT_MAX];
size_t plen = strlen(g_strip_prompt);
size_t ilen = strlen(input_so_far);
if (plen >= sizeof(combined)) plen = sizeof(combined) - 1u;
memcpy(combined, g_strip_prompt, plen);
if (plen + ilen >= sizeof(combined)) ilen = sizeof(combined) - 1u - plen;
memcpy(combined + plen, input_so_far, ilen);
combined[plen + ilen] = '\0';
console_fb_strip_draw(combined);
}
/*===========================================================================
* USE-word dispatch: which VM receives REPL input.
*
@@ -90,6 +157,9 @@ static int sk_readline(char *buf, int size)
{
int n = 0;
buf[0] = '\0';
strip_refresh(buf); /* FABRIC.md 4.4x: show the bare prompt before any input */
for (;;) {
int c = console_getc(); /* non-blocking poll */
@@ -128,10 +198,12 @@ static int sk_readline(char *buf, int size)
/* backspace: DEL (0x7F) or BS (0x08) */
if ((c == 0x7F || c == '\b') && n > 0) {
n--;
buf[n] = '\0';
/* VT100 erase: move back, overwrite with space, move back again */
console_putc('\b');
console_putc(' ');
console_putc('\b');
strip_refresh(buf);
continue;
}
@@ -139,7 +211,9 @@ static int sk_readline(char *buf, int size)
if (n >= size - 1) continue; /* buffer full — drop character */
buf[n++] = (char)c;
buf[n] = '\0';
console_putc((char)c); /* echo */
strip_refresh(buf);
}
buf[n] = '\0';
@@ -180,7 +254,7 @@ static void sk_fault_handler(VM *vm) {
int sk_repl_step(VM *vm)
{
char input[256];
char input[INPUT_BUFFER_SIZE]; /* FABRIC.md 4.4w: matches the strip's input width */
if (!vm || vm->halted) return 0;
@@ -194,6 +268,7 @@ int sk_repl_step(VM *vm)
int is_hera = (!vn || (vn[0]=='H' && vn[1]=='e' && vn[2]=='r' && vn[3]=='a' && vn[4]=='\0'));
vm->emergency_console = is_hera ? (vm->zuse_session ? 0 : 1) : 0;
console_puts(SK_PROMPT_TEXT);
set_strip_prompt(vm); /* FABRIC.md 4.4x: bottom strip mirrors the same prompt */
}
sk_readline(input, sizeof(input));
@@ -222,7 +297,7 @@ int sk_repl_step(VM *vm)
void sk_repl_run(VM *vm)
{
char input[256];
char input[INPUT_BUFFER_SIZE]; /* FABRIC.md 4.4w: matches the strip's input width */
VM *active;
vm->halted = 0;
@@ -241,6 +316,7 @@ void sk_repl_run(VM *vm)
int is_hera = (!vn || (vn[0]=='H' && vn[1]=='e' && vn[2]=='r' && vn[3]=='a' && vn[4]=='\0'));
active->emergency_console = is_hera ? (active->zuse_session ? 0 : 1) : 0;
console_puts(SK_PROMPT_TEXT);
set_strip_prompt(active); /* FABRIC.md 4.4x: bottom strip mirrors the same prompt */
}
sk_readline(input, sizeof(input));