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
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(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 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
fn fmt_status_row(table: supervisor.ServiceTable, idx: 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, 14))
// 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
mut out = ""
if want_banner != 0 and ui.ui_mode() != ui.MODE_PLAIN()
out = str.concat(out, " ")
out = str.concat(out, ui.sigil())
out = str.concat(out, " zyginit 0.1.3 · ")
out = str.concat(out, ui.paint("frame",
str.concat(str.concat("multi-user · ", 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", 14),
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))
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 row = str.concat(fmt_status_row(table, idx), "\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
|