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

This commit is contained in:
2026-07-15 07:53:38 -04:00
parent aa22225c61
commit 9853ac0270
33 changed files with 480 additions and 745 deletions
+1 -1
View File
@@ -200,7 +200,7 @@ Pure logic, no rendering — usable independently:
- Width calculations count runes, not terminal columns; East Asian wide
characters and combining marks are not width-aware.
- Zero-value `terminal.RGB` in style fields generally means "inherit"
- Zero-value `color.RGB` in style fields generally means "inherit"
(widget default or row background) — check specific widget docs.
- Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require
application-side geometry from the regions used.
+15 -13
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -36,7 +37,7 @@ const (
// --- Box Rendering ---
// Box draws border around region edge
func (r Region) Box(line LineType, fg terminal.RGB) {
func (r Region) Box(line LineType, fg color.RGB) {
if r.W < 2 || r.H < 2 {
return
}
@@ -45,7 +46,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) {
}
chars := boxChars[line]
bg := terminal.RGB{} // Transparent (use existing bg)
bg := color.RGB{} // Transparent (use existing bg)
// Corners
r.Cell(0, 0, chars[boxTL], fg, bg, terminal.AttrNone)
@@ -67,7 +68,7 @@ func (r Region) Box(line LineType, fg terminal.RGB) {
}
// BoxFilled draws border and fills interior with background
func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) {
func (r Region) BoxFilled(line LineType, fg, bg color.RGB) {
// Fill interior first
for y := 1; y < r.H-1; y++ {
for x := 1; x < r.W-1; x++ {
@@ -81,7 +82,7 @@ func (r Region) BoxFilled(line LineType, fg, bg terminal.RGB) {
// --- Line rendering ---
// HLine draws horizontal line across region width at row y
func (r Region) HLine(y int, line LineType, fg terminal.RGB) {
func (r Region) HLine(y int, line LineType, fg color.RGB) {
if y < 0 || y >= r.H {
return
}
@@ -90,12 +91,12 @@ func (r Region) HLine(y int, line LineType, fg terminal.RGB) {
}
ch := boxChars[line][boxH]
for x := 0; x < r.W; x++ {
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
}
}
// VLine draws vertical line across region height at column x
func (r Region) VLine(x int, line LineType, fg terminal.RGB) {
func (r Region) VLine(x int, line LineType, fg color.RGB) {
if x < 0 || x >= r.W {
return
}
@@ -104,12 +105,12 @@ func (r Region) VLine(x int, line LineType, fg terminal.RGB) {
}
ch := boxChars[line][boxV]
for y := 0; y < r.H; y++ {
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
}
}
// Divider draws horizontal line with optional centered label
func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
func (r Region) Divider(y int, label string, line LineType, fg color.RGB) {
if y < 0 || y >= r.H {
return
}
@@ -121,7 +122,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
// Fill with horizontal line
for x := 0; x < r.W; x++ {
r.Cell(x, y, hChar, fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x, y, hChar, fg, color.RGB{}, terminal.AttrNone)
}
// Center label if provided
@@ -134,7 +135,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
}
startX := (r.W - textLen) / 2
for i, ch := range text {
r.Cell(startX+i, y, ch, fg, terminal.RGB{}, terminal.AttrBold)
r.Cell(startX+i, y, ch, fg, color.RGB{}, terminal.AttrBold)
}
}
}
@@ -142,7 +143,7 @@ func (r Region) Divider(y int, label string, line LineType, fg terminal.RGB) {
// --- Card rendering ---
// Card draws titled border and returns inner content region
func (r Region) Card(title string, line LineType, fg terminal.RGB) Region {
func (r Region) Card(title string, line LineType, fg color.RGB) Region {
r.Box(line, fg)
if title != "" && r.W > 4 {
@@ -152,8 +153,9 @@ func (r Region) Card(title string, line LineType, fg terminal.RGB) Region {
displayTitle = Truncate(displayTitle, maxTitleLen)
}
titleX := (r.W - RuneLen(displayTitle) - 2) / 2
r.Text(titleX, 0, " "+displayTitle+" ", fg, terminal.RGB{}, terminal.AttrBold)
r.Text(titleX, 0, " "+displayTitle+" ", fg, color.RGB{}, terminal.AttrBold)
}
return r.Inset(1)
}
}
+19 -15
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Button defines a single button in a button bar
type Button struct {
@@ -18,27 +21,27 @@ type ButtonBarOpts struct {
// ButtonStyle defines button bar colors
type ButtonStyle struct {
LabelFg terminal.RGB
LabelBg terminal.RGB
KeyFg terminal.RGB
FocusFg terminal.RGB
FocusBg terminal.RGB
Bg terminal.RGB
LabelFg color.RGB
LabelBg color.RGB
KeyFg color.RGB
FocusFg color.RGB
FocusBg color.RGB
Bg color.RGB
}
// DefaultButtonStyle returns default button colors with dark background
func DefaultButtonStyle() ButtonStyle {
return DefaultButtonStyleFrom(terminal.RGB{R: 25, G: 25, B: 35})
return DefaultButtonStyleFrom(color.RGB{R: 25, G: 25, B: 35})
}
// DefaultButtonStyleFrom returns default button colors using the given background
func DefaultButtonStyleFrom(bg terminal.RGB) ButtonStyle {
func DefaultButtonStyleFrom(bg color.RGB) ButtonStyle {
return ButtonStyle{
LabelFg: terminal.RGB{R: 200, G: 200, B: 200},
LabelBg: terminal.RGB{R: 50, G: 50, B: 60},
KeyFg: terminal.RGB{R: 130, G: 130, B: 150},
FocusFg: terminal.RGB{R: 255, G: 255, B: 255},
FocusBg: terminal.RGB{R: 60, G: 80, B: 120},
LabelFg: color.RGB{R: 200, G: 200, B: 200},
LabelBg: color.RGB{R: 50, G: 50, B: 60},
KeyFg: color.RGB{R: 130, G: 130, B: 150},
FocusFg: color.RGB{R: 255, G: 255, B: 255},
FocusBg: color.RGB{R: 60, G: 80, B: 120},
Bg: bg,
}
}
@@ -124,4 +127,5 @@ func (r Region) ButtonBar(y int, buttons []Button, opts ButtonBarOpts) {
x += gap
}
}
}
}
+7 -5
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -15,7 +16,7 @@ const (
)
// Checkbox draws a checkbox indicator
func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) {
func (r Region) Checkbox(x, y int, state CheckState, fg color.RGB) {
if x < 0 || x+2 >= r.W || y < 0 || y >= r.H {
return
}
@@ -30,7 +31,8 @@ func (r Region) Checkbox(x, y int, state CheckState, fg terminal.RGB) {
case CheckPlus:
ch = '+'
}
r.Cell(x, y, '[', fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x+1, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x+2, y, ']', fg, terminal.RGB{}, terminal.AttrNone)
}
r.Cell(x, y, '[', fg, color.RGB{}, terminal.AttrNone)
r.Cell(x+1, y, ch, fg, color.RGB{}, terminal.AttrNone)
r.Cell(x+2, y, ']', fg, color.RGB{}, terminal.AttrNone)
}
+26 -22
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ConfirmResult represents dialog outcome
type ConfirmResult uint8
@@ -98,31 +101,31 @@ type ConfirmOpts struct {
// ConfirmStyle defines dialog colors
type ConfirmStyle struct {
BorderFg terminal.RGB
TitleFg terminal.RGB
MessageFg terminal.RGB
Bg terminal.RGB
ButtonFg terminal.RGB
ButtonBg terminal.RGB
ButtonFocusFg terminal.RGB
ButtonFocusBg terminal.RGB
DestructiveFg terminal.RGB
DestructiveBg terminal.RGB
BorderFg color.RGB
TitleFg color.RGB
MessageFg color.RGB
Bg color.RGB
ButtonFg color.RGB
ButtonBg color.RGB
ButtonFocusFg color.RGB
ButtonFocusBg color.RGB
DestructiveFg color.RGB
DestructiveBg color.RGB
}
// DefaultConfirmStyle returns default dialog colors
func DefaultConfirmStyle() ConfirmStyle {
return ConfirmStyle{
BorderFg: terminal.RGB{R: 100, G: 100, B: 120},
TitleFg: terminal.RGB{R: 255, G: 255, B: 255},
MessageFg: terminal.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 30, G: 30, B: 40},
ButtonFg: terminal.RGB{R: 180, G: 180, B: 180},
ButtonBg: terminal.RGB{R: 50, G: 50, B: 60},
ButtonFocusFg: terminal.RGB{R: 255, G: 255, B: 255},
ButtonFocusBg: terminal.RGB{R: 60, G: 80, B: 120},
DestructiveFg: terminal.RGB{R: 255, G: 255, B: 255},
DestructiveBg: terminal.RGB{R: 180, G: 60, B: 60},
BorderFg: color.RGB{R: 100, G: 100, B: 120},
TitleFg: color.RGB{R: 255, G: 255, B: 255},
MessageFg: color.RGB{R: 200, G: 200, B: 200},
Bg: color.RGB{R: 30, G: 30, B: 40},
ButtonFg: color.RGB{R: 180, G: 180, B: 180},
ButtonBg: color.RGB{R: 50, G: 50, B: 60},
ButtonFocusFg: color.RGB{R: 255, G: 255, B: 255},
ButtonFocusBg: color.RGB{R: 60, G: 80, B: 120},
DestructiveFg: color.RGB{R: 255, G: 255, B: 255},
DestructiveBg: color.RGB{R: 180, G: 60, B: 60},
}
}
@@ -321,4 +324,5 @@ func (r Region) AlertDialog(opts AlertOpts) Region {
}
return content.Sub(0, 0, content.W, buttonY-1)
}
}
+19 -17
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -16,27 +17,27 @@ type EditorOpts struct {
// EditorStyle defines editor colors
type EditorStyle struct {
TextFg terminal.RGB
TextBg terminal.RGB
CursorFg terminal.RGB
CursorBg terminal.RGB
LineNumFg terminal.RGB
LineNumBg terminal.RGB
CurrentLineBg terminal.RGB
BorderFg terminal.RGB
TextFg color.RGB
TextBg color.RGB
CursorFg color.RGB
CursorBg color.RGB
LineNumFg color.RGB
LineNumBg color.RGB
CurrentLineBg color.RGB
BorderFg color.RGB
}
// DefaultEditorStyle returns default colors
func DefaultEditorStyle() EditorStyle {
return EditorStyle{
TextFg: terminal.RGB{R: 220, G: 220, B: 220},
TextBg: terminal.RGB{R: 25, G: 25, B: 35},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
LineNumFg: terminal.RGB{R: 100, G: 100, B: 120},
LineNumBg: terminal.RGB{R: 30, G: 30, B: 40},
CurrentLineBg: terminal.RGB{R: 35, G: 35, B: 50},
BorderFg: terminal.RGB{R: 80, G: 80, B: 100},
TextFg: color.RGB{R: 220, G: 220, B: 220},
TextBg: color.RGB{R: 25, G: 25, B: 35},
CursorFg: color.RGB{R: 0, G: 0, B: 0},
CursorBg: color.RGB{R: 200, G: 200, B: 200},
LineNumFg: color.RGB{R: 100, G: 100, B: 120},
LineNumBg: color.RGB{R: 30, G: 30, B: 40},
CurrentLineBg: color.RGB{R: 35, G: 35, B: 50},
BorderFg: color.RGB{R: 80, G: 80, B: 100},
}
}
@@ -194,4 +195,5 @@ func formatLineNum(num, width int) string {
s = " " + s
}
return s
}
}
+20 -16
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// FormField pairs a label with an editable text field
type FormField struct {
@@ -103,25 +106,25 @@ type FormOpts struct {
// FormStyle defines form colors
type FormStyle struct {
LabelFg terminal.RGB
FieldFg terminal.RGB
FieldBg terminal.RGB
FocusBg terminal.RGB
CursorFg terminal.RGB
CursorBg terminal.RGB
Bg terminal.RGB
LabelFg color.RGB
FieldFg color.RGB
FieldBg color.RGB
FocusBg color.RGB
CursorFg color.RGB
CursorBg color.RGB
Bg color.RGB
}
// DefaultFormStyle returns default form colors
func DefaultFormStyle() FormStyle {
return FormStyle{
LabelFg: terminal.RGB{R: 150, G: 150, B: 180},
FieldFg: terminal.RGB{R: 220, G: 220, B: 220},
FieldBg: terminal.RGB{R: 35, G: 35, B: 45},
FocusBg: terminal.RGB{R: 45, G: 45, B: 60},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 25, G: 25, B: 35},
LabelFg: color.RGB{R: 150, G: 150, B: 180},
FieldFg: color.RGB{R: 220, G: 220, B: 220},
FieldBg: color.RGB{R: 35, G: 35, B: 45},
FocusBg: color.RGB{R: 45, G: 45, B: 60},
CursorFg: color.RGB{R: 0, G: 0, B: 0},
CursorBg: color.RGB{R: 200, G: 200, B: 200},
Bg: color.RGB{R: 25, G: 25, B: 35},
}
}
@@ -225,4 +228,5 @@ func (r Region) Form(state *FormState, opts FormOpts) int {
}
return y
}
}
+7 -5
View File
@@ -1,18 +1,19 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// InputOpts configures single-line input field
type InputOpts struct {
Label string
LabelFg terminal.RGB
LabelFg color.RGB
Text string
Cursor int // Cursor position in text (rune index)
CursorBg terminal.RGB
TextFg terminal.RGB
Bg terminal.RGB
CursorBg color.RGB
TextFg color.RGB
Bg color.RGB
}
// Input renders labeled text input field on row y, handling cursor display and horizontal scrolling
@@ -75,4 +76,5 @@ func (r Region) Input(y int, opts InputOpts) {
if cursor == len(runes) && cursor-scroll < inputW {
r.Cell(x+cursor-scroll, y, ' ', opts.TextFg, opts.CursorBg, terminal.AttrNone)
}
}
}
+13 -9
View File
@@ -1,22 +1,25 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ListItem represents a single row in a scrollable list
type ListItem struct {
Indent int // Left padding in cells
Icon rune // Expand indicator or bullet, 0 = none
IconFg terminal.RGB
IconFg color.RGB
Check CheckState // CheckNone to skip checkbox
CheckFg terminal.RGB
CheckFg color.RGB
Text string
TextStyle Style
}
// ListOpts configures list rendering
type ListOpts struct {
CursorBg terminal.RGB
DefaultBg terminal.RGB
CursorBg color.RGB
DefaultBg color.RGB
IconWidth int // Width reserved for icon, default 2
}
@@ -49,7 +52,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
// Clear row
for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone)
r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
}
x := item.Indent
@@ -61,7 +64,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
x += iconW
// Checkbox
if item.Check != CheckNone || item.CheckFg != (terminal.RGB{}) {
if item.Check != CheckNone || item.CheckFg != (color.RGB{}) {
if x+3 <= r.W {
var ch rune
switch item.Check {
@@ -83,7 +86,7 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
// Text
textStyle := item.TextStyle
if textStyle.Bg == (terminal.RGB{}) {
if textStyle.Bg == (color.RGB{}) {
textStyle.Bg = bg
}
text := item.Text
@@ -96,4 +99,5 @@ func (r Region) List(items []ListItem, cursor, scroll int, opts ListOpts) int {
}
return rendered
}
}
+7 -5
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -9,10 +10,10 @@ type ModalOpts struct {
Title string
Hint string // Right-aligned hint text
Border LineType
BorderFg terminal.RGB
TitleFg terminal.RGB
HintFg terminal.RGB
Bg terminal.RGB
BorderFg color.RGB
TitleFg color.RGB
HintFg color.RGB
Bg color.RGB
}
// Modal fills region with background, draws border with title/hint, returns content region
@@ -63,4 +64,5 @@ func (r Region) Modal(opts ModalOpts) Region {
// Return content region
return r.Sub(1, 1, r.W-2, r.H-2)
}
}
+28 -24
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// OverlayStyle specifies overlay appearance
type OverlayStyle uint8
@@ -17,10 +20,10 @@ type OverlayOpts struct {
Style OverlayStyle
Title string
Border LineType
Bg terminal.RGB
Fg terminal.RGB // Border and title color
TitleBg terminal.RGB // Title bar background, zero = same as Fg
TitleFg terminal.RGB // Title text color, zero = same as Bg
Bg color.RGB
Fg color.RGB // Border and title color
TitleBg color.RGB // Title bar background, zero = same as Fg
TitleFg color.RGB // Title text color, zero = same as Bg
// Modal/Floating positioning (ignored for Fullscreen)
Width int // 0 = 80% of region
@@ -28,7 +31,7 @@ type OverlayOpts struct {
X, Y int // Offset from center, 0 = centered
// Shadow for Floating style
ShadowColor terminal.RGB
ShadowColor color.RGB
}
// DefaultOverlayOpts returns sensible defaults for modal overlay
@@ -37,10 +40,10 @@ func DefaultOverlayOpts(title string) OverlayOpts {
Style: OverlayModal,
Title: title,
Border: LineDouble,
Bg: terminal.RGB{R: 25, G: 25, B: 35},
Fg: terminal.RGB{R: 100, G: 140, B: 180},
TitleBg: terminal.RGB{R: 40, G: 60, B: 90},
TitleFg: terminal.RGB{R: 255, G: 255, B: 255},
Bg: color.RGB{R: 25, G: 25, B: 35},
Fg: color.RGB{R: 100, G: 140, B: 180},
TitleBg: color.RGB{R: 40, G: 60, B: 90},
TitleFg: color.RGB{R: 255, G: 255, B: 255},
}
}
@@ -49,10 +52,10 @@ func FullscreenOverlayOpts(title string) OverlayOpts {
return OverlayOpts{
Style: OverlayFullscreen,
Title: title,
Bg: terminal.RGB{R: 20, G: 20, B: 30},
Fg: terminal.RGB{R: 100, G: 140, B: 180},
TitleBg: terminal.RGB{R: 40, G: 60, B: 90},
TitleFg: terminal.RGB{R: 255, G: 255, B: 255},
Bg: color.RGB{R: 20, G: 20, B: 30},
Fg: color.RGB{R: 100, G: 140, B: 180},
TitleBg: color.RGB{R: 40, G: 60, B: 90},
TitleFg: color.RGB{R: 255, G: 255, B: 255},
}
}
@@ -98,11 +101,11 @@ func (r Region) renderFullscreenOverlay(opts OverlayOpts) OverlayResult {
// Title bar
if opts.Title != "" {
titleBg := opts.TitleBg
if titleBg == (terminal.RGB{}) {
if titleBg == (color.RGB{}) {
titleBg = opts.Fg
}
titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) {
if titleFg == (color.RGB{}) {
titleFg = opts.Bg
}
@@ -188,11 +191,11 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult {
// Title in top border
if opts.Title != "" && contentW > 2 {
titleBg := opts.TitleBg
if titleBg == (terminal.RGB{}) {
if titleBg == (color.RGB{}) {
titleBg = opts.Fg
}
titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) {
if titleFg == (color.RGB{}) {
titleFg = opts.Bg
}
@@ -224,8 +227,8 @@ func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult {
func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
// Same as modal but with shadow
shadowColor := opts.ShadowColor
if shadowColor == (terminal.RGB{}) {
shadowColor = terminal.RGB{R: 10, G: 10, B: 15}
if shadowColor == (color.RGB{}) {
shadowColor = color.RGB{R: 10, G: 10, B: 15}
}
// Calculate dimensions (same as modal)
@@ -285,11 +288,11 @@ func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult {
if opts.Title != "" && contentW > 2 {
titleBg := opts.TitleBg
if titleBg == (terminal.RGB{}) {
if titleBg == (color.RGB{}) {
titleBg = opts.Fg
}
titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) {
if titleFg == (color.RGB{}) {
titleFg = opts.Bg
}
@@ -327,7 +330,7 @@ func (r Region) renderBorderTitleOverlay(opts OverlayOpts) OverlayResult {
if opts.Title != "" && r.W > 6 {
titleFg := opts.TitleFg
if titleFg == (terminal.RGB{}) {
if titleFg == (color.RGB{}) {
titleFg = opts.Fg
}
title := " " + opts.Title + " "
@@ -370,4 +373,5 @@ func (o *OverlayState) Hide() {
// Toggle switches visibility
func (o *OverlayState) Toggle() {
o.Visible = !o.Visible
}
}
+8 -6
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -8,9 +9,9 @@ import (
type PaneOpts struct {
Title string
Border LineType
BorderFg terminal.RGB
Bg terminal.RGB
TitleFg terminal.RGB
BorderFg color.RGB
Bg color.RGB
TitleFg color.RGB
}
// Pane draws bordered pane with optional title, returns content region
@@ -49,7 +50,7 @@ func (r Region) Pane(opts PaneOpts) Region {
// TitledPane fills region with background, draws centered title at top, returns content region
// Content region starts at row 1 with full width
func (r Region) TitledPane(title string, titleFg, bg terminal.RGB) Region {
func (r Region) TitledPane(title string, titleFg, bg color.RGB) Region {
r.Fill(bg)
if title != "" && r.H > 0 {
r.TextCenter(0, title, titleFg, bg, terminal.AttrBold)
@@ -61,9 +62,10 @@ func (r Region) TitledPane(title string, titleFg, bg terminal.RGB) Region {
}
// TitledPaneFocused is TitledPane with focus-dependent background
func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg terminal.RGB, focused bool) Region {
func (r Region) TitledPaneFocused(title string, titleFg, bg, focusBg color.RGB, focused bool) Region {
if focused {
bg = focusBg
}
return r.TitledPane(title, titleFg, bg)
}
}
+8 -6
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -12,7 +13,7 @@ const (
)
// Progress draws horizontal progress bar (0.0-1.0)
func (r Region) Progress(x, y, w int, pct float64, fg, bg terminal.RGB) {
func (r Region) Progress(x, y, w int, pct float64, fg, bg color.RGB) {
if y < 0 || y >= r.H || w <= 0 {
return
}
@@ -43,7 +44,7 @@ func (r Region) Progress(x, y, w int, pct float64, fg, bg terminal.RGB) {
}
// ProgressV draws vertical progress bar (fills bottom-up)
func (r Region) ProgressV(x, y, h int, pct float64, fg, bg terminal.RGB) {
func (r Region) ProgressV(x, y, h int, pct float64, fg, bg color.RGB) {
if x < 0 || x >= r.W || h <= 0 {
return
}
@@ -72,7 +73,7 @@ func (r Region) ProgressV(x, y, h int, pct float64, fg, bg terminal.RGB) {
}
// Spinner draws spinner character based on frame counter
func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) {
func (r Region) Spinner(x, y int, frame int, fg color.RGB) {
if x < 0 || x >= r.W || y < 0 || y >= r.H {
return
}
@@ -80,11 +81,11 @@ func (r Region) Spinner(x, y int, frame int, fg terminal.RGB) {
if idx < 0 {
idx = -idx
}
r.Cell(x, y, spinnerFrames[idx], fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x, y, spinnerFrames[idx], fg, color.RGB{}, terminal.AttrNone)
}
// Gauge draws labeled gauge with percentage
func (r Region) Gauge(x, y, w int, value, max int, fg, bg terminal.RGB) {
func (r Region) Gauge(x, y, w int, value, max int, fg, bg color.RGB) {
if w < 5 || y < 0 || y >= r.H {
return
}
@@ -121,4 +122,5 @@ func (r Region) Gauge(x, y, w int, value, max int, fg, bg terminal.RGB) {
label = " " + string(rune('0'+pctInt)) + "%"
}
r.Text(x+2+barW, y, label, fg, bg, terminal.AttrNone)
}
}
+25 -21
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ProgressType specifies progress indicator variant
type ProgressType uint8
@@ -93,11 +96,11 @@ type ProgressOverlayOpts struct {
Width int // Overlay width, 0 = auto
Cancelable bool // Show cancel hint
CancelKey string // e.g., "Esc"
Fg terminal.RGB
Bg terminal.RGB
BarFg terminal.RGB
BarBg terminal.RGB
AccentFg terminal.RGB // Spinner/highlight color
Fg color.RGB
Bg color.RGB
BarFg color.RGB
BarBg color.RGB
AccentFg color.RGB // Spinner/highlight color
}
// DefaultProgressOpts returns sensible defaults
@@ -111,11 +114,11 @@ func DefaultProgressOpts(title, message string, ptype ProgressType) ProgressOver
BarStyle: BarStyleBlock,
ShowPercent: true,
Width: 40,
Fg: terminal.RGB{R: 220, G: 220, B: 220},
Bg: terminal.RGB{R: 30, G: 30, B: 40},
BarFg: terminal.RGB{R: 80, G: 160, B: 255},
BarBg: terminal.RGB{R: 50, G: 50, B: 60},
AccentFg: terminal.RGB{R: 100, G: 200, B: 255},
Fg: color.RGB{R: 220, G: 220, B: 220},
Bg: color.RGB{R: 30, G: 30, B: 40},
BarFg: color.RGB{R: 80, G: 160, B: 255},
BarBg: color.RGB{R: 50, G: 50, B: 60},
AccentFg: color.RGB{R: 100, G: 200, B: 255},
}
}
@@ -169,12 +172,12 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
case ProgressStyleShadow:
// Draw shadow first
shadow := r.Sub(overlay.X-r.X+1, overlay.Y-r.Y+1, overlayW, overlayH)
shadow.Fill(terminal.RGB{R: 10, G: 10, B: 15})
shadow.Fill(color.RGB{R: 10, G: 10, B: 15})
borderLine = LineSingle
case ProgressStyleNeon:
borderLine = LineDouble
opts.AccentFg = terminal.RGB{R: 0, G: 255, B: 200}
opts.BarFg = terminal.RGB{R: 255, G: 0, B: 255}
opts.AccentFg = color.RGB{R: 0, G: 255, B: 200}
opts.BarFg = color.RGB{R: 255, G: 0, B: 255}
case ProgressStyleRetro:
borderLine = LineHeavy
opts.BarStyle = BarStyleBlock
@@ -242,7 +245,7 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
if opts.CancelKey == "" {
hint = "Esc to cancel"
}
content.TextCenter(y, hint, terminal.RGB{R: 120, G: 120, B: 130}, opts.Bg, terminal.AttrDim)
content.TextCenter(y, hint, color.RGB{R: 120, G: 120, B: 130}, opts.Bg, terminal.AttrDim)
}
return overlay
@@ -301,7 +304,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
for x := 0; x < barW; x++ {
var ch rune
var fg terminal.RGB
var fg color.RGB
if x < filled {
ch = chars[0]
fg = opts.BarFg
@@ -334,7 +337,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
for x := 0; x < barW; x++ {
var ch rune
var fg terminal.RGB
var fg color.RGB
if x >= pos && x < pos+markerW {
ch = chars[0]
fg = opts.BarFg
@@ -365,11 +368,11 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
for x := 0; x < barW; x++ {
var ch rune
var fg terminal.RGB
var fg color.RGB
if x < filled {
ch = chars[0]
// Pulse the color
fg = terminal.RGB{
fg = color.RGB{
R: uint8(float64(opts.BarFg.R) * (0.5 + intensity*0.5)),
G: uint8(float64(opts.BarFg.G) * (0.5 + intensity*0.5)),
B: uint8(float64(opts.BarFg.B) * (0.5 + intensity*0.5)),
@@ -390,7 +393,7 @@ func (r Region) renderProgressBar(bar Region, opts ProgressOverlayOpts) {
// ETA
if opts.ShowETA != "" {
etaX := bar.W - RuneLen(opts.ShowETA)
bar.Text(etaX, 0, opts.ShowETA, terminal.RGB{R: 150, G: 150, B: 160}, opts.Bg, terminal.AttrDim)
bar.Text(etaX, 0, opts.ShowETA, color.RGB{R: 150, G: 150, B: 160}, opts.Bg, terminal.AttrDim)
}
}
@@ -463,4 +466,5 @@ func (p *ProgressState) Dismiss() {
// Show displays the progress overlay
func (p *ProgressState) Show() {
p.Visible = true
}
}
+10 -6
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Region represents a rectangular area within a cell buffer
// All coordinates are relative to the region's origin
@@ -63,7 +66,7 @@ func (r Region) Inset(n int) Region {
}
// Cell sets a single cell with bounds checking
func (r Region) Cell(x, y int, ch rune, fg, bg terminal.RGB, attr terminal.Attr) {
func (r Region) Cell(x, y int, ch rune, fg, bg color.RGB, attr terminal.Attr) {
if x < 0 || x >= r.W || y < 0 || y >= r.H {
return
}
@@ -83,17 +86,17 @@ func (r Region) Cell(x, y int, ch rune, fg, bg terminal.RGB, attr terminal.Attr)
}
// Fill fills entire region with background color
func (r Region) Fill(bg terminal.RGB) {
func (r Region) Fill(bg color.RGB) {
for y := 0; y < r.H; y++ {
for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone)
r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
}
}
}
// Clear fills region with spaces and zero colors
func (r Region) Clear() {
r.Fill(terminal.RGB{})
r.Fill(color.RGB{})
}
// Width returns region width
@@ -109,4 +112,5 @@ func (r Region) Height() int {
// Bounds returns absolute position and dimensions
func (r Region) Bounds() (x, y, w, h int) {
return r.X, r.Y, r.W, r.H
}
}
+8 -6
View File
@@ -1,12 +1,15 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Default spinner frames for Region.Spinner
var spinnerFrames = spinnerSets[SpinnerBraille]
// Text renders text at position, truncates at region edge
func (r Region) Text(x, y int, s string, fg, bg terminal.RGB, attr terminal.Attr) {
func (r Region) Text(x, y int, s string, fg, bg color.RGB, attr terminal.Attr) {
if y < 0 || y >= r.H {
return
}
@@ -40,19 +43,19 @@ func (r Region) TextStyled(x, y int, s string, style Style) {
}
// TextRight renders text right-aligned on row
func (r Region) TextRight(y int, s string, fg, bg terminal.RGB, attr terminal.Attr) {
func (r Region) TextRight(y int, s string, fg, bg color.RGB, attr terminal.Attr) {
x := r.W - RuneLen(s)
r.Text(x, y, s, fg, bg, attr)
}
// TextCenter renders text centered on row
func (r Region) TextCenter(y int, s string, fg, bg terminal.RGB, attr terminal.Attr) {
func (r Region) TextCenter(y int, s string, fg, bg color.RGB, attr terminal.Attr) {
x := (r.W - RuneLen(s)) / 2
r.Text(x, y, s, fg, bg, attr)
}
// TextBlock renders wrapped text within region bounds, returns number of lines rendered
func (r Region) TextBlock(x, y int, text string, fg, bg terminal.RGB, attr terminal.Attr) int {
func (r Region) TextBlock(x, y int, text string, fg, bg color.RGB, attr terminal.Attr) int {
if x >= r.W || y >= r.H || text == "" {
return 0
}
@@ -81,4 +84,3 @@ func (r Region) TextBlock(x, y int, text string, fg, bg terminal.RGB, attr termi
func (r Region) TextBlockStyled(x, y int, text string, style Style) int {
return r.TextBlock(x, y, text, style.Fg, style.Bg, style.Attr)
}
+6 -6
View File
@@ -1,11 +1,12 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ScrollBar draws vertical scrollbar track with thumb
func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
func (r Region) ScrollBar(x int, offset, visible, total int, fg color.RGB) {
if x < 0 || x >= r.W || r.H < 1 {
return
}
@@ -14,7 +15,7 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
if total <= visible || trackH < 3 {
// No scrolling needed or track too small
for y := range trackH {
r.Cell(x, y, '│', fg, terminal.RGB{}, terminal.AttrDim)
r.Cell(x, y, '│', fg, color.RGB{}, terminal.AttrDim)
}
return
}
@@ -42,12 +43,12 @@ func (r Region) ScrollBar(x int, offset, visible, total int, fg terminal.RGB) {
} else {
ch = '░'
}
r.Cell(x, y, ch, fg, terminal.RGB{}, terminal.AttrNone)
r.Cell(x, y, ch, fg, color.RGB{}, terminal.AttrNone)
}
}
// ScrollIndicator draws compact indicator text (Top/Bot/XX%)
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg terminal.RGB) {
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg color.RGB) {
if y < 0 || y >= r.H {
return
}
@@ -68,6 +69,5 @@ func (r Region) ScrollIndicator(y int, offset, visible, total int, fg terminal.R
}
}
r.TextRight(y, text, fg, terminal.RGB{}, terminal.AttrDim)
r.TextRight(y, text, fg, color.RGB{}, terminal.AttrDim)
}
+9 -6
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// BarSection represents one segment of a status bar
type BarSection struct {
@@ -25,7 +28,7 @@ const (
type BarOpts struct {
Separator string // Between sections, default " │ "
SepStyle Style // Separator styling
Bg terminal.RGB
Bg color.RGB
Align BarAlign
Padding int // Left/right padding, default 1
}
@@ -34,7 +37,7 @@ type BarOpts struct {
func DefaultBarOpts() BarOpts {
return BarOpts{
Separator: " │ ",
SepStyle: Style{Fg: terminal.RGB{R: 80, G: 80, B: 100}},
SepStyle: Style{Fg: color.RGB{R: 80, G: 80, B: 100}},
Padding: 1,
Align: BarAlignRight,
}
@@ -55,7 +58,7 @@ func (r Region) StatusBar(y int, sections []BarSection, opts BarOpts) {
// Fill background
for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, opts.Bg, terminal.AttrNone)
r.Cell(x, y, ' ', color.RGB{}, opts.Bg, terminal.AttrNone)
}
sepLen := RuneLen(opts.Separator)
@@ -193,7 +196,7 @@ func truncateSections(sections []BarSection, widths []int, sepLen, availW int) (
}
// QuickStatusBar renders simple label:value pairs right-aligned
func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg terminal.RGB) {
func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg color.RGB) {
sections := make([]BarSection, len(pairs))
for i, p := range pairs {
sections[i] = BarSection{
@@ -207,4 +210,4 @@ func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg te
Bg: bg,
Align: BarAlignRight,
})
}
}
+7 -5
View File
@@ -1,22 +1,24 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// Style bundles foreground, background, and attributes for text rendering
type Style struct {
Fg terminal.RGB
Bg terminal.RGB
Fg color.RGB
Bg color.RGB
Attr terminal.Attr
}
// DefaultStyle returns style with zero values (transparent bg)
func DefaultStyle(fg terminal.RGB) Style {
func DefaultStyle(fg color.RGB) Style {
return Style{Fg: fg}
}
// IsZero returns true if style has no colors or attributes set
func (s Style) IsZero() bool {
return s.Fg == (terminal.RGB{}) && s.Bg == (terminal.RGB{}) && s.Attr == terminal.AttrNone
}
return s.Fg == (color.RGB{}) && s.Bg == (color.RGB{}) && s.Attr == terminal.AttrNone
}
+17 -15
View File
@@ -1,6 +1,7 @@
package tui
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
@@ -18,25 +19,25 @@ type TextFieldOpts struct {
// DefaultTextFieldStyle returns default colors
func DefaultTextFieldStyle() TextFieldStyle {
return TextFieldStyle{
TextFg: terminal.RGB{R: 220, G: 220, B: 220},
TextBg: terminal.RGB{R: 30, G: 30, B: 40},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
PlaceholderFg: terminal.RGB{R: 100, G: 100, B: 110},
PrefixFg: terminal.RGB{R: 150, G: 150, B: 180},
BorderFg: terminal.RGB{R: 80, G: 80, B: 100},
TextFg: color.RGB{R: 220, G: 220, B: 220},
TextBg: color.RGB{R: 30, G: 30, B: 40},
CursorFg: color.RGB{R: 0, G: 0, B: 0},
CursorBg: color.RGB{R: 200, G: 200, B: 200},
PlaceholderFg: color.RGB{R: 100, G: 100, B: 110},
PrefixFg: color.RGB{R: 150, G: 150, B: 180},
BorderFg: color.RGB{R: 80, G: 80, B: 100},
}
}
// TextFieldStyle defines text field colors
type TextFieldStyle struct {
TextFg terminal.RGB
TextBg terminal.RGB
CursorFg terminal.RGB
CursorBg terminal.RGB
PlaceholderFg terminal.RGB
PrefixFg terminal.RGB
BorderFg terminal.RGB
TextFg color.RGB
TextBg color.RGB
CursorFg color.RGB
CursorBg color.RGB
PlaceholderFg color.RGB
PrefixFg color.RGB
BorderFg color.RGB
}
// TextField renders text field and returns content height used
@@ -168,4 +169,5 @@ func boolToInt(b bool) int {
return 1
}
return 0
}
}
+53 -50
View File
@@ -1,63 +1,66 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
)
// Theme defines semantic colors for TUI components
type Theme struct {
Bg terminal.RGB
Fg terminal.RGB
FocusBg terminal.RGB
CursorBg terminal.RGB
Bg color.RGB
Fg color.RGB
FocusBg color.RGB
CursorBg color.RGB
Selected terminal.RGB
Unselected terminal.RGB
Partial terminal.RGB
Error terminal.RGB
Warning terminal.RGB
Selected color.RGB
Unselected color.RGB
Partial color.RGB
Error color.RGB
Warning color.RGB
Border terminal.RGB
HeaderBg terminal.RGB
HeaderFg terminal.RGB
StatusFg terminal.RGB
HintFg terminal.RGB
InputBg terminal.RGB
Border color.RGB
HeaderBg color.RGB
HeaderFg color.RGB
StatusFg color.RGB
HintFg color.RGB
InputBg color.RGB
DirFg terminal.RGB
FileFg terminal.RGB
SymbolFg terminal.RGB
DirFg color.RGB
FileFg color.RGB
SymbolFg color.RGB
SyntaxComment terminal.RGB
SyntaxString terminal.RGB
SyntaxKeyword terminal.RGB
SyntaxType terminal.RGB
SyntaxNumber terminal.RGB
SyntaxSymbol terminal.RGB
SyntaxComment color.RGB
SyntaxString color.RGB
SyntaxKeyword color.RGB
SyntaxType color.RGB
SyntaxNumber color.RGB
SyntaxSymbol color.RGB
}
// DefaultTheme provides reasonable defaults
var DefaultTheme = Theme{
Bg: terminal.RGB{R: 20, G: 20, B: 30},
Fg: terminal.RGB{R: 200, G: 200, B: 200},
FocusBg: terminal.RGB{R: 30, G: 35, B: 45},
CursorBg: terminal.RGB{R: 50, G: 50, B: 70},
Selected: terminal.RGB{R: 80, G: 200, B: 80},
Unselected: terminal.RGB{R: 100, G: 100, B: 100},
Partial: terminal.RGB{R: 80, G: 160, B: 220},
Error: terminal.RGB{R: 255, G: 80, B: 80},
Warning: terminal.RGB{R: 255, G: 80, B: 80},
Border: terminal.RGB{R: 60, G: 80, B: 100},
HeaderBg: terminal.RGB{R: 40, G: 60, B: 90},
HeaderFg: terminal.RGB{R: 255, G: 255, B: 255},
StatusFg: terminal.RGB{R: 140, G: 140, B: 140},
HintFg: terminal.RGB{R: 100, G: 180, B: 200},
InputBg: terminal.RGB{R: 30, G: 30, B: 50},
DirFg: terminal.RGB{R: 130, G: 170, B: 220},
FileFg: terminal.RGB{R: 200, G: 200, B: 200},
SymbolFg: terminal.RGB{R: 180, G: 220, B: 220},
SyntaxComment: terminal.RGB{R: 100, G: 110, B: 120},
SyntaxString: terminal.RGB{R: 180, G: 220, B: 140},
SyntaxKeyword: terminal.RGB{R: 180, G: 140, B: 220},
SyntaxType: terminal.RGB{R: 80, G: 200, B: 200},
SyntaxNumber: terminal.RGB{R: 220, G: 180, B: 120},
SyntaxSymbol: terminal.RGB{R: 220, G: 180, B: 80},
}
Bg: color.RGB{R: 20, G: 20, B: 30},
Fg: color.RGB{R: 200, G: 200, B: 200},
FocusBg: color.RGB{R: 30, G: 35, B: 45},
CursorBg: color.RGB{R: 50, G: 50, B: 70},
Selected: color.RGB{R: 80, G: 200, B: 80},
Unselected: color.RGB{R: 100, G: 100, B: 100},
Partial: color.RGB{R: 80, G: 160, B: 220},
Error: color.RGB{R: 255, G: 80, B: 80},
Warning: color.RGB{R: 255, G: 80, B: 80},
Border: color.RGB{R: 60, G: 80, B: 100},
HeaderBg: color.RGB{R: 40, G: 60, B: 90},
HeaderFg: color.RGB{R: 255, G: 255, B: 255},
StatusFg: color.RGB{R: 140, G: 140, B: 140},
HintFg: color.RGB{R: 100, G: 180, B: 200},
InputBg: color.RGB{R: 30, G: 30, B: 50},
DirFg: color.RGB{R: 130, G: 170, B: 220},
FileFg: color.RGB{R: 200, G: 200, B: 200},
SymbolFg: color.RGB{R: 180, G: 220, B: 220},
SyntaxComment: color.RGB{R: 100, G: 110, B: 120},
SyntaxString: color.RGB{R: 180, G: 220, B: 140},
SyntaxKeyword: color.RGB{R: 180, G: 140, B: 220},
SyntaxType: color.RGB{R: 80, G: 200, B: 200},
SyntaxNumber: color.RGB{R: 220, G: 180, B: 120},
SyntaxSymbol: color.RGB{R: 220, G: 180, B: 80},
}
+27 -23
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ToastPosition specifies where toast renders
type ToastPosition uint8
@@ -46,26 +49,26 @@ var ToastIcons = map[ToastSeverity]rune{
}
// ToastColors default colors per severity
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon terminal.RGB }{
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon color.RGB }{
ToastInfo: {
Fg: terminal.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 40, G: 40, B: 50},
Icon: terminal.RGB{R: 100, G: 150, B: 255},
Fg: color.RGB{R: 200, G: 200, B: 200},
Bg: color.RGB{R: 40, G: 40, B: 50},
Icon: color.RGB{R: 100, G: 150, B: 255},
},
ToastSuccess: {
Fg: terminal.RGB{R: 220, G: 255, B: 220},
Bg: terminal.RGB{R: 30, G: 60, B: 30},
Icon: terminal.RGB{R: 80, G: 220, B: 80},
Fg: color.RGB{R: 220, G: 255, B: 220},
Bg: color.RGB{R: 30, G: 60, B: 30},
Icon: color.RGB{R: 80, G: 220, B: 80},
},
ToastWarning: {
Fg: terminal.RGB{R: 255, G: 240, B: 200},
Bg: terminal.RGB{R: 60, G: 50, B: 20},
Icon: terminal.RGB{R: 255, G: 200, B: 60},
Fg: color.RGB{R: 255, G: 240, B: 200},
Bg: color.RGB{R: 60, G: 50, B: 20},
Icon: color.RGB{R: 255, G: 200, B: 60},
},
ToastError: {
Fg: terminal.RGB{R: 255, G: 220, B: 220},
Bg: terminal.RGB{R: 60, G: 25, B: 25},
Icon: terminal.RGB{R: 255, G: 80, B: 80},
Fg: color.RGB{R: 255, G: 220, B: 220},
Bg: color.RGB{R: 60, G: 25, B: 25},
Icon: color.RGB{R: 255, G: 80, B: 80},
},
}
@@ -81,9 +84,9 @@ type ToastOpts struct {
Padding int // Horizontal padding, default 1
MarginX int // Margin from edge for floating positions
MarginY int // Margin from edge for floating positions
CustomFg terminal.RGB
CustomBg terminal.RGB
CustomIcon terminal.RGB
CustomFg color.RGB
CustomBg color.RGB
CustomIcon color.RGB
}
// DefaultToastOpts returns sensible defaults
@@ -109,13 +112,13 @@ func (r Region) Toast(opts ToastOpts) Region {
// Resolve colors
fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon
if fg == (terminal.RGB{}) {
if fg == (color.RGB{}) {
fg = ToastColors[opts.Severity].Fg
}
if bg == (terminal.RGB{}) {
if bg == (color.RGB{}) {
bg = ToastColors[opts.Severity].Bg
}
if iconFg == (terminal.RGB{}) {
if iconFg == (color.RGB{}) {
iconFg = ToastColors[opts.Severity].Icon
}
@@ -218,7 +221,7 @@ func (r Region) Toast(opts ToastOpts) Region {
case ToastStyleShadow:
// Shadow offset
shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH)
shadowRegion.Fill(terminal.RGB{R: 10, G: 10, B: 15})
shadowRegion.Fill(color.RGB{R: 10, G: 10, B: 15})
toastRegion.BoxFilled(LineSingle, fg, bg)
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
}
@@ -226,7 +229,7 @@ func (r Region) Toast(opts ToastOpts) Region {
return toastRegion
}
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg terminal.RGB, _ int) {
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg color.RGB, _ int) {
if content.W < 1 || content.H < 1 {
return
}
@@ -298,4 +301,5 @@ func (t *ToastState) Show(opts ToastOpts, frames int) {
t.Opts = opts
t.FramesLeft = frames
t.Visible = true
}
}
+27 -23
View File
@@ -1,6 +1,9 @@
package tui
import "github.com/lixenwraith/terminal"
import (
"github.com/lixenwraith/color"
"github.com/lixenwraith/terminal"
)
// ExpandIcon chars
const (
@@ -30,20 +33,20 @@ type TreeNode struct {
Key string // Unique identifier for expansion state
Label string // Display text
Icon rune // Custom icon, 0 = auto (▶/▼ for expandable, • for leaf)
IconFg terminal.RGB
IconFg color.RGB
Expandable bool // Has children
Expanded bool // Currently expanded
Depth int // Nesting level (0 = root)
Check CheckState // Optional checkbox, CheckNone to skip
CheckFg terminal.RGB
CheckFg color.RGB
Style Style // Text styling
IsLast bool // Last sibling at this depth (for tree lines)
Data any // Application payload
Suffix string // Secondary text after label (e.g., "(5 files)")
SuffixStyle Style // Styling for suffix, zero = dimmed version of Style
Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none
BadgeFg terminal.RGB // Badge color
Suffix string // Secondary text after label (e.g., "(5 files)")
SuffixStyle Style // Styling for suffix, zero = dimmed version of Style
Badge rune // Icon between checkbox and label (e.g., '★'), 0 = none
BadgeFg color.RGB // Badge color
}
// ancestorHasMoreSiblings checks if there are more nodes at given depth after idx
@@ -59,12 +62,12 @@ func (r Region) ancestorHasMoreSiblings(nodes []TreeNode, idx, depth int) bool {
// TreeOpts configures tree rendering
type TreeOpts struct {
CursorBg terminal.RGB
DefaultBg terminal.RGB
CursorBg color.RGB
DefaultBg color.RGB
IndentWidth int // Cells per depth, default 2
IconWidth int // Width for icon column, default 2
LineMode TreeLineMode
LineFg terminal.RGB
LineFg color.RGB
}
// DefaultTreeOpts returns sensible defaults
@@ -73,7 +76,7 @@ func DefaultTreeOpts() TreeOpts {
IndentWidth: 2,
IconWidth: 2,
LineMode: TreeLinesNone,
LineFg: terminal.RGB{R: 80, G: 80, B: 100},
LineFg: color.RGB{R: 80, G: 80, B: 100},
}
}
@@ -94,7 +97,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
}
lineFg := opts.LineFg
if lineFg == (terminal.RGB{}) {
if lineFg == (color.RGB{}) {
lineFg = DefaultTheme.Border
}
@@ -114,7 +117,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
}
for x := 0; x < r.W; x++ {
r.Cell(x, y, ' ', terminal.RGB{}, bg, terminal.AttrNone)
r.Cell(x, y, ' ', color.RGB{}, bg, terminal.AttrNone)
}
x := 0
@@ -139,7 +142,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
icon = IconBullet
}
}
if iconFg == (terminal.RGB{}) {
if iconFg == (color.RGB{}) {
iconFg = lineFg
}
if x < r.W {
@@ -148,10 +151,10 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
x += iconW
// Checkbox
if node.Check != CheckNone || node.CheckFg != (terminal.RGB{}) {
if node.Check != CheckNone || node.CheckFg != (color.RGB{}) {
if x+3 <= r.W {
checkFg := node.CheckFg
if checkFg == (terminal.RGB{}) {
if checkFg == (color.RGB{}) {
checkFg = node.Style.Fg
}
var ch rune
@@ -176,7 +179,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
if node.Badge != 0 {
if x < r.W {
badgeFg := node.BadgeFg
if badgeFg == (terminal.RGB{}) {
if badgeFg == (color.RGB{}) {
badgeFg = node.Style.Fg
}
r.Cell(x, y, node.Badge, badgeFg, bg, terminal.AttrNone)
@@ -186,7 +189,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
// Label
style := node.Style
if style.Bg == (terminal.RGB{}) {
if style.Bg == (color.RGB{}) {
style.Bg = bg
}
@@ -214,15 +217,15 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
// Suffix
if suffix != "" {
suffixStyle := node.SuffixStyle
if suffixStyle.Fg == (terminal.RGB{}) {
if suffixStyle.Fg == (color.RGB{}) {
// Default: dimmed version of label style
suffixStyle.Fg = terminal.RGB{
suffixStyle.Fg = color.RGB{
R: node.Style.Fg.R / 2,
G: node.Style.Fg.G / 2,
B: node.Style.Fg.B / 2,
}
}
if suffixStyle.Bg == (terminal.RGB{}) {
if suffixStyle.Bg == (color.RGB{}) {
suffixStyle.Bg = bg
}
r.TextStyled(x, y, suffix, suffixStyle)
@@ -235,7 +238,7 @@ func (r Region) Tree(nodes []TreeNode, cursor, scroll int, opts TreeOpts) int {
}
// renderTreeLines draws connector lines for tree structure, returns x position after lines
func (r Region) renderTreeLines(y, startX int, node TreeNode, nodes []TreeNode, idx, scroll, indentW int, fg terminal.RGB, bg terminal.RGB) int {
func (r Region) renderTreeLines(y, startX int, node TreeNode, nodes []TreeNode, idx, scroll, indentW int, fg color.RGB, bg color.RGB) int {
x := startX
for d := 0; d < node.Depth; d++ {
@@ -334,4 +337,5 @@ func FindPrevSiblingIndex(nodes []TreeNode, idx int) int {
}
}
return -1
}
}