v0.3.69 6abb410

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	Background   *TerminalColor // optional full-row TUI background; nil keeps terminal default
43	User         int            // label color for the user role
44	UserBubbleBG TerminalColor  // background tint behind user message rows
45	UserBubbleFG int            // foreground colour for user message rows
46	Assistant    int            // label color for the zot role
47	Tool         int
48	ToolOut      int
49	Error        int
50	Warning      int
51	Spinner      int // spinner + funny working line
52	ThinkingMax  int // status color for the opt-in max reasoning level
53	SelectionBG  int // background for highlighted rows
54	SelectionFG  int // foreground for highlighted rows
55
56	SpinnerFrames     []string
57	SpinnerMessages   []string
58	SpinnerIntervalMS int
59
60	SyntaxBaseStyle string
61	Syntax          SyntaxTheme
62}
63
64// SyntaxTheme contains the chroma token colors used by code fences,
65// file previews, and diffs. Values are chroma style entries, so they
66// may include attributes after the color (for example "#81a1c1 bold").
67type SyntaxTheme struct {
68	Keyword             string
69	KeywordConstant     string
70	KeywordDeclaration  string
71	KeywordNamespace    string
72	KeywordReserved     string
73	KeywordType         string
74	NameBuiltin         string
75	NameFunction        string
76	NameClass           string
77	NameDecorator       string
78	LiteralString       string
79	LiteralStringEscape string
80	LiteralNumber       string
81	Comment             string
82	CommentPreproc      string
83	Operator            string
84	Punctuation         string
85	Text                string
86}
87
88var nordSyntax = SyntaxTheme{
89	Keyword:             "#81a1c1 bold",
90	KeywordConstant:     "#81a1c1",
91	KeywordDeclaration:  "#81a1c1",
92	KeywordNamespace:    "#81a1c1",
93	KeywordReserved:     "#81a1c1 bold",
94	KeywordType:         "#88c0d0",
95	NameBuiltin:         "#88c0d0",
96	NameFunction:        "#8fbcbb",
97	NameClass:           "#a3be8c bold",
98	NameDecorator:       "#b48ead",
99	LiteralString:       "#a3be8c",
100	LiteralStringEscape: "#bf616a",
101	LiteralNumber:       "#d08770",
102	Comment:             "#616e88 italic",
103	CommentPreproc:      "#b48ead",
104	Operator:            "#eceff4",
105	Punctuation:         "#d8dee9",
106	Text:                "#e5e9f0",
107}
108
109var defaultSpinnerFrames = []string{
110	"⠋",
111	"⠙",
112	"⠚",
113	"⠞",
114	"⠖",
115	"⠦",
116	"⠴",
117	"⠲",
118	"⠳",
119	"⠓",
120}
121
122var defaultSpinnerMessages = []string{
123	"thinking",
124	"reticulating splines",
125	"bribing the tokenizer",
126	"asking the rubber duck",
127	"summoning daemons",
128	"consulting the oracle",
129	"herding tokens",
130	"compiling excuses",
131	"poking the model",
132	"negotiating with rate limits",
133	"picking a fight with syntax",
134	"reading between the bits",
135	"tasting the semicolons",
136	"pretending to understand the code",
137	"petting the cache",
138	"drafting clever replies",
139	"warming up the GPU choir",
140	"arguing with a stack trace",
141	"googling the answer (not really)",
142	"rewriting history",
143	"every draft is a stone in the work",
144	"bringing order to the unhewn",
145	"finding the load-bearing measure",
146	"where clarity grows, work grows lighter",
147	"every correction serves the work",
148}
149
150var Dark = Theme{
151	FG:                253,
152	Muted:             244,
153	Accent:            111,                  // soft blue
154	User:              180,                  // warm tan (unused now that the speaker label is gone, kept for skin compat)
155	UserBubbleBG:      ColorRGB(66, 69, 75), // #42454B
156	UserBubbleFG:      248,                  // slightly lighter grey for readability on #42454B
157	Assistant:         117,                  // bright cyan — the zot label color
158	Tool:              114,                  // green
159	ToolOut:           245,
160	Error:             203,
161	Warning:           214,
162	Spinner:           183, // soft purple
163	ThinkingMax:       207, // vivid magenta
164	SelectionBG:       24,  // deep blue background
165	SelectionFG:       231, // near-white foreground
166	SpinnerFrames:     defaultSpinnerFrames,
167	SpinnerMessages:   defaultSpinnerMessages,
168	SpinnerIntervalMS: 80,
169	SyntaxBaseStyle:   "monokai",
170	Syntax:            nordSyntax,
171}
172
173var Light = Theme{
174	FG:                236,
175	Muted:             244,
176	Accent:            33,
177	User:              94,
178	UserBubbleBG:      Color256(254), // very pale grey panel behind user rows on light theme
179	UserBubbleFG:      240,           // dark grey text, legible on the pale panel
180	Assistant:         31,            // deep cyan
181	Tool:              28,
182	ToolOut:           240,
183	Error:             160,
184	Warning:           166,
185	Spinner:           91,  // purple
186	ThinkingMax:       127, // deep magenta
187	SelectionBG:       153, // light blue
188	SelectionFG:       232, // near-black
189	SpinnerFrames:     defaultSpinnerFrames,
190	SpinnerMessages:   defaultSpinnerMessages,
191	SpinnerIntervalMS: 80,
192	SyntaxBaseStyle:   "monokai",
193	Syntax:            nordSyntax,
194}
195
196// FG256 wraps s in foreground color c using ANSI 256-color SGR.
197func (t Theme) FG256(c int, s string) string {
198	return sgrFG(c) + s + reset
199}
200
201// BG256 wraps s in background color c using ANSI 256-color SGR.
202// Useful when the visible cell needs a coloured fill but the
203// underlying character should be a regular space (so mouse-copy
204// from the terminal yields whitespace instead of a glyph).
205func (t Theme) BG256(c int, s string) string {
206	return sgrBG(c) + s + reset
207}
208
209// BG wraps s in a terminal background colour. Unlike BG256, this can
210// target ANSI theme slots or truecolor RGB, which lets selected UI
211// surfaces follow the user's terminal theme.
212func (t Theme) BG(c TerminalColor, s string) string {
213	return sgrBGColor(c) + s + reset
214}
215
216// AccentBar returns a 2-cell-wide leader: a coloured half-block
217// glyph followed by a plain space gutter. Used as the speaker-label
218// prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so
219// the bar reads consistently across the UI.
220func (t Theme) AccentBar(c int) string {
221	return t.FG256(c, "▌ ")
222}
223
224// Highlight paints s with the theme's selection colors (foreground +
225// background). The caller is responsible for padding s to the desired
226// width; styling alone does not extend the background past content.
227func (t Theme) Highlight(s string) string {
228	return t.SelectionStyle() + s + reset
229}
230
231// SelectionStyle returns the SGR prefix for the theme's selected row.
232func (t Theme) SelectionStyle() string {
233	return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG)
234}
235
236// BackgroundStyle returns the SGR prefix for the optional full-row
237// TUI background. Empty means zot should leave the terminal's
238// configured background untouched.
239func (t Theme) BackgroundStyle() string {
240	if t.Background == nil {
241		return ""
242	}
243	return sgrBGColor(*t.Background)
244}
245
246// SelectionStyleFG returns the SGR prefix for selected-row text with
247// a custom foreground. Useful for preserving semantic marks inside a
248// highlighted row without hardcoding escape sequences outside theme.go.
249func (t Theme) SelectionStyleFG(fg int) string {
250	return sgrFG(fg) + sgrBG(t.SelectionBG)
251}
252
253// PadHighlight styles s and extends the selection background to the
254// full terminal width so the highlight is a full row, not just a
255// rectangle around the text.
256func (t Theme) PadHighlight(s string, width int) string {
257	visible := visibleWidth(s)
258	if visible < width {
259		s += strings.Repeat(" ", width-visible)
260	}
261	return t.SelectionStyle() + s + reset
262}
263
264// UserBubble paints a single user message row with the bubble
265// background colour, padding to width so the tint extends to the
266// full terminal width. Foreground stays in UserBubbleFG so text
267// remains legible against the tint.
268func (t Theme) UserBubble(s string, width int) string {
269	visible := visibleWidth(s)
270	if visible < width {
271		s += strings.Repeat(" ", width-visible)
272	}
273	return sgrFG(t.UserBubbleFG) + sgrBGColor(t.UserBubbleBG) + s + reset
274}
275
276// UserBubbleRow renders one user-bubble row prefixed with a coloured
277// half-block accent bar ("▌ ") so every line of the bubble has the
278// zot-blue gutter at the very left. The bar lives outside the bubble
279// tint (chat bg) so the bubble itself sits inside it. Width is the
280// outer width including the bar; the bubble content is padded to
281// width-2 (the bar + its trailing space).
282func (t Theme) UserBubbleRow(content string, width int) string {
283	// Bar plus a single space gutter, in the assistant accent colour
284	// so it matches the tool-box / app accent and reads as zot's voice
285	// marker. Two cells wide.
286	bar := t.FG256(t.Assistant, "▌ ")
287	bubbleW := width - 2
288	if bubbleW < 1 {
289		bubbleW = 1
290	}
291	return bar + t.UserBubble(content, bubbleW)
292}
293
294// Bold wraps s in bold SGR.
295func Bold(s string) string { return "\x1b[1m" + s + "\x1b[22m" }
296
297// Dim wraps s in dim SGR.
298func Dim(s string) string { return "\x1b[2m" + s + "\x1b[22m" }
299
300// Italic wraps s in italic SGR.
301func Italic(s string) string { return "\x1b[3m" + s + "\x1b[23m" }
302
303const reset = "\x1b[0m"
304
305func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
306func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
307func sgrBGColor(c TerminalColor) string {
308	switch c.Mode {
309	case terminalColorANSI:
310		return "\x1b[" + itoa(c.Index) + "m"
311	case terminalColorRGB:
312		return "\x1b[48;2;" + itoa(c.R) + ";" + itoa(c.G) + ";" + itoa(c.B) + "m"
313	default:
314		return sgrBG(c.Index)
315	}
316}
317
318// small itoa to avoid pulling strconv into this hot path twice.
319func itoa(n int) string {
320	if n == 0 {
321		return "0"
322	}
323	buf := [4]byte{}
324	i := len(buf)
325	for n > 0 {
326		i--
327		buf[i] = byte('0' + n%10)
328		n /= 10
329	}
330	return string(buf[i:])
331}

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