mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
Single Go module, four top-level packages under packages/. Import
paths become github.com/patriceckhart/zot/packages/<name>; downstream
consumers can depend on individual packages without pulling the rest.
Layout:
packages/provider/ LLM clients + catalog
packages/provider/auth/ credential store + OAuth + login server
packages/core/ agent loop, sessions, cost
packages/tui/ terminal toolkit + chat view
packages/agent/ CLI wiring, system prompt
extensions/ extproto/ modes/ tools/ skills/ swarm/
sdk/ (was pkg/zotcore, package renamed zotcore -> sdk)
ext/ (was pkg/zotext, package renamed zotext -> ext)
internal/ and pkg/ removed. The internal/assets logo moved into
packages/provider/auth/assets.
Public Go SDK identifiers renamed:
pkg/zotcore (package zotcore) -> packages/agent/sdk (package sdk)
pkg/zotext (package zotext) -> packages/agent/ext (package ext)
This breaks Go-based extensions and embedders; the JSON wire protocol
for extensions and RPC is unchanged, so non-Go extensions, already-
built extension binaries, and zot rpc consumers are unaffected.
Docs, examples, and the built-in write-zot-extension skill updated
for the new paths and identifiers. Shadow-bug fixes in code samples
(ext := ext.New -> e := ext.New).
202 lines
6.7 KiB
Go
202 lines
6.7 KiB
Go
// Package tui is a small terminal UI: raw-mode input, differential
|
|
// line renderer, multi-line editor, and chat view. No external TUI
|
|
// framework; just ANSI escape codes.
|
|
package tui
|
|
|
|
import "strings"
|
|
|
|
// TerminalColor describes a terminal colour in one of the colour
|
|
// spaces terminals commonly support. Most of zot's theme still uses
|
|
// xterm-256 indexes, but user-bubble backgrounds can also use ANSI
|
|
// theme slots (for example SGR 100 / bright-black background) so they
|
|
// match the user's terminal theme rather than the fixed 256-colour
|
|
// cube.
|
|
type TerminalColor struct {
|
|
Mode terminalColorMode
|
|
Index int
|
|
R int
|
|
G int
|
|
B int
|
|
}
|
|
|
|
type terminalColorMode int
|
|
|
|
const (
|
|
terminalColor256 terminalColorMode = iota
|
|
terminalColorANSI
|
|
terminalColorRGB
|
|
)
|
|
|
|
func Color256(index int) TerminalColor { return TerminalColor{Mode: terminalColor256, Index: index} }
|
|
func ColorANSI(sgr int) TerminalColor { return TerminalColor{Mode: terminalColorANSI, Index: sgr} }
|
|
func ColorRGB(r, g, b int) TerminalColor {
|
|
return TerminalColor{Mode: terminalColorRGB, R: r, G: g, B: b}
|
|
}
|
|
|
|
// ANSI 256-color palette used by zot. Defined as numeric codes so we
|
|
// can swap themes without changing any render code.
|
|
type Theme struct {
|
|
FG int
|
|
Muted int
|
|
Accent int
|
|
User int // label color for the user role
|
|
UserBubbleBG TerminalColor // background tint behind user message rows
|
|
UserBubbleFG int // foreground colour for user message rows
|
|
Assistant int // label color for the zot role
|
|
Tool int
|
|
ToolOut int
|
|
Error int
|
|
Warning int
|
|
Spinner int // spinner + funny working line
|
|
SelectionBG int // background for highlighted rows
|
|
SelectionFG int // foreground for highlighted rows
|
|
}
|
|
|
|
var Dark = Theme{
|
|
FG: 253,
|
|
Muted: 244,
|
|
Accent: 111, // soft blue
|
|
User: 180, // warm tan (unused now that the speaker label is gone, kept for skin compat)
|
|
UserBubbleBG: ColorRGB(66, 69, 75), // #42454B
|
|
UserBubbleFG: 248, // slightly lighter grey for readability on #42454B
|
|
Assistant: 117, // bright cyan — the zot label color
|
|
Tool: 114, // green
|
|
ToolOut: 245,
|
|
Error: 203,
|
|
Warning: 214,
|
|
Spinner: 183, // soft purple
|
|
SelectionBG: 24, // deep blue background
|
|
SelectionFG: 231, // near-white foreground
|
|
}
|
|
|
|
var Light = Theme{
|
|
FG: 236,
|
|
Muted: 244,
|
|
Accent: 33,
|
|
User: 94,
|
|
UserBubbleBG: Color256(254), // very pale grey panel behind user rows on light theme
|
|
UserBubbleFG: 240, // dark grey text, legible on the pale panel
|
|
Assistant: 31, // deep cyan
|
|
Tool: 28,
|
|
ToolOut: 240,
|
|
Error: 160,
|
|
Warning: 166,
|
|
Spinner: 91, // purple
|
|
SelectionBG: 153, // light blue
|
|
SelectionFG: 232, // near-black
|
|
}
|
|
|
|
// FG256 wraps s in foreground color c using ANSI 256-color SGR.
|
|
func (t Theme) FG256(c int, s string) string {
|
|
return sgrFG(c) + s + reset
|
|
}
|
|
|
|
// BG256 wraps s in background color c using ANSI 256-color SGR.
|
|
// Useful when the visible cell needs a coloured fill but the
|
|
// underlying character should be a regular space (so mouse-copy
|
|
// from the terminal yields whitespace instead of a glyph).
|
|
func (t Theme) BG256(c int, s string) string {
|
|
return sgrBG(c) + s + reset
|
|
}
|
|
|
|
// BG wraps s in a terminal background colour. Unlike BG256, this can
|
|
// target ANSI theme slots or truecolor RGB, which lets selected UI
|
|
// surfaces follow the user's terminal theme.
|
|
func (t Theme) BG(c TerminalColor, s string) string {
|
|
return sgrBGColor(c) + s + reset
|
|
}
|
|
|
|
// AccentBar returns a 2-cell-wide leader: a coloured half-block
|
|
// glyph followed by a plain space gutter. Used as the speaker-label
|
|
// prefix in the chat ("▌ you", "▌ zot") and as the editor prompt so
|
|
// the bar reads consistently across the UI.
|
|
func (t Theme) AccentBar(c int) string {
|
|
return t.FG256(c, "▌ ")
|
|
}
|
|
|
|
// Highlight paints s with the theme's selection colors (foreground +
|
|
// background). The caller is responsible for padding s to the desired
|
|
// width; styling alone does not extend the background past content.
|
|
func (t Theme) Highlight(s string) string {
|
|
return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
|
|
}
|
|
|
|
// PadHighlight styles s and extends the selection background to the
|
|
// full terminal width so the highlight is a full row, not just a
|
|
// rectangle around the text.
|
|
func (t Theme) PadHighlight(s string, width int) string {
|
|
visible := visibleWidth(s)
|
|
if visible < width {
|
|
s += strings.Repeat(" ", width-visible)
|
|
}
|
|
return sgrFG(t.SelectionFG) + sgrBG(t.SelectionBG) + s + reset
|
|
}
|
|
|
|
// UserBubble paints a single user message row with the bubble
|
|
// background colour, padding to width so the tint extends to the
|
|
// full terminal width. Foreground stays in UserBubbleFG so text
|
|
// remains legible against the tint.
|
|
func (t Theme) UserBubble(s string, width int) string {
|
|
visible := visibleWidth(s)
|
|
if visible < width {
|
|
s += strings.Repeat(" ", width-visible)
|
|
}
|
|
return sgrFG(t.UserBubbleFG) + sgrBGColor(t.UserBubbleBG) + s + reset
|
|
}
|
|
|
|
// UserBubbleRow renders one user-bubble row prefixed with a coloured
|
|
// half-block accent bar ("▌ ") so every line of the bubble has the
|
|
// zot-blue gutter at the very left. The bar lives outside the bubble
|
|
// tint (chat bg) so the bubble itself sits inside it. Width is the
|
|
// outer width including the bar; the bubble content is padded to
|
|
// width-2 (the bar + its trailing space).
|
|
func (t Theme) UserBubbleRow(content string, width int) string {
|
|
// Bar plus a single space gutter, in the assistant accent colour
|
|
// so it matches the tool-box / app accent and reads as zot's voice
|
|
// marker. Two cells wide.
|
|
bar := t.FG256(t.Assistant, "▌ ")
|
|
bubbleW := width - 2
|
|
if bubbleW < 1 {
|
|
bubbleW = 1
|
|
}
|
|
return bar + t.UserBubble(content, bubbleW)
|
|
}
|
|
|
|
// Bold wraps s in bold SGR.
|
|
func Bold(s string) string { return "\x1b[1m" + s + "\x1b[22m" }
|
|
|
|
// Dim wraps s in dim SGR.
|
|
func Dim(s string) string { return "\x1b[2m" + s + "\x1b[22m" }
|
|
|
|
// Italic wraps s in italic SGR.
|
|
func Italic(s string) string { return "\x1b[3m" + s + "\x1b[23m" }
|
|
|
|
const reset = "\x1b[0m"
|
|
|
|
func sgrFG(c int) string { return "\x1b[38;5;" + itoa(c) + "m" }
|
|
func sgrBG(c int) string { return "\x1b[48;5;" + itoa(c) + "m" }
|
|
func sgrBGColor(c TerminalColor) string {
|
|
switch c.Mode {
|
|
case terminalColorANSI:
|
|
return "\x1b[" + itoa(c.Index) + "m"
|
|
case terminalColorRGB:
|
|
return "\x1b[48;2;" + itoa(c.R) + ";" + itoa(c.G) + ";" + itoa(c.B) + "m"
|
|
default:
|
|
return sgrBG(c.Index)
|
|
}
|
|
}
|
|
|
|
// small itoa to avoid pulling strconv into this hot path twice.
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
buf := [4]byte{}
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|