1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com
Project: zyginit
Filename: ui_render.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
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
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
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
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)
if 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 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
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(int_to_str(failed), " failed"))))))))
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
|