1package gust
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "strings"
8
9 "github.com/BurntSushi/toml"
10 "github.com/charmbracelet/lipgloss"
11)
12
13type Config struct {
14 Mode string `toml:"mode"`
15 Stream string `toml:"stream"`
16 Args []string
17 Package string `toml:"package"`
18 Summarized bool `toml:"summarized"`
19 Help bool `toml:"help"`
20 Styles Styles `toml:"styles"`
21 Signs Signs `toml:"signs"`
22 Context int `toml:"context"`
23}
24
25type Styles struct {
26 Mode Style `toml:"mode"`
27 Success Style `toml:"success"`
28 Running Style `toml:"running"`
29 Error Style `toml:"error"`
30 Warning Style `toml:"warning"`
31 Duration Style `toml:"duration"`
32 Separator Style `toml:"separator"`
33 File Style `toml:"file"`
34 Line Style `toml:"line"`
35 Stdout Style `toml:"stdout"`
36 Stderr Style `toml:"stderr"`
37 Context ContextStyles `toml:"context"`
38}
39
40type ContextStyles struct {
41 ActiveLine Style `toml:"activeLine"`
42 ActiveLineNr Style `toml:"activeLineNr"`
43 PassiveLine Style `toml:"passiveLine"`
44 PassiveLineNr Style `toml:"passiveLineNr"`
45}
46
47type Signs struct {
48 Error string `toml:"error"`
49 Warning string `toml:"warning"`
50 System string `toml:"system"`
51 HorizontalBar string `toml:"line"`
52 Separator string `toml:"separator"`
53 PassiveIndent string `toml:"passiveIndent"`
54 ActiveIndent string `toml:"activeIndent"`
55}
56
57type Style struct {
58 lipgloss.Style
59}
60
61type styleTOML struct {
62 Fg string `toml:"fg,omitempty"`
63 Bg string `toml:"bg,omitempty"`
64 Bold bool `toml:"bold,omitempty"`
65 Italic bool `toml:"italic,omitempty"`
66 Underline bool `toml:"underline,omitempty"`
67 Faint bool `toml:"faint,omitempty"`
68 Reverse bool `toml:"reverse,omitempty"`
69}
70
71func (s Style) MarshalTOML() ([]byte, error) {
72 fg := "5"
73 bg := ""
74
75 color, err := marshalColor(s.GetForeground())
76 if err != nil {
77 return nil, err
78 }
79 if color != nil {
80 fg = *color
81 }
82
83 color, err = marshalColor(s.GetBackground())
84 if err != nil {
85 return nil, err
86 }
87 if color != nil {
88 bg = *color
89 }
90
91 tomlData := styleTOML{
92 Fg: fg,
93 Bg: bg,
94 Bold: s.GetBold(),
95 Italic: s.GetItalic(),
96 Underline: s.GetUnderline(),
97 Faint: s.GetFaint(),
98 Reverse: s.GetReverse(),
99 }
100
101 var buf bytes.Buffer
102 err = toml.NewEncoder(&buf).Encode(tomlData)
103 if err != nil {
104 return nil, fmt.Errorf("failed to encode styleTOML to TOML: %w", err)
105 }
106
107 return buf.Bytes(), nil
108}
109
110func (s styleTOML) MarshalTOML() ([]byte, error) {
111 var parts []string
112
113 if s.Fg != "" {
114 parts = append(parts, fmt.Sprintf("fg = \"%s\"", s.Fg))
115 }
116 if s.Bg != "" {
117 parts = append(parts, fmt.Sprintf("bg = \"%s\"", s.Bg))
118 }
119 if s.Bold {
120 parts = append(parts, fmt.Sprintf("bold = %v", s.Bold))
121 }
122 if s.Italic {
123 parts = append(parts, fmt.Sprintf("italic = %v", s.Italic))
124 }
125 if s.Underline {
126 parts = append(parts, fmt.Sprintf("underline = %v", s.Underline))
127 }
128 if s.Faint {
129 parts = append(parts, fmt.Sprintf("faint = %v", s.Faint))
130 }
131 if s.Reverse {
132 parts = append(parts, fmt.Sprintf("reverse = %v", s.Reverse))
133 }
134
135 if len(parts) == 0 {
136 return nil, nil
137 }
138
139 return []byte("{ " + strings.Join(parts, ", ") + " }"), nil
140}
141
142func marshalColor(tc lipgloss.TerminalColor) (*string, error) {
143 if tc != nil {
144 switch color := tc.(type) {
145 case lipgloss.ANSIColor:
146 s := fmt.Sprintf("%d", color)
147 return &s, nil
148 case lipgloss.Color:
149 s := string(color)
150 return &s, nil
151 case lipgloss.NoColor:
152 return nil, nil
153 default:
154 return nil, fmt.Errorf("fg is not serializable, type: %T", color)
155 }
156 } else {
157 return nil, nil
158 }
159}
160
161func (s *Style) UnmarshalTOML(data any) error {
162 m := data.(map[string]any)
163 style := lipgloss.NewStyle()
164
165 if v, ok := m["fg"].(string); ok {
166 style = style.Foreground(lipgloss.Color(v))
167 }
168 if v, ok := m["bg"].(string); ok {
169 style = style.Background(lipgloss.Color(v))
170 }
171 if v, ok := m["bold"].(bool); ok {
172 style = style.Bold(v)
173 }
174 if v, ok := m["italic"].(bool); ok {
175 style = style.Italic(v)
176 }
177 if v, ok := m["underline"].(bool); ok {
178 style = style.Underline(v)
179 }
180 if v, ok := m["faint"].(bool); ok {
181 style = style.Faint(v)
182 }
183 if v, ok := m["reverse"].(bool); ok {
184 style = style.Reverse(v)
185 }
186 s.Style = style
187 return nil
188}
189
190func (s *Style) UnmarshalText(text []byte) error {
191 str := string(text)
192 tokens := strings.Fields(str)
193 style := lipgloss.NewStyle()
194 for i := 0; i < len(tokens); i++ {
195 switch tokens[i] {
196 case "bold":
197 style = style.Bold(true)
198 case "italic":
199 style = style.Italic(true)
200 case "underline":
201 style = style.Underline(true)
202 case "faint":
203 style = style.Faint(true)
204 case "on":
205 if i+1 < len(tokens) {
206 style = style.Background(lipgloss.Color(tokens[i+1]))
207 i++
208 }
209 default:
210 style = style.Foreground(lipgloss.Color(tokens[i]))
211 }
212 }
213 s.Style = style
214 return nil
215}
216
217func DefaultConfig() Config {
218 return Config{
219 Summarized: false,
220 Help: true,
221 Mode: "build",
222 Stream: "stdout",
223 Package: "./cmd/gust",
224 Context: 0,
225 Styles: Styles{
226 Mode: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("5"))},
227 Success: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("2"))},
228 Running: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))},
229 Error: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))},
230 Warning: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))},
231 File: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("4"))},
232 Duration: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))},
233 Separator: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))},
234 Line: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))},
235 Stdout: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))},
236 Stderr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))},
237 Context: ContextStyles{
238 ActiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))},
239 ActiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))},
240 PassiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))},
241 PassiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))},
242 },
243 },
244 Signs: Signs{
245 Error: "err",
246 Warning: "wrn",
247 System: "sys",
248 HorizontalBar: "─",
249 Separator: " · ",
250 PassiveIndent: " | ",
251 ActiveIndent: " > ",
252 },
253 }
254}
255
256func (cfg Config) Dump() {
257 err := toml.NewEncoder(os.Stdout).Encode(cfg)
258 if err != nil {
259 fmt.Fprintf(os.Stderr, "Failed to write config: %v", err)
260 }
261}
262
263func LoadConfig() Config {
264 cfg := DefaultConfig()
265
266 if _, err := toml.DecodeFile(".gust.toml", &cfg); err != nil {
267 fmt.Fprintf(os.Stderr, "%s", err.Error())
268 }
269
270 return cfg
271}
272
273func WithPackage(pkg string) func(*Config) {
274 return func(cfg *Config) {
275 cfg.Package = pkg
276 }
277}
278
279func WithMode(mode string) func(*Config) {
280 return func(cfg *Config) {
281 cfg.Mode = mode
282 }
283}
284
285func WithArgs(args []string) func(*Config) {
286 return func(cfg *Config) {
287 cfg.Args = args
288 }
289}