refactor(theme): use ColorSchemeResolver in ThemeManager, remove legacy system.go
- commit
- 34930dd710d938bf30e4e510c961b82237229b6a
- author
- bnema · · 2025-12-29 20:51 UTC
- parent
- 2607ac5
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
3 files · +381 -99
M internal/ui/theme/manager.go +39 -21
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 8 | 9 | "github.com/bnema/dumber/internal/logging" | |
| 9 | 10 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| @@ -12,18 +13,30 @@ import ( | |||
| 12 | 13 | ||
| 13 | 14 | // Manager handles theme state and CSS application. | |
| 14 | 15 | type Manager struct { | |
| 15 | - | scheme string // "light", "dark", "system" | |
| 16 | - | prefersDark bool // Resolved dark mode preference | |
| 17 | - | lightPalette Palette // Light theme colors | |
| 18 | - | darkPalette Palette // Dark theme colors | |
| 19 | - | uiScale float64 // UI scaling factor (1.0 = 100%) | |
| 20 | - | fonts FontConfig | |
| 21 | - | modeColors ModeColors // Modal mode indicator colors | |
| 22 | - | cssProvider *gtk.CssProvider | |
| 16 | + | scheme string // "light", "dark", "system" | |
| 17 | + | prefersDark bool // Resolved dark mode preference | |
| 18 | + | lightPalette Palette // Light theme colors | |
| 19 | + | darkPalette Palette // Dark theme colors | |
| 20 | + | uiScale float64 // UI scaling factor (1.0 = 100%) | |
| 21 | + | fonts FontConfig | |
| 22 | + | modeColors ModeColors // Modal mode indicator colors | |
| 23 | + | cssProvider *gtk.CssProvider | |
| 24 | + | colorResolver port.ColorSchemeResolver // Resolver for dynamic detection | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // ManagerOption is a functional option for configuring Manager. | |
| 28 | + | type ManagerOption func(*Manager) | |
| 29 | + | ||
| 30 | + | // WithResolver configures the Manager to use a ColorSchemeResolver. | |
| 31 | + | func WithResolver(resolver port.ColorSchemeResolver) ManagerOption { | |
| 32 | + | return func(m *Manager) { | |
| 33 | + | m.colorResolver = resolver | |
| 34 | + | } | |
| 23 | 35 | } | |
| 24 | 36 | ||
| 25 | 37 | // NewManager creates a new theme manager from configuration. | |
| 26 | - | func NewManager(ctx context.Context, cfg *config.Config) *Manager { | |
| 38 | + | // The ColorSchemeResolver is required for proper color scheme detection. | |
| 39 | + | func NewManager(ctx context.Context, cfg *config.Config, resolver port.ColorSchemeResolver) *Manager { | |
| 27 | 40 | log := logging.FromContext(ctx) | |
| 28 | 41 | ||
| 29 | 42 | // Determine color scheme preference | |
| @@ -32,8 +45,9 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager { | |||
| 32 | 45 | scheme = cfg.Appearance.ColorScheme | |
| 33 | 46 | } | |
| 34 | 47 | ||
| 35 | - | // Resolve whether we should use dark mode | |
| 36 | - | prefersDark := ResolveColorScheme(scheme) | |
| 48 | + | // Resolve whether we should use dark mode via resolver | |
| 49 | + | pref := resolver.Resolve() | |
| 50 | + | prefersDark := pref.PrefersDark | |
| 37 | 51 | ||
| 38 | 52 | // Build palettes from config or defaults | |
| 39 | 53 | var lightPalette, darkPalette Palette | |
| @@ -69,13 +83,14 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager { | |||
| 69 | 83 | Msg("theme manager initialized") | |
| 70 | 84 | ||
| 71 | 85 | return &Manager{ | |
| 72 | - | scheme: scheme, | |
| 73 | - | prefersDark: prefersDark, | |
| 74 | - | lightPalette: lightPalette, | |
| 75 | - | darkPalette: darkPalette, | |
| 76 | - | uiScale: uiScale, | |
| 77 | - | fonts: fonts, | |
| 78 | - | modeColors: modeColors, | |
| 86 | + | scheme: scheme, | |
| 87 | + | prefersDark: prefersDark, | |
| 88 | + | lightPalette: lightPalette, | |
| 89 | + | darkPalette: darkPalette, | |
| 90 | + | uiScale: uiScale, | |
| 91 | + | fonts: fonts, | |
| 92 | + | modeColors: modeColors, | |
| 93 | + | colorResolver: resolver, | |
| 79 | 94 | } | |
| 80 | 95 | } | |
| 81 | 96 | ||
| @@ -178,11 +193,13 @@ func (m *Manager) SetColorScheme(ctx context.Context, scheme string, display *gd | |||
| 178 | 193 | log := logging.FromContext(ctx) | |
| 179 | 194 | ||
| 180 | 195 | m.scheme = scheme | |
| 181 | - | m.prefersDark = ResolveColorScheme(scheme) | |
| 196 | + | pref := m.colorResolver.Refresh() | |
| 197 | + | m.prefersDark = pref.PrefersDark | |
| 182 | 198 | ||
| 183 | 199 | log.Info(). | |
| 184 | 200 | Str("scheme", scheme). | |
| 185 | 201 | Bool("prefers_dark", m.prefersDark). | |
| 202 | + | Str("source", pref.Source). | |
| 186 | 203 | Msg("color scheme changed") | |
| 187 | 204 | ||
| 188 | 205 | // Re-apply CSS if display is available | |
| @@ -199,13 +216,14 @@ func (m *Manager) UpdateFromConfig(ctx context.Context, cfg *config.Config, disp | |||
| 199 | 216 | return | |
| 200 | 217 | } | |
| 201 | 218 | ||
| 202 | - | // Update scheme and resolve prefersDark | |
| 219 | + | // Update scheme and resolve prefersDark via resolver | |
| 203 | 220 | scheme := "system" | |
| 204 | 221 | if cfg.Appearance.ColorScheme != "" { | |
| 205 | 222 | scheme = cfg.Appearance.ColorScheme | |
| 206 | 223 | } | |
| 207 | 224 | m.scheme = scheme | |
| 208 | - | m.prefersDark = ResolveColorScheme(scheme) | |
| 225 | + | pref := m.colorResolver.Refresh() | |
| 226 | + | m.prefersDark = pref.PrefersDark | |
| 209 | 227 | ||
| 210 | 228 | // Update palettes | |
| 211 | 229 | m.lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false) | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | "github.com/bnema/dumber/internal/infrastructure/config" | 8 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 8 | "github.com/bnema/dumber/internal/logging" | 9 | "github.com/bnema/dumber/internal/logging" | ||
| 9 | "github.com/jwijenbergh/puregotk/v4/gdk" | 10 | "github.com/jwijenbergh/puregotk/v4/gdk" | ||
| @@ -12,18 +13,30 @@ import ( | |||||
| 12 | 13 | ||||
| 13 | // Manager handles theme state and CSS application. | 14 | // Manager handles theme state and CSS application. | ||
| 14 | type Manager struct { | 15 | type Manager struct { | ||
| 15 | - | scheme string // "light", "dark", "system" | 16 | + | scheme string // "light", "dark", "system" |
| 16 | - | prefersDark bool // Resolved dark mode preference | 17 | + | prefersDark bool // Resolved dark mode preference |
| 17 | - | lightPalette Palette // Light theme colors | 18 | + | lightPalette Palette // Light theme colors |
| 18 | - | darkPalette Palette // Dark theme colors | 19 | + | darkPalette Palette // Dark theme colors |
| 19 | - | uiScale float64 // UI scaling factor (1.0 = 100%) | 20 | + | uiScale float64 // UI scaling factor (1.0 = 100%) |
| 20 | - | fonts FontConfig | 21 | + | fonts FontConfig |
| 21 | - | modeColors ModeColors // Modal mode indicator colors | 22 | + | modeColors ModeColors // Modal mode indicator colors |
| 22 | - | cssProvider *gtk.CssProvider | 23 | + | cssProvider *gtk.CssProvider |
| 24 | + | colorResolver port.ColorSchemeResolver // Resolver for dynamic detection | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // ManagerOption is a functional option for configuring Manager. | |||
| 28 | + | type ManagerOption func(*Manager) | |||
| 29 | + | ||||
| 30 | + | // WithResolver configures the Manager to use a ColorSchemeResolver. | |||
| 31 | + | func WithResolver(resolver port.ColorSchemeResolver) ManagerOption { | |||
| 32 | + | return func(m *Manager) { | |||
| 33 | + | m.colorResolver = resolver | |||
| 34 | + | } | |||
| 23 | } | 35 | } | ||
| 24 | 36 | ||||
| 25 | // NewManager creates a new theme manager from configuration. | 37 | // NewManager creates a new theme manager from configuration. | ||
| 26 | - | func NewManager(ctx context.Context, cfg *config.Config) *Manager { | 38 | + | // The ColorSchemeResolver is required for proper color scheme detection. |
| 39 | + | func NewManager(ctx context.Context, cfg *config.Config, resolver port.ColorSchemeResolver) *Manager { | |||
| 27 | log := logging.FromContext(ctx) | 40 | log := logging.FromContext(ctx) | ||
| 28 | 41 | ||||
| 29 | // Determine color scheme preference | 42 | // Determine color scheme preference | ||
| @@ -32,8 +45,9 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager { | |||||
| 32 | scheme = cfg.Appearance.ColorScheme | 45 | scheme = cfg.Appearance.ColorScheme | ||
| 33 | } | 46 | } | ||
| 34 | 47 | ||||
| 35 | - | // Resolve whether we should use dark mode | 48 | + | // Resolve whether we should use dark mode via resolver |
| 36 | - | prefersDark := ResolveColorScheme(scheme) | 49 | + | pref := resolver.Resolve() |
| 50 | + | prefersDark := pref.PrefersDark | |||
| 37 | 51 | ||||
| 38 | // Build palettes from config or defaults | 52 | // Build palettes from config or defaults | ||
| 39 | var lightPalette, darkPalette Palette | 53 | var lightPalette, darkPalette Palette | ||
| @@ -69,13 +83,14 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager { | |||||
| 69 | Msg("theme manager initialized") | 83 | Msg("theme manager initialized") | ||
| 70 | 84 | ||||
| 71 | return &Manager{ | 85 | return &Manager{ | ||
| 72 | - | scheme: scheme, | 86 | + | scheme: scheme, |
| 73 | - | prefersDark: prefersDark, | 87 | + | prefersDark: prefersDark, |
| 74 | - | lightPalette: lightPalette, | 88 | + | lightPalette: lightPalette, |
| 75 | - | darkPalette: darkPalette, | 89 | + | darkPalette: darkPalette, |
| 76 | - | uiScale: uiScale, | 90 | + | uiScale: uiScale, |
| 77 | - | fonts: fonts, | 91 | + | fonts: fonts, |
| 78 | - | modeColors: modeColors, | 92 | + | modeColors: modeColors, |
| 93 | + | colorResolver: resolver, | |||
| 79 | } | 94 | } | ||
| 80 | } | 95 | } | ||
| 81 | 96 | ||||
| @@ -178,11 +193,13 @@ func (m *Manager) SetColorScheme(ctx context.Context, scheme string, display *gd | |||||
| 178 | log := logging.FromContext(ctx) | 193 | log := logging.FromContext(ctx) | ||
| 179 | 194 | ||||
| 180 | m.scheme = scheme | 195 | m.scheme = scheme | ||
| 181 | - | m.prefersDark = ResolveColorScheme(scheme) | 196 | + | pref := m.colorResolver.Refresh() |
| 197 | + | m.prefersDark = pref.PrefersDark | |||
| 182 | 198 | ||||
| 183 | log.Info(). | 199 | log.Info(). | ||
| 184 | Str("scheme", scheme). | 200 | Str("scheme", scheme). | ||
| 185 | Bool("prefers_dark", m.prefersDark). | 201 | Bool("prefers_dark", m.prefersDark). | ||
| 202 | + | Str("source", pref.Source). | |||
| 186 | Msg("color scheme changed") | 203 | Msg("color scheme changed") | ||
| 187 | 204 | ||||
| 188 | // Re-apply CSS if display is available | 205 | // Re-apply CSS if display is available | ||
| @@ -199,13 +216,14 @@ func (m *Manager) UpdateFromConfig(ctx context.Context, cfg *config.Config, disp | |||||
| 199 | return | 216 | return | ||
| 200 | } | 217 | } | ||
| 201 | 218 | ||||
| 202 | - | // Update scheme and resolve prefersDark | 219 | + | // Update scheme and resolve prefersDark via resolver |
| 203 | scheme := "system" | 220 | scheme := "system" | ||
| 204 | if cfg.Appearance.ColorScheme != "" { | 221 | if cfg.Appearance.ColorScheme != "" { | ||
| 205 | scheme = cfg.Appearance.ColorScheme | 222 | scheme = cfg.Appearance.ColorScheme | ||
| 206 | } | 223 | } | ||
| 207 | m.scheme = scheme | 224 | m.scheme = scheme | ||
| 208 | - | m.prefersDark = ResolveColorScheme(scheme) | 225 | + | pref := m.colorResolver.Refresh() |
| 226 | + | m.prefersDark = pref.PrefersDark | |||
| 209 | 227 | ||||
| 210 | // Update palettes | 228 | // Update palettes | ||
| 211 | m.lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false) | 229 | m.lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false) | ||
A internal/ui/theme/manager_test.go +342 -0
| @@ -0,0 +1,342 @@ | |||
| 1 | + | package theme | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func TestNewManager_DarkMode(t *testing.T) { | |
| 15 | + | ctx := context.Background() | |
| 16 | + | cfg := &config.Config{ | |
| 17 | + | Appearance: config.AppearanceConfig{ | |
| 18 | + | ColorScheme: "default", | |
| 19 | + | }, | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 23 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 24 | + | PrefersDark: true, | |
| 25 | + | Source: "test", | |
| 26 | + | }) | |
| 27 | + | ||
| 28 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 29 | + | ||
| 30 | + | assert.True(t, manager.PrefersDark()) | |
| 31 | + | assert.Equal(t, "default", manager.scheme) | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func TestNewManager_LightMode(t *testing.T) { | |
| 35 | + | ctx := context.Background() | |
| 36 | + | cfg := &config.Config{ | |
| 37 | + | Appearance: config.AppearanceConfig{ | |
| 38 | + | ColorScheme: "default", | |
| 39 | + | }, | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 43 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 44 | + | PrefersDark: false, | |
| 45 | + | Source: "test", | |
| 46 | + | }) | |
| 47 | + | ||
| 48 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 49 | + | ||
| 50 | + | assert.False(t, manager.PrefersDark()) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func TestNewManager_WithNilConfig(t *testing.T) { | |
| 54 | + | ctx := context.Background() | |
| 55 | + | ||
| 56 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 57 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 58 | + | PrefersDark: true, | |
| 59 | + | Source: "fallback", | |
| 60 | + | }) | |
| 61 | + | ||
| 62 | + | manager := NewManager(ctx, nil, mockResolver) | |
| 63 | + | ||
| 64 | + | require.NotNil(t, manager) | |
| 65 | + | assert.True(t, manager.PrefersDark()) | |
| 66 | + | assert.Equal(t, "system", manager.scheme) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func TestNewManager_UsesConfigScheme(t *testing.T) { | |
| 70 | + | ctx := context.Background() | |
| 71 | + | cfg := &config.Config{ | |
| 72 | + | Appearance: config.AppearanceConfig{ | |
| 73 | + | ColorScheme: "prefer-dark", | |
| 74 | + | }, | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 78 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 79 | + | PrefersDark: true, | |
| 80 | + | Source: "config", | |
| 81 | + | }) | |
| 82 | + | ||
| 83 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 84 | + | ||
| 85 | + | assert.Equal(t, "prefer-dark", manager.scheme) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func TestManager_GetCurrentPalette_Dark(t *testing.T) { | |
| 89 | + | ctx := context.Background() | |
| 90 | + | cfg := &config.Config{} | |
| 91 | + | ||
| 92 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 93 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 94 | + | PrefersDark: true, | |
| 95 | + | Source: "test", | |
| 96 | + | }) | |
| 97 | + | ||
| 98 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 99 | + | palette := manager.GetCurrentPalette() | |
| 100 | + | ||
| 101 | + | // Dark palette should be returned | |
| 102 | + | assert.Equal(t, manager.GetDarkPalette(), palette) | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func TestManager_GetCurrentPalette_Light(t *testing.T) { | |
| 106 | + | ctx := context.Background() | |
| 107 | + | cfg := &config.Config{} | |
| 108 | + | ||
| 109 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 110 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 111 | + | PrefersDark: false, | |
| 112 | + | Source: "test", | |
| 113 | + | }) | |
| 114 | + | ||
| 115 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 116 | + | palette := manager.GetCurrentPalette() | |
| 117 | + | ||
| 118 | + | // Light palette should be returned | |
| 119 | + | assert.Equal(t, manager.GetLightPalette(), palette) | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | func TestManager_SetColorScheme(t *testing.T) { | |
| 123 | + | ctx := context.Background() | |
| 124 | + | cfg := &config.Config{} | |
| 125 | + | ||
| 126 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 127 | + | // Initial resolve call | |
| 128 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 129 | + | PrefersDark: true, | |
| 130 | + | Source: "test", | |
| 131 | + | }) | |
| 132 | + | // Refresh call when SetColorScheme is called | |
| 133 | + | mockResolver.EXPECT().Refresh().Return(port.ColorSchemePreference{ | |
| 134 | + | PrefersDark: false, | |
| 135 | + | Source: "config", | |
| 136 | + | }) | |
| 137 | + | ||
| 138 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 139 | + | assert.True(t, manager.PrefersDark()) | |
| 140 | + | ||
| 141 | + | // Change scheme - this calls Refresh on resolver | |
| 142 | + | manager.SetColorScheme(ctx, "prefer-light", nil) | |
| 143 | + | ||
| 144 | + | assert.False(t, manager.PrefersDark()) | |
| 145 | + | assert.Equal(t, "prefer-light", manager.scheme) | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | func TestManager_UpdateFromConfig(t *testing.T) { | |
| 149 | + | ctx := context.Background() | |
| 150 | + | cfg := &config.Config{ | |
| 151 | + | Appearance: config.AppearanceConfig{ | |
| 152 | + | ColorScheme: "default", | |
| 153 | + | }, | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 157 | + | // Initial resolve | |
| 158 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 159 | + | PrefersDark: true, | |
| 160 | + | Source: "test", | |
| 161 | + | }) | |
| 162 | + | // Refresh when UpdateFromConfig is called | |
| 163 | + | mockResolver.EXPECT().Refresh().Return(port.ColorSchemePreference{ | |
| 164 | + | PrefersDark: false, | |
| 165 | + | Source: "config", | |
| 166 | + | }) | |
| 167 | + | ||
| 168 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 169 | + | assert.True(t, manager.PrefersDark()) | |
| 170 | + | ||
| 171 | + | // Update with new config | |
| 172 | + | newCfg := &config.Config{ | |
| 173 | + | Appearance: config.AppearanceConfig{ | |
| 174 | + | ColorScheme: "prefer-light", | |
| 175 | + | }, | |
| 176 | + | } | |
| 177 | + | manager.UpdateFromConfig(ctx, newCfg, nil) | |
| 178 | + | ||
| 179 | + | assert.False(t, manager.PrefersDark()) | |
| 180 | + | assert.Equal(t, "prefer-light", manager.scheme) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func TestManager_UpdateFromConfig_NilConfig(t *testing.T) { | |
| 184 | + | ctx := context.Background() | |
| 185 | + | cfg := &config.Config{} | |
| 186 | + | ||
| 187 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 188 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 189 | + | PrefersDark: true, | |
| 190 | + | Source: "test", | |
| 191 | + | }) | |
| 192 | + | ||
| 193 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 194 | + | initialScheme := manager.scheme | |
| 195 | + | ||
| 196 | + | // UpdateFromConfig with nil should be a no-op | |
| 197 | + | manager.UpdateFromConfig(ctx, nil, nil) | |
| 198 | + | ||
| 199 | + | assert.Equal(t, initialScheme, manager.scheme) | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | func TestManager_GetWebUIThemeCSS(t *testing.T) { | |
| 203 | + | ctx := context.Background() | |
| 204 | + | cfg := &config.Config{} | |
| 205 | + | ||
| 206 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 207 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 208 | + | PrefersDark: true, | |
| 209 | + | Source: "test", | |
| 210 | + | }) | |
| 211 | + | ||
| 212 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 213 | + | css := manager.GetWebUIThemeCSS() | |
| 214 | + | ||
| 215 | + | // Should contain both light and dark variables | |
| 216 | + | assert.Contains(t, css, ":root{") | |
| 217 | + | assert.Contains(t, css, ".dark{") | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | func TestManager_GetBackgroundRGBA(t *testing.T) { | |
| 221 | + | ctx := context.Background() | |
| 222 | + | cfg := &config.Config{} | |
| 223 | + | ||
| 224 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 225 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 226 | + | PrefersDark: true, | |
| 227 | + | Source: "test", | |
| 228 | + | }) | |
| 229 | + | ||
| 230 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 231 | + | r, g, b, a := manager.GetBackgroundRGBA() | |
| 232 | + | ||
| 233 | + | // Should return valid RGBA values (0-1 range) | |
| 234 | + | assert.GreaterOrEqual(t, r, float32(0)) | |
| 235 | + | assert.LessOrEqual(t, r, float32(1)) | |
| 236 | + | assert.GreaterOrEqual(t, g, float32(0)) | |
| 237 | + | assert.LessOrEqual(t, g, float32(1)) | |
| 238 | + | assert.GreaterOrEqual(t, b, float32(0)) | |
| 239 | + | assert.LessOrEqual(t, b, float32(1)) | |
| 240 | + | assert.GreaterOrEqual(t, a, float32(0)) | |
| 241 | + | assert.LessOrEqual(t, a, float32(1)) | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | func TestManager_CustomPalettes(t *testing.T) { | |
| 245 | + | ctx := context.Background() | |
| 246 | + | cfg := &config.Config{ | |
| 247 | + | Appearance: config.AppearanceConfig{ | |
| 248 | + | LightPalette: config.ColorPalette{ | |
| 249 | + | Background: "#ffffff", | |
| 250 | + | Text: "#000000", | |
| 251 | + | }, | |
| 252 | + | DarkPalette: config.ColorPalette{ | |
| 253 | + | Background: "#000000", | |
| 254 | + | Text: "#ffffff", | |
| 255 | + | }, | |
| 256 | + | }, | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 260 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 261 | + | PrefersDark: true, | |
| 262 | + | Source: "test", | |
| 263 | + | }) | |
| 264 | + | ||
| 265 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 266 | + | ||
| 267 | + | lightPalette := manager.GetLightPalette() | |
| 268 | + | darkPalette := manager.GetDarkPalette() | |
| 269 | + | ||
| 270 | + | assert.Equal(t, "#ffffff", lightPalette.Background) | |
| 271 | + | assert.Equal(t, "#000000", lightPalette.Text) | |
| 272 | + | assert.Equal(t, "#000000", darkPalette.Background) | |
| 273 | + | assert.Equal(t, "#ffffff", darkPalette.Text) | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func TestManager_UIScale(t *testing.T) { | |
| 277 | + | ctx := context.Background() | |
| 278 | + | cfg := &config.Config{ | |
| 279 | + | DefaultUIScale: 1.5, | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 283 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 284 | + | PrefersDark: true, | |
| 285 | + | Source: "test", | |
| 286 | + | }) | |
| 287 | + | ||
| 288 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 289 | + | ||
| 290 | + | // UI scale should be stored (we can't directly access it, but we can verify | |
| 291 | + | // the manager was created without error) | |
| 292 | + | require.NotNil(t, manager) | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | func TestManager_Fonts(t *testing.T) { | |
| 296 | + | ctx := context.Background() | |
| 297 | + | cfg := &config.Config{ | |
| 298 | + | Appearance: config.AppearanceConfig{ | |
| 299 | + | SansFont: "Inter", | |
| 300 | + | MonospaceFont: "JetBrains Mono", | |
| 301 | + | }, | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 305 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 306 | + | PrefersDark: true, | |
| 307 | + | Source: "test", | |
| 308 | + | }) | |
| 309 | + | ||
| 310 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 311 | + | ||
| 312 | + | // Fonts should be stored (manager creation should succeed) | |
| 313 | + | require.NotNil(t, manager) | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | func TestManager_ModeColors(t *testing.T) { | |
| 317 | + | ctx := context.Background() | |
| 318 | + | cfg := &config.Config{ | |
| 319 | + | Workspace: config.WorkspaceConfig{ | |
| 320 | + | Styling: config.WorkspaceStylingConfig{ | |
| 321 | + | PaneModeColor: "#ff0000", | |
| 322 | + | TabModeColor: "#00ff00", | |
| 323 | + | SessionModeColor: "#0000ff", | |
| 324 | + | ResizeModeColor: "#ffff00", | |
| 325 | + | }, | |
| 326 | + | }, | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |
| 330 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |
| 331 | + | PrefersDark: true, | |
| 332 | + | Source: "test", | |
| 333 | + | }) | |
| 334 | + | ||
| 335 | + | manager := NewManager(ctx, cfg, mockResolver) | |
| 336 | + | modeColors := manager.GetModeColors() | |
| 337 | + | ||
| 338 | + | assert.Equal(t, "#ff0000", modeColors.PaneMode) | |
| 339 | + | assert.Equal(t, "#00ff00", modeColors.TabMode) | |
| 340 | + | assert.Equal(t, "#0000ff", modeColors.SessionMode) | |
| 341 | + | assert.Equal(t, "#ffff00", modeColors.ResizeMode) | |
| 342 | + | } | |
| @@ -0,0 +1,342 @@ | |||||
| 1 | + | package theme | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func TestNewManager_DarkMode(t *testing.T) { | |||
| 15 | + | ctx := context.Background() | |||
| 16 | + | cfg := &config.Config{ | |||
| 17 | + | Appearance: config.AppearanceConfig{ | |||
| 18 | + | ColorScheme: "default", | |||
| 19 | + | }, | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 23 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 24 | + | PrefersDark: true, | |||
| 25 | + | Source: "test", | |||
| 26 | + | }) | |||
| 27 | + | ||||
| 28 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 29 | + | ||||
| 30 | + | assert.True(t, manager.PrefersDark()) | |||
| 31 | + | assert.Equal(t, "default", manager.scheme) | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func TestNewManager_LightMode(t *testing.T) { | |||
| 35 | + | ctx := context.Background() | |||
| 36 | + | cfg := &config.Config{ | |||
| 37 | + | Appearance: config.AppearanceConfig{ | |||
| 38 | + | ColorScheme: "default", | |||
| 39 | + | }, | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 43 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 44 | + | PrefersDark: false, | |||
| 45 | + | Source: "test", | |||
| 46 | + | }) | |||
| 47 | + | ||||
| 48 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 49 | + | ||||
| 50 | + | assert.False(t, manager.PrefersDark()) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func TestNewManager_WithNilConfig(t *testing.T) { | |||
| 54 | + | ctx := context.Background() | |||
| 55 | + | ||||
| 56 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 57 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 58 | + | PrefersDark: true, | |||
| 59 | + | Source: "fallback", | |||
| 60 | + | }) | |||
| 61 | + | ||||
| 62 | + | manager := NewManager(ctx, nil, mockResolver) | |||
| 63 | + | ||||
| 64 | + | require.NotNil(t, manager) | |||
| 65 | + | assert.True(t, manager.PrefersDark()) | |||
| 66 | + | assert.Equal(t, "system", manager.scheme) | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | func TestNewManager_UsesConfigScheme(t *testing.T) { | |||
| 70 | + | ctx := context.Background() | |||
| 71 | + | cfg := &config.Config{ | |||
| 72 | + | Appearance: config.AppearanceConfig{ | |||
| 73 | + | ColorScheme: "prefer-dark", | |||
| 74 | + | }, | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 78 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 79 | + | PrefersDark: true, | |||
| 80 | + | Source: "config", | |||
| 81 | + | }) | |||
| 82 | + | ||||
| 83 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 84 | + | ||||
| 85 | + | assert.Equal(t, "prefer-dark", manager.scheme) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func TestManager_GetCurrentPalette_Dark(t *testing.T) { | |||
| 89 | + | ctx := context.Background() | |||
| 90 | + | cfg := &config.Config{} | |||
| 91 | + | ||||
| 92 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 93 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 94 | + | PrefersDark: true, | |||
| 95 | + | Source: "test", | |||
| 96 | + | }) | |||
| 97 | + | ||||
| 98 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 99 | + | palette := manager.GetCurrentPalette() | |||
| 100 | + | ||||
| 101 | + | // Dark palette should be returned | |||
| 102 | + | assert.Equal(t, manager.GetDarkPalette(), palette) | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func TestManager_GetCurrentPalette_Light(t *testing.T) { | |||
| 106 | + | ctx := context.Background() | |||
| 107 | + | cfg := &config.Config{} | |||
| 108 | + | ||||
| 109 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 110 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 111 | + | PrefersDark: false, | |||
| 112 | + | Source: "test", | |||
| 113 | + | }) | |||
| 114 | + | ||||
| 115 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 116 | + | palette := manager.GetCurrentPalette() | |||
| 117 | + | ||||
| 118 | + | // Light palette should be returned | |||
| 119 | + | assert.Equal(t, manager.GetLightPalette(), palette) | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | func TestManager_SetColorScheme(t *testing.T) { | |||
| 123 | + | ctx := context.Background() | |||
| 124 | + | cfg := &config.Config{} | |||
| 125 | + | ||||
| 126 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 127 | + | // Initial resolve call | |||
| 128 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 129 | + | PrefersDark: true, | |||
| 130 | + | Source: "test", | |||
| 131 | + | }) | |||
| 132 | + | // Refresh call when SetColorScheme is called | |||
| 133 | + | mockResolver.EXPECT().Refresh().Return(port.ColorSchemePreference{ | |||
| 134 | + | PrefersDark: false, | |||
| 135 | + | Source: "config", | |||
| 136 | + | }) | |||
| 137 | + | ||||
| 138 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 139 | + | assert.True(t, manager.PrefersDark()) | |||
| 140 | + | ||||
| 141 | + | // Change scheme - this calls Refresh on resolver | |||
| 142 | + | manager.SetColorScheme(ctx, "prefer-light", nil) | |||
| 143 | + | ||||
| 144 | + | assert.False(t, manager.PrefersDark()) | |||
| 145 | + | assert.Equal(t, "prefer-light", manager.scheme) | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | func TestManager_UpdateFromConfig(t *testing.T) { | |||
| 149 | + | ctx := context.Background() | |||
| 150 | + | cfg := &config.Config{ | |||
| 151 | + | Appearance: config.AppearanceConfig{ | |||
| 152 | + | ColorScheme: "default", | |||
| 153 | + | }, | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 157 | + | // Initial resolve | |||
| 158 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 159 | + | PrefersDark: true, | |||
| 160 | + | Source: "test", | |||
| 161 | + | }) | |||
| 162 | + | // Refresh when UpdateFromConfig is called | |||
| 163 | + | mockResolver.EXPECT().Refresh().Return(port.ColorSchemePreference{ | |||
| 164 | + | PrefersDark: false, | |||
| 165 | + | Source: "config", | |||
| 166 | + | }) | |||
| 167 | + | ||||
| 168 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 169 | + | assert.True(t, manager.PrefersDark()) | |||
| 170 | + | ||||
| 171 | + | // Update with new config | |||
| 172 | + | newCfg := &config.Config{ | |||
| 173 | + | Appearance: config.AppearanceConfig{ | |||
| 174 | + | ColorScheme: "prefer-light", | |||
| 175 | + | }, | |||
| 176 | + | } | |||
| 177 | + | manager.UpdateFromConfig(ctx, newCfg, nil) | |||
| 178 | + | ||||
| 179 | + | assert.False(t, manager.PrefersDark()) | |||
| 180 | + | assert.Equal(t, "prefer-light", manager.scheme) | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | func TestManager_UpdateFromConfig_NilConfig(t *testing.T) { | |||
| 184 | + | ctx := context.Background() | |||
| 185 | + | cfg := &config.Config{} | |||
| 186 | + | ||||
| 187 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 188 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 189 | + | PrefersDark: true, | |||
| 190 | + | Source: "test", | |||
| 191 | + | }) | |||
| 192 | + | ||||
| 193 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 194 | + | initialScheme := manager.scheme | |||
| 195 | + | ||||
| 196 | + | // UpdateFromConfig with nil should be a no-op | |||
| 197 | + | manager.UpdateFromConfig(ctx, nil, nil) | |||
| 198 | + | ||||
| 199 | + | assert.Equal(t, initialScheme, manager.scheme) | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | func TestManager_GetWebUIThemeCSS(t *testing.T) { | |||
| 203 | + | ctx := context.Background() | |||
| 204 | + | cfg := &config.Config{} | |||
| 205 | + | ||||
| 206 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 207 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 208 | + | PrefersDark: true, | |||
| 209 | + | Source: "test", | |||
| 210 | + | }) | |||
| 211 | + | ||||
| 212 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 213 | + | css := manager.GetWebUIThemeCSS() | |||
| 214 | + | ||||
| 215 | + | // Should contain both light and dark variables | |||
| 216 | + | assert.Contains(t, css, ":root{") | |||
| 217 | + | assert.Contains(t, css, ".dark{") | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | func TestManager_GetBackgroundRGBA(t *testing.T) { | |||
| 221 | + | ctx := context.Background() | |||
| 222 | + | cfg := &config.Config{} | |||
| 223 | + | ||||
| 224 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 225 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 226 | + | PrefersDark: true, | |||
| 227 | + | Source: "test", | |||
| 228 | + | }) | |||
| 229 | + | ||||
| 230 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 231 | + | r, g, b, a := manager.GetBackgroundRGBA() | |||
| 232 | + | ||||
| 233 | + | // Should return valid RGBA values (0-1 range) | |||
| 234 | + | assert.GreaterOrEqual(t, r, float32(0)) | |||
| 235 | + | assert.LessOrEqual(t, r, float32(1)) | |||
| 236 | + | assert.GreaterOrEqual(t, g, float32(0)) | |||
| 237 | + | assert.LessOrEqual(t, g, float32(1)) | |||
| 238 | + | assert.GreaterOrEqual(t, b, float32(0)) | |||
| 239 | + | assert.LessOrEqual(t, b, float32(1)) | |||
| 240 | + | assert.GreaterOrEqual(t, a, float32(0)) | |||
| 241 | + | assert.LessOrEqual(t, a, float32(1)) | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | func TestManager_CustomPalettes(t *testing.T) { | |||
| 245 | + | ctx := context.Background() | |||
| 246 | + | cfg := &config.Config{ | |||
| 247 | + | Appearance: config.AppearanceConfig{ | |||
| 248 | + | LightPalette: config.ColorPalette{ | |||
| 249 | + | Background: "#ffffff", | |||
| 250 | + | Text: "#000000", | |||
| 251 | + | }, | |||
| 252 | + | DarkPalette: config.ColorPalette{ | |||
| 253 | + | Background: "#000000", | |||
| 254 | + | Text: "#ffffff", | |||
| 255 | + | }, | |||
| 256 | + | }, | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 260 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 261 | + | PrefersDark: true, | |||
| 262 | + | Source: "test", | |||
| 263 | + | }) | |||
| 264 | + | ||||
| 265 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 266 | + | ||||
| 267 | + | lightPalette := manager.GetLightPalette() | |||
| 268 | + | darkPalette := manager.GetDarkPalette() | |||
| 269 | + | ||||
| 270 | + | assert.Equal(t, "#ffffff", lightPalette.Background) | |||
| 271 | + | assert.Equal(t, "#000000", lightPalette.Text) | |||
| 272 | + | assert.Equal(t, "#000000", darkPalette.Background) | |||
| 273 | + | assert.Equal(t, "#ffffff", darkPalette.Text) | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | func TestManager_UIScale(t *testing.T) { | |||
| 277 | + | ctx := context.Background() | |||
| 278 | + | cfg := &config.Config{ | |||
| 279 | + | DefaultUIScale: 1.5, | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 283 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 284 | + | PrefersDark: true, | |||
| 285 | + | Source: "test", | |||
| 286 | + | }) | |||
| 287 | + | ||||
| 288 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 289 | + | ||||
| 290 | + | // UI scale should be stored (we can't directly access it, but we can verify | |||
| 291 | + | // the manager was created without error) | |||
| 292 | + | require.NotNil(t, manager) | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | func TestManager_Fonts(t *testing.T) { | |||
| 296 | + | ctx := context.Background() | |||
| 297 | + | cfg := &config.Config{ | |||
| 298 | + | Appearance: config.AppearanceConfig{ | |||
| 299 | + | SansFont: "Inter", | |||
| 300 | + | MonospaceFont: "JetBrains Mono", | |||
| 301 | + | }, | |||
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 305 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 306 | + | PrefersDark: true, | |||
| 307 | + | Source: "test", | |||
| 308 | + | }) | |||
| 309 | + | ||||
| 310 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 311 | + | ||||
| 312 | + | // Fonts should be stored (manager creation should succeed) | |||
| 313 | + | require.NotNil(t, manager) | |||
| 314 | + | } | |||
| 315 | + | ||||
| 316 | + | func TestManager_ModeColors(t *testing.T) { | |||
| 317 | + | ctx := context.Background() | |||
| 318 | + | cfg := &config.Config{ | |||
| 319 | + | Workspace: config.WorkspaceConfig{ | |||
| 320 | + | Styling: config.WorkspaceStylingConfig{ | |||
| 321 | + | PaneModeColor: "#ff0000", | |||
| 322 | + | TabModeColor: "#00ff00", | |||
| 323 | + | SessionModeColor: "#0000ff", | |||
| 324 | + | ResizeModeColor: "#ffff00", | |||
| 325 | + | }, | |||
| 326 | + | }, | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | mockResolver := mocks.NewMockColorSchemeResolver(t) | |||
| 330 | + | mockResolver.EXPECT().Resolve().Return(port.ColorSchemePreference{ | |||
| 331 | + | PrefersDark: true, | |||
| 332 | + | Source: "test", | |||
| 333 | + | }) | |||
| 334 | + | ||||
| 335 | + | manager := NewManager(ctx, cfg, mockResolver) | |||
| 336 | + | modeColors := manager.GetModeColors() | |||
| 337 | + | ||||
| 338 | + | assert.Equal(t, "#ff0000", modeColors.PaneMode) | |||
| 339 | + | assert.Equal(t, "#00ff00", modeColors.TabMode) | |||
| 340 | + | assert.Equal(t, "#0000ff", modeColors.SessionMode) | |||
| 341 | + | assert.Equal(t, "#ffff00", modeColors.ResizeMode) | |||
| 342 | + | } | |||
D internal/ui/theme/system.go +0 -78
| @@ -1,78 +0,0 @@ | |||
| 1 | - | package theme | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "os" | |
| 5 | - | "os/exec" | |
| 6 | - | "strings" | |
| 7 | - | ||
| 8 | - | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | // DetectSystemDarkMode checks if the system prefers dark mode. | |
| 12 | - | // It checks multiple sources in order of preference: | |
| 13 | - | // 1. GTK_THEME environment variable (contains "dark") | |
| 14 | - | // 2. GNOME gsettings color-scheme (prefer-dark/prefer-light) | |
| 15 | - | // 3. GTK Settings gtk-application-prefer-dark-theme property | |
| 16 | - | // Returns true if dark mode is preferred. | |
| 17 | - | func DetectSystemDarkMode() bool { | |
| 18 | - | // Check GTK_THEME environment variable first | |
| 19 | - | gtkTheme := os.Getenv("GTK_THEME") | |
| 20 | - | if gtkTheme != "" { | |
| 21 | - | if strings.Contains(strings.ToLower(gtkTheme), "dark") { | |
| 22 | - | return true | |
| 23 | - | } | |
| 24 | - | // If GTK_THEME is set but doesn't contain "dark", it's likely light | |
| 25 | - | return false | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // Check GNOME gsettings color-scheme (modern GNOME/GTK4 method) | |
| 29 | - | if colorScheme := getGsettingsColorScheme(); colorScheme != "" { | |
| 30 | - | switch colorScheme { | |
| 31 | - | case "prefer-dark": | |
| 32 | - | return true | |
| 33 | - | case "prefer-light": | |
| 34 | - | return false | |
| 35 | - | // "default" falls through to next check | |
| 36 | - | } | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | // Try GTK Settings | |
| 40 | - | settings := gtk.SettingsGetDefault() | |
| 41 | - | if settings != nil { | |
| 42 | - | // gtk-application-prefer-dark-theme is a boolean property | |
| 43 | - | preferDark := settings.GetPropertyGtkApplicationPreferDarkTheme() | |
| 44 | - | return preferDark | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | // Default to dark mode if detection fails (better for eyes) | |
| 48 | - | return true | |
| 49 | - | } | |
| 50 | - | ||
| 51 | - | // getGsettingsColorScheme queries GNOME gsettings for the color-scheme preference. | |
| 52 | - | // Returns "prefer-dark", "prefer-light", "default", or empty string on error. | |
| 53 | - | func getGsettingsColorScheme() string { | |
| 54 | - | cmd := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme") | |
| 55 | - | output, err := cmd.Output() | |
| 56 | - | if err != nil { | |
| 57 | - | return "" | |
| 58 | - | } | |
| 59 | - | // Output is like "'prefer-dark'\n", strip quotes and whitespace | |
| 60 | - | result := strings.TrimSpace(string(output)) | |
| 61 | - | result = strings.Trim(result, "'\"") | |
| 62 | - | return result | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | // ResolveColorScheme determines the effective dark mode preference. | |
| 66 | - | // It takes the config value ("prefer-dark", "prefer-light", "default") | |
| 67 | - | // and resolves "default" to the system preference. | |
| 68 | - | func ResolveColorScheme(configScheme string) bool { | |
| 69 | - | switch strings.ToLower(configScheme) { | |
| 70 | - | case "prefer-dark", "dark": | |
| 71 | - | return true | |
| 72 | - | case "prefer-light", "light": | |
| 73 | - | return false | |
| 74 | - | default: | |
| 75 | - | // "default" or empty - follow system | |
| 76 | - | return DetectSystemDarkMode() | |
| 77 | - | } | |
| 78 | - | } | |
| @@ -1,78 +0,0 @@ | |||||
| 1 | - | package theme | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "os" | |||
| 5 | - | "os/exec" | |||
| 6 | - | "strings" | |||
| 7 | - | ||||
| 8 | - | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | // DetectSystemDarkMode checks if the system prefers dark mode. | |||
| 12 | - | // It checks multiple sources in order of preference: | |||
| 13 | - | // 1. GTK_THEME environment variable (contains "dark") | |||
| 14 | - | // 2. GNOME gsettings color-scheme (prefer-dark/prefer-light) | |||
| 15 | - | // 3. GTK Settings gtk-application-prefer-dark-theme property | |||
| 16 | - | // Returns true if dark mode is preferred. | |||
| 17 | - | func DetectSystemDarkMode() bool { | |||
| 18 | - | // Check GTK_THEME environment variable first | |||
| 19 | - | gtkTheme := os.Getenv("GTK_THEME") | |||
| 20 | - | if gtkTheme != "" { | |||
| 21 | - | if strings.Contains(strings.ToLower(gtkTheme), "dark") { | |||
| 22 | - | return true | |||
| 23 | - | } | |||
| 24 | - | // If GTK_THEME is set but doesn't contain "dark", it's likely light | |||
| 25 | - | return false | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | // Check GNOME gsettings color-scheme (modern GNOME/GTK4 method) | |||
| 29 | - | if colorScheme := getGsettingsColorScheme(); colorScheme != "" { | |||
| 30 | - | switch colorScheme { | |||
| 31 | - | case "prefer-dark": | |||
| 32 | - | return true | |||
| 33 | - | case "prefer-light": | |||
| 34 | - | return false | |||
| 35 | - | // "default" falls through to next check | |||
| 36 | - | } | |||
| 37 | - | } | |||
| 38 | - | ||||
| 39 | - | // Try GTK Settings | |||
| 40 | - | settings := gtk.SettingsGetDefault() | |||
| 41 | - | if settings != nil { | |||
| 42 | - | // gtk-application-prefer-dark-theme is a boolean property | |||
| 43 | - | preferDark := settings.GetPropertyGtkApplicationPreferDarkTheme() | |||
| 44 | - | return preferDark | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | // Default to dark mode if detection fails (better for eyes) | |||
| 48 | - | return true | |||
| 49 | - | } | |||
| 50 | - | ||||
| 51 | - | // getGsettingsColorScheme queries GNOME gsettings for the color-scheme preference. | |||
| 52 | - | // Returns "prefer-dark", "prefer-light", "default", or empty string on error. | |||
| 53 | - | func getGsettingsColorScheme() string { | |||
| 54 | - | cmd := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme") | |||
| 55 | - | output, err := cmd.Output() | |||
| 56 | - | if err != nil { | |||
| 57 | - | return "" | |||
| 58 | - | } | |||
| 59 | - | // Output is like "'prefer-dark'\n", strip quotes and whitespace | |||
| 60 | - | result := strings.TrimSpace(string(output)) | |||
| 61 | - | result = strings.Trim(result, "'\"") | |||
| 62 | - | return result | |||
| 63 | - | } | |||
| 64 | - | ||||
| 65 | - | // ResolveColorScheme determines the effective dark mode preference. | |||
| 66 | - | // It takes the config value ("prefer-dark", "prefer-light", "default") | |||
| 67 | - | // and resolves "default" to the system preference. | |||
| 68 | - | func ResolveColorScheme(configScheme string) bool { | |||
| 69 | - | switch strings.ToLower(configScheme) { | |||
| 70 | - | case "prefer-dark", "dark": | |||
| 71 | - | return true | |||
| 72 | - | case "prefer-light", "light": | |||
| 73 | - | return false | |||
| 74 | - | default: | |||
| 75 | - | // "default" or empty - follow system | |||
| 76 | - | return DetectSystemDarkMode() | |||
| 77 | - | } | |||
| 78 | - | } | |||