v0.1.1 true color extracted to a standalone package, lut256 lazy build

This commit is contained in:
2026-07-15 07:53:38 -04:00
parent aa22225c61
commit 9853ac0270
33 changed files with 480 additions and 745 deletions
-187
View File
@@ -1,187 +0,0 @@
package terminal
import "math"
const softLightLUTSize = 256
// Perez SoftLight lookup tables (array access, no pointers)
// Pre-computed at init to avoid sqrt/division in per-cell loops
var (
softLightG [softLightLUTSize]float64
softLightDF [softLightLUTSize]float64
)
func init() {
for i := range softLightLUTSize {
df := float64(i) / 255.0
softLightDF[i] = df
if df <= 0.25 {
softLightG[i] = ((16.0*df-12.0)*df + 4.0) * df
} else {
softLightG[i] = math.Sqrt(df)
}
}
}
// clampU8 converts float to uint8 with saturation
func clampU8(v float64) uint8 {
if v >= 255.0 {
return 255
}
if v <= 0.0 {
return 0
}
// Round-half-up; unifies rounding across Blend/Scale/Lerp/SoftLight
return uint8(v + 0.5)
}
// addU8 is saturating uint8 addition
func addU8(a, b uint8) uint8 {
sum := int(a) + int(b)
if sum > 255 {
return 255
}
return uint8(sum)
}
// fastDiv255 approximates x / 255 using integer math: (x + (x >> 8) + 1) >> 8
// Faster than DIV instruction, exact for x in [0, 255*255]
func fastDiv255(x int) int {
return (x + (x >> 8) + 1) >> 8
}
// softLightChannel applies Perez soft light to one channel via LUTs
func softLightChannel(d, s uint8, intensity float64) uint8 {
df := softLightDF[d]
sf := softLightDF[s]
var result float64
if sf < 0.5 {
result = df - (1.0-2.0*sf)*df*(1.0-df)
} else {
// LUT replaces math.Sqrt
result = df + (2.0*sf-1.0)*(softLightG[d]-df)
}
// Lerp toward result by intensity, single dependency chain
result = df + (result-df)*intensity
return clampU8(result * 255.0)
}
// overlayChannel combines multiply (d < 128) and screen (d >= 128),
// preserving destination highlights and shadows
func overlayChannel(d, s uint8) uint8 {
if d < 128 {
return uint8(fastDiv255(2 * int(d) * int(s)))
}
return uint8(255 - fastDiv255(2*(255-int(d))*(255-int(s))))
}
// Blend performs linear alpha blend of src over dst
// alpha <= 0 returns dst, alpha >= 1 returns src
func Blend(dst, src RGB, alpha float64) RGB {
if alpha >= 1.0 {
return src
}
if alpha <= 0.0 {
return dst
}
inv := 1.0 - alpha
return RGB{
R: clampU8(float64(src.R)*alpha + float64(dst.R)*inv),
G: clampU8(float64(src.G)*alpha + float64(dst.G)*inv),
B: clampU8(float64(src.B)*alpha + float64(dst.B)*inv),
}
}
// SoftLight applies Perez soft light blend, gentler than linear alpha
// intensity in [0,1] mixes between dst and the blended result
func SoftLight(dst, src RGB, intensity float64) RGB {
return RGB{
R: softLightChannel(dst.R, src.R, intensity),
G: softLightChannel(dst.G, src.G, intensity),
B: softLightChannel(dst.B, src.B, intensity),
}
}
// Max returns per-channel maximum, alpha-blended over dst
func Max(dst, src RGB, alpha float64) RGB {
if alpha <= 0.0 {
return dst
}
maxed := RGB{
R: max(dst.R, src.R),
G: max(dst.G, src.G),
B: max(dst.B, src.B),
}
if alpha >= 1.0 {
return maxed
}
return Blend(dst, maxed, alpha)
}
// Add performs saturating additive blend, alpha-blended over dst
func Add(dst, src RGB, alpha float64) RGB {
if alpha <= 0.0 {
return dst
}
added := RGB{
R: addU8(dst.R, src.R),
G: addU8(dst.G, src.G),
B: addU8(dst.B, src.B),
}
if alpha >= 1.0 {
return added
}
return Blend(dst, added, alpha)
}
// Screen applies 1-(1-dst)*(1-src), alpha-blended over dst
// Always lightens; useful for glow accumulation without clipping harshness of Add
func Screen(dst, src RGB, alpha float64) RGB {
if alpha <= 0.0 {
return dst
}
screened := RGB{
R: uint8(255 - fastDiv255((255-int(dst.R))*(255-int(src.R)))),
G: uint8(255 - fastDiv255((255-int(dst.G))*(255-int(src.G)))),
B: uint8(255 - fastDiv255((255-int(dst.B))*(255-int(src.B)))),
}
if alpha >= 1.0 {
return screened
}
return Blend(dst, screened, alpha)
}
// Overlay combines multiply (darks) and screen (lights), alpha-blended over dst
func Overlay(dst, src RGB, alpha float64) RGB {
if alpha <= 0.0 {
return dst
}
overlaid := RGB{
R: overlayChannel(dst.R, src.R),
G: overlayChannel(dst.G, src.G),
B: overlayChannel(dst.B, src.B),
}
if alpha >= 1.0 {
return overlaid
}
return Blend(dst, overlaid, alpha)
}
// Scale multiplies all channels by factor, saturating (factor > 1.0 brightens)
func Scale(c RGB, factor float64) RGB {
return RGB{
R: clampU8(float64(c.R) * factor),
G: clampU8(float64(c.G) * factor),
B: clampU8(float64(c.B) * factor),
}
}
// Grayscale converts to grayscale using Rec. 601 luma coefficients
// Y = R*0.299 + G*0.587 + B*0.114, integer math
func Grayscale(c RGB) RGB {
gray := uint8((int(c.R)*299 + int(c.G)*587 + int(c.B)*114) / 1000)
return RGB{R: gray, G: gray, B: gray}
}
+72 -71
View File
@@ -1,5 +1,12 @@
package terminal package terminal
import (
"sync"
"sync/atomic"
"github.com/lixenwraith/color"
)
// ColorMode indicates terminal color capability // ColorMode indicates terminal color capability
type ColorMode uint8 type ColorMode uint8
@@ -8,85 +15,89 @@ const (
ColorModeTrueColor // 24-bit RGB ColorModeTrueColor // 24-bit RGB
) )
// RGB represents a 24-bit color
type RGB struct {
R uint8 `toml:"r"`
G uint8 `toml:"g"`
B uint8 `toml:"b"`
}
// Lerp linearly interpolates from c to other by t, clamped to [0,1]
func (c RGB) Lerp(other RGB, t float64) RGB {
if t <= 0 {
return c
}
if t >= 1 {
return other
}
return RGB{
R: clampU8(float64(c.R) + (float64(other.R)-float64(c.R))*t),
G: clampU8(float64(c.G) + (float64(other.G)-float64(c.G))*t),
B: clampU8(float64(c.B) + (float64(other.B)-float64(c.B))*t),
}
}
// RGBBlack is the zero value black color
var RGBBlack = RGB{0, 0, 0}
// 6-bit quantized LUT for Redmean-based 256-color mapping // 6-bit quantized LUT for Redmean-based 256-color mapping
// 64×64×64 = 262,144 bytes, fits in L2 cache // 64×64×64 = 262,144 bytes, fits in L2 cache
var lut256 [64 * 64 * 64]uint8 const lut256Size = 64 * 64 * 64
func init() { // init() -> first-use build. Construction is ~63M Redmean evaluations
// Pre-compute Redmean-based palette mapping for all 6-bit quantized RGB values // and 256 KiB resident; truecolor sessions never read the table.
for r := 0; r < 64; r++ { var (
for g := 0; g < 64; g++ { lut256Ptr atomic.Pointer[[lut256Size]uint8]
for b := 0; b < 64; b++ { lut256Once sync.Once
// Expand 6-bit to 8-bit (shift left 2, add 2 for midpoint) )
r8 := (r << 2) | 2
g8 := (g << 2) | 2 // lut256 returns the palette LUT, building it on first use.
b8 := (b << 2) | 2 // Fast path is an acquire load plus a predictable branch, and inlines into RGBTo256.
lut256[r<<12|g<<6|b] = computeRedmean256(r8, g8, b8) func lut256() *[lut256Size]uint8 {
} if p := lut256Ptr.Load(); p != nil {
} return p
} }
return lut256Build()
} }
// computeRedmean256 finds the nearest 256-palette index using Redmean distance // lut256Build populates the table. Cold path, kept out of line so lut256 stays inlinable.
// Called only at init() to populate LUT // The release store publishes the fully written array to acquire loads in lut256.
func computeRedmean256(r, g, b int) uint8 { //
//go:noinline
func lut256Build() *[lut256Size]uint8 {
lut256Once.Do(func() {
t := new([lut256Size]uint8)
for r := range 64 {
for g := range 64 {
for b := range 64 {
// Expand 6-bit to 8-bit (shift left 2, add 2 for midpoint)
c := color.RGB{
R: uint8(r<<2 | 2),
G: uint8(g<<2 | 2),
B: uint8(b<<2 | 2),
}
t[r<<12|g<<6|b] = computeRedmean256(c)
}
}
}
lut256Ptr.Store(t)
})
return lut256Ptr.Load()
}
// WarmPalette256 forces LUT construction. Idempotent, safe for concurrent use.
// Call before the first render when ColorMode is ColorMode256.
// RGBTo256 runs on the render path under the terminal mutex; a lazy build there stalls the frame.
func WarmPalette256() { _ = lut256() }
// computeRedmean256 finds the nearest 256-palette index using Redmean distance called from lut256Build, not init()
func computeRedmean256(c color.RGB) uint8 {
// Grayscale fast path // Grayscale fast path
if r == g && g == b { if c.R == c.G && c.G == c.B {
if r < 8 { if c.R < 8 {
return 16 return 16
} }
if r > 238 { if c.R > 238 {
return 231 return 231
} }
return uint8(232 + (r-8)/10) return uint8(232 + (int(c.R)-8)/10)
} }
bestIdx := uint8(16) bestIdx := uint8(16)
minDist := 1 << 30 minDist := 1 << 30
// Search 6×6×6 cube (indices 16-231) // Search 6×6×6 cube (indices 16-231)
for i := 0; i < 216; i++ { for i := range 216 {
cr := cubeValues[i/36] cand := color.RGB{
cg := cubeValues[(i/6)%6] R: cubeValues[i/36],
cb := cubeValues[i%6] G: cubeValues[(i/6)%6],
B: cubeValues[i%6],
d := redmeanDistance(r, g, b, cr, cg, cb) }
if d < minDist { if d := color.RedmeanDistance(c, cand); d < minDist {
minDist = d minDist = d
bestIdx = uint8(16 + i) bestIdx = uint8(16 + i)
} }
} }
// Search grayscale ramp (indices 232-255) // Search grayscale ramp (indices 232-255)
for i := 0; i < 24; i++ { for i := range 24 {
gray := 8 + i*10 g := uint8(8 + i*10)
d := redmeanDistance(r, g, b, gray, gray, gray) if d := color.RedmeanDistance(c, color.RGB{R: g, G: g, B: g}); d < minDist {
if d < minDist {
minDist = d minDist = d
bestIdx = uint8(232 + i) bestIdx = uint8(232 + i)
} }
@@ -95,21 +106,11 @@ func computeRedmean256(r, g, b int) uint8 {
return bestIdx return bestIdx
} }
// redmeanDistance calculates perceptually-weighted color distance
// Formula: https://en.wikipedia.org/wiki/Color_difference#sRGB
func redmeanDistance(r1, g1, b1, r2, g2, b2 int) int {
rmean := (r1 + r2) / 2
dr := r1 - r2
dg := g1 - g2
db := b1 - b2
return (((512 + rmean) * dr * dr) >> 8) + 4*dg*dg + (((767 - rmean) * db * db) >> 8)
}
// Color cube values for 6×6×6 palette (indices 16-231) // Color cube values for 6×6×6 palette (indices 16-231)
var cubeValues = [6]int{0, 95, 135, 175, 215, 255} var cubeValues = [6]uint8{0, 95, 135, 175, 215, 255}
// RGBTo256 converts RGB to nearest 256-color palette index // RGBTo256 converts RGB to nearest 256-color palette index.
// O(1) lookup via pre-computed Redmean LUT // O(1) via the Redmean LUT; the first call builds it (see WarmPalette256).
func RGBTo256(c RGB) uint8 { func RGBTo256(c color.RGB) uint8 {
return lut256[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)] return lut256()[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)]
} }
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"os" "os"
"time" "time"
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
"github.com/lixenwraith/terminal/inline" "github.com/lixenwraith/terminal/inline"
) )
@@ -12,9 +13,9 @@ import (
func main() { func main() {
p := inline.New(os.Stdout) p := inline.New(os.Stdout)
name := inline.Fg(terminal.LightSkyBlue).Attr(terminal.AttrBold) name := inline.Fg(color.LightSkyBlue).Attr(terminal.AttrBold)
okSt := inline.Fg(terminal.LimeGreen).Attr(terminal.AttrBold) okSt := inline.Fg(color.LimeGreen).Attr(terminal.AttrBold)
dim := inline.Fg(terminal.IronGray) dim := inline.Fg(color.IronGray)
pkgs := []string{"openssl", "zlib", "curl", "git", "go"} pkgs := []string{"openssl", "zlib", "curl", "git", "go"}
frame := 0 frame := 0
+2 -1
View File
@@ -1,8 +1,9 @@
module github.com/lixenwraith/terminal module github.com/lixenwraith/terminal
go 1.26.4 go 1.26.0
require ( require (
github.com/lixenwraith/color v0.0.0-20260715115131-49124ee97342
golang.org/x/sys v0.47.0 golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0 golang.org/x/term v0.45.0
) )
+2
View File
@@ -1,3 +1,5 @@
github.com/lixenwraith/color v0.0.0-20260715115131-49124ee97342 h1:KhjVSKhsHrngqeQ6sIcXX7HMmocixQ7ZZCumycbEKBo=
github.com/lixenwraith/color v0.0.0-20260715115131-49124ee97342/go.mod h1:p02MsAGqlmZu3sc6BPYCKmaedF+lqBoijnuu5cOrYeo=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
+4
View File
@@ -48,6 +48,10 @@ func New(w io.Writer) *Printer {
} }
p.color = p.tty != nil && os.Getenv("NO_COLOR") == "" p.color = p.tty != nil && os.Getenv("NO_COLOR") == ""
p.mode = terminal.DetectColorMode() p.mode = terminal.DetectColorMode()
// Keep the LUT build out of the first Paint call
if p.color && p.mode == terminal.ColorMode256 {
terminal.WarmPalette256()
}
return p return p
} }
+4 -3
View File
@@ -7,22 +7,23 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
// Style describes text appearance; zero value is unstyled. // Style describes text appearance; zero value is unstyled.
// Composable: inline.Fg(terminal.Amber).Attr(terminal.AttrBold) // Composable: inline.Fg(terminal.Amber).Attr(terminal.AttrBold)
type Style struct { type Style struct {
fg, bg terminal.RGB fg, bg color.RGB
hasFg, hasBg bool hasFg, hasBg bool
attr terminal.Attr attr terminal.Attr
} }
// Fg starts a style with foreground color // Fg starts a style with foreground color
func Fg(c terminal.RGB) Style { return Style{fg: c, hasFg: true} } func Fg(c color.RGB) Style { return Style{fg: c, hasFg: true} }
// Bg sets background color // Bg sets background color
func (s Style) Bg(c terminal.RGB) Style { s.bg, s.hasBg = c, true; return s } func (s Style) Bg(c color.RGB) Style { s.bg, s.hasBg = c, true; return s }
// Attr adds attribute bits // Attr adds attribute bits
func (s Style) Attr(a terminal.Attr) Style { s.attr |= a; return s } func (s Style) Attr(a terminal.Attr) Style { s.attr |= a; return s }
+11 -9
View File
@@ -2,6 +2,8 @@ package terminal
import ( import (
"bufio" "bufio"
"github.com/lixenwraith/color"
) )
// outputBuffer manages double-buffered terminal output with diffing // outputBuffer manages double-buffered terminal output with diffing
@@ -17,8 +19,8 @@ type outputBuffer struct {
cursorValid bool cursorValid bool
// Style state for coalescing // Style state for coalescing
lastFg RGB lastFg color.RGB
lastBg RGB lastBg color.RGB
lastAttr Attr lastAttr Attr
lastValid bool lastValid bool
} }
@@ -251,7 +253,7 @@ func (o *outputBuffer) moveCursorTo(w *bufio.Writer, x, y int) {
} }
// writeStyleCoalesced emits a single combined SGR sequence when style changes // writeStyleCoalesced emits a single combined SGR sequence when style changes
func (o *outputBuffer) writeStyleCoalesced(w *bufio.Writer, fg, bg RGB, attr Attr) { func (o *outputBuffer) writeStyleCoalesced(w *bufio.Writer, fg, bg color.RGB, attr Attr) {
// Check what changed // Check what changed
fgChanged := !o.lastValid || fg != o.lastFg || (attr&AttrFg256) != (o.lastAttr&AttrFg256) fgChanged := !o.lastValid || fg != o.lastFg || (attr&AttrFg256) != (o.lastAttr&AttrFg256)
bgChanged := !o.lastValid || bg != o.lastBg || (attr&AttrBg256) != (o.lastAttr&AttrBg256) bgChanged := !o.lastValid || bg != o.lastBg || (attr&AttrBg256) != (o.lastAttr&AttrBg256)
@@ -340,7 +342,7 @@ func (o *outputBuffer) writeStyleCoalesced(w *bufio.Writer, fg, bg RGB, attr Att
} }
// writeFgInline writes fg color parameters (no CSI prefix, no 'm' suffix) // writeFgInline writes fg color parameters (no CSI prefix, no 'm' suffix)
func (o *outputBuffer) writeFgInline(w *bufio.Writer, fg RGB, attr Attr) { func (o *outputBuffer) writeFgInline(w *bufio.Writer, fg color.RGB, attr Attr) {
w.WriteByte(';') w.WriteByte(';')
if attr&AttrFg256 != 0 { if attr&AttrFg256 != 0 {
// 256-color: 38;5;N // 256-color: 38;5;N
@@ -362,7 +364,7 @@ func (o *outputBuffer) writeFgInline(w *bufio.Writer, fg RGB, attr Attr) {
} }
// writeBgInline writes bg color parameters (no CSI prefix, no 'm' suffix) // writeBgInline writes bg color parameters (no CSI prefix, no 'm' suffix)
func (o *outputBuffer) writeBgInline(w *bufio.Writer, bg RGB, attr Attr) { func (o *outputBuffer) writeBgInline(w *bufio.Writer, bg color.RGB, attr Attr) {
w.WriteByte(';') w.WriteByte(';')
if attr&AttrBg256 != 0 { if attr&AttrBg256 != 0 {
// 256-color: 48;5;N // 256-color: 48;5;N
@@ -384,7 +386,7 @@ func (o *outputBuffer) writeBgInline(w *bufio.Writer, bg RGB, attr Attr) {
} }
// writeFgFull writes complete fg color sequence // writeFgFull writes complete fg color sequence
func (o *outputBuffer) writeFgFull(w *bufio.Writer, fg RGB, attr Attr) { func (o *outputBuffer) writeFgFull(w *bufio.Writer, fg color.RGB, attr Attr) {
if attr&AttrFg256 != 0 { if attr&AttrFg256 != 0 {
w.Write(csiFg256) w.Write(csiFg256)
writeInt(w, int(fg.R)) writeInt(w, int(fg.R))
@@ -405,7 +407,7 @@ func (o *outputBuffer) writeFgFull(w *bufio.Writer, fg RGB, attr Attr) {
} }
// writeBgFull writes complete bg color sequence // writeBgFull writes complete bg color sequence
func (o *outputBuffer) writeBgFull(w *bufio.Writer, bg RGB, attr Attr) { func (o *outputBuffer) writeBgFull(w *bufio.Writer, bg color.RGB, attr Attr) {
if attr&AttrBg256 != 0 { if attr&AttrBg256 != 0 {
w.Write(csiBg256) w.Write(csiBg256)
writeInt(w, int(bg.R)) writeInt(w, int(bg.R))
@@ -435,7 +437,7 @@ func (o *outputBuffer) forceFullRedraw() {
} }
// clear writes a clear screen with specified background // clear writes a clear screen with specified background
func (o *outputBuffer) clear(bg RGB) { func (o *outputBuffer) clear(bg color.RGB) {
w := o.writer w := o.writer
w.Write(csiSGR0) w.Write(csiSGR0)
o.writeBgFull(w, bg, 0) o.writeBgFull(w, bg, 0)
@@ -453,4 +455,4 @@ func (o *outputBuffer) clear(bg RGB) {
// invalidateCursor marks cursor position as unknown // invalidateCursor marks cursor position as unknown
func (o *outputBuffer) invalidateCursor() { func (o *outputBuffer) invalidateCursor() {
o.cursorValid = false o.cursorValid = false
} }
-159
View File
@@ -1,159 +0,0 @@
package terminal
// Generic TrueColor palette — pure RGB definitions without game semantics
// Game systems and renderers reference these via aliases in their own parameter files
//
// Naming: standard color names where RGB closely matches (CSS, X11, Pantone-adjacent),
// descriptive compound names otherwise. Ordered dark-to-light within each hue group.
var (
// --- Achromatic ---
Black = RGB{0, 0, 0}
Charcoal = RGB{5, 5, 5}
Obsidian = RGB{20, 20, 30} // Blue-black
Gunmetal = RGB{26, 27, 38} // Blue-tinted near-black
DarkSlate = RGB{35, 36, 48} // Blue-gray near-black
DimGray = RGB{55, 55, 55}
DarkGray = RGB{60, 60, 60}
IronGray = RGB{80, 80, 80}
SlateGray = RGB{80, 80, 90} // Cool-tinted
Taupe = RGB{100, 95, 85} // Warm gray
Gray = RGB{120, 120, 120}
MidGray = RGB{128, 128, 128}
CoolSilver = RGB{140, 145, 155} // Blue-tinted silver
DimSilver = RGB{155, 155, 155}
Silver = RGB{180, 180, 180}
LightGray = RGB{200, 200, 200}
NearWhite = RGB{250, 250, 250}
White = RGB{255, 255, 255}
// --- Brown / Earth ---
Chocolate = RGB{90, 25, 15}
SaddleBrown = RGB{101, 67, 33}
DarkRust = RGB{140, 35, 25}
Sienna = RGB{140, 60, 0}
DarkPlum = RGB{60, 30, 40} // Warm dark purple-brown
BlueCharcoal = RGB{40, 45, 60} // Cool dark blue-gray
// --- Red ---
BlackRed = RGB{50, 15, 15}
Oxblood = RGB{100, 20, 20}
DarkBurgundy = RGB{100, 25, 20}
DarkCrimson = RGB{139, 0, 0}
Brick = RGB{180, 40, 40}
Cinnabar = RGB{200, 60, 50}
IndianRed = RGB{180, 60, 60}
BurntSienna = RGB{200, 60, 25}
Vermilion = RGB{227, 66, 82}
Red = RGB{255, 0, 0}
BrightRed = RGB{255, 60, 60}
Coral = RGB{255, 80, 80}
Salmon = RGB{255, 100, 100}
LightCoral = RGB{255, 140, 140}
LightRose = RGB{255, 150, 150}
MistyRose = RGB{255, 200, 200}
// --- Orange ---
DarkAmber = RGB{60, 40, 0}
Rust = RGB{180, 60, 20}
Amber = RGB{180, 120, 0}
Bronze = RGB{200, 100, 0}
BurntOrange = RGB{200, 110, 0}
Terracotta = RGB{220, 100, 50}
FlameOrange = RGB{240, 100, 30}
OrangeRed = RGB{255, 69, 0}
RedOrange = RGB{255, 80, 40}
Mango = RGB{255, 120, 50}
TigerOrange = RGB{255, 140, 0}
WarmOrange = RGB{255, 140, 40}
Apricot = RGB{255, 160, 60}
Orange = RGB{255, 165, 0}
// --- Yellow ---
DarkGold = RGB{200, 150, 0}
OliveYellow = RGB{200, 180, 60}
Gold = RGB{255, 215, 0}
LemonYellow = RGB{255, 240, 60}
Yellow = RGB{255, 255, 0}
PaleGold = RGB{255, 200, 100}
Buttercream = RGB{255, 250, 150}
PaleLemon = RGB{255, 255, 100}
Ivory = RGB{255, 255, 220}
Cream = RGB{255, 255, 200}
// --- Green ---
BlackGreen = RGB{0, 40, 0}
DarkFern = RGB{30, 80, 25}
DeepForest = RGB{25, 80, 35}
HunterGreen = RGB{35, 90, 30}
DarkGreen = RGB{15, 130, 15}
ForestGreen = RGB{34, 139, 34}
MediumGreen = RGB{40, 150, 40}
FernGreen = RGB{50, 140, 45}
LeafGreen = RGB{60, 160, 60}
SeaGreen = RGB{60, 180, 80}
SageGreen = RGB{70, 170, 100}
GrassGreen = RGB{70, 180, 55}
EmeraldGreen = RGB{60, 220, 100}
MintGreen = RGB{100, 220, 130}
BrightGreen = RGB{20, 200, 20}
YellowGreen = RGB{100, 220, 80}
LimeGreen = RGB{50, 205, 50}
NeonGreen = RGB{50, 255, 50}
BrightLime = RGB{120, 255, 80}
Lime = RGB{0, 255, 0}
LightGreen = RGB{144, 238, 144}
PaleGreen = RGB{120, 255, 120}
PastelGreen = RGB{100, 220, 100}
PaleMint = RGB{150, 255, 180}
Honeydew = RGB{200, 255, 200}
// --- Cyan / Teal ---
Teal = RGB{0, 139, 139}
DimCyan = RGB{0, 160, 160}
VibrantCyan = RGB{0, 200, 200}
DarkTurquoise = RGB{0, 206, 209}
BrightCyan = RGB{0, 220, 220}
Cyan = RGB{0, 255, 255}
SkyTeal = RGB{80, 200, 220}
PaleCyan = RGB{200, 255, 255}
AliceBlue = RGB{230, 245, 255}
IceCyan = RGB{240, 255, 255}
// --- Blue ---
DeepNavy = RGB{15, 25, 50}
DeepIndigo = RGB{40, 0, 180}
NavyBlue = RGB{30, 60, 120}
CobaltBlue = RGB{50, 80, 200}
SteelBlue = RGB{60, 100, 180}
MediumBlue = RGB{60, 120, 200}
RoyalBlue = RGB{65, 105, 225}
CeruleanBlue = RGB{80, 140, 220}
Cornflower = RGB{80, 130, 255}
DodgerBlue = RGB{40, 180, 255}
LightBlue = RGB{120, 170, 255}
LightSkyBlue = RGB{135, 206, 250}
BabyBlue = RGB{160, 210, 255}
Blue = RGB{0, 0, 255}
// --- Purple / Violet ---
DeepPurple = RGB{60, 20, 80}
DarkViolet = RGB{120, 40, 180}
MutedPurple = RGB{160, 100, 160}
MediumPurple = RGB{170, 100, 210}
ElectricViolet = RGB{180, 130, 255}
LightOrchid = RGB{200, 130, 210}
Orchid = RGB{200, 120, 220}
PaleVioletRed = RGB{219, 112, 147}
SoftLavender = RGB{220, 150, 230}
PaleLavender = RGB{220, 180, 255}
// --- Pink / Rose ---
RoseRed = RGB{255, 60, 120}
HotMagenta = RGB{255, 60, 200}
HotPink = RGB{255, 140, 200}
PalePink = RGB{255, 145, 220}
LightPink = RGB{255, 182, 193}
Pink = RGB{255, 192, 203}
Magenta = RGB{255, 0, 255}
)
+14 -7
View File
@@ -5,6 +5,8 @@ import (
"os" "os"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/lixenwraith/color"
) )
// Attr represents text attributes (bitmask) // Attr represents text attributes (bitmask)
@@ -28,8 +30,8 @@ const AttrStyle Attr = AttrBold | AttrDim | AttrItalic | AttrUnderline | AttrBli
// Cell represents a single terminal cell // Cell represents a single terminal cell
type Cell struct { type Cell struct {
Rune rune Rune rune
Fg RGB Fg color.RGB
Bg RGB Bg color.RGB
Attrs Attr Attrs Attr
} }
@@ -55,7 +57,7 @@ type Terminal interface {
Flush(cells []Cell, width, height int) Flush(cells []Cell, width, height int)
// Clear fills screen with specified background color // Clear fills screen with specified background color
Clear(bg RGB) Clear(bg color.RGB)
// SetCursorVisible shows/hides cursor // SetCursorVisible shows/hides cursor
SetCursorVisible(visible bool) SetCursorVisible(visible bool)
@@ -172,8 +174,13 @@ func (t *termImpl) Init() error {
// Invisible cursor // Invisible cursor
t.cursorVisible.Store(false) t.cursorVisible.Store(false)
// Build the LUT at startup, not on the first frame under t.mu
if t.output.colorMode == ColorMode256 {
WarmPalette256()
}
// Clear screen // Clear screen
t.output.clear(RGBBlack) t.output.clear(color.Black)
// Start input reader // Start input reader
t.input.start() t.input.start()
@@ -259,7 +266,7 @@ func (t *termImpl) Flush(cells []Cell, width, height int) {
} }
// Clear fills screen with background color // Clear fills screen with background color
func (t *termImpl) Clear(bg RGB) { func (t *termImpl) Clear(bg color.RGB) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
@@ -336,7 +343,7 @@ func (t *termImpl) Sync() {
// Clear terminal before full redraw // Clear terminal before full redraw
// Diff-based rendering assumes physical terminal matches front buffer state // Diff-based rendering assumes physical terminal matches front buffer state
t.output.clear(RGBBlack) t.output.clear(color.Black)
t.output.forceFullRedraw() t.output.forceFullRedraw()
} }
@@ -455,4 +462,4 @@ func EmergencyReset(w io.Writer) {
// Attempt raw mode reset via stty - escape sequences alone don't restore termios // Attempt raw mode reset via stty - escape sequences alone don't restore termios
// This is best-effort; ignore errors in crash context // This is best-effort; ignore errors in crash context
resetTerminalMode() resetTerminalMode()
} }
+1 -1
View File
@@ -200,7 +200,7 @@ Pure logic, no rendering — usable independently:
- Width calculations count runes, not terminal columns; East Asian wide - Width calculations count runes, not terminal columns; East Asian wide
characters and combining marks are not width-aware. characters and combining marks are not width-aware.
- Zero-value `terminal.RGB` in style fields generally means "inherit" - Zero-value `color.RGB` in style fields generally means "inherit"
(widget default or row background) — check specific widget docs. (widget default or row background) — check specific widget docs.
- Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require - Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require
application-side geometry from the regions used. application-side geometry from the regions used.
+15 -13
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -36,7 +37,7 @@ const (
// --- Box Rendering --- // --- Box Rendering ---
// Box draws border around region edge // Box draws border around region edge
func (r Region) Box(line LineType, fg terminal.RGB) { func (r Region) Box(line LineType, fg color.RGB) {
if r.W < 2 || r.H < 2 { if r.W < 2 || r.H < 2 {
return return
} }
@@ -45,7 +46,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) {
} }
chars := boxChars[line] chars := boxChars[line]
bg := terminal.RGB{} // Transparent (use existing bg) bg := color.RGB{} // Transparent (use existing bg)
// Corners // Corners
r.Cell(0, 0, chars[boxTL], fg, bg, terminal.AttrNone) r.Cell(0, 0, chars[boxTL], fg, bg, terminal.AttrNone)
@@ -67,7 +68,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) {
} }
// BoxFilled draws border and fills interior with background // BoxFilled draws border and fills interior with background
func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) { func (r Region) BoxFilled(line LineType, fg, bg color.RGB) {
// Fill interior first // Fill interior first
for y := 1; y < r.H-1; y++ { for y := 1; y < r.H-1; y++ {
for x := 1; x < r.W-1; x++ { for x := 1; x < r.W-1; x++ {
@@ -81,7 +82,7 @@ func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) {
// --- Line rendering --- // --- Line rendering ---
// HLine draws horizontal line across region width at row y // HLine draws horizontal line across region width at row y
func (r Region) HLine(y int, line LineType, fg terminal.RGB) { func (r Region) HLine(y int, line LineType, fg color.RGB) {
if y < 0 || y >= r.H { if y < 0 || y >= r.H {
return return
} }
@@ -90,12 +91,12 @@ func (r Region) HLine(y int, line LineType, fg terminal.RGB) {
} }
ch := boxChars[line][boxH] ch := boxChars[line][boxH]
for x := 0; x < r.W; x++ { for x := 0; x < r.W; x++ {
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
} }
} }
// VLine draws vertical line across region height at column x // VLine draws vertical line across region height at column x
func (r Region) VLine(x int, line LineType, fg terminal.RGB) { func (r Region) VLine(x int, line LineType, fg color.RGB) {
if x < 0 || x >= r.W { if x < 0 || x >= r.W {
return return
} }
@@ -104,12 +105,12 @@ func (r Region) VLine(x int, line LineType, fg terminal.RGB) {
} }
ch := boxChars[line][boxV] ch := boxChars[line][boxV]
for y := 0; y < r.H; y++ { for y := 0; y < r.H; y++ {
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
} }
} }
// Divider draws horizontal line with optional centered label // Divider draws horizontal line with optional centered label
func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) { func (r Region) Divider(y int, label string, line LineType, fg color.RGB) {
if y < 0 || y >= r.H { if y < 0 || y >= r.H {
return return
} }
@@ -121,7 +122,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
// Fill with horizontal line // Fill with horizontal line
for x := 0; x < r.W; x++ { for x := 0; x < r.W; x++ {
r.Cell(x, y, hChar, fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x, y, hChar, fg, color.RGB{}, terminal.AttrNone)
} }
// Center label if provided // Center label if provided
@@ -134,7 +135,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
} }
startX := (r.W - textLen) / 2 startX := (r.W - textLen) / 2
for i, ch := range text { for i, ch := range text {
r.Cell(startX+i, y, ch, fg, terminal.RGB{}, terminal.AttrBold) r.Cell(startX+i, y, ch, fg, color.RGB{}, terminal.AttrBold)
} }
} }
} }
@@ -142,7 +143,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
// --- Card rendering --- // --- Card rendering ---
// Card draws titled border and returns inner content region // Card draws titled border and returns inner content region
func (r Region) Card(title string, line LineType, fg terminal.RGB) Region { func (r Region) Card(title string, line LineType, fg color.RGB) Region {
r.Box(line, fg) r.Box(line, fg)
if title != "" && r.W > 4 { if title != "" && r.W > 4 {
@@ -152,8 +153,9 @@ func (r Region) Card(title string, line LineType, fg terminal.RGB) Region {
displayTitle = Truncate(displayTitle, maxTitleLen) displayTitle = Truncate(displayTitle, maxTitleLen)
} }
titleX := (r.W - RuneLen(displayTitle) - 2) / 2 titleX := (r.W - RuneLen(displayTitle) - 2) / 2
r.Text(titleX, 0, " "+displayTitle+" ", fg, terminal.RGB{}, terminal.AttrBold) r.Text(titleX, 0, " "+displayTitle+" ", fg, color.RGB{}, terminal.AttrBold)
} }
return r.Inset(1) return r.Inset(1)
} }
+19 -15
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Button defines a single button in a button bar // Button defines a single button in a button bar
type Button struct { type Button struct {
@@ -18,27 +21,27 @@ type ButtonBarOpts struct {
// ButtonStyle defines button bar colors // ButtonStyle defines button bar colors
type ButtonStyle struct { type ButtonStyle struct {
LabelFg terminal.RGB LabelFg color.RGB
LabelBg terminal.RGB LabelBg color.RGB
KeyFg terminal.RGB KeyFg color.RGB
FocusFg terminal.RGB FocusFg color.RGB
FocusBg terminal.RGB FocusBg color.RGB
Bg terminal.RGB Bg color.RGB
} }
// DefaultButtonStyle returns default button colors with dark background // DefaultButtonStyle returns default button colors with dark background
func DefaultButtonStyle() ButtonStyle { func DefaultButtonStyle() ButtonStyle {
return DefaultButtonStyleFrom(terminal.RGB{R: 25, G: 25, B: 35}) return DefaultButtonStyleFrom(color.RGB{R: 25, G: 25, B: 35})
} }
// DefaultButtonStyleFrom returns default button colors using the given background // DefaultButtonStyleFrom returns default button colors using the given background
func DefaultButtonStyleFrom(bg terminal.RGB) ButtonStyle { func DefaultButtonStyleFrom(bg color.RGB) ButtonStyle {
return ButtonStyle{ return ButtonStyle{
LabelFg: terminal.RGB{R: 200, G: 200, B: 200}, LabelFg: color.RGB{R: 200, G: 200, B: 200},
LabelBg: terminal.RGB{R: 50, G: 50, B: 60}, LabelBg: color.RGB{R: 50, G: 50, B: 60},
KeyFg: terminal.RGB{R: 130, G: 130, B: 150}, KeyFg: color.RGB{R: 130, G: 130, B: 150},
FocusFg: terminal.RGB{R: 255, G: 255, B: 255}, FocusFg: color.RGB{R: 255, G: 255, B: 255},
FocusBg: terminal.RGB{R: 60, G: 80, B: 120}, FocusBg: color.RGB{R: 60, G: 80, B: 120},
Bg: bg, Bg: bg,
} }
} }
@@ -124,4 +127,5 @@ func (r Region) ButtonBar(y int, buttons []Button, opts ButtonBarOpts) {
x += gap x += gap
} }
} }
} }
+7 -5
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -15,7 +16,7 @@ const (
) )
// Checkbox draws a checkbox indicator // Checkbox draws a checkbox indicator
func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) { func (r Region) Checkbox(x, y int, state CheckState, fg color.RGB) {
if x < 0 || x+2 >= r.W || y < 0 || y >= r.H { if x < 0 || x+2 >= r.W || y < 0 || y >= r.H {
return return
} }
@@ -30,7 +31,8 @@ func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) {
case CheckPlus: case CheckPlus:
ch = '+' ch = '+'
} }
r.Cell(x, y, '[', fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x, y, '[', fg, color.RGB{}, terminal.AttrNone)
r.Cell(x+1, y, ch, fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x+1, y, ch, fg, color.RGB{}, terminal.AttrNone)
r.Cell(x+2, y, ']', fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x+2, y, ']', fg, color.RGB{}, terminal.AttrNone)
} }
+26 -22
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ConfirmResult represents dialog outcome // ConfirmResult represents dialog outcome
type ConfirmResult uint8 type ConfirmResult uint8
@@ -98,31 +101,31 @@ type ConfirmOpts struct {
// ConfirmStyle defines dialog colors // ConfirmStyle defines dialog colors
type ConfirmStyle struct { type ConfirmStyle struct {
BorderFg terminal.RGB BorderFg color.RGB
TitleFg terminal.RGB TitleFg color.RGB
MessageFg terminal.RGB MessageFg color.RGB
Bg terminal.RGB Bg color.RGB
ButtonFg terminal.RGB ButtonFg color.RGB
ButtonBg terminal.RGB ButtonBg color.RGB
ButtonFocusFg terminal.RGB ButtonFocusFg color.RGB
ButtonFocusBg terminal.RGB ButtonFocusBg color.RGB
DestructiveFg terminal.RGB DestructiveFg color.RGB
DestructiveBg terminal.RGB DestructiveBg color.RGB
} }
// DefaultConfirmStyle returns default dialog colors // DefaultConfirmStyle returns default dialog colors
func DefaultConfirmStyle() ConfirmStyle { func DefaultConfirmStyle() ConfirmStyle {
return ConfirmStyle{ return ConfirmStyle{
BorderFg: terminal.RGB{R: 100, G: 100, B: 120}, BorderFg: color.RGB{R: 100, G: 100, B: 120},
TitleFg: terminal.RGB{R: 255, G: 255, B: 255}, TitleFg: color.RGB{R: 255, G: 255, B: 255},
MessageFg: terminal.RGB{R: 200, G: 200, B: 200}, MessageFg: color.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 30, G: 30, B: 40}, Bg: color.RGB{R: 30, G: 30, B: 40},
ButtonFg: terminal.RGB{R: 180, G: 180, B: 180}, ButtonFg: color.RGB{R: 180, G: 180, B: 180},
ButtonBg: terminal.RGB{R: 50, G: 50, B: 60}, ButtonBg: color.RGB{R: 50, G: 50, B: 60},
ButtonFocusFg: terminal.RGB{R: 255, G: 255, B: 255}, ButtonFocusFg: color.RGB{R: 255, G: 255, B: 255},
ButtonFocusBg: terminal.RGB{R: 60, G: 80, B: 120}, ButtonFocusBg: color.RGB{R: 60, G: 80, B: 120},
DestructiveFg: terminal.RGB{R: 255, G: 255, B: 255}, DestructiveFg: color.RGB{R: 255, G: 255, B: 255},
DestructiveBg: terminal.RGB{R: 180, G: 60, B: 60}, DestructiveBg: color.RGB{R: 180, G: 60, B: 60},
} }
} }
@@ -321,4 +324,5 @@ func (r Region) AlertDialog(opts AlertOpts) Region {
} }
return content.Sub(0, 0, content.W, buttonY-1) return content.Sub(0, 0, content.W, buttonY-1)
} }
+19 -17
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -16,27 +17,27 @@ type EditorOpts struct {
// EditorStyle defines editor colors // EditorStyle defines editor colors
type EditorStyle struct { type EditorStyle struct {
TextFg terminal.RGB TextFg color.RGB
TextBg terminal.RGB TextBg color.RGB
CursorFg terminal.RGB CursorFg color.RGB
CursorBg terminal.RGB CursorBg color.RGB
LineNumFg terminal.RGB LineNumFg color.RGB
LineNumBg terminal.RGB LineNumBg color.RGB
CurrentLineBg terminal.RGB CurrentLineBg color.RGB
BorderFg terminal.RGB BorderFg color.RGB
} }
// DefaultEditorStyle returns default colors // DefaultEditorStyle returns default colors
func DefaultEditorStyle() EditorStyle { func DefaultEditorStyle() EditorStyle {
return EditorStyle{ return EditorStyle{
TextFg: terminal.RGB{R: 220, G: 220, B: 220}, TextFg: color.RGB{R: 220, G: 220, B: 220},
TextBg: terminal.RGB{R: 25, G: 25, B: 35}, TextBg: color.RGB{R: 25, G: 25, B: 35},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0}, CursorFg: color.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200}, CursorBg: color.RGB{R: 200, G: 200, B: 200},
LineNumFg: terminal.RGB{R: 100, G: 100, B: 120}, LineNumFg: color.RGB{R: 100, G: 100, B: 120},
LineNumBg: terminal.RGB{R: 30, G: 30, B: 40}, LineNumBg: color.RGB{R: 30, G: 30, B: 40},
CurrentLineBg: terminal.RGB{R: 35, G: 35, B: 50}, CurrentLineBg: color.RGB{R: 35, G: 35, B: 50},
BorderFg: terminal.RGB{R: 80, G: 80, B: 100}, BorderFg: color.RGB{R: 80, G: 80, B: 100},
} }
} }
@@ -194,4 +195,5 @@ func formatLineNum(num, width int) string {
s = " " + s s = " " + s
} }
return s return s
} }
+20 -16
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// FormField pairs a label with an editable text field // FormField pairs a label with an editable text field
type FormField struct { type FormField struct {
@@ -103,25 +106,25 @@ type FormOpts struct {
// FormStyle defines form colors // FormStyle defines form colors
type FormStyle struct { type FormStyle struct {
LabelFg terminal.RGB LabelFg color.RGB
FieldFg terminal.RGB FieldFg color.RGB
FieldBg terminal.RGB FieldBg color.RGB
FocusBg terminal.RGB FocusBg color.RGB
CursorFg terminal.RGB CursorFg color.RGB
CursorBg terminal.RGB CursorBg color.RGB
Bg terminal.RGB Bg color.RGB
} }
// DefaultFormStyle returns default form colors // DefaultFormStyle returns default form colors
func DefaultFormStyle() FormStyle { func DefaultFormStyle() FormStyle {
return FormStyle{ return FormStyle{
LabelFg: terminal.RGB{R: 150, G: 150, B: 180}, LabelFg: color.RGB{R: 150, G: 150, B: 180},
FieldFg: terminal.RGB{R: 220, G: 220, B: 220}, FieldFg: color.RGB{R: 220, G: 220, B: 220},
FieldBg: terminal.RGB{R: 35, G: 35, B: 45}, FieldBg: color.RGB{R: 35, G: 35, B: 45},
FocusBg: terminal.RGB{R: 45, G: 45, B: 60}, FocusBg: color.RGB{R: 45, G: 45, B: 60},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0}, CursorFg: color.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200}, CursorBg: color.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 25, G: 25, B: 35}, Bg: color.RGB{R: 25, G: 25, B: 35},
} }
} }
@@ -225,4 +228,5 @@ func (r Region) Form(state *FormState, opts FormOpts) int {
} }
return y return y
} }
+7 -5
View File
@@ -1,18 +1,19 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
// InputOpts configures single-line input field // InputOpts configures single-line input field
type InputOpts struct { type InputOpts struct {
Label string Label string
LabelFg terminal.RGB LabelFg color.RGB
Text string Text string
Cursor int // Cursor position in text (rune index) Cursor int // Cursor position in text (rune index)
CursorBg terminal.RGB CursorBg color.RGB
TextFg terminal.RGB TextFg color.RGB
Bg terminal.RGB Bg color.RGB
} }
// Input renders labeled text input field on row y, handling cursor display and horizontal scrolling // Input renders labeled text input field on row y, handling cursor display and horizontal scrolling
@@ -75,4 +76,5 @@ func (r Region) Input(y int, opts InputOpts) {
if cursor == len(runes) && cursor-scroll < inputW { if cursor == len(runes) && cursor-scroll < inputW {
r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone) r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone)
} }
} }
+13 -9
View File
@@ -1,22 +1,25 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ListItem represents a single row in a scrollable list // ListItem represents a single row in a scrollable list
type ListItem struct { type ListItem struct {
Indent int // Left padding in cells Indent int // Left padding in cells
Icon rune // Expand indicator or bullet, 0 = none Icon rune // Expand indicator or bullet, 0 = none
IconFg terminal.RGB IconFg color.RGB
Check CheckState // CheckNone to skip checkbox Check CheckState // CheckNone to skip checkbox
CheckFg terminal.RGB CheckFg color.RGB
Text string Text string
TextStyle Style TextStyle Style
} }
// ListOpts configures list rendering // ListOpts configures list rendering
type ListOpts struct { type ListOpts struct {
CursorBg terminal.RGB CursorBg color.RGB
DefaultBg terminal.RGB DefaultBg color.RGB
IconWidth int // Width reserved for icon, default 2 IconWidth int // Width reserved for icon, default 2
} }
@@ -49,7 +52,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
// Clear row // Clear row
for x := 0; x < r.W; x++ { for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone) r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
} }
x := item.Indent x := item.Indent
@@ -61,7 +64,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
x += iconW x += iconW
// Checkbox // Checkbox
if item.Check != CheckNone || item.CheckFg != (terminal.RGB{}) { if item.Check != CheckNone || item.CheckFg != (color.RGB{}) {
if x+3 <= r.W { if x+3 <= r.W {
var ch rune var ch rune
switch item.Check { switch item.Check {
@@ -83,7 +86,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
// Text // Text
textStyle := item.TextStyle textStyle := item.TextStyle
if textStyle.Bg == (terminal.RGB{}) { if textStyle.Bg == (color.RGB{}) {
textStyle.Bg = bg textStyle.Bg = bg
} }
text := item.Text text := item.Text
@@ -96,4 +99,5 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
} }
return rendered return rendered
} }
+7 -5
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -9,10 +10,10 @@ type ModalOpts struct {
Title string Title string
Hint string // Right-aligned hint text Hint string // Right-aligned hint text
Border LineType Border LineType
BorderFg terminal.RGB BorderFg color.RGB
TitleFg terminal.RGB TitleFg color.RGB
HintFg terminal.RGB HintFg color.RGB
Bg terminal.RGB Bg color.RGB
} }
// Modal fills region with background, draws border with title/hint, returns content region // Modal fills region with background, draws border with title/hint, returns content region
@@ -63,4 +64,5 @@ func (r Region) Modal(opts ModalOpts) Region {
// Return content region // Return content region
return r.Sub(1, 1, r.W-2, r.H-2) return r.Sub(1, 1, r.W-2, r.H-2)
} }
+28 -24
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// OverlayStyle specifies overlay appearance // OverlayStyle specifies overlay appearance
type OverlayStyle uint8 type OverlayStyle uint8
@@ -17,10 +20,10 @@ type OverlayOpts struct {
Style OverlayStyle Style OverlayStyle
Title string Title string
Border LineType Border LineType
Bg terminal.RGB Bg color.RGB
Fg terminal.RGB // Border and title color Fg color.RGB // Border and title color
TitleBg terminal.RGB // Title bar background, zero = same as Fg TitleBg color.RGB // Title bar background, zero = same as Fg
TitleFg terminal.RGB // Title text color, zero = same as Bg TitleFg color.RGB // Title text color, zero = same as Bg
// Modal/Floating positioning (ignored for Fullscreen) // Modal/Floating positioning (ignored for Fullscreen)
Width int // 0 = 80% of region Width int // 0 = 80% of region
@@ -28,7 +31,7 @@ type OverlayOpts struct {
X, Y int // Offset from center, 0 = centered X, Y int // Offset from center, 0 = centered
// Shadow for Floating style // Shadow for Floating style
ShadowColor terminal.RGB ShadowColor color.RGB
} }
// DefaultOverlayOpts returns sensible defaults for modal overlay // DefaultOverlayOpts returns sensible defaults for modal overlay
@@ -37,10 +40,10 @@ func DefaultOverlayOpts(title string) OverlayOpts {
Style: OverlayModal, Style: OverlayModal,
Title: title, Title: title,
Border: LineDouble, Border: LineDouble,
Bg: terminal.RGB{R: 25, G: 25, B: 35}, Bg: color.RGB{R: 25, G: 25, B: 35},
Fg: terminal.RGB{R: 100, G: 140, B: 180}, Fg: color.RGB{R: 100, G: 140, B: 180},
TitleBg: terminal.RGB{R: 40, G: 60, B: 90}, TitleBg: color.RGB{R: 40, G: 60, B: 90},
TitleFg: terminal.RGB{R: 255, G: 255, B: 255}, TitleFg: color.RGB{R: 255, G: 255, B: 255},
} }
} }
@@ -49,10 +52,10 @@ func FullscreenOverlayOpts(title string) OverlayOpts {
return OverlayOpts{ return OverlayOpts{
Style: OverlayFullscreen, Style: OverlayFullscreen,
Title: title, Title: title,
Bg: terminal.RGB{R: 20, G: 20, B: 30}, Bg: color.RGB{R: 20, G: 20, B: 30},
Fg: terminal.RGB{R: 100, G: 140, B: 180}, Fg: color.RGB{R: 100, G: 140, B: 180},
TitleBg: terminal.RGB{R: 40, G: 60, B: 90}, TitleBg: color.RGB{R: 40, G: 60, B: 90},
TitleFg: terminal.RGB{R: 255, G: 255, B: 255}, TitleFg: color.RGB{R: 255, G: 255, B: 255},
} }
} }
@@ -98,11 +101,11 @@ func (r Region) renderFullscreenOverlay(opts OverlayOpts) OverlayResult {
// Title bar // Title bar
if opts.Title != "" { if opts.Title != "" {
titleBg := opts.TitleBg titleBg := opts.TitleBg
if titleBg == (terminal.RGB{}) { if titleBg == (color.RGB{}) {
titleBg = opts.Fg titleBg = opts.Fg
} }
titleFg := opts.TitleFg titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) { if titleFg == (color.RGB{}) {
titleFg = opts.Bg titleFg = opts.Bg
} }
@@ -188,11 +191,11 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult {
// Title in top border // Title in top border
if opts.Title != "" && contentW > 2 { if opts.Title != "" && contentW > 2 {
titleBg := opts.TitleBg titleBg := opts.TitleBg
if titleBg == (terminal.RGB{}) { if titleBg == (color.RGB{}) {
titleBg = opts.Fg titleBg = opts.Fg
} }
titleFg := opts.TitleFg titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) { if titleFg == (color.RGB{}) {
titleFg = opts.Bg titleFg = opts.Bg
} }
@@ -224,8 +227,8 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult {
func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult { func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
// Same as modal but with shadow // Same as modal but with shadow
shadowColor := opts.ShadowColor shadowColor := opts.ShadowColor
if shadowColor == (terminal.RGB{}) { if shadowColor == (color.RGB{}) {
shadowColor = terminal.RGB{R: 10, G: 10, B: 15} shadowColor = color.RGB{R: 10, G: 10, B: 15}
} }
// Calculate dimensions (same as modal) // Calculate dimensions (same as modal)
@@ -285,11 +288,11 @@ func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
if opts.Title != "" && contentW > 2 { if opts.Title != "" && contentW > 2 {
titleBg := opts.TitleBg titleBg := opts.TitleBg
if titleBg == (terminal.RGB{}) { if titleBg == (color.RGB{}) {
titleBg = opts.Fg titleBg = opts.Fg
} }
titleFg := opts.TitleFg titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) { if titleFg == (color.RGB{}) {
titleFg = opts.Bg titleFg = opts.Bg
} }
@@ -327,7 +330,7 @@ func (r Region) renderBorderTitleOverlay(opts OverlayOpts) OverlayResult {
if opts.Title != "" && r.W > 6 { if opts.Title != "" && r.W > 6 {
titleFg := opts.TitleFg titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) { if titleFg == (color.RGB{}) {
titleFg = opts.Fg titleFg = opts.Fg
} }
title := " " + opts.Title + " " title := " " + opts.Title + " "
@@ -370,4 +373,5 @@ func (o *OverlayState) Hide() {
// Toggle switches visibility // Toggle switches visibility
func (o *OverlayState) Toggle() { func (o *OverlayState) Toggle() {
o.Visible = !o.Visible o.Visible = !o.Visible
} }
+8 -6
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -8,9 +9,9 @@ import (
type PaneOpts struct { type PaneOpts struct {
Title string Title string
Border LineType Border LineType
BorderFg terminal.RGB BorderFg color.RGB
Bg terminal.RGB Bg color.RGB
TitleFg terminal.RGB TitleFg color.RGB
} }
// Pane draws bordered pane with optional title, returns content region // Pane draws bordered pane with optional title, returns content region
@@ -49,7 +50,7 @@ func (r Region) Pane(opts PaneOpts) Region {
// TitledPane fills region with background, draws centered title at top, returns content region // TitledPane fills region with background, draws centered title at top, returns content region
// Content region starts at row 1 with full width // Content region starts at row 1 with full width
func (r Region) TitledPane(title string, titleFg, bg terminal.RGB) Region { func (r Region) TitledPane(title string, titleFg, bg color.RGB) Region {
r.Fill(bg) r.Fill(bg)
if title != "" && r.H > 0 { if title != "" && r.H > 0 {
r.TextCenter(0, title, titleFg, bg, terminal.AttrBold) r.TextCenter(0, title, titleFg, bg, terminal.AttrBold)
@@ -61,9 +62,10 @@ func (r Region) TitledPane(title string, titleFg, bg terminal.RGB) Region {
} }
// TitledPaneFocused is TitledPane with focus-dependent background // TitledPaneFocused is TitledPane with focus-dependent background
func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg terminal.RGB, focused bool) Region { func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg color.RGB, focused bool) Region {
if focused { if focused {
bg = focusBg bg = focusBg
} }
return r.TitledPane(title, titleFg, bg) return r.TitledPane(title, titleFg, bg)
} }
+8 -6
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -12,7 +13,7 @@ const (
) )
// Progress draws horizontal progress bar (0.0-1.0) // Progress draws horizontal progress bar (0.0-1.0)
func (r Region) Progress(x, y, w int, pct float64, fg, bg terminal.RGB) { func (r Region) Progress(x, y, w int, pct float64, fg, bg color.RGB) {
if y < 0 || y >= r.H || w <= 0 { if y < 0 || y >= r.H || w <= 0 {
return return
} }
@@ -43,7 +44,7 @@ func (r Region) Progress(x, y, w int, pct float64, fg, bg terminal.RGB) {
} }
// ProgressV draws vertical progress bar (fills bottom-up) // ProgressV draws vertical progress bar (fills bottom-up)
func (r Region) ProgressV(x, y, h int, pct float64, fg, bg terminal.RGB) { func (r Region) ProgressV(x, y, h int, pct float64, fg, bg color.RGB) {
if x < 0 || x >= r.W || h <= 0 { if x < 0 || x >= r.W || h <= 0 {
return return
} }
@@ -72,7 +73,7 @@ func (r Region) ProgressV(x, y, h int, pct float64, fg, bg terminal.RGB) {
} }
// Spinner draws spinner character based on frame counter // Spinner draws spinner character based on frame counter
func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) { func (r Region) Spinner(x, y int, frame int, fg color.RGB) {
if x < 0 || x >= r.W || y < 0 || y >= r.H { if x < 0 || x >= r.W || y < 0 || y >= r.H {
return return
} }
@@ -80,11 +81,11 @@ func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) {
if idx < 0 { if idx < 0 {
idx = -idx idx = -idx
} }
r.Cell(x, y, spinnerFrames[idx], fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x, y, spinnerFrames[idx], fg, color.RGB{}, terminal.AttrNone)
} }
// Gauge draws labeled gauge with percentage // Gauge draws labeled gauge with percentage
func (r Region) Gauge(x, y, w int, value, max int, fg, bg terminal.RGB) { func (r Region) Gauge(x, y, w int, value, max int, fg, bg color.RGB) {
if w < 5 || y < 0 || y >= r.H { if w < 5 || y < 0 || y >= r.H {
return return
} }
@@ -121,4 +122,5 @@ func (r Region) Gauge(x, y, w int, value, max int, fg, bg terminal.RGB) {
label = " " + string(rune('0'+pctInt)) + "%" label = " " + string(rune('0'+pctInt)) + "%"
} }
r.Text(x+2+barW, y, label, fg, bg, terminal.AttrNone) r.Text(x+2+barW, y, label, fg, bg, terminal.AttrNone)
} }
+25 -21
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ProgressType specifies progress indicator variant // ProgressType specifies progress indicator variant
type ProgressType uint8 type ProgressType uint8
@@ -93,11 +96,11 @@ type ProgressOverlayOpts struct {
Width int // Overlay width, 0 = auto Width int // Overlay width, 0 = auto
Cancelable bool // Show cancel hint Cancelable bool // Show cancel hint
CancelKey string // e.g., "Esc" CancelKey string // e.g., "Esc"
Fg terminal.RGB Fg color.RGB
Bg terminal.RGB Bg color.RGB
BarFg terminal.RGB BarFg color.RGB
BarBg terminal.RGB BarBg color.RGB
AccentFg terminal.RGB // Spinner/highlight color AccentFg color.RGB // Spinner/highlight color
} }
// DefaultProgressOpts returns sensible defaults // DefaultProgressOpts returns sensible defaults
@@ -111,11 +114,11 @@ func DefaultProgressOpts(title, message string, ptype ProgressType) ProgressOver
BarStyle: BarStyleBlock, BarStyle: BarStyleBlock,
ShowPercent: true, ShowPercent: true,
Width: 40, Width: 40,
Fg: terminal.RGB{R: 220, G: 220, B: 220}, Fg: color.RGB{R: 220, G: 220, B: 220},
Bg: terminal.RGB{R: 30, G: 30, B: 40}, Bg: color.RGB{R: 30, G: 30, B: 40},
BarFg: terminal.RGB{R: 80, G: 160, B: 255}, BarFg: color.RGB{R: 80, G: 160, B: 255},
BarBg: terminal.RGB{R: 50, G: 50, B: 60}, BarBg: color.RGB{R: 50, G: 50, B: 60},
AccentFg: terminal.RGB{R: 100, G: 200, B: 255}, AccentFg: color.RGB{R: 100, G: 200, B: 255},
} }
} }
@@ -169,12 +172,12 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
case ProgressStyleShadow: case ProgressStyleShadow:
// Draw shadow first // Draw shadow first
shadow := r.Sub(overlay.X-r.X+1, overlay.Y-r.Y+1, overlayW, overlayH) shadow := r.Sub(overlay.X-r.X+1, overlay.Y-r.Y+1, overlayW, overlayH)
shadow.Fill(terminal.RGB{R: 10, G: 10, B: 15}) shadow.Fill(color.RGB{R: 10, G: 10, B: 15})
borderLine = LineSingle borderLine = LineSingle
case ProgressStyleNeon: case ProgressStyleNeon:
borderLine = LineDouble borderLine = LineDouble
opts.AccentFg = terminal.RGB{R: 0, G: 255, B: 200} opts.AccentFg = color.RGB{R: 0, G: 255, B: 200}
opts.BarFg = terminal.RGB{R: 255, G: 0, B: 255} opts.BarFg = color.RGB{R: 255, G: 0, B: 255}
case ProgressStyleRetro: case ProgressStyleRetro:
borderLine = LineHeavy borderLine = LineHeavy
opts.BarStyle = BarStyleBlock opts.BarStyle = BarStyleBlock
@@ -242,7 +245,7 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
if opts.CancelKey == "" { if opts.CancelKey == "" {
hint = "Esc to cancel" hint = "Esc to cancel"
} }
content.TextCenter(y, hint, terminal.RGB{R: 120, G: 120, B: 130}, opts.Bg, terminal.AttrDim) content.TextCenter(y, hint, color.RGB{R: 120, G: 120, B: 130}, opts.Bg, terminal.AttrDim)
} }
return overlay return overlay
@@ -301,7 +304,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
for x := 0; x < barW; x++ { for x := 0; x < barW; x++ {
var ch rune var ch rune
var fg terminal.RGB var fg color.RGB
if x < filled { if x < filled {
ch = chars[0] ch = chars[0]
fg = opts.BarFg fg = opts.BarFg
@@ -334,7 +337,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
for x := 0; x < barW; x++ { for x := 0; x < barW; x++ {
var ch rune var ch rune
var fg terminal.RGB var fg color.RGB
if x >= pos && x < pos+markerW { if x >= pos && x < pos+markerW {
ch = chars[0] ch = chars[0]
fg = opts.BarFg fg = opts.BarFg
@@ -365,11 +368,11 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
for x := 0; x < barW; x++ { for x := 0; x < barW; x++ {
var ch rune var ch rune
var fg terminal.RGB var fg color.RGB
if x < filled { if x < filled {
ch = chars[0] ch = chars[0]
// Pulse the color // Pulse the color
fg = terminal.RGB{ fg = color.RGB{
R: uint8(float64(opts.BarFg.R) * (0.5 + intensity*0.5)), R: uint8(float64(opts.BarFg.R) * (0.5 + intensity*0.5)),
G: uint8(float64(opts.BarFg.G) * (0.5 + intensity*0.5)), G: uint8(float64(opts.BarFg.G) * (0.5 + intensity*0.5)),
B: uint8(float64(opts.BarFg.B) * (0.5 + intensity*0.5)), B: uint8(float64(opts.BarFg.B) * (0.5 + intensity*0.5)),
@@ -390,7 +393,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
// ETA // ETA
if opts.ShowETA != "" { if opts.ShowETA != "" {
etaX := bar.W - RuneLen(opts.ShowETA) etaX := bar.W - RuneLen(opts.ShowETA)
bar.Text(etaX, 0, opts.ShowETA, terminal.RGB{R: 150, G: 150, B: 160}, opts.Bg, terminal.AttrDim) bar.Text(etaX, 0, opts.ShowETA, color.RGB{R: 150, G: 150, B: 160}, opts.Bg, terminal.AttrDim)
} }
} }
@@ -463,4 +466,5 @@ func (p *ProgressState) Dismiss() {
// Show displays the progress overlay // Show displays the progress overlay
func (p *ProgressState) Show() { func (p *ProgressState) Show() {
p.Visible = true p.Visible = true
} }
+10 -6
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Region represents a rectangular area within a cell buffer // Region represents a rectangular area within a cell buffer
// All coordinates are relative to the region's origin // All coordinates are relative to the region's origin
@@ -63,7 +66,7 @@ func (r Region) Inset(n int) Region {
} }
// Cell sets a single cell with bounds checking // Cell sets a single cell with bounds checking
func (r Region) Cell(x, y int, ch rune, fg, bg terminal.RGB, attr terminal.Attr) { func (r Region) Cell(x, y int, ch rune, fg, bg color.RGB, attr terminal.Attr) {
if x < 0 || x >= r.W || y < 0 || y >= r.H { if x < 0 || x >= r.W || y < 0 || y >= r.H {
return return
} }
@@ -83,17 +86,17 @@ func (r Region) Cell(x, y int, ch rune, fg, bg terminal.RGB, attr terminal.Attr)
} }
// Fill fills entire region with background color // Fill fills entire region with background color
func (r Region) Fill(bg terminal.RGB) { func (r Region) Fill(bg color.RGB) {
for y := 0; y < r.H; y++ { for y := 0; y < r.H; y++ {
for x := 0; x < r.W; x++ { for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone) r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
} }
} }
} }
// Clear fills region with spaces and zero colors // Clear fills region with spaces and zero colors
func (r Region) Clear() { func (r Region) Clear() {
r.Fill(terminal.RGB{}) r.Fill(color.RGB{})
} }
// Width returns region width // Width returns region width
@@ -109,4 +112,5 @@ func (r Region) Height() int {
// Bounds returns absolute position and dimensions // Bounds returns absolute position and dimensions
func (r Region) Bounds() (x, y, w, h int) { func (r Region) Bounds() (x, y, w, h int) {
return r.X, r.Y, r.W, r.H return r.X, r.Y, r.W, r.H
} }
+8 -6
View File
@@ -1,12 +1,15 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Default spinner frames for Region.Spinner // Default spinner frames for Region.Spinner
var spinnerFrames = spinnerSets[SpinnerBraille] var spinnerFrames = spinnerSets[SpinnerBraille]
// Text renders text at position, truncates at region edge // Text renders text at position, truncates at region edge
func (r Region) Text(x, y int, s string, fg, bg terminal.RGB, attr terminal.Attr) { func (r Region) Text(x, y int, s string, fg, bg color.RGB, attr terminal.Attr) {
if y < 0 || y >= r.H { if y < 0 || y >= r.H {
return return
} }
@@ -40,19 +43,19 @@ func (r Region) TextStyled(x, y int, s string, style Style) {
} }
// TextRight renders text right-aligned on row // TextRight renders text right-aligned on row
func (r Region) TextRight(y int, s string, fg, bg terminal.RGB, attr terminal.Attr) { func (r Region) TextRight(y int, s string, fg, bg color.RGB, attr terminal.Attr) {
x := r.W - RuneLen(s) x := r.W - RuneLen(s)
r.Text(x, y, s, fg, bg, attr) r.Text(x, y, s, fg, bg, attr)
} }
// TextCenter renders text centered on row // TextCenter renders text centered on row
func (r Region) TextCenter(y int, s string, fg, bg terminal.RGB, attr terminal.Attr) { func (r Region) TextCenter(y int, s string, fg, bg color.RGB, attr terminal.Attr) {
x := (r.W - RuneLen(s)) / 2 x := (r.W - RuneLen(s)) / 2
r.Text(x, y, s, fg, bg, attr) r.Text(x, y, s, fg, bg, attr)
} }
// TextBlock renders wrapped text within region bounds, returns number of lines rendered // TextBlock renders wrapped text within region bounds, returns number of lines rendered
func (r Region) TextBlock(x, y int, text string, fg, bg terminal.RGB, attr terminal.Attr) int { func (r Region) TextBlock(x, y int, text string, fg, bg color.RGB, attr terminal.Attr) int {
if x >= r.W || y >= r.H || text == "" { if x >= r.W || y >= r.H || text == "" {
return 0 return 0
} }
@@ -81,4 +84,3 @@ func (r Region) TextBlock(x, y int, text string, fg, bg terminal.RGB, attr termi
func (r Region) TextBlockStyled(x, y int, text string, style Style) int { func (r Region) TextBlockStyled(x, y int, text string, style Style) int {
return r.TextBlock(x, y, text, style.Fg, style.Bg, style.Attr) return r.TextBlock(x, y, text, style.Fg, style.Bg, style.Attr)
} }
+6 -6
View File
@@ -1,11 +1,12 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
// ScrollBar draws vertical scrollbar track with thumb // ScrollBar draws vertical scrollbar track with thumb
func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) { func (r Region) ScrollBar(x int, offset, visible, total int, fg color.RGB) {
if x < 0 || x >= r.W || r.H < 1 { if x < 0 || x >= r.W || r.H < 1 {
return return
} }
@@ -14,7 +15,7 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
if total <= visible || trackH < 3 { if total <= visible || trackH < 3 {
// No scrolling needed or track too small // No scrolling needed or track too small
for y := range trackH { for y := range trackH {
r.Cell(x, y, '│', fg, terminal.RGB{}, terminal.AttrDim) r.Cell(x, y, '│', fg, color.RGB{}, terminal.AttrDim)
} }
return return
} }
@@ -42,12 +43,12 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
} else { } else {
ch = '░' ch = '░'
} }
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone) r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
} }
} }
// ScrollIndicator draws compact indicator text (Top/Bot/XX%) // ScrollIndicator draws compact indicator text (Top/Bot/XX%)
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg terminal.RGB) { func (r Region) ScrollIndicator(y int, offset, visible, total int, fg color.RGB) {
if y < 0 || y >= r.H { if y < 0 || y >= r.H {
return return
} }
@@ -68,6 +69,5 @@ func (r Region) ScrollIndicator(y int, offset, visible, total int, fg terminal.R
} }
} }
r.TextRight(y, text, fg, terminal.RGB{}, terminal.AttrDim) r.TextRight(y, text, fg, color.RGB{}, terminal.AttrDim)
} }
+9 -6
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// BarSection represents one segment of a status bar // BarSection represents one segment of a status bar
type BarSection struct { type BarSection struct {
@@ -25,7 +28,7 @@ const (
type BarOpts struct { type BarOpts struct {
Separator string // Between sections, default " │ " Separator string // Between sections, default " │ "
SepStyle Style // Separator styling SepStyle Style // Separator styling
Bg terminal.RGB Bg color.RGB
Align BarAlign Align BarAlign
Padding int // Left/right padding, default 1 Padding int // Left/right padding, default 1
} }
@@ -34,7 +37,7 @@ type BarOpts struct {
func DefaultBarOpts() BarOpts { func DefaultBarOpts() BarOpts {
return BarOpts{ return BarOpts{
Separator: " │ ", Separator: " │ ",
SepStyle: Style{Fg: terminal.RGB{R: 80, G: 80, B: 100}}, SepStyle: Style{Fg: color.RGB{R: 80, G: 80, B: 100}},
Padding: 1, Padding: 1,
Align: BarAlignRight, Align: BarAlignRight,
} }
@@ -55,7 +58,7 @@ func (r Region) StatusBar(y int, sections []BarSection, opts BarOpts) {
// Fill background // Fill background
for x := 0; x < r.W; x++ { for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, opts.Bg, terminal.AttrNone) r.Cell(x, y, ' ', color.RGB{}, opts.Bg, terminal.AttrNone)
} }
sepLen := RuneLen(opts.Separator) sepLen := RuneLen(opts.Separator)
@@ -193,7 +196,7 @@ func truncateSections(sections []BarSection, widths []int, sepLen, availW int) (
} }
// QuickStatusBar renders simple label:value pairs right-aligned // QuickStatusBar renders simple label:value pairs right-aligned
func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg terminal.RGB) { func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg color.RGB) {
sections := make([]BarSection, len(pairs)) sections := make([]BarSection, len(pairs))
for i, p := range pairs { for i, p := range pairs {
sections[i] = BarSection{ sections[i] = BarSection{
@@ -207,4 +210,4 @@ func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg te
Bg: bg, Bg: bg,
Align: BarAlignRight, Align: BarAlignRight,
}) })
} }
+7 -5
View File
@@ -1,22 +1,24 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
// Style bundles foreground, background, and attributes for text rendering // Style bundles foreground, background, and attributes for text rendering
type Style struct { type Style struct {
Fg terminal.RGB Fg color.RGB
Bg terminal.RGB Bg color.RGB
Attr terminal.Attr Attr terminal.Attr
} }
// DefaultStyle returns style with zero values (transparent bg) // DefaultStyle returns style with zero values (transparent bg)
func DefaultStyle(fg terminal.RGB) Style { func DefaultStyle(fg color.RGB) Style {
return Style{Fg: fg} return Style{Fg: fg}
} }
// IsZero returns true if style has no colors or attributes set // IsZero returns true if style has no colors or attributes set
func (s Style) IsZero() bool { func (s Style) IsZero() bool {
return s.Fg == (terminal.RGB{}) && s.Bg == (terminal.RGB{}) && s.Attr == terminal.AttrNone return s.Fg == (color.RGB{}) && s.Bg == (color.RGB{}) && s.Attr == terminal.AttrNone
} }
+17 -15
View File
@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal"
) )
@@ -18,25 +19,25 @@ type TextFieldOpts struct {
// DefaultTextFieldStyle returns default colors // DefaultTextFieldStyle returns default colors
func DefaultTextFieldStyle() TextFieldStyle { func DefaultTextFieldStyle() TextFieldStyle {
return TextFieldStyle{ return TextFieldStyle{
TextFg: terminal.RGB{R: 220, G: 220, B: 220}, TextFg: color.RGB{R: 220, G: 220, B: 220},
TextBg: terminal.RGB{R: 30, G: 30, B: 40}, TextBg: color.RGB{R: 30, G: 30, B: 40},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0}, CursorFg: color.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200}, CursorBg: color.RGB{R: 200, G: 200, B: 200},
PlaceholderFg: terminal.RGB{R: 100, G: 100, B: 110}, PlaceholderFg: color.RGB{R: 100, G: 100, B: 110},
PrefixFg: terminal.RGB{R: 150, G: 150, B: 180}, PrefixFg: color.RGB{R: 150, G: 150, B: 180},
BorderFg: terminal.RGB{R: 80, G: 80, B: 100}, BorderFg: color.RGB{R: 80, G: 80, B: 100},
} }
} }
// TextFieldStyle defines text field colors // TextFieldStyle defines text field colors
type TextFieldStyle struct { type TextFieldStyle struct {
TextFg terminal.RGB TextFg color.RGB
TextBg terminal.RGB TextBg color.RGB
CursorFg terminal.RGB CursorFg color.RGB
CursorBg terminal.RGB CursorBg color.RGB
PlaceholderFg terminal.RGB PlaceholderFg color.RGB
PrefixFg terminal.RGB PrefixFg color.RGB
BorderFg terminal.RGB BorderFg color.RGB
} }
// TextField renders text field and returns content height used // TextField renders text field and returns content height used
@@ -168,4 +169,5 @@ func boolToInt(b bool) int {
return 1 return 1
} }
return 0 return 0
} }
+53 -50
View File
@@ -1,63 +1,66 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
)
// Theme defines semantic colors for TUI components // Theme defines semantic colors for TUI components
type Theme struct { type Theme struct {
Bg terminal.RGB Bg color.RGB
Fg terminal.RGB Fg color.RGB
FocusBg terminal.RGB FocusBg color.RGB
CursorBg terminal.RGB CursorBg color.RGB
Selected terminal.RGB Selected color.RGB
Unselected terminal.RGB Unselected color.RGB
Partial terminal.RGB Partial color.RGB
Error terminal.RGB Error color.RGB
Warning terminal.RGB Warning color.RGB
Border terminal.RGB Border color.RGB
HeaderBg terminal.RGB HeaderBg color.RGB
HeaderFg terminal.RGB HeaderFg color.RGB
StatusFg terminal.RGB StatusFg color.RGB
HintFg terminal.RGB HintFg color.RGB
InputBg terminal.RGB InputBg color.RGB
DirFg terminal.RGB DirFg color.RGB
FileFg terminal.RGB FileFg color.RGB
SymbolFg terminal.RGB SymbolFg color.RGB
SyntaxComment terminal.RGB SyntaxComment color.RGB
SyntaxString terminal.RGB SyntaxString color.RGB
SyntaxKeyword terminal.RGB SyntaxKeyword color.RGB
SyntaxType terminal.RGB SyntaxType color.RGB
SyntaxNumber terminal.RGB SyntaxNumber color.RGB
SyntaxSymbol terminal.RGB SyntaxSymbol color.RGB
} }
// DefaultTheme provides reasonable defaults // DefaultTheme provides reasonable defaults
var DefaultTheme = Theme{ var DefaultTheme = Theme{
Bg: terminal.RGB{R: 20, G: 20, B: 30}, Bg: color.RGB{R: 20, G: 20, B: 30},
Fg: terminal.RGB{R: 200, G: 200, B: 200}, Fg: color.RGB{R: 200, G: 200, B: 200},
FocusBg: terminal.RGB{R: 30, G: 35, B: 45}, FocusBg: color.RGB{R: 30, G: 35, B: 45},
CursorBg: terminal.RGB{R: 50, G: 50, B: 70}, CursorBg: color.RGB{R: 50, G: 50, B: 70},
Selected: terminal.RGB{R: 80, G: 200, B: 80}, Selected: color.RGB{R: 80, G: 200, B: 80},
Unselected: terminal.RGB{R: 100, G: 100, B: 100}, Unselected: color.RGB{R: 100, G: 100, B: 100},
Partial: terminal.RGB{R: 80, G: 160, B: 220}, Partial: color.RGB{R: 80, G: 160, B: 220},
Error: terminal.RGB{R: 255, G: 80, B: 80}, Error: color.RGB{R: 255, G: 80, B: 80},
Warning: terminal.RGB{R: 255, G: 80, B: 80}, Warning: color.RGB{R: 255, G: 80, B: 80},
Border: terminal.RGB{R: 60, G: 80, B: 100}, Border: color.RGB{R: 60, G: 80, B: 100},
HeaderBg: terminal.RGB{R: 40, G: 60, B: 90}, HeaderBg: color.RGB{R: 40, G: 60, B: 90},
HeaderFg: terminal.RGB{R: 255, G: 255, B: 255}, HeaderFg: color.RGB{R: 255, G: 255, B: 255},
StatusFg: terminal.RGB{R: 140, G: 140, B: 140}, StatusFg: color.RGB{R: 140, G: 140, B: 140},
HintFg: terminal.RGB{R: 100, G: 180, B: 200}, HintFg: color.RGB{R: 100, G: 180, B: 200},
InputBg: terminal.RGB{R: 30, G: 30, B: 50}, InputBg: color.RGB{R: 30, G: 30, B: 50},
DirFg: terminal.RGB{R: 130, G: 170, B: 220}, DirFg: color.RGB{R: 130, G: 170, B: 220},
FileFg: terminal.RGB{R: 200, G: 200, B: 200}, FileFg: color.RGB{R: 200, G: 200, B: 200},
SymbolFg: terminal.RGB{R: 180, G: 220, B: 220}, SymbolFg: color.RGB{R: 180, G: 220, B: 220},
SyntaxComment: terminal.RGB{R: 100, G: 110, B: 120}, SyntaxComment: color.RGB{R: 100, G: 110, B: 120},
SyntaxString: terminal.RGB{R: 180, G: 220, B: 140}, SyntaxString: color.RGB{R: 180, G: 220, B: 140},
SyntaxKeyword: terminal.RGB{R: 180, G: 140, B: 220}, SyntaxKeyword: color.RGB{R: 180, G: 140, B: 220},
SyntaxType: terminal.RGB{R: 80, G: 200, B: 200}, SyntaxType: color.RGB{R: 80, G: 200, B: 200},
SyntaxNumber: terminal.RGB{R: 220, G: 180, B: 120}, SyntaxNumber: color.RGB{R: 220, G: 180, B: 120},
SyntaxSymbol: terminal.RGB{R: 220, G: 180, B: 80}, SyntaxSymbol: color.RGB{R: 220, G: 180, B: 80},
} }
+27 -23
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ToastPosition specifies where toast renders // ToastPosition specifies where toast renders
type ToastPosition uint8 type ToastPosition uint8
@@ -46,26 +49,26 @@ var ToastIcons = map[ToastSeverity]rune{
} }
// ToastColors default colors per severity // ToastColors default colors per severity
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon terminal.RGB }{ var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon color.RGB }{
ToastInfo: { ToastInfo: {
Fg: terminal.RGB{R: 200, G: 200, B: 200}, Fg: color.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 40, G: 40, B: 50}, Bg: color.RGB{R: 40, G: 40, B: 50},
Icon: terminal.RGB{R: 100, G: 150, B: 255}, Icon: color.RGB{R: 100, G: 150, B: 255},
}, },
ToastSuccess: { ToastSuccess: {
Fg: terminal.RGB{R: 220, G: 255, B: 220}, Fg: color.RGB{R: 220, G: 255, B: 220},
Bg: terminal.RGB{R: 30, G: 60, B: 30}, Bg: color.RGB{R: 30, G: 60, B: 30},
Icon: terminal.RGB{R: 80, G: 220, B: 80}, Icon: color.RGB{R: 80, G: 220, B: 80},
}, },
ToastWarning: { ToastWarning: {
Fg: terminal.RGB{R: 255, G: 240, B: 200}, Fg: color.RGB{R: 255, G: 240, B: 200},
Bg: terminal.RGB{R: 60, G: 50, B: 20}, Bg: color.RGB{R: 60, G: 50, B: 20},
Icon: terminal.RGB{R: 255, G: 200, B: 60}, Icon: color.RGB{R: 255, G: 200, B: 60},
}, },
ToastError: { ToastError: {
Fg: terminal.RGB{R: 255, G: 220, B: 220}, Fg: color.RGB{R: 255, G: 220, B: 220},
Bg: terminal.RGB{R: 60, G: 25, B: 25}, Bg: color.RGB{R: 60, G: 25, B: 25},
Icon: terminal.RGB{R: 255, G: 80, B: 80}, Icon: color.RGB{R: 255, G: 80, B: 80},
}, },
} }
@@ -81,9 +84,9 @@ type ToastOpts struct {
Padding int // Horizontal padding, default 1 Padding int // Horizontal padding, default 1
MarginX int // Margin from edge for floating positions MarginX int // Margin from edge for floating positions
MarginY int // Margin from edge for floating positions MarginY int // Margin from edge for floating positions
CustomFg terminal.RGB CustomFg color.RGB
CustomBg terminal.RGB CustomBg color.RGB
CustomIcon terminal.RGB CustomIcon color.RGB
} }
// DefaultToastOpts returns sensible defaults // DefaultToastOpts returns sensible defaults
@@ -109,13 +112,13 @@ func (r Region) Toast(opts ToastOpts) Region {
// Resolve colors // Resolve colors
fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon
if fg == (terminal.RGB{}) { if fg == (color.RGB{}) {
fg = ToastColors[opts.Severity].Fg fg = ToastColors[opts.Severity].Fg
} }
if bg == (terminal.RGB{}) { if bg == (color.RGB{}) {
bg = ToastColors[opts.Severity].Bg bg = ToastColors[opts.Severity].Bg
} }
if iconFg == (terminal.RGB{}) { if iconFg == (color.RGB{}) {
iconFg = ToastColors[opts.Severity].Icon iconFg = ToastColors[opts.Severity].Icon
} }
@@ -218,7 +221,7 @@ func (r Region) Toast(opts ToastOpts) Region {
case ToastStyleShadow: case ToastStyleShadow:
// Shadow offset // Shadow offset
shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH) shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH)
shadowRegion.Fill(terminal.RGB{R: 10, G: 10, B: 15}) shadowRegion.Fill(color.RGB{R: 10, G: 10, B: 15})
toastRegion.BoxFilled(LineSingle, fg, bg) toastRegion.BoxFilled(LineSingle, fg, bg)
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0) r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
} }
@@ -226,7 +229,7 @@ func (r Region) Toast(opts ToastOpts) Region {
return toastRegion return toastRegion
} }
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg terminal.RGB, _ int) { func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg color.RGB, _ int) {
if content.W < 1 || content.H < 1 { if content.W < 1 || content.H < 1 {
return return
} }
@@ -298,4 +301,5 @@ func (t *ToastState) Show(opts ToastOpts, frames int) {
t.Opts = opts t.Opts = opts
t.FramesLeft = frames t.FramesLeft = frames
t.Visible = true t.Visible = true
} }
+27 -23
View File
@@ -1,6 +1,9 @@
package tui package tui
import "github.com/lixenwraith/terminal" import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ExpandIcon chars // ExpandIcon chars
const ( const (
@@ -30,20 +33,20 @@ type TreeNode struct {
Key string // Unique identifier for expansion state Key string // Unique identifier for expansion state
Label string // Display text Label string // Display text
Icon rune // Custom icon, 0 = auto (▶/▼ for expandable, • for leaf) Icon rune // Custom icon, 0 = auto (▶/▼ for expandable, • for leaf)
IconFg terminal.RGB IconFg color.RGB
Expandable bool // Has children Expandable bool // Has children
Expanded bool // Currently expanded Expanded bool // Currently expanded
Depth int // Nesting level (0 = root) Depth int // Nesting level (0 = root)
Check CheckState // Optional checkbox, CheckNone to skip Check CheckState // Optional checkbox, CheckNone to skip
CheckFg terminal.RGB CheckFg color.RGB
Style Style // Text styling Style Style // Text styling
IsLast bool // Last sibling at this depth (for tree lines) IsLast bool // Last sibling at this depth (for tree lines)
Data any // Application payload Data any // Application payload
Suffix string // Secondary text after label (e.g., "(5 files)") Suffix string // Secondary text after label (e.g., "(5 files)")
SuffixStyle Style // Styling for suffix, zero = dimmed version of Style SuffixStyle Style // Styling for suffix, zero = dimmed version of Style
Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none
BadgeFg terminal.RGB // Badge color BadgeFg color.RGB // Badge color
} }
// ancestorHasMoreSiblings checks if there are more nodes at given depth after idx // ancestorHasMoreSiblings checks if there are more nodes at given depth after idx
@@ -59,12 +62,12 @@ func (r Region) ancestorHasMoreSiblings(nodes []TreeNode, idx, depth int) bool {
// TreeOpts configures tree rendering // TreeOpts configures tree rendering
type TreeOpts struct { type TreeOpts struct {
CursorBg terminal.RGB CursorBg color.RGB
DefaultBg terminal.RGB DefaultBg color.RGB
IndentWidth int // Cells per depth, default 2 IndentWidth int // Cells per depth, default 2
IconWidth int // Width for icon column, default 2 IconWidth int // Width for icon column, default 2
LineMode TreeLineMode LineMode TreeLineMode
LineFg terminal.RGB LineFg color.RGB
} }
// DefaultTreeOpts returns sensible defaults // DefaultTreeOpts returns sensible defaults
@@ -73,7 +76,7 @@ func DefaultTreeOpts() TreeOpts {
IndentWidth: 2, IndentWidth: 2,
IconWidth: 2, IconWidth: 2,
LineMode: TreeLinesNone, LineMode: TreeLinesNone,
LineFg: terminal.RGB{R: 80, G: 80, B: 100}, LineFg: color.RGB{R: 80, G: 80, B: 100},
} }
} }
@@ -94,7 +97,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
} }
lineFg := opts.LineFg lineFg := opts.LineFg
if lineFg == (terminal.RGB{}) { if lineFg == (color.RGB{}) {
lineFg = DefaultTheme.Border lineFg = DefaultTheme.Border
} }
@@ -114,7 +117,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
} }
for x := 0; x < r.W; x++ { for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone) r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
} }
x := 0 x := 0
@@ -139,7 +142,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
icon = IconBullet icon = IconBullet
} }
} }
if iconFg == (terminal.RGB{}) { if iconFg == (color.RGB{}) {
iconFg = lineFg iconFg = lineFg
} }
if x < r.W { if x < r.W {
@@ -148,10 +151,10 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
x += iconW x += iconW
// Checkbox // Checkbox
if node.Check != CheckNone || node.CheckFg != (terminal.RGB{}) { if node.Check != CheckNone || node.CheckFg != (color.RGB{}) {
if x+3 <= r.W { if x+3 <= r.W {
checkFg := node.CheckFg checkFg := node.CheckFg
if checkFg == (terminal.RGB{}) { if checkFg == (color.RGB{}) {
checkFg = node.Style.Fg checkFg = node.Style.Fg
} }
var ch rune var ch rune
@@ -176,7 +179,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
if node.Badge != 0 { if node.Badge != 0 {
if x < r.W { if x < r.W {
badgeFg := node.BadgeFg badgeFg := node.BadgeFg
if badgeFg == (terminal.RGB{}) { if badgeFg == (color.RGB{}) {
badgeFg = node.Style.Fg badgeFg = node.Style.Fg
} }
r.Cell(x, y, node.Badge, badgeFg, bg, terminal.AttrNone) r.Cell(x, y, node.Badge, badgeFg, bg, terminal.AttrNone)
@@ -186,7 +189,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
// Label // Label
style := node.Style style := node.Style
if style.Bg == (terminal.RGB{}) { if style.Bg == (color.RGB{}) {
style.Bg = bg style.Bg = bg
} }
@@ -214,15 +217,15 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
// Suffix // Suffix
if suffix != "" { if suffix != "" {
suffixStyle := node.SuffixStyle suffixStyle := node.SuffixStyle
if suffixStyle.Fg == (terminal.RGB{}) { if suffixStyle.Fg == (color.RGB{}) {
// Default: dimmed version of label style // Default: dimmed version of label style
suffixStyle.Fg = terminal.RGB{ suffixStyle.Fg = color.RGB{
R: node.Style.Fg.R / 2, R: node.Style.Fg.R / 2,
G: node.Style.Fg.G / 2, G: node.Style.Fg.G / 2,
B: node.Style.Fg.B / 2, B: node.Style.Fg.B / 2,
} }
} }
if suffixStyle.Bg == (terminal.RGB{}) { if suffixStyle.Bg == (color.RGB{}) {
suffixStyle.Bg = bg suffixStyle.Bg = bg
} }
r.TextStyled(x, y, suffix, suffixStyle) r.TextStyled(x, y, suffix, suffixStyle)
@@ -235,7 +238,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
} }
// renderTreeLines draws connector lines for tree structure, returns x position after lines // renderTreeLines draws connector lines for tree structure, returns x position after lines
func (r Region) renderTreeLines(y, startX int, node TreeNode, nodes []TreeNode, idx, scroll, indentW int, fg terminal.RGB, bg terminal.RGB) int { func (r Region) renderTreeLines(y, startX int, node TreeNode, nodes []TreeNode, idx, scroll, indentW int, fg color.RGB, bg color.RGB) int {
x := startX x := startX
for d := 0; d < node.Depth; d++ { for d := 0; d < node.Depth; d++ {
@@ -334,4 +337,5 @@ func FindPrevSiblingIndex(nodes []TreeNode, idx int) int {
} }
} }
return -1 return -1
} }