package gust import ( "bytes" "fmt" "os" "strings" "github.com/BurntSushi/toml" "github.com/charmbracelet/lipgloss" ) type Config struct { Mode string `toml:"mode"` Stream string `toml:"stream"` Args []string Package string `toml:"package"` Summarized bool `toml:"summarized"` Help bool `toml:"help"` Styles Styles `toml:"styles"` Signs Signs `toml:"signs"` Context int `toml:"context"` } type Styles struct { Mode Style `toml:"mode"` Success Style `toml:"success"` Running Style `toml:"running"` Error Style `toml:"error"` Warning Style `toml:"warning"` Duration Style `toml:"duration"` Separator Style `toml:"separator"` File Style `toml:"file"` Line Style `toml:"line"` Stdout Style `toml:"stdout"` Stderr Style `toml:"stderr"` Context ContextStyles `toml:"context"` } type ContextStyles struct { ActiveLine Style `toml:"activeLine"` ActiveLineNr Style `toml:"activeLineNr"` PassiveLine Style `toml:"passiveLine"` PassiveLineNr Style `toml:"passiveLineNr"` } type Signs struct { Error string `toml:"error"` Warning string `toml:"warning"` System string `toml:"system"` HorizontalBar string `toml:"line"` Separator string `toml:"separator"` PassiveIndent string `toml:"passiveIndent"` ActiveIndent string `toml:"activeIndent"` } type Style struct { lipgloss.Style } type styleTOML struct { Fg string `toml:"fg,omitempty"` Bg string `toml:"bg,omitempty"` Bold bool `toml:"bold,omitempty"` Italic bool `toml:"italic,omitempty"` Underline bool `toml:"underline,omitempty"` Faint bool `toml:"faint,omitempty"` Reverse bool `toml:"reverse,omitempty"` } func (s Style) MarshalTOML() ([]byte, error) { fg := "5" bg := "" color, err := marshalColor(s.GetForeground()) if err != nil { return nil, err } if color != nil { fg = *color } color, err = marshalColor(s.GetBackground()) if err != nil { return nil, err } if color != nil { bg = *color } tomlData := styleTOML{ Fg: fg, Bg: bg, Bold: s.GetBold(), Italic: s.GetItalic(), Underline: s.GetUnderline(), Faint: s.GetFaint(), Reverse: s.GetReverse(), } var buf bytes.Buffer err = toml.NewEncoder(&buf).Encode(tomlData) if err != nil { return nil, fmt.Errorf("failed to encode styleTOML to TOML: %w", err) } return buf.Bytes(), nil } func (s styleTOML) MarshalTOML() ([]byte, error) { var parts []string if s.Fg != "" { parts = append(parts, fmt.Sprintf("fg = \"%s\"", s.Fg)) } if s.Bg != "" { parts = append(parts, fmt.Sprintf("bg = \"%s\"", s.Bg)) } if s.Bold { parts = append(parts, fmt.Sprintf("bold = %v", s.Bold)) } if s.Italic { parts = append(parts, fmt.Sprintf("italic = %v", s.Italic)) } if s.Underline { parts = append(parts, fmt.Sprintf("underline = %v", s.Underline)) } if s.Faint { parts = append(parts, fmt.Sprintf("faint = %v", s.Faint)) } if s.Reverse { parts = append(parts, fmt.Sprintf("reverse = %v", s.Reverse)) } if len(parts) == 0 { return nil, nil } return []byte("{ " + strings.Join(parts, ", ") + " }"), nil } func marshalColor(tc lipgloss.TerminalColor) (*string, error) { if tc != nil { switch color := tc.(type) { case lipgloss.ANSIColor: s := fmt.Sprintf("%d", color) return &s, nil case lipgloss.Color: s := string(color) return &s, nil case lipgloss.NoColor: return nil, nil default: return nil, fmt.Errorf("fg is not serializable, type: %T", color) } } else { return nil, nil } } func (s *Style) UnmarshalTOML(data any) error { m := data.(map[string]any) style := lipgloss.NewStyle() if v, ok := m["fg"].(string); ok { style = style.Foreground(lipgloss.Color(v)) } if v, ok := m["bg"].(string); ok { style = style.Background(lipgloss.Color(v)) } if v, ok := m["bold"].(bool); ok { style = style.Bold(v) } if v, ok := m["italic"].(bool); ok { style = style.Italic(v) } if v, ok := m["underline"].(bool); ok { style = style.Underline(v) } if v, ok := m["faint"].(bool); ok { style = style.Faint(v) } if v, ok := m["reverse"].(bool); ok { style = style.Reverse(v) } s.Style = style return nil } func (s *Style) UnmarshalText(text []byte) error { str := string(text) tokens := strings.Fields(str) style := lipgloss.NewStyle() for i := 0; i < len(tokens); i++ { switch tokens[i] { case "bold": style = style.Bold(true) case "italic": style = style.Italic(true) case "underline": style = style.Underline(true) case "faint": style = style.Faint(true) case "on": if i+1 < len(tokens) { style = style.Background(lipgloss.Color(tokens[i+1])) i++ } default: style = style.Foreground(lipgloss.Color(tokens[i])) } } s.Style = style return nil } func DefaultConfig() Config { return Config{ Summarized: false, Help: true, Mode: "build", Stream: "stdout", Package: "./cmd/gust", Context: 0, Styles: Styles{ Mode: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("5"))}, Success: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("2"))}, Running: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))}, Error: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))}, Warning: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))}, File: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("4"))}, Duration: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))}, Separator: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, Line: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))}, Stdout: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))}, Stderr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))}, Context: ContextStyles{ ActiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, ActiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))}, PassiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, PassiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, }, }, Signs: Signs{ Error: "err", Warning: "wrn", System: "sys", HorizontalBar: "─", Separator: " · ", PassiveIndent: " | ", ActiveIndent: " > ", }, } } func (cfg Config) Dump() { err := toml.NewEncoder(os.Stdout).Encode(cfg) if err != nil { fmt.Fprintf(os.Stderr, "Failed to write config: %v", err) } } func LoadConfig() Config { cfg := DefaultConfig() if _, err := toml.DecodeFile(".gust.toml", &cfg); err != nil { fmt.Fprintf(os.Stderr, "%s", err.Error()) } return cfg } func WithPackage(pkg string) func(*Config) { return func(cfg *Config) { cfg.Package = pkg } } func WithMode(mode string) func(*Config) { return func(cfg *Config) { cfg.Mode = mode } } func WithArgs(args []string) func(*Config) { return func(cfg *Config) { cfg.Args = args } }