// 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 a theme colour in one of the colour spaces // terminals commonly support. Every semantic theme colour uses this type; // xterm-256, ANSI, and RGB values are rendered according to terminal capability. 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} } // TerminalProfile is the best-effort color snapshot reported by the // controlling terminal. The ANSI palette is optional; unknown slots are // resolved through the classic xterm-256 palette. type TerminalProfile struct { Foreground TerminalColor Background TerminalColor HasForeground bool HasBackground bool Palette [16]TerminalColor PaletteKnown uint16 TrueColor bool Light bool SchemeKnown bool } // 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)< 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 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:]) }