// console_ui screen layer — full-screen panels for TUI navigation. // // The output half of FlashOS's shell-first navigation: the shell is the // primary interface and only specific tools take over the whole screen. A // full-screen tool — the coming /bin/mon hardware monitor, a pager, a // log viewer — takes over the console with enter(), paints panels // (panelTop/panelRow/panelBottom) and metric rows (kv), reads keys via // flibc.readKey, and restores the shell view with leave() when the user quits. // // Pure + freestanding, exactly like palette.flash / tags.flash: every renderer // takes a caller-supplied Sink and emits bytes — no allocator, no module-level // state, no dependency on the kernel or flibc. All control bytes are plain // ANSI/VT100 over the serial console; there is no framebuffer (the project goal // is a text workstation "ohne grafischen Overhead"). Box glyphs follow the // shared palette.unicode charset knob (ASCII by default — the UART console // passes raw bytes and only UTF-8 terminals render the Unicode forms). // // Zero footprint until referenced: like every console_ui decl, these are // analyzed only when a call site names them, so staging the file leaves the // kernel + fsh images byte-identical until the first consumer (/bin/mon) lands. use "palette" as palette /// Byte sink — structurally identical to console_ui.Sink (Zig fn-pointer types /// are structural), so a consumer threads one sink through the line renderers /// and these screen renderers alike. pub const Sink = *fn([]u8) void // ---- alternate-screen lifecycle -------------------------------------------- /// Enter full-screen: switch to the alternate screen buffer, hide the cursor, /// home it, and clear. Pairs with leave(); the \e[?1049h alt buffer leaves the /// shell's scrollback untouched so it reappears verbatim on leave(). pub fn enter(sink Sink) void { sink("\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J") } /// Leave full-screen: restore the cursor and the main screen buffer. A /// full-screen tool MUST call this on every exit path; fsh also resets the /// console after each wait() as a backstop. pub fn leave(sink Sink) void { sink("\x1b[?25h\x1b[?1049l") } /// Clear the screen and home the cursor without touching the buffer stack. pub fn clear(sink Sink) void { sink("\x1b[H\x1b[2J") } /// Move the cursor to (row, col), 1-based — \e[;H. pub fn moveTo(sink Sink, row u16, col u16) void { var buf [16]u8 = undefined var i usize = 0 buf[i] = 0x1b i += 1 buf[i] = '[' i += 1 i += writeDec(buf[i..], row) buf[i] = ';' i += 1 i += writeDec(buf[i..], col) buf[i] = 'H' i += 1 sink(buf[0..i]) } // ---- panels ---------------------------------------------------------------- // Box-drawing charset, chosen at comptime from palette.unicode. ASCII default // keeps a dumb-terminal capture legible; the Unicode forms render on UTF-8. const glyph = if (palette.unicode) struct { const tl = "\u{250c}" const tr = "\u{2510}" const bl = "\u{2514}" const br = "\u{2518}" const h = "\u{2500}" const v = "\u{2502}" } else struct { const tl = "+" const tr = "+" const bl = "+" const br = "+" const h = "-" const v = "|" } /// A bordered panel. `width` is the total column count including both borders; /// the inner content width is `width - 2`. The caller positions the cursor /// (moveTo) before each row for a full-screen layout, or just emits the rows /// inline. pub const Panel = struct { title []u8, width u16, fn inner(self Panel) usize { return if (self.width >= 2) self.width - 2 else 0 } } /// Top border carrying the title: `+- title -----------+` — rendered only when /// the inner width has room for "- "; otherwise a plain filled border. pub fn panelTop(sink Sink, p Panel) void { const inw = p.inner() sink(glyph.tl) var used usize = 0 if inw >= p.title.len + 3 { sink(glyph.h) sink(" ") sink(p.title) sink(" ") used = p.title.len + 3 } repeat(sink, glyph.h, inw - used) sink(glyph.tr) sink("\n") } /// A content row: `| text<pad> |`, text clipped / space-padded to the inner /// width minus the one-space gutter on each side. pub fn panelRow(sink Sink, p Panel, text []u8) void { const inw = p.inner() sink(glyph.v) if inw >= 2 { sink(" ") const room = inw - 2 const t = if (text.len <= room) text else text[0..room] sink(t) repeat(sink, " ", room - t.len) sink(" ") } else { repeat(sink, " ", inw) } sink(glyph.v) sink("\n") } /// Bottom border: `+-------------------+`. pub fn panelBottom(sink Sink, p Panel) void { sink(glyph.bl) repeat(sink, glyph.h, p.inner()) sink(glyph.br) sink("\n") } // ---- key/value rows -------------------------------------------------------- /// Column the value starts at in a kv() row. Eight fits "CPU"/"MEM"/"UP"/"USER" /// with a margin; a longer key gets a single trailing space instead. pub const kv_col usize = 8 /// A "key value" metric row + newline — the renderer sysinfo and /bin/mon /// use for each line. The key is padded to kv_col; an over-long key falls back /// to a single space so the value never collides. pub fn kv(sink Sink, key []u8, value []u8) void { sink(key) const pad = if (key.len < kv_col) kv_col - key.len else 1 repeat(sink, " ", pad) sink(value) sink("\n") } // ---- helpers --------------------------------------------------------------- /// Emit `s` `n` times. fn repeat(sink Sink, s []u8, n usize) void { var i usize = 0 while i < n { sink(s) i += 1 } } /// Write `v` as decimal ASCII into `out` (>= 5 bytes), returning the count. fn writeDec(out []mut u8, v u16) usize { if v == 0 { out[0] = '0' return 1 } var tmp [5]u8 = undefined var n usize = 0 var x = v while x != 0 { tmp[n] = '0' + #as(u8, #intCast(x % 10)) n += 1 x /= 10 } var i usize = 0 while i < n { out[i] = tmp[n - 1 - i] i += 1 } return n } // ---- host tests ------------------------------------------------------------ const std = #import("std") const testing = std.testing // Capturing sink for tests: appends to a fixed buffer. Host tests run // single-threaded, so a module-global is fine here. var cap_buf [512]u8 = undefined var cap_len usize = 0 fn capSink(bytes []u8) void { for b in bytes { if cap_len < cap_buf.len { cap_buf[cap_len] = b cap_len += 1 } } } fn capReset() void { cap_len = 0 } fn captured() []u8 { return cap_buf[0..cap_len] } test "kv pads a short key to kv_col" { capReset() kv(capSink, "CPU", "1.50 GHz") try testing.expectEqualStrings("CPU 1.50 GHz\n", captured()) } test "kv falls back to a single space for an over-long key" { capReset() kv(capSink, "LONGKEYNAME", "v") try testing.expectEqualStrings("LONGKEYNAME v\n", captured()) } test "moveTo emits a 1-based CUP sequence" { capReset() moveTo(capSink, 3, 12) try testing.expectEqualStrings("\x1b[3;12H", captured()) } test "panelBottom width math (ASCII default)" { capReset() panelBottom(capSink, .{ .title = "x", .width = 6 }) // width 6 => 4 inner '-' between the corners try testing.expectEqualStrings("+----+\n", captured()) } test "panelRow clips and pads to the inner width" { capReset() panelRow(capSink, .{ .title = "t", .width = 8 }, "ab") // inner 6, gutter 1 each side, room 4 => "ab" + 2 pad try testing.expectEqualStrings("| ab |\n", captured()) } test "writeDec handles zero and the u16 max" { var b [5]u8 = undefined try testing.expectEqual(#as(usize, 1), writeDec(&b, 0)) try testing.expectEqual(#as(u8, '0'), b[0]) const n = writeDec(&b, 65535) try testing.expectEqualStrings("65535", b[0..n]) }