v0.1.57 168941f

theme.go RawHistoryFolder

1// Package tui is a small terminal UI: raw-mode input, differential
2// line renderer, multi-line editor, and chat view. No external TUI
3// framework; just ANSI escape codes.
4package tui
5
6import "strings"
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.
14type TerminalColor struct {
15	Mode  terminalColorMode
16	Index int
17	R     int
18	G     int
19	B     int
20}
21
22type terminalColorMode int
23
24const (
25	terminalColor256 terminalColorMode = iota
26	terminalColorANSI
27	terminalColorRGB
28)
29
30func Color256(index int) TerminalColor { return TerminalColor{Mode: terminalColor256, Index: index} }
31func ColorANSI(sgr int) TerminalColor  { return TerminalColor{Mode: terminalColorANSI, Index: sgr} }
32func ColorRGB(r, g, b int) TerminalColor {
33	return TerminalColor{Mode: terminalColorRGB, R: r, G: g, B: b}
34}
35
36// ANSI 256-color palette used by zot. Defined as numeric codes so we
37// can swap themes without changing any render code.
38type Theme struct {
39	FG           int
40	Muted        int
41	Accent       int
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
46	Tool         int
47	ToolOut      int
48	Error        int
49	Warning      int
50	Spinner      int // spinner + funny working line
51	SelectionBG  int // background for highlighted rows
52	SelectionFG  int // foreground for highlighted rows
53}
54
55var Dark = Theme{
56	FG:           253,
57	Muted:        244,
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
64	ToolOut:      245,
65	Error:        203,
66	Warning:      214,
67	Spinner:      183, // soft purple
68	SelectionBG:  24,  // deep blue background
69	SelectionFG:  231, // near-white foreground
70}
71
72var Light = Theme{
73	FG:           236,
74	Muted:        244,
75	Accent:       33,
76	User:         94,
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
80	Tool:         28,
81	ToolOut:      240,
82	Error:        160,
83	Warning:      166,
84	Spinner:      91,  // purple
85	SelectionBG:  153, // light blue
86	SelectionFG:  232, // near-black
87}
88
89// FG256 wraps s in foreground color c using ANSI 256-color SGR.
90func (t Theme) FG256(c int, s string) string {
91	return sgrFG(c) + s + reset
92}
93
94// BG256 wraps s in background color c using ANSI 256-color SGR.
95// Useful when the visible cell needs a coloured fill but the
96// underlying character should be a regular space (so mouse-copy
97// from the terminal yields whitespace instead of a glyph).
98func (t Theme) BG256(c int, s string) string {
99	return sgrBG(c) + s + reset
100}
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.
105func (t Theme) BG(c TerminalColor, s string) string {
106	return sgrBGColor(c) + s + reset
107}
108
109// AccentBar returns a 2-cell-wide leader: a coloured half-block
110// glyph followed by a plain space gutter. Used as the speaker-label
111// prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so
112// the bar reads consistently across the UI.
113func (t Theme) AccentBar(c int) string {
114	return t.FG256(c, "▌ ")
115}
116
117// Highlight paints s with the theme's selection colors (foreground +
118// background). The caller is responsible for padding s to the desired
119// width; styling alone does not extend the background past content.
120func (t Theme) Highlight(s string) string {
121	return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
122}
123
124// PadHighlight styles s and extends the selection background to the
125// full terminal width so the highlight is a full row, not just a
126// rectangle around the text.
127func (t Theme) PadHighlight(s string, width int) string {
128	visible := visibleWidth(s)
129	if visible < width {
130		s += strings.Repeat(" ", width-visible)
131	}
132	return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
133}
134
135// UserBubble paints a single user message row with the bubble
136// background colour, padding to width so the tint extends to the
137// full terminal width. Foreground stays in UserBubbleFG so text
138// remains legible against the tint.
139func (t Theme) UserBubble(s string, width int) string {
140	visible := visibleWidth(s)
141	if visible < width {
142		s += strings.Repeat(" ", width-visible)
143	}
144	return sgrFG(t.UserBubbleFG) + sgrBGColor(t.UserBubbleBG) + s + reset
145}
146
147// UserBubbleRow renders one user-bubble row prefixed with a coloured
148// half-block accent bar ("▌ ") so every line of the bubble has the
149// zot-blue gutter at the very left. The bar lives outside the bubble
150// tint (chat bg) so the bubble itself sits inside it. Width is the
151// outer width including the bar; the bubble content is padded to
152// width-2 (the bar + its trailing space).
153func (t Theme) UserBubbleRow(content string, width int) string {
154	// Bar plus a single space gutter, in the assistant accent colour
155	// so it matches the tool-box / app accent and reads as zot's voice
156	// marker. Two cells wide.
157	bar := t.FG256(t.Assistant, "▌ ")
158	bubbleW := width - 2
159	if bubbleW < 1 {
160		bubbleW = 1
161	}
162	return bar + t.UserBubble(content, bubbleW)
163}
164
165// Bold wraps s in bold SGR.
166func Bold(s string) string { return "\x1b[1m" + s + "\x1b[22m" }
167
168// Dim wraps s in dim SGR.
169func Dim(s string) string { return "\x1b[2m" + s + "\x1b[22m" }
170
171// Italic wraps s in italic SGR.
172func Italic(s string) string { return "\x1b[3m" + s + "\x1b[23m" }
173
174const reset = "\x1b[0m"
175
176func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
177func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
178func 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}
188
189// small itoa to avoid pulling strconv into this hot path twice.
190func itoa(n int) string {
191	if n == 0 {
192		return "0"
193	}
194	buf := [4]byte{}
195	i := len(buf)
196	for n > 0 {
197		i--
198		buf[i] = byte('0' + n%10)
199		n /= 10
200	}
201	return string(buf[i:])
202}

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