docs/specs/2026-10-01-cli-terminal-output-design.md

main
gitbay/docs/specs/2026-10-01-cli-terminal-output-design.md rendered · source · history · blame · raw

189 lines · 11392 bytes

12 symbols in this file
  1# CLI terminal output
  2
  3Status: accepted, 2026-10-01. Findings from a sweep of 51 read-only commands
  4against gitbay.org at a 110×50 colour terminal (`TERM=xterm-256color`),
  5plus error paths and piped output.
  6
  7## How output works today
  8
  9The CLI sends `--term=<cols>[,color]` as the first server argument when
 10stdout is a terminal. The server renders everything: `table` in
 11`internal/control/table.go` (header, padding, fit-to-width, relative
 12ages, colour on `kindState` cells), `view` in `view.go` (title line,
 13aligned fields, rendered body, events, comments), helpers in `term.go`.
 14Piped output is tab-separated rows with no header; `--json` is the
 15contract. Both are correct and stay unchanged.
 16
 17The terminal mode exists but is thin. Colour applies only to state cells
 18whose word appears in `stateColor`'s fixed list, and to dim headers and
 19bold 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
 78Things that work and should stay: column fitting with `…`, relative
 79ages, `more: gitbay … --cursor` on stderr, pager for show/diff/log,
 80`nothing to list` on stderr, `mr show` checks sub-table, exit codes and
 81the not-found/usage messages.
 82
 83## Plan
 84
 85One issue per phase, one MR each, merged in order.
 86
 87### Phase 1: renderer foundations (`term.go`, `table.go`, `view.go`)
 88
 891. 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`.
 962. Paint `kindRef` cells cyan (R2).
 973. `relAge` handles the future: `in 5h`, `in 3w`, a date past 8 weeks
 98   (R3). Convert remaining timestamp `cText` cells to `cAge` (R4).
 994. New cell kinds `cSize(bytes)` and `cDur(seconds)`; terminal renders
100   `3.0 MiB` / `53m43s`, plain keeps the number (R5, R6).
1015. 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).
1046. A flex column blank on more than half its rows is capped at a third
105   of the terminal width (R8).
1067. One `section()` style: Title Case, bold, no colon, in every view;
107   empty sections are skipped everywhere (R11).
1088. 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
1129. `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
116Tests: `table_test.go` and `term_test.go` cases per kind and role;
117plain output stays covered by the existing plain-mode table tests,
118since every change above is gated on `Term.Cols`.
119
120### Phase 2: diffs and logs
121
1221. 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`.
1262. `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)**.
1303. `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
135Each item adds columns or fields only at a terminal unless noted; plain
136columns stay as they are.
137
1381. `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`.
1432. `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.
1463. `issue list`: LABELS (first two, `+n`), ASSIGNEE, comment count,
147   UPDATED age.
1484. `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.
1515. `repo log`: AUTHOR as name only, an AGE column, signature coloured.
1526. `repo refs`: `SortVersions`, newest first, default branch marked
153   **(plain: order only)**.
1547. `repo tree`: NAME first, directories cyan, SIZE humanized, SHA last.
1558. `release list`: DATE column, `10 assets`; `release show`: sizes
156   humanized, SHA-256 cut to 12 at a terminal.
1579. `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.
16010. `feed`/activity: colour the outcome word and refs.
16111. `milestone list`: blank due instead of `due -`, progress as
162    `1/2` plus a percentage.
16312. `audit`: DATA rendered as `key=value` pairs at a terminal.
16413. `auth keys list`, `web sessions list`: mark the current key or
165    session with `*` (as `mr revisions` does).
16614. `auth whoami`: at a terminal, add instance, key label and scope.
16715. Mutations: a second line with the web URL for anything with a page
168    (issue, MR, release, repo, snippet, wiki page).
16916. `admin runners`: full fingerprints, consistent with the other lists.
170
171### Phase 4: terminal capabilities
172
1731. 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.
1762. Glyphs for compact columns (`✓ ✗ ●`), words kept everywhere else.
177
178## Decisions (2026-10-01)
179
1801. Yellow (`warn`) marks only what needs the viewer's action. `private`
181   is red.
1822. R8: a mostly-blank flex column is capped.
1833. 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`.
1884. Per-row data for `mr list` / `issue list` comes from one batch
189   query per page in `internal/store`.