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 Assistant int // label color for the zot role
16 Tool int
17 ToolOut int
18 Error int
19 Warning int
20 Spinner int // spinner + funny working line
21 SelectionBG int // background for highlighted rows
22 SelectionFG int // foreground for highlighted rows
23}
24
25var Dark = Theme{
26 FG: 253,
27 Muted: 244,
28 Accent: 111, // soft blue
29 User: 180, // warm tan
30 Assistant: 117, // bright cyan — the zot label color
31 Tool: 114, // green
32 ToolOut: 245,
33 Error: 203,
34 Warning: 214,
35 Spinner: 183, // soft purple
36 SelectionBG: 24, // deep blue background
37 SelectionFG: 231, // near-white foreground
38}
39
40var Light = Theme{
41 FG: 236,
42 Muted: 244,
43 Accent: 33,
44 User: 94,
45 Assistant: 31, // deep cyan
46 Tool: 28,
47 ToolOut: 240,
48 Error: 160,
49 Warning: 166,
50 Spinner: 91, // purple
51 SelectionBG: 153, // light blue
52 SelectionFG: 232, // near-black
53}
54
55// FG256 wraps s in foreground color c using ANSI 256-color SGR.
56func (t Theme) FG256(c int, s string) string {
57 return sgrFG(c) + s + reset
58}
59
60// Highlight paints s with the theme's selection colors (foreground +
61// background). The caller is responsible for padding s to the desired
62// width; styling alone does not extend the background past content.
63func (t Theme) Highlight(s string) string {
64 return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
65}
66
67// PadHighlight styles s and extends the selection background to the
68// full terminal width so the highlight is a full row, not just a
69// rectangle around the text.
70func (t Theme) PadHighlight(s string, width int) string {
71 visible := visibleWidth(s)
72 if visible < width {
73 s += strings.Repeat(" ", width-visible)
74 }
75 // Emit the background color AFTER the reset via a trailing
76 // erase-to-end-of-line so the highlight extends to the terminal
77 // edge even if the cell count is slightly off.
78 return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + "\x1b[K" + reset
79}
80
81// Bold wraps s in bold SGR.
82func Bold(s string) string { return "\x1b[1m" + s + "\x1b[22m" }
83
84// Dim wraps s in dim SGR.
85func Dim(s string) string { return "\x1b[2m" + s + "\x1b[22m" }
86
87// Italic wraps s in italic SGR.
88func Italic(s string) string { return "\x1b[3m" + s + "\x1b[23m" }
89
90const reset = "\x1b[0m"
91
92func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
93func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
94
95// small itoa to avoid pulling strconv into this hot path twice.
96func itoa(n int) string {
97 if n == 0 {
98 return "0"
99 }
100 buf := [4]byte{}
101 i := len(buf)
102 for n > 0 {
103 i--
104 buf[i] = byte('0' + n%10)
105 n /= 10
106 }
107 return string(buf[i:])
108}