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)
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"yellowjacket/backend/events"
|
"yellowjacket/backend/events"
|
||||||
"yellowjacket/backend/favorites"
|
"yellowjacket/backend/favorites"
|
||||||
"yellowjacket/backend/library"
|
"yellowjacket/backend/library"
|
||||||
|
"yellowjacket/backend/shortcuts"
|
||||||
"yellowjacket/backend/system"
|
"yellowjacket/backend/system"
|
||||||
"yellowjacket/backend/theme"
|
"yellowjacket/backend/theme"
|
||||||
"yellowjacket/backend/tracklist"
|
"yellowjacket/backend/tracklist"
|
||||||
@@ -30,6 +31,7 @@ type Config struct {
|
|||||||
Window *WindowConfig `toml:"Window"`
|
Window *WindowConfig `toml:"Window"`
|
||||||
TrackList *tracklist.Config `toml:"TrackList"`
|
TrackList *tracklist.Config `toml:"TrackList"`
|
||||||
Favorites *favorites.Config `toml:"Favorites"`
|
Favorites *favorites.Config `toml:"Favorites"`
|
||||||
|
Shortcuts *shortcuts.Config `toml:"Shortcuts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new config by loading it from disk.
|
// 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 {
|
if configErrs != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"one or more config parts are invalid: %w",
|
"one or more config parts are invalid: %w",
|
||||||
@@ -190,6 +198,12 @@ func (c *Config) applyDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Favorites.ApplyDefaults()
|
c.Favorites.ApplyDefaults()
|
||||||
|
|
||||||
|
if c.Shortcuts == nil {
|
||||||
|
c.Shortcuts = &shortcuts.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Shortcuts.ApplyDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContext sets the Wails runtime context for event emission.
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
|
|
||||||
if cancelled {
|
if cancelled {
|
||||||
metrics.Cancelled = true
|
metrics.Cancelled = true
|
||||||
|
|
||||||
l.logger.Info("scan cancelled, skipping orphan cleanup")
|
l.logger.Info("scan cancelled, skipping orphan cleanup")
|
||||||
} else {
|
} else {
|
||||||
// --- Phase 5: orphan cleanup ---
|
// --- Phase 5: orphan cleanup ---
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+8
@@ -13,6 +13,8 @@ export function GetPinDefaultPlaylist():Promise<boolean>;
|
|||||||
|
|
||||||
export function GetScanConcurrency():Promise<string>;
|
export function GetScanConcurrency():Promise<string>;
|
||||||
|
|
||||||
|
export function GetShortcuts():Promise<Record<string, string>>;
|
||||||
|
|
||||||
export function GetThemeAccentColor():Promise<string>;
|
export function GetThemeAccentColor():Promise<string>;
|
||||||
|
|
||||||
export function GetThemeBackgroundShade():Promise<string>;
|
export function GetThemeBackgroundShade():Promise<string>;
|
||||||
@@ -21,6 +23,8 @@ export function GetTrackListColumns():Promise<Array<tracklist.Column>>;
|
|||||||
|
|
||||||
export function Load():Promise<void>;
|
export function Load():Promise<void>;
|
||||||
|
|
||||||
|
export function ResetShortcuts():Promise<void>;
|
||||||
|
|
||||||
export function Save():Promise<void>;
|
export function Save():Promise<void>;
|
||||||
|
|
||||||
export function SetContext(arg1:context.Context):Promise<void>;
|
export function SetContext(arg1:context.Context):Promise<void>;
|
||||||
@@ -35,6 +39,10 @@ export function SetPinDefaultPlaylist(arg1:boolean):Promise<void>;
|
|||||||
|
|
||||||
export function SetScanConcurrency(arg1:string):Promise<void>;
|
export function SetScanConcurrency(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SetShortcut(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SetShortcuts(arg1:Record<string, string>):Promise<void>;
|
||||||
|
|
||||||
export function SetThemeAccentColor(arg1:string):Promise<void>;
|
export function SetThemeAccentColor(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function SetThemeBackgroundShade(arg1:string):Promise<void>;
|
export function SetThemeBackgroundShade(arg1:string):Promise<void>;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export function GetScanConcurrency() {
|
|||||||
return window['go']['config']['Config']['GetScanConcurrency']();
|
return window['go']['config']['Config']['GetScanConcurrency']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetShortcuts() {
|
||||||
|
return window['go']['config']['Config']['GetShortcuts']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetThemeAccentColor() {
|
export function GetThemeAccentColor() {
|
||||||
return window['go']['config']['Config']['GetThemeAccentColor']();
|
return window['go']['config']['Config']['GetThemeAccentColor']();
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,10 @@ export function Load() {
|
|||||||
return window['go']['config']['Config']['Load']();
|
return window['go']['config']['Config']['Load']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ResetShortcuts() {
|
||||||
|
return window['go']['config']['Config']['ResetShortcuts']();
|
||||||
|
}
|
||||||
|
|
||||||
export function Save() {
|
export function Save() {
|
||||||
return window['go']['config']['Config']['Save']();
|
return window['go']['config']['Config']['Save']();
|
||||||
}
|
}
|
||||||
@@ -66,6 +74,14 @@ export function SetScanConcurrency(arg1) {
|
|||||||
return window['go']['config']['Config']['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) {
|
export function SetThemeAccentColor(arg1) {
|
||||||
return window['go']['config']['Config']['SetThemeAccentColor'](arg1);
|
return window['go']['config']['Config']['SetThemeAccentColor'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export namespace library {
|
|||||||
updated: number;
|
updated: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
removed: number;
|
removed: number;
|
||||||
|
cancelled: boolean;
|
||||||
warnings: ScanWarning[];
|
warnings: ScanWarning[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
@@ -141,6 +142,7 @@ export namespace library {
|
|||||||
this.updated = source["updated"];
|
this.updated = source["updated"];
|
||||||
this.skipped = source["skipped"];
|
this.skipped = source["skipped"];
|
||||||
this.removed = source["removed"];
|
this.removed = source["removed"];
|
||||||
|
this.cancelled = source["cancelled"];
|
||||||
this.warnings = this.convertValues(source["warnings"], ScanWarning);
|
this.warnings = this.convertValues(source["warnings"], ScanWarning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user