Change style package, new components, ...

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-08-18 20:10:32 +02:00
parent 499d61bddf
commit 21b574eb6f
56 changed files with 2741 additions and 363 deletions
+18
View File
@@ -0,0 +1,18 @@
# style
Shared Base16 theming for bubbletea/lipgloss TUIs. Loaded automatically on import from
`~/.config/ilovetui/config.yaml` (embedded default as fallback), exposed as the package-level
`style.S`.
- `S.NerdFonts` and `S.BorderType` come from the same config as the colors. This package has no
icon registry: components that want icons read `NerdFonts` and pick their own glyphs.
- `RenderWithTitle` renders a bordered box with a title embedded in the top border, following
`S.BorderType`.
- Never imports `charm.land/bubbles/v2/*`. Anything that needs to know about a specific component
belongs in `bubbles/` instead.
## Config
Copy [`default.yaml`](default.yaml) to `~/.config/ilovetui/config.yaml` and edit it.
See `examples/style`.
+57
View File
@@ -0,0 +1,57 @@
package style
import (
"strings"
"charm.land/lipgloss/v2"
)
var borderTypes = map[string]lipgloss.Border{
"rounded": lipgloss.RoundedBorder(),
"normal": lipgloss.NormalBorder(),
"thick": lipgloss.ThickBorder(),
"double": lipgloss.DoubleBorder(),
"hidden": lipgloss.HiddenBorder(),
"ascii": lipgloss.ASCIIBorder(),
}
func resolveBorderType(name string) lipgloss.Border {
if b, ok := borderTypes[strings.ToLower(strings.TrimSpace(name))]; ok {
return b
}
return lipgloss.RoundedBorder()
}
func ContentHeight(totalH int) int {
h := totalH - 2
if h < 0 {
return 0
}
return h
}
func RenderWithTitle(border lipgloss.Style, title, content string, width, height int) string {
boxH := height - 1
if contentH := boxH - 1; contentH > 0 {
lines := strings.Split(content, "\n")
if len(lines) > contentH {
content = strings.Join(lines[:contentH], "\n")
}
}
box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
titleW := lipgloss.Width(title)
b, _, _, _, _ := border.GetBorder()
topLeft, top, topRight := b.TopLeft, b.Top, b.TopRight
fillW := boxWidth - titleW - lipgloss.Width(topLeft) - lipgloss.Width(topRight) - 2
if fillW < 0 {
fillW = 0
}
bc := lipgloss.NewStyle().Foreground(border.GetBorderTopForeground())
topLine := bc.Render(topLeft+" ") + bc.Render(title) + bc.Render(" "+strings.Repeat(top, fillW)+topRight)
return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
}
+63
View File
@@ -0,0 +1,63 @@
package style
type colorsYAML struct {
Base00 string `yaml:"base00"`
Base01 string `yaml:"base01"`
Base02 string `yaml:"base02"`
Base03 string `yaml:"base03"`
Base04 string `yaml:"base04"`
Base05 string `yaml:"base05"`
Base06 string `yaml:"base06"`
Base07 string `yaml:"base07"`
Base08 string `yaml:"base08"`
Base09 string `yaml:"base09"`
Base0A string `yaml:"base0a"`
Base0B string `yaml:"base0b"`
Base0C string `yaml:"base0c"`
Base0D string `yaml:"base0d"`
Base0E string `yaml:"base0e"`
Base0F string `yaml:"base0f"`
}
type configYAML struct {
Colors colorsYAML `yaml:"colors"`
NerdFonts bool `yaml:"nerd_fonts"`
Border string `yaml:"border"`
}
func pickString(base, user string) string {
if user != "" {
return user
}
return base
}
func mergeConfig(base, user configYAML) configYAML {
return configYAML{
Colors: mergeColors(base.Colors, user.Colors),
NerdFonts: base.NerdFonts || user.NerdFonts,
Border: pickString(base.Border, user.Border),
}
}
func mergeColors(base, user colorsYAML) colorsYAML {
return colorsYAML{
Base00: pickString(base.Base00, user.Base00),
Base01: pickString(base.Base01, user.Base01),
Base02: pickString(base.Base02, user.Base02),
Base03: pickString(base.Base03, user.Base03),
Base04: pickString(base.Base04, user.Base04),
Base05: pickString(base.Base05, user.Base05),
Base06: pickString(base.Base06, user.Base06),
Base07: pickString(base.Base07, user.Base07),
Base08: pickString(base.Base08, user.Base08),
Base09: pickString(base.Base09, user.Base09),
Base0A: pickString(base.Base0A, user.Base0A),
Base0B: pickString(base.Base0B, user.Base0B),
Base0C: pickString(base.Base0C, user.Base0C),
Base0D: pickString(base.Base0D, user.Base0D),
Base0E: pickString(base.Base0E, user.Base0E),
Base0F: pickString(base.Base0F, user.Base0F),
}
}
+28
View File
@@ -0,0 +1,28 @@
# ilovetui default config
# Copy to ~/.config/ilovetui/config.yaml and edit to customize.
# Whether components may use Nerd Font glyphs (requires a patched font).
# Leave false for maximum terminal/font compatibility.
nerd_fonts: false
# Border style used by panels across all ilovetui-based TUIs.
# One of: rounded, normal, thick, double, hidden, ascii.
border: rounded
colors:
base00: "#110F12" # Background
base01: "#1C1920" # Lighter Background / Status Bars
base02: "#1D1A26" # Selection Background
base03: "#514D63" # Comments / Invisibles
base04: "#8E8AA0" # Dark Foreground / Status Bars
base05: "#C2BED6" # Default Foreground
base06: "#D8D5EA" # Light Foreground
base07: "#EAE7F7" # Light Background
base08: "#E07080" # Variables / Errors / Diff Deleted
base09: "#D49070" # Integers / Constants / Booleans
base0a: "#C4B060" # Classes / Warnings / Search Background
base0b: "#80B880" # Strings / Success / Diff Inserted
base0c: "#70B8C0" # Support / Regex / Escape Characters
base0d: "#9E97F8" # Functions / Methods / Headings / Accent
base0e: "#C090E8" # Keywords / Storage / Diff Changed
base0f: "#D080A0" # Embedded / Misc
+241
View File
@@ -0,0 +1,241 @@
package style
import (
"fmt"
"image/color"
"charm.land/glamour/v2/ansi"
)
func hexColor(c color.Color) *string {
r, g, b, _ := c.RGBA()
s := fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8)
return &s
}
func GlamourStyleConfig() ansi.StyleConfig {
str := func(s string) *string { return &s }
boolPtr := func(b bool) *bool { return &b }
uintPtr := func(u uint) *uint { return &u }
hex := hexColor
return ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockPrefix: "\n",
BlockSuffix: "\n",
Color: hex(S.Base05),
},
Margin: uintPtr(2),
},
BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: hex(S.Base03),
Italic: boolPtr(true),
},
Indent: uintPtr(1),
IndentToken: str("│ "),
},
List: ansi.StyleList{
LevelIndent: 2,
},
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
Color: hex(S.Base0D),
Bold: boolPtr(true),
},
},
H1: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: " ",
Suffix: " ",
Color: hex(S.Base07),
BackgroundColor: hex(S.Base0D),
Bold: boolPtr(true),
},
},
H2: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "## ",
Color: hex(S.Base0D),
Bold: boolPtr(true),
},
},
H3: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "### ",
Color: hex(S.Base0C),
},
},
H4: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "#### ",
Color: hex(S.Base0B),
},
},
H5: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "##### ",
Color: hex(S.Base09),
},
},
H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "###### ",
Color: hex(S.Base08),
Bold: boolPtr(false),
},
},
Strikethrough: ansi.StylePrimitive{
CrossedOut: boolPtr(true),
},
Emph: ansi.StylePrimitive{
Italic: boolPtr(true),
},
Strong: ansi.StylePrimitive{
Bold: boolPtr(true),
},
HorizontalRule: ansi.StylePrimitive{
Color: hex(S.Base03),
Format: "\n--------\n",
},
Item: ansi.StylePrimitive{
BlockPrefix: "• ",
},
Enumeration: ansi.StylePrimitive{
BlockPrefix: ". ",
},
Task: ansi.StyleTask{
Ticked: "[✓] ",
Unticked: "[ ] ",
},
Link: ansi.StylePrimitive{
Color: hex(S.Base0C),
Underline: boolPtr(true),
},
LinkText: ansi.StylePrimitive{
Color: hex(S.Base0D),
Bold: boolPtr(true),
},
Image: ansi.StylePrimitive{
Color: hex(S.Base0C),
Underline: boolPtr(true),
},
ImageText: ansi.StylePrimitive{
Color: hex(S.Base04),
Format: "Image: {{.text}} ->",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: " ",
Suffix: " ",
Color: hex(S.Base0B),
BackgroundColor: hex(S.Base01),
},
},
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: hex(S.Base04),
},
Margin: uintPtr(2),
},
Chroma: &ansi.Chroma{
Text: ansi.StylePrimitive{
Color: hex(S.Base05),
},
Error: ansi.StylePrimitive{
Color: hex(S.Base07),
BackgroundColor: hex(S.Base08),
},
Comment: ansi.StylePrimitive{
Color: hex(S.Base03),
Italic: boolPtr(true),
},
CommentPreproc: ansi.StylePrimitive{
Color: hex(S.Base09),
},
Keyword: ansi.StylePrimitive{
Color: hex(S.Base0E),
},
KeywordReserved: ansi.StylePrimitive{
Color: hex(S.Base0E),
},
KeywordNamespace: ansi.StylePrimitive{
Color: hex(S.Base0D),
},
KeywordType: ansi.StylePrimitive{
Color: hex(S.Base0A),
},
Operator: ansi.StylePrimitive{
Color: hex(S.Base05),
},
Punctuation: ansi.StylePrimitive{
Color: hex(S.Base05),
},
Name: ansi.StylePrimitive{
Color: hex(S.Base05),
},
NameBuiltin: ansi.StylePrimitive{
Color: hex(S.Base0D),
},
NameTag: ansi.StylePrimitive{
Color: hex(S.Base08),
},
NameAttribute: ansi.StylePrimitive{
Color: hex(S.Base0A),
},
NameClass: ansi.StylePrimitive{
Color: hex(S.Base0A),
Bold: boolPtr(true),
Underline: boolPtr(true),
},
NameConstant: ansi.StylePrimitive{
Color: hex(S.Base09),
},
NameDecorator: ansi.StylePrimitive{
Color: hex(S.Base0C),
},
NameFunction: ansi.StylePrimitive{
Color: hex(S.Base0D),
},
LiteralNumber: ansi.StylePrimitive{
Color: hex(S.Base09),
},
LiteralString: ansi.StylePrimitive{
Color: hex(S.Base0B),
},
LiteralStringEscape: ansi.StylePrimitive{
Color: hex(S.Base0C),
},
GenericDeleted: ansi.StylePrimitive{
Color: hex(S.Base08),
},
GenericEmph: ansi.StylePrimitive{
Italic: boolPtr(true),
},
GenericInserted: ansi.StylePrimitive{
Color: hex(S.Base0B),
},
GenericStrong: ansi.StylePrimitive{
Bold: boolPtr(true),
},
GenericSubheading: ansi.StylePrimitive{
Color: hex(S.Base04),
},
Background: ansi.StylePrimitive{
BackgroundColor: hex(S.Base01),
},
},
},
Table: ansi.StyleTable{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
},
DefinitionDescription: ansi.StylePrimitive{
BlockPrefix: "\n> ",
},
}
}
+114
View File
@@ -0,0 +1,114 @@
package style
import (
"image/color"
"strings"
"charm.land/lipgloss/v2"
)
type Styles struct {
Base00 color.Color
Base01 color.Color
Base02 color.Color
Base03 color.Color
Base04 color.Color
Base05 color.Color
Base06 color.Color
Base07 color.Color
Base08 color.Color
Base09 color.Color
Base0A color.Color
Base0B color.Color
Base0C color.Color
Base0D color.Color
Base0E color.Color
Base0F color.Color
Background color.Color
SubtleBg color.Color
Selection color.Color
Subtle color.Color
Muted color.Color
Text color.Color
Primary color.Color
Success color.Color
Warning color.Color
Error color.Color
Bold lipgloss.Style
Faint lipgloss.Style
NerdFonts bool
BorderType lipgloss.Border
Panel lipgloss.Style
PanelFocused lipgloss.Style
PagerDotActive string
PagerDotInactive string
}
func newStyles(c colorsYAML, nerdFonts bool, borderName string) Styles {
lc := func(s string) color.Color {
s = strings.TrimSpace(s)
if s != "" && s[0] != '#' {
s = "#" + s
}
return lipgloss.Color(s)
}
b00 := lc(c.Base00)
b01 := lc(c.Base01)
b02 := lc(c.Base02)
b03 := lc(c.Base03)
b04 := lc(c.Base04)
b05 := lc(c.Base05)
b06 := lc(c.Base06)
b07 := lc(c.Base07)
b08 := lc(c.Base08)
b09 := lc(c.Base09)
b0A := lc(c.Base0A)
b0B := lc(c.Base0B)
b0C := lc(c.Base0C)
b0D := lc(c.Base0D)
b0E := lc(c.Base0E)
b0F := lc(c.Base0F)
borderType := resolveBorderType(borderName)
return Styles{
Base00: b00, Base01: b01, Base02: b02, Base03: b03,
Base04: b04, Base05: b05, Base06: b06, Base07: b07,
Base08: b08, Base09: b09, Base0A: b0A, Base0B: b0B,
Base0C: b0C, Base0D: b0D, Base0E: b0E, Base0F: b0F,
Background: b00,
SubtleBg: b01,
Selection: b02,
Subtle: b03,
Muted: b04,
Text: b05,
Primary: b0D,
Success: b0B,
Warning: b09,
Error: b08,
Bold: lipgloss.NewStyle().Bold(true),
Faint: lipgloss.NewStyle().Foreground(b03).Faint(true),
NerdFonts: nerdFonts,
BorderType: borderType,
Panel: lipgloss.NewStyle().
Border(borderType).
BorderForeground(b03),
PanelFocused: lipgloss.NewStyle().
Border(borderType).
BorderForeground(b0D),
PagerDotActive: lipgloss.NewStyle().Foreground(b0D).SetString("•").String(),
PagerDotInactive: lipgloss.NewStyle().Foreground(b03).SetString("•").String(),
}
}
+84
View File
@@ -0,0 +1,84 @@
package style
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
//go:embed default.yaml
var DefaultConfig []byte
var S Styles
func init() {
path := DefaultConfigPath()
if data, err := os.ReadFile(path); err == nil {
if s, err := stylesFromBytes(data); err == nil {
S = s
return
}
}
s, _ := stylesFromBytes(DefaultConfig)
S = s
}
func Init() error {
path := DefaultConfigPath()
data, err := os.ReadFile(path)
if err != nil {
s, e := stylesFromBytes(DefaultConfig)
if e != nil {
return e
}
S = s
return nil
}
return InitFromBytes(data)
}
func InitFrom(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("style: read config: %w", err)
}
return InitFromBytes(data)
}
func InitFromBytes(data []byte) error {
s, err := stylesFromBytes(data)
if err != nil {
return err
}
S = s
return nil
}
func DefaultConfigPath() string {
return filepath.Join(configDir(), "ilovetui", "config.yaml")
}
func stylesFromBytes(data []byte) (Styles, error) {
var base configYAML
if err := yaml.Unmarshal(DefaultConfig, &base); err != nil {
return Styles{}, fmt.Errorf("style: parse default config: %w", err)
}
var user configYAML
if err := yaml.Unmarshal(data, &user); err != nil {
return Styles{}, fmt.Errorf("style: parse config: %w", err)
}
merged := mergeConfig(base, user)
return newStyles(merged.Colors, merged.NerdFonts, merged.Border), nil
}
func configDir() string {
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
return dir
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config")
}