v0.1.42 42173ed

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// ANSI 256-color palette used by zot. Defined as numeric codes so we
9// can swap themes without changing any render code.
10type Theme struct {
11	FG           int
12	Muted        int
13	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
18	Tool         int
19	ToolOut      int
20	Error        int
21	Warning      int
22	Spinner      int // spinner + funny working line
23	SelectionBG  int // background for highlighted rows
24	SelectionFG  int // foreground for highlighted rows
25}
26
27var Dark = Theme{
28	FG:           253,
29	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
36	ToolOut:      245,
37	Error:        203,
38	Warning:      214,
39	Spinner:      183, // soft purple
40	SelectionBG:  24,  // deep blue background
41	SelectionFG:  231, // near-white foreground
42}
43
44var Light = Theme{
45	FG:           236,
46	Muted:        244,
47	Accent:       33,
48	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
52	Tool:         28,
53	ToolOut:      240,
54	Error:        160,
55	Warning:      166,
56	Spinner:      91,  // purple
57	SelectionBG:  153, // light blue
58	SelectionFG:  232, // near-black
59}
60
61// FG256 wraps s in foreground color c using ANSI 256-color SGR.
62func (t Theme) FG256(c int, s string) string {
63	return sgrFG(c) + s + reset
64}
65
66// BG256 wraps s in background color c using ANSI 256-color SGR.
67// Useful when the visible cell needs a coloured fill but the
68// underlying character should be a regular space (so mouse-copy
69// from the terminal yields whitespace instead of a glyph).
70func (t Theme) BG256(c int, s string) string {
71	return sgrBG(c) + s + reset
72}
73
74// AccentBar returns a 2-cell-wide leader: a coloured half-block
75// glyph followed by a plain space gutter. Used as the speaker-label
76// prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so
77// the bar reads consistently across the UI.
78func (t Theme) AccentBar(c int) string {
79	return t.FG256(c, "▌ ")
80}
81
82// Highlight paints s with the theme's selection colors (foreground +
83// background). The caller is responsible for padding s to the desired
84// width; styling alone does not extend the background past content.
85func (t Theme) Highlight(s string) string {
86	return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
87}
88
89// PadHighlight styles s and extends the selection background to the
90// full terminal width so the highlight is a full row, not just a
91// rectangle around the text.
92func (t Theme) PadHighlight(s string, width int) string {
93	visible := visibleWidth(s)
94	if visible < width {
95		s += strings.Repeat(" ", width-visible)
96	}
97	return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
98}
99
100// UserBubble paints a single user message row with the bubble
101// background colour, padding to width so the tint extends to the
102// full terminal width. Foreground stays in UserBubbleFG so text
103// remains legible against the tint.
104func (t Theme) UserBubble(s string, width int) string {
105	visible := visibleWidth(s)
106	if visible < width {
107		s += strings.Repeat(" ", width-visible)
108	}
109	return sgrFG(t.UserBubbleFG) + sgrBG(t.UserBubbleBG) + s + reset
110}
111
112// UserBubbleRow renders one user-bubble row prefixed with a coloured
113// half-block accent bar ("▌ ") so every line of the bubble has the
114// zot-blue gutter at the very left. The bar lives outside the bubble
115// tint (chat bg) so the bubble itself sits inside it. Width is the
116// outer width including the bar; the bubble content is padded to
117// width-2 (the bar + its trailing space).
118func (t Theme) UserBubbleRow(content string, width int) string {
119	// Bar plus a single space gutter, in the assistant accent colour
120	// so it matches the tool-box / app accent and reads as zot's voice
121	// marker. Two cells wide.
122	bar := t.FG256(t.Assistant, "▌ ")
123	bubbleW := width - 2
124	if bubbleW < 1 {
125		bubbleW = 1
126	}
127	return bar + t.UserBubble(content, bubbleW)
128}
129
130// Bold wraps s in bold SGR.
131func Bold(s string) string { return "\x1b[1m" + s + "\x1b[22m" }
132
133// Dim wraps s in dim SGR.
134func Dim(s string) string { return "\x1b[2m" + s + "\x1b[22m" }
135
136// Italic wraps s in italic SGR.
137func Italic(s string) string { return "\x1b[3m" + s + "\x1b[23m" }
138
139const reset = "\x1b[0m"
140
141func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
142func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
143
144// small itoa to avoid pulling strconv into this hot path twice.
145func itoa(n int) string {
146	if n == 0 {
147		return "0"
148	}
149	buf := [4]byte{}
150	i := len(buf)
151	for n > 0 {
152		i--
153		buf[i] = byte('0' + n%10)
154		n /= 10
155	}
156	return string(buf[i:])
157}

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