# Progress Gauge in Header Divider — Design

**Date:** 2026-06-01
**Status:** Approved (brainstorm complete); implementation plan to follow
**Scope:** Relocate the boot/shutdown progress indicator from the last row of the screen into the existing horizontal divider in the boot header. Eliminates a long-standing "progress bar disappears and reappears" artifact and reclaims two rows of vertical space for the rolling tape. Symmetric treatment for shutdown.

## 1. Problem

The boot screen rendered by `render_boot_screen()` in `src/ui.reef` ends with a horizontal divider plus a separate progress bar row, occupying the bottom two rows of the screen. The progress bar is the very last row — the file's own comment at `src/ui.reef:517-519` notes that the layout fits exactly `g_screen_rows` and cannot afford even a trailing newline.

Between zyginit's `\e[2J\e[H` redraws, any other writer to the console — a kernel `printk`, a daemon's startup stderr, a not-yet-suppressed driver message — scrolls the screen up. The last row is the first thing to scroll off. The user sees the progress bar disappear, then reappear on the next redraw, then disappear again. The artifact is endemic to placing dynamic content on the last row of a shared console.

## 2. Goals

1. **Stability under concurrent writers.** Move the progress indicator out of the most-volatile screen row.
2. **No layout twitch at boot completion.** The transition from live gauge to final boot-time label must not shift any surrounding glyph.
3. **Reclaim vertical space.** Eliminate both the bottom cosmetic divider and the bottom progress row — give those rows to the rolling tape.
4. **Symmetric shutdown.** Apply the same treatment to the shutdown screen; same bug exists there.
5. **No regressions in plain or ASCII mode.** `MODE_PLAIN` continues to emit line-by-line events; `MODE_RICH_ASCII` continues to use only ASCII characters.

## 3. Non-goals

- **No new TOML schema fields.** This is purely a console-rendering change.
- **No `zygctl` protocol changes.** `zygctl status` is rendered by `src/ui_render.reef` and is not touched.
- **No palette, sigil, or glyph changes.** The visual pass shipped in 0.1.3 stays as-is; only layout moves.
- **No animation changes.** Spinner cadence, tape-row behavior, and tier-progression logic are unchanged.
- **No supervision or dependency-graph changes.** Pure UI work.

## 4. Visual design

### 4.1 Before

```
  [Z] zyginit 0.2.2 · multi-user   elapsed 2.4s
  20 services · tier 3 · 8 done · 0 failed · 0 skipped
  failed: none
  ─────────────────────────────────────────       (top divider — cosmetic)
  [tape row 1]
  …
  [tape row N]
  ─────────────────────────────────────────       (bottom divider — cosmetic)
  ████░░░░░░░░░░░░░  8/20  31%                    (progress bar — last row, scrolls off)
```

### 4.2 After — boot in progress

```
  [Z] zyginit 0.2.2 · multi-user   elapsed 2.4s
  20 services · tier 3 · 8 done · 0 failed · 0 skipped
  failed: none
  ─────[████████░░░░░░░░░░  31%]───────────       (gauge inside divider)
  [tape row 1]
  …
  [tape row N+2]                                  (+2 rows reclaimed)
```

### 4.3 After — boot complete

The existing `ui_boot_complete()` at `src/ui.reef:708` clears the screen and paints a separate "final card" (sigil, version, `boot Xs`, online/failed/skipped summary, optional failure hint, slowest-services list). **The final card is unchanged** by this work. The new divider gauge therefore runs only during the live boot screen; the boot post-label (`[ boot 10.0s ]`) is rendered in code but is only momentarily visible — within one frame, the final card replaces the boot screen.

