Commit 791af1d907
Verified · cmc
Layout: unified · split
docs/specs/2026-10-01-cli-terminal-output-design.md added +189
| @@ -0,0 +1,189 @@ | ||
| 1 | # CLI terminal output | |
| 2 | ||
| 3 | Status: accepted, 2026-10-01. Findings from a sweep of 51 read-only commands | |
| 4 | against gitbay.org at a 110×50 colour terminal (`TERM=xterm-256color`), | |
| 5 | plus error paths and piped output. | |
| 6 | ||
| 7 | ## How output works today | |
| 8 | ||
| 9 | The CLI sends `--term=<cols>[,color]` as the first server argument when | |
| 10 | stdout is a terminal. The server renders everything: `table` in | |
| 11 | `internal/control/table.go` (header, padding, fit-to-width, relative | |
| 12 | ages, colour on `kindState` cells), `view` in `view.go` (title line, | |
| 13 | aligned fields, rendered body, events, comments), helpers in `term.go`. | |
| 14 | Piped output is tab-separated rows with no header; `--json` is the | |
| 15 | contract. Both are correct and stay unchanged. | |
| 16 | ||
| 17 | The terminal mode exists but is thin. Colour applies only to state cells | |
| 18 | whose word appears in `stateColor`'s fixed list, and to dim headers and | |
| 19 | bold titles. Everything else renders as uncoloured text. | |
| 20 | ||
| 21 | ## Constraints | |
| 22 | ||
| 23 | - Plain and `--json` output are byte-identical before and after, except | |
| 24 | for the bug fixes marked **(plain)** below. | |
| 25 | - All rendering stays server-side in `internal/control`, so stock | |
| 26 | `ssh git@gitbay.org --term=120,color repo list` gets the same result | |
| 27 | as the CLI. No formatting in `cmd/gitbay`. | |
| 28 | - Colour is never the only signal: every coloured cell keeps its word. | |
| 29 | `NO_COLOR`, `TERM=dumb`, `--no-color` keep working. | |
| 30 | - Colour roles follow the stylesheet: green ok, magenta done, red bad, | |
| 31 | dim neutral, yellow for what wants the viewer (the web's orange), | |
| 32 | blue/cyan for refs (the web's accent, "what you can act on"). | |
| 33 | ||
| 34 | ## Findings | |
| 35 | ||
| 36 | ### Renderer-wide | |
| 37 | ||
| 38 | | # | Finding | Evidence | | |
| 39 | |---|---|---| | |
| 40 | | R1 | Only words in `stateColor`'s list are coloured. `public`/`private`, `verified`/`unsigned`, `full`/`runner` scopes, `admin`, `[archived]`, `primary` print plain. | `repo list`, `repo log`, `auth keys list`, `admin user list` | | |
| 41 | | R2 | `kindRef` exists but is never painted; refs (`krz/gitbay#12`, `!546`, SHAs, branch names) look like prose. | every list | | |
| 42 | | R3 | Future timestamps: `relAge` clamps negative durations to zero. Session expiry renders "until just now". | `web sessions list` | | |
| 43 | | R4 | Some timestamp columns are `cText`, so they stay RFC3339 at a terminal. | `auth token list` EXPIRES | | |
| 44 | | R5 | Durations in raw seconds. | `admin runners`: `wait avg 3223s`, `wait max 12886s` | | |
| 45 | | R6 | Sizes in raw bytes where `admin stats` already humanizes. | `repo tree`, `release show` assets (`3177346`) | | |
| 46 | | R7 | An optional trailing cell (`[archived]`, `primary`) adds an unnamed column: the header and every row get trailing whitespace, and the header has a blank title. | `repo list`, `auth email list` | | |
| 47 | | R8 | A flex column that is blank on most rows still takes its maximum width. | `release list`: 90-cell TITLE column, blank on every visible row | | |
| 48 | | R9 | Diffs are uncoloured. | `repo commit`, `mr diff`, `repo diff`, `mr range-diff` | | |
| 49 | | R10 | Errors print as bare stderr lines with no visual distinction; an unknown flag gets no suggestion; list-command usage is a single 200-character line. | `issue list --stat open` | | |
| 50 | | R11 | Section headings are inconsistent: `waiting on your review:` (lowercase, colon) on the dashboard; `link`/`org`/`repo` (singular) on `profile show`; `mirror` on `repo show`. The dashboard prints `none` under empty sections; other views skip them. | `dashboard`, `profile show`, `repo show` | | |
| 51 | ||
| 52 | ### Per command | |
| 53 | ||
| 54 | | Command | Finding | | |
| 55 | |---|---| | |
| 56 | | `dashboard` | Empty sections ("waiting on your review: none") come first. No summary of what needs the viewer. A failing build looks the same as the other rows apart from its state word. Recent activity repeats `feed` (19 rows). | | |
| 57 | | `mr list` | No draft marker, checks result, review state, mergeability or age, so the open-MR list cannot answer "what do I do next". | | |
| 58 | | `issue list` | No labels, assignee, comment count or updated age. | | |
| 59 | | `repo show` | The description renders as a bold title, and `public` has no colour. No clone URL, open issue/MR counts, latest release or last build. | | |
| 60 | | `repo log` | Author as `(Name <email>)` takes a third of the width. No age column. Signature state is uncoloured, so an unsigned commit does not stand out. | | |
| 61 | | `repo refs` | **(plain)** Tags sort lexically (`v1.10.0` before `v1.2.0`), oldest first; `gitutil.SortVersions` already does this for the web. The default branch is not marked. | | |
| 62 | | `repo tree` | The SHA column comes first and is rarely wanted. Directories are not distinguished except by `/` and `-`. | | |
| 63 | | `repo commit` | **(plain)** The subject prints twice: `message` is everything after the first blank line of the payload, which is the whole message. The signature state and checks are computed and included in the JSON but not printed. The date is RFC3339. | | |
| 64 | | `release list` | No date. `10 asset(s)`. See R8. | | |
| 65 | | `release show` | Byte sizes; full 64-hex SHA-256 per asset. | | |
| 66 | | `label list` | Colour shown as hex text. A label with no colour leaves the cell blank. | | |
| 67 | | `milestone list` | `due -` when there is no due date; progress as text. | | |
| 68 | | `build show` | No steps, failed step, or pointer to `build log`. No link to the MR it belongs to. | | |
| 69 | | `build log` | `$ step` lines are not distinguished from output. | | |
| 70 | | `feed`, dashboard activity | Event sentences are uncoloured: `build success` and `build failure` read the same at a glance. | | |
| 71 | | `audit` | DATA column is raw JSON. | | |
| 72 | | `admin runners` | Fingerprints truncated (`SHA256:b5HGR…`) while `auth keys list` and `repo runner list` print them in full. | | |
| 73 | | `auth keys list`, `web sessions list` | The key or session in use is not marked. | | |
| 74 | | `auth whoami` | Prints the username only; at a terminal it could add instance, key label and scope. | | |
| 75 | | `profile show` | The README heading repeats the tagline already on the title line. | | |
| 76 | | mutations | `created krz/gitbay#312` with no URL; the output rules allow a second line for something to copy. | | |
| 77 | ||
| 78 | Things that work and should stay: column fitting with `…`, relative | |
| 79 | ages, `more: gitbay … --cursor` on stderr, pager for show/diff/log, | |
| 80 | `nothing to list` on stderr, `mr show` checks sub-table, exit codes and | |
| 81 | the not-found/usage messages. | |
| 82 | ||
| 83 | ## Plan | |
| 84 | ||
| 85 | One issue per phase, one MR each, merged in order. | |
| 86 | ||
| 87 | ### Phase 1: renderer foundations (`term.go`, `table.go`, `view.go`) | |
| 88 | ||
| 89 | 1. Colour roles. Replace `stateColor`'s word switch with a role table | |
| 90 | (`ok`, `done`, `bad`, `neutral`, `warn`, `ref`) and add the missing | |
| 91 | words: visibility (`private` bad, `public` default), signature | |
| 92 | states from `internal/sig` (`verified` ok, `unsigned` and | |
| 93 | `signed_unknown_key` neutral, the other `signed_*` and | |
| 94 | `bad_signature` bad), `archived`, `primary`, | |
| 95 | `admin`, scopes. Add `sgrYellow`, `sgrCyan`. | |
| 96 | 2. Paint `kindRef` cells cyan (R2). | |
| 97 | 3. `relAge` handles the future: `in 5h`, `in 3w`, a date past 8 weeks | |
| 98 | (R3). Convert remaining timestamp `cText` cells to `cAge` (R4). | |
| 99 | 4. New cell kinds `cSize(bytes)` and `cDur(seconds)`; terminal renders | |
| 100 | `3.0 MiB` / `53m43s`, plain keeps the number (R5, R6). | |
| 101 | 5. Optional trailing cells move into a named column or merge into the | |
| 102 | state cell (`public, archived`); `join` never pads the last column | |
| 103 | and trims trailing spaces (R7). | |
| 104 | 6. A flex column blank on more than half its rows is capped at a third | |
| 105 | of the terminal width (R8). | |
| 106 | 7. One `section()` style: Title Case, bold, no colon, in every view; | |
| 107 | empty sections are skipped everywhere (R11). | |
| 108 | 8. Errors: at a terminal, `error:` prefix in red on stderr. Unknown | |
| 109 | flags get `did you mean --state?` from the registered usage (edit | |
| 110 | distance ≤ 2). Long usage wraps one alternative per line (R10). | |
| 111 | ||
| 112 | 9. `ParseTerm` reads `<cols>[,opt]...` and ignores options it does not | |
| 113 | know, so phase 4's capability tokens reach an instance that has | |
| 114 | phase 1 without turning its output plain. | |
| 115 | ||
| 116 | Tests: `table_test.go` and `term_test.go` cases per kind and role; | |
| 117 | plain output stays covered by the existing plain-mode table tests, | |
| 118 | since every change above is gated on `Term.Cols`. | |
| 119 | ||
| 120 | ### Phase 2: diffs and logs | |
| 121 | ||
| 122 | 1. A `diffPaint` helper for unified diffs at a terminal: file headers | |
| 123 | bold, `@@` cyan, `+` green, `-` red, the stat block's `+`/`-` bars | |
| 124 | coloured. Used by `repo commit`, `repo diff`, `mr diff`, | |
| 125 | `mr range-diff`. | |
| 126 | 2. `repo commit` as a view: title line with short SHA, subject and | |
| 127 | signature state; fields for author, date (`when`), signer, checks; | |
| 128 | body; coloured diff. Fix the duplicated subject in plain output | |
| 129 | **(plain)**. | |
| 130 | 3. `build log`: `$ step` lines bold at a terminal; the failing step's | |
| 131 | line red when the build failed. | |
| 132 | ||
| 133 | ### Phase 3: action-oriented content | |
| 134 | ||
| 135 | Each item adds columns or fields only at a terminal unless noted; plain | |
| 136 | columns stay as they are. | |
| 137 | ||
| 138 | 1. `dashboard`: a first line summarising what needs the viewer | |
| 139 | (`2 need you: 1 review requested, 1 failing build`, yellow); | |
| 140 | non-empty action sections first (review requested, assigned, your | |
| 141 | failing builds), then open MRs/issues, pinned, builds; activity cut | |
| 142 | to 8 rows with a pointer to `gitbay feed`. | |
| 143 | 2. `mr list`: DRAFT marker in the state cell, CHECKS (`3/3`, red on a | |
| 144 | failure), REVIEW (`approved`, `changes requested`, | |
| 145 | `review requested` in yellow when it is the viewer), UPDATED age. | |
| 146 | 3. `issue list`: LABELS (first two, `+n`), ASSIGNEE, comment count, | |
| 147 | UPDATED age. | |
| 148 | 4. `repo show`: description as a body line rather than the title; | |
| 149 | fields for clone URL (ssh), open issues/MRs, latest release, last | |
| 150 | default-branch build with state. | |
| 151 | 5. `repo log`: AUTHOR as name only, an AGE column, signature coloured. | |
| 152 | 6. `repo refs`: `SortVersions`, newest first, default branch marked | |
| 153 | **(plain: order only)**. | |
| 154 | 7. `repo tree`: NAME first, directories cyan, SIZE humanized, SHA last. | |
| 155 | 8. `release list`: DATE column, `10 assets`; `release show`: sizes | |
| 156 | humanized, SHA-256 cut to 12 at a terminal. | |
| 157 | 9. `build show`: steps with state and duration, the failing step | |
| 158 | highlighted, `gitbay build log <n>` hint on stderr, the MR it | |
| 159 | belongs to. | |
| 160 | 10. `feed`/activity: colour the outcome word and refs. | |
| 161 | 11. `milestone list`: blank due instead of `due -`, progress as | |
| 162 | `1/2` plus a percentage. | |
| 163 | 12. `audit`: DATA rendered as `key=value` pairs at a terminal. | |
| 164 | 13. `auth keys list`, `web sessions list`: mark the current key or | |
| 165 | session with `*` (as `mr revisions` does). | |
| 166 | 14. `auth whoami`: at a terminal, add instance, key label and scope. | |
| 167 | 15. Mutations: a second line with the web URL for anything with a page | |
| 168 | (issue, MR, release, repo, snippet, wiki page). | |
| 169 | 16. `admin runners`: full fingerprints, consistent with the other lists. | |
| 170 | ||
| 171 | ### Phase 4: terminal capabilities | |
| 172 | ||
| 173 | 1. Capability tokens in `--term`: `truecolor` for label swatches | |
| 174 | (`●` in the label's colour), `links` for OSC 8 hyperlinks on refs. | |
| 175 | Relies on phase 1's `ParseTerm` change being deployed first. | |
| 176 | 2. Glyphs for compact columns (`✓ ✗ ●`), words kept everywhere else. | |
| 177 | ||
| 178 | ## Decisions (2026-10-01) | |
| 179 | ||
| 180 | 1. Yellow (`warn`) marks only what needs the viewer's action. `private` | |
| 181 | is red. | |
| 182 | 2. R8: a mostly-blank flex column is capped. | |
| 183 | 3. Phase 4 is in scope. Swatches go on when the client reports | |
| 184 | `COLORTERM=truecolor` or `24bit`; links go on when the client's | |
| 185 | terminal is known to support OSC 8 (`TERM_PROGRAM` in iTerm.app, | |
| 186 | WezTerm, ghostty, vscode; `KITTY_WINDOW_ID`; `VTE_VERSION` >= 5000) | |
| 187 | or `GITBAY_LINKS=1`, and off with `GITBAY_LINKS=0`. | |
| 188 | 4. Per-row data for `mr list` / `issue list` comes from one batch | |
| 189 | query per page in `internal/store`. | |