detached 34bf709

refactor(color): remove unused ColorCapabilities.Colors, pulseStyle wrapper and exported ReducedColor; document drawClientToast as info-only

commit
34bf70956b09e9132f26c2ae495389db46d299df
author
brice · · 2026-10-03 14:04 UTC
committer
brice · · 2026-10-03 14:14 UTC
parent
7ac2f1b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
feat/color-capabilities
6 files · +25 -39
M internal/domain/terminalcap/capabilities.go +0 -14
@@ -34,20 +34,6 @@ func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Vali
3434 // RGB reports whether the attachment can receive RGB ANSI output.
3535 func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor }
3636
37-// Colors reports the number of colors the attachment can display.
38-func (c ColorCapabilities) Colors() int {
39- switch c.Mode {
40- case ANSI256:
41- return 256
42- case ANSI16:
43- return 16
44- case Monochrome:
45- return 0
46- default:
47- return 1 << 24
48- }
49-}
50-
5137 // Source records how confidently a terminal capability was selected.
5238 type Source uint8
5339
@@ -34,20 +34,6 @@ func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Vali
34 // RGB reports whether the attachment can receive RGB ANSI output.34 // RGB reports whether the attachment can receive RGB ANSI output.
35 func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor }35 func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor }
36 36
37-// Colors reports the number of colors the attachment can display.
38-func (c ColorCapabilities) Colors() int {
39- switch c.Mode {
40- case ANSI256:
41- return 256
42- case ANSI16:
43- return 16
44- case Monochrome:
45- return 0
46- default:
47- return 1 << 24
48- }
49-}
50-
51 // Source records how confidently a terminal capability was selected.37 // Source records how confidently a terminal capability was selected.
52 type Source uint838 type Source uint8
53 39
M internal/domain/terminalcap/capabilities_test.go +8 -10
@@ -8,21 +8,19 @@ import (
88
99 func TestColorCapabilities(t *testing.T) {
1010 tests := []struct {
11- name string
12- in ColorCapabilities
13- wantRGB bool
14- wantColors int
11+ name string
12+ in ColorCapabilities
13+ wantRGB bool
1514 }{
16- {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216},
17- {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216},
18- {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256},
19- {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false, wantColors: 16},
20- {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false, wantColors: 0},
15+ {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true},
16+ {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true},
17+ {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false},
18+ {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false},
19+ {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false},
2120 }
2221 for _, tt := range tests {
2322 t.Run(tt.name, func(t *testing.T) {
2423 require.Equal(t, tt.wantRGB, tt.in.RGB())
25- require.Equal(t, tt.wantColors, tt.in.Colors())
2624 })
2725 }
2826 }
@@ -8,21 +8,19 @@ import (
8 8
9 func TestColorCapabilities(t *testing.T) {9 func TestColorCapabilities(t *testing.T) {
10 tests := []struct {10 tests := []struct {
11- name string11+ name string
12- in ColorCapabilities12+ in ColorCapabilities
13- wantRGB bool13+ wantRGB bool
14- wantColors int
15 }{14 }{
16- {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216},15+ {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true},
17- {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216},16+ {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true},
18- {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256},17+ {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false},
19- {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false, wantColors: 16},18+ {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false},
20- {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false, wantColors: 0},19+ {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false},
21 }20 }
22 for _, tt := range tests {21 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {22 t.Run(tt.name, func(t *testing.T) {
24 require.Equal(t, tt.wantRGB, tt.in.RGB())23 require.Equal(t, tt.wantRGB, tt.in.RGB())
25- require.Equal(t, tt.wantColors, tt.in.Colors())
26 })24 })
27 }25 }
28 }26 }
M internal/usecase/client/client_toast.go +6 -0
@@ -17,6 +17,12 @@ import (
1717 // reconciliation; input pumps must only publish a request for it.
1818 // The anchor places it: transitions stay centered, notices sit top-right so
1919 // they never cover the picker list. The border is colored by severity.
20+//
21+// The border uses fixed xterm-256 colors and does not follow the attachment
22+// color profile, so production callers (the transition notice) pass only
23+// domain.NoticeInfo, which emits no color. Toasts with a severity that must
24+// honor 16-color or monochrome terminals go through toastBorderSGRFor, as the
25+// picker does.
2026 func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) {
2127 bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor})
2228 if bounds.Width <= 0 || bounds.Height <= 0 {
@@ -17,6 +17,12 @@ import (
17 // reconciliation; input pumps must only publish a request for it.17 // reconciliation; input pumps must only publish a request for it.
18 // The anchor places it: transitions stay centered, notices sit top-right so18 // The anchor places it: transitions stay centered, notices sit top-right so
19 // they never cover the picker list. The border is colored by severity.19 // they never cover the picker list. The border is colored by severity.
20+//
21+// The border uses fixed xterm-256 colors and does not follow the attachment
22+// color profile, so production callers (the transition notice) pass only
23+// domain.NoticeInfo, which emits no color. Toasts with a severity that must
24+// honor 16-color or monochrome terminals go through toastBorderSGRFor, as the
25+// picker does.
20 func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) {26 func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) {
21 bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor})27 bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor})
22 if bounds.Width <= 0 || bounds.Height <= 0 {28 if bounds.Width <= 0 || bounds.Height <= 0 {
M internal/usecase/daemon/status.go +3 -7
@@ -27,16 +27,12 @@ func pulseVisible(frame int) bool {
2727 return frame%pulseFrameCount != 0
2828 }
2929
30-// pulseStyle returns the style for the attention bell glyph at frame, built
30+// pulseStyleFor returns the style for the attention bell glyph at frame, built
3131 // on top of base so the bell always keeps the caller's background (the tab's
3232 // accent/statusBar, or a faded MRU entry's blended colors) instead of
3333 // punching a default-background hole in a themed bar. On the invisible beat
34-// it returns (base, false) unchanged.
35-func pulseStyle(frame int, base renderer.Style) (renderer.Style, bool) {
36- return pulseStyleFor(frame, base, false)
37-}
38-
39-// pulseStyleFor is pulseStyle for chrome built by the reduced-color policy.
34+// it returns (base, false) unchanged. reduced selects the attribute-only pulse
35+// for chrome built by the reduced-color policy.
4036 func pulseStyleFor(frame int, base renderer.Style, reduced bool) (renderer.Style, bool) {
4137 if !pulseVisible(frame) {
4238 return base, false
@@ -27,16 +27,12 @@ func pulseVisible(frame int) bool {
27 return frame%pulseFrameCount != 027 return frame%pulseFrameCount != 0
28 }28 }
29 29
30-// pulseStyle returns the style for the attention bell glyph at frame, built30+// pulseStyleFor returns the style for the attention bell glyph at frame, built
31 // on top of base so the bell always keeps the caller's background (the tab's31 // on top of base so the bell always keeps the caller's background (the tab's
32 // accent/statusBar, or a faded MRU entry's blended colors) instead of32 // accent/statusBar, or a faded MRU entry's blended colors) instead of
33 // punching a default-background hole in a themed bar. On the invisible beat33 // punching a default-background hole in a themed bar. On the invisible beat
34-// it returns (base, false) unchanged.34+// it returns (base, false) unchanged. reduced selects the attribute-only pulse
35-func pulseStyle(frame int, base renderer.Style) (renderer.Style, bool) {35+// for chrome built by the reduced-color policy.
36- return pulseStyleFor(frame, base, false)
37-}
38-
39-// pulseStyleFor is pulseStyle for chrome built by the reduced-color policy.
40 func pulseStyleFor(frame int, base renderer.Style, reduced bool) (renderer.Style, bool) {36 func pulseStyleFor(frame int, base renderer.Style, reduced bool) (renderer.Style, bool) {
41 if !pulseVisible(frame) {37 if !pulseVisible(frame) {
42 return base, false38 return base, false
M internal/usecase/daemon/status_test.go +5 -5
@@ -885,32 +885,32 @@ func TestPulseStyleFadesFromBaseAndHidesAtFrameZero(t *testing.T) {
885885
886886 t.Run("invisible frame returns base unchanged", func(t *testing.T) {
887887 for _, base := range []renderer.Style{rgbBase, indexedBase} {
888- style, visible := pulseStyle(0, base)
888+ style, visible := pulseStyleFor(0, base, false)
889889 require.False(t, visible)
890890 require.True(t, style.Equal(base))
891891 }
892892 })
893893
894894 t.Run("rgb base blends the glyph foreground from the base background to its foreground, keeping the base background", func(t *testing.T) {
895- low, visible := pulseStyle(1, rgbBase)
895+ low, visible := pulseStyleFor(1, rgbBase, false)
896896 require.True(t, visible)
897897 require.True(t, low.Bold)
898898 require.True(t, low.HasBackgroundRGB)
899899 require.Equal(t, rgbBase.BackgroundRGB, low.BackgroundRGB, "bell keeps the caller's background so it never punches a hole in a themed bar")
900900
901- peak, visible := pulseStyle(pulseFrameCount/2, rgbBase)
901+ peak, visible := pulseStyleFor(pulseFrameCount/2, rgbBase, false)
902902 require.True(t, visible)
903903 require.Equal(t, rgbBase.ForegroundRGB, peak.ForegroundRGB, "peak intensity reaches the base foreground exactly")
904904 require.NotEqual(t, low.ForegroundRGB, peak.ForegroundRGB, "the glyph should ramp, not jump straight to full intensity")
905905 })
906906
907907 t.Run("non-RGB base falls back to the indexed grey ramp and preserves other base attributes", func(t *testing.T) {
908- low, visible := pulseStyle(1, indexedBase)
908+ low, visible := pulseStyleFor(1, indexedBase, false)
909909 require.True(t, visible)
910910 require.True(t, low.Bold)
911911 require.True(t, low.Inverse, "non-color base attributes like inverse must survive")
912912
913- peak, visible := pulseStyle(pulseFrameCount/2, indexedBase)
913+ peak, visible := pulseStyleFor(pulseFrameCount/2, indexedBase, false)
914914 require.True(t, visible)
915915 require.Greater(t, peak.Foreground, low.Foreground)
916916 })
@@ -885,32 +885,32 @@ func TestPulseStyleFadesFromBaseAndHidesAtFrameZero(t *testing.T) {
885 885
886 t.Run("invisible frame returns base unchanged", func(t *testing.T) {886 t.Run("invisible frame returns base unchanged", func(t *testing.T) {
887 for _, base := range []renderer.Style{rgbBase, indexedBase} {887 for _, base := range []renderer.Style{rgbBase, indexedBase} {
888- style, visible := pulseStyle(0, base)888+ style, visible := pulseStyleFor(0, base, false)
889 require.False(t, visible)889 require.False(t, visible)
890 require.True(t, style.Equal(base))890 require.True(t, style.Equal(base))
891 }891 }
892 })892 })
893 893
894 t.Run("rgb base blends the glyph foreground from the base background to its foreground, keeping the base background", func(t *testing.T) {894 t.Run("rgb base blends the glyph foreground from the base background to its foreground, keeping the base background", func(t *testing.T) {
895- low, visible := pulseStyle(1, rgbBase)895+ low, visible := pulseStyleFor(1, rgbBase, false)
896 require.True(t, visible)896 require.True(t, visible)
897 require.True(t, low.Bold)897 require.True(t, low.Bold)
898 require.True(t, low.HasBackgroundRGB)898 require.True(t, low.HasBackgroundRGB)
899 require.Equal(t, rgbBase.BackgroundRGB, low.BackgroundRGB, "bell keeps the caller's background so it never punches a hole in a themed bar")899 require.Equal(t, rgbBase.BackgroundRGB, low.BackgroundRGB, "bell keeps the caller's background so it never punches a hole in a themed bar")
900 900
901- peak, visible := pulseStyle(pulseFrameCount/2, rgbBase)901+ peak, visible := pulseStyleFor(pulseFrameCount/2, rgbBase, false)
902 require.True(t, visible)902 require.True(t, visible)
903 require.Equal(t, rgbBase.ForegroundRGB, peak.ForegroundRGB, "peak intensity reaches the base foreground exactly")903 require.Equal(t, rgbBase.ForegroundRGB, peak.ForegroundRGB, "peak intensity reaches the base foreground exactly")
904 require.NotEqual(t, low.ForegroundRGB, peak.ForegroundRGB, "the glyph should ramp, not jump straight to full intensity")904 require.NotEqual(t, low.ForegroundRGB, peak.ForegroundRGB, "the glyph should ramp, not jump straight to full intensity")
905 })905 })
906 906
907 t.Run("non-RGB base falls back to the indexed grey ramp and preserves other base attributes", func(t *testing.T) {907 t.Run("non-RGB base falls back to the indexed grey ramp and preserves other base attributes", func(t *testing.T) {
908- low, visible := pulseStyle(1, indexedBase)908+ low, visible := pulseStyleFor(1, indexedBase, false)
909 require.True(t, visible)909 require.True(t, visible)
910 require.True(t, low.Bold)910 require.True(t, low.Bold)
911 require.True(t, low.Inverse, "non-color base attributes like inverse must survive")911 require.True(t, low.Inverse, "non-color base attributes like inverse must survive")
912 912
913- peak, visible := pulseStyle(pulseFrameCount/2, indexedBase)913+ peak, visible := pulseStyleFor(pulseFrameCount/2, indexedBase, false)
914 require.True(t, visible)914 require.True(t, visible)
915 require.Greater(t, peak.Foreground, low.Foreground)915 require.Greater(t, peak.Foreground, low.Foreground)
916 })916 })
M internal/usecase/theme/color_policy.go +3 -3
@@ -6,10 +6,10 @@ import (
66 "github.com/bnema/vev/internal/domain/terminalcap"
77 )
88
9-// ReducedColor reports whether chrome for c must be built from attributes and
9+// reducedColor reports whether chrome for c must be built from attributes and
1010 // terminal palette slots instead of RGB surfaces: 16-color and monochrome
1111 // attachments.
12-func ReducedColor(c terminalcap.ColorCapabilities) bool {
12+func reducedColor(c terminalcap.ColorCapabilities) bool {
1313 return c.Mode == terminalcap.ANSI16 || c.Mode == terminalcap.Monochrome
1414 }
1515
@@ -19,7 +19,7 @@ func ReducedColor(c terminalcap.ColorCapabilities) bool {
1919 // and mark the returned Theme so dimming uses the faint attribute instead of
2020 // RGB blends. Color is per attachment, so callers resolve once per attachment.
2121 func ResolveForColor(t Theme, policy domain.ThemeAccent, color terminalcap.ColorCapabilities) ResolvedTheme {
22- if !ReducedColor(color) {
22+ if !reducedColor(color) {
2323 return Resolve(t, policy)
2424 }
2525 t.DimByAttribute = true
@@ -6,10 +6,10 @@ import (
6 "github.com/bnema/vev/internal/domain/terminalcap"6 "github.com/bnema/vev/internal/domain/terminalcap"
7 )7 )
8 8
9-// ReducedColor reports whether chrome for c must be built from attributes and9+// reducedColor reports whether chrome for c must be built from attributes and
10 // terminal palette slots instead of RGB surfaces: 16-color and monochrome10 // terminal palette slots instead of RGB surfaces: 16-color and monochrome
11 // attachments.11 // attachments.
12-func ReducedColor(c terminalcap.ColorCapabilities) bool {12+func reducedColor(c terminalcap.ColorCapabilities) bool {
13 return c.Mode == terminalcap.ANSI16 || c.Mode == terminalcap.Monochrome13 return c.Mode == terminalcap.ANSI16 || c.Mode == terminalcap.Monochrome
14 }14 }
15 15
@@ -19,7 +19,7 @@ func ReducedColor(c terminalcap.ColorCapabilities) bool {
19 // and mark the returned Theme so dimming uses the faint attribute instead of19 // and mark the returned Theme so dimming uses the faint attribute instead of
20 // RGB blends. Color is per attachment, so callers resolve once per attachment.20 // RGB blends. Color is per attachment, so callers resolve once per attachment.
21 func ResolveForColor(t Theme, policy domain.ThemeAccent, color terminalcap.ColorCapabilities) ResolvedTheme {21 func ResolveForColor(t Theme, policy domain.ThemeAccent, color terminalcap.ColorCapabilities) ResolvedTheme {
22- if !ReducedColor(color) {22+ if !reducedColor(color) {
23 return Resolve(t, policy)23 return Resolve(t, policy)
24 }24 }
25 t.DimByAttribute = true25 t.DimByAttribute = true

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close