Commit ee26be756c

ee26be756c97c0eff7fa956370cd1af97c350939

parent: a136534ff9

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-02 02:03 UTC

docs/specs: CLI views design

Layout: unified · split

docs/specs/2026-10-01-cli-views-design.md added +248
@@ -0,0 +1,248 @@
1# CLI views
2
3Status: proposed, 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 the screen is for
94the repository the CLI would infer, so each line is what the user types.
95
96## Model
97
98The existing `view` (`internal/control/view.go`) is an imperative writer
99used by 18 files; it renders both piped and terminal output, branching on
100`Term.Cols == 0`. It stays as the plain writer. The declarative model is a
101new type, `screen`, in `internal/control/screen.go`:
102
103```go
104type screen struct {
105 fields []field
106 body string // markup source
107 format string // body's markup format
108 sections []section
109 actions []action
110}
111
112type field struct {
113 label string
114 value []cell
115}
116
117type section struct {
118 title string
119 n int // total, shown as "(n)"
120 rows [][]cell
121 more string // command for the rest, when n > len(rows)
122 empty bool // draw "(0)" rather than omit
123}
124
125type action struct {
126 group string
127 label string
128 argv []string
129}
130```
131
132Rows use the existing typed cells (`cRef`, `cAge`, `cSize`, `cSwatch`,
133...) plus a new `cGlyph(state)`.
134
135A migrated command calls
136
137```go
138c.emitView(data, plain, func() screen { ... })
139```
140
141Under `--json` it encodes `data`; piped, it runs `plain`; at a terminal it
142builds the screen and renders it. `emitPage` gains the same form; at a
143terminal its cursor becomes a final `Next page` action carrying the full
144command, replacing the `more:` line.
145
146The width logic in `table.flush` (`fit`, `capSparse`, `dropEmpty`) moves
147into the section renderer. At a terminal, `table` renders as one untitled
148section, so lists that have not migrated take the new row style in stage 1.
149Piped `table` output is unchanged. `view`'s terminal branches are removed
150once no command reaches them.
151
152Actions are data. Each command chooses them from the state it just read:
153behind the target offers `mr rebase`, a requested reviewer gets
154`mr approve`, `mr merge` appears only when the merge gates pass, a caller
155without write access gets no `issue close`. The renderer never fails a
156command; a field without data is omitted.
157
158## Screens (stage 2)
159
160`--json` for each is unchanged.
161
162**`dashboard`**
163
164- Header: `User`, `Instance`. For an admin, an `Instance` problem line
165 (`✗ 1 mirror error`) when one exists. `Server` and `Queues` leave the
166 terminal screen; `admin stats` carries them.
167- Sections: `Review requested`, `Assigned issues`, `Your merge requests`,
168 `Failed builds` (last 24h, repositories the viewer can write),
169 `Recent activity` (5), `Pinned` (refs on one line). Passing builds
170 collapse to one dim line under activity: `14 builds passed today`.
171- Legend: the command for the first item of each non-empty section,
172 `feed`, `build list`, and `admin stats` for an admin.
173
174**`mr show`**
175
176- Header: `Merge`, `State`, `Checks`, `Review`, `Gates` (each
177 `MergeGates` gate as a glyph).
178- Body: description.
179- Sections: `Commits` (SHA, signature glyph, subject), `Files` with
180 `+a −d`, `Discussion` (threads as who · age and the first line;
181 suggestions marked; shown when empty).
182- Legend: Unblock (`mr rebase`; `mr checkout` on a conflict), Review (approve, comment,
183 `apply-suggestion` when there are suggestions), Merge (when the gates
184 pass), Read (`mr diff`, `browse`).
185
186**`issue show`**
187
188- Header: `Issue`, `State`, `Labels` (swatches), `Assignee`,
189 `Milestone`, `Linked` (merge requests that close it).
190- Body: description.
191- Sections: `Discussion`.
192- Legend: comment, assign, label, close or reopen, filtered by permission.
193
194**`repo show`**
195
196- Header: `Repo`, `Clone`, `Head` (`main ✓ 2/2`), `Release`, `Mirror`
197 (only on error).
198- Body: description and topics.
199- Sections: `Open merge requests`, `Open issues`, `Recent commits` (5).
200- Legend: `mr create`, `issue create`, `repo log`, `browse`.
201
202**`build show`**
203
204- Header: `Build`, `State`, `Commit`, `Ref`, `MR`, `Duration`.
205- Sections: `Steps` (glyph, name, duration; the failed step red).
206- Legend: `build log`.
207
208**`mr list`, `issue list`, `build list`**
209
210- One section each (`Open merge requests (n)`, ...).
211- Legend: `create`, the state filter not in use, `Next page`.
212
213## Stages
214
215Each stage is tracked by its own issue under one tracking issue, and lands
216as merge requests in order.
217
2181. `screen.go`, the renderer, `emitView`, `cGlyph`; `table` rendered as an
219 untitled section. Rewrite the Users wiki "Output rules" section.
2202. The stage-2 screens: one MR for the show screens, one for the lists and
221 `dashboard`.
2223. Every other `c.table` and `c.view` caller, file by file, starting with
223 `profile`, `release`, `milestone`, `org`, `label`. Remove `view`'s
224 terminal branches.
225
226Out of scope: a branch-aware `gitbay status` (needs the CLI to send the
227local branch), interactive input of any kind, and Emacs integration.
228
229## Testing
230
231- **Renderer goldens** (`screen_test.go`) at 80 and 120 columns, colour on
232 and off. Every golden asserts that `stripSGR` of the colour render equals the colourless render, so
233 colour never carries structure alone.
234- **Screens as data.** Per-command tests assert on the `screen` value, not
235 rendered text: sections present, counts, which actions are offered for a
236 given state and caller.
237- **Legend commands resolve.** A test passes every action argv that any
238 screen produces in its fixtures through the registry's `Lookup` and
239 `parseFlags`. A renamed command or flag fails CI.
240- **Plain output pinned.** Before a command migrates, its piped output is
241 captured as a golden; the migration MR reproduces it byte for byte.
242 `--json` stays covered by the existing tests.
243
244## Compatibility
245
246All changes are server-side behind `--term`. Any CLI from v1.41.0 gets the
247new screens when the instance upgrades; older CLIs that send no `--term`
248get plain output as before. Scripts see no change.