Skip to content

Commit 9d22a6e

Browse files
authored
feat: add theme-aware default for label_color - Closes #489 (#491)
1 parent 7aab9a3 commit 9d22a6e

22 files changed

+532
-59
lines changed

configs/default-config.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ keys = "uijk"
248248
line_color = "#FF8EE2FF" # Color of grid lines (light blue, alpha FF = 100% opacity)
249249
line_width = 1 # Width of grid lines
250250
highlight_color = "#4D00BFFF" # Color for cell highlighting (deep sky blue, alpha 4D ≈ 30% opacity)
251-
label_color = "#FFFFFFFF" # Color of cell labels (white, alpha FF = 100% opacity)
251+
# label_color = "" # Theme-aware: auto white in Dark Mode, black in Light Mode
252+
# Uncomment and set a value to override (e.g. "#FFFFFFFF")
252253
label_font_size = 10 # Font size for labels
253254
label_font_family = "" # Font family for labels
254255

docs/CONFIGURATION.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -702,13 +702,19 @@ j → k j = Lower-left
702702

703703
### Visual Options
704704

705-
| Option | Type | Default | Description |
706-
| ----------------- | ------ | ------------- | ----------------------- |
707-
| `line_color` | string | `"#FF8EE2FF"` | Grid line color |
708-
| `line_width` | int | `1` | Line thickness |
709-
| `highlight_color` | string | `"#4D00BFFF"` | Selected cell highlight |
710-
| `label_color` | string | `"#FFFFFFFF"` | Cell label text |
711-
| `label_font_size` | int | `10` | Label size |
705+
| Option | Type | Default | Description |
706+
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------------ |
707+
| `line_color` | string | `"#FF8EE2FF"` | Grid line color |
708+
| `line_width` | int | `1` | Line thickness |
709+
| `highlight_color` | string | `"#4D00BFFF"` | Selected cell highlight |
710+
| `label_color` | string | _(theme-aware)_ | Cell label text. Auto-adapts to macOS Dark/Light Mode if not explicitly set |
711+
| `label_font_size` | int | `10` | Label size |
712+
713+
> **Theme-aware `label_color`:** When `label_color` is not set in your config file,
714+
> Neru automatically uses white (`#FFFFFFFF`) in Dark Mode and black (`#FF000000`) in
715+
> Light Mode. The color updates in real time when you switch system themes — no restart
716+
> required. If you explicitly set `label_color`, your value is always used regardless
717+
> of the system theme.
712718
713719
---
714720

