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
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
)
|
||||||
|
|
||||||
// ColorMode indicates terminal color capability
|
// ColorMode indicates terminal color capability
|
||||||
type ColorMode uint8
|
type ColorMode uint8
|
||||||
|
|
||||||
@@ -8,85 +15,89 @@ const (
|
|||||||
ColorModeTrueColor // 24-bit RGB
|
ColorModeTrueColor // 24-bit RGB
|
||||||
)
|
)
|
||||||
|
|
||||||
// RGB represents a 24-bit color
|
|
||||||
type RGB struct {
|
|
||||||
R uint8 `toml:"r"`
|
|
||||||
G uint8 `toml:"g"`
|
|
||||||
B uint8 `toml:"b"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lerp linearly interpolates from c to other by t, clamped to [0,1]
|
|
||||||
func (c RGB) Lerp(other RGB, t float64) RGB {
|
|
||||||
if t <= 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
if t >= 1 {
|
|
||||||
return other
|
|
||||||
}
|
|
||||||
return RGB{
|
|
||||||
R: clampU8(float64(c.R) + (float64(other.R)-float64(c.R))*t),
|
|
||||||
G: clampU8(float64(c.G) + (float64(other.G)-float64(c.G))*t),
|
|
||||||
B: clampU8(float64(c.B) + (float64(other.B)-float64(c.B))*t),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGBBlack is the zero value black color
|
|
||||||
var RGBBlack = RGB{0, 0, 0}
|
|
||||||
|
|
||||||
// 6-bit quantized LUT for Redmean-based 256-color mapping
|
// 6-bit quantized LUT for Redmean-based 256-color mapping
|
||||||
// 64×64×64 = 262,144 bytes, fits in L2 cache
|
// 64×64×64 = 262,144 bytes, fits in L2 cache
|
||||||
var lut256 [64 * 64 * 64]uint8
|
const lut256Size = 64 * 64 * 64
|
||||||
|
|
||||||
func init() {
|
// init() -> first-use build. Construction is ~63M Redmean evaluations
|
||||||
// Pre-compute Redmean-based palette mapping for all 6-bit quantized RGB values
|
// and 256 KiB resident; truecolor sessions never read the table.
|
||||||
for r := 0; r < 64; r++ {
|
var (
|
||||||
for g := 0; g < 64; g++ {
|
lut256Ptr atomic.Pointer[[lut256Size]uint8]
|
||||||
for b := 0; b < 64; b++ {
|
lut256Once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
// Expand 6-bit to 8-bit (shift left 2, add 2 for midpoint)
|
||||||
r8 := (r << 2) | 2
|
c := color.RGB{
|
||||||
g8 := (g << 2) | 2
|
R: uint8(r<<2 | 2),
|
||||||
b8 := (b << 2) | 2
|
G: uint8(g<<2 | 2),
|
||||||
lut256[r<<12|g<<6|b] = computeRedmean256(r8, g8, b8)
|
B: uint8(b<<2 | 2),
|
||||||
|
}
|
||||||
|
t[r<<12|g<<6|b] = computeRedmean256(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lut256Ptr.Store(t)
|
||||||
|
})
|
||||||
|
return lut256Ptr.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// computeRedmean256 finds the nearest 256-palette index using Redmean distance
|
// WarmPalette256 forces LUT construction. Idempotent, safe for concurrent use.
|
||||||
// Called only at init() to populate LUT
|
// Call before the first render when ColorMode is ColorMode256.
|
||||||
func computeRedmean256(r, g, b int) uint8 {
|
// RGBTo256 runs on the render path under the terminal mutex; a lazy build there stalls the frame.
|
||||||
|
func WarmPalette256() { _ = lut256() }
|
||||||
|
|
||||||
|
// computeRedmean256 finds the nearest 256-palette index using Redmean distance called from lut256Build, not init()
|
||||||
|
func computeRedmean256(c color.RGB) uint8 {
|
||||||
// Grayscale fast path
|
// Grayscale fast path
|
||||||
if r == g && g == b {
|
if c.R == c.G && c.G == c.B {
|
||||||
if r < 8 {
|
if c.R < 8 {
|
||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
if r > 238 {
|
if c.R > 238 {
|
||||||
return 231
|
return 231
|
||||||
}
|
}
|
||||||
return uint8(232 + (r-8)/10)
|
return uint8(232 + (int(c.R)-8)/10)
|
||||||
}
|
}
|
||||||
|
|
||||||
bestIdx := uint8(16)
|
bestIdx := uint8(16)
|
||||||
minDist := 1 << 30
|
minDist := 1 << 30
|
||||||
|
|
||||||
// Search 6×6×6 cube (indices 16-231)
|
// Search 6×6×6 cube (indices 16-231)
|
||||||
for i := 0; i < 216; i++ {
|
for i := range 216 {
|
||||||
cr := cubeValues[i/36]
|
cand := color.RGB{
|
||||||
cg := cubeValues[(i/6)%6]
|
R: cubeValues[i/36],
|
||||||
cb := cubeValues[i%6]
|
G: cubeValues[(i/6)%6],
|
||||||
|
B: cubeValues[i%6],
|
||||||
d := redmeanDistance(r, g, b, cr, cg, cb)
|
}
|
||||||
if d < minDist {
|
if d := color.RedmeanDistance(c, cand); d < minDist {
|
||||||
minDist = d
|
minDist = d
|
||||||
bestIdx = uint8(16 + i)
|
bestIdx = uint8(16 + i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search grayscale ramp (indices 232-255)
|
// Search grayscale ramp (indices 232-255)
|
||||||
for i := 0; i < 24; i++ {
|
for i := range 24 {
|
||||||
gray := 8 + i*10
|
g := uint8(8 + i*10)
|
||||||
d := redmeanDistance(r, g, b, gray, gray, gray)
|
if d := color.RedmeanDistance(c, color.RGB{R: g, G: g, B: g}); d < minDist {
|
||||||
if d < minDist {
|
|
||||||
minDist = d
|
minDist = d
|
||||||
bestIdx = uint8(232 + i)
|
bestIdx = uint8(232 + i)
|
||||||
}
|
}
|
||||||
@@ -95,21 +106,11 @@ func computeRedmean256(r, g, b int) uint8 {
|
|||||||
return bestIdx
|
return bestIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
// redmeanDistance calculates perceptually-weighted color distance
|
|
||||||
// Formula: https://en.wikipedia.org/wiki/Color_difference#sRGB
|
|
||||||
func redmeanDistance(r1, g1, b1, r2, g2, b2 int) int {
|
|
||||||
rmean := (r1 + r2) / 2
|
|
||||||
dr := r1 - r2
|
|
||||||
dg := g1 - g2
|
|
||||||
db := b1 - b2
|
|
||||||
return (((512 + rmean) * dr * dr) >> 8) + 4*dg*dg + (((767 - rmean) * db * db) >> 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color cube values for 6×6×6 palette (indices 16-231)
|
// Color cube values for 6×6×6 palette (indices 16-231)
|
||||||
var cubeValues = [6]int{0, 95, 135, 175, 215, 255}
|
var cubeValues = [6]uint8{0, 95, 135, 175, 215, 255}
|
||||||
|
|
||||||
// RGBTo256 converts RGB to nearest 256-color palette index
|
// RGBTo256 converts RGB to nearest 256-color palette index.
|
||||||
// O(1) lookup via pre-computed Redmean LUT
|
// O(1) via the Redmean LUT; the first call builds it (see WarmPalette256).
|
||||||
func RGBTo256(c RGB) uint8 {
|
func RGBTo256(c color.RGB) uint8 {
|
||||||
return lut256[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)]
|
return lut256()[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
"github.com/lixenwraith/terminal/inline"
|
"github.com/lixenwraith/terminal/inline"
|
||||||
)
|
)
|
||||||
@@ -12,9 +13,9 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
p := inline.New(os.Stdout)
|
p := inline.New(os.Stdout)
|
||||||
|
|
||||||
name := inline.Fg(terminal.LightSkyBlue).Attr(terminal.AttrBold)
|
name := inline.Fg(color.LightSkyBlue).Attr(terminal.AttrBold)
|
||||||
okSt := inline.Fg(terminal.LimeGreen).Attr(terminal.AttrBold)
|
okSt := inline.Fg(color.LimeGreen).Attr(terminal.AttrBold)
|
||||||
dim := inline.Fg(terminal.IronGray)
|
dim := inline.Fg(color.IronGray)
|
||||||
|
|
||||||
pkgs := []string{"openssl", "zlib", "curl", "git", "go"}
|
pkgs := []string{"openssl", "zlib", "curl", "git", "go"}
|
||||||
frame := 0
|
frame := 0
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
module github.com/lixenwraith/terminal
|
module github.com/lixenwraith/terminal
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/lixenwraith/color v0.0.0-20260714170240-79433f872c42
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.45.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ func New(w io.Writer) *Printer {
|
|||||||
}
|
}
|
||||||
p.color = p.tty != nil && os.Getenv("NO_COLOR") == ""
|
p.color = p.tty != nil && os.Getenv("NO_COLOR") == ""
|
||||||
p.mode = terminal.DetectColorMode()
|
p.mode = terminal.DetectColorMode()
|
||||||
|
// Keep the LUT build out of the first Paint call
|
||||||
|
if p.color && p.mode == terminal.ColorMode256 {
|
||||||
|
terminal.WarmPalette256()
|
||||||
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -7,22 +7,23 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Style describes text appearance; zero value is unstyled.
|
// Style describes text appearance; zero value is unstyled.
|
||||||
// Composable: inline.Fg(terminal.Amber).Attr(terminal.AttrBold)
|
// Composable: inline.Fg(terminal.Amber).Attr(terminal.AttrBold)
|
||||||
type Style struct {
|
type Style struct {
|
||||||
fg, bg terminal.RGB
|
fg, bg color.RGB
|
||||||
hasFg, hasBg bool
|
hasFg, hasBg bool
|
||||||
attr terminal.Attr
|
attr terminal.Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fg starts a style with foreground color
|
// Fg starts a style with foreground color
|
||||||
func Fg(c terminal.RGB) Style { return Style{fg: c, hasFg: true} }
|
func Fg(c color.RGB) Style { return Style{fg: c, hasFg: true} }
|
||||||
|
|
||||||
// Bg sets background color
|
// Bg sets background color
|
||||||
func (s Style) Bg(c terminal.RGB) Style { s.bg, s.hasBg = c, true; return s }
|
func (s Style) Bg(c color.RGB) Style { s.bg, s.hasBg = c, true; return s }
|
||||||
|
|
||||||
// Attr adds attribute bits
|
// Attr adds attribute bits
|
||||||
func (s Style) Attr(a terminal.Attr) Style { s.attr |= a; return s }
|
func (s Style) Attr(a terminal.Attr) Style { s.attr |= a; return s }
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package terminal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
// outputBuffer manages double-buffered terminal output with diffing
|
// outputBuffer manages double-buffered terminal output with diffing
|
||||||
@@ -17,8 +19,8 @@ type outputBuffer struct {
|
|||||||
cursorValid bool
|
cursorValid bool
|
||||||
|
|
||||||
// Style state for coalescing
|
// Style state for coalescing
|
||||||
lastFg RGB
|
lastFg color.RGB
|
||||||
lastBg RGB
|
lastBg color.RGB
|
||||||
lastAttr Attr
|
lastAttr Attr
|
||||||
lastValid bool
|
lastValid bool
|
||||||
}
|
}
|
||||||
@@ -251,7 +253,7 @@ func (o *outputBuffer) moveCursorTo(w *bufio.Writer, x, y int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeStyleCoalesced emits a single combined SGR sequence when style changes
|
// writeStyleCoalesced emits a single combined SGR sequence when style changes
|
||||||
func (o *outputBuffer) writeStyleCoalesced(w *bufio.Writer, fg, bg RGB, attr Attr) {
|
func (o *outputBuffer) writeStyleCoalesced(w *bufio.Writer, fg, bg color.RGB, attr Attr) {
|
||||||
// Check what changed
|
// Check what changed
|
||||||
fgChanged := !o.lastValid || fg != o.lastFg || (attr&AttrFg256) != (o.lastAttr&AttrFg256)
|
fgChanged := !o.lastValid || fg != o.lastFg || (attr&AttrFg256) != (o.lastAttr&AttrFg256)
|
||||||
bgChanged := !o.lastValid || bg != o.lastBg || (attr&AttrBg256) != (o.lastAttr&AttrBg256)
|
bgChanged := !o.lastValid || bg != o.lastBg || (attr&AttrBg256) != (o.lastAttr&AttrBg256)
|
||||||
@@ -340,7 +342,7 @@ func (o *outputBuffer) writeStyleCoalesced(w *bufio.Writer, fg, bg RGB, attr Att
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeFgInline writes fg color parameters (no CSI prefix, no 'm' suffix)
|
// writeFgInline writes fg color parameters (no CSI prefix, no 'm' suffix)
|
||||||
func (o *outputBuffer) writeFgInline(w *bufio.Writer, fg RGB, attr Attr) {
|
func (o *outputBuffer) writeFgInline(w *bufio.Writer, fg color.RGB, attr Attr) {
|
||||||
w.WriteByte(';')
|
w.WriteByte(';')
|
||||||
if attr&AttrFg256 != 0 {
|
if attr&AttrFg256 != 0 {
|
||||||
// 256-color: 38;5;N
|
// 256-color: 38;5;N
|
||||||
@@ -362,7 +364,7 @@ func (o *outputBuffer) writeFgInline(w *bufio.Writer, fg RGB, attr Attr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeBgInline writes bg color parameters (no CSI prefix, no 'm' suffix)
|
// writeBgInline writes bg color parameters (no CSI prefix, no 'm' suffix)
|
||||||
func (o *outputBuffer) writeBgInline(w *bufio.Writer, bg RGB, attr Attr) {
|
func (o *outputBuffer) writeBgInline(w *bufio.Writer, bg color.RGB, attr Attr) {
|
||||||
w.WriteByte(';')
|
w.WriteByte(';')
|
||||||
if attr&AttrBg256 != 0 {
|
if attr&AttrBg256 != 0 {
|
||||||
// 256-color: 48;5;N
|
// 256-color: 48;5;N
|
||||||
@@ -384,7 +386,7 @@ func (o *outputBuffer) writeBgInline(w *bufio.Writer, bg RGB, attr Attr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeFgFull writes complete fg color sequence
|
// writeFgFull writes complete fg color sequence
|
||||||
func (o *outputBuffer) writeFgFull(w *bufio.Writer, fg RGB, attr Attr) {
|
func (o *outputBuffer) writeFgFull(w *bufio.Writer, fg color.RGB, attr Attr) {
|
||||||
if attr&AttrFg256 != 0 {
|
if attr&AttrFg256 != 0 {
|
||||||
w.Write(csiFg256)
|
w.Write(csiFg256)
|
||||||
writeInt(w, int(fg.R))
|
writeInt(w, int(fg.R))
|
||||||
@@ -405,7 +407,7 @@ func (o *outputBuffer) writeFgFull(w *bufio.Writer, fg RGB, attr Attr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeBgFull writes complete bg color sequence
|
// writeBgFull writes complete bg color sequence
|
||||||
func (o *outputBuffer) writeBgFull(w *bufio.Writer, bg RGB, attr Attr) {
|
func (o *outputBuffer) writeBgFull(w *bufio.Writer, bg color.RGB, attr Attr) {
|
||||||
if attr&AttrBg256 != 0 {
|
if attr&AttrBg256 != 0 {
|
||||||
w.Write(csiBg256)
|
w.Write(csiBg256)
|
||||||
writeInt(w, int(bg.R))
|
writeInt(w, int(bg.R))
|
||||||
@@ -435,7 +437,7 @@ func (o *outputBuffer) forceFullRedraw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clear writes a clear screen with specified background
|
// clear writes a clear screen with specified background
|
||||||
func (o *outputBuffer) clear(bg RGB) {
|
func (o *outputBuffer) clear(bg color.RGB) {
|
||||||
w := o.writer
|
w := o.writer
|
||||||
w.Write(csiSGR0)
|
w.Write(csiSGR0)
|
||||||
o.writeBgFull(w, bg, 0)
|
o.writeBgFull(w, bg, 0)
|
||||||
|
|||||||
@@ -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"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attr represents text attributes (bitmask)
|
// Attr represents text attributes (bitmask)
|
||||||
@@ -28,8 +30,8 @@ const AttrStyle Attr = AttrBold | AttrDim | AttrItalic | AttrUnderline | AttrBli
|
|||||||
// Cell represents a single terminal cell
|
// Cell represents a single terminal cell
|
||||||
type Cell struct {
|
type Cell struct {
|
||||||
Rune rune
|
Rune rune
|
||||||
Fg RGB
|
Fg color.RGB
|
||||||
Bg RGB
|
Bg color.RGB
|
||||||
Attrs Attr
|
Attrs Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ type Terminal interface {
|
|||||||
Flush(cells []Cell, width, height int)
|
Flush(cells []Cell, width, height int)
|
||||||
|
|
||||||
// Clear fills screen with specified background color
|
// Clear fills screen with specified background color
|
||||||
Clear(bg RGB)
|
Clear(bg color.RGB)
|
||||||
|
|
||||||
// SetCursorVisible shows/hides cursor
|
// SetCursorVisible shows/hides cursor
|
||||||
SetCursorVisible(visible bool)
|
SetCursorVisible(visible bool)
|
||||||
@@ -172,8 +174,13 @@ func (t *termImpl) Init() error {
|
|||||||
// Invisible cursor
|
// Invisible cursor
|
||||||
t.cursorVisible.Store(false)
|
t.cursorVisible.Store(false)
|
||||||
|
|
||||||
|
// Build the LUT at startup, not on the first frame under t.mu
|
||||||
|
if t.output.colorMode == ColorMode256 {
|
||||||
|
WarmPalette256()
|
||||||
|
}
|
||||||
|
|
||||||
// Clear screen
|
// Clear screen
|
||||||
t.output.clear(RGBBlack)
|
t.output.clear(color.Black)
|
||||||
|
|
||||||
// Start input reader
|
// Start input reader
|
||||||
t.input.start()
|
t.input.start()
|
||||||
@@ -259,7 +266,7 @@ func (t *termImpl) Flush(cells []Cell, width, height int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear fills screen with background color
|
// Clear fills screen with background color
|
||||||
func (t *termImpl) Clear(bg RGB) {
|
func (t *termImpl) Clear(bg color.RGB) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
@@ -336,7 +343,7 @@ func (t *termImpl) Sync() {
|
|||||||
|
|
||||||
// Clear terminal before full redraw
|
// Clear terminal before full redraw
|
||||||
// Diff-based rendering assumes physical terminal matches front buffer state
|
// Diff-based rendering assumes physical terminal matches front buffer state
|
||||||
t.output.clear(RGBBlack)
|
t.output.clear(color.Black)
|
||||||
t.output.forceFullRedraw()
|
t.output.forceFullRedraw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -200,7 +200,7 @@ Pure logic, no rendering — usable independently:
|
|||||||
|
|
||||||
- Width calculations count runes, not terminal columns; East Asian wide
|
- Width calculations count runes, not terminal columns; East Asian wide
|
||||||
characters and combining marks are not width-aware.
|
characters and combining marks are not width-aware.
|
||||||
- Zero-value `terminal.RGB` in style fields generally means "inherit"
|
- Zero-value `color.RGB` in style fields generally means "inherit"
|
||||||
(widget default or row background) — check specific widget docs.
|
(widget default or row background) — check specific widget docs.
|
||||||
- Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require
|
- Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require
|
||||||
application-side geometry from the regions used.
|
application-side geometry from the regions used.
|
||||||
|
|||||||
+14
-12
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ const (
|
|||||||
// --- Box Rendering ---
|
// --- Box Rendering ---
|
||||||
|
|
||||||
// Box draws border around region edge
|
// Box draws border around region edge
|
||||||
func (r Region) Box(line LineType, fg terminal.RGB) {
|
func (r Region) Box(line LineType, fg color.RGB) {
|
||||||
if r.W < 2 || r.H < 2 {
|
if r.W < 2 || r.H < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chars := boxChars[line]
|
chars := boxChars[line]
|
||||||
bg := terminal.RGB{} // Transparent (use existing bg)
|
bg := color.RGB{} // Transparent (use existing bg)
|
||||||
|
|
||||||
// Corners
|
// Corners
|
||||||
r.Cell(0, 0, chars[boxTL], fg, bg, terminal.AttrNone)
|
r.Cell(0, 0, chars[boxTL], fg, bg, terminal.AttrNone)
|
||||||
@@ -67,7 +68,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BoxFilled draws border and fills interior with background
|
// BoxFilled draws border and fills interior with background
|
||||||
func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) {
|
func (r Region) BoxFilled(line LineType, fg, bg color.RGB) {
|
||||||
// Fill interior first
|
// Fill interior first
|
||||||
for y := 1; y < r.H-1; y++ {
|
for y := 1; y < r.H-1; y++ {
|
||||||
for x := 1; x < r.W-1; x++ {
|
for x := 1; x < r.W-1; x++ {
|
||||||
@@ -81,7 +82,7 @@ func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) {
|
|||||||
// --- Line rendering ---
|
// --- Line rendering ---
|
||||||
|
|
||||||
// HLine draws horizontal line across region width at row y
|
// HLine draws horizontal line across region width at row y
|
||||||
func (r Region) HLine(y int, line LineType, fg terminal.RGB) {
|
func (r Region) HLine(y int, line LineType, fg color.RGB) {
|
||||||
if y < 0 || y >= r.H {
|
if y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,12 +91,12 @@ func (r Region) HLine(y int, line LineType, fg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
ch := boxChars[line][boxH]
|
ch := boxChars[line][boxH]
|
||||||
for x := 0; x < r.W; x++ {
|
for x := 0; x < r.W; x++ {
|
||||||
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VLine draws vertical line across region height at column x
|
// VLine draws vertical line across region height at column x
|
||||||
func (r Region) VLine(x int, line LineType, fg terminal.RGB) {
|
func (r Region) VLine(x int, line LineType, fg color.RGB) {
|
||||||
if x < 0 || x >= r.W {
|
if x < 0 || x >= r.W {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -104,12 +105,12 @@ func (r Region) VLine(x int, line LineType, fg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
ch := boxChars[line][boxV]
|
ch := boxChars[line][boxV]
|
||||||
for y := 0; y < r.H; y++ {
|
for y := 0; y < r.H; y++ {
|
||||||
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divider draws horizontal line with optional centered label
|
// Divider draws horizontal line with optional centered label
|
||||||
func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
|
func (r Region) Divider(y int, label string, line LineType, fg color.RGB) {
|
||||||
if y < 0 || y >= r.H {
|
if y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
|
|||||||
|
|
||||||
// Fill with horizontal line
|
// Fill with horizontal line
|
||||||
for x := 0; x < r.W; x++ {
|
for x := 0; x < r.W; x++ {
|
||||||
r.Cell(x, y, hChar, fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x, y, hChar, fg, color.RGB{}, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center label if provided
|
// Center label if provided
|
||||||
@@ -134,7 +135,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
startX := (r.W - textLen) / 2
|
startX := (r.W - textLen) / 2
|
||||||
for i, ch := range text {
|
for i, ch := range text {
|
||||||
r.Cell(startX+i, y, ch, fg, terminal.RGB{}, terminal.AttrBold)
|
r.Cell(startX+i, y, ch, fg, color.RGB{}, terminal.AttrBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +143,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
|
|||||||
// --- Card rendering ---
|
// --- Card rendering ---
|
||||||
|
|
||||||
// Card draws titled border and returns inner content region
|
// Card draws titled border and returns inner content region
|
||||||
func (r Region) Card(title string, line LineType, fg terminal.RGB) Region {
|
func (r Region) Card(title string, line LineType, fg color.RGB) Region {
|
||||||
r.Box(line, fg)
|
r.Box(line, fg)
|
||||||
|
|
||||||
if title != "" && r.W > 4 {
|
if title != "" && r.W > 4 {
|
||||||
@@ -152,8 +153,9 @@ func (r Region) Card(title string, line LineType, fg terminal.RGB) Region {
|
|||||||
displayTitle = Truncate(displayTitle, maxTitleLen)
|
displayTitle = Truncate(displayTitle, maxTitleLen)
|
||||||
}
|
}
|
||||||
titleX := (r.W - RuneLen(displayTitle) - 2) / 2
|
titleX := (r.W - RuneLen(displayTitle) - 2) / 2
|
||||||
r.Text(titleX, 0, " "+displayTitle+" ", fg, terminal.RGB{}, terminal.AttrBold)
|
r.Text(titleX, 0, " "+displayTitle+" ", fg, color.RGB{}, terminal.AttrBold)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Inset(1)
|
return r.Inset(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-14
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// Button defines a single button in a button bar
|
// Button defines a single button in a button bar
|
||||||
type Button struct {
|
type Button struct {
|
||||||
@@ -18,27 +21,27 @@ type ButtonBarOpts struct {
|
|||||||
|
|
||||||
// ButtonStyle defines button bar colors
|
// ButtonStyle defines button bar colors
|
||||||
type ButtonStyle struct {
|
type ButtonStyle struct {
|
||||||
LabelFg terminal.RGB
|
LabelFg color.RGB
|
||||||
LabelBg terminal.RGB
|
LabelBg color.RGB
|
||||||
KeyFg terminal.RGB
|
KeyFg color.RGB
|
||||||
FocusFg terminal.RGB
|
FocusFg color.RGB
|
||||||
FocusBg terminal.RGB
|
FocusBg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultButtonStyle returns default button colors with dark background
|
// DefaultButtonStyle returns default button colors with dark background
|
||||||
func DefaultButtonStyle() ButtonStyle {
|
func DefaultButtonStyle() ButtonStyle {
|
||||||
return DefaultButtonStyleFrom(terminal.RGB{R: 25, G: 25, B: 35})
|
return DefaultButtonStyleFrom(color.RGB{R: 25, G: 25, B: 35})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultButtonStyleFrom returns default button colors using the given background
|
// DefaultButtonStyleFrom returns default button colors using the given background
|
||||||
func DefaultButtonStyleFrom(bg terminal.RGB) ButtonStyle {
|
func DefaultButtonStyleFrom(bg color.RGB) ButtonStyle {
|
||||||
return ButtonStyle{
|
return ButtonStyle{
|
||||||
LabelFg: terminal.RGB{R: 200, G: 200, B: 200},
|
LabelFg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
LabelBg: terminal.RGB{R: 50, G: 50, B: 60},
|
LabelBg: color.RGB{R: 50, G: 50, B: 60},
|
||||||
KeyFg: terminal.RGB{R: 130, G: 130, B: 150},
|
KeyFg: color.RGB{R: 130, G: 130, B: 150},
|
||||||
FocusFg: terminal.RGB{R: 255, G: 255, B: 255},
|
FocusFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
FocusBg: terminal.RGB{R: 60, G: 80, B: 120},
|
FocusBg: color.RGB{R: 60, G: 80, B: 120},
|
||||||
Bg: bg,
|
Bg: bg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,3 +128,4 @@ func (r Region) ButtonBar(y int, buttons []Button, opts ButtonBarOpts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Checkbox draws a checkbox indicator
|
// Checkbox draws a checkbox indicator
|
||||||
func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) {
|
func (r Region) Checkbox(x, y int, state CheckState, fg color.RGB) {
|
||||||
if x < 0 || x+2 >= r.W || y < 0 || y >= r.H {
|
if x < 0 || x+2 >= r.W || y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,8 @@ func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) {
|
|||||||
case CheckPlus:
|
case CheckPlus:
|
||||||
ch = '+'
|
ch = '+'
|
||||||
}
|
}
|
||||||
r.Cell(x, y, '[', fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x, y, '[', fg, color.RGB{}, terminal.AttrNone)
|
||||||
r.Cell(x+1, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x+1, y, ch, fg, color.RGB{}, terminal.AttrNone)
|
||||||
r.Cell(x+2, y, ']', fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x+2, y, ']', fg, color.RGB{}, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-21
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// ConfirmResult represents dialog outcome
|
// ConfirmResult represents dialog outcome
|
||||||
type ConfirmResult uint8
|
type ConfirmResult uint8
|
||||||
@@ -98,31 +101,31 @@ type ConfirmOpts struct {
|
|||||||
|
|
||||||
// ConfirmStyle defines dialog colors
|
// ConfirmStyle defines dialog colors
|
||||||
type ConfirmStyle struct {
|
type ConfirmStyle struct {
|
||||||
BorderFg terminal.RGB
|
BorderFg color.RGB
|
||||||
TitleFg terminal.RGB
|
TitleFg color.RGB
|
||||||
MessageFg terminal.RGB
|
MessageFg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
ButtonFg terminal.RGB
|
ButtonFg color.RGB
|
||||||
ButtonBg terminal.RGB
|
ButtonBg color.RGB
|
||||||
ButtonFocusFg terminal.RGB
|
ButtonFocusFg color.RGB
|
||||||
ButtonFocusBg terminal.RGB
|
ButtonFocusBg color.RGB
|
||||||
DestructiveFg terminal.RGB
|
DestructiveFg color.RGB
|
||||||
DestructiveBg terminal.RGB
|
DestructiveBg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfirmStyle returns default dialog colors
|
// DefaultConfirmStyle returns default dialog colors
|
||||||
func DefaultConfirmStyle() ConfirmStyle {
|
func DefaultConfirmStyle() ConfirmStyle {
|
||||||
return ConfirmStyle{
|
return ConfirmStyle{
|
||||||
BorderFg: terminal.RGB{R: 100, G: 100, B: 120},
|
BorderFg: color.RGB{R: 100, G: 100, B: 120},
|
||||||
TitleFg: terminal.RGB{R: 255, G: 255, B: 255},
|
TitleFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
MessageFg: terminal.RGB{R: 200, G: 200, B: 200},
|
MessageFg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
Bg: terminal.RGB{R: 30, G: 30, B: 40},
|
Bg: color.RGB{R: 30, G: 30, B: 40},
|
||||||
ButtonFg: terminal.RGB{R: 180, G: 180, B: 180},
|
ButtonFg: color.RGB{R: 180, G: 180, B: 180},
|
||||||
ButtonBg: terminal.RGB{R: 50, G: 50, B: 60},
|
ButtonBg: color.RGB{R: 50, G: 50, B: 60},
|
||||||
ButtonFocusFg: terminal.RGB{R: 255, G: 255, B: 255},
|
ButtonFocusFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
ButtonFocusBg: terminal.RGB{R: 60, G: 80, B: 120},
|
ButtonFocusBg: color.RGB{R: 60, G: 80, B: 120},
|
||||||
DestructiveFg: terminal.RGB{R: 255, G: 255, B: 255},
|
DestructiveFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
DestructiveBg: terminal.RGB{R: 180, G: 60, B: 60},
|
DestructiveBg: color.RGB{R: 180, G: 60, B: 60},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,3 +325,4 @@ func (r Region) AlertDialog(opts AlertOpts) Region {
|
|||||||
|
|
||||||
return content.Sub(0, 0, content.W, buttonY-1)
|
return content.Sub(0, 0, content.W, buttonY-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,27 +17,27 @@ type EditorOpts struct {
|
|||||||
|
|
||||||
// EditorStyle defines editor colors
|
// EditorStyle defines editor colors
|
||||||
type EditorStyle struct {
|
type EditorStyle struct {
|
||||||
TextFg terminal.RGB
|
TextFg color.RGB
|
||||||
TextBg terminal.RGB
|
TextBg color.RGB
|
||||||
CursorFg terminal.RGB
|
CursorFg color.RGB
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
LineNumFg terminal.RGB
|
LineNumFg color.RGB
|
||||||
LineNumBg terminal.RGB
|
LineNumBg color.RGB
|
||||||
CurrentLineBg terminal.RGB
|
CurrentLineBg color.RGB
|
||||||
BorderFg terminal.RGB
|
BorderFg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultEditorStyle returns default colors
|
// DefaultEditorStyle returns default colors
|
||||||
func DefaultEditorStyle() EditorStyle {
|
func DefaultEditorStyle() EditorStyle {
|
||||||
return EditorStyle{
|
return EditorStyle{
|
||||||
TextFg: terminal.RGB{R: 220, G: 220, B: 220},
|
TextFg: color.RGB{R: 220, G: 220, B: 220},
|
||||||
TextBg: terminal.RGB{R: 25, G: 25, B: 35},
|
TextBg: color.RGB{R: 25, G: 25, B: 35},
|
||||||
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
|
CursorFg: color.RGB{R: 0, G: 0, B: 0},
|
||||||
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
|
CursorBg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
LineNumFg: terminal.RGB{R: 100, G: 100, B: 120},
|
LineNumFg: color.RGB{R: 100, G: 100, B: 120},
|
||||||
LineNumBg: terminal.RGB{R: 30, G: 30, B: 40},
|
LineNumBg: color.RGB{R: 30, G: 30, B: 40},
|
||||||
CurrentLineBg: terminal.RGB{R: 35, G: 35, B: 50},
|
CurrentLineBg: color.RGB{R: 35, G: 35, B: 50},
|
||||||
BorderFg: terminal.RGB{R: 80, G: 80, B: 100},
|
BorderFg: color.RGB{R: 80, G: 80, B: 100},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,3 +196,4 @@ func formatLineNum(num, width int) string {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-15
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// FormField pairs a label with an editable text field
|
// FormField pairs a label with an editable text field
|
||||||
type FormField struct {
|
type FormField struct {
|
||||||
@@ -103,25 +106,25 @@ type FormOpts struct {
|
|||||||
|
|
||||||
// FormStyle defines form colors
|
// FormStyle defines form colors
|
||||||
type FormStyle struct {
|
type FormStyle struct {
|
||||||
LabelFg terminal.RGB
|
LabelFg color.RGB
|
||||||
FieldFg terminal.RGB
|
FieldFg color.RGB
|
||||||
FieldBg terminal.RGB
|
FieldBg color.RGB
|
||||||
FocusBg terminal.RGB
|
FocusBg color.RGB
|
||||||
CursorFg terminal.RGB
|
CursorFg color.RGB
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultFormStyle returns default form colors
|
// DefaultFormStyle returns default form colors
|
||||||
func DefaultFormStyle() FormStyle {
|
func DefaultFormStyle() FormStyle {
|
||||||
return FormStyle{
|
return FormStyle{
|
||||||
LabelFg: terminal.RGB{R: 150, G: 150, B: 180},
|
LabelFg: color.RGB{R: 150, G: 150, B: 180},
|
||||||
FieldFg: terminal.RGB{R: 220, G: 220, B: 220},
|
FieldFg: color.RGB{R: 220, G: 220, B: 220},
|
||||||
FieldBg: terminal.RGB{R: 35, G: 35, B: 45},
|
FieldBg: color.RGB{R: 35, G: 35, B: 45},
|
||||||
FocusBg: terminal.RGB{R: 45, G: 45, B: 60},
|
FocusBg: color.RGB{R: 45, G: 45, B: 60},
|
||||||
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
|
CursorFg: color.RGB{R: 0, G: 0, B: 0},
|
||||||
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
|
CursorBg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
Bg: terminal.RGB{R: 25, G: 25, B: 35},
|
Bg: color.RGB{R: 25, G: 25, B: 35},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,3 +229,4 @@ func (r Region) Form(state *FormState, opts FormOpts) int {
|
|||||||
|
|
||||||
return y
|
return y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -1,18 +1,19 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputOpts configures single-line input field
|
// InputOpts configures single-line input field
|
||||||
type InputOpts struct {
|
type InputOpts struct {
|
||||||
Label string
|
Label string
|
||||||
LabelFg terminal.RGB
|
LabelFg color.RGB
|
||||||
Text string
|
Text string
|
||||||
Cursor int // Cursor position in text (rune index)
|
Cursor int // Cursor position in text (rune index)
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
TextFg terminal.RGB
|
TextFg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input renders labeled text input field on row y, handling cursor display and horizontal scrolling
|
// Input renders labeled text input field on row y, handling cursor display and horizontal scrolling
|
||||||
@@ -76,3 +77,4 @@ func (r Region) Input(y int, opts InputOpts) {
|
|||||||
r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone)
|
r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -1,22 +1,25 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// ListItem represents a single row in a scrollable list
|
// ListItem represents a single row in a scrollable list
|
||||||
type ListItem struct {
|
type ListItem struct {
|
||||||
Indent int // Left padding in cells
|
Indent int // Left padding in cells
|
||||||
Icon rune // Expand indicator or bullet, 0 = none
|
Icon rune // Expand indicator or bullet, 0 = none
|
||||||
IconFg terminal.RGB
|
IconFg color.RGB
|
||||||
Check CheckState // CheckNone to skip checkbox
|
Check CheckState // CheckNone to skip checkbox
|
||||||
CheckFg terminal.RGB
|
CheckFg color.RGB
|
||||||
Text string
|
Text string
|
||||||
TextStyle Style
|
TextStyle Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListOpts configures list rendering
|
// ListOpts configures list rendering
|
||||||
type ListOpts struct {
|
type ListOpts struct {
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
DefaultBg terminal.RGB
|
DefaultBg color.RGB
|
||||||
IconWidth int // Width reserved for icon, default 2
|
IconWidth int // Width reserved for icon, default 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
|
|||||||
|
|
||||||
// Clear row
|
// Clear row
|
||||||
for x := 0; x < r.W; x++ {
|
for x := 0; x < r.W; x++ {
|
||||||
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone)
|
r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
x := item.Indent
|
x := item.Indent
|
||||||
@@ -61,7 +64,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
|
|||||||
x += iconW
|
x += iconW
|
||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
if item.Check != CheckNone || item.CheckFg != (terminal.RGB{}) {
|
if item.Check != CheckNone || item.CheckFg != (color.RGB{}) {
|
||||||
if x+3 <= r.W {
|
if x+3 <= r.W {
|
||||||
var ch rune
|
var ch rune
|
||||||
switch item.Check {
|
switch item.Check {
|
||||||
@@ -83,7 +86,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
|
|||||||
|
|
||||||
// Text
|
// Text
|
||||||
textStyle := item.TextStyle
|
textStyle := item.TextStyle
|
||||||
if textStyle.Bg == (terminal.RGB{}) {
|
if textStyle.Bg == (color.RGB{}) {
|
||||||
textStyle.Bg = bg
|
textStyle.Bg = bg
|
||||||
}
|
}
|
||||||
text := item.Text
|
text := item.Text
|
||||||
@@ -97,3 +100,4 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
|
|||||||
|
|
||||||
return rendered
|
return rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,10 +10,10 @@ type ModalOpts struct {
|
|||||||
Title string
|
Title string
|
||||||
Hint string // Right-aligned hint text
|
Hint string // Right-aligned hint text
|
||||||
Border LineType
|
Border LineType
|
||||||
BorderFg terminal.RGB
|
BorderFg color.RGB
|
||||||
TitleFg terminal.RGB
|
TitleFg color.RGB
|
||||||
HintFg terminal.RGB
|
HintFg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal fills region with background, draws border with title/hint, returns content region
|
// Modal fills region with background, draws border with title/hint, returns content region
|
||||||
@@ -64,3 +65,4 @@ func (r Region) Modal(opts ModalOpts) Region {
|
|||||||
// Return content region
|
// Return content region
|
||||||
return r.Sub(1, 1, r.W-2, r.H-2)
|
return r.Sub(1, 1, r.W-2, r.H-2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-23
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// OverlayStyle specifies overlay appearance
|
// OverlayStyle specifies overlay appearance
|
||||||
type OverlayStyle uint8
|
type OverlayStyle uint8
|
||||||
@@ -17,10 +20,10 @@ type OverlayOpts struct {
|
|||||||
Style OverlayStyle
|
Style OverlayStyle
|
||||||
Title string
|
Title string
|
||||||
Border LineType
|
Border LineType
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
Fg terminal.RGB // Border and title color
|
Fg color.RGB // Border and title color
|
||||||
TitleBg terminal.RGB // Title bar background, zero = same as Fg
|
TitleBg color.RGB // Title bar background, zero = same as Fg
|
||||||
TitleFg terminal.RGB // Title text color, zero = same as Bg
|
TitleFg color.RGB // Title text color, zero = same as Bg
|
||||||
|
|
||||||
// Modal/Floating positioning (ignored for Fullscreen)
|
// Modal/Floating positioning (ignored for Fullscreen)
|
||||||
Width int // 0 = 80% of region
|
Width int // 0 = 80% of region
|
||||||
@@ -28,7 +31,7 @@ type OverlayOpts struct {
|
|||||||
X, Y int // Offset from center, 0 = centered
|
X, Y int // Offset from center, 0 = centered
|
||||||
|
|
||||||
// Shadow for Floating style
|
// Shadow for Floating style
|
||||||
ShadowColor terminal.RGB
|
ShadowColor color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOverlayOpts returns sensible defaults for modal overlay
|
// DefaultOverlayOpts returns sensible defaults for modal overlay
|
||||||
@@ -37,10 +40,10 @@ func DefaultOverlayOpts(title string) OverlayOpts {
|
|||||||
Style: OverlayModal,
|
Style: OverlayModal,
|
||||||
Title: title,
|
Title: title,
|
||||||
Border: LineDouble,
|
Border: LineDouble,
|
||||||
Bg: terminal.RGB{R: 25, G: 25, B: 35},
|
Bg: color.RGB{R: 25, G: 25, B: 35},
|
||||||
Fg: terminal.RGB{R: 100, G: 140, B: 180},
|
Fg: color.RGB{R: 100, G: 140, B: 180},
|
||||||
TitleBg: terminal.RGB{R: 40, G: 60, B: 90},
|
TitleBg: color.RGB{R: 40, G: 60, B: 90},
|
||||||
TitleFg: terminal.RGB{R: 255, G: 255, B: 255},
|
TitleFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +52,10 @@ func FullscreenOverlayOpts(title string) OverlayOpts {
|
|||||||
return OverlayOpts{
|
return OverlayOpts{
|
||||||
Style: OverlayFullscreen,
|
Style: OverlayFullscreen,
|
||||||
Title: title,
|
Title: title,
|
||||||
Bg: terminal.RGB{R: 20, G: 20, B: 30},
|
Bg: color.RGB{R: 20, G: 20, B: 30},
|
||||||
Fg: terminal.RGB{R: 100, G: 140, B: 180},
|
Fg: color.RGB{R: 100, G: 140, B: 180},
|
||||||
TitleBg: terminal.RGB{R: 40, G: 60, B: 90},
|
TitleBg: color.RGB{R: 40, G: 60, B: 90},
|
||||||
TitleFg: terminal.RGB{R: 255, G: 255, B: 255},
|
TitleFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +101,11 @@ func (r Region) renderFullscreenOverlay(opts OverlayOpts) OverlayResult {
|
|||||||
// Title bar
|
// Title bar
|
||||||
if opts.Title != "" {
|
if opts.Title != "" {
|
||||||
titleBg := opts.TitleBg
|
titleBg := opts.TitleBg
|
||||||
if titleBg == (terminal.RGB{}) {
|
if titleBg == (color.RGB{}) {
|
||||||
titleBg = opts.Fg
|
titleBg = opts.Fg
|
||||||
}
|
}
|
||||||
titleFg := opts.TitleFg
|
titleFg := opts.TitleFg
|
||||||
if titleFg == (terminal.RGB{}) {
|
if titleFg == (color.RGB{}) {
|
||||||
titleFg = opts.Bg
|
titleFg = opts.Bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,11 +191,11 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult {
|
|||||||
// Title in top border
|
// Title in top border
|
||||||
if opts.Title != "" && contentW > 2 {
|
if opts.Title != "" && contentW > 2 {
|
||||||
titleBg := opts.TitleBg
|
titleBg := opts.TitleBg
|
||||||
if titleBg == (terminal.RGB{}) {
|
if titleBg == (color.RGB{}) {
|
||||||
titleBg = opts.Fg
|
titleBg = opts.Fg
|
||||||
}
|
}
|
||||||
titleFg := opts.TitleFg
|
titleFg := opts.TitleFg
|
||||||
if titleFg == (terminal.RGB{}) {
|
if titleFg == (color.RGB{}) {
|
||||||
titleFg = opts.Bg
|
titleFg = opts.Bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +227,8 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult {
|
|||||||
func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
|
func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
|
||||||
// Same as modal but with shadow
|
// Same as modal but with shadow
|
||||||
shadowColor := opts.ShadowColor
|
shadowColor := opts.ShadowColor
|
||||||
if shadowColor == (terminal.RGB{}) {
|
if shadowColor == (color.RGB{}) {
|
||||||
shadowColor = terminal.RGB{R: 10, G: 10, B: 15}
|
shadowColor = color.RGB{R: 10, G: 10, B: 15}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate dimensions (same as modal)
|
// Calculate dimensions (same as modal)
|
||||||
@@ -285,11 +288,11 @@ func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
|
|||||||
|
|
||||||
if opts.Title != "" && contentW > 2 {
|
if opts.Title != "" && contentW > 2 {
|
||||||
titleBg := opts.TitleBg
|
titleBg := opts.TitleBg
|
||||||
if titleBg == (terminal.RGB{}) {
|
if titleBg == (color.RGB{}) {
|
||||||
titleBg = opts.Fg
|
titleBg = opts.Fg
|
||||||
}
|
}
|
||||||
titleFg := opts.TitleFg
|
titleFg := opts.TitleFg
|
||||||
if titleFg == (terminal.RGB{}) {
|
if titleFg == (color.RGB{}) {
|
||||||
titleFg = opts.Bg
|
titleFg = opts.Bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +330,7 @@ func (r Region) renderBorderTitleOverlay(opts OverlayOpts) OverlayResult {
|
|||||||
|
|
||||||
if opts.Title != "" && r.W > 6 {
|
if opts.Title != "" && r.W > 6 {
|
||||||
titleFg := opts.TitleFg
|
titleFg := opts.TitleFg
|
||||||
if titleFg == (terminal.RGB{}) {
|
if titleFg == (color.RGB{}) {
|
||||||
titleFg = opts.Fg
|
titleFg = opts.Fg
|
||||||
}
|
}
|
||||||
title := " " + opts.Title + " "
|
title := " " + opts.Title + " "
|
||||||
@@ -371,3 +374,4 @@ func (o *OverlayState) Hide() {
|
|||||||
func (o *OverlayState) Toggle() {
|
func (o *OverlayState) Toggle() {
|
||||||
o.Visible = !o.Visible
|
o.Visible = !o.Visible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -8,9 +9,9 @@ import (
|
|||||||
type PaneOpts struct {
|
type PaneOpts struct {
|
||||||
Title string
|
Title string
|
||||||
Border LineType
|
Border LineType
|
||||||
BorderFg terminal.RGB
|
BorderFg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
TitleFg terminal.RGB
|
TitleFg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pane draws bordered pane with optional title, returns content region
|
// Pane draws bordered pane with optional title, returns content region
|
||||||
@@ -49,7 +50,7 @@ func (r Region) Pane(opts PaneOpts) Region {
|
|||||||
|
|
||||||
// TitledPane fills region with background, draws centered title at top, returns content region
|
// TitledPane fills region with background, draws centered title at top, returns content region
|
||||||
// Content region starts at row 1 with full width
|
// Content region starts at row 1 with full width
|
||||||
func (r Region) TitledPane(title string, titleFg, bg terminal.RGB) Region {
|
func (r Region) TitledPane(title string, titleFg, bg color.RGB) Region {
|
||||||
r.Fill(bg)
|
r.Fill(bg)
|
||||||
if title != "" && r.H > 0 {
|
if title != "" && r.H > 0 {
|
||||||
r.TextCenter(0, title, titleFg, bg, terminal.AttrBold)
|
r.TextCenter(0, title, titleFg, bg, terminal.AttrBold)
|
||||||
@@ -61,9 +62,10 @@ func (r Region) TitledPane(title string, titleFg, bg terminal.RGB) Region {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TitledPaneFocused is TitledPane with focus-dependent background
|
// TitledPaneFocused is TitledPane with focus-dependent background
|
||||||
func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg terminal.RGB, focused bool) Region {
|
func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg color.RGB, focused bool) Region {
|
||||||
if focused {
|
if focused {
|
||||||
bg = focusBg
|
bg = focusBg
|
||||||
}
|
}
|
||||||
return r.TitledPane(title, titleFg, bg)
|
return r.TitledPane(title, titleFg, bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Progress draws horizontal progress bar (0.0-1.0)
|
// Progress draws horizontal progress bar (0.0-1.0)
|
||||||
func (r Region) Progress(x, y, w int, pct float64, fg, bg terminal.RGB) {
|
func (r Region) Progress(x, y, w int, pct float64, fg, bg color.RGB) {
|
||||||
if y < 0 || y >= r.H || w <= 0 {
|
if y < 0 || y >= r.H || w <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ func (r Region) Progress(x, y, w int, pct float64, fg, bg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ProgressV draws vertical progress bar (fills bottom-up)
|
// ProgressV draws vertical progress bar (fills bottom-up)
|
||||||
func (r Region) ProgressV(x, y, h int, pct float64, fg, bg terminal.RGB) {
|
func (r Region) ProgressV(x, y, h int, pct float64, fg, bg color.RGB) {
|
||||||
if x < 0 || x >= r.W || h <= 0 {
|
if x < 0 || x >= r.W || h <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,7 @@ func (r Region) ProgressV(x, y, h int, pct float64, fg, bg terminal.RGB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spinner draws spinner character based on frame counter
|
// Spinner draws spinner character based on frame counter
|
||||||
func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) {
|
func (r Region) Spinner(x, y int, frame int, fg color.RGB) {
|
||||||
if x < 0 || x >= r.W || y < 0 || y >= r.H {
|
if x < 0 || x >= r.W || y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -80,11 +81,11 @@ func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) {
|
|||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
idx = -idx
|
idx = -idx
|
||||||
}
|
}
|
||||||
r.Cell(x, y, spinnerFrames[idx], fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x, y, spinnerFrames[idx], fg, color.RGB{}, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gauge draws labeled gauge with percentage
|
// Gauge draws labeled gauge with percentage
|
||||||
func (r Region) Gauge(x, y, w int, value, max int, fg, bg terminal.RGB) {
|
func (r Region) Gauge(x, y, w int, value, max int, fg, bg color.RGB) {
|
||||||
if w < 5 || y < 0 || y >= r.H {
|
if w < 5 || y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -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)
|
r.Text(x+2+barW, y, label, fg, bg, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-20
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// ProgressType specifies progress indicator variant
|
// ProgressType specifies progress indicator variant
|
||||||
type ProgressType uint8
|
type ProgressType uint8
|
||||||
@@ -93,11 +96,11 @@ type ProgressOverlayOpts struct {
|
|||||||
Width int // Overlay width, 0 = auto
|
Width int // Overlay width, 0 = auto
|
||||||
Cancelable bool // Show cancel hint
|
Cancelable bool // Show cancel hint
|
||||||
CancelKey string // e.g., "Esc"
|
CancelKey string // e.g., "Esc"
|
||||||
Fg terminal.RGB
|
Fg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
BarFg terminal.RGB
|
BarFg color.RGB
|
||||||
BarBg terminal.RGB
|
BarBg color.RGB
|
||||||
AccentFg terminal.RGB // Spinner/highlight color
|
AccentFg color.RGB // Spinner/highlight color
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultProgressOpts returns sensible defaults
|
// DefaultProgressOpts returns sensible defaults
|
||||||
@@ -111,11 +114,11 @@ func DefaultProgressOpts(title, message string, ptype ProgressType) ProgressOver
|
|||||||
BarStyle: BarStyleBlock,
|
BarStyle: BarStyleBlock,
|
||||||
ShowPercent: true,
|
ShowPercent: true,
|
||||||
Width: 40,
|
Width: 40,
|
||||||
Fg: terminal.RGB{R: 220, G: 220, B: 220},
|
Fg: color.RGB{R: 220, G: 220, B: 220},
|
||||||
Bg: terminal.RGB{R: 30, G: 30, B: 40},
|
Bg: color.RGB{R: 30, G: 30, B: 40},
|
||||||
BarFg: terminal.RGB{R: 80, G: 160, B: 255},
|
BarFg: color.RGB{R: 80, G: 160, B: 255},
|
||||||
BarBg: terminal.RGB{R: 50, G: 50, B: 60},
|
BarBg: color.RGB{R: 50, G: 50, B: 60},
|
||||||
AccentFg: terminal.RGB{R: 100, G: 200, B: 255},
|
AccentFg: color.RGB{R: 100, G: 200, B: 255},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +172,12 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
|
|||||||
case ProgressStyleShadow:
|
case ProgressStyleShadow:
|
||||||
// Draw shadow first
|
// Draw shadow first
|
||||||
shadow := r.Sub(overlay.X-r.X+1, overlay.Y-r.Y+1, overlayW, overlayH)
|
shadow := r.Sub(overlay.X-r.X+1, overlay.Y-r.Y+1, overlayW, overlayH)
|
||||||
shadow.Fill(terminal.RGB{R: 10, G: 10, B: 15})
|
shadow.Fill(color.RGB{R: 10, G: 10, B: 15})
|
||||||
borderLine = LineSingle
|
borderLine = LineSingle
|
||||||
case ProgressStyleNeon:
|
case ProgressStyleNeon:
|
||||||
borderLine = LineDouble
|
borderLine = LineDouble
|
||||||
opts.AccentFg = terminal.RGB{R: 0, G: 255, B: 200}
|
opts.AccentFg = color.RGB{R: 0, G: 255, B: 200}
|
||||||
opts.BarFg = terminal.RGB{R: 255, G: 0, B: 255}
|
opts.BarFg = color.RGB{R: 255, G: 0, B: 255}
|
||||||
case ProgressStyleRetro:
|
case ProgressStyleRetro:
|
||||||
borderLine = LineHeavy
|
borderLine = LineHeavy
|
||||||
opts.BarStyle = BarStyleBlock
|
opts.BarStyle = BarStyleBlock
|
||||||
@@ -242,7 +245,7 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
|
|||||||
if opts.CancelKey == "" {
|
if opts.CancelKey == "" {
|
||||||
hint = "Esc to cancel"
|
hint = "Esc to cancel"
|
||||||
}
|
}
|
||||||
content.TextCenter(y, hint, terminal.RGB{R: 120, G: 120, B: 130}, opts.Bg, terminal.AttrDim)
|
content.TextCenter(y, hint, color.RGB{R: 120, G: 120, B: 130}, opts.Bg, terminal.AttrDim)
|
||||||
}
|
}
|
||||||
|
|
||||||
return overlay
|
return overlay
|
||||||
@@ -301,7 +304,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
|
|||||||
|
|
||||||
for x := 0; x < barW; x++ {
|
for x := 0; x < barW; x++ {
|
||||||
var ch rune
|
var ch rune
|
||||||
var fg terminal.RGB
|
var fg color.RGB
|
||||||
if x < filled {
|
if x < filled {
|
||||||
ch = chars[0]
|
ch = chars[0]
|
||||||
fg = opts.BarFg
|
fg = opts.BarFg
|
||||||
@@ -334,7 +337,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
|
|||||||
|
|
||||||
for x := 0; x < barW; x++ {
|
for x := 0; x < barW; x++ {
|
||||||
var ch rune
|
var ch rune
|
||||||
var fg terminal.RGB
|
var fg color.RGB
|
||||||
if x >= pos && x < pos+markerW {
|
if x >= pos && x < pos+markerW {
|
||||||
ch = chars[0]
|
ch = chars[0]
|
||||||
fg = opts.BarFg
|
fg = opts.BarFg
|
||||||
@@ -365,11 +368,11 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
|
|||||||
|
|
||||||
for x := 0; x < barW; x++ {
|
for x := 0; x < barW; x++ {
|
||||||
var ch rune
|
var ch rune
|
||||||
var fg terminal.RGB
|
var fg color.RGB
|
||||||
if x < filled {
|
if x < filled {
|
||||||
ch = chars[0]
|
ch = chars[0]
|
||||||
// Pulse the color
|
// Pulse the color
|
||||||
fg = terminal.RGB{
|
fg = color.RGB{
|
||||||
R: uint8(float64(opts.BarFg.R) * (0.5 + intensity*0.5)),
|
R: uint8(float64(opts.BarFg.R) * (0.5 + intensity*0.5)),
|
||||||
G: uint8(float64(opts.BarFg.G) * (0.5 + intensity*0.5)),
|
G: uint8(float64(opts.BarFg.G) * (0.5 + intensity*0.5)),
|
||||||
B: uint8(float64(opts.BarFg.B) * (0.5 + intensity*0.5)),
|
B: uint8(float64(opts.BarFg.B) * (0.5 + intensity*0.5)),
|
||||||
@@ -390,7 +393,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
|
|||||||
// ETA
|
// ETA
|
||||||
if opts.ShowETA != "" {
|
if opts.ShowETA != "" {
|
||||||
etaX := bar.W - RuneLen(opts.ShowETA)
|
etaX := bar.W - RuneLen(opts.ShowETA)
|
||||||
bar.Text(etaX, 0, opts.ShowETA, terminal.RGB{R: 150, G: 150, B: 160}, opts.Bg, terminal.AttrDim)
|
bar.Text(etaX, 0, opts.ShowETA, color.RGB{R: 150, G: 150, B: 160}, opts.Bg, terminal.AttrDim)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,3 +467,4 @@ func (p *ProgressState) Dismiss() {
|
|||||||
func (p *ProgressState) Show() {
|
func (p *ProgressState) Show() {
|
||||||
p.Visible = true
|
p.Visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// Region represents a rectangular area within a cell buffer
|
// Region represents a rectangular area within a cell buffer
|
||||||
// All coordinates are relative to the region's origin
|
// All coordinates are relative to the region's origin
|
||||||
@@ -63,7 +66,7 @@ func (r Region) Inset(n int) Region {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cell sets a single cell with bounds checking
|
// Cell sets a single cell with bounds checking
|
||||||
func (r Region) Cell(x, y int, ch rune, fg, bg terminal.RGB, attr terminal.Attr) {
|
func (r Region) Cell(x, y int, ch rune, fg, bg color.RGB, attr terminal.Attr) {
|
||||||
if x < 0 || x >= r.W || y < 0 || y >= r.H {
|
if x < 0 || x >= r.W || y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,17 +86,17 @@ func (r Region) Cell(x, y int, ch rune, fg, bg terminal.RGB, attr terminal.Attr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fill fills entire region with background color
|
// Fill fills entire region with background color
|
||||||
func (r Region) Fill(bg terminal.RGB) {
|
func (r Region) Fill(bg color.RGB) {
|
||||||
for y := 0; y < r.H; y++ {
|
for y := 0; y < r.H; y++ {
|
||||||
for x := 0; x < r.W; x++ {
|
for x := 0; x < r.W; x++ {
|
||||||
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone)
|
r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear fills region with spaces and zero colors
|
// Clear fills region with spaces and zero colors
|
||||||
func (r Region) Clear() {
|
func (r Region) Clear() {
|
||||||
r.Fill(terminal.RGB{})
|
r.Fill(color.RGB{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width returns region width
|
// Width returns region width
|
||||||
@@ -110,3 +113,4 @@ func (r Region) Height() int {
|
|||||||
func (r Region) Bounds() (x, y, w, h int) {
|
func (r Region) Bounds() (x, y, w, h int) {
|
||||||
return r.X, r.Y, r.W, r.H
|
return r.X, r.Y, r.W, r.H
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-6
@@ -1,12 +1,15 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// Default spinner frames for Region.Spinner
|
// Default spinner frames for Region.Spinner
|
||||||
var spinnerFrames = spinnerSets[SpinnerBraille]
|
var spinnerFrames = spinnerSets[SpinnerBraille]
|
||||||
|
|
||||||
// Text renders text at position, truncates at region edge
|
// Text renders text at position, truncates at region edge
|
||||||
func (r Region) Text(x, y int, s string, fg, bg terminal.RGB, attr terminal.Attr) {
|
func (r Region) Text(x, y int, s string, fg, bg color.RGB, attr terminal.Attr) {
|
||||||
if y < 0 || y >= r.H {
|
if y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -40,19 +43,19 @@ func (r Region) TextStyled(x, y int, s string, style Style) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TextRight renders text right-aligned on row
|
// TextRight renders text right-aligned on row
|
||||||
func (r Region) TextRight(y int, s string, fg, bg terminal.RGB, attr terminal.Attr) {
|
func (r Region) TextRight(y int, s string, fg, bg color.RGB, attr terminal.Attr) {
|
||||||
x := r.W - RuneLen(s)
|
x := r.W - RuneLen(s)
|
||||||
r.Text(x, y, s, fg, bg, attr)
|
r.Text(x, y, s, fg, bg, attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextCenter renders text centered on row
|
// TextCenter renders text centered on row
|
||||||
func (r Region) TextCenter(y int, s string, fg, bg terminal.RGB, attr terminal.Attr) {
|
func (r Region) TextCenter(y int, s string, fg, bg color.RGB, attr terminal.Attr) {
|
||||||
x := (r.W - RuneLen(s)) / 2
|
x := (r.W - RuneLen(s)) / 2
|
||||||
r.Text(x, y, s, fg, bg, attr)
|
r.Text(x, y, s, fg, bg, attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextBlock renders wrapped text within region bounds, returns number of lines rendered
|
// TextBlock renders wrapped text within region bounds, returns number of lines rendered
|
||||||
func (r Region) TextBlock(x, y int, text string, fg, bg terminal.RGB, attr terminal.Attr) int {
|
func (r Region) TextBlock(x, y int, text string, fg, bg color.RGB, attr terminal.Attr) int {
|
||||||
if x >= r.W || y >= r.H || text == "" {
|
if x >= r.W || y >= r.H || text == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -81,4 +84,3 @@ func (r Region) TextBlock(x, y int, text string, fg, bg terminal.RGB, attr termi
|
|||||||
func (r Region) TextBlockStyled(x, y int, text string, style Style) int {
|
func (r Region) TextBlockStyled(x, y int, text string, style Style) int {
|
||||||
return r.TextBlock(x, y, text, style.Fg, style.Bg, style.Attr)
|
return r.TextBlock(x, y, text, style.Fg, style.Bg, style.Attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -1,11 +1,12 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScrollBar draws vertical scrollbar track with thumb
|
// ScrollBar draws vertical scrollbar track with thumb
|
||||||
func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
|
func (r Region) ScrollBar(x int, offset, visible, total int, fg color.RGB) {
|
||||||
if x < 0 || x >= r.W || r.H < 1 {
|
if x < 0 || x >= r.W || r.H < 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -14,7 +15,7 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
|
|||||||
if total <= visible || trackH < 3 {
|
if total <= visible || trackH < 3 {
|
||||||
// No scrolling needed or track too small
|
// No scrolling needed or track too small
|
||||||
for y := range trackH {
|
for y := range trackH {
|
||||||
r.Cell(x, y, '│', fg, terminal.RGB{}, terminal.AttrDim)
|
r.Cell(x, y, '│', fg, color.RGB{}, terminal.AttrDim)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -42,12 +43,12 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
|
|||||||
} else {
|
} else {
|
||||||
ch = '░'
|
ch = '░'
|
||||||
}
|
}
|
||||||
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
|
r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrollIndicator draws compact indicator text (Top/Bot/XX%)
|
// ScrollIndicator draws compact indicator text (Top/Bot/XX%)
|
||||||
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg terminal.RGB) {
|
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg color.RGB) {
|
||||||
if y < 0 || y >= r.H {
|
if y < 0 || y >= r.H {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -68,6 +69,5 @@ func (r Region) ScrollIndicator(y int, offset, visible, total int, fg terminal.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.TextRight(y, text, fg, terminal.RGB{}, terminal.AttrDim)
|
r.TextRight(y, text, fg, color.RGB{}, terminal.AttrDim)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// BarSection represents one segment of a status bar
|
// BarSection represents one segment of a status bar
|
||||||
type BarSection struct {
|
type BarSection struct {
|
||||||
@@ -25,7 +28,7 @@ const (
|
|||||||
type BarOpts struct {
|
type BarOpts struct {
|
||||||
Separator string // Between sections, default " │ "
|
Separator string // Between sections, default " │ "
|
||||||
SepStyle Style // Separator styling
|
SepStyle Style // Separator styling
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
Align BarAlign
|
Align BarAlign
|
||||||
Padding int // Left/right padding, default 1
|
Padding int // Left/right padding, default 1
|
||||||
}
|
}
|
||||||
@@ -34,7 +37,7 @@ type BarOpts struct {
|
|||||||
func DefaultBarOpts() BarOpts {
|
func DefaultBarOpts() BarOpts {
|
||||||
return BarOpts{
|
return BarOpts{
|
||||||
Separator: " │ ",
|
Separator: " │ ",
|
||||||
SepStyle: Style{Fg: terminal.RGB{R: 80, G: 80, B: 100}},
|
SepStyle: Style{Fg: color.RGB{R: 80, G: 80, B: 100}},
|
||||||
Padding: 1,
|
Padding: 1,
|
||||||
Align: BarAlignRight,
|
Align: BarAlignRight,
|
||||||
}
|
}
|
||||||
@@ -55,7 +58,7 @@ func (r Region) StatusBar(y int, sections []BarSection, opts BarOpts) {
|
|||||||
|
|
||||||
// Fill background
|
// Fill background
|
||||||
for x := 0; x < r.W; x++ {
|
for x := 0; x < r.W; x++ {
|
||||||
r.Cell(x, y, ' ', terminal.RGB{}, opts.Bg, terminal.AttrNone)
|
r.Cell(x, y, ' ', color.RGB{}, opts.Bg, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
sepLen := RuneLen(opts.Separator)
|
sepLen := RuneLen(opts.Separator)
|
||||||
@@ -193,7 +196,7 @@ func truncateSections(sections []BarSection, widths []int, sepLen, availW int) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// QuickStatusBar renders simple label:value pairs right-aligned
|
// QuickStatusBar renders simple label:value pairs right-aligned
|
||||||
func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg terminal.RGB) {
|
func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg color.RGB) {
|
||||||
sections := make([]BarSection, len(pairs))
|
sections := make([]BarSection, len(pairs))
|
||||||
for i, p := range pairs {
|
for i, p := range pairs {
|
||||||
sections[i] = BarSection{
|
sections[i] = BarSection{
|
||||||
|
|||||||
+6
-4
@@ -1,22 +1,24 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Style bundles foreground, background, and attributes for text rendering
|
// Style bundles foreground, background, and attributes for text rendering
|
||||||
type Style struct {
|
type Style struct {
|
||||||
Fg terminal.RGB
|
Fg color.RGB
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
Attr terminal.Attr
|
Attr terminal.Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultStyle returns style with zero values (transparent bg)
|
// DefaultStyle returns style with zero values (transparent bg)
|
||||||
func DefaultStyle(fg terminal.RGB) Style {
|
func DefaultStyle(fg color.RGB) Style {
|
||||||
return Style{Fg: fg}
|
return Style{Fg: fg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero returns true if style has no colors or attributes set
|
// IsZero returns true if style has no colors or attributes set
|
||||||
func (s Style) IsZero() bool {
|
func (s Style) IsZero() bool {
|
||||||
return s.Fg == (terminal.RGB{}) && s.Bg == (terminal.RGB{}) && s.Attr == terminal.AttrNone
|
return s.Fg == (color.RGB{}) && s.Bg == (color.RGB{}) && s.Attr == terminal.AttrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-14
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
"github.com/lixenwraith/terminal"
|
"github.com/lixenwraith/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,25 +19,25 @@ type TextFieldOpts struct {
|
|||||||
// DefaultTextFieldStyle returns default colors
|
// DefaultTextFieldStyle returns default colors
|
||||||
func DefaultTextFieldStyle() TextFieldStyle {
|
func DefaultTextFieldStyle() TextFieldStyle {
|
||||||
return TextFieldStyle{
|
return TextFieldStyle{
|
||||||
TextFg: terminal.RGB{R: 220, G: 220, B: 220},
|
TextFg: color.RGB{R: 220, G: 220, B: 220},
|
||||||
TextBg: terminal.RGB{R: 30, G: 30, B: 40},
|
TextBg: color.RGB{R: 30, G: 30, B: 40},
|
||||||
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
|
CursorFg: color.RGB{R: 0, G: 0, B: 0},
|
||||||
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
|
CursorBg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
PlaceholderFg: terminal.RGB{R: 100, G: 100, B: 110},
|
PlaceholderFg: color.RGB{R: 100, G: 100, B: 110},
|
||||||
PrefixFg: terminal.RGB{R: 150, G: 150, B: 180},
|
PrefixFg: color.RGB{R: 150, G: 150, B: 180},
|
||||||
BorderFg: terminal.RGB{R: 80, G: 80, B: 100},
|
BorderFg: color.RGB{R: 80, G: 80, B: 100},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextFieldStyle defines text field colors
|
// TextFieldStyle defines text field colors
|
||||||
type TextFieldStyle struct {
|
type TextFieldStyle struct {
|
||||||
TextFg terminal.RGB
|
TextFg color.RGB
|
||||||
TextBg terminal.RGB
|
TextBg color.RGB
|
||||||
CursorFg terminal.RGB
|
CursorFg color.RGB
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
PlaceholderFg terminal.RGB
|
PlaceholderFg color.RGB
|
||||||
PrefixFg terminal.RGB
|
PrefixFg color.RGB
|
||||||
BorderFg terminal.RGB
|
BorderFg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextField renders text field and returns content height used
|
// TextField renders text field and returns content height used
|
||||||
@@ -169,3 +170,4 @@ func boolToInt(b bool) int {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+52
-49
@@ -1,63 +1,66 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
)
|
||||||
|
|
||||||
// Theme defines semantic colors for TUI components
|
// Theme defines semantic colors for TUI components
|
||||||
type Theme struct {
|
type Theme struct {
|
||||||
Bg terminal.RGB
|
Bg color.RGB
|
||||||
Fg terminal.RGB
|
Fg color.RGB
|
||||||
FocusBg terminal.RGB
|
FocusBg color.RGB
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
|
|
||||||
Selected terminal.RGB
|
Selected color.RGB
|
||||||
Unselected terminal.RGB
|
Unselected color.RGB
|
||||||
Partial terminal.RGB
|
Partial color.RGB
|
||||||
Error terminal.RGB
|
Error color.RGB
|
||||||
Warning terminal.RGB
|
Warning color.RGB
|
||||||
|
|
||||||
Border terminal.RGB
|
Border color.RGB
|
||||||
HeaderBg terminal.RGB
|
HeaderBg color.RGB
|
||||||
HeaderFg terminal.RGB
|
HeaderFg color.RGB
|
||||||
StatusFg terminal.RGB
|
StatusFg color.RGB
|
||||||
HintFg terminal.RGB
|
HintFg color.RGB
|
||||||
InputBg terminal.RGB
|
InputBg color.RGB
|
||||||
|
|
||||||
DirFg terminal.RGB
|
DirFg color.RGB
|
||||||
FileFg terminal.RGB
|
FileFg color.RGB
|
||||||
SymbolFg terminal.RGB
|
SymbolFg color.RGB
|
||||||
|
|
||||||
SyntaxComment terminal.RGB
|
SyntaxComment color.RGB
|
||||||
SyntaxString terminal.RGB
|
SyntaxString color.RGB
|
||||||
SyntaxKeyword terminal.RGB
|
SyntaxKeyword color.RGB
|
||||||
SyntaxType terminal.RGB
|
SyntaxType color.RGB
|
||||||
SyntaxNumber terminal.RGB
|
SyntaxNumber color.RGB
|
||||||
SyntaxSymbol terminal.RGB
|
SyntaxSymbol color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultTheme provides reasonable defaults
|
// DefaultTheme provides reasonable defaults
|
||||||
var DefaultTheme = Theme{
|
var DefaultTheme = Theme{
|
||||||
Bg: terminal.RGB{R: 20, G: 20, B: 30},
|
Bg: color.RGB{R: 20, G: 20, B: 30},
|
||||||
Fg: terminal.RGB{R: 200, G: 200, B: 200},
|
Fg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
FocusBg: terminal.RGB{R: 30, G: 35, B: 45},
|
FocusBg: color.RGB{R: 30, G: 35, B: 45},
|
||||||
CursorBg: terminal.RGB{R: 50, G: 50, B: 70},
|
CursorBg: color.RGB{R: 50, G: 50, B: 70},
|
||||||
Selected: terminal.RGB{R: 80, G: 200, B: 80},
|
Selected: color.RGB{R: 80, G: 200, B: 80},
|
||||||
Unselected: terminal.RGB{R: 100, G: 100, B: 100},
|
Unselected: color.RGB{R: 100, G: 100, B: 100},
|
||||||
Partial: terminal.RGB{R: 80, G: 160, B: 220},
|
Partial: color.RGB{R: 80, G: 160, B: 220},
|
||||||
Error: terminal.RGB{R: 255, G: 80, B: 80},
|
Error: color.RGB{R: 255, G: 80, B: 80},
|
||||||
Warning: terminal.RGB{R: 255, G: 80, B: 80},
|
Warning: color.RGB{R: 255, G: 80, B: 80},
|
||||||
Border: terminal.RGB{R: 60, G: 80, B: 100},
|
Border: color.RGB{R: 60, G: 80, B: 100},
|
||||||
HeaderBg: terminal.RGB{R: 40, G: 60, B: 90},
|
HeaderBg: color.RGB{R: 40, G: 60, B: 90},
|
||||||
HeaderFg: terminal.RGB{R: 255, G: 255, B: 255},
|
HeaderFg: color.RGB{R: 255, G: 255, B: 255},
|
||||||
StatusFg: terminal.RGB{R: 140, G: 140, B: 140},
|
StatusFg: color.RGB{R: 140, G: 140, B: 140},
|
||||||
HintFg: terminal.RGB{R: 100, G: 180, B: 200},
|
HintFg: color.RGB{R: 100, G: 180, B: 200},
|
||||||
InputBg: terminal.RGB{R: 30, G: 30, B: 50},
|
InputBg: color.RGB{R: 30, G: 30, B: 50},
|
||||||
DirFg: terminal.RGB{R: 130, G: 170, B: 220},
|
DirFg: color.RGB{R: 130, G: 170, B: 220},
|
||||||
FileFg: terminal.RGB{R: 200, G: 200, B: 200},
|
FileFg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
SymbolFg: terminal.RGB{R: 180, G: 220, B: 220},
|
SymbolFg: color.RGB{R: 180, G: 220, B: 220},
|
||||||
SyntaxComment: terminal.RGB{R: 100, G: 110, B: 120},
|
SyntaxComment: color.RGB{R: 100, G: 110, B: 120},
|
||||||
SyntaxString: terminal.RGB{R: 180, G: 220, B: 140},
|
SyntaxString: color.RGB{R: 180, G: 220, B: 140},
|
||||||
SyntaxKeyword: terminal.RGB{R: 180, G: 140, B: 220},
|
SyntaxKeyword: color.RGB{R: 180, G: 140, B: 220},
|
||||||
SyntaxType: terminal.RGB{R: 80, G: 200, B: 200},
|
SyntaxType: color.RGB{R: 80, G: 200, B: 200},
|
||||||
SyntaxNumber: terminal.RGB{R: 220, G: 180, B: 120},
|
SyntaxNumber: color.RGB{R: 220, G: 180, B: 120},
|
||||||
SyntaxSymbol: terminal.RGB{R: 220, G: 180, B: 80},
|
SyntaxSymbol: color.RGB{R: 220, G: 180, B: 80},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-22
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// ToastPosition specifies where toast renders
|
// ToastPosition specifies where toast renders
|
||||||
type ToastPosition uint8
|
type ToastPosition uint8
|
||||||
@@ -46,26 +49,26 @@ var ToastIcons = map[ToastSeverity]rune{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ToastColors default colors per severity
|
// ToastColors default colors per severity
|
||||||
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon terminal.RGB }{
|
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon color.RGB }{
|
||||||
ToastInfo: {
|
ToastInfo: {
|
||||||
Fg: terminal.RGB{R: 200, G: 200, B: 200},
|
Fg: color.RGB{R: 200, G: 200, B: 200},
|
||||||
Bg: terminal.RGB{R: 40, G: 40, B: 50},
|
Bg: color.RGB{R: 40, G: 40, B: 50},
|
||||||
Icon: terminal.RGB{R: 100, G: 150, B: 255},
|
Icon: color.RGB{R: 100, G: 150, B: 255},
|
||||||
},
|
},
|
||||||
ToastSuccess: {
|
ToastSuccess: {
|
||||||
Fg: terminal.RGB{R: 220, G: 255, B: 220},
|
Fg: color.RGB{R: 220, G: 255, B: 220},
|
||||||
Bg: terminal.RGB{R: 30, G: 60, B: 30},
|
Bg: color.RGB{R: 30, G: 60, B: 30},
|
||||||
Icon: terminal.RGB{R: 80, G: 220, B: 80},
|
Icon: color.RGB{R: 80, G: 220, B: 80},
|
||||||
},
|
},
|
||||||
ToastWarning: {
|
ToastWarning: {
|
||||||
Fg: terminal.RGB{R: 255, G: 240, B: 200},
|
Fg: color.RGB{R: 255, G: 240, B: 200},
|
||||||
Bg: terminal.RGB{R: 60, G: 50, B: 20},
|
Bg: color.RGB{R: 60, G: 50, B: 20},
|
||||||
Icon: terminal.RGB{R: 255, G: 200, B: 60},
|
Icon: color.RGB{R: 255, G: 200, B: 60},
|
||||||
},
|
},
|
||||||
ToastError: {
|
ToastError: {
|
||||||
Fg: terminal.RGB{R: 255, G: 220, B: 220},
|
Fg: color.RGB{R: 255, G: 220, B: 220},
|
||||||
Bg: terminal.RGB{R: 60, G: 25, B: 25},
|
Bg: color.RGB{R: 60, G: 25, B: 25},
|
||||||
Icon: terminal.RGB{R: 255, G: 80, B: 80},
|
Icon: color.RGB{R: 255, G: 80, B: 80},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +84,9 @@ type ToastOpts struct {
|
|||||||
Padding int // Horizontal padding, default 1
|
Padding int // Horizontal padding, default 1
|
||||||
MarginX int // Margin from edge for floating positions
|
MarginX int // Margin from edge for floating positions
|
||||||
MarginY int // Margin from edge for floating positions
|
MarginY int // Margin from edge for floating positions
|
||||||
CustomFg terminal.RGB
|
CustomFg color.RGB
|
||||||
CustomBg terminal.RGB
|
CustomBg color.RGB
|
||||||
CustomIcon terminal.RGB
|
CustomIcon color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultToastOpts returns sensible defaults
|
// DefaultToastOpts returns sensible defaults
|
||||||
@@ -109,13 +112,13 @@ func (r Region) Toast(opts ToastOpts) Region {
|
|||||||
|
|
||||||
// Resolve colors
|
// Resolve colors
|
||||||
fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon
|
fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon
|
||||||
if fg == (terminal.RGB{}) {
|
if fg == (color.RGB{}) {
|
||||||
fg = ToastColors[opts.Severity].Fg
|
fg = ToastColors[opts.Severity].Fg
|
||||||
}
|
}
|
||||||
if bg == (terminal.RGB{}) {
|
if bg == (color.RGB{}) {
|
||||||
bg = ToastColors[opts.Severity].Bg
|
bg = ToastColors[opts.Severity].Bg
|
||||||
}
|
}
|
||||||
if iconFg == (terminal.RGB{}) {
|
if iconFg == (color.RGB{}) {
|
||||||
iconFg = ToastColors[opts.Severity].Icon
|
iconFg = ToastColors[opts.Severity].Icon
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +221,7 @@ func (r Region) Toast(opts ToastOpts) Region {
|
|||||||
case ToastStyleShadow:
|
case ToastStyleShadow:
|
||||||
// Shadow offset
|
// Shadow offset
|
||||||
shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH)
|
shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH)
|
||||||
shadowRegion.Fill(terminal.RGB{R: 10, G: 10, B: 15})
|
shadowRegion.Fill(color.RGB{R: 10, G: 10, B: 15})
|
||||||
toastRegion.BoxFilled(LineSingle, fg, bg)
|
toastRegion.BoxFilled(LineSingle, fg, bg)
|
||||||
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
|
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
|
||||||
}
|
}
|
||||||
@@ -226,7 +229,7 @@ func (r Region) Toast(opts ToastOpts) Region {
|
|||||||
return toastRegion
|
return toastRegion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg terminal.RGB, _ int) {
|
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg color.RGB, _ int) {
|
||||||
if content.W < 1 || content.H < 1 {
|
if content.W < 1 || content.H < 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -299,3 +302,4 @@ func (t *ToastState) Show(opts ToastOpts, frames int) {
|
|||||||
t.FramesLeft = frames
|
t.FramesLeft = frames
|
||||||
t.Visible = true
|
t.Visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-19
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "github.com/lixenwraith/terminal"
|
import (
|
||||||
|
"github.com/lixenwraith/color"
|
||||||
|
"github.com/lixenwraith/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
// ExpandIcon chars
|
// ExpandIcon chars
|
||||||
const (
|
const (
|
||||||
@@ -30,12 +33,12 @@ type TreeNode struct {
|
|||||||
Key string // Unique identifier for expansion state
|
Key string // Unique identifier for expansion state
|
||||||
Label string // Display text
|
Label string // Display text
|
||||||
Icon rune // Custom icon, 0 = auto (▶/▼ for expandable, • for leaf)
|
Icon rune // Custom icon, 0 = auto (▶/▼ for expandable, • for leaf)
|
||||||
IconFg terminal.RGB
|
IconFg color.RGB
|
||||||
Expandable bool // Has children
|
Expandable bool // Has children
|
||||||
Expanded bool // Currently expanded
|
Expanded bool // Currently expanded
|
||||||
Depth int // Nesting level (0 = root)
|
Depth int // Nesting level (0 = root)
|
||||||
Check CheckState // Optional checkbox, CheckNone to skip
|
Check CheckState // Optional checkbox, CheckNone to skip
|
||||||
CheckFg terminal.RGB
|
CheckFg color.RGB
|
||||||
Style Style // Text styling
|
Style Style // Text styling
|
||||||
IsLast bool // Last sibling at this depth (for tree lines)
|
IsLast bool // Last sibling at this depth (for tree lines)
|
||||||
Data any // Application payload
|
Data any // Application payload
|
||||||
@@ -43,7 +46,7 @@ type TreeNode struct {
|
|||||||
Suffix string // Secondary text after label (e.g., "(5 files)")
|
Suffix string // Secondary text after label (e.g., "(5 files)")
|
||||||
SuffixStyle Style // Styling for suffix, zero = dimmed version of Style
|
SuffixStyle Style // Styling for suffix, zero = dimmed version of Style
|
||||||
Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none
|
Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none
|
||||||
BadgeFg terminal.RGB // Badge color
|
BadgeFg color.RGB // Badge color
|
||||||
}
|
}
|
||||||
|
|
||||||
// ancestorHasMoreSiblings checks if there are more nodes at given depth after idx
|
// ancestorHasMoreSiblings checks if there are more nodes at given depth after idx
|
||||||
@@ -59,12 +62,12 @@ func (r Region) ancestorHasMoreSiblings(nodes []TreeNode, idx, depth int) bool {
|
|||||||
|
|
||||||
// TreeOpts configures tree rendering
|
// TreeOpts configures tree rendering
|
||||||
type TreeOpts struct {
|
type TreeOpts struct {
|
||||||
CursorBg terminal.RGB
|
CursorBg color.RGB
|
||||||
DefaultBg terminal.RGB
|
DefaultBg color.RGB
|
||||||
IndentWidth int // Cells per depth, default 2
|
IndentWidth int // Cells per depth, default 2
|
||||||
IconWidth int // Width for icon column, default 2
|
IconWidth int // Width for icon column, default 2
|
||||||
LineMode TreeLineMode
|
LineMode TreeLineMode
|
||||||
LineFg terminal.RGB
|
LineFg color.RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultTreeOpts returns sensible defaults
|
// DefaultTreeOpts returns sensible defaults
|
||||||
@@ -73,7 +76,7 @@ func DefaultTreeOpts() TreeOpts {
|
|||||||
IndentWidth: 2,
|
IndentWidth: 2,
|
||||||
IconWidth: 2,
|
IconWidth: 2,
|
||||||
LineMode: TreeLinesNone,
|
LineMode: TreeLinesNone,
|
||||||
LineFg: terminal.RGB{R: 80, G: 80, B: 100},
|
LineFg: color.RGB{R: 80, G: 80, B: 100},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lineFg := opts.LineFg
|
lineFg := opts.LineFg
|
||||||
if lineFg == (terminal.RGB{}) {
|
if lineFg == (color.RGB{}) {
|
||||||
lineFg = DefaultTheme.Border
|
lineFg = DefaultTheme.Border
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +117,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for x := 0; x < r.W; x++ {
|
for x := 0; x < r.W; x++ {
|
||||||
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone)
|
r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
x := 0
|
x := 0
|
||||||
@@ -139,7 +142,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
icon = IconBullet
|
icon = IconBullet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if iconFg == (terminal.RGB{}) {
|
if iconFg == (color.RGB{}) {
|
||||||
iconFg = lineFg
|
iconFg = lineFg
|
||||||
}
|
}
|
||||||
if x < r.W {
|
if x < r.W {
|
||||||
@@ -148,10 +151,10 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
x += iconW
|
x += iconW
|
||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
if node.Check != CheckNone || node.CheckFg != (terminal.RGB{}) {
|
if node.Check != CheckNone || node.CheckFg != (color.RGB{}) {
|
||||||
if x+3 <= r.W {
|
if x+3 <= r.W {
|
||||||
checkFg := node.CheckFg
|
checkFg := node.CheckFg
|
||||||
if checkFg == (terminal.RGB{}) {
|
if checkFg == (color.RGB{}) {
|
||||||
checkFg = node.Style.Fg
|
checkFg = node.Style.Fg
|
||||||
}
|
}
|
||||||
var ch rune
|
var ch rune
|
||||||
@@ -176,7 +179,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
if node.Badge != 0 {
|
if node.Badge != 0 {
|
||||||
if x < r.W {
|
if x < r.W {
|
||||||
badgeFg := node.BadgeFg
|
badgeFg := node.BadgeFg
|
||||||
if badgeFg == (terminal.RGB{}) {
|
if badgeFg == (color.RGB{}) {
|
||||||
badgeFg = node.Style.Fg
|
badgeFg = node.Style.Fg
|
||||||
}
|
}
|
||||||
r.Cell(x, y, node.Badge, badgeFg, bg, terminal.AttrNone)
|
r.Cell(x, y, node.Badge, badgeFg, bg, terminal.AttrNone)
|
||||||
@@ -186,7 +189,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
|
|
||||||
// Label
|
// Label
|
||||||
style := node.Style
|
style := node.Style
|
||||||
if style.Bg == (terminal.RGB{}) {
|
if style.Bg == (color.RGB{}) {
|
||||||
style.Bg = bg
|
style.Bg = bg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,15 +217,15 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
// Suffix
|
// Suffix
|
||||||
if suffix != "" {
|
if suffix != "" {
|
||||||
suffixStyle := node.SuffixStyle
|
suffixStyle := node.SuffixStyle
|
||||||
if suffixStyle.Fg == (terminal.RGB{}) {
|
if suffixStyle.Fg == (color.RGB{}) {
|
||||||
// Default: dimmed version of label style
|
// Default: dimmed version of label style
|
||||||
suffixStyle.Fg = terminal.RGB{
|
suffixStyle.Fg = color.RGB{
|
||||||
R: node.Style.Fg.R / 2,
|
R: node.Style.Fg.R / 2,
|
||||||
G: node.Style.Fg.G / 2,
|
G: node.Style.Fg.G / 2,
|
||||||
B: node.Style.Fg.B / 2,
|
B: node.Style.Fg.B / 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if suffixStyle.Bg == (terminal.RGB{}) {
|
if suffixStyle.Bg == (color.RGB{}) {
|
||||||
suffixStyle.Bg = bg
|
suffixStyle.Bg = bg
|
||||||
}
|
}
|
||||||
r.TextStyled(x, y, suffix, suffixStyle)
|
r.TextStyled(x, y, suffix, suffixStyle)
|
||||||
@@ -235,7 +238,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderTreeLines draws connector lines for tree structure, returns x position after lines
|
// renderTreeLines draws connector lines for tree structure, returns x position after lines
|
||||||
func (r Region) renderTreeLines(y, startX int, node TreeNode, nodes []TreeNode, idx, scroll, indentW int, fg terminal.RGB, bg terminal.RGB) int {
|
func (r Region) renderTreeLines(y, startX int, node TreeNode, nodes []TreeNode, idx, scroll, indentW int, fg color.RGB, bg color.RGB) int {
|
||||||
x := startX
|
x := startX
|
||||||
|
|
||||||
for d := 0; d < node.Depth; d++ {
|
for d := 0; d < node.Depth; d++ {
|
||||||
@@ -335,3 +338,4 @@ func FindPrevSiblingIndex(nodes []TreeNode, idx int) int {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user