From 6285ca9dc4e6f211197e377d01c485b1ef65c300 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Fri, 6 Mar 2026 21:46:09 -0500 Subject: [PATCH] feat(09-02): add shortcuts config package with default bindings and Wails persistence - Create backend/shortcuts/config.go with DefaultBindings(), ApplyDefaults(), Validate() - Wire Shortcuts field into main Config struct with TOML persistence - Add GetShortcuts, SetShortcuts, SetShortcut, ResetShortcuts Wails binding methods - Regenerate Wails TypeScript bindings for new config methods - Fix wsl lint in library.go (blank line before logger call) --- backend/config/config.go | 112 +++++++++++++++++++++++++ backend/library/library.go | 1 + backend/shortcuts/config.go | 64 ++++++++++++++ frontend/wailsjs/go/config/Config.d.ts | 8 ++ frontend/wailsjs/go/config/Config.js | 16 ++++ frontend/wailsjs/go/models.ts | 2 + 6 files changed, 203 insertions(+) create mode 100644 backend/shortcuts/config.go diff --git a/backend/config/config.go b/backend/config/config.go index 45bab72..94c834d 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -15,6 +15,7 @@ import ( "yellowjacket/backend/events" "yellowjacket/backend/favorites" "yellowjacket/backend/library" + "yellowjacket/backend/shortcuts" "yellowjacket/backend/system" "yellowjacket/backend/theme" "yellowjacket/backend/tracklist" @@ -30,6 +31,7 @@ type Config struct { Window *WindowConfig `toml:"Window"` TrackList *tracklist.Config `toml:"TrackList"` Favorites *favorites.Config `toml:"Favorites"` + Shortcuts *shortcuts.Config `toml:"Shortcuts"` } // NewConfig creates a new config by loading it from disk. @@ -86,6 +88,12 @@ func (c *Config) Validate() error { } } + if c.Shortcuts != nil { + if err := c.Shortcuts.Validate(); err != nil { + configErrs = errors.Join(configErrs, err) + } + } + if configErrs != nil { return fmt.Errorf( "one or more config parts are invalid: %w", @@ -190,6 +198,12 @@ func (c *Config) applyDefaults() { } c.Favorites.ApplyDefaults() + + if c.Shortcuts == nil { + c.Shortcuts = &shortcuts.Config{} + } + + c.Shortcuts.ApplyDefaults() } // SetContext sets the Wails runtime context for event emission. @@ -582,3 +596,101 @@ func (c *Config) emitFavoritesChanged() { }, ) } + +// GetShortcuts returns the current shortcut bindings map. +func (c *Config) GetShortcuts() map[string]string { + if c.Shortcuts == nil { + c.Shortcuts = &shortcuts.Config{} + c.Shortcuts.ApplyDefaults() + } + + return c.Shortcuts.Bindings +} + +// SetShortcuts saves the entire shortcut bindings map. +func (c *Config) SetShortcuts( + bindings map[string]string, +) error { + if c.Shortcuts == nil { + c.Shortcuts = &shortcuts.Config{} + } + + c.Shortcuts.Bindings = bindings + + if err := c.Save(); err != nil { + return fmt.Errorf( + "could not save shortcuts config: %w", err, + ) + } + + if c.ctx != nil { + runtime.EventsEmit( + c.ctx, + events.ShortcutsConfigChanged, + bindings, + ) + } + + c.logger.Info("shortcuts config updated") + + return nil +} + +// SetShortcut saves a single shortcut binding. +func (c *Config) SetShortcut( + action string, key string, +) error { + if c.Shortcuts == nil { + c.Shortcuts = &shortcuts.Config{} + c.Shortcuts.ApplyDefaults() + } + + c.Shortcuts.Bindings[action] = key + + if err := c.Save(); err != nil { + return fmt.Errorf( + "could not save shortcut: %w", err, + ) + } + + if c.ctx != nil { + runtime.EventsEmit( + c.ctx, + events.ShortcutsConfigChanged, + c.Shortcuts.Bindings, + ) + } + + c.logger.Info( + "shortcut updated", + "action", action, + "key", key, + ) + + return nil +} + +// ResetShortcuts resets all shortcuts to defaults. +func (c *Config) ResetShortcuts() error { + c.Shortcuts = &shortcuts.Config{ + Bindings: shortcuts.DefaultBindings(), + } + + if err := c.Save(); err != nil { + return fmt.Errorf( + "could not save shortcuts reset: %w", err, + ) + } + + if c.ctx != nil { + runtime.EventsEmit( + c.ctx, + events.ShortcutsConfigChanged, + c.Shortcuts.Bindings, + ) + } + + c.logger.Info("shortcuts reset to defaults") + + return nil +} diff --git a/backend/library/library.go b/backend/library/library.go index 249a933..e52854f 100644 --- a/backend/library/library.go +++ b/backend/library/library.go @@ -590,6 +590,7 @@ func (l *Library) Scan() (*ScanMetrics, error) { if cancelled { metrics.Cancelled = true + l.logger.Info("scan cancelled, skipping orphan cleanup") } else { // --- Phase 5: orphan cleanup --- diff --git a/backend/shortcuts/config.go b/backend/shortcuts/config.go new file mode 100644 index 0000000..e8060f4 --- /dev/null +++ b/backend/shortcuts/config.go @@ -0,0 +1,64 @@ +// Package shortcuts manages keyboard shortcut configuration. +package shortcuts + +// Config holds user-customized keyboard shortcut bindings. +// Keys are action IDs (e.g. "player.playPause"), values are +// key combo strings in canonical format (e.g. "Ctrl+F", "Space"). +type Config struct { + Bindings map[string]string `toml:"Bindings"` +} + +// DefaultBindings returns the default keyboard shortcut bindings. +// Follows hybrid style: Space/arrows for player, Ctrl+key for app actions. +func DefaultBindings() map[string]string { + return map[string]string{ + // Player controls (Global scope, no modifier) + "player.playPause": "Space", + "player.next": "N", + "player.previous": "P", + "player.volumeUp": "Up", + "player.volumeDown": "Down", + "player.seekForward": "Right", + "player.seekBack": "Left", + "player.shuffle": "S", + "player.repeat": "R", + "player.mute": "M", + + // Navigation (Global scope) + "nav.search": "/", + "nav.searchAlt": "Ctrl+F", + "nav.queue": "Q", + + // App actions (Global scope, Ctrl modifier) + "app.selectAll": "Ctrl+A", + + // Panel-specific (track list) + "tracklist.play": "Enter", + "tracklist.delete": "Delete", + } +} + +// ApplyDefaults fills any missing bindings with defaults. +// Existing user customizations are preserved. +func (c *Config) ApplyDefaults() { + if c.Bindings == nil { + c.Bindings = DefaultBindings() + + return + } + + defaults := DefaultBindings() + for action, key := range defaults { + if _, exists := c.Bindings[action]; !exists { + c.Bindings[action] = key + } + } +} + +// Validate checks that the config is well-formed. +func (c *Config) Validate() error { + c.ApplyDefaults() + // No validation errors possible — any string is a valid binding. + // Conflict detection is a frontend UX concern, not a config error. + return nil +} diff --git a/frontend/wailsjs/go/config/Config.d.ts b/frontend/wailsjs/go/config/Config.d.ts index 98a82c7..753213b 100755 --- a/frontend/wailsjs/go/config/Config.d.ts +++ b/frontend/wailsjs/go/config/Config.d.ts @@ -13,6 +13,8 @@ export function GetPinDefaultPlaylist():Promise; export function GetScanConcurrency():Promise; +export function GetShortcuts():Promise>; + export function GetThemeAccentColor():Promise; export function GetThemeBackgroundShade():Promise; @@ -21,6 +23,8 @@ export function GetTrackListColumns():Promise>; export function Load():Promise; +export function ResetShortcuts():Promise; + export function Save():Promise; export function SetContext(arg1:context.Context):Promise; @@ -35,6 +39,10 @@ export function SetPinDefaultPlaylist(arg1:boolean):Promise; export function SetScanConcurrency(arg1:string):Promise; +export function SetShortcut(arg1:string,arg2:string):Promise; + +export function SetShortcuts(arg1:Record):Promise; + export function SetThemeAccentColor(arg1:string):Promise; export function SetThemeBackgroundShade(arg1:string):Promise; diff --git a/frontend/wailsjs/go/config/Config.js b/frontend/wailsjs/go/config/Config.js index 2b04eb1..ae647ca 100755 --- a/frontend/wailsjs/go/config/Config.js +++ b/frontend/wailsjs/go/config/Config.js @@ -22,6 +22,10 @@ export function GetScanConcurrency() { return window['go']['config']['Config']['GetScanConcurrency'](); } +export function GetShortcuts() { + return window['go']['config']['Config']['GetShortcuts'](); +} + export function GetThemeAccentColor() { return window['go']['config']['Config']['GetThemeAccentColor'](); } @@ -38,6 +42,10 @@ export function Load() { return window['go']['config']['Config']['Load'](); } +export function ResetShortcuts() { + return window['go']['config']['Config']['ResetShortcuts'](); +} + export function Save() { return window['go']['config']['Config']['Save'](); } @@ -66,6 +74,14 @@ export function SetScanConcurrency(arg1) { return window['go']['config']['Config']['SetScanConcurrency'](arg1); } +export function SetShortcut(arg1, arg2) { + return window['go']['config']['Config']['SetShortcut'](arg1, arg2); +} + +export function SetShortcuts(arg1) { + return window['go']['config']['Config']['SetShortcuts'](arg1); +} + export function SetThemeAccentColor(arg1) { return window['go']['config']['Config']['SetThemeAccentColor'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index bb585c6..af39a0f 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -108,6 +108,7 @@ export namespace library { updated: number; skipped: number; removed: number; + cancelled: boolean; warnings: ScanWarning[]; static createFrom(source: any = {}) { @@ -141,6 +142,7 @@ export namespace library { this.updated = source["updated"]; this.skipped = source["skipped"]; this.removed = source["removed"]; + this.cancelled = source["cancelled"]; this.warnings = this.convertValues(source["warnings"], ScanWarning); }