mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
562 lines
25 KiB
JavaScript
562 lines
25 KiB
JavaScript
// vtmgr — virtual console manager for TVDOS
|
||
//
|
||
// Spawns up to 6 independent shell sessions (virtual consoles), each in its
|
||
// own parallel GraalVM context with its own thread. Each pane runs a real
|
||
// `command -fancy` shell. The dispatcher (this file) owns the physical
|
||
// keyboard, polls Alt-N hotkeys at 30 Hz, blits the active pane's text
|
||
// plane to the GPU's text area, and routes typed characters into the
|
||
// active pane's input ring buffer.
|
||
//
|
||
// Hotkeys: Alt-1..Alt-6 switch to that VT (lazy-spawn on first use).
|
||
// Alt-0 cleanly tears down vtmgr.
|
||
// Builtins: `chvt N` from inside a pane writes to the switch register.
|
||
|
||
// ─── shared memory layout ───────────────────────────────────────────────────
|
||
// CTRL_AREA (64 bytes from base)
|
||
// +0 active_vt u8 (1..6)
|
||
// +1 switch_request u8 (0 = none, 1..6 = target; set by chvt, cleared by dispatcher)
|
||
// +2 debounce_held u8
|
||
// +3 vt_spawned_bits u8 (bit n-1 set if VT n is alive)
|
||
// +4..63 reserved
|
||
// VT block (× MAX_VT) starting at base + 64, each VT_BLOCK_SIZE bytes
|
||
// +0..7 reserved (cursor & color state lives inside text plane itself)
|
||
// +8 queue_head u8 (next-read index)
|
||
// +9 queue_tail u8 (next-write index)
|
||
// +10..11 reserved
|
||
// +12..267 queue_data (256-byte ring buffer; one slot lost to full/empty disambiguation)
|
||
// +268..271 reserved (alignment)
|
||
// +272..7953 text_plane (7682 bytes; mirrors GPU textArea layout exactly)
|
||
|
||
const MAX_VT = 6
|
||
const CTRL_AREA_SIZE = 64
|
||
const VT_BLOCK_SIZE = 8000
|
||
const TEXT_PLANE_OFFSET = 272
|
||
const TEXT_PLANE_SIZE = 7682
|
||
const QUEUE_DATA_OFFSET = 12
|
||
|
||
const CTRL_ACTIVE_VT = 0
|
||
const CTRL_SWITCH_REQUEST = 1
|
||
const CTRL_DEBOUNCE_HELD = 2
|
||
const CTRL_SPAWNED_BITS = 3
|
||
|
||
const GPU_TEXTAREA_OFFSET = 253950
|
||
const TEXT_COLS = 80
|
||
const TEXT_ROWS = 32
|
||
|
||
const TP_FORE_BASE = 2
|
||
const TP_BACK_BASE = 2 + 2560
|
||
const TP_TEXT_BASE = 2 + 2560 + 2560
|
||
|
||
const TOTAL_ALLOC_SIZE = CTRL_AREA_SIZE + MAX_VT * VT_BLOCK_SIZE
|
||
const BASE = sys.malloc(TOTAL_ALLOC_SIZE)
|
||
if (!BASE || BASE === 0) { printerrln("vtmgr: sys.malloc failed"); return 1 }
|
||
for (let i = 0; i < TOTAL_ALLOC_SIZE; i++) sys.poke(BASE + i, 0)
|
||
|
||
const CTRL = BASE
|
||
function vtBlockAddr(n) { return BASE + CTRL_AREA_SIZE + (n - 1) * VT_BLOCK_SIZE }
|
||
function vtTextPlaneAddr(n) { return vtBlockAddr(n) + TEXT_PLANE_OFFSET }
|
||
|
||
// ─── pane bootstrap ─────────────────────────────────────────────────────────
|
||
// Read TVDOS.SYS once at startup. Each pane's bootstrap embeds the source
|
||
// (via JSON.stringify-escaped string literal) and evaluates it together with
|
||
// the shell-start code as ONE direct-eval call. This matters because strict-
|
||
// mode direct eval is scope-isolated; if TVDOS.SYS and the shell launcher
|
||
// were two separate evals, the shell launcher wouldn't see `_TVDOS`,
|
||
// `files`, `execApp`, etc. defined by the first eval.
|
||
|
||
const TVDOS_SYS_SRC = files.open("A:/tvdos/TVDOS.SYS").sread()
|
||
|
||
// _BIOS is set by the real BIOS before TVDOS.SYS runs; TVDOS.SYS reads
|
||
// _BIOS.FIRST_BOOTABLE_PORT during init. Each pane is a fresh context with no
|
||
// BIOS, so capture the live value here (vtmgr runs in the main context where
|
||
// _BIOS is visible) and re-declare it in every pane bootstrap.
|
||
const BIOS_FIRST_BOOTABLE_PORT = JSON.stringify(_BIOS.FIRST_BOOTABLE_PORT)
|
||
|
||
// Environment no longer needs snapshotting/replaying: each pane re-evaluates
|
||
// TVDOS.SYS, whose boot block runs \commandrc in every context, so the pane
|
||
// gets the same PATH / KEYBOARD / etc. natively. The pane then runs
|
||
// \AUTOEXEC.BAT (the per-console launch script: IME + interactive shell).
|
||
|
||
function makePaneBootstrap(vtNum) {
|
||
const TP_BASE = vtTextPlaneAddr(vtNum)
|
||
const VT_BLK = vtBlockAddr(vtNum)
|
||
|
||
// Launcher code runs after TVDOS.SYS in the SAME eval scope, so `files`,
|
||
// `eval`, `_TVDOS` etc. resolve via lexical closure. TVDOS.SYS's boot
|
||
// block already ran \commandrc (env) and skipped its own AUTOEXEC because
|
||
// the pane sets _TVDOS_IS_VT_PANE; here we run \AUTOEXEC.BAT to launch the
|
||
// per-console shell.
|
||
const SHELL_START = ";\n"
|
||
+ "var _cmdfileSrc = files.open('A:/tvdos/bin/command.js').sread();\n"
|
||
+ "eval('var _VTSHELL=function(exec_args){' + _cmdfileSrc + '\\n};_VTSHELL')(['', '-c', '\\\\AUTOEXEC.BAT']);\n"
|
||
|
||
const combined = TVDOS_SYS_SRC + SHELL_START
|
||
|
||
const raw = `
|
||
globalThis.VT_NUM = ${vtNum}
|
||
globalThis.VT_TEXT_PLANE = ${TP_BASE}
|
||
globalThis.VT_BLOCK_ADDR = ${VT_BLK}
|
||
globalThis.VT_CTRL_ADDR = ${CTRL}
|
||
const TP = ${TP_BASE}
|
||
const VT_BLK = ${VT_BLK}
|
||
const CTRL = ${CTRL}
|
||
const QUEUE_DATA = VT_BLK + ${QUEUE_DATA_OFFSET}
|
||
const QUEUE_HEAD_ADDR = VT_BLK + 8
|
||
const QUEUE_TAIL_ADDR = VT_BLK + 9
|
||
const ACTIVE_VT_ADDR = CTRL + ${CTRL_ACTIVE_VT}
|
||
const COLS = ${TEXT_COLS}, ROWS = ${TEXT_ROWS}
|
||
const FORE_BASE = ${TP_FORE_BASE}, BACK_BASE = ${TP_BACK_BASE}, TEXT_BASE = ${TP_TEXT_BASE}
|
||
|
||
// ── output shims (write into the per-VT text-plane buffer in shared mem) ──
|
||
// This is a faithful JS port of the GPU's TTY interpreter (GlassTty.acceptChar
|
||
// + GraphicsAdapter handlers). TVDOS apps drive the screen by printing control
|
||
// bytes and escape sequences through print(), so the shim must interpret them
|
||
// exactly as the hardware would: the \\x84<decimal>u "emit char by code" escape
|
||
// (used by con.prnch), CSI cursor moves / erase / SGR colours, and the ?25
|
||
// cursor-visibility private sequence.
|
||
let curX = 0, curY = 0
|
||
let foreCol = 254
|
||
let backCol = 255
|
||
|
||
// Per-pane cursor visibility lives at VT_BLK+2 (1 = blink on, 0 = hidden).
|
||
// The compositor pushes the active pane's value into the GPU's blink bit.
|
||
const CURSOR_VIS_ADDR = VT_BLK + 2
|
||
sys.poke(CURSOR_VIS_ADDR, 1)
|
||
|
||
// SGR 30-37 / 40-47 → default 8-colour palette (matches GraphicsAdapter).
|
||
const SGR_PAL = [240, 211, 61, 230, 49, 219, 114, 254]
|
||
|
||
function writeCursor() {
|
||
let pos = curY * COLS + curX
|
||
sys.poke(TP + 0, pos & 0xFF)
|
||
sys.poke(TP + 1, (pos >> 8) & 0xFF)
|
||
}
|
||
function scrollBufUp(n) {
|
||
if (n < 1) n = 1
|
||
if (n > ROWS) n = ROWS
|
||
for (let p of [FORE_BASE, BACK_BASE, TEXT_BASE]) {
|
||
for (let y = 0; y < ROWS - n; y++) {
|
||
for (let x = 0; x < COLS; x++) {
|
||
sys.poke(TP + p + y * COLS + x, sys.peek(TP + p + (y + n) * COLS + x))
|
||
}
|
||
}
|
||
let clearVal = (p === TEXT_BASE) ? 0 : (p === FORE_BASE ? foreCol : backCol)
|
||
for (let y = ROWS - n; y < ROWS; y++)
|
||
for (let x = 0; x < COLS; x++) sys.poke(TP + p + y * COLS + x, clearVal)
|
||
}
|
||
}
|
||
function putCharRaw(x, y, c) {
|
||
if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return
|
||
let off = y * COLS + x
|
||
sys.poke(TP + TEXT_BASE + off, c & 0xFF)
|
||
sys.poke(TP + FORE_BASE + off, foreCol)
|
||
sys.poke(TP + BACK_BASE + off, backCol)
|
||
}
|
||
// Mirror of GraphicsAdapter.setCursorPos: wrap on overflow x, scroll on
|
||
// overflow y, clamp y above the screen.
|
||
function setCursorPos(x, y) {
|
||
let nx = x, ny = y
|
||
if (nx >= COLS) { nx = 0; ny += 1 }
|
||
else if (nx < 0) nx = 0
|
||
if (ny < 0) ny = 0
|
||
else if (ny >= ROWS) { scrollBufUp(ny - ROWS + 1); ny = ROWS - 1 }
|
||
curX = nx; curY = ny
|
||
writeCursor()
|
||
}
|
||
|
||
// ── TTY actions (mirror the GraphicsAdapter overrides) ────────────────────
|
||
function ttyPrintable(c) { putCharRaw(curX, curY, c); setCursorPos(curX + 1, curY) }
|
||
function ttyCrlf() {
|
||
let ny = curY + 1
|
||
setCursorPos(0, (ny >= ROWS) ? ROWS - 1 : ny)
|
||
if (ny >= ROWS) scrollBufUp(1)
|
||
}
|
||
function ttyBackspace() { let x = curX, y = curY; setCursorPos(x - 1, y); putCharRaw(curX, curY, 0x20) }
|
||
function ttyTab() { setCursorPos(((curX / 8 | 0) + 1) * 8, curY) }
|
||
function ttyResetStatus() { foreCol = 253; backCol = 255 }
|
||
function ttyEmitChar(code) { putCharRaw(curX, curY, code); setCursorPos(curX + 1, curY) }
|
||
function ttyCursorUp(n) { setCursorPos(curX, curY - n) }
|
||
function ttyCursorDown(n) { let ny = curY + n; setCursorPos(curX, (ny >= ROWS) ? ROWS - 1 : ny) }
|
||
function ttyCursorFwd(n) { setCursorPos(curX + n, curY) }
|
||
function ttyCursorBack(n) { setCursorPos(curX - n, curY) }
|
||
function ttyCursorNextLine(n) { let ny = curY + n; setCursorPos(0, (ny >= ROWS) ? ROWS - 1 : ny); if (ny >= ROWS) scrollBufUp(ny - ROWS + 1) }
|
||
function ttyCursorPrevLine(n) { setCursorPos(0, curY - n) }
|
||
function ttyCursorX(n) { setCursorPos(n, curY) }
|
||
function ttyCursorXY(row, col) { setCursorPos(col - 1, row - 1) }
|
||
function ttyEraseInDisp(arg) {
|
||
if (arg === 2) {
|
||
for (let i = 0; i < COLS * ROWS; i++) {
|
||
sys.poke(TP + TEXT_BASE + i, 0)
|
||
sys.poke(TP + FORE_BASE + i, foreCol)
|
||
sys.poke(TP + BACK_BASE + i, backCol)
|
||
}
|
||
curX = 0; curY = 0; writeCursor()
|
||
}
|
||
// other args: GraphicsAdapter TODOs (throws); we no-op for safety
|
||
}
|
||
function ttySgr1(arg) {
|
||
if (arg >= 30 && arg <= 37) foreCol = SGR_PAL[arg - 30]
|
||
else if (arg >= 40 && arg <= 47) backCol = SGR_PAL[arg - 40]
|
||
else if (arg === 7) { let t = foreCol; foreCol = backCol; backCol = t }
|
||
else if (arg === 0) { foreCol = 253; backCol = 255; sys.poke(CURSOR_VIS_ADDR, 1) }
|
||
}
|
||
function ttySgr3(a1, a2, a3) {
|
||
if (a1 === 38 && a2 === 5) foreCol = a3
|
||
else if (a1 === 48 && a2 === 5) backCol = a3
|
||
}
|
||
function ttyPrivH(arg) { if (arg === 25) sys.poke(CURSOR_VIS_ADDR, 1) }
|
||
function ttyPrivL(arg) { if (arg === 25) sys.poke(CURSOR_VIS_ADDR, 0) }
|
||
|
||
// ── escape-sequence state machine (mirror of GlassTty.acceptChar) ─────────
|
||
// States: 0 INITIAL, 1 ESC, 2 CSI, 3 NUM1, 4 SEP1, 5 NUM2, 6 SEP2, 7 NUM3,
|
||
// 8 PRIVATESEQ, 9 PRIVATENUM, 10 XCSI, 11 XNUM1
|
||
let escState = 0
|
||
let escArgs = []
|
||
function isDig(c) { return c >= 0x30 && c <= 0x39 }
|
||
function escReset() { escState = 0; escArgs.length = 0 }
|
||
// reject() in hardware returns the char as printable; replicate by printing it
|
||
function escRejectPrint(c) { escReset(); ttyPrintable(c) }
|
||
|
||
function processByte(c) {
|
||
switch (escState) {
|
||
case 0: // INITIAL
|
||
if (c === 0x1B) escState = 1
|
||
else if (c === 0x84) escState = 10
|
||
else if (c === 0x0A) ttyCrlf()
|
||
else if (c === 0x08) ttyBackspace()
|
||
else if (c === 0x09) ttyTab()
|
||
else if (c === 0x07) { /* bell */ }
|
||
else if (c >= 0x00 && c <= 0x1F) { /* other control: ignored */ }
|
||
else ttyPrintable(c)
|
||
break
|
||
case 1: // ESC
|
||
if (c === 0x63) { ttyResetStatus(); escReset() } // 'c'
|
||
else if (c === 0x5B) escState = 2 // '['
|
||
else escRejectPrint(c)
|
||
break
|
||
case 2: // CSI
|
||
if (c === 0x41) { ttyCursorUp(1); escReset() }
|
||
else if (c === 0x42) { ttyCursorDown(1); escReset() }
|
||
else if (c === 0x43) { ttyCursorFwd(1); escReset() }
|
||
else if (c === 0x44) { ttyCursorBack(1); escReset() }
|
||
else if (c === 0x45) { ttyCursorNextLine(1); escReset() }
|
||
else if (c === 0x46) { ttyCursorPrevLine(1); escReset() }
|
||
else if (c === 0x47) { ttyCursorX(1); escReset() }
|
||
else if (c === 0x4A) { ttyEraseInDisp(0); escReset() }
|
||
else if (c === 0x4B) { escReset() } // eraseInLine: no-op
|
||
else if (c === 0x53) { scrollBufUp(1); escReset() } // S
|
||
else if (c === 0x54) { escReset() } // T scrollDown: no-op
|
||
else if (c === 0x6D) { ttySgr1(0); escReset() } // m
|
||
else if (c === 0x3F) escState = 8 // '?'
|
||
else if (c === 0x3B) { escArgs.push(0); escState = 4 } // ';'
|
||
else if (isDig(c)) { escArgs.push(c - 0x30); escState = 3 }
|
||
else escRejectPrint(c)
|
||
break
|
||
case 3: // NUM1
|
||
if (c === 0x41) { ttyCursorUp(escArgs.pop()); escReset() }
|
||
else if (c === 0x42) { ttyCursorDown(escArgs.pop()); escReset() }
|
||
else if (c === 0x43) { ttyCursorFwd(escArgs.pop()); escReset() }
|
||
else if (c === 0x44) { ttyCursorBack(escArgs.pop()); escReset() }
|
||
else if (c === 0x45) { ttyCursorNextLine(escArgs.pop()); escReset() }
|
||
else if (c === 0x46) { ttyCursorPrevLine(escArgs.pop()); escReset() }
|
||
else if (c === 0x47) { ttyCursorX(escArgs.pop()); escReset() }
|
||
else if (c === 0x4A) { ttyEraseInDisp(escArgs.pop()); escReset() }
|
||
else if (c === 0x4B) { escArgs.pop(); escReset() }
|
||
else if (c === 0x53) { scrollBufUp(escArgs.pop()); escReset() }
|
||
else if (c === 0x54) { escArgs.pop(); escReset() }
|
||
else if (c === 0x6D) { ttySgr1(escArgs.pop()); escReset() }
|
||
else if (c === 0x3B) escState = 4
|
||
else if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||
else escRejectPrint(c)
|
||
break
|
||
case 4: // SEP1 (seen "n;")
|
||
if (isDig(c)) { escArgs.push(c - 0x30); escState = 5 }
|
||
else if (c === 0x48) { let a1 = escArgs.pop(); ttyCursorXY(a1, 0); escReset() } // H
|
||
else if (c === 0x6D) { ttySgr1(escArgs.pop()); escReset() } // m (2-arg unimpl in HW)
|
||
else if (c === 0x3B) { escArgs.push(0); escState = 6 }
|
||
else escRejectPrint(c)
|
||
break
|
||
case 5: // NUM2 (seen "n;n")
|
||
if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||
else if (c === 0x48) { let a2 = escArgs.pop(), a1 = escArgs.pop(); ttyCursorXY(a1, a2); escReset() }
|
||
else if (c === 0x6D) { escArgs.pop(); escArgs.pop(); escReset() } // 2-arg SGR unimpl in HW
|
||
else if (c === 0x3B) escState = 6
|
||
else escRejectPrint(c)
|
||
break
|
||
case 6: // SEP2 (seen "n;n;")
|
||
if (c === 0x6D) { let a2 = escArgs.pop(), a1 = escArgs.pop(); ttySgr3(a1, a2, 0); escReset() }
|
||
else if (isDig(c)) { escArgs.push(c - 0x30); escState = 7 }
|
||
else escRejectPrint(c)
|
||
break
|
||
case 7: // NUM3 (seen "n;n;n")
|
||
if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||
else if (c === 0x6D) { let a3 = escArgs.pop(), a2 = escArgs.pop(), a1 = escArgs.pop(); ttySgr3(a1, a2, a3); escReset() }
|
||
else escRejectPrint(c)
|
||
break
|
||
case 8: // PRIVATESEQ (seen "?")
|
||
if (isDig(c)) { escArgs.push(c - 0x30); escState = 9 }
|
||
else escRejectPrint(c)
|
||
break
|
||
case 9: // PRIVATENUM (seen "?n")
|
||
if (c === 0x68) { ttyPrivH(escArgs.pop()); escReset() } // h
|
||
else if (c === 0x6C) { ttyPrivL(escArgs.pop()); escReset() } // l
|
||
else if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||
else escRejectPrint(c)
|
||
break
|
||
case 10: // XCSI (seen \\x84)
|
||
if (c === 0x75) { ttyEmitChar(0); escReset() } // 'u'
|
||
else if (isDig(c)) { escArgs.push(c - 0x30); escState = 11 }
|
||
else escRejectPrint(c)
|
||
break
|
||
case 11: // XNUM1 (seen \\x84<digits>)
|
||
if (c === 0x75) { ttyEmitChar(escArgs.pop()); escReset() } // 'u'
|
||
else if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||
else escRejectPrint(c)
|
||
break
|
||
}
|
||
}
|
||
|
||
print = function(s) {
|
||
if (s === undefined || s === null) return
|
||
let str = '' + s
|
||
for (let i = 0; i < str.length; i++) processByte(str.charCodeAt(i))
|
||
}
|
||
println = function(s) {
|
||
if (s === undefined) print("\\n")
|
||
else print(s + "\\n")
|
||
}
|
||
printerr = function(s) { print(s) }
|
||
printerrln = function(s) { println(s) }
|
||
|
||
// command.js's shell.execute reassigns the global print/println/printerr/
|
||
// printerrln to shell.stdio.out.* (which call sys.print → physical GPU,
|
||
// bypassing these shims). Expose the buffer writers through a global hook so
|
||
// shell.stdio.out can delegate to them when running inside a VT pane. The
|
||
// non-VT path in command.js stays unchanged (hook is undefined there).
|
||
globalThis.__VT_OUT = { print: print, println: println, printerr: printerr, printerrln: printerrln }
|
||
|
||
// con.move / con.getyx are 1-based in TVDOS (graphics.setCursorYX does cx-1,
|
||
// getCursorYX returns cx+1). Internal curX/curY are 0-based, so convert.
|
||
con.move = function(y, x) {
|
||
curY = Math.max(0, Math.min(ROWS - 1, (y | 0) - 1))
|
||
curX = Math.max(0, Math.min(COLS - 1, (x | 0) - 1))
|
||
writeCursor()
|
||
}
|
||
con.getyx = function() { return [curY + 1, curX + 1] }
|
||
con.getmaxyx = function() { return [ROWS, COLS] }
|
||
con.color_pair = function(f, b) { foreCol = f & 0xFF; backCol = b & 0xFF }
|
||
con.color_fore = function(n) { foreCol = n & 0xFF }
|
||
con.color_back = function(n) { backCol = n & 0xFF }
|
||
con.get_color_fore = function() { return foreCol }
|
||
con.get_color_back = function() { return backCol }
|
||
// addch writes a glyph at the cursor WITHOUT advancing — matching
|
||
// graphics.putSymbol(). TVDOS code pairs addch with explicit curs_right();
|
||
// advancing here would double-step and leave gaps (e.g. the fancy prompt).
|
||
con.addch = function(c) { putCharRaw(curX, curY, c) }
|
||
con.mvaddch = function(y, x, c) { con.move(y, x); con.addch(c) }
|
||
con.curs_up = function(n) { n = n || 1; curY = Math.max(0, curY - n); writeCursor() }
|
||
con.curs_down = function(n) { n = n || 1; curY = Math.min(ROWS - 1, curY + n); writeCursor() }
|
||
con.curs_left = function(n) { n = n || 1; curX = Math.max(0, curX - n); writeCursor() }
|
||
con.curs_right = function(n) { n = n || 1; curX = Math.min(COLS - 1, curX + n); writeCursor() }
|
||
con.curs_set = function(arg) { sys.poke(CURSOR_VIS_ADDR, ((arg | 0) === 0) ? 0 : 1) }
|
||
con.video_reverse = function() { /* unsupported; ANSI swallowed */ }
|
||
con.reset_graphics = function() { foreCol = 254; backCol = 255 }
|
||
con.clear = function() {
|
||
for (let i = 0; i < COLS * ROWS; i++) {
|
||
sys.poke(TP + TEXT_BASE + i, 0)
|
||
sys.poke(TP + FORE_BASE + i, foreCol)
|
||
sys.poke(TP + BACK_BASE + i, backCol)
|
||
}
|
||
curX = 0; curY = 0; writeCursor()
|
||
}
|
||
// prnch prints a glyph and DOES advance (unlike addch) — the real impl emits
|
||
// it through print() as \\x84<code>u, so route it through the interpreter.
|
||
con.prnch = function(c) {
|
||
if (Array.isArray(c)) c.forEach(x => ttyEmitChar(x))
|
||
else ttyEmitChar(c)
|
||
}
|
||
|
||
// ── input shims ──────────────────────────────────────────────────────────
|
||
// Pane reads from its own ring buffer in shared mem. NEVER touches physical
|
||
// keyboard MMIO — that's the dispatcher's exclusive territory. Cooperative
|
||
// gate on active_vt keeps background panes parked when they call getch.
|
||
|
||
function queuePop() {
|
||
let head = sys.peek(QUEUE_HEAD_ADDR)
|
||
let tail = sys.peek(QUEUE_TAIL_ADDR)
|
||
if (head === tail) return -1
|
||
let b = sys.peek(QUEUE_DATA + head)
|
||
sys.poke(QUEUE_HEAD_ADDR, (head + 1) & 0xFF)
|
||
return b
|
||
}
|
||
con.getch = function() {
|
||
while (true) {
|
||
if (sys.peek(ACTIVE_VT_ADDR) === VT_NUM) {
|
||
let k = queuePop()
|
||
if (k >= 0) return k
|
||
}
|
||
sys.sleep(20)
|
||
}
|
||
}
|
||
con.hitterminate = function() { return false }
|
||
con.hiteof = function() { return false }
|
||
con.resetkeybuf = function() { sys.poke(QUEUE_HEAD_ADDR, sys.peek(QUEUE_TAIL_ADDR)) }
|
||
con.poll_keys = function() { return [0,0,0,0,0,0,0,0] }
|
||
|
||
// ── TVDOS.SYS init flags + BIOS stub ───────────────────────────────────────
|
||
globalThis._TVDOS_IS_VT_PANE = true
|
||
globalThis._BIOS = { FIRST_BOOTABLE_PORT: ${BIOS_FIRST_BOOTABLE_PORT} }
|
||
|
||
// ── load TVDOS.SYS and run AUTOEXEC.BAT (the per-console shell) in one direct-eval ─────
|
||
// Strict-mode direct eval is scope-isolated, so TVDOS.SYS's \`const _TVDOS\`
|
||
// only survives within the eval scope. The shell launcher must run inside
|
||
// the same eval to access it (via lexical closure into nested evals).
|
||
eval(${JSON.stringify(combined)})
|
||
`
|
||
// The outer execApp's injectIntChk rewrote the first while/for/do (each
|
||
// kind) in our literal source to call a per-exec SIGTERM check function.
|
||
// Some of those rewrites landed inside this template literal — the pane
|
||
// has no such symbol in scope. Strip them; panes don't need SIGTERM
|
||
// checks (parallel.kill handles teardown).
|
||
return raw.replace(/tvdosSIGTERM_[A-Za-z0-9_]+\(\);?/g, '')
|
||
}
|
||
|
||
// ─── pane lifecycle ─────────────────────────────────────────────────────────
|
||
// Lazy spawn: VT 1 at boot; VT 2-6 the first time the user requests them.
|
||
// Re-spawn if the previous pane's thread has died (e.g. user typed `exit`).
|
||
|
||
const panes = {} // n -> { runner, thread }
|
||
|
||
function isPaneAlive(n) {
|
||
return panes[n] && parallel.isRunning(panes[n].thread)
|
||
}
|
||
|
||
function spawnPane(n) {
|
||
serial.println("[vtmgr] spawning VT " + n)
|
||
let runner = parallel.spawnNewContext()
|
||
let thread = parallel.attachProgram("vt" + n, runner, makePaneBootstrap(n))
|
||
parallel.launch(thread)
|
||
panes[n] = { runner: runner, thread: thread }
|
||
sys.poke(CTRL + CTRL_SPAWNED_BITS, sys.peek(CTRL + CTRL_SPAWNED_BITS) | (1 << (n - 1)))
|
||
}
|
||
|
||
function ensurePane(n) {
|
||
if (!isPaneAlive(n)) {
|
||
sys.poke(CTRL + CTRL_SPAWNED_BITS, sys.peek(CTRL + CTRL_SPAWNED_BITS) & ~(1 << (n - 1)))
|
||
spawnPane(n)
|
||
}
|
||
}
|
||
|
||
ensurePane(1)
|
||
sys.poke(CTRL + CTRL_ACTIVE_VT, 1)
|
||
// VT 1's TVDOS.SYS eval is slow; give it room before we start compositing.
|
||
sys.sleep(800)
|
||
|
||
// ─── compositor / dispatcher loop ───────────────────────────────────────────
|
||
// 30 Hz: blit active pane → GPU text area; honour switch_request; detect
|
||
// Alt-N with debounce; drain typed chars into active pane's queue.
|
||
|
||
const gpuBase = graphics.getGpuMemBase()
|
||
const TEXTAREA_BASE_ABS = gpuBase - GPU_TEXTAREA_OFFSET
|
||
function blitVt(srcAddr) {
|
||
sys.memcpy(srcAddr, TEXTAREA_BASE_ABS, TEXT_PLANE_SIZE - 2)
|
||
sys.poke(TEXTAREA_BASE_ABS - (TEXT_PLANE_SIZE - 2), sys.peek(srcAddr + TEXT_PLANE_SIZE - 2))
|
||
sys.poke(TEXTAREA_BASE_ABS - (TEXT_PLANE_SIZE - 1), sys.peek(srcAddr + TEXT_PLANE_SIZE - 1))
|
||
}
|
||
|
||
// GPU textmode-attribute MMIO byte (offset 6): bit 0 = blinkCursor, bit 1 =
|
||
// rawMode, bits 4-7 = chrrom. We flip only bit 0 to match the active pane's
|
||
// cursor visibility. getGpuMemBase() = -1 - 1MB*slot; the peripheral's MMIO
|
||
// window sits at IOSpace offset 128KB*slot, so MMIO byte k = -1 - (128KB*slot + k).
|
||
const gpuSlot = (((-gpuBase) - 1) / 1048576) | 0
|
||
const GPU_MMIO_ATTR = -1 - (131072 * gpuSlot + 6)
|
||
let lastCursorVis = -1
|
||
function applyCursorVis(active) {
|
||
let vis = sys.peek(vtBlockAddr(active) + 2)
|
||
if (vis === lastCursorVis) return
|
||
let attr = sys.peek(GPU_MMIO_ATTR)
|
||
sys.poke(GPU_MMIO_ATTR, vis ? (attr | 1) : (attr & 0xFE))
|
||
lastCursorVis = vis
|
||
}
|
||
|
||
function queuePush(vtN, byte) {
|
||
let qBase = vtBlockAddr(vtN)
|
||
let head = sys.peek(qBase + 8)
|
||
let tail = sys.peek(qBase + 9)
|
||
let next = (tail + 1) & 0xFF
|
||
if (next === head) return false
|
||
sys.poke(qBase + QUEUE_DATA_OFFSET + tail, byte)
|
||
sys.poke(qBase + 9, next)
|
||
return true
|
||
}
|
||
|
||
function switchTo(n) {
|
||
if (n < 1 || n > MAX_VT) return
|
||
ensurePane(n)
|
||
sys.poke(CTRL + CTRL_ACTIVE_VT, n)
|
||
}
|
||
|
||
sys.poke(-39, 1) // enable physical keyboard input collection
|
||
|
||
let running = true
|
||
while (running) {
|
||
let active = sys.peek(CTRL + CTRL_ACTIVE_VT)
|
||
if (active < 1 || active > MAX_VT) active = 1
|
||
blitVt(vtTextPlaneAddr(active))
|
||
applyCursorVis(active)
|
||
|
||
// honour chvt's switch request
|
||
let req = sys.peek(CTRL + CTRL_SWITCH_REQUEST)
|
||
if (req >= 1 && req <= MAX_VT) {
|
||
if (req !== active) {
|
||
serial.println("[vtmgr] chvt switch -> VT " + req)
|
||
switchTo(req)
|
||
}
|
||
sys.poke(CTRL + CTRL_SWITCH_REQUEST, 0)
|
||
}
|
||
|
||
// Alt-N (and Alt-0 = exit) detection
|
||
sys.poke(-40, 1)
|
||
let keys = [sys.peek(-41), sys.peek(-42), sys.peek(-43), sys.peek(-44),
|
||
sys.peek(-45), sys.peek(-46), sys.peek(-47), sys.peek(-48)]
|
||
let altHeld = keys.indexOf(57) >= 0 || keys.indexOf(58) >= 0
|
||
let digit = -1
|
||
for (let n = 0; n <= MAX_VT; n++) {
|
||
if (keys.indexOf(7 + n) >= 0) { digit = n; break }
|
||
}
|
||
let debounce = sys.peek(CTRL + CTRL_DEBOUNCE_HELD) !== 0
|
||
|
||
if (debounce) {
|
||
if (!altHeld && digit < 0) sys.poke(CTRL + CTRL_DEBOUNCE_HELD, 0)
|
||
}
|
||
else if (altHeld && digit === 0) {
|
||
serial.println("[vtmgr] Alt-0 -> exit")
|
||
running = false
|
||
sys.poke(CTRL + CTRL_DEBOUNCE_HELD, 1)
|
||
sys.poke(-39, 1)
|
||
}
|
||
else if (altHeld && digit >= 1) {
|
||
serial.println("[vtmgr] Alt-" + digit + " -> switching to VT " + digit)
|
||
switchTo(digit)
|
||
sys.poke(CTRL + CTRL_DEBOUNCE_HELD, 1)
|
||
sys.poke(-39, 1) // swallow the digit char so it doesn't leak into the queue
|
||
}
|
||
|
||
if (!running) break
|
||
|
||
// drain typed chars into the active pane's queue
|
||
while (sys.peek(-50) !== 0) {
|
||
let k = sys.peek(-38)
|
||
if (k < 0) k += 256
|
||
queuePush(active, k)
|
||
}
|
||
|
||
sys.sleep(33)
|
||
}
|
||
|
||
for (let n = 1; n <= MAX_VT; n++) if (panes[n]) parallel.kill(panes[n].thread)
|
||
con.color_pair(254, 255)
|
||
con.clear()
|
||
println("vtmgr exited.")
|
||
return 0
|