diff --git a/blend.go b/blend.go deleted file mode 100644 index 0047b9d..0000000 --- a/blend.go +++ /dev/null @@ -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} -} diff --git a/color.go b/color.go index 1837452..da74a90 100644 --- a/color.go +++ b/color.go @@ -1,5 +1,12 @@ package terminal +import ( + "sync" + "sync/atomic" + + "github.com/lixenwraith/color" +) + // ColorMode indicates terminal color capability type ColorMode uint8 @@ -8,85 +15,89 @@ const ( 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 // 64×64×64 = 262,144 bytes, fits in L2 cache -var lut256 [64 * 64 * 64]uint8 +const lut256Size = 64 * 64 * 64 -func init() { - // Pre-compute Redmean-based palette mapping for all 6-bit quantized RGB values - for r := 0; r < 64; r++ { - for g := 0; g < 64; g++ { - for b := 0; b < 64; b++ { - // Expand 6-bit to 8-bit (shift left 2, add 2 for midpoint) - r8 := (r << 2) | 2 - g8 := (g << 2) | 2 - b8 := (b << 2) | 2 - lut256[r<<12|g<<6|b] = computeRedmean256(r8, g8, b8) - } - } +// init() -> first-use build. Construction is ~63M Redmean evaluations +// and 256 KiB resident; truecolor sessions never read the table. +var ( + lut256Ptr atomic.Pointer[[lut256Size]uint8] + lut256Once sync.Once +) + +// lut256 returns the palette LUT, building it on first use. +// Fast path is an acquire load plus a predictable branch, and inlines into RGBTo256. +func lut256() *[lut256Size]uint8 { + if p := lut256Ptr.Load(); p != nil { + return p } + return lut256Build() } -// computeRedmean256 finds the nearest 256-palette index using Redmean distance -// Called only at init() to populate LUT -func computeRedmean256(r, g, b int) uint8 { +// lut256Build populates the table. Cold path, kept out of line so lut256 stays inlinable. +// The release store publishes the fully written array to acquire loads in lut256. +// +//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 - if r == g && g == b { - if r < 8 { + if c.R == c.G && c.G == c.B { + if c.R < 8 { return 16 } - if r > 238 { + if c.R > 238 { return 231 } - return uint8(232 + (r-8)/10) + return uint8(232 + (int(c.R)-8)/10) } bestIdx := uint8(16) minDist := 1 << 30 // Search 6×6×6 cube (indices 16-231) - for i := 0; i < 216; i++ { - cr := cubeValues[i/36] - cg := cubeValues[(i/6)%6] - cb := cubeValues[i%6] - - d := redmeanDistance(r, g, b, cr, cg, cb) - if d < minDist { + for i := range 216 { + cand := color.RGB{ + R: cubeValues[i/36], + G: cubeValues[(i/6)%6], + B: cubeValues[i%6], + } + if d := color.RedmeanDistance(c, cand); d < minDist { minDist = d bestIdx = uint8(16 + i) } } // Search grayscale ramp (indices 232-255) - for i := 0; i < 24; i++ { - gray := 8 + i*10 - d := redmeanDistance(r, g, b, gray, gray, gray) - if d < minDist { + for i := range 24 { + g := uint8(8 + i*10) + if d := color.RedmeanDistance(c, color.RGB{R: g, G: g, B: g}); d < minDist { minDist = d bestIdx = uint8(232 + i) } @@ -95,21 +106,11 @@ func computeRedmean256(r, g, b int) uint8 { 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) -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 -// O(1) lookup via pre-computed Redmean LUT -func RGBTo256(c RGB) uint8 { - return lut256[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)] +// RGBTo256 converts RGB to nearest 256-color palette index. +// O(1) via the Redmean LUT; the first call builds it (see WarmPalette256). +func RGBTo256(c color.RGB) uint8 { + return lut256()[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)] } diff --git a/examples/inline/main.go b/examples/inline/main.go index 92192bc..ac38bc1 100644 --- a/examples/inline/main.go +++ b/examples/inline/main.go @@ -5,6 +5,7 @@ import ( "os" "time" + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" "github.com/lixenwraith/terminal/inline" ) @@ -12,9 +13,9 @@ import ( func main() { p := inline.New(os.Stdout) - name := inline.Fg(terminal.LightSkyBlue).Attr(terminal.AttrBold) - okSt := inline.Fg(terminal.LimeGreen).Attr(terminal.AttrBold) - dim := inline.Fg(terminal.IronGray) + name := inline.Fg(color.LightSkyBlue).Attr(terminal.AttrBold) + okSt := inline.Fg(color.LimeGreen).Attr(terminal.AttrBold) + dim := inline.Fg(color.IronGray) pkgs := []string{"openssl", "zlib", "curl", "git", "go"} frame := 0 diff --git a/go.mod b/go.mod index 01d3952..7581626 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/lixenwraith/terminal -go 1.26.4 +go 1.26.0 require ( + github.com/lixenwraith/color v0.0.0-20260714170240-79433f872c42 golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 ) diff --git a/go.sum b/go.sum index d2468be..8814064 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/lixenwraith/color v0.0.0-20260714170240-79433f872c42 h1:wBqu4zX4jtEOhQVTI1x492N/DKrNpawY4RSxGyDlEXM= +github.com/lixenwraith/color v0.0.0-20260714170240-79433f872c42/go.mod h1:p02MsAGqlmZu3sc6BPYCKmaedF+lqBoijnuu5cOrYeo= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= diff --git a/inline/inline.go b/inline/inline.go index 9734285..0e2dbc7 100644 --- a/inline/inline.go +++ b/inline/inline.go @@ -48,6 +48,10 @@ func New(w io.Writer) *Printer { } p.color = p.tty != nil && os.Getenv("NO_COLOR") == "" 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 } diff --git a/inline/style.go b/inline/style.go index 5503c87..728ccc0 100644 --- a/inline/style.go +++ b/inline/style.go @@ -7,22 +7,23 @@ import ( "strings" "unicode/utf8" + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) // Style describes text appearance; zero value is unstyled. // Composable: inline.Fg(terminal.Amber).Attr(terminal.AttrBold) type Style struct { - fg, bg terminal.RGB + fg, bg color.RGB hasFg, hasBg bool attr terminal.Attr } // 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 -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 func (s Style) Attr(a terminal.Attr) Style { s.attr |= a; return s } diff --git a/output.go b/output.go index 38b16f7..b59daa5 100644 --- a/output.go +++ b/output.go @@ -2,6 +2,8 @@ package terminal import ( "bufio" + + "github.com/lixenwraith/color" ) // outputBuffer manages double-buffered terminal output with diffing @@ -17,8 +19,8 @@ type outputBuffer struct { cursorValid bool // Style state for coalescing - lastFg RGB - lastBg RGB + lastFg color.RGB + lastBg color.RGB lastAttr Attr 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 -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 fgChanged := !o.lastValid || fg != o.lastFg || (attr&AttrFg256) != (o.lastAttr&AttrFg256) 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) -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(';') if attr&AttrFg256 != 0 { // 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) -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(';') if attr&AttrBg256 != 0 { // 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 -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 { w.Write(csiFg256) 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 -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 { w.Write(csiBg256) writeInt(w, int(bg.R)) @@ -435,7 +437,7 @@ func (o *outputBuffer) forceFullRedraw() { } // 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.Write(csiSGR0) o.writeBgFull(w, bg, 0) @@ -453,4 +455,4 @@ func (o *outputBuffer) clear(bg RGB) { // invalidateCursor marks cursor position as unknown func (o *outputBuffer) invalidateCursor() { o.cursorValid = false -} \ No newline at end of file +} diff --git a/rgb_truecolor.go b/rgb_truecolor.go deleted file mode 100644 index 1d8db87..0000000 --- a/rgb_truecolor.go +++ /dev/null @@ -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} -) \ No newline at end of file diff --git a/terminal.go b/terminal.go index 1ced627..bf9021e 100644 --- a/terminal.go +++ b/terminal.go @@ -5,6 +5,8 @@ import ( "os" "sync" "sync/atomic" + + "github.com/lixenwraith/color" ) // Attr represents text attributes (bitmask) @@ -28,8 +30,8 @@ const AttrStyle Attr = AttrBold | AttrDim | AttrItalic | AttrUnderline | AttrBli // Cell represents a single terminal cell type Cell struct { Rune rune - Fg RGB - Bg RGB + Fg color.RGB + Bg color.RGB Attrs Attr } @@ -55,7 +57,7 @@ type Terminal interface { Flush(cells []Cell, width, height int) // Clear fills screen with specified background color - Clear(bg RGB) + Clear(bg color.RGB) // SetCursorVisible shows/hides cursor SetCursorVisible(visible bool) @@ -172,8 +174,13 @@ func (t *termImpl) Init() error { // Invisible cursor 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 - t.output.clear(RGBBlack) + t.output.clear(color.Black) // Start input reader t.input.start() @@ -259,7 +266,7 @@ func (t *termImpl) Flush(cells []Cell, width, height int) { } // Clear fills screen with background color -func (t *termImpl) Clear(bg RGB) { +func (t *termImpl) Clear(bg color.RGB) { t.mu.Lock() defer t.mu.Unlock() @@ -336,7 +343,7 @@ func (t *termImpl) Sync() { // Clear terminal before full redraw // Diff-based rendering assumes physical terminal matches front buffer state - t.output.clear(RGBBlack) + t.output.clear(color.Black) 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 // This is best-effort; ignore errors in crash context resetTerminalMode() -} \ No newline at end of file +} diff --git a/tui/README.md b/tui/README.md index a2ffb5c..ac4865a 100644 --- a/tui/README.md +++ b/tui/README.md @@ -200,7 +200,7 @@ Pure logic, no rendering — usable independently: - Width calculations count runes, not terminal columns; East Asian wide 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. - Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require application-side geometry from the regions used. diff --git a/tui/border.go b/tui/border.go index 4039950..71eaa7c 100644 --- a/tui/border.go +++ b/tui/border.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -36,7 +37,7 @@ const ( // --- Box Rendering --- // 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 { return } @@ -45,7 +46,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) { } chars := boxChars[line] - bg := terminal.RGB{} // Transparent (use existing bg) + bg := color.RGB{} // Transparent (use existing bg) // Corners 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 -func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) { +func (r Region) BoxFilled(line LineType, fg, bg color.RGB) { // Fill interior first for y := 1; y < r.H-1; y++ { for x := 1; x < r.W-1; x++ { @@ -81,7 +82,7 @@ func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) { // --- Line rendering --- // 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 { return } @@ -90,12 +91,12 @@ func (r Region) HLine(y int, line LineType, fg terminal.RGB) { } ch := boxChars[line][boxH] 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 -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 { return } @@ -104,12 +105,12 @@ func (r Region) VLine(x int, line LineType, fg terminal.RGB) { } ch := boxChars[line][boxV] 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 -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 { return } @@ -121,7 +122,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) { // Fill with horizontal line 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 @@ -134,7 +135,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) { } startX := (r.W - textLen) / 2 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 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) 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) } 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) -} \ No newline at end of file +} + diff --git a/tui/button.go b/tui/button.go index 2dbc996..9fe47c6 100644 --- a/tui/button.go +++ b/tui/button.go @@ -1,6 +1,9 @@ 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 type Button struct { @@ -18,27 +21,27 @@ type ButtonBarOpts struct { // ButtonStyle defines button bar colors type ButtonStyle struct { - LabelFg terminal.RGB - LabelBg terminal.RGB - KeyFg terminal.RGB - FocusFg terminal.RGB - FocusBg terminal.RGB - Bg terminal.RGB + LabelFg color.RGB + LabelBg color.RGB + KeyFg color.RGB + FocusFg color.RGB + FocusBg color.RGB + Bg color.RGB } // DefaultButtonStyle returns default button colors with dark background 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 -func DefaultButtonStyleFrom(bg terminal.RGB) ButtonStyle { +func DefaultButtonStyleFrom(bg color.RGB) ButtonStyle { return ButtonStyle{ - LabelFg: terminal.RGB{R: 200, G: 200, B: 200}, - LabelBg: terminal.RGB{R: 50, G: 50, B: 60}, - KeyFg: terminal.RGB{R: 130, G: 130, B: 150}, - FocusFg: terminal.RGB{R: 255, G: 255, B: 255}, - FocusBg: terminal.RGB{R: 60, G: 80, B: 120}, + LabelFg: color.RGB{R: 200, G: 200, B: 200}, + LabelBg: color.RGB{R: 50, G: 50, B: 60}, + KeyFg: color.RGB{R: 130, G: 130, B: 150}, + FocusFg: color.RGB{R: 255, G: 255, B: 255}, + FocusBg: color.RGB{R: 60, G: 80, B: 120}, Bg: bg, } } @@ -124,4 +127,5 @@ func (r Region) ButtonBar(y int, buttons []Button, opts ButtonBarOpts) { x += gap } } -} \ No newline at end of file +} + diff --git a/tui/checkbox.go b/tui/checkbox.go index 9dadcf4..cad4264 100644 --- a/tui/checkbox.go +++ b/tui/checkbox.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -15,7 +16,7 @@ const ( ) // 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 { return } @@ -30,7 +31,8 @@ func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) { case CheckPlus: ch = '+' } - r.Cell(x, y, '[', fg, terminal.RGB{}, terminal.AttrNone) - r.Cell(x+1, y, ch, fg, terminal.RGB{}, terminal.AttrNone) - r.Cell(x+2, y, ']', fg, terminal.RGB{}, terminal.AttrNone) -} \ No newline at end of file + r.Cell(x, y, '[', fg, color.RGB{}, terminal.AttrNone) + r.Cell(x+1, y, ch, fg, color.RGB{}, terminal.AttrNone) + r.Cell(x+2, y, ']', fg, color.RGB{}, terminal.AttrNone) +} + diff --git a/tui/dialog.go b/tui/dialog.go index d942601..fb9d633 100644 --- a/tui/dialog.go +++ b/tui/dialog.go @@ -1,6 +1,9 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // ConfirmResult represents dialog outcome type ConfirmResult uint8 @@ -98,31 +101,31 @@ type ConfirmOpts struct { // ConfirmStyle defines dialog colors type ConfirmStyle struct { - BorderFg terminal.RGB - TitleFg terminal.RGB - MessageFg terminal.RGB - Bg terminal.RGB - ButtonFg terminal.RGB - ButtonBg terminal.RGB - ButtonFocusFg terminal.RGB - ButtonFocusBg terminal.RGB - DestructiveFg terminal.RGB - DestructiveBg terminal.RGB + BorderFg color.RGB + TitleFg color.RGB + MessageFg color.RGB + Bg color.RGB + ButtonFg color.RGB + ButtonBg color.RGB + ButtonFocusFg color.RGB + ButtonFocusBg color.RGB + DestructiveFg color.RGB + DestructiveBg color.RGB } // DefaultConfirmStyle returns default dialog colors func DefaultConfirmStyle() ConfirmStyle { return ConfirmStyle{ - BorderFg: terminal.RGB{R: 100, G: 100, B: 120}, - TitleFg: terminal.RGB{R: 255, G: 255, B: 255}, - MessageFg: terminal.RGB{R: 200, G: 200, B: 200}, - Bg: terminal.RGB{R: 30, G: 30, B: 40}, - ButtonFg: terminal.RGB{R: 180, G: 180, B: 180}, - ButtonBg: terminal.RGB{R: 50, G: 50, B: 60}, - ButtonFocusFg: terminal.RGB{R: 255, G: 255, B: 255}, - ButtonFocusBg: terminal.RGB{R: 60, G: 80, B: 120}, - DestructiveFg: terminal.RGB{R: 255, G: 255, B: 255}, - DestructiveBg: terminal.RGB{R: 180, G: 60, B: 60}, + BorderFg: color.RGB{R: 100, G: 100, B: 120}, + TitleFg: color.RGB{R: 255, G: 255, B: 255}, + MessageFg: color.RGB{R: 200, G: 200, B: 200}, + Bg: color.RGB{R: 30, G: 30, B: 40}, + ButtonFg: color.RGB{R: 180, G: 180, B: 180}, + ButtonBg: color.RGB{R: 50, G: 50, B: 60}, + ButtonFocusFg: color.RGB{R: 255, G: 255, B: 255}, + ButtonFocusBg: color.RGB{R: 60, G: 80, B: 120}, + DestructiveFg: color.RGB{R: 255, G: 255, B: 255}, + 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) -} \ No newline at end of file +} + diff --git a/tui/editor.go b/tui/editor.go index 618f993..6f83052 100644 --- a/tui/editor.go +++ b/tui/editor.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -16,27 +17,27 @@ type EditorOpts struct { // EditorStyle defines editor colors type EditorStyle struct { - TextFg terminal.RGB - TextBg terminal.RGB - CursorFg terminal.RGB - CursorBg terminal.RGB - LineNumFg terminal.RGB - LineNumBg terminal.RGB - CurrentLineBg terminal.RGB - BorderFg terminal.RGB + TextFg color.RGB + TextBg color.RGB + CursorFg color.RGB + CursorBg color.RGB + LineNumFg color.RGB + LineNumBg color.RGB + CurrentLineBg color.RGB + BorderFg color.RGB } // DefaultEditorStyle returns default colors func DefaultEditorStyle() EditorStyle { return EditorStyle{ - TextFg: terminal.RGB{R: 220, G: 220, B: 220}, - TextBg: terminal.RGB{R: 25, G: 25, B: 35}, - CursorFg: terminal.RGB{R: 0, G: 0, B: 0}, - CursorBg: terminal.RGB{R: 200, G: 200, B: 200}, - LineNumFg: terminal.RGB{R: 100, G: 100, B: 120}, - LineNumBg: terminal.RGB{R: 30, G: 30, B: 40}, - CurrentLineBg: terminal.RGB{R: 35, G: 35, B: 50}, - BorderFg: terminal.RGB{R: 80, G: 80, B: 100}, + TextFg: color.RGB{R: 220, G: 220, B: 220}, + TextBg: color.RGB{R: 25, G: 25, B: 35}, + CursorFg: color.RGB{R: 0, G: 0, B: 0}, + CursorBg: color.RGB{R: 200, G: 200, B: 200}, + LineNumFg: color.RGB{R: 100, G: 100, B: 120}, + LineNumBg: color.RGB{R: 30, G: 30, B: 40}, + CurrentLineBg: color.RGB{R: 35, G: 35, B: 50}, + BorderFg: color.RGB{R: 80, G: 80, B: 100}, } } @@ -194,4 +195,5 @@ func formatLineNum(num, width int) string { s = " " + s } return s -} \ No newline at end of file +} + diff --git a/tui/form.go b/tui/form.go index 5a7ed36..4feeaff 100644 --- a/tui/form.go +++ b/tui/form.go @@ -1,6 +1,9 @@ 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 type FormField struct { @@ -103,25 +106,25 @@ type FormOpts struct { // FormStyle defines form colors type FormStyle struct { - LabelFg terminal.RGB - FieldFg terminal.RGB - FieldBg terminal.RGB - FocusBg terminal.RGB - CursorFg terminal.RGB - CursorBg terminal.RGB - Bg terminal.RGB + LabelFg color.RGB + FieldFg color.RGB + FieldBg color.RGB + FocusBg color.RGB + CursorFg color.RGB + CursorBg color.RGB + Bg color.RGB } // DefaultFormStyle returns default form colors func DefaultFormStyle() FormStyle { return FormStyle{ - LabelFg: terminal.RGB{R: 150, G: 150, B: 180}, - FieldFg: terminal.RGB{R: 220, G: 220, B: 220}, - FieldBg: terminal.RGB{R: 35, G: 35, B: 45}, - FocusBg: terminal.RGB{R: 45, G: 45, B: 60}, - CursorFg: terminal.RGB{R: 0, G: 0, B: 0}, - CursorBg: terminal.RGB{R: 200, G: 200, B: 200}, - Bg: terminal.RGB{R: 25, G: 25, B: 35}, + LabelFg: color.RGB{R: 150, G: 150, B: 180}, + FieldFg: color.RGB{R: 220, G: 220, B: 220}, + FieldBg: color.RGB{R: 35, G: 35, B: 45}, + FocusBg: color.RGB{R: 45, G: 45, B: 60}, + CursorFg: color.RGB{R: 0, G: 0, B: 0}, + CursorBg: color.RGB{R: 200, G: 200, B: 200}, + Bg: color.RGB{R: 25, G: 25, B: 35}, } } @@ -225,4 +228,5 @@ func (r Region) Form(state *FormState, opts FormOpts) int { } return y -} \ No newline at end of file +} + diff --git a/tui/input.go b/tui/input.go index b5c28f8..d3434f5 100644 --- a/tui/input.go +++ b/tui/input.go @@ -1,18 +1,19 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) // InputOpts configures single-line input field type InputOpts struct { Label string - LabelFg terminal.RGB + LabelFg color.RGB Text string Cursor int // Cursor position in text (rune index) - CursorBg terminal.RGB - TextFg terminal.RGB - Bg terminal.RGB + CursorBg color.RGB + TextFg color.RGB + Bg color.RGB } // 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 { r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone) } -} \ No newline at end of file +} + diff --git a/tui/list.go b/tui/list.go index d0ab82c..52fc638 100644 --- a/tui/list.go +++ b/tui/list.go @@ -1,22 +1,25 @@ 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 type ListItem struct { Indent int // Left padding in cells Icon rune // Expand indicator or bullet, 0 = none - IconFg terminal.RGB + IconFg color.RGB Check CheckState // CheckNone to skip checkbox - CheckFg terminal.RGB + CheckFg color.RGB Text string TextStyle Style } // ListOpts configures list rendering type ListOpts struct { - CursorBg terminal.RGB - DefaultBg terminal.RGB + CursorBg color.RGB + DefaultBg color.RGB 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 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 @@ -61,7 +64,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int { x += iconW // Checkbox - if item.Check != CheckNone || item.CheckFg != (terminal.RGB{}) { + if item.Check != CheckNone || item.CheckFg != (color.RGB{}) { if x+3 <= r.W { var ch rune switch item.Check { @@ -83,7 +86,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int { // Text textStyle := item.TextStyle - if textStyle.Bg == (terminal.RGB{}) { + if textStyle.Bg == (color.RGB{}) { textStyle.Bg = bg } text := item.Text @@ -96,4 +99,5 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int { } return rendered -} \ No newline at end of file +} + diff --git a/tui/modal.go b/tui/modal.go index 0d28c54..b44be58 100644 --- a/tui/modal.go +++ b/tui/modal.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -9,10 +10,10 @@ type ModalOpts struct { Title string Hint string // Right-aligned hint text Border LineType - BorderFg terminal.RGB - TitleFg terminal.RGB - HintFg terminal.RGB - Bg terminal.RGB + BorderFg color.RGB + TitleFg color.RGB + HintFg color.RGB + Bg color.RGB } // 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 r.Sub(1, 1, r.W-2, r.H-2) -} \ No newline at end of file +} + diff --git a/tui/overlay.go b/tui/overlay.go index f4411c2..5b512f1 100644 --- a/tui/overlay.go +++ b/tui/overlay.go @@ -1,6 +1,9 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // OverlayStyle specifies overlay appearance type OverlayStyle uint8 @@ -17,10 +20,10 @@ type OverlayOpts struct { Style OverlayStyle Title string Border LineType - Bg terminal.RGB - Fg terminal.RGB // Border and title color - TitleBg terminal.RGB // Title bar background, zero = same as Fg - TitleFg terminal.RGB // Title text color, zero = same as Bg + Bg color.RGB + Fg color.RGB // Border and title color + TitleBg color.RGB // Title bar background, zero = same as Fg + TitleFg color.RGB // Title text color, zero = same as Bg // Modal/Floating positioning (ignored for Fullscreen) Width int // 0 = 80% of region @@ -28,7 +31,7 @@ type OverlayOpts struct { X, Y int // Offset from center, 0 = centered // Shadow for Floating style - ShadowColor terminal.RGB + ShadowColor color.RGB } // DefaultOverlayOpts returns sensible defaults for modal overlay @@ -37,10 +40,10 @@ func DefaultOverlayOpts(title string) OverlayOpts { Style: OverlayModal, Title: title, Border: LineDouble, - Bg: terminal.RGB{R: 25, G: 25, B: 35}, - Fg: terminal.RGB{R: 100, G: 140, B: 180}, - TitleBg: terminal.RGB{R: 40, G: 60, B: 90}, - TitleFg: terminal.RGB{R: 255, G: 255, B: 255}, + Bg: color.RGB{R: 25, G: 25, B: 35}, + Fg: color.RGB{R: 100, G: 140, B: 180}, + TitleBg: color.RGB{R: 40, G: 60, B: 90}, + TitleFg: color.RGB{R: 255, G: 255, B: 255}, } } @@ -49,10 +52,10 @@ func FullscreenOverlayOpts(title string) OverlayOpts { return OverlayOpts{ Style: OverlayFullscreen, Title: title, - Bg: terminal.RGB{R: 20, G: 20, B: 30}, - Fg: terminal.RGB{R: 100, G: 140, B: 180}, - TitleBg: terminal.RGB{R: 40, G: 60, B: 90}, - TitleFg: terminal.RGB{R: 255, G: 255, B: 255}, + Bg: color.RGB{R: 20, G: 20, B: 30}, + Fg: color.RGB{R: 100, G: 140, B: 180}, + TitleBg: color.RGB{R: 40, G: 60, B: 90}, + TitleFg: color.RGB{R: 255, G: 255, B: 255}, } } @@ -98,11 +101,11 @@ func (r Region) renderFullscreenOverlay(opts OverlayOpts) OverlayResult { // Title bar if opts.Title != "" { titleBg := opts.TitleBg - if titleBg == (terminal.RGB{}) { + if titleBg == (color.RGB{}) { titleBg = opts.Fg } titleFg := opts.TitleFg - if titleFg == (terminal.RGB{}) { + if titleFg == (color.RGB{}) { titleFg = opts.Bg } @@ -188,11 +191,11 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult { // Title in top border if opts.Title != "" && contentW > 2 { titleBg := opts.TitleBg - if titleBg == (terminal.RGB{}) { + if titleBg == (color.RGB{}) { titleBg = opts.Fg } titleFg := opts.TitleFg - if titleFg == (terminal.RGB{}) { + if titleFg == (color.RGB{}) { titleFg = opts.Bg } @@ -224,8 +227,8 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult { func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult { // Same as modal but with shadow shadowColor := opts.ShadowColor - if shadowColor == (terminal.RGB{}) { - shadowColor = terminal.RGB{R: 10, G: 10, B: 15} + if shadowColor == (color.RGB{}) { + shadowColor = color.RGB{R: 10, G: 10, B: 15} } // Calculate dimensions (same as modal) @@ -285,11 +288,11 @@ func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult { if opts.Title != "" && contentW > 2 { titleBg := opts.TitleBg - if titleBg == (terminal.RGB{}) { + if titleBg == (color.RGB{}) { titleBg = opts.Fg } titleFg := opts.TitleFg - if titleFg == (terminal.RGB{}) { + if titleFg == (color.RGB{}) { titleFg = opts.Bg } @@ -327,7 +330,7 @@ func (r Region) renderBorderTitleOverlay(opts OverlayOpts) OverlayResult { if opts.Title != "" && r.W > 6 { titleFg := opts.TitleFg - if titleFg == (terminal.RGB{}) { + if titleFg == (color.RGB{}) { titleFg = opts.Fg } title := " " + opts.Title + " " @@ -370,4 +373,5 @@ func (o *OverlayState) Hide() { // Toggle switches visibility func (o *OverlayState) Toggle() { o.Visible = !o.Visible -} \ No newline at end of file +} + diff --git a/tui/pane.go b/tui/pane.go index 3669baa..39bf03d 100644 --- a/tui/pane.go +++ b/tui/pane.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -8,9 +9,9 @@ import ( type PaneOpts struct { Title string Border LineType - BorderFg terminal.RGB - Bg terminal.RGB - TitleFg terminal.RGB + BorderFg color.RGB + Bg color.RGB + TitleFg color.RGB } // 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 // 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) if title != "" && r.H > 0 { 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 -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 { bg = focusBg } return r.TitledPane(title, titleFg, bg) -} \ No newline at end of file +} + diff --git a/tui/progress.go b/tui/progress.go index c710032..7d0a6b7 100644 --- a/tui/progress.go +++ b/tui/progress.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -12,7 +13,7 @@ const ( ) // 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 { 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) -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 { 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 -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 { return } @@ -80,11 +81,11 @@ func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) { if idx < 0 { 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 -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 { 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)) + "%" } r.Text(x+2+barW, y, label, fg, bg, terminal.AttrNone) -} \ No newline at end of file +} + diff --git a/tui/progress_overlay.go b/tui/progress_overlay.go index 193639a..45184e6 100644 --- a/tui/progress_overlay.go +++ b/tui/progress_overlay.go @@ -1,6 +1,9 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // ProgressType specifies progress indicator variant type ProgressType uint8 @@ -93,11 +96,11 @@ type ProgressOverlayOpts struct { Width int // Overlay width, 0 = auto Cancelable bool // Show cancel hint CancelKey string // e.g., "Esc" - Fg terminal.RGB - Bg terminal.RGB - BarFg terminal.RGB - BarBg terminal.RGB - AccentFg terminal.RGB // Spinner/highlight color + Fg color.RGB + Bg color.RGB + BarFg color.RGB + BarBg color.RGB + AccentFg color.RGB // Spinner/highlight color } // DefaultProgressOpts returns sensible defaults @@ -111,11 +114,11 @@ func DefaultProgressOpts(title, message string, ptype ProgressType) ProgressOver BarStyle: BarStyleBlock, ShowPercent: true, Width: 40, - Fg: terminal.RGB{R: 220, G: 220, B: 220}, - Bg: terminal.RGB{R: 30, G: 30, B: 40}, - BarFg: terminal.RGB{R: 80, G: 160, B: 255}, - BarBg: terminal.RGB{R: 50, G: 50, B: 60}, - AccentFg: terminal.RGB{R: 100, G: 200, B: 255}, + Fg: color.RGB{R: 220, G: 220, B: 220}, + Bg: color.RGB{R: 30, G: 30, B: 40}, + BarFg: color.RGB{R: 80, G: 160, B: 255}, + BarBg: color.RGB{R: 50, G: 50, B: 60}, + AccentFg: color.RGB{R: 100, G: 200, B: 255}, } } @@ -169,12 +172,12 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region { case ProgressStyleShadow: // Draw shadow first 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 case ProgressStyleNeon: borderLine = LineDouble - opts.AccentFg = terminal.RGB{R: 0, G: 255, B: 200} - opts.BarFg = terminal.RGB{R: 255, G: 0, B: 255} + opts.AccentFg = color.RGB{R: 0, G: 255, B: 200} + opts.BarFg = color.RGB{R: 255, G: 0, B: 255} case ProgressStyleRetro: borderLine = LineHeavy opts.BarStyle = BarStyleBlock @@ -242,7 +245,7 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region { if opts.CancelKey == "" { 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 @@ -301,7 +304,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) { for x := 0; x < barW; x++ { var ch rune - var fg terminal.RGB + var fg color.RGB if x < filled { ch = chars[0] fg = opts.BarFg @@ -334,7 +337,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) { for x := 0; x < barW; x++ { var ch rune - var fg terminal.RGB + var fg color.RGB if x >= pos && x < pos+markerW { ch = chars[0] fg = opts.BarFg @@ -365,11 +368,11 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) { for x := 0; x < barW; x++ { var ch rune - var fg terminal.RGB + var fg color.RGB if x < filled { ch = chars[0] // Pulse the color - fg = terminal.RGB{ + fg = color.RGB{ R: uint8(float64(opts.BarFg.R) * (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)), @@ -390,7 +393,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) { // ETA if 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 func (p *ProgressState) Show() { p.Visible = true -} \ No newline at end of file +} + diff --git a/tui/region.go b/tui/region.go index 9da97f3..e68aea8 100644 --- a/tui/region.go +++ b/tui/region.go @@ -1,6 +1,9 @@ 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 // 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 -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 { 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 -func (r Region) Fill(bg terminal.RGB) { +func (r Region) Fill(bg color.RGB) { for y := 0; y < r.H; y++ { 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 func (r Region) Clear() { - r.Fill(terminal.RGB{}) + r.Fill(color.RGB{}) } // Width returns region width @@ -109,4 +112,5 @@ func (r Region) Height() int { // Bounds returns absolute position and dimensions func (r Region) Bounds() (x, y, w, h int) { return r.X, r.Y, r.W, r.H -} \ No newline at end of file +} + diff --git a/tui/render.go b/tui/render.go index d9fe7b9..d6d2356 100644 --- a/tui/render.go +++ b/tui/render.go @@ -1,12 +1,15 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // Default spinner frames for Region.Spinner var spinnerFrames = spinnerSets[SpinnerBraille] // 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 { return } @@ -40,19 +43,19 @@ func (r Region) TextStyled(x, y int, s string, style Style) { } // 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) r.Text(x, y, s, fg, bg, attr) } // 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 r.Text(x, y, s, fg, bg, attr) } // 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 == "" { 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 { return r.TextBlock(x, y, text, style.Fg, style.Bg, style.Attr) } - diff --git a/tui/scrollbar.go b/tui/scrollbar.go index d6bb4ae..ed44fc7 100644 --- a/tui/scrollbar.go +++ b/tui/scrollbar.go @@ -1,11 +1,12 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) // 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 { return } @@ -14,7 +15,7 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) { if total <= visible || trackH < 3 { // No scrolling needed or track too small for y := range trackH { - r.Cell(x, y, '│', fg, terminal.RGB{}, terminal.AttrDim) + r.Cell(x, y, '│', fg, color.RGB{}, terminal.AttrDim) } return } @@ -42,12 +43,12 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) { } else { 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%) -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 { 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) } - diff --git a/tui/status_bar.go b/tui/status_bar.go index 6037cae..c808441 100644 --- a/tui/status_bar.go +++ b/tui/status_bar.go @@ -1,6 +1,9 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // BarSection represents one segment of a status bar type BarSection struct { @@ -25,7 +28,7 @@ const ( type BarOpts struct { Separator string // Between sections, default " │ " SepStyle Style // Separator styling - Bg terminal.RGB + Bg color.RGB Align BarAlign Padding int // Left/right padding, default 1 } @@ -34,7 +37,7 @@ type BarOpts struct { func DefaultBarOpts() BarOpts { return BarOpts{ 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, Align: BarAlignRight, } @@ -55,7 +58,7 @@ func (r Region) StatusBar(y int, sections []BarSection, opts BarOpts) { // Fill background 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) @@ -193,7 +196,7 @@ func truncateSections(sections []BarSection, widths []int, sepLen, availW int) ( } // 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)) for i, p := range pairs { sections[i] = BarSection{ @@ -207,4 +210,4 @@ func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg te Bg: bg, Align: BarAlignRight, }) -} \ No newline at end of file +} diff --git a/tui/style.go b/tui/style.go index 1dd0d7b..5cf8d84 100644 --- a/tui/style.go +++ b/tui/style.go @@ -1,22 +1,24 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) // Style bundles foreground, background, and attributes for text rendering type Style struct { - Fg terminal.RGB - Bg terminal.RGB + Fg color.RGB + Bg color.RGB Attr terminal.Attr } // DefaultStyle returns style with zero values (transparent bg) -func DefaultStyle(fg terminal.RGB) Style { +func DefaultStyle(fg color.RGB) Style { return Style{Fg: fg} } // IsZero returns true if style has no colors or attributes set func (s Style) IsZero() bool { - return s.Fg == (terminal.RGB{}) && s.Bg == (terminal.RGB{}) && s.Attr == terminal.AttrNone -} \ No newline at end of file + return s.Fg == (color.RGB{}) && s.Bg == (color.RGB{}) && s.Attr == terminal.AttrNone +} + diff --git a/tui/textfield.go b/tui/textfield.go index 6a79260..0c0eb30 100644 --- a/tui/textfield.go +++ b/tui/textfield.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) @@ -18,25 +19,25 @@ type TextFieldOpts struct { // DefaultTextFieldStyle returns default colors func DefaultTextFieldStyle() TextFieldStyle { return TextFieldStyle{ - TextFg: terminal.RGB{R: 220, G: 220, B: 220}, - TextBg: terminal.RGB{R: 30, G: 30, B: 40}, - CursorFg: terminal.RGB{R: 0, G: 0, B: 0}, - CursorBg: terminal.RGB{R: 200, G: 200, B: 200}, - PlaceholderFg: terminal.RGB{R: 100, G: 100, B: 110}, - PrefixFg: terminal.RGB{R: 150, G: 150, B: 180}, - BorderFg: terminal.RGB{R: 80, G: 80, B: 100}, + TextFg: color.RGB{R: 220, G: 220, B: 220}, + TextBg: color.RGB{R: 30, G: 30, B: 40}, + CursorFg: color.RGB{R: 0, G: 0, B: 0}, + CursorBg: color.RGB{R: 200, G: 200, B: 200}, + PlaceholderFg: color.RGB{R: 100, G: 100, B: 110}, + PrefixFg: color.RGB{R: 150, G: 150, B: 180}, + BorderFg: color.RGB{R: 80, G: 80, B: 100}, } } // TextFieldStyle defines text field colors type TextFieldStyle struct { - TextFg terminal.RGB - TextBg terminal.RGB - CursorFg terminal.RGB - CursorBg terminal.RGB - PlaceholderFg terminal.RGB - PrefixFg terminal.RGB - BorderFg terminal.RGB + TextFg color.RGB + TextBg color.RGB + CursorFg color.RGB + CursorBg color.RGB + PlaceholderFg color.RGB + PrefixFg color.RGB + BorderFg color.RGB } // TextField renders text field and returns content height used @@ -168,4 +169,5 @@ func boolToInt(b bool) int { return 1 } return 0 -} \ No newline at end of file +} + diff --git a/tui/theme.go b/tui/theme.go index 02591e3..e735a2d 100644 --- a/tui/theme.go +++ b/tui/theme.go @@ -1,63 +1,66 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" +) // Theme defines semantic colors for TUI components type Theme struct { - Bg terminal.RGB - Fg terminal.RGB - FocusBg terminal.RGB - CursorBg terminal.RGB + Bg color.RGB + Fg color.RGB + FocusBg color.RGB + CursorBg color.RGB - Selected terminal.RGB - Unselected terminal.RGB - Partial terminal.RGB - Error terminal.RGB - Warning terminal.RGB + Selected color.RGB + Unselected color.RGB + Partial color.RGB + Error color.RGB + Warning color.RGB - Border terminal.RGB - HeaderBg terminal.RGB - HeaderFg terminal.RGB - StatusFg terminal.RGB - HintFg terminal.RGB - InputBg terminal.RGB + Border color.RGB + HeaderBg color.RGB + HeaderFg color.RGB + StatusFg color.RGB + HintFg color.RGB + InputBg color.RGB - DirFg terminal.RGB - FileFg terminal.RGB - SymbolFg terminal.RGB + DirFg color.RGB + FileFg color.RGB + SymbolFg color.RGB - SyntaxComment terminal.RGB - SyntaxString terminal.RGB - SyntaxKeyword terminal.RGB - SyntaxType terminal.RGB - SyntaxNumber terminal.RGB - SyntaxSymbol terminal.RGB + SyntaxComment color.RGB + SyntaxString color.RGB + SyntaxKeyword color.RGB + SyntaxType color.RGB + SyntaxNumber color.RGB + SyntaxSymbol color.RGB } // DefaultTheme provides reasonable defaults var DefaultTheme = Theme{ - Bg: terminal.RGB{R: 20, G: 20, B: 30}, - Fg: terminal.RGB{R: 200, G: 200, B: 200}, - FocusBg: terminal.RGB{R: 30, G: 35, B: 45}, - CursorBg: terminal.RGB{R: 50, G: 50, B: 70}, - Selected: terminal.RGB{R: 80, G: 200, B: 80}, - Unselected: terminal.RGB{R: 100, G: 100, B: 100}, - Partial: terminal.RGB{R: 80, G: 160, B: 220}, - Error: terminal.RGB{R: 255, G: 80, B: 80}, - Warning: terminal.RGB{R: 255, G: 80, B: 80}, - Border: terminal.RGB{R: 60, G: 80, B: 100}, - HeaderBg: terminal.RGB{R: 40, G: 60, B: 90}, - HeaderFg: terminal.RGB{R: 255, G: 255, B: 255}, - StatusFg: terminal.RGB{R: 140, G: 140, B: 140}, - HintFg: terminal.RGB{R: 100, G: 180, B: 200}, - InputBg: terminal.RGB{R: 30, G: 30, B: 50}, - DirFg: terminal.RGB{R: 130, G: 170, B: 220}, - FileFg: terminal.RGB{R: 200, G: 200, B: 200}, - SymbolFg: terminal.RGB{R: 180, G: 220, B: 220}, - SyntaxComment: terminal.RGB{R: 100, G: 110, B: 120}, - SyntaxString: terminal.RGB{R: 180, G: 220, B: 140}, - SyntaxKeyword: terminal.RGB{R: 180, G: 140, B: 220}, - SyntaxType: terminal.RGB{R: 80, G: 200, B: 200}, - SyntaxNumber: terminal.RGB{R: 220, G: 180, B: 120}, - SyntaxSymbol: terminal.RGB{R: 220, G: 180, B: 80}, -} \ No newline at end of file + Bg: color.RGB{R: 20, G: 20, B: 30}, + Fg: color.RGB{R: 200, G: 200, B: 200}, + FocusBg: color.RGB{R: 30, G: 35, B: 45}, + CursorBg: color.RGB{R: 50, G: 50, B: 70}, + Selected: color.RGB{R: 80, G: 200, B: 80}, + Unselected: color.RGB{R: 100, G: 100, B: 100}, + Partial: color.RGB{R: 80, G: 160, B: 220}, + Error: color.RGB{R: 255, G: 80, B: 80}, + Warning: color.RGB{R: 255, G: 80, B: 80}, + Border: color.RGB{R: 60, G: 80, B: 100}, + HeaderBg: color.RGB{R: 40, G: 60, B: 90}, + HeaderFg: color.RGB{R: 255, G: 255, B: 255}, + StatusFg: color.RGB{R: 140, G: 140, B: 140}, + HintFg: color.RGB{R: 100, G: 180, B: 200}, + InputBg: color.RGB{R: 30, G: 30, B: 50}, + DirFg: color.RGB{R: 130, G: 170, B: 220}, + FileFg: color.RGB{R: 200, G: 200, B: 200}, + SymbolFg: color.RGB{R: 180, G: 220, B: 220}, + SyntaxComment: color.RGB{R: 100, G: 110, B: 120}, + SyntaxString: color.RGB{R: 180, G: 220, B: 140}, + SyntaxKeyword: color.RGB{R: 180, G: 140, B: 220}, + SyntaxType: color.RGB{R: 80, G: 200, B: 200}, + SyntaxNumber: color.RGB{R: 220, G: 180, B: 120}, + SyntaxSymbol: color.RGB{R: 220, G: 180, B: 80}, +} + diff --git a/tui/toast.go b/tui/toast.go index d8489ef..39651e4 100644 --- a/tui/toast.go +++ b/tui/toast.go @@ -1,6 +1,9 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // ToastPosition specifies where toast renders type ToastPosition uint8 @@ -46,26 +49,26 @@ var ToastIcons = map[ToastSeverity]rune{ } // 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: { - Fg: terminal.RGB{R: 200, G: 200, B: 200}, - Bg: terminal.RGB{R: 40, G: 40, B: 50}, - Icon: terminal.RGB{R: 100, G: 150, B: 255}, + Fg: color.RGB{R: 200, G: 200, B: 200}, + Bg: color.RGB{R: 40, G: 40, B: 50}, + Icon: color.RGB{R: 100, G: 150, B: 255}, }, ToastSuccess: { - Fg: terminal.RGB{R: 220, G: 255, B: 220}, - Bg: terminal.RGB{R: 30, G: 60, B: 30}, - Icon: terminal.RGB{R: 80, G: 220, B: 80}, + Fg: color.RGB{R: 220, G: 255, B: 220}, + Bg: color.RGB{R: 30, G: 60, B: 30}, + Icon: color.RGB{R: 80, G: 220, B: 80}, }, ToastWarning: { - Fg: terminal.RGB{R: 255, G: 240, B: 200}, - Bg: terminal.RGB{R: 60, G: 50, B: 20}, - Icon: terminal.RGB{R: 255, G: 200, B: 60}, + Fg: color.RGB{R: 255, G: 240, B: 200}, + Bg: color.RGB{R: 60, G: 50, B: 20}, + Icon: color.RGB{R: 255, G: 200, B: 60}, }, ToastError: { - Fg: terminal.RGB{R: 255, G: 220, B: 220}, - Bg: terminal.RGB{R: 60, G: 25, B: 25}, - Icon: terminal.RGB{R: 255, G: 80, B: 80}, + Fg: color.RGB{R: 255, G: 220, B: 220}, + Bg: color.RGB{R: 60, G: 25, B: 25}, + Icon: color.RGB{R: 255, G: 80, B: 80}, }, } @@ -81,9 +84,9 @@ type ToastOpts struct { Padding int // Horizontal padding, default 1 MarginX int // Margin from edge for floating positions MarginY int // Margin from edge for floating positions - CustomFg terminal.RGB - CustomBg terminal.RGB - CustomIcon terminal.RGB + CustomFg color.RGB + CustomBg color.RGB + CustomIcon color.RGB } // DefaultToastOpts returns sensible defaults @@ -109,13 +112,13 @@ func (r Region) Toast(opts ToastOpts) Region { // Resolve colors fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon - if fg == (terminal.RGB{}) { + if fg == (color.RGB{}) { fg = ToastColors[opts.Severity].Fg } - if bg == (terminal.RGB{}) { + if bg == (color.RGB{}) { bg = ToastColors[opts.Severity].Bg } - if iconFg == (terminal.RGB{}) { + if iconFg == (color.RGB{}) { iconFg = ToastColors[opts.Severity].Icon } @@ -218,7 +221,7 @@ func (r Region) Toast(opts ToastOpts) Region { case ToastStyleShadow: // Shadow offset 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) r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0) } @@ -226,7 +229,7 @@ func (r Region) Toast(opts ToastOpts) Region { 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 { return } @@ -298,4 +301,5 @@ func (t *ToastState) Show(opts ToastOpts, frames int) { t.Opts = opts t.FramesLeft = frames t.Visible = true -} \ No newline at end of file +} + diff --git a/tui/tree.go b/tui/tree.go index 65d5099..a530d37 100644 --- a/tui/tree.go +++ b/tui/tree.go @@ -1,6 +1,9 @@ package tui -import "github.com/lixenwraith/terminal" +import ( + "github.com/lixenwraith/color" + "github.com/lixenwraith/terminal" +) // ExpandIcon chars const ( @@ -30,20 +33,20 @@ type TreeNode struct { Key string // Unique identifier for expansion state Label string // Display text Icon rune // Custom icon, 0 = auto (▶/▼ for expandable, • for leaf) - IconFg terminal.RGB + IconFg color.RGB Expandable bool // Has children Expanded bool // Currently expanded Depth int // Nesting level (0 = root) Check CheckState // Optional checkbox, CheckNone to skip - CheckFg terminal.RGB + CheckFg color.RGB Style Style // Text styling IsLast bool // Last sibling at this depth (for tree lines) Data any // Application payload - Suffix string // Secondary text after label (e.g., "(5 files)") - SuffixStyle Style // Styling for suffix, zero = dimmed version of Style - Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none - BadgeFg terminal.RGB // Badge color + Suffix string // Secondary text after label (e.g., "(5 files)") + SuffixStyle Style // Styling for suffix, zero = dimmed version of Style + Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none + BadgeFg color.RGB // Badge color } // 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 type TreeOpts struct { - CursorBg terminal.RGB - DefaultBg terminal.RGB + CursorBg color.RGB + DefaultBg color.RGB IndentWidth int // Cells per depth, default 2 IconWidth int // Width for icon column, default 2 LineMode TreeLineMode - LineFg terminal.RGB + LineFg color.RGB } // DefaultTreeOpts returns sensible defaults @@ -73,7 +76,7 @@ func DefaultTreeOpts() TreeOpts { IndentWidth: 2, IconWidth: 2, 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 - if lineFg == (terminal.RGB{}) { + if lineFg == (color.RGB{}) { 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++ { - r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone) + r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone) } x := 0 @@ -139,7 +142,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int { icon = IconBullet } } - if iconFg == (terminal.RGB{}) { + if iconFg == (color.RGB{}) { iconFg = lineFg } if x < r.W { @@ -148,10 +151,10 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int { x += iconW // Checkbox - if node.Check != CheckNone || node.CheckFg != (terminal.RGB{}) { + if node.Check != CheckNone || node.CheckFg != (color.RGB{}) { if x+3 <= r.W { checkFg := node.CheckFg - if checkFg == (terminal.RGB{}) { + if checkFg == (color.RGB{}) { checkFg = node.Style.Fg } var ch rune @@ -176,7 +179,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int { if node.Badge != 0 { if x < r.W { badgeFg := node.BadgeFg - if badgeFg == (terminal.RGB{}) { + if badgeFg == (color.RGB{}) { badgeFg = node.Style.Fg } 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 style := node.Style - if style.Bg == (terminal.RGB{}) { + if style.Bg == (color.RGB{}) { style.Bg = bg } @@ -214,15 +217,15 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int { // Suffix if suffix != "" { suffixStyle := node.SuffixStyle - if suffixStyle.Fg == (terminal.RGB{}) { + if suffixStyle.Fg == (color.RGB{}) { // Default: dimmed version of label style - suffixStyle.Fg = terminal.RGB{ + suffixStyle.Fg = color.RGB{ R: node.Style.Fg.R / 2, G: node.Style.Fg.G / 2, B: node.Style.Fg.B / 2, } } - if suffixStyle.Bg == (terminal.RGB{}) { + if suffixStyle.Bg == (color.RGB{}) { suffixStyle.Bg = bg } 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 -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 for d := 0; d < node.Depth; d++ { @@ -334,4 +337,5 @@ func FindPrevSiblingIndex(nodes []TreeNode, idx int) int { } } return -1 -} \ No newline at end of file +} +