internal/app/app_config.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ func (a *App) prepareForConfigUpdate() {
4949

5050
// applyAppSpecificConfigUpdates applies app-specific configuration updates.
5151
func (a *App) applyAppSpecificConfigUpdates(loadResult *config.LoadResult) {
52-
a.config = loadResult.Config
53-
a.ConfigPath = loadResult.ConfigPath
54-
5552
if loadResult.Config.Hints.Enabled {
5653
a.logger.Info("Updating clickable roles",
5754
zap.Int("count", len(loadResult.Config.Hints.ClickableRoles)))
@@ -61,6 +58,12 @@ func (a *App) applyAppSpecificConfigUpdates(loadResult *config.LoadResult) {
6158

6259
// reconfigureAfterUpdate reconfigures components and services after config update.
6360
func (a *App) reconfigureAfterUpdate(loadResult *config.LoadResult) {
61+
// Update the config pointer under configMu so that concurrent readers
62+
// (e.g. screen-change handlers, theme observer) see a consistent value.
63+
a.configMu.Lock()
64+
a.config = loadResult.Config
65+
a.ConfigPath = loadResult.ConfigPath
66+
a.configMu.Unlock()
6467
a.configureEventTapHotkeys(loadResult.Config, a.logger)
6568

6669
if a.hintsComponent != nil {

internal/app/app_getters.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ import (
1010
"go.uber.org/zap"
1111
)
1212

13+
// configSnapshot returns the current config pointer under a read lock.
14+
// Callers should use the returned pointer for all reads within a single
15+
// logical operation to avoid seeing a partially-updated config.
16+
func (a *App) configSnapshot() *config.Config {
17+
a.configMu.RLock()
18+
cfg := a.config
19+
a.configMu.RUnlock()
20+
21+
return cfg
22+
}
23+
1324
// SetEnabled sets the enabled state of the application.
1425
func (a *App) SetEnabled(v bool) {
1526
a.appState.SetEnabled(v)
@@ -27,22 +38,28 @@ func (a *App) ToggleEnabled() {
2738

2839
// HintsEnabled returns true if hints are enabled.
2940
func (a *App) HintsEnabled() bool {
30-
return a.config != nil && a.config.Hints.Enabled
41+
cfg := a.configSnapshot()
42+
43+
return cfg != nil && cfg.Hints.Enabled
3144
}
3245

3346
// GridEnabled returns true if grid is enabled.
3447
func (a *App) GridEnabled() bool {
35-
return a.config != nil && a.config.Grid.Enabled
48+
cfg := a.configSnapshot()
49+
50+
return cfg != nil && cfg.Grid.Enabled
3651
}
3752

3853
// RecursiveGridEnabled returns true if recursive-grid is enabled.
3954
func (a *App) RecursiveGridEnabled() bool {
40-
return a.config != nil && a.config.RecursiveGrid.Enabled
55+
cfg := a.configSnapshot()
56+
57+
return cfg != nil && cfg.RecursiveGrid.Enabled
4158
}
4259

4360
// Config returns the application configuration.
4461
func (a *App) Config() *config.Config {
45-
return a.config
62+
return a.configSnapshot()
4663
}
4764

4865
// Logger returns the application logger.
@@ -71,7 +88,11 @@ func (a *App) Renderer() *ui.OverlayRenderer {
7188

7289
// GetConfigPath returns the config path.
7390
func (a *App) GetConfigPath() string {
74-
return a.ConfigPath
91+
a.configMu.RLock()
92+
p := a.ConfigPath
93+
a.configMu.RUnlock()
94+
95+
return p
7596
}
7697

7798
// SetHintOverlayNeedsRefresh sets the hint overlay needs refresh flag.

internal/app/app_initialization_steps.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,10 @@ func initializeRendererAndOverlays(app *App) {
193193
recursiveGridStyle = app.recursiveGridComponent.Style
194194
} else {
195195
// Fallback to default style if component is nil
196-
recursiveGridStyle = recursivegrid.BuildStyle(config.DefaultConfig().RecursiveGrid)
196+
recursiveGridStyle = recursivegrid.BuildStyle(
197+
config.DefaultConfig().RecursiveGrid,
198+
defaultThemeProvider,
199+
)
197200
}
198201

199202
app.renderer = ui.NewOverlayRenderer(
@@ -300,6 +303,7 @@ func initializeModeHandler(app *App) {
300303
deps.callbacks.enableEventTap,
301304
deps.callbacks.disableEventTap,
302305
deps.callbacks.refreshHotkeys,
306+
defaultThemeProvider,
303307
)
304308
}
305309

internal/app/app_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ type App struct {
5656
stopChan chan struct{}
5757
stopOnce sync.Once
5858

59+
// configMu serializes access to config-dependent component state between
60+
// concurrent writers (theme change observer, IPC config reload, systray reload).
61+
configMu sync.RWMutex
62+
5963
// New Architecture Services
6064
hintService *services.HintService
6165
gridService *services.GridService

internal/app/component_factory.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,10 @@ func (f *ComponentFactory) CreateRecursiveGridComponent(
243243
}
244244

245245
return &components.RecursiveGridComponent{
246-
Overlay: recursiveGridOverlay,
247-
Context: &recursivegrid.Context{},
248-
Style: recursivegrid.BuildStyle(f.config.RecursiveGrid),
246+
Overlay: recursiveGridOverlay,
247+
Context: &recursivegrid.Context{},
248+
Style: recursivegrid.BuildStyle(f.config.RecursiveGrid, defaultThemeProvider),
249+
ThemeProvider: defaultThemeProvider,
249250
}, nil
250251
}
251252

internal/app/components/recursivegrid/overlay.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,14 @@ func (s Style) LabelFontFamily() string {
439439
}
440440

441441
// BuildStyle creates a Style from RecursiveGridConfig.
442-
func BuildStyle(cfg config.RecursiveGridConfig) Style {
442+
// The theme parameter is used to resolve the label color when it is not
443+
// explicitly specified in the configuration (empty string = theme-aware default).
444+
func BuildStyle(cfg config.RecursiveGridConfig, theme config.ThemeProvider) Style {
443445
return Style{
444446
lineColor: cfg.LineColor,
445447
lineWidth: cfg.LineWidth,
446448
highlightColor: cfg.HighlightColor,
447-
labelColor: cfg.LabelColor,
449+
labelColor: config.ResolvedLabelColor(cfg.LabelColor, theme),
448450
labelFontSize: cfg.LabelFontSize,
449451
labelFontFamily: cfg.LabelFontFamily,
450452
}

internal/app/components/types.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,17 @@ func (m *ModeIndicatorComponent) UpdateConfig(cfg *config.Config, _ *zap.Logger)
106106

107107
// RecursiveGridComponent encapsulates all recursive-grid-related functionality.
108108
type RecursiveGridComponent struct {
109-
Manager *domainRecursiveGrid.Manager
110-
Overlay *recursivegrid.Overlay
111-
Context *recursivegrid.Context
112-
Style recursivegrid.Style
109+
Manager *domainRecursiveGrid.Manager
110+
Overlay *recursivegrid.Overlay
111+
Context *recursivegrid.Context
112+
Style recursivegrid.Style
113+
ThemeProvider config.ThemeProvider
113114
}
114115

115116
// UpdateConfig updates the recursive-grid component with new configuration.
116117
func (q *RecursiveGridComponent) UpdateConfig(cfg *config.Config, _ *zap.Logger) {
117118
if cfg.RecursiveGrid.Enabled {
118-
q.Style = recursivegrid.BuildStyle(cfg.RecursiveGrid)
119+
q.Style = recursivegrid.BuildStyle(cfg.RecursiveGrid, q.ThemeProvider)
119120
if q.Overlay != nil {
120121
q.Overlay.SetConfig(cfg.RecursiveGrid)
121122
}

internal/app/hotkeys.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414

1515
// registerHotkeys registers all global hotkeys defined in the configuration.
1616
func (a *App) registerHotkeys() {
17-
for key, value := range a.config.Hotkeys.Bindings {
17+
cfg := a.configSnapshot()
18+
19+
for key, value := range cfg.Hotkeys.Bindings {
1820
trimmedKey := strings.TrimSpace(key)
1921

2022
actionStr := strings.TrimSpace(value)
@@ -27,15 +29,15 @@ func (a *App) registerHotkeys() {
2729
mode = parts[0]
2830
}
2931

30-
if mode == domain.ModeString(domain.ModeHints) && !a.config.Hints.Enabled {
32+
if mode == domain.ModeString(domain.ModeHints) && !cfg.Hints.Enabled {
3133
continue
3234
}
3335

34-
if mode == domain.ModeString(domain.ModeGrid) && !a.config.Grid.Enabled {
36+
if mode == domain.ModeString(domain.ModeGrid) && !cfg.Grid.Enabled {
3537
continue
3638
}
3739

38-
if mode == domain.ModeString(domain.ModeRecursiveGrid) && !a.config.RecursiveGrid.Enabled {
40+
if mode == domain.ModeString(domain.ModeRecursiveGrid) && !cfg.RecursiveGrid.Enabled {
3941
continue
4042
}
4143

@@ -182,7 +184,9 @@ func (a *App) refreshHotkeysForAppOrCurrent(bundleID string) {
182184
}
183185
}
184186

185-
if a.config.IsAppExcluded(bundleID) {
187+
cfg := a.configSnapshot()
188+
189+
if cfg.IsAppExcluded(bundleID) {
186190
if a.appState.HotkeysRegistered() {
187191
a.logger.Info("Focused app excluded; unregistering global hotkeys",
188192
zap.String("bundle_id", bundleID))

0 commit comments

Comments
 (0)