/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com Project: zyginit Filename: ui.reef Authors: Chris Tusa License: Description: Unified visual rendering: palette, glyphs, sigil, rolling tape, divider gauge, spinner, final cards. ******************************************************************************/ module ui import core.str as str import sys.env as env import sys.process as process import time.time as time import version export fn MODE_PLAIN(): int fn MODE_RICH_ASCII(): int fn MODE_RICH_16(): int fn MODE_RICH_TRUE(): int proc ui_init(rich_mode: int) proc ui_detect_winsize() fn ui_mode(): int fn ui_runlevel(): string fn ui_demo(scenario: string): int fn detect_rich_mode(): int fn detect_rich_mode_for_main(): int fn paint(token: string, text: string): string fn glyph_ok(): string fn glyph_fail(): string fn glyph_pending(): string fn glyph_starting(): string fn sigil(): string proc ui_boot_start(num_svc: int, num_tiers: int, runlevel: string) proc ui_tier_start(tier: int) proc ui_tier_done(tier: int) proc ui_event_started(name: string, dur_ms: int) proc ui_event_failed(name: string, exit_code: int, dur_ms: int) proc ui_event_starting(name: string) proc ui_event_stopping(name: string) proc ui_event_skipped(name: string, reason: string) proc ui_tick() fn ui_spinner_frame(reverse: bool): string fn fmt_divider_gauge(done: int, total: int): string type BootStats proc ui_boot_complete(stats: BootStats) proc ui_shutdown_start(reason: string) proc ui_event_stopped(name: string, dur_ms: int) proc ui_shutdown_complete(reason: string, elapsed_ms: int) fn pad_right(s: string, n: int): string fn make_divider(): string proc ui_set_mode(m: int) fn zyginit_monotonic_ms(): int end export type BootStats = struct elapsed_ms: int online: int failed: int skipped: int slowest: [string] slowest_count: int end BootStats // Render modes. Decided once at startup by ui_init(). fn MODE_PLAIN(): int return 0 end MODE_PLAIN fn MODE_RICH_ASCII(): int return 1 end MODE_RICH_ASCII fn MODE_RICH_16(): int return 2 end MODE_RICH_16 fn MODE_RICH_TRUE(): int return 3 end MODE_RICH_TRUE mut g_mode: int = 0 mut g_failed: bool = false // set if write() to /dev/console errors // Declared here (before TAPE_HEIGHT) so the C prototype is emitted before // its first use; also appears grouped with the other tape externs below. extern "C" fn zyginit_tape_slots(): int // Dynamic screen size — populated by ui_detect_winsize(), called from // main.reef after setup_pid1_console() so /dev/console is on fd 1. // Safe defaults (80x25) used when TIOCGWINSZ fails (Linux dev builds, // pipes, integration tests with ZYGINIT_FORCE_80x25=1). mut g_screen_rows: int = 25 mut g_screen_cols: int = 80 mut g_tape_height: int = 12 mut g_line_width: int = 78 fn TAPE_HEIGHT(): int return g_tape_height end TAPE_HEIGHT fn LINE_WIDTH(): int return g_line_width end LINE_WIDTH mut g_phase: string = "" mut g_runlevel: string = "" mut g_num_svc: int = 0 mut g_num_tiers: int = 0 mut g_cur_tier: int = 0 mut g_done: int = 0 mut g_failed_count: int = 0 mut g_skipped: int = 0 mut g_failed_first: string = "" mut g_failed_first_exit: int = 0 mut g_boot_start_ms: int = 0 mut g_shutdown_reason: string = "" // Spinner state mut g_tick: int = 0 // Tape state counters. The backing arrays live in helpers.c (C static buffers) // to avoid Reef's module-level new[] limitation (Reef emits GCC statement-expr // initializers which are invalid as C static initializers). mut g_tape_head: int = 0 mut g_tape_count: int = 0 extern "C" fn zyginit_isatty(fd: int): int extern "C" fn zyginit_esc_str(): string extern "C" fn zyginit_monotonic_ms(): int extern "C" fn zyginit_tape_slots(): int extern "C" proc zyginit_tape_clear() extern "C" proc zyginit_tape_set(slot: int, elapsed: int, glyph: string, svc_name: string, note: string, active_flag: int) extern "C" fn zyginit_tape_get_elapsed(slot: int): int extern "C" fn zyginit_tape_get_glyph(slot: int): string extern "C" fn zyginit_tape_get_name(slot: int): string extern "C" fn zyginit_tape_get_note(slot: int): string extern "C" fn zyginit_tape_get_active(slot: int): int extern "C" proc zyginit_tape_set_active(slot: int, active_flag: int) extern "C" proc zyginit_tape_set_glyph(slot: int, glyph: string) extern "C" proc zyginit_tape_set_note(slot: int, note: string) extern "C" fn zyginit_get_winsize(fd: int, out_rows: [int], out_cols: [int]): int fn N_FRAMES(): int return 4 end N_FRAMES // Return the current spinner frame character. // Forward (starting): cycles / - \ | with increasing g_tick // Reverse (stopping): cycles | \ - / with increasing g_tick (unwinds) fn ui_spinner_frame(reverse: bool): string let i = g_tick % N_FRAMES() if reverse let r = (N_FRAMES() - 1) - i if r == 0 return "/" end if if r == 1 return "-" end if if r == 2 return "\\" end if return "|" end if if i == 0 return "/" end if if i == 1 return "-" end if if i == 2 return "\\" end if return "|" end ui_spinner_frame // Pure terminal capability detector (no PID check, no isatty check). // Inspects only env vars and TERM. Use this from --ui-demo and snapshot tests. // Production callers (main.reef boot path) should use // detect_rich_mode_for_main() — it adds the PID-1 and isatty guards. fn detect_rich_mode(): int if env.has_env("ZYGINIT_NO_UI") return MODE_PLAIN() end if if env.has_env("NO_COLOR") return MODE_PLAIN() end if let term = env.get_env_or("TERM", "") if term == "dumb" return MODE_PLAIN() end if if env.has_env("ZYGINIT_ASCII") return MODE_RICH_ASCII() end if if term == "sun" return MODE_RICH_16() end if if term == "vt100" return MODE_RICH_16() end if if term == "vt220" return MODE_RICH_16() end if if term == "sun-color" return MODE_RICH_TRUE() end if if str.index_of(term, "256color") >= 0 return MODE_RICH_TRUE() end if // Hammerhead UEFI framebuffer: kernel doesn't set TERM but tem // supports full ANSI/CSI/truecolor. Default to truecolor when // we have no TERM hint. if str.length(term) == 0 return MODE_RICH_TRUE() end if // Anything else: safe 16-color baseline. return MODE_RICH_16() end detect_rich_mode // PID-1-aware, tty-aware wrapper around detect_rich_mode. // Adds: non-PID-1 instances always emit plain; non-tty stdout always emits plain. // Call this from main.reef boot path; call detect_rich_mode() for tests/demo. fn detect_rich_mode_for_main(): int // Hard overrides if env.has_env("ZYGINIT_NO_UI") return MODE_PLAIN() end if if env.has_env("NO_COLOR") return MODE_PLAIN() end if // Production gates if process.getpid() != 1 return MODE_PLAIN() end if if zyginit_isatty(1) == 0 return MODE_PLAIN() end if // TERM-driven mode let term = env.get_env_or("TERM", "") if term == "dumb" return MODE_PLAIN() end if if env.has_env("ZYGINIT_ASCII") return MODE_RICH_ASCII() end if if term == "sun" return MODE_RICH_16() end if if term == "vt100" return MODE_RICH_16() end if if term == "vt220" return MODE_RICH_16() end if if term == "sun-color" return MODE_RICH_TRUE() end if if str.index_of(term, "256color") >= 0 return MODE_RICH_TRUE() end if // Hammerhead UEFI framebuffer: kernel doesn't set TERM but tem // supports full ANSI/CSI/truecolor. Default to truecolor when we // have a tty but no TERM hint. if str.length(term) == 0 return MODE_RICH_TRUE() end if // Anything else: safe 16-color baseline. return MODE_RICH_16() end detect_rich_mode_for_main // SGR escape sequences. ESC char via C helper (Reef lexer has no \x escapes). // esc_reset returns the SGR reset sequence "\e[0m". fn esc_reset(): string return str.concat(zyginit_esc_str(), "[0m") end esc_reset // 16-color foreground codes. fn sgr_steel(): string return str.concat(zyginit_esc_str(), "[37m") end sgr_steel fn sgr_teal(): string return str.concat(zyginit_esc_str(), "[36m") end sgr_teal fn sgr_ok(): string return str.concat(zyginit_esc_str(), "[32m") end sgr_ok fn sgr_warn(): string return str.concat(zyginit_esc_str(), "[33m") end sgr_warn fn sgr_fail(): string return str.concat(zyginit_esc_str(), "[31m") end sgr_fail fn sgr_mute(): string return str.concat(zyginit_esc_str(), "[2m") end sgr_mute // Truecolor foreground codes. fn sgr_steel_true(): string return str.concat(zyginit_esc_str(), "[38;2;110;123;139m") end sgr_steel_true fn sgr_teal_true(): string return str.concat(zyginit_esc_str(), "[38;2;0;106;111m") end sgr_teal_true fn sgr_ok_true(): string return str.concat(zyginit_esc_str(), "[38;2;46;139;87m") end sgr_ok_true fn sgr_warn_true(): string return str.concat(zyginit_esc_str(), "[38;2;255;191;0m") end sgr_warn_true fn sgr_fail_true(): string return str.concat(zyginit_esc_str(), "[38;2;200;32;31m") end sgr_fail_true // Wrap text in the given palette token, honoring g_mode. fn paint(token: string, text: string): string if g_mode == MODE_PLAIN() return text end if mut on = "" if g_mode == MODE_RICH_TRUE() if token == "frame" on = sgr_steel_true() elif token == "accent" on = sgr_teal_true() elif token == "ok" on = sgr_ok_true() elif token == "warn" on = sgr_warn_true() elif token == "fail" on = sgr_fail_true() elif token == "mute" on = sgr_mute() end if else if token == "frame" on = sgr_steel() elif token == "accent" on = sgr_teal() elif token == "ok" on = sgr_ok() elif token == "warn" on = sgr_warn() elif token == "fail" on = sgr_fail() elif token == "mute" on = sgr_mute() end if end if return str.concat(str.concat(on, text), esc_reset()) end paint // Glyphs. Selected by g_mode. fn glyph_ok(): string if g_mode == MODE_RICH_ASCII() return "#" end if if g_mode == MODE_PLAIN() return "ok" end if return "●" end glyph_ok fn glyph_fail(): string if g_mode == MODE_RICH_ASCII() return "X" end if if g_mode == MODE_PLAIN() return "fail" end if return "✕" end glyph_fail fn glyph_pending(): string if g_mode == MODE_RICH_ASCII() return "." end if if g_mode == MODE_PLAIN() return "pending" end if return "·" end glyph_pending fn glyph_starting(): string if g_mode == MODE_RICH_ASCII() return "o" end if if g_mode == MODE_PLAIN() return "starting" end if return "◉" end glyph_starting // Sigil: "[Z]" colored c.accent in rich modes. fn sigil(): string return paint("accent", "[Z]") end sigil // Plain-mode line: " [k=v ...]" fn fmt_plain_event(elapsed_ms: int, level: string, event: string, name: string, kv: string): string let secs = int_to_str(elapsed_ms / 1000) // centiseconds: truncate the within-second remainder to 0-99 (2 digits) let frac = int_to_str((elapsed_ms % 1000) / 10) mut pad = "" if str.length(frac) < 2 pad = "0" end if mut line = str.concat(" ", secs) line = str.concat(line, ".") line = str.concat(line, pad) line = str.concat(line, frac) line = str.concat(line, " ") line = str.concat(line, level) line = str.concat(line, " ") line = str.concat(line, event) line = str.concat(line, " ") line = str.concat(line, name) if str.length(kv) > 0 line = str.concat(line, " ") line = str.concat(line, kv) end if return line end fmt_plain_event proc tape_push(elapsed_ms: int, glyph: string, name: string, note: string, active_flag: int) let slot = g_tape_head % TAPE_HEIGHT() zyginit_tape_set(slot, elapsed_ms, glyph, name, note, active_flag) g_tape_head = g_tape_head + 1 if g_tape_count < TAPE_HEIGHT() g_tape_count = g_tape_count + 1 end if end tape_push fn fmt_tape_row(row_elapsed_ms: int, row_glyph: string, row_name: string, row_note: string): string let secs = row_elapsed_ms / 1000 let frac = (row_elapsed_ms % 1000) / 10 mut sf = int_to_str(frac) if str.length(sf) < 2 sf = str.concat("0", sf) end if let elapsed = str.concat(str.concat(int_to_str(secs), "."), sf) mut pad = "" let pad_n = 6 - str.length(elapsed) mut i = 0 while i < pad_n pad = str.concat(pad, " ") i = i + 1 end while mut row = str.concat(pad, elapsed) row = str.concat(row, " ") row = str.concat(row, row_glyph) row = str.concat(row, " ") row = str.concat(row, row_name) if str.length(row_note) > 0 row = str.concat(row, " ") row = str.concat(row, paint("mute", str.concat(str.concat("(", row_note), ")"))) end if return row end fmt_tape_row fn make_divider(): string mut div = "" mut k = 0 let dlen = g_line_width - 16 while k < dlen div = str.concat(div, "─") k = k + 1 end while return div end make_divider fn fmt_header(elapsed_ms: int): string let secs = elapsed_ms / 1000 let frac = (elapsed_ms % 1000) / 100 let elapsed = str.concat(str.concat(int_to_str(secs), "."), str.concat(int_to_str(frac), "s")) let phase_label = g_runlevel mut out = str.concat(" ", sigil()) out = str.concat(out, str.concat(" zyginit ", str.concat(version.VERSION(), " · "))) out = str.concat(out, paint("frame", phase_label)) let boot_visually_done = (g_phase == "boot") and ((g_done + g_skipped + g_failed_count) >= g_num_svc) and (g_num_svc > 0) if not boot_visually_done out = str.concat(out, " ") out = str.concat(out, paint("accent", str.concat("elapsed ", elapsed))) end if out = str.concat(out, "\n ") out = str.concat(out, paint("frame", str.concat(str.concat(int_to_str(g_num_svc), " services · tier "), str.concat(str.concat(int_to_str(g_cur_tier), " · "), str.concat(str.concat(int_to_str(g_done), " done · "), str.concat(str.concat(int_to_str(g_failed_count), " failed · "), str.concat(int_to_str(g_skipped), " skipped"))))))) out = str.concat(out, "\n ") if g_failed_count == 0 out = str.concat(out, paint("mute", "failed: none")) elif g_failed_count == 1 out = str.concat(out, paint("fail", str.concat(str.concat("failed: ", g_failed_first), str.concat(" exit=", int_to_str(g_failed_first_exit))))) else out = str.concat(out, paint("fail", str.concat(str.concat("failed: ", int_to_str(g_failed_count)), " services"))) end if out = str.concat(out, "\n ") out = str.concat(out, fmt_divider_gauge(g_done + g_skipped, g_num_svc)) return out end fmt_header // Render the boot/shutdown header's bottom rule as a divider with an // embedded progress gauge. Replaces make_divider() in fmt_header's // trailing rule and superseded fmt_progress_bar() (now removed) in // render_boot_screen. Total visual width = // g_line_width - 16 (matches make_divider). Brackets + 26-cell inner // content + equal lpad/rpad of dashes. Narrow-terminal fallback drops // the brackets and shows just "── NN% ──". fn fmt_divider_gauge(done: int, total: int): string if g_mode == MODE_PLAIN() return "" end if let total_width = g_line_width - 16 let bar_width = 20 let inner_width = 26 // bar(20) + " "(2) + "NNN%"(4) let bracket_width = inner_width + 2 // 28: '[' + inner + ']' mut filled = 0 if total > 0 filled = (done * bar_width) / total end if if filled > bar_width filled = bar_width end if if filled < 0 filled = 0 end if mut pct = 0 if total > 0 pct = (done * 100) / total end if if pct > 100 pct = 100 end if if pct < 0 pct = 0 end if // pct_field: right-aligned 3 cells + '%' = 4 cells. E.g. " 3%", " 31%", "100%". let pct_str = int_to_str(pct) let pct_len = str.length(pct_str) mut pct_field = "" mut pp = 0 while pp < (3 - pct_len) pct_field = str.concat(pct_field, " ") pp = pp + 1 end while pct_field = str.concat(pct_field, pct_str) pct_field = str.concat(pct_field, "%") // Narrow-terminal fallback: not enough room for brackets + padding. if total_width < bracket_width + 2 // Emit "── NN% ──" centered in total_width. let label = str.concat(" ", str.concat(pct_field, " ")) let label_len = 1 + 4 + 1 // " " + "NNN%" + " " mut pad = 0 if total_width > label_len pad = total_width - label_len end if let lpad_n = pad / 2 let rpad_n = pad - lpad_n mut out = "" mut a = 0 while a < lpad_n out = str.concat(out, "─") a = a + 1 end while out = str.concat(out, label) a = 0 while a < rpad_n out = str.concat(out, "─") a = a + 1 end while return paint("accent", out) end if // Full layout: lpad + '[' + bar + " " + pct_field + ']' + rpad let pad_total = total_width - bracket_width let lpad_n = pad_total / 2 let rpad_n = pad_total - lpad_n // Build the bar. Painting per-cell to support per-mode chars. mut bar = "" mut i = 0 while i < bar_width if g_mode == MODE_RICH_ASCII() if i < filled bar = str.concat(bar, "#") else bar = str.concat(bar, " ") end if else if i < filled bar = str.concat(bar, paint("accent", "█")) else bar = str.concat(bar, paint("mute", "░")) end if end if i = i + 1 end while // Assemble. Brackets, pct, and pad chars all painted "accent". mut out = "" mut j = 0 while j < lpad_n out = str.concat(out, "─") j = j + 1 end while out = paint("accent", out) out = str.concat(out, paint("accent", "[")) out = str.concat(out, bar) out = str.concat(out, paint("accent", " ")) out = str.concat(out, paint("accent", pct_field)) out = str.concat(out, paint("accent", "]")) mut rpad = "" j = 0 while j < rpad_n rpad = str.concat(rpad, "─") j = j + 1 end while out = str.concat(out, paint("accent", rpad)) return out end fmt_divider_gauge // Render a single tape row, overriding the stored glyph with the current // spinner frame when the row's active flag indicates a starting or stopping // transition (active==1 = forward spinner, active==2 = reverse spinner). fn fmt_tape_row_at(slot: int): string let elapsed_ms = zyginit_tape_get_elapsed(slot) let name = zyginit_tape_get_name(slot) let note = zyginit_tape_get_note(slot) let stored_glyph = zyginit_tape_get_glyph(slot) let row_active = zyginit_tape_get_active(slot) mut glyph = stored_glyph if row_active == 1 glyph = paint("warn", ui_spinner_frame(false)) elif row_active == 2 glyph = paint("warn", ui_spinner_frame(true)) end if return fmt_tape_row(elapsed_ms, glyph, name, note) end fmt_tape_row_at // Scan tape for any slot with a non-zero active flag (i.e., a spinner is live). fn has_active_tape_row(): bool mut i = 0 while i < g_tape_count let logical = g_tape_head - 1 - i mut slot = logical % TAPE_HEIGHT() if slot < 0 slot = slot + TAPE_HEIGHT() end if if zyginit_tape_get_active(slot) != 0 return true end if i = i + 1 end while return false end has_active_tape_row // Find the most-recently-pushed tape slot with a non-zero active flag and // whose name matches `name`. Returns the slot index or -1 if not found. fn tape_find_active_by_name(name: string): int if g_tape_count == 0 return 0 - 1 end if mut i = 0 while i < g_tape_count let logical = g_tape_head - 1 - i mut slot = logical % TAPE_HEIGHT() if slot < 0 slot = slot + TAPE_HEIGHT() end if if zyginit_tape_get_active(slot) != 0 if zyginit_tape_get_name(slot) == name return slot end if end if i = i + 1 end while return 0 - 1 end tape_find_active_by_name fn render_boot_screen(elapsed_ms: int): string if g_mode == MODE_PLAIN() return "" end if mut out = str.concat(fmt_header(elapsed_ms), "\n") let start = g_tape_head - g_tape_count mut i = 0 while i < TAPE_HEIGHT() if i < g_tape_count let slot = (start + i) % TAPE_HEIGHT() out = str.concat(out, fmt_tape_row_at(slot)) end if if i < (TAPE_HEIGHT() - 1) out = str.concat(out, "\n") end if i = i + 1 end while // No trailing newline: total height is exactly g_screen_rows. A trailing // \n would advance the cursor past the bottom edge and scroll the [Z] // banner off the top of the screen. The progress gauge lives in the // header rule (see fmt_divider_gauge), not on the last row. return out end render_boot_screen // Advance the tick counter and, if in rich mode with an active spinner, redraw. proc ui_tick() g_tick = g_tick + 1 if g_mode == MODE_PLAIN() return end if if not has_active_tape_row() return end if let elapsed = zyginit_monotonic_ms() - g_boot_start_ms print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) print(render_boot_screen(elapsed)) end ui_tick // Rich-mode-only redraw. Plain mode emits its own line before calling this. proc emit_redraw(elapsed_ms: int) if g_mode == MODE_PLAIN() return end if print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) print(render_boot_screen(elapsed_ms)) end emit_redraw proc ui_boot_start(num_svc: int, num_tiers: int, runlevel: string) g_phase = "boot" g_runlevel = runlevel g_num_svc = num_svc g_num_tiers = num_tiers g_cur_tier = 0 g_done = 0 g_failed_count = 0 g_skipped = 0 g_failed_first = "" g_failed_first_exit = 0 g_tape_head = 0 g_tape_count = 0 zyginit_tape_clear() g_boot_start_ms = zyginit_monotonic_ms() end ui_boot_start proc ui_tier_start(tier: int) g_cur_tier = tier end ui_tier_start proc ui_tier_done(tier: int) // header repaints happen via emit_redraw on next event/tick end ui_tier_done // dur_ms: service's own runtime (from supervisor). Reserved for future // use (Task 6 spinner display); the tape currently shows wall-clock // elapsed since boot, not per-service duration. proc ui_event_started(name: string, dur_ms: int) let elapsed = zyginit_monotonic_ms() - g_boot_start_ms g_done = g_done + 1 let slot = tape_find_active_by_name(name) if slot >= 0 // Update the existing active row in place zyginit_tape_set_glyph(slot, paint("ok", glyph_ok())) zyginit_tape_set_note(slot, "") zyginit_tape_set_active(slot, 0) else // No prior starting row — push a fresh static row tape_push(elapsed, paint("ok", glyph_ok()), name, "", 0) end if if g_mode == MODE_PLAIN() println(fmt_plain_event(elapsed, "info", "started", name, "")) else emit_redraw(elapsed) end if end ui_event_started // dur_ms: service's own runtime (from supervisor). Reserved for future // use (Task 6 spinner display); the tape currently shows wall-clock // elapsed since boot, not per-service duration. proc ui_event_failed(name: string, exit_code: int, dur_ms: int) let elapsed = zyginit_monotonic_ms() - g_boot_start_ms g_done = g_done + 1 g_failed_count = g_failed_count + 1 if g_failed_count == 1 g_failed_first = name g_failed_first_exit = exit_code end if let exit_kv = str.concat("exit=", int_to_str(exit_code)) let slot = tape_find_active_by_name(name) if slot >= 0 zyginit_tape_set_glyph(slot, paint("fail", glyph_fail())) zyginit_tape_set_note(slot, exit_kv) zyginit_tape_set_active(slot, 0) else tape_push(elapsed, paint("fail", glyph_fail()), name, exit_kv, 0) end if if g_mode == MODE_PLAIN() println(fmt_plain_event(elapsed, "err", "failed", name, exit_kv)) else emit_redraw(elapsed) end if end ui_event_failed // Called when a service is transitioning to started (forward spinner visible). proc ui_event_starting(name: string) let elapsed = zyginit_monotonic_ms() - g_boot_start_ms // active=1: forward spinner (starting) tape_push(elapsed, paint("warn", ui_spinner_frame(false)), name, "", 1) if g_mode == MODE_PLAIN() println(fmt_plain_event(elapsed, "info", "starting", name, "")) else emit_redraw(elapsed) end if end ui_event_starting // Called when a service is transitioning to stopped (reverse spinner visible). proc ui_event_stopping(name: string) let elapsed = zyginit_monotonic_ms() - g_boot_start_ms // active=2: reverse spinner (stopping) tape_push(elapsed, paint("warn", ui_spinner_frame(true)), name, "", 2) if g_mode == MODE_PLAIN() println(fmt_plain_event(elapsed, "info", "stopping", name, "")) else emit_redraw(elapsed) end if end ui_event_stopping // Called when a service's [condition] block fails and the service is skipped // (STATE_SKIPPED). No process is ever forked; the service is counted as done // for progress purposes but not as online or failed. proc ui_event_skipped(name: string, reason: string) let elapsed = zyginit_monotonic_ms() - g_boot_start_ms g_skipped = g_skipped + 1 let note = str.concat("skipped: ", reason) tape_push(elapsed, paint("mute", glyph_pending()), name, note, 0) if g_mode == MODE_PLAIN() let kv = str.concat("reason=", reason) println(fmt_plain_event(elapsed, "info", "skipped", name, kv)) else emit_redraw(elapsed) end if end ui_event_skipped proc ui_init(rich_mode: int) g_mode = rich_mode g_failed = false end ui_init // Detect terminal size via TIOCGWINSZ and update g_tape_height / g_line_width. // Called from main.reef after setup_pid1_console() (so fd 1 = /dev/console) // and before ui_boot_start(). // When ZYGINIT_FORCE_80x25 is set (integration / snapshot tests), lock to // 80x25 so snapshot output is deterministic regardless of the host terminal. proc ui_detect_winsize() if env.has_env("ZYGINIT_FORCE_80x25") g_screen_rows = 25 g_screen_cols = 80 else let rows = new [int](1) let cols = new [int](1) rows[0] = 25 cols[0] = 80 if zyginit_get_winsize(1, rows, cols) == 0 g_screen_rows = rows[0] g_screen_cols = cols[0] end if end if // Tape height = screen rows - (header 4 rows including gauge divider). // Gauge lives inside the header, not on a separate trailing row. g_tape_height = g_screen_rows - 4 if g_tape_height < 4 g_tape_height = 4 end if if g_tape_height > 32 g_tape_height = 32 end if // Line width = cols - 2 (1-char margin each side) g_line_width = g_screen_cols - 2 if g_line_width < 40 g_line_width = 40 end if end ui_detect_winsize fn ui_mode(): int return g_mode end ui_mode fn ui_runlevel(): string return g_runlevel end ui_runlevel proc ui_set_mode(m: int) g_mode = m end ui_set_mode proc ui_boot_complete(stats: BootStats) if g_mode == MODE_PLAIN() println(fmt_plain_event(stats.elapsed_ms, "info", "boot_complete", "summary", str.concat(str.concat("online=", int_to_str(stats.online)), str.concat(str.concat(" failed=", int_to_str(stats.failed)), str.concat(" skipped=", int_to_str(stats.skipped)))))) return end if // Clear screen, paint final card. print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) let secs = stats.elapsed_ms / 1000 let frac = (stats.elapsed_ms % 1000) / 100 let elapsed = str.concat(str.concat(int_to_str(secs), "."), str.concat(int_to_str(frac), "s")) let summary_count = int_to_str(stats.online + stats.failed + stats.skipped) mut out = str.concat(" ", sigil()) out = str.concat(out, str.concat(" zyginit ", str.concat(version.VERSION(), " · "))) out = str.concat(out, paint("frame", g_runlevel)) out = str.concat(out, " ") out = str.concat(out, paint("accent", str.concat("boot ", elapsed))) out = str.concat(out, "\n") out = str.concat(out, " ") out = str.concat(out, paint("frame", str.concat(str.concat(summary_count, " services — "), str.concat(str.concat(int_to_str(stats.online), " online, "), str.concat(str.concat(int_to_str(stats.failed), " failed, "), str.concat(int_to_str(stats.skipped), " skipped")))))) out = str.concat(out, "\n\n") if stats.failed == 0 out = str.concat(out, " ") out = str.concat(out, paint("ok", "all services online")) out = str.concat(out, "\n") elif stats.failed == 1 out = str.concat(out, " ") out = str.concat(out, paint("fail", str.concat(str.concat("failed: ", g_failed_first), str.concat(" exit=", int_to_str(g_failed_first_exit))))) out = str.concat(out, "\n") out = str.concat(out, " ") out = str.concat(out, paint("accent", str.concat("→ zygctl log ", g_failed_first))) out = str.concat(out, paint("mute", " to see why")) out = str.concat(out, "\n") else out = str.concat(out, " ") out = str.concat(out, paint("fail", str.concat(str.concat("failed: ", int_to_str(stats.failed)), " services"))) out = str.concat(out, "\n") out = str.concat(out, " ") out = str.concat(out, paint("accent", "→ zygctl status")) out = str.concat(out, paint("mute", " to see them")) out = str.concat(out, "\n") end if out = str.concat(out, " ") out = str.concat(out, paint("accent", make_divider())) out = str.concat(out, "\n") // Slowest line let slow_n = stats.slowest_count if slow_n > 0 mut slow = "slowest:" mut i = 0 mut max = slow_n if max > 3 max = 3 end if while i < max slow = str.concat(slow, " ") slow = str.concat(slow, stats.slowest[i]) i = i + 1 end while out = str.concat(out, " ") out = str.concat(out, paint("frame", slow)) out = str.concat(out, "\n") end if print(out) end ui_boot_complete proc ui_shutdown_start(reason: string) g_phase = "shutdown" g_shutdown_reason = reason g_runlevel = str.concat("stopping for ", reason) g_done = 0 g_failed_count = 0 g_skipped = 0 g_failed_first = "" g_failed_first_exit = 0 g_tape_head = 0 g_tape_count = 0 g_boot_start_ms = zyginit_monotonic_ms() end ui_shutdown_start proc ui_event_stopped(name: string, dur_ms: int) let elapsed = zyginit_monotonic_ms() - g_boot_start_ms g_done = g_done + 1 let slot = tape_find_active_by_name(name) if slot >= 0 zyginit_tape_set_glyph(slot, paint("mute", glyph_pending())) zyginit_tape_set_note(slot, "") zyginit_tape_set_active(slot, 0) else tape_push(elapsed, paint("mute", glyph_pending()), name, "", 0) end if if g_mode == MODE_PLAIN() println(fmt_plain_event(elapsed, "info", "stopped", name, "")) else emit_redraw(elapsed) end if end ui_event_stopped proc ui_shutdown_complete(reason: string, elapsed_ms: int) if g_mode == MODE_PLAIN() println(fmt_plain_event(elapsed_ms, "info", "shutdown_complete", reason, str.concat("down=", int_to_str(g_done)))) return end if print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) let secs = elapsed_ms / 1000 let frac = (elapsed_ms % 1000) / 100 let elapsed = str.concat(str.concat(int_to_str(secs), "."), str.concat(int_to_str(frac), "s")) mut out = str.concat(" ", sigil()) out = str.concat(out, str.concat(" zyginit ", str.concat(version.VERSION(), " · "))) out = str.concat(out, paint("frame", str.concat(reason, " complete"))) out = str.concat(out, " ") out = str.concat(out, paint("accent", str.concat(str.concat(reason, " "), elapsed))) out = str.concat(out, "\n ") mut stat_str = "stopped cleanly" if g_failed_count > 0 stat_str = str.concat(str.concat("stopped (", int_to_str(g_failed_count)), " force-killed)") end if out = str.concat(out, paint("frame", str.concat(str.concat(int_to_str(g_num_svc), " services "), stat_str))) out = str.concat(out, "\n ") out = str.concat(out, paint("accent", make_divider())) out = str.concat(out, "\n ") out = str.concat(out, paint("mute", "invoking uadmin(A_SHUTDOWN, AD_BOOT)")) out = str.concat(out, "\n") print(out) end ui_shutdown_complete // Scenario dispatcher for --ui-demo. Called from main.reef. // Returns 0 on success, 1 if the scenario name is unknown. fn ui_demo(scenario: string): int if scenario == "skeleton" println("ui.reef skeleton ok, mode=" + int_to_str(g_mode)) return 0 end if if scenario == "plain_boot" println(fmt_plain_event(40, "info", "started", "root-fs", "")) println(fmt_plain_event(120, "info", "started", "crypto", "dur_ms=80")) println(fmt_plain_event(310, "info", "started", "devfs", "dur_ms=190")) println(fmt_plain_event(5020, "err", "failed", "network", "exit=1 dur_ms=4023")) return 0 end if if str.index_of(scenario, "detect_") == 0 g_mode = detect_rich_mode() println("mode=" + int_to_str(g_mode)) return 0 end if if scenario == "palette_true" or scenario == "palette_16" g_mode = detect_rich_mode() println(paint("frame", "frame text")) println(paint("accent", "accent text")) println(paint("ok", "ok text")) println(paint("warn", "warn text")) println(paint("fail", "fail text")) println(paint("mute", "mute text")) return 0 end if if scenario == "glyphs_unicode" or scenario == "glyphs_ascii" g_mode = detect_rich_mode() println(glyph_ok() + " " + glyph_starting() + " " + glyph_fail() + " " + glyph_pending()) return 0 end if if scenario == "sigil_rich" g_mode = detect_rich_mode() println(sigil() + " zyginit " + version.VERSION()) return 0 end if if scenario == "boot_tape_rich" or scenario == "boot_tape_ascii" or scenario == "boot_tape_plain" g_mode = detect_rich_mode() ui_boot_start(17, 8, "multi-user") g_boot_start_ms = 0 let names = new [string](9) names[0] = "root-fs" names[1] = "crypto" names[2] = "devfs" names[3] = "swap" names[4] = "filesystem" names[5] = "identity" names[6] = "sysconfig" names[7] = "dlmgmtd" names[8] = "network" let elapsed_seq = new [int](9) elapsed_seq[0] = 40 elapsed_seq[1] = 160 elapsed_seq[2] = 310 elapsed_seq[3] = 490 elapsed_seq[4] = 1040 elapsed_seq[5] = 1080 elapsed_seq[6] = 1360 elapsed_seq[7] = 1550 elapsed_seq[8] = 5020 mut i = 0 while i < 8 g_done = g_done + 1 tape_push(elapsed_seq[i], paint("ok", glyph_ok()), names[i], "", 0) i = i + 1 end while g_failed_count = 1 g_failed_first = "network" g_failed_first_exit = 1 g_cur_tier = 5 g_done = g_done + 1 tape_push(5020, paint("fail", glyph_fail()), "network", "exit=1", 0) if g_mode == MODE_PLAIN() let lvls = new [string](9) let evs = new [string](9) let kvs = new [string](9) mut j = 0 while j < 8 lvls[j] = "info" evs[j] = "started" kvs[j] = "" j = j + 1 end while lvls[8] = "err" evs[8] = "failed" kvs[8] = "exit=1" mut k = 0 while k < 9 println(fmt_plain_event(elapsed_seq[k], lvls[k], evs[k], names[k], kvs[k])) k = k + 1 end while else print(render_boot_screen(5020)) end if return 0 end if if scenario == "progress_rich" or scenario == "progress_ascii" if scenario == "progress_ascii" g_mode = MODE_RICH_ASCII() else g_mode = detect_rich_mode() if g_mode == MODE_PLAIN() g_mode = MODE_RICH_TRUE() end if end if ui_detect_winsize() ui_boot_start(17, 8, "multi-user") g_done = 12 g_failed_count = 1 println(fmt_divider_gauge(g_done + g_skipped, g_num_svc)) return 0 end if if scenario == "progress_full" g_mode = detect_rich_mode() if g_mode == MODE_PLAIN() g_mode = MODE_RICH_TRUE() end if ui_detect_winsize() ui_boot_start(17, 8, "multi-user") g_done = 17 println(fmt_divider_gauge(g_done + g_skipped, g_num_svc)) return 0 end if if scenario == "gauge_live_31" or scenario == "gauge_live_31_ascii" if scenario == "gauge_live_31_ascii" g_mode = MODE_RICH_ASCII() else g_mode = detect_rich_mode() if g_mode == MODE_PLAIN() g_mode = MODE_RICH_TRUE() end if end if ui_detect_winsize() println(fmt_divider_gauge(31, 100)) return 0 end if if scenario == "gauge_live_100" g_mode = detect_rich_mode() if g_mode == MODE_PLAIN() g_mode = MODE_RICH_TRUE() end if ui_detect_winsize() println(fmt_divider_gauge(100, 100)) return 0 end if if scenario == "gauge_live_0" g_mode = detect_rich_mode() if g_mode == MODE_PLAIN() g_mode = MODE_RICH_TRUE() end if ui_detect_winsize() println(fmt_divider_gauge(0, 100)) return 0 end if if scenario == "spinner_forward" g_mode = detect_rich_mode() mut i = 0 while i < 8 g_tick = i print(ui_spinner_frame(false)) i = i + 1 end while println("") return 0 end if if scenario == "spinner_reverse" g_mode = detect_rich_mode() mut i = 0 while i < 8 g_tick = i print(ui_spinner_frame(true)) i = i + 1 end while println("") return 0 end if if scenario == "spinner_in_tape" g_mode = detect_rich_mode() ui_boot_start(17, 8, "multi-user") g_boot_start_ms = 0 tape_push(40, paint("ok", glyph_ok()), "root-fs", "", 0) tape_push(160, paint("ok", glyph_ok()), "crypto", "", 0) tape_push(310, paint("ok", glyph_ok()), "devfs", "", 0) g_done = 3 // Push the network row with active=1 (forward spinner). // Bypass ui_event_starting to avoid a host-dependent monotonic_ms() // reading in the demo. g_tick = 0 tape_push(2100, paint("warn", ui_spinner_frame(false)), "network", "", 1) // Now bump tick so the live re-render shows frame index 2. g_tick = 2 print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) print(render_boot_screen(2100)) return 0 end if if scenario == "final_card_ok" g_mode = detect_rich_mode() ui_boot_start(17, 8, "multi-user") let slow = new [string](3) slow[0] = "dlmgmtd 1.2s" slow[1] = "filesystem 0.55s" slow[2] = "sshd 0.37s" let stats = BootStats{ elapsed_ms: 8300, online: 17, failed: 0, skipped: 0, slowest: slow, slowest_count: 3 } ui_boot_complete(stats) return 0 end if if scenario == "final_card_failed" g_mode = detect_rich_mode() ui_boot_start(17, 8, "multi-user") g_failed_first = "network" g_failed_first_exit = 1 let slow = new [string](3) slow[0] = "network 5.02s" slow[1] = "filesystem 0.55s" slow[2] = "sshd 0.37s" let stats = BootStats{ elapsed_ms: 8300, online: 16, failed: 1, skipped: 0, slowest: slow, slowest_count: 3 } ui_boot_complete(stats) return 0 end if if scenario == "shutdown_mirror" g_mode = detect_rich_mode() ui_boot_start(17, 8, "multi-user") ui_shutdown_start("reboot") g_boot_start_ms = 0 tape_push(40, paint("mute", glyph_pending()), "sshd", "", 0) tape_push(160, paint("mute", glyph_pending()), "console-login", "", 0) tape_push(310, paint("mute", glyph_pending()), "cron", "", 0) // active=2: reverse spinner (stopping) g_tick = 0 tape_push(480, paint("warn", ui_spinner_frame(true)), "syslogd", "", 2) g_tick = 3 g_done = 4 g_cur_tier = 6 print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) print(render_boot_screen(480)) return 0 end if if scenario == "shutdown_final" g_mode = detect_rich_mode() ui_boot_start(17, 8, "multi-user") ui_shutdown_start("reboot") g_num_svc = 17 g_done = 17 ui_shutdown_complete("reboot", 2100) return 0 end if if scenario == "task_ok" g_mode = detect_rich_mode() ui_boot_start(5, 2, "multi-user") g_boot_start_ms = 0 tape_push(40, paint("ok", glyph_ok()), "root-fs", "", 0) tape_push(160, paint("ok", glyph_ok()), "task-svc", "", 0) g_done = 2 let slow_tok = new [string](3) let stats_tok = BootStats{ elapsed_ms: 200, online: 5, failed: 0, skipped: 0, slowest: slow_tok, slowest_count: 0 } ui_boot_complete(stats_tok) return 0 end if if scenario == "task_failed" g_mode = detect_rich_mode() ui_boot_start(5, 2, "multi-user") g_boot_start_ms = 0 tape_push(40, paint("ok", glyph_ok()), "root-fs", "", 0) tape_push(160, paint("fail", glyph_fail()), "task-svc", "exit=1", 0) g_done = 2 g_failed_count = 1 g_failed_first = "task-svc" g_failed_first_exit = 1 let slow_tf = new [string](3) let stats_tf = BootStats{ elapsed_ms: 200, online: 4, failed: 1, skipped: 0, slowest: slow_tf, slowest_count: 0 } ui_boot_complete(stats_tf) return 0 end if if scenario == "boot_with_skipped" g_mode = detect_rich_mode() ui_boot_start(5, 2, "multi-user") g_boot_start_ms = 0 tape_push(40, paint("ok", glyph_ok()), "root-fs", "", 0) tape_push(160, paint("mute", glyph_pending()), "acpihpd", "skipped: missing /dev/acpihp", 0) tape_push(310, paint("ok", glyph_ok()), "cron", "", 0) g_done = 2 g_skipped = 1 print(str.concat(str.concat(zyginit_esc_str(), "[2J"), str.concat(zyginit_esc_str(), "[H"))) print(render_boot_screen(400)) return 0 end if if scenario == "final_card_with_skipped" g_mode = detect_rich_mode() ui_boot_start(23, 9, "multi-user") let slow_fcs = new [string](3) let stats_fcs = BootStats{ elapsed_ms: 8300, online: 19, failed: 0, skipped: 4, slowest: slow_fcs, slowest_count: 0 } ui_boot_complete(stats_fcs) return 0 end if if scenario == "status_skipped" // status_skipped requires zygctl status table rendering with a // SKIPPED row. The existing --ui-demo infrastructure doesn't yet // build a ServiceTable mock that ui_render.ui_render_status can // render. Deferred — emit a placeholder note so the test exists // and tracks the gap. g_mode = detect_rich_mode() println("status_skipped: deferred — zygctl status snapshot via --ui-demo not yet supported") return 0 end if println("ui.reef: unknown demo scenario: " + scenario) return 1 end ui_demo fn int_to_str(n: int): string if n == 0 return "0" end if mut value = n if n < 0 value = 0 - n end if mut result = "" while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while if n < 0 result = str.concat("-", result) end if return result end int_to_str // ============================================================================ // Status/list rendering (used by socket command handlers) // ============================================================================ fn pad_right(s: string, n: int): string let extra = n - str.length(s) if extra <= 0 return s end if mut out = s mut i = 0 while i < extra out = str.concat(out, " ") i = i + 1 end while return out end pad_right end module