docs/specs/2026-10-01-cli-views-design.md

v1.43.0
gitbay/docs/specs/2026-10-01-cli-views-design.md rendered · source · history · blame · raw

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.