detached 6ba41f2

tui: tune user bubble colors

commit
6ba41f29eff53060c2b0901b27edb9b62a00ab03
author
patriceckhart · · 2026-05-03 17:23 UTC
parent
a40d1cf
signature
unsigned
refs
tag: v0.1.46
3 files · +61 -16
M internal/agent/modes/btw_dialog.go +1 -1
@@ -392,7 +392,7 @@ func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string {
392392 if innerWidth < 1 {
393393 innerWidth = 1
394394 }
395- bar := th.BG256(th.UserBubbleBG, th.FG256(th.Accent, "▌ "))
395+ bar := th.BG(th.UserBubbleBG, th.FG256(th.Accent, "▌ "))
396396 row := func(content string) string {
397397 inner := strings.Repeat(" ", leftGutter) + content
398398 return " " + bar + th.UserBubble(inner, bubbleWidth-2)
@@ -392,7 +392,7 @@ func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string {
392 if innerWidth < 1 {392 if innerWidth < 1 {
393 innerWidth = 1393 innerWidth = 1
394 }394 }
395- bar := th.BG256(th.UserBubbleBG, th.FG256(th.Accent, "▌ "))395+ bar := th.BG(th.UserBubbleBG, th.FG256(th.Accent, "▌ "))
396 row := func(content string) string {396 row := func(content string) string {
397 inner := strings.Repeat(" ", leftGutter) + content397 inner := strings.Repeat(" ", leftGutter) + content
398 return " " + bar + th.UserBubble(inner, bubbleWidth-2)398 return " " + bar + th.UserBubble(inner, bubbleWidth-2)
M internal/tui/theme.go +59 -14
@@ -5,16 +5,44 @@ package tui
55
66 import "strings"
77
8+// TerminalColor describes a terminal colour in one of the colour
9+// spaces terminals commonly support. Most of zot's theme still uses
10+// xterm-256 indexes, but user-bubble backgrounds can also use ANSI
11+// theme slots (for example SGR 100 / bright-black background) so they
12+// match the user's terminal theme rather than the fixed 256-colour
13+// cube.
14+type TerminalColor struct {
15+ Mode terminalColorMode
16+ Index int
17+ R int
18+ G int
19+ B int
20+}
21+
22+type terminalColorMode int
23+
24+const (
25+ terminalColor256 terminalColorMode = iota
26+ terminalColorANSI
27+ terminalColorRGB
28+)
29+
30+func Color256(index int) TerminalColor { return TerminalColor{Mode: terminalColor256, Index: index} }
31+func ColorANSI(sgr int) TerminalColor { return TerminalColor{Mode: terminalColorANSI, Index: sgr} }
32+func ColorRGB(r, g, b int) TerminalColor {
33+ return TerminalColor{Mode: terminalColorRGB, R: r, G: g, B: b}
34+}
35+
836 // ANSI 256-color palette used by zot. Defined as numeric codes so we
937 // can swap themes without changing any render code.
1038 type Theme struct {
1139 FG int
1240 Muted int
1341 Accent int
14- User int // label color for the user role
15- UserBubbleBG int // background tint behind user message rows
16- UserBubbleFG int // foreground colour for user message rows
17- Assistant int // label color for the zot role
42+ User int // label color for the user role
43+ UserBubbleBG TerminalColor // background tint behind user message rows
44+ UserBubbleFG int // foreground colour for user message rows
45+ Assistant int // label color for the zot role
1846 Tool int
1947 ToolOut int
2048 Error int
@@ -27,12 +55,12 @@ type Theme struct {
2755 var Dark = Theme{
2856 FG: 253,
2957 Muted: 244,
30- Accent: 111, // soft blue
31- User: 180, // warm tan (unused now that the speaker label is gone, kept for skin compat)
32- UserBubbleBG: 237, // soft mid-dark grey panel behind user rows
33- UserBubbleFG: 246, // matches Theme.Muted (status bar text colour)
34- Assistant: 117, // bright cyan — the zot label color
35- Tool: 114, // green
58+ Accent: 111, // soft blue
59+ User: 180, // warm tan (unused now that the speaker label is gone, kept for skin compat)
60+ UserBubbleBG: ColorRGB(66, 69, 75), // #42454B
61+ UserBubbleFG: 248, // slightly lighter grey for readability on #42454B
62+ Assistant: 117, // bright cyan — the zot label color
63+ Tool: 114, // green
3664 ToolOut: 245,
3765 Error: 203,
3866 Warning: 214,
@@ -46,9 +74,9 @@ var Light = Theme{
4674 Muted: 244,
4775 Accent: 33,
4876 User: 94,
49- UserBubbleBG: 254, // very pale grey panel behind user rows on light theme
50- UserBubbleFG: 240, // dark grey text, legible on the pale panel
51- Assistant: 31, // deep cyan
77+ UserBubbleBG: Color256(254), // very pale grey panel behind user rows on light theme
78+ UserBubbleFG: 240, // dark grey text, legible on the pale panel
79+ Assistant: 31, // deep cyan
5280 Tool: 28,
5381 ToolOut: 240,
5482 Error: 160,
@@ -71,6 +99,13 @@ func (t Theme) BG256(c int, s string) string {
7199 return sgrBG(c) + s + reset
72100 }
73101
102+// BG wraps s in a terminal background colour. Unlike BG256, this can
103+// target ANSI theme slots or truecolor RGB, which lets selected UI
104+// surfaces follow the user's terminal theme.
105+func (t Theme) BG(c TerminalColor, s string) string {
106+ return sgrBGColor(c) + s + reset
107+}
108+
74109 // AccentBar returns a 2-cell-wide leader: a coloured half-block
75110 // glyph followed by a plain space gutter. Used as the speaker-label
76111 // prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so
@@ -106,7 +141,7 @@ func (t Theme) UserBubble(s string, width int) string {
106141 if visible < width {
107142 s += strings.Repeat(" ", width-visible)
108143 }
109- return sgrFG(t.UserBubbleFG) + sgrBG(t.UserBubbleBG) + s + reset
144+ return sgrFG(t.UserBubbleFG) + sgrBGColor(t.UserBubbleBG) + s + reset
110145 }
111146
112147 // UserBubbleRow renders one user-bubble row prefixed with a coloured
@@ -140,6 +175,16 @@ const reset = "\x1b[0m"
140175
141176 func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
142177 func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
178+func sgrBGColor(c TerminalColor) string {
179+ switch c.Mode {
180+ case terminalColorANSI:
181+ return "\x1b[" + itoa(c.Index) + "m"
182+ case terminalColorRGB:
183+ return "\x1b[48;2;" + itoa(c.R) + ";" + itoa(c.G) + ";" + itoa(c.B) + "m"
184+ default:
185+ return sgrBG(c.Index)
186+ }
187+}
143188
144189 // small itoa to avoid pulling strconv into this hot path twice.
145190 func itoa(n int) string {
@@ -5,16 +5,44 @@ package tui
5 5
6 import "strings"6 import "strings"
7 7
8+// TerminalColor describes a terminal colour in one of the colour
9+// spaces terminals commonly support. Most of zot's theme still uses
10+// xterm-256 indexes, but user-bubble backgrounds can also use ANSI
11+// theme slots (for example SGR 100 / bright-black background) so they
12+// match the user's terminal theme rather than the fixed 256-colour
13+// cube.
14+type TerminalColor struct {
15+ Mode terminalColorMode
16+ Index int
17+ R int
18+ G int
19+ B int
20+}
21+
22+type terminalColorMode int
23+
24+const (
25+ terminalColor256 terminalColorMode = iota
26+ terminalColorANSI
27+ terminalColorRGB
28+)
29+
30+func Color256(index int) TerminalColor { return TerminalColor{Mode: terminalColor256, Index: index} }
31+func ColorANSI(sgr int) TerminalColor { return TerminalColor{Mode: terminalColorANSI, Index: sgr} }
32+func ColorRGB(r, g, b int) TerminalColor {
33+ return TerminalColor{Mode: terminalColorRGB, R: r, G: g, B: b}
34+}
35+
8 // ANSI 256-color palette used by zot. Defined as numeric codes so we36 // ANSI 256-color palette used by zot. Defined as numeric codes so we
9 // can swap themes without changing any render code.37 // can swap themes without changing any render code.
10 type Theme struct {38 type Theme struct {
11 FG int39 FG int
12 Muted int40 Muted int
13 Accent int41 Accent int
14- User int // label color for the user role42+ User int // label color for the user role
15- UserBubbleBG int // background tint behind user message rows43+ UserBubbleBG TerminalColor // background tint behind user message rows
16- UserBubbleFG int // foreground colour for user message rows44+ UserBubbleFG int // foreground colour for user message rows
17- Assistant int // label color for the zot role45+ Assistant int // label color for the zot role
18 Tool int46 Tool int
19 ToolOut int47 ToolOut int
20 Error int48 Error int
@@ -27,12 +55,12 @@ type Theme struct {
27 var Dark = Theme{55 var Dark = Theme{
28 FG: 253,56 FG: 253,
29 Muted: 244,57 Muted: 244,
30- Accent: 111, // soft blue58+ Accent: 111, // soft blue
31- User: 180, // warm tan (unused now that the speaker label is gone, kept for skin compat)59+ User: 180, // warm tan (unused now that the speaker label is gone, kept for skin compat)
32- UserBubbleBG: 237, // soft mid-dark grey panel behind user rows60+ UserBubbleBG: ColorRGB(66, 69, 75), // #42454B
33- UserBubbleFG: 246, // matches Theme.Muted (status bar text colour)61+ UserBubbleFG: 248, // slightly lighter grey for readability on #42454B
34- Assistant: 117, // bright cyan — the zot label color62+ Assistant: 117, // bright cyan — the zot label color
35- Tool: 114, // green63+ Tool: 114, // green
36 ToolOut: 245,64 ToolOut: 245,
37 Error: 203,65 Error: 203,
38 Warning: 214,66 Warning: 214,
@@ -46,9 +74,9 @@ var Light = Theme{
46 Muted: 244,74 Muted: 244,
47 Accent: 33,75 Accent: 33,
48 User: 94,76 User: 94,
49- UserBubbleBG: 254, // very pale grey panel behind user rows on light theme77+ UserBubbleBG: Color256(254), // very pale grey panel behind user rows on light theme
50- UserBubbleFG: 240, // dark grey text, legible on the pale panel78+ UserBubbleFG: 240, // dark grey text, legible on the pale panel
51- Assistant: 31, // deep cyan79+ Assistant: 31, // deep cyan
52 Tool: 28,80 Tool: 28,
53 ToolOut: 240,81 ToolOut: 240,
54 Error: 160,82 Error: 160,
@@ -71,6 +99,13 @@ func (t Theme) BG256(c int, s string) string {
71 return sgrBG(c) + s + reset99 return sgrBG(c) + s + reset
72 }100 }
73 101
102+// BG wraps s in a terminal background colour. Unlike BG256, this can
103+// target ANSI theme slots or truecolor RGB, which lets selected UI
104+// surfaces follow the user's terminal theme.
105+func (t Theme) BG(c TerminalColor, s string) string {
106+ return sgrBGColor(c) + s + reset
107+}
108+
74 // AccentBar returns a 2-cell-wide leader: a coloured half-block109 // AccentBar returns a 2-cell-wide leader: a coloured half-block
75 // glyph followed by a plain space gutter. Used as the speaker-label110 // glyph followed by a plain space gutter. Used as the speaker-label
76 // prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so111 // prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so
@@ -106,7 +141,7 @@ func (t Theme) UserBubble(s string, width int) string {
106 if visible < width {141 if visible < width {
107 s += strings.Repeat(" ", width-visible)142 s += strings.Repeat(" ", width-visible)
108 }143 }
109- return sgrFG(t.UserBubbleFG) + sgrBG(t.UserBubbleBG) + s + reset144+ return sgrFG(t.UserBubbleFG) + sgrBGColor(t.UserBubbleBG) + s + reset
110 }145 }
111 146
112 // UserBubbleRow renders one user-bubble row prefixed with a coloured147 // UserBubbleRow renders one user-bubble row prefixed with a coloured
@@ -140,6 +175,16 @@ const reset = "\x1b[0m"
140 175
141 func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }176 func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
142 func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }177 func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
178+func sgrBGColor(c TerminalColor) string {
179+ switch c.Mode {
180+ case terminalColorANSI:
181+ return "\x1b[" + itoa(c.Index) + "m"
182+ case terminalColorRGB:
183+ return "\x1b[48;2;" + itoa(c.R) + ";" + itoa(c.G) + ";" + itoa(c.B) + "m"
184+ default:
185+ return sgrBG(c.Index)
186+ }
187+}
143 188
144 // small itoa to avoid pulling strconv into this hot path twice.189 // small itoa to avoid pulling strconv into this hot path twice.
145 func itoa(n int) string {190 func itoa(n int) string {
M internal/tui/view.go +1 -1
@@ -489,7 +489,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str
489489 row := func(content string) string {
490490 inner := strings.Repeat(" ", leftGutter) + content
491491 padded := v.Theme.UserBubble(inner, width-2)
492- bar := v.Theme.BG256(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ "))
492+ bar := v.Theme.BG(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ "))
493493 return bar + padded
494494 }
495495 var bubble []string
@@ -489,7 +489,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str
489 row := func(content string) string {489 row := func(content string) string {
490 inner := strings.Repeat(" ", leftGutter) + content490 inner := strings.Repeat(" ", leftGutter) + content
491 padded := v.Theme.UserBubble(inner, width-2)491 padded := v.Theme.UserBubble(inner, width-2)
492- bar := v.Theme.BG256(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ "))492+ bar := v.Theme.BG(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ "))
493 return bar + padded493 return bar + padded
494 }494 }
495 var bubble []string495 var bubble []string

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 1

Tags 476

↑↓ moveenter openesc close