|
root / src / ui_render.reef
ui_render.reef Reef 258 lines 9.9 KB
  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