11package shared
22
3+ import (
4+ "fmt"
5+ "sort"
6+
7+ "gopkg.in/yaml.v3"
8+ )
9+
310// Config manages UI settings
411type 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
19147func 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