The `elapsed Xs` field on header line 1 is dropped from the boot screen once `g_done + g_skipped + g_failed_count == g_num_svc` (i.e., the boot has visually completed even if the final-card transition hasn't fired yet). This avoids showing a stale `elapsed 9.8s` while the gauge reads 100% in the brief window before the final card.

### 4.4 Shutdown

Symmetric with boot. `ui_shutdown_complete()` at `src/ui.reef:818` already paints a shutdown final card after the last service stops (sigil, `reboot X.Ys`, "invoking uadmin..." line). **The shutdown final card is unchanged.** The new divider gauge runs only during the live shutdown screen as services stop:

```
  ─────[█████████████░░░░░░░  65%]──────────       (during stop sequence)
```

Header content (status lines, failure summary) above the divider is unchanged.

## 5. Rendering rules

A new function `fmt_divider_gauge(done: int, total: int): string` replaces:

- The `make_divider()` call at the end of `fmt_header()` (the top divider).
- The bottom divider line in `render_boot_screen()`.
- The `fmt_progress_bar()` line in `render_boot_screen()`.

Width: identical to today's `make_divider()` — `g_line_width - 16` cells of total divider width, with the leading two-space indent preserved. The brackets plus their inner content consume 28 cells (`[` + 26 inner + `]`); the remaining `g_line_width - 44` cells are split equally between left and right padding (odd cell goes right).

**Narrow-terminal fallback.** If `g_line_width < 46` (i.e., `total_width < 30`) the bracket pair plus at least one flanking dash on each side won't fit. In that case `fmt_divider_gauge()` degrades to a label-only divider — no bracket pair, just `<lpad>── 31% ──<rpad>`. This path is exercised only on artificially small consoles; the Hammerhead framebuffer and standard serial console widths (80+) always hit the full layout.

The layout inside the divider (full path) is:

```
  <lpad>[<content>]<rpad>
```

Where:

- **Brackets** are literal `[` and `]`, painted with the `accent` color.
- **lpad / rpad** are `─` chars in `MODE_RICH`, `-` chars in `MODE_RICH_ASCII`, painted `accent`. Padding is distributed equally on both sides; if total padding is odd, the extra cell goes to the right side.
- **content** has a fixed inner width so the bracket positions never move between live and post-complete renders. Inner width = bar(20) + two-space gap + 4-char `NNN%` field = 26 cells.

### 5.1 Live content

```
<bar>  <pct>%
```

- **bar**: 20 cells. In `MODE_RICH`: `█` (accent) for filled, `░` (mute) for empty. In `MODE_RICH_ASCII`: `#` for filled, space for empty. Filled count = `(done * 20) / total`, clamped to `[0, 20]`.
- **pct**: integer `(done * 100) / total`, clamped to `[0, 100]`, right-aligned in a 3-cell field, followed by `%`. So `"  3%"`, `" 31%"`, `"100%"` all consume 4 cells.

### 5.2 Plain mode

`MODE_PLAIN` continues to emit one line per event via `fmt_plain_event()` and ignores the divider gauge entirely. `fmt_divider_gauge()` returns `""` in plain mode, same as `fmt_progress_bar()` does today.

## 6. State changes

**None.** Both `ui_boot_complete()` (`src/ui.reef:708`) and `ui_shutdown_complete()` (`src/ui.reef:818`) already exist and paint final cards that replace the live screen. The new divider gauge is purely a transform on existing counters (`g_done`, `g_skipped`, `g_num_svc`), so no new `g_*` flags are required.

Header behavior:

- `fmt_header()` drops the `elapsed Xs` field from line 1 when `g_done + g_skipped + g_failed_count >= g_num_svc` during the boot phase. This is a pure boolean check on existing counters; no new flag. During shutdown the elapsed field is left as-is (the shutdown final card carries the timer once complete).

## 7. Files touched

- **`src/ui.reef`** — only file touched.
  - New: `fmt_divider_gauge(done, total)` — main rendering function. Includes the narrow-terminal fallback in §5.
  - Update: `fmt_header()` — conditional `elapsed Xs` (suppressed when `g_done + g_skipped + g_failed_count >= g_num_svc` and phase is boot); divider line now calls `fmt_divider_gauge(g_done + g_skipped, g_num_svc)`.
  - Update: `render_boot_screen()` at `src/ui.reef:499` — remove the bottom divider concatenation and the `fmt_progress_bar()` call. Trailing-newline accounting recomputed so total height still equals `g_screen_rows`.
  - Update: tape height calculation at `src/ui.reef:682` — `g_tape_height = g_screen_rows - 6` becomes `g_screen_rows - 4` (header is 4 rows including the gauge divider; no trailing rows).
  - Update: scenario harness in `ui_demo()` — the existing `progress_rich`, `progress_ascii`, `progress_full` scenarios at lines 950-963 currently call `fmt_progress_bar()`; convert them to exercise `fmt_divider_gauge()` instead. Add fresh `gauge_full_screen_boot` scenario for full-layout visual check.
  - Delete: `fmt_progress_bar()` (lines 393-439), `PROGRESS_WIDTH()` (line 391). No remaining callers after the harness conversion.

- **`src/main.reef`** — no changes. `ui_boot_complete()` and `ui_shutdown_complete()` already wired.
- **`src/shutdown.reef`** — no changes.

- **`src/ui_render.reef`** — no changes. `zygctl status` rendering is independent.

## 8. Backwards compatibility

- **`MODE_PLAIN` output** — unchanged. Same line-per-event format consumers parse today.
- **`MODE_RICH_ASCII` output** — gauge characters (`#`, space) and divider characters (`-`) are pure ASCII. No new Unicode dependencies.
- **`MODE_RICH` output** — gauge characters (`█`, `░`) and divider character (`─`) were already in use in the prior layout. No new code points.
- **Screen-height math** — assumes `g_screen_rows >= 5` (1 header line + 3 status lines + at least 1 tape row). Already an existing assumption; no change.
- **External consumers** — none. `zygctl status`, scripted log parsers, and the syslog stream all read `MODE_PLAIN` or daemon log output, not the framebuffer rendering.

## 9. Testing

Manual on `hh-prototest` (the Hammerhead VM at 192.168.122.50):

1. **Cold boot.** Confirm gauge fills as tiers progress; confirm post-boot label `boot Xs` appears after the last tier; confirm `elapsed Xs` disappears from header line 1.
2. **Boot under noise.** Trigger a kernel printk during boot (e.g. a `dladm` configure with verbose driver). Confirm gauge stays visible across the scroll.
3. **Shutdown.** `zygctl halt`. Confirm gauge inverse-fills; confirm `down Xs` label appears before reboot.
4. **Single-user boot.** `boot -s`. Confirm gauge still renders correctly in the smaller service set.
5. **Plain mode.** `ZYGINIT_NO_UI=1`, `NO_COLOR=1`, non-TTY redirect, or running non-PID-1. Confirm line-per-event output is unchanged.
6. **ASCII mode.** `TERM=dumb` (or any non-color-capable terminal that doesn't trip the plain-mode fallback). Confirm `#` / space gauge renders inside the divider.

No unit tests added — the boot UI has historically been validated visually on the test VM. Existing integration tests in `tests/integration/` continue to cover supervisor behavior independently.

## 10. Open questions

None. All decisions made during brainstorming:

- Gauge style: block fill (`█`/`░` in rich, `#`/space in ASCII) — matches the prior progress bar's character set.
- `elapsed Xs` on header line 1: shown only while boot is in progress; dropped once all services have resolved (done/failed/skipped sum reaches total).
- Boot completion: existing `ui_boot_complete()` final card kept as-is. The new gauge runs only during the live boot screen.
- Shutdown: same treatment as boot. Existing `ui_shutdown_complete()` final card kept as-is. Gauge fills 0→100% as services stop, then the final card replaces the screen.
