Commit ee26be756c
Verified · cmc
Layout: unified · split
docs/specs/2026-10-01-cli-views-design.md added +248
| @@ -0,0 +1,248 @@ | ||
| 1 | # CLI views | |
| 2 | ||
| 3 | Status: proposed, 2026-10-01. Follows `2026-10-01-cli-terminal-output-design.md` | |
| 4 | (v1.41.0, #312–#315). | |
| 5 | ||
| 6 | ## Problem | |
| 7 | ||
| 8 | Terminal output after v1.41.0 is coloured and humanized but still noisy. | |
| 9 | Captured 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 | ||
| 27 | The design bar is magit's status buffer: a header block of `Label:` lines, | |
| 28 | counted section headings, short dim refs on the left, and a menu of the | |
| 29 | actions 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 | ||
| 44 | A screen has up to four parts, in order, each separated by one blank line: | |
| 45 | a header block, a body, sections, and an action legend. Sections are | |
| 46 | separated from each other by one blank line. | |
| 47 | ||
| 48 | **Header block.** Aligned `Label:` lines. Labels are dim and padded to the | |
| 49 | longest label in the block; values are normal weight. The first line names | |
| 50 | the object: `Merge: !552 wire $PAGER through long views`. | |
| 51 | ||
| 52 | **Body.** Markup text (an issue or MR description, release notes), rendered | |
| 53 | through `termtext` as `view.body` renders it today. | |
| 54 | ||
| 55 | **Section heading.** `Title (n)`, bold blue. `n` is the total, not the | |
| 56 | number of rows shown. An empty section is omitted unless the screen marks | |
| 57 | it as worth showing empty (`Discussion (0)`). | |
| 58 | ||
| 59 | **Rows.** No header row, no indent. Column order: ref, glyph, text, | |
| 60 | metadata. | |
| 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 | ||
| 79 | Nothing 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 | |
| 92 | group is a bold name over its commands, blue. Below 80 columns the groups | |
| 93 | stack in one column. Commands omit `<owner/name>` when the screen is for | |
| 94 | the repository the CLI would infer, so each line is what the user types. | |
| 95 | ||
| 96 | ## Model | |
| 97 | ||
| 98 | The existing `view` (`internal/control/view.go`) is an imperative writer | |
| 99 | used 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 | |
| 101 | new type, `screen`, in `internal/control/screen.go`: | |
| 102 | ||
| 103 | ```go | |
| 104 | type 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 | ||
| 112 | type field struct { | |
| 113 | label string | |
| 114 | value []cell | |
| 115 | } | |
| 116 | ||
| 117 | type 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 | ||
| 125 | type action struct { | |
| 126 | group string | |
| 127 | label string | |
| 128 | argv []string | |
| 129 | } | |
| 130 | ``` | |
| 131 | ||
| 132 | Rows use the existing typed cells (`cRef`, `cAge`, `cSize`, `cSwatch`, | |
| 133 | ...) plus a new `cGlyph(state)`. | |
| 134 | ||
| 135 | A migrated command calls | |
| 136 | ||
| 137 | ```go | |
| 138 | c.emitView(data, plain, func() screen { ... }) | |
| 139 | ``` | |
| 140 | ||
| 141 | Under `--json` it encodes `data`; piped, it runs `plain`; at a terminal it | |
| 142 | builds the screen and renders it. `emitPage` gains the same form; at a | |
| 143 | terminal its cursor becomes a final `Next page` action carrying the full | |
| 144 | command, replacing the `more:` line. | |
| 145 | ||
| 146 | The width logic in `table.flush` (`fit`, `capSparse`, `dropEmpty`) moves | |
| 147 | into the section renderer. At a terminal, `table` renders as one untitled | |
| 148 | section, so lists that have not migrated take the new row style in stage 1. | |
| 149 | Piped `table` output is unchanged. `view`'s terminal branches are removed | |
| 150 | once no command reaches them. | |
| 151 | ||
| 152 | Actions are data. Each command chooses them from the state it just read: | |
| 153 | behind the target offers `mr rebase`, a requested reviewer gets | |
| 154 | `mr approve`, `mr merge` appears only when the merge gates pass, a caller | |
| 155 | without write access gets no `issue close`. The renderer never fails a | |
| 156 | command; 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 | ||
| 215 | Each stage is tracked by its own issue under one tracking issue, and lands | |
| 216 | as merge requests in order. | |
| 217 | ||
| 218 | 1. `screen.go`, the renderer, `emitView`, `cGlyph`; `table` rendered as an | |
| 219 | untitled section. Rewrite the Users wiki "Output rules" section. | |
| 220 | 2. The stage-2 screens: one MR for the show screens, one for the lists and | |
| 221 | `dashboard`. | |
| 222 | 3. 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 | ||
| 226 | Out of scope: a branch-aware `gitbay status` (needs the CLI to send the | |
| 227 | local 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 | ||
| 246 | All changes are server-side behind `--term`. Any CLI from v1.41.0 gets the | |
| 247 | new screens when the instance upgrades; older CLIs that send no `--term` | |
| 248 | get plain output as before. Scripts see no change. | |