docs/specs/2026-10-01-cli-views-design.md
259 lines · 10009 bytes
1# CLI views
2
3Status: accepted, 2026-10-01. Follows `2026-10-01-cli-terminal-output-design.md`
4(v1.41.0, #312–#315).
5
6## Problem
7
8Terminal output after v1.41.0 is coloured and humanized but still noisy.
9Captured at 110 columns against gitbay.org:
10
11- Every list and every dashboard section is the same grid under a dim
12 ALL-CAPS header row. There is no hierarchy; a header row costs a line
13 and says little (`#`, `!`, `REF`, `WHEN`).
14- Cyan marks every ref, path, SHA and feed target. A `build list` row has
15 three cyan cells, so cyan carries no information.
16- `dashboard` runs to about 70 lines: 20 builds, 7 pinned repositories,
17 8 activity rows, and the admin `Server`/`Queues` blocks. `Queues` uses
18 raw tabs, and its nested tables are padded inside the colour codes, so
19 they do not align with the parent.
20- `krz/gitbay` repeats on 15 of 20 build rows; refs keep their owner
21 prefix inside the repository they belong to.
22- State reads three ways: `✓ 2/2` on `repo show`, coloured words on
23 `build list`, an uncoloured `running`.
24- Nothing says what to run next, apart from a `more:` line that repeats
25 the command with an opaque cursor.
26
27The design bar is magit's status buffer: a header block of `Label:` lines,
28counted section headings, short dim refs on the left, and a menu of the
29actions that apply right now.
30
31## Constraints
32
33- Piped output and `--json` are byte-identical before and after.
34- Rendering stays server-side in `internal/control` behind `--term`, so
35 stock `ssh` with `--term=<cols>,color` gets the same screens as the CLI.
36 No formatting moves into `cmd/gitbay`.
37- Colour is never the only signal. `NO_COLOR`, `--no-color` and
38 `TERM=dumb` drop colour and keep glyphs and layout.
39- Diffs, `build log` and blobs are streams, not screens. They keep the
40 v1.41.0 rendering; only their header lines change.
41
42## Visual rules
43
44A screen has up to four parts, in order, each separated by one blank line:
45a header block, a body, sections, and an action legend. Sections are
46separated from each other by one blank line.
47
48**Header block.** Aligned `Label:` lines. Labels are dim and padded to the
49longest label in the block; values are normal weight. The first line names
50the object: `Merge: !552 wire $PAGER through long views`.
51
52**Body.** Markup text (an issue or MR description, release notes), rendered
53through `termtext` as `view.body` renders it today.
54
55**Section heading.** `Title (n)`, bold blue. `n` is the total, not the
56number of rows shown. An empty section is omitted unless the screen marks
57it as worth showing empty (`Discussion (0)`).
58
59**Rows.** No header row, no indent. Column order: ref, glyph, text,
60metadata.
61
62- Ref: the short form (`!549`, `#12`, `8f3a1c2`, `1779`), dim,
63 left-aligned. The owner prefix is dropped when the screen already names
64 the repository.
65- Text: normal weight. The title column keeps the existing one-third-width
66 cap for sparse columns.
67- Metadata: dim, after the text, joined by ` · `.
68- A section capped by its screen ends with a dim
69 `+12 more gitbay build list`.
70
71**Colour.** Follows the stylesheet's two accents.
72
73| Role | Colour | Used for |
74|---|---|---|
75| What you can do | blue | section headings, legend commands |
76| What wants you | yellow (`--warn` under truecolor) | review requested, assigned to you, behind, needs approval |
77| State | green / red / none | passed, open, signed / failed, blocked / finished, neutral |
78
79Nothing else is coloured. Refs and paths lose their cyan.
80
81**Glyphs.** First in a row or field, one meaning each, everywhere:
82
83| Glyph | Meaning |
84|---|---|
85| `✓` | passed, ok |
86| `✗` | failed, blocked |
87| `◐` | running, pending |
88| `●` | waiting on you |
89| `○` | closed, draft |
90
91**Legend.** A rule line, then up to three columns of action groups. Each
92group is a bold name over its commands, blue. Below 80 columns the groups
93stack in one column. Commands omit `<owner/name>` when it equals the repository the CLI
94inferred from its clone, which the CLI sends as `here=<owner/name>` in
95`--term` (an older server ignores the option). Commands may name
96CLI-local commands (`mr rebase`, `mr checkout`). There is no `browse`:
97the header's first field links to the page where the terminal shows
98links, and a `URL:` field carries it otherwise.
99
100## Model
101
102The existing `view` (`internal/control/view.go`) is an imperative writer
103used by 18 files; it renders both piped and terminal output, branching on
104`Term.Cols == 0`. It stays as the plain writer. The declarative model is a
105new type, `screen`, in `internal/control/screen.go`:
106
107```go
108type screen struct {
109 fields []field
110 body string // markup source
111 format string // body's markup format
112 sections []section
113 actions []action
114}
115
116type field struct {
117 label string
118 value []cell
119}
120
121type section struct {
122 title string
123 n int // total, shown as "(n)"
124 rows []row // cells, plus a markup body drawn beneath (a comment)
125 more []string // command for the rest, when n > len(rows)
126 empty bool // draw "(0)" rather than omit
127}
128
129type row struct {
130 cells []cell
131 body string
132 format string
133}
134
135type action struct {
136 group string
137 argv []string
138}
139```
140
141Rows use the existing typed cells (`cRef`, `cAge`, `cSize`, `cSwatch`,
142...) plus a new `cGlyph(state)`.
143
144A migrated command calls
145
146```go
147c.emitView(data, plain, func() screen { ... })
148```
149
150Under `--json` it encodes `data`; piped, it runs `plain`; at a terminal it
151builds the screen and renders it. `emitPage` gains the same form; at a
152terminal its cursor becomes a final `Next page` action carrying the full
153command, replacing the `more:` line.
154
155The width logic in `table.flush` (`fit`, `capSparse`, `dropEmpty`) moves
156into the section renderer. At a terminal, `table` renders as one untitled
157section, so lists that have not migrated take the new row style in stage 1. Such a
158table keeps a dim header row only when a column is a number or size
159(`admin runners`, `admin stats`), which is unreadable without one.
160Piped `table` output is unchanged. `view`'s terminal branches are removed
161once no command reaches them.
162
163Actions are data. Each command chooses them from the state it just read:
164behind the target offers `mr rebase`, a requested reviewer gets
165`mr approve`, `mr merge` appears only when the merge gates pass, a caller
166without write access gets no `issue close`. The renderer never fails a
167command; a field without data is omitted.
168
169## Screens (stage 2)
170
171`--json` for each is unchanged.
172
173**`dashboard`**
174
175- Header: `User`, `Instance`. For an admin, an `Instance` problem line
176 (`✗ 1 mirror error`) when one exists. `Server` and `Queues` leave the
177 terminal screen; `admin stats` carries them.
178- Sections: `Review requested`, `Assigned issues`, `Your merge requests`,
179 `Failed builds` (last 24h, repositories the viewer can write),
180 `Recent activity` (5), `Pinned` (refs on one line). Passing builds
181 collapse to one dim line under activity: `14 builds passed today`.
182- Legend: the command for the first item of each non-empty section,
183 `feed`, `build list`, and `admin stats` for an admin.
184
185**`mr show`**
186
187- Header: `Merge`, `State`, `Checks`, `Review`, `Gates` (each
188 `MergeGates` gate as a glyph).
189- Body: description.
190- Sections: `Commits` (SHA, subject), `Files` with `+a −d`,
191 `Discussion` (each comment as who · age with its body beneath; shown
192 when empty).
193- Legend: Unblock (`mr rebase` when behind), Review (`mr review
194 --approve`, `mr comment`), Merge (when the gates pass), Read
195 (`mr diff`).
196
197**`issue show`**
198
199- Header: `Issue`, `State`, `Labels` (swatches), `Assignee`,
200 `Milestone`, `Linked` (merge requests that close it).
201- Body: description.
202- Sections: `Discussion` as on `mr show`.
203- Legend: comment, assign, label, close or reopen, filtered by permission.
204
205**`repo show`**
206
207- Header: `Repo`, `Clone`, `Head` (`main ✓ 2/2`), `Release`, `Mirror`
208 (only on error).
209- Body: description and topics.
210- Sections: `Open merge requests`, `Open issues`, `Recent commits` (5).
211- Legend: `mr create`, `issue create`, `repo log`.
212
213**`build show`**
214
215- Header: `Build`, `State`, `Commit`, `Ref`, `MR`, `Duration`.
216- Sections: `Steps` (glyph, name, duration; the failed step red).
217- Legend: `build log`.
218
219**`mr list`, `issue list`, `build list`**
220
221- One section each (`Open merge requests (n)`, ...).
222- Legend: `create`, the state filter not in use, `Next page`.
223
224## Stages
225
226Each stage is tracked by its own issue under one tracking issue, and lands
227as merge requests in order.
228
2291. `screen.go`, the renderer, `emitView`, `cGlyph`; `table` rendered as an
230 untitled section. Rewrite the Users wiki "Output rules" section.
2312. The stage-2 screens: one MR for the show screens, one for the lists and
232 `dashboard`.
2333. Every other `c.table` and `c.view` caller, file by file, starting with
234 `profile`, `release`, `milestone`, `org`, `label`. Remove `view`'s
235 terminal branches.
236
237Out of scope: a branch-aware `gitbay status` (needs the CLI to send the
238local branch), interactive input of any kind, and Emacs integration.
239
240## Testing
241
242- **Renderer goldens** (`screen_test.go`) at 80 and 120 columns, colour on
243 and off. Every golden asserts that `stripSGR` of the colour render equals the colourless render, so
244 colour never carries structure alone.
245- **Screens as data.** Per-command tests assert on the `screen` value, not
246 rendered text: sections present, counts, which actions are offered for a
247 given state and caller.
248- **Legend commands resolve.** A test passes every action argv that any
249 screen produces in its fixtures through the registry's `Lookup` and
250 `parseFlags`. A renamed command or flag fails CI.
251- **Plain output pinned.** Before a command migrates, its piped output is
252 captured as a golden; the migration MR reproduces it byte for byte.
253 `--json` stays covered by the existing tests.
254
255## Compatibility
256
257All changes are server-side behind `--term`. Any CLI from v1.41.0 gets the
258new screens when the instance upgrades; older CLIs that send no `--term`
259get plain output as before. Scripts see no change.