/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com Project: zyginit Filename: ui_render.reef Authors: Chris Tusa License: Description: Status/list table rendering for zygctl status and list commands. Separated from ui.reef to avoid a circular dependency with supervisor. ******************************************************************************/ module ui_render import ui import supervisor import config import version import core.str as str import time.time as time export fn ui_render_status(table: supervisor.ServiceTable, want_banner: int, plain: int): string fn ui_render_status_one(table: supervisor.ServiceTable, idx: int, plain: int): string fn ui_render_list(table: supervisor.ServiceTable, plain: int): string end export // ============================================================================ // Helpers // ============================================================================ 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 // Helper: return the palette token ("ok"/"warn"/"fail"/"mute") for a state. fn state_color(state: int): string if state == supervisor.STATE_RUNNING() return "ok" end if if state == supervisor.STATE_STARTING() return "warn" end if if state == supervisor.STATE_STOPPING() return "warn" end if if state == supervisor.STATE_STOPPED() return "mute" end if if state == supervisor.STATE_FAILED() return "fail" end if if state == supervisor.STATE_MAINTENANCE() return "fail" end if if state == supervisor.STATE_DISABLED() return "mute" end if if state == supervisor.STATE_WAITING() return "warn" end if if state == supervisor.STATE_SKIPPED() return "mute" end if return "mute" end state_color // Helper: return the raw glyph character for a state (no paint wrapping). fn state_glyph(state: int): string if state == supervisor.STATE_RUNNING() return ui.glyph_ok() end if if state == supervisor.STATE_STARTING() return ui.glyph_starting() end if if state == supervisor.STATE_STOPPING() return ui.glyph_starting() end if if state == supervisor.STATE_STOPPED() return ui.glyph_pending() end if if state == supervisor.STATE_FAILED() return ui.glyph_fail() end if if state == supervisor.STATE_MAINTENANCE() return ui.glyph_fail() end if if state == supervisor.STATE_DISABLED() return ui.glyph_pending() end if if state == supervisor.STATE_WAITING() return ui.glyph_starting() end if if state == supervisor.STATE_SKIPPED() return ui.glyph_pending() end if return "?" end state_glyph // Helper: return the label string for a state (leading space, no glyph). fn state_label(state: int): string if state == supervisor.STATE_RUNNING() return " running" end if if state == supervisor.STATE_STARTING() return " starting" end if if state == supervisor.STATE_STOPPING() return " stopping" end if if state == supervisor.STATE_STOPPED() return " stopped" end if if state == supervisor.STATE_FAILED() return " failed" end if if state == supervisor.STATE_MAINTENANCE() return " maint." end if if state == supervisor.STATE_DISABLED() return " disabled" end if if state == supervisor.STATE_WAITING() return " waiting" end if if state == supervisor.STATE_SKIPPED() return " skipped" end if return " ?" end state_label // Compute the SERVICE column width for status tables. // Scans all service names and returns the widest + 2 padding, clamped to [12, 24]. // The "SERVICE" header is 7 chars; we use that as the floor before clamping. fn compute_service_col_width(table: supervisor.ServiceTable): int let count = supervisor.service_count(table) mut max_len = 7 // "SERVICE" header length mut i = 0 while i < count let rt = supervisor.get_runtime(table, i) let def = supervisor.rt_def(rt) let name = config.svc_name(def) let n = str.length(name) if n > max_len max_len = n end if i = i + 1 end while // Clamp: 12 minimum (visual stability), 24 maximum (avoid eating other columns) if max_len < 12 max_len = 12 end if if max_len > 24 max_len = 24 end if return max_len + 2 // 2 cols of padding after the name end compute_service_col_width fn fmt_status_row(table: supervisor.ServiceTable, idx: int, name_col_width: int): string let rt = supervisor.get_runtime(table, idx) let def = supervisor.rt_def(rt) let name = config.svc_name(def) let state = supervisor.rt_state(rt) let pid = supervisor.rt_pid(rt) let ctid = supervisor.rt_contract_id(rt) mut row = str.concat(" ", ui.pad_right(name, name_col_width)) // STATE column: paint only the glyph, then pad the label so ANSI escapes // don't inflate the byte count and shift downstream columns. let glyph_color = state_color(state) let g = state_glyph(state) let label = state_label(state) row = str.concat(row, ui.paint(glyph_color, g)) row = str.concat(row, ui.pad_right(label, 10)) if pid > 0 row = str.concat(row, ui.pad_right(int_to_str(pid), 7)) else row = str.concat(row, ui.pad_right("", 7)) end if if ctid > 0 row = str.concat(row, ui.pad_right(int_to_str(ctid), 7)) else row = str.concat(row, ui.pad_right("", 7)) end if // Uptime: only for RUNNING with a valid start time let last_start = supervisor.rt_last_start_time(rt) if state == supervisor.STATE_RUNNING() and last_start > 0 let uptime_s = time.time_now() - last_start row = str.concat(row, ui.pad_right(supervisor.format_duration(uptime_s), 10)) else row = str.concat(row, ui.pad_right("", 10)) end if // Notes column: exit code and restart count mut notes = "" let last_exit = supervisor.rt_last_exit_code(rt) let restart_count = supervisor.rt_restart_count(rt) let reason = supervisor.rt_skip_reason(rt) if state == supervisor.STATE_SKIPPED() and str.length(reason) > 0 notes = reason elif state == supervisor.STATE_FAILED() and str.length(reason) > 0 notes = reason elif last_exit >= 0 and state != supervisor.STATE_RUNNING() and state != supervisor.STATE_WAITING() notes = str.concat("exit=", int_to_str(last_exit)) if restart_count > 0 notes = str.concat(notes, str.concat(" restarts=", int_to_str(restart_count))) end if elif restart_count > 0 notes = str.concat("restarts=", int_to_str(restart_count)) end if row = str.concat(row, notes) return row end fmt_status_row // ============================================================================ // Public render functions // ============================================================================ fn ui_render_status(table: supervisor.ServiceTable, want_banner: int, plain: int): string let prev_mode = ui.ui_mode() // Mode source: when plain=1 the client requested no escapes (always // honored). When plain=0, the daemon's own g_mode (set at startup // by TTY/term detection) determines rendering. Task 10 ensures the // client only sends a bare STATUS/LIST when its own stdout is rich. if plain != 0 ui.ui_set_mode(ui.MODE_PLAIN()) end if let count = supervisor.service_count(table) // Summary counts mut online = 0 mut failed = 0 mut skipped = 0 mut i = 0 while i < count let rt = supervisor.get_runtime(table, i) let st = supervisor.rt_state(rt) if st == supervisor.STATE_RUNNING() online = online + 1 elif st == supervisor.STATE_STOPPED() // A oneshot that exited 0 is "done and online" from the system's perspective. let rt2 = supervisor.get_runtime(table, i) if supervisor.rt_last_exit_code(rt2) == 0 online = online + 1 end if elif st == supervisor.STATE_FAILED() or st == supervisor.STATE_MAINTENANCE() failed = failed + 1 elif st == supervisor.STATE_SKIPPED() skipped = skipped + 1 end if i = i + 1 end while let name_col = compute_service_col_width(table) mut out = "" if want_banner != 0 and ui.ui_mode() != ui.MODE_PLAIN() let rl = ui.ui_runlevel() let rl_str = if str.length(rl) > 0 then rl else "running" end if out = str.concat(out, " ") out = str.concat(out, ui.sigil()) out = str.concat(out, str.concat(" zyginit ", str.concat(version.VERSION(), " · "))) out = str.concat(out, ui.paint("frame", str.concat(str.concat(rl_str, " · "), str.concat(int_to_str(count), str.concat(" services · ", str.concat(int_to_str(online), str.concat(" online, ", str.concat(str.concat(int_to_str(failed), " failed, "), str.concat(int_to_str(skipped), " skipped"))))))))) out = str.concat(out, "\n ") out = str.concat(out, ui.paint("accent", ui.make_divider())) out = str.concat(out, "\n") end if // Column header out = str.concat(out, " ") let header_text = str.concat(ui.pad_right("SERVICE", name_col), str.concat(ui.pad_right("STATE", 11), str.concat(ui.pad_right("PID", 7), str.concat(ui.pad_right("CTID", 7), str.concat(ui.pad_right("UPTIME", 10), "NOTES"))))) out = str.concat(out, ui.paint("frame", header_text)) out = str.concat(out, "\n") // Rows mut j = 0 while j < count out = str.concat(out, fmt_status_row(table, j, name_col)) out = str.concat(out, "\n") j = j + 1 end while ui.ui_set_mode(prev_mode) return out end ui_render_status fn ui_render_status_one(table: supervisor.ServiceTable, idx: int, plain: int): string let prev_mode = ui.ui_mode() if plain != 0 ui.ui_set_mode(ui.MODE_PLAIN()) end if let name_col = compute_service_col_width(table) let row = str.concat(fmt_status_row(table, idx, name_col), "\n") ui.ui_set_mode(prev_mode) return row end ui_render_status_one fn ui_render_list(table: supervisor.ServiceTable, plain: int): string let prev_mode = ui.ui_mode() if plain != 0 ui.ui_set_mode(ui.MODE_PLAIN()) end if let count = supervisor.service_count(table) if count == 0 ui.ui_set_mode(prev_mode) return "no services loaded\n" end if mut out = "" mut i = 0 while i < count let rt = supervisor.get_runtime(table, i) let def = supervisor.rt_def(rt) let name = config.svc_name(def) let stype = config.service_type_name(config.svc_type(def)) let chip = ui.paint("accent", stype) out = str.concat(out, name) out = str.concat(out, " (") out = str.concat(out, chip) out = str.concat(out, ", enabled)\n") i = i + 1 end while ui.ui_set_mode(prev_mode) return out end ui_render_list end module