v0.1.1 true color extracted to a standalone package, lut256 lazy build
This commit is contained in:
@@ -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}
|
||||
}
|
||||
@@ -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)]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-20260715115131-49124ee97342
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
)
|
||||
|
||||
@@ -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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
+13
-6
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+14
-12
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+18
-14
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -125,3 +128,4 @@ func (r Region) ButtonBar(y int, buttons []Button, opts ButtonBarOpts) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -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)
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+25
-21
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,3 +325,4 @@ func (r Region) AlertDialog(opts AlertOpts) Region {
|
||||
|
||||
return content.Sub(0, 0, content.W, buttonY-1)
|
||||
}
|
||||
|
||||
|
||||
+18
-16
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,3 +196,4 @@ func formatLineNum(num, width int) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
+19
-15
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,3 +229,4 @@ func (r Region) Form(state *FormState, opts FormOpts) int {
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -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
|
||||
@@ -76,3 +77,4 @@ func (r Region) Input(y int, opts InputOpts) {
|
||||
r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-8
@@ -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
|
||||
@@ -97,3 +100,4 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -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
|
||||
@@ -64,3 +65,4 @@ func (r Region) Modal(opts ModalOpts) Region {
|
||||
// Return content region
|
||||
return r.Sub(1, 1, r.W-2, r.H-2)
|
||||
}
|
||||
|
||||
|
||||
+27
-23
@@ -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 + " "
|
||||
@@ -371,3 +374,4 @@ func (o *OverlayState) Hide() {
|
||||
func (o *OverlayState) Toggle() {
|
||||
o.Visible = !o.Visible
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -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
|
||||
}
|
||||
@@ -122,3 +123,4 @@ func (r Region) Gauge(x, y, w int, value, max int, fg, bg terminal.RGB) {
|
||||
}
|
||||
r.Text(x+2+barW, y, label, fg, bg, terminal.AttrNone)
|
||||
}
|
||||
|
||||
|
||||
+24
-20
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,3 +467,4 @@ func (p *ProgressState) Dismiss() {
|
||||
func (p *ProgressState) Show() {
|
||||
p.Visible = true
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -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
|
||||
@@ -110,3 +113,4 @@ func (r Region) Height() int {
|
||||
func (r Region) Bounds() (x, y, w, h int) {
|
||||
return r.X, r.Y, r.W, r.H
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+8
-5
@@ -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{
|
||||
|
||||
+6
-4
@@ -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
|
||||
return s.Fg == (color.RGB{}) && s.Bg == (color.RGB{}) && s.Attr == terminal.AttrNone
|
||||
}
|
||||
|
||||
|
||||
+16
-14
@@ -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
|
||||
@@ -169,3 +170,4 @@ func boolToInt(b bool) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
+52
-49
@@ -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},
|
||||
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},
|
||||
}
|
||||
|
||||
|
||||
+26
-22
@@ -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
|
||||
}
|
||||
@@ -299,3 +302,4 @@ func (t *ToastState) Show(opts ToastOpts, frames int) {
|
||||
t.FramesLeft = frames
|
||||
t.Visible = true
|
||||
}
|
||||
|
||||
|
||||
+26
-22
@@ -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++ {
|
||||
@@ -335,3 +338,4 @@ func FindPrevSiblingIndex(nodes []TreeNode, idx int) int {
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user