Skip to content

Commit 9c3154d

Browse files
authored
Merge pull request #52 from servak/feat/add-theme-color
feat: Implement comprehensive theme system with harmonized color palette
2 parents 0f4a9d4 + cc30d32 commit 9c3154d

11 files changed

Lines changed: 745 additions & 204 deletions

File tree

internal/ui/shared/config.go

Lines changed: 333 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,346 @@
11
package shared
22

3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"gopkg.in/yaml.v3"
8+
)
9+
310
// Config manages UI settings
411
type Config struct {
5-
Title string `yaml:"-"`
6-
Border bool `yaml:"border"`
7-
EnableColors bool `yaml:"enable_colors"`
8-
Colors struct {
9-
Header string `yaml:"header"`
10-
Footer string `yaml:"footer"`
11-
Success string `yaml:"success"`
12-
Warning string `yaml:"warning"`
13-
Error string `yaml:"error"`
14-
ModalBorder string `yaml:"modal_border"`
15-
} `yaml:"colors"`
12+
Title string `yaml:"-"`
13+
Theme string `yaml:"theme"` // "light", "dark", "custom"
14+
Themes map[string]Theme `yaml:"themes"` // User-defined themes
15+
}
16+
17+
// UnmarshalYAML implements yaml.Unmarshaler to handle theme merging
18+
func (c *Config) UnmarshalYAML(value *yaml.Node) error {
19+
// Create a temporary structure to unmarshal into
20+
type tempConfig struct {
21+
Theme string `yaml:"theme"`
22+
Themes map[string]Theme `yaml:"themes"`
23+
}
24+
25+
var temp tempConfig
26+
if err := value.Decode(&temp); err != nil {
27+
return err
28+
}
29+
30+
// Apply theme if provided
31+
if temp.Theme != "" {
32+
c.Theme = temp.Theme
33+
}
34+
35+
// Merge themes if provided
36+
if temp.Themes != nil {
37+
if err := c.mergeThemes(temp.Themes); err != nil {
38+
return err
39+
}
40+
}
41+
42+
return nil
43+
}
44+
45+
// Theme represents a color theme with direct color values
46+
type Theme struct {
47+
// Base text colors
48+
Primary string `yaml:"primary"` // 主要テキスト色
49+
Secondary string `yaml:"secondary"` // 補助テキスト色
50+
51+
// Background colors
52+
Background string `yaml:"background"` // メイン背景色
53+
54+
// State colors
55+
Success string `yaml:"success"` // 成功状態
56+
Warning string `yaml:"warning"` // 警告状態
57+
Error string `yaml:"error"` // エラー状態
58+
59+
// Table colors
60+
TableHeader string `yaml:"table_header"` // テーブルヘッダー
61+
62+
// Interactive colors
63+
SelectionBg string `yaml:"selection_bg"` // 選択背景
64+
SelectionFg string `yaml:"selection_fg"` // 選択前景
65+
66+
// Detail colors
67+
Accent string `yaml:"accent"` // アクセント色(ラベル用)
68+
Separator string `yaml:"separator"` // 区切り線
69+
Timestamp string `yaml:"timestamp"` // タイムスタンプ
70+
}
71+
72+
// PredefinedThemes contains built-in color themes
73+
var PredefinedThemes = map[string]Theme{
74+
"dark": {
75+
Primary: "#ffffff",
76+
Secondary: "#cccccc",
77+
Background: "#000000",
78+
Success: "#5FFF87",
79+
Warning: "#ffff00",
80+
Error: "#FF5F87",
81+
TableHeader: "#ffff00",
82+
SelectionBg: "#006400",
83+
SelectionFg: "#ffffff",
84+
Accent: "#5FAFFF",
85+
Separator: "#666666",
86+
Timestamp: "#999999",
87+
},
88+
"light": {
89+
Primary: "#000000",
90+
Secondary: "#333333",
91+
Background: "#ffffff",
92+
Success: "#008000",
93+
Warning: "#ff8c00",
94+
Error: "#cc0000",
95+
TableHeader: "#000080",
96+
SelectionBg: "#add8e6",
97+
SelectionFg: "#000000",
98+
Accent: "#000080",
99+
Separator: "#666666",
100+
Timestamp: "#666666",
101+
},
102+
"monokai": {
103+
Primary: "#f8f8f2",
104+
Secondary: "#cfcfc2",
105+
Background: "#272822",
106+
Success: "#a6e22e",
107+
Warning: "#fd971f",
108+
Error: "#f92672",
109+
TableHeader: "#66d9ef",
110+
SelectionBg: "#49483e",
111+
SelectionFg: "#f8f8f2",
112+
Accent: "#ae81ff",
113+
Separator: "#75715e",
114+
Timestamp: "#75715e",
115+
},
116+
"nord": {
117+
Primary: "#d8dee9",
118+
Secondary: "#e5e9f0",
119+
Background: "#2e3440",
120+
Success: "#a3be8c",
121+
Warning: "#ebcb8b",
122+
Error: "#bf616a",
123+
TableHeader: "#81a1c1",
124+
SelectionBg: "#3b4252",
125+
SelectionFg: "#eceff4",
126+
Accent: "#88c0d0",
127+
Separator: "#4c566a",
128+
Timestamp: "#616e88",
129+
},
130+
"xoria256": {
131+
Primary: "#d0d0d0", // Normal text
132+
Secondary: "#9e9e9e", // LineNr, secondary text
133+
Background: "#1c1c1c", // Normal background
134+
Success: "#afdf87", // PreProc green
135+
Warning: "#ffffaf", // Constant yellow
136+
Error: "#df8787", // Special/Error red
137+
TableHeader: "#87afdf", // Statement blue
138+
SelectionBg: "#5f5f87", // Folded background
139+
SelectionFg: "#eeeeee", // Folded foreground
140+
Accent: "#dfafdf", // Identifier purple
141+
Separator: "#808080", // Comment gray
142+
Timestamp: "#dfaf87", // Number tan
143+
},
16144
}
17145

18146
// DefaultConfig returns default configuration
19147
func DefaultConfig() *Config {
20148
cfg := &Config{
21-
Title: "mping",
22-
Border: true,
23-
EnableColors: true, // Enable colors by default
149+
Title: "mping",
150+
Theme: "dark",
151+
Themes: make(map[string]Theme),
24152
}
25153

26-
// Use color names available in tview
27-
cfg.Colors.Header = "dodgerblue"
28-
cfg.Colors.Footer = "gray"
29-
cfg.Colors.Success = "green"
30-
cfg.Colors.Warning = "yellow"
31-
cfg.Colors.Error = "red"
32-
cfg.Colors.ModalBorder = "white"
154+
// Pre-populate with predefined themes so they can be overridden in YAML
155+
for name, theme := range PredefinedThemes {
156+
cfg.Themes[name] = theme
157+
}
33158

34159
return cfg
35-
}
160+
}
161+
162+
// GetTheme returns the appropriate theme based on config
163+
func (c *Config) GetTheme() *Theme {
164+
themeName := c.Theme
165+
166+
// Check user-defined themes first
167+
if theme, exists := c.Themes[themeName]; exists {
168+
return &theme
169+
}
170+
171+
// Check predefined themes
172+
if theme, exists := PredefinedThemes[themeName]; exists {
173+
return &theme
174+
}
175+
176+
// Fallback to dark theme
177+
if theme, exists := PredefinedThemes["dark"]; exists {
178+
return &theme
179+
}
180+
181+
// Ultimate fallback
182+
return &Theme{
183+
Primary: "#ffffff",
184+
Secondary: "#cccccc",
185+
Success: "#00ff00",
186+
Warning: "#ffff00",
187+
Error: "#ff0000",
188+
TableHeader: "#ffff00",
189+
SelectionBg: "#006400",
190+
SelectionFg: "#ffffff",
191+
Accent: "#00ffff",
192+
Separator: "#666666",
193+
Timestamp: "#999999",
194+
}
195+
}
196+
197+
// GetThemeList returns ordered list of available themes
198+
func GetThemeList() []string {
199+
themes := make([]string, 0, len(PredefinedThemes))
200+
for name := range PredefinedThemes {
201+
themes = append(themes, name)
202+
}
203+
sort.Strings(themes)
204+
return themes
205+
}
206+
207+
// GetAllThemeList returns ordered list of all available themes (predefined + user-defined)
208+
func (c *Config) GetAllThemeList() []string {
209+
themes := make([]string, 0, len(c.Themes))
210+
for name := range c.Themes {
211+
themes = append(themes, name)
212+
}
213+
sort.Strings(themes)
214+
return themes
215+
}
216+
217+
// CycleTheme cycles to the next theme in the available list
218+
func (c *Config) CycleTheme() {
219+
themes := c.GetAllThemeList()
220+
currentIndex := -1
221+
222+
// Find current theme index
223+
for i, theme := range themes {
224+
if theme == c.Theme {
225+
currentIndex = i
226+
break
227+
}
228+
}
229+
230+
// Move to next theme (cycle back to first if at end)
231+
nextIndex := (currentIndex + 1) % len(themes)
232+
c.Theme = themes[nextIndex]
233+
}
234+
235+
// mergeThemes merges user theme overrides with base themes
236+
func (c *Config) mergeThemes(userThemes map[string]Theme) error {
237+
for name, userTheme := range userThemes {
238+
// Check if this is a predefined theme (allows partial override)
239+
if _, isPredefined := PredefinedThemes[name]; isPredefined {
240+
// For predefined themes, allow partial override
241+
var baseTheme Theme
242+
if existing, exists := c.Themes[name]; exists {
243+
baseTheme = existing
244+
} else {
245+
baseTheme = PredefinedThemes[name]
246+
}
247+
248+
// Merge user theme with base theme
249+
merged := mergeTheme(baseTheme, userTheme)
250+
c.Themes[name] = merged
251+
} else {
252+
// For custom themes, require complete definition
253+
if err := validateTheme(userTheme); err != nil {
254+
return fmt.Errorf("custom theme '%s': %w", name, err)
255+
}
256+
c.Themes[name] = userTheme
257+
}
258+
}
259+
return nil
260+
}
261+
262+
// mergeTheme merges user theme settings with base theme, preserving non-empty values
263+
func mergeTheme(base, user Theme) Theme {
264+
result := base // Start with base theme
265+
266+
// Override with non-empty user values
267+
if user.Primary != "" {
268+
result.Primary = user.Primary
269+
}
270+
if user.Secondary != "" {
271+
result.Secondary = user.Secondary
272+
}
273+
if user.Background != "" {
274+
result.Background = user.Background
275+
}
276+
if user.Success != "" {
277+
result.Success = user.Success
278+
}
279+
if user.Warning != "" {
280+
result.Warning = user.Warning
281+
}
282+
if user.Error != "" {
283+
result.Error = user.Error
284+
}
285+
if user.TableHeader != "" {
286+
result.TableHeader = user.TableHeader
287+
}
288+
if user.SelectionBg != "" {
289+
result.SelectionBg = user.SelectionBg
290+
}
291+
if user.SelectionFg != "" {
292+
result.SelectionFg = user.SelectionFg
293+
}
294+
if user.Accent != "" {
295+
result.Accent = user.Accent
296+
}
297+
if user.Separator != "" {
298+
result.Separator = user.Separator
299+
}
300+
if user.Timestamp != "" {
301+
result.Timestamp = user.Timestamp
302+
}
303+
304+
return result
305+
}
306+
307+
// validateTheme checks if a theme has all required fields defined
308+
func validateTheme(theme Theme) error {
309+
if theme.Primary == "" {
310+
return fmt.Errorf("primary color is required")
311+
}
312+
if theme.Secondary == "" {
313+
return fmt.Errorf("secondary color is required")
314+
}
315+
if theme.Background == "" {
316+
return fmt.Errorf("background color is required")
317+
}
318+
if theme.Success == "" {
319+
return fmt.Errorf("success color is required")
320+
}
321+
if theme.Warning == "" {
322+
return fmt.Errorf("warning color is required")
323+
}
324+
if theme.Error == "" {
325+
return fmt.Errorf("error color is required")
326+
}
327+
if theme.TableHeader == "" {
328+
return fmt.Errorf("table_header color is required")
329+
}
330+
if theme.SelectionBg == "" {
331+
return fmt.Errorf("selection_bg color is required")
332+
}
333+
if theme.SelectionFg == "" {
334+
return fmt.Errorf("selection_fg color is required")
335+
}
336+
if theme.Accent == "" {
337+
return fmt.Errorf("accent color is required")
338+
}
339+
if theme.Separator == "" {
340+
return fmt.Errorf("separator color is required")
341+
}
342+
if theme.Timestamp == "" {
343+
return fmt.Errorf("timestamp color is required")
344+
}
345+
return nil
346+
}

0 commit comments

Comments
 (0)