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. | ||