// 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" "sync/atomic" ) // TerminalColor describes both explicit colors and terminal-owned colors. // TerminalDefault and TerminalPaletteSlot intentionally differ from literal // xterm-256 values: an adaptive theme follows the terminal palette while a // custom numeric value retains its xterm meaning. type TerminalColor struct { Mode terminalColorMode Index int R int G int B int } type terminalColorMode int const ( terminalColorDefault terminalColorMode = iota terminalColorPalette terminalColor256 terminalColorANSI terminalColorRGB ) func TerminalDefault() TerminalColor { return TerminalColor{Mode: terminalColorDefault} } func TerminalPaletteSlot(index int) TerminalColor { return TerminalColor{Mode: terminalColorPalette, Index: index} } 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} } // ColorDepth is the terminal output capability used when resolving a color. type ColorDepth uint8 const ( // Indexed256 is the zero value to preserve the legacy behavior for // profiles assembled by callers that predate ColorDepth. ColorDepthIndexed256 ColorDepth = iota ColorDepthANSI16 ColorDepthTrueColor ) // TerminalProfile is the best-effort color snapshot reported by the // controlling terminal. Palette entries are optional; an unknown slot is // emitted as its direct ANSI SGR rather than substituted with a fixed zut RGB. type TerminalProfile struct { Foreground TerminalColor Background TerminalColor HasForeground bool HasBackground bool Palette [16]TerminalColor PaletteKnown uint16 Depth ColorDepth // TrueColor is retained for callers compiled against the older profile. // Depth is authoritative when it is ColorDepthTrueColor. TrueColor bool Light bool SchemeKnown bool } func (p TerminalProfile) ColorDepth() ColorDepth { if p.Depth == ColorDepthTrueColor || p.TrueColor { return ColorDepthTrueColor } if p.Depth == ColorDepthANSI16 { return ColorDepthANSI16 } return ColorDepthIndexed256 } // PaletteColor returns a reported ANSI palette entry when available. func (p TerminalProfile) PaletteColor(index int) (TerminalColor, bool) { if index < 0 || index >= len(p.Palette) || p.PaletteKnown&(uint16(1)< 15 { return 15 } return index } func ansiIndexForegroundSGR(index int) int { index = clampANSIIndex(index) if index < 8 { return 30 + index } return 90 + index - 8 } func nearestANSIColor(index int) int { r, g, b := xterm256RGB(clampXtermIndex(index)) best, bestDistance := 0, int(^uint(0)>>1) for slot := 0; slot < 16; slot++ { pr, pg, pb := xterm256RGB(slot) dr, dg, db := r-pr, g-pg, b-pb distance := dr*dr + dg*dg + db*db if distance < bestDistance { best, bestDistance = slot, distance } } return best } func clampXtermIndex(index int) int { if index < 0 { return 0 } if index > 255 { return 255 } return index } func clampByte(value int) int { if value < 0 { return 0 } if value > 255 { return 255 } return value } func clampPercent(percent int) int { if percent < 0 { return 0 } if percent > 100 { return 100 } return percent } func rgbForTerminalColor(color TerminalColor) ([3]int, bool) { switch color.Mode { case terminalColorRGB: return [3]int{clampByte(color.R), clampByte(color.G), clampByte(color.B)}, true case terminalColor256: r, g, b := xterm256RGB(clampXtermIndex(color.Index)) return [3]int{r, g, b}, true case terminalColorPalette: r, g, b := xterm256RGB(clampANSIIndex(color.Index)) return [3]int{r, g, b}, true case terminalColorANSI: if index, ok := ansiSGRToXtermIndex(color.Index); ok { r, g, b := xterm256RGB(index) return [3]int{r, g, b}, true } } return [3]int{}, false } func ansiSGRToXtermIndex(sgr int) (int, bool) { switch { case sgr >= 30 && sgr <= 37: return sgr - 30, true case sgr >= 90 && sgr <= 97: return sgr - 90 + 8, true case sgr >= 40 && sgr <= 47: return sgr - 40, true case sgr >= 100 && sgr <= 107: return sgr - 100 + 8, true default: return 0, false } } func ansiForegroundSGR(sgr int) int { switch { case sgr >= 40 && sgr <= 47: return sgr - 10 case sgr >= 100 && sgr <= 107: return sgr - 10 default: return sgr } } func ansiBackgroundSGR(sgr int) int { switch { case sgr >= 30 && sgr <= 37: return sgr + 10 case sgr >= 90 && sgr <= 97: return sgr + 10 default: return sgr } } func blendChannel(from, to, percent int) int { return clampByte((from*(100-percent) + to*percent + 50) / 100) } const xtermColorCacheSize = 256 type xtermColorCacheEntry struct { value atomic.Uint64 } var xtermColorCache [xtermColorCacheSize]xtermColorCacheEntry func nearestXtermColor(r, g, b int) int { r = clampByte(r) g = clampByte(g) b = clampByte(b) key := uint64(r)<<16 | uint64(g)<<8 | uint64(b) entry := &xtermColorCache[(key*2654435761)&(xtermColorCacheSize-1)] cached := entry.value.Load() keyPrefix := (key + 1) << 8 if cached&^uint64(0xff) == keyPrefix { return int(cached & 0xff) } best := 0 bestDistance := int(^uint(0) >> 1) for index := 0; index < 256; index++ { cr, cg, cb := xterm256RGB(index) dr := r - cr dg := g - cg db := b - cb distance := dr*dr + dg*dg + db*db if distance < bestDistance { best = index bestDistance = distance } } entry.value.Store(keyPrefix | uint64(best)) return best } // 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:]) }