Commit 0c0ae8e08b
Verified · cmc
Layout: unified · split
internal/control/table.go +29 −2
| @@ -18,6 +18,8 @@ const ( | |||
| 18 | kindNum | 18 | kindNum |
| 19 | kindSize | 19 | kindSize |
| 20 | kindSwatch | 20 | kindSwatch |
| 21 | kindGlyph | ||
| 22 | kindMeta | ||
| 21 | ) | 23 | ) |
| 22 | 24 | ||
| 23 | // cell is one column of a table row. The kind decides colour, time | 25 | // cell is one column of a table row. The kind decides colour, time |
| @@ -49,6 +51,27 @@ func cSwatch(hex string) cell { return cell{kind: kindSwatch, s: hex} } | |||
| 49 | // its word: "2 failed" red, "review requested" yellow. | 51 | // its word: "2 failed" red, "review requested" yellow. |
| 50 | func cMark(s, sgr string) cell { return cell{kind: kindText, s: s, sgr: sgr} } | 52 | func cMark(s, sgr string) cell { return cell{kind: kindText, s: s, sgr: sgr} } |
| 51 | 53 | ||
| 54 | // cGlyph is a state's mark, coloured for the state. | ||
| 55 | func cGlyph(state string) cell { | ||
| 56 | g, sgr := glyph(state) | ||
| 57 | return cell{kind: kindGlyph, s: g, sgr: sgr} | ||
| 58 | } | ||
| 59 | |||
| 60 | // cYou is the mark for a row that waits on the viewer. | ||
| 61 | func cYou() cell { return cell{kind: kindGlyph, s: "●", sgr: sgrYellow} } | ||
| 62 | |||
| 63 | // cMeta is a row's trailing facts, dim and joined by " · ". Empty parts | ||
| 64 | // are skipped. | ||
| 65 | func cMeta(parts ...string) cell { | ||
| 66 | var keep []string | ||
| 67 | for _, p := range parts { | ||
| 68 | if p != "" { | ||
| 69 | keep = append(keep, p) | ||
| 70 | } | ||
| 71 | } | ||
| 72 | return cell{kind: kindMeta, s: strings.Join(keep, " · ")} | ||
| 73 | } | ||
| 74 | |||
| 52 | // table is a list command's rows. Plain, each row is written as it | 75 | // table is a list command's rows. Plain, each row is written as it |
| 53 | // comes, tab-separated with no header. At a terminal rows are held | 76 | // comes, tab-separated with no header. At a terminal rows are held |
| 54 | // until flush, then written under a header, padded, and fitted to the | 77 | // until flush, then written under a header, padded, and fitted to the |
| @@ -233,7 +256,7 @@ func (t *table) fit(widths []int) { | |||
| 233 | } | 256 | } |
| 234 | } | 257 | } |
| 235 | for i := len(kinds) - 1; i >= 0; i-- { | 258 | for i := len(kinds) - 1; i >= 0; i-- { |
| 236 | if kinds[i] == kindText { | 259 | if kinds[i] == kindText || kinds[i] == kindMeta { |
| 237 | shrink(i) | 260 | shrink(i) |
| 238 | } | 261 | } |
| 239 | } | 262 | } |
| @@ -272,7 +295,11 @@ func (t *table) joinRow(r []cell, line []string, widths []int) string { | |||
| 272 | case kindState: | 295 | case kindState: |
| 273 | s = t.term.paintState(s) | 296 | s = t.term.paintState(s) |
| 274 | case kindRef: | 297 | case kindRef: |
| 275 | s = t.term.link(r[i].url, t.term.paint(sgrCyan, s)) | 298 | s = t.term.link(r[i].url, t.term.paint(sgrDim, s)) |
| 299 | case kindGlyph: | ||
| 300 | s = t.term.paint(r[i].sgr, s) | ||
| 301 | case kindMeta: | ||
| 302 | s = t.term.paint(sgrDim, s) | ||
| 276 | case kindSwatch: | 303 | case kindSwatch: |
| 277 | s = t.term.swatch(s) | 304 | s = t.term.swatch(s) |
| 278 | default: | 305 | default: |
internal/control/table_test.go +27
| @@ -211,3 +211,30 @@ func TestTableSwatchAligned(t *testing.T) { | |||
| 211 | t.Errorf("plain = %q", plain.String()) | 211 | t.Errorf("plain = %q", plain.String()) |
| 212 | } | 212 | } |
| 213 | } | 213 | } |
| 214 | |||
| 215 | func TestRefsDimNotCyan(t *testing.T) { | ||
| 216 | var b bytes.Buffer | ||
| 217 | tb := (&Ctx{Term: Term{Cols: 80, Color: true}}).table(&b, "#", "TITLE") | ||
| 218 | tb.row(cRef("#12"), cFlex("Android app")) | ||
| 219 | tb.flush() | ||
| 220 | if strings.Contains(b.String(), sgrCyan) { | ||
| 221 | t.Errorf("ref painted cyan: %q", b.String()) | ||
| 222 | } | ||
| 223 | if !strings.Contains(b.String(), sgrDim+"#12"+sgrReset) { | ||
| 224 | t.Errorf("ref not dim: %q", b.String()) | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | func TestGlyphAndMetaCells(t *testing.T) { | ||
| 229 | var b bytes.Buffer | ||
| 230 | tb := (&Ctx{Term: Term{Cols: 80, Color: true}}).table(&b) | ||
| 231 | tb.row(cRef("1779"), cGlyph("failure"), cFlex("test"), cMeta("gitbay", "2h")) | ||
| 232 | tb.row(cRef("1780"), cYou(), cFlex("review"), cMeta("", "3h")) | ||
| 233 | tb.flush() | ||
| 234 | out := b.String() | ||
| 235 | for _, want := range []string{sgrRed + "✗" + sgrReset, sgrYellow + "●" + sgrReset, sgrDim + "gitbay · 2h" + sgrReset, sgrDim + "3h" + sgrReset} { | ||
| 236 | if !strings.Contains(out, want) { | ||
| 237 | t.Errorf("missing %q in %q", want, out) | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
internal/control/term.go +21
| @@ -56,6 +56,7 @@ const ( | |||
| 56 | sgrRed = "\x1b[31m" | 56 | sgrRed = "\x1b[31m" |
| 57 | sgrGreen = "\x1b[32m" | 57 | sgrGreen = "\x1b[32m" |
| 58 | sgrYellow = "\x1b[33m" | 58 | sgrYellow = "\x1b[33m" |
| 59 | sgrBlue = "\x1b[34m" | ||
| 59 | sgrMagenta = "\x1b[35m" | 60 | sgrMagenta = "\x1b[35m" |
| 60 | sgrCyan = "\x1b[36m" | 61 | sgrCyan = "\x1b[36m" |
| 61 | ) | 62 | ) |
| @@ -121,6 +122,26 @@ func stateColor(s string) string { | |||
| 121 | return "" | 122 | return "" |
| 122 | } | 123 | } |
| 123 | 124 | ||
| 125 | // glyph is the mark a screen puts first in a row or field for a state | ||
| 126 | // word, and its colour: ✓ passed, ✗ failed or blocked, ◐ still going, | ||
| 127 | // ○ closed or draft. A state with no mark ("open") returns "". | ||
| 128 | func glyph(state string) (g, sgr string) { | ||
| 129 | switch state { | ||
| 130 | case "merged": | ||
| 131 | return "✓", "" | ||
| 132 | case "success", "ok", "approved", "verified", "passed": | ||
| 133 | return "✓", sgrGreen | ||
| 134 | case "failure", "failed", "error", "changes requested", | ||
| 135 | "bad_signature", "signed_email_mismatch", "signed_key_expired", "signed_key_revoked": | ||
| 136 | return "✗", sgrRed | ||
| 137 | case "pending", "running", "queued": | ||
| 138 | return "◐", "" | ||
| 139 | case "closed", "draft", "canceled", "cancelled", "skipped": | ||
| 140 | return "○", "" | ||
| 141 | } | ||
| 142 | return "", "" | ||
| 143 | } | ||
| 144 | |||
| 124 | // link makes s a hyperlink to url when the terminal shows them (OSC 8). | 145 | // link makes s a hyperlink to url when the terminal shows them (OSC 8). |
| 125 | // A url with a control byte in it is left out rather than sent. | 146 | // A url with a control byte in it is left out rather than sent. |
| 126 | func (t Term) link(url, s string) string { | 147 | func (t Term) link(url, s string) string { |
internal/control/term_test.go +22
| @@ -219,3 +219,25 @@ func TestLinkAndSwatch(t *testing.T) { | |||
| 219 | t.Errorf("bad hex = %q", got) | 219 | t.Errorf("bad hex = %q", got) |
| 220 | } | 220 | } |
| 221 | } | 221 | } |
| 222 | |||
| 223 | func TestGlyph(t *testing.T) { | ||
| 224 | for _, c := range []struct{ state, g, sgr string }{ | ||
| 225 | {"success", "✓", sgrGreen}, | ||
| 226 | {"approved", "✓", sgrGreen}, | ||
| 227 | {"merged", "✓", ""}, | ||
| 228 | {"failure", "✗", sgrRed}, | ||
| 229 | {"changes requested", "✗", sgrRed}, | ||
| 230 | {"signed_key_revoked", "✗", sgrRed}, | ||
| 231 | {"running", "◐", ""}, | ||
| 232 | {"pending", "◐", ""}, | ||
| 233 | {"closed", "○", ""}, | ||
| 234 | {"draft", "○", ""}, | ||
| 235 | {"open", "", ""}, | ||
| 236 | {"", "", ""}, | ||
| 237 | } { | ||
| 238 | g, sgr := glyph(c.state) | ||
| 239 | if g != c.g || sgr != c.sgr { | ||
| 240 | t.Errorf("glyph(%q) = %q %q, want %q %q", c.state, g, sgr, c.g, c.sgr) | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
internal/control/view.go +2 −2
| @@ -96,14 +96,14 @@ func (v *view) title(ref, title, state string) { | |||
| 96 | rest += state | 96 | rest += state |
| 97 | } | 97 | } |
| 98 | if rest == "" { | 98 | if rest == "" { |
| 99 | io.WriteString(v.w, t.paint(sgrCyan, ref)+"\n") | 99 | io.WriteString(v.w, t.paint(sgrDim, ref)+"\n") |
| 100 | return | 100 | return |
| 101 | } | 101 | } |
| 102 | lines := termtext.Wrap(rest, t.Cols-cells(prefix)) | 102 | lines := termtext.Wrap(rest, t.Cols-cells(prefix)) |
| 103 | for i, line := range lines { | 103 | for i, line := range lines { |
| 104 | p := indent | 104 | p := indent |
| 105 | if i == 0 { | 105 | if i == 0 { |
| 106 | p = t.paint(sgrCyan, ref) + " " | 106 | p = t.paint(sgrDim, ref) + " " |
| 107 | } | 107 | } |
| 108 | if i < len(lines)-1 || state == "" { | 108 | if i < len(lines)-1 || state == "" { |
| 109 | io.WriteString(v.w, p+t.paint(sgrBold, line)+"\n") | 109 | io.WriteString(v.w, p+t.paint(sgrBold, line)+"\n") |