diff --git a/config/keybinds.go b/config/keybinds.go index 7f7c54b19ba416db703e8d890256f7239471bcad..bb53d13979569f3b78a2914ea9f27905aef84eb4 100644 --- a/config/keybinds.go +++ b/config/keybinds.go @@ -3,12 +3,11 @@ package config import ( _ "embed" "encoding/json" - "fmt" - "os" - "path/filepath" + + keybind "github.com/floatpane/go-keybind" ) -const keyDelete = "delete" +const keyDelete = "delete" // used in ValidateKeybinds action map keys //go:embed default_keybinds.json var defaultKeybindsJSON []byte @@ -94,27 +93,9 @@ func defaultKeybinds() KeybindsConfig { // LoadKeybindsFromDir reads keybinds.json from cfgDir, writing defaults if // the file does not exist, then updates the package-level Keybinds var. func LoadKeybindsFromDir(cfgDir string) error { - path := filepath.Join(cfgDir, "keybinds.json") - - data, err := os.ReadFile(path) + kb, err := keybind.Load(cfgDir, "keybinds.json", defaultKeybinds()) if err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("keybinds: read %s: %w", path, err) - } - // File missing — write defaults. - if err := os.MkdirAll(cfgDir, 0700); err != nil { - return fmt.Errorf("keybinds: mkdir %s: %w", cfgDir, err) - } - if err := os.WriteFile(path, defaultKeybindsJSON, 0600); err != nil { - return fmt.Errorf("keybinds: write defaults to %s: %w", path, err) - } - Keybinds = defaultKeybinds() - return nil - } - - kb := defaultKeybinds() - if err := json.Unmarshal(data, &kb); err != nil { - return fmt.Errorf("keybinds: parse %s: %w", path, err) + return err } Keybinds = kb return nil @@ -124,73 +105,55 @@ func LoadKeybindsFromDir(cfgDir string) error { // actions within the same area are mapped to the same key. Cross-area // duplicates are intentional (e.g. "d" = delete in both inbox and email view). func ValidateKeybinds(kb KeybindsConfig) []string { - var conflicts []string - - check := func(area string, bindings map[string]string) { - seen := make(map[string]string) // key → action name - for action, key := range bindings { - if key == "" { - continue - } - if prev, ok := seen[key]; ok { - conflicts = append(conflicts, - fmt.Sprintf("conflict in %s: key %q used for both %q and %q", area, key, prev, action)) - } else { - seen[key] = action - } - } - } - - check("global", map[string]string{ - "quit": kb.Global.Quit, - "cancel": kb.Global.Cancel, - "nav_up": kb.Global.NavUp, - "nav_down": kb.Global.NavDown, + return keybind.Validate(map[string]map[string]string{ + "global": { + "quit": kb.Global.Quit, + "cancel": kb.Global.Cancel, + "nav_up": kb.Global.NavUp, + "nav_down": kb.Global.NavDown, + }, + "inbox": { + "visual_mode": kb.Inbox.VisualMode, + "toggle_threaded": kb.Inbox.ToggleThreaded, + keyDelete: kb.Inbox.Delete, + "archive": kb.Inbox.Archive, + "refresh": kb.Inbox.Refresh, + "search": kb.Inbox.Search, + "filter": kb.Inbox.Filter, + "open": kb.Inbox.Open, + "next_tab": kb.Inbox.NextTab, + "prev_tab": kb.Inbox.PrevTab, + }, + "email": { + "reply": kb.Email.Reply, + "forward": kb.Email.Forward, + keyDelete: kb.Email.Delete, + "archive": kb.Email.Archive, + "toggle_images": kb.Email.ToggleImages, + "rsvp_accept": kb.Email.RsvpAccept, + "rsvp_decline": kb.Email.RsvpDecline, + "rsvp_tentative": kb.Email.RsvpTentative, + "focus_attachments": kb.Email.FocusAttachments, + }, + "composer": { + "external_editor": kb.Composer.ExternalEditor, + "next_field": kb.Composer.NextField, + "prev_field": kb.Composer.PrevField, + keyDelete: kb.Composer.Delete, + // spell_* bindings intentionally excluded — spell_accept reusing + // "tab" with next_field and spell_dismiss reusing "esc" with cancel + // are deliberate: the spellcheck popup intercepts before those handlers. + }, + "folder": { + "next_folder": kb.Folder.NextFolder, + "prev_folder": kb.Folder.PrevFolder, + "move": kb.Folder.Move, + "focus_preview": kb.Folder.FocusPreview, + "focus_inbox": kb.Folder.FocusInbox, + }, + "drafts": { + "open": kb.Drafts.Open, + keyDelete: kb.Drafts.Delete, + }, }) - check("inbox", map[string]string{ - "visual_mode": kb.Inbox.VisualMode, - "toggle_threaded": kb.Inbox.ToggleThreaded, - keyDelete: kb.Inbox.Delete, - "archive": kb.Inbox.Archive, - "refresh": kb.Inbox.Refresh, - "search": kb.Inbox.Search, - "filter": kb.Inbox.Filter, - "open": kb.Inbox.Open, - "next_tab": kb.Inbox.NextTab, - "prev_tab": kb.Inbox.PrevTab, - }) - check("email", map[string]string{ - "reply": kb.Email.Reply, - "forward": kb.Email.Forward, - keyDelete: kb.Email.Delete, - "archive": kb.Email.Archive, - "toggle_images": kb.Email.ToggleImages, - "rsvp_accept": kb.Email.RsvpAccept, - "rsvp_decline": kb.Email.RsvpDecline, - "rsvp_tentative": kb.Email.RsvpTentative, - "focus_attachments": kb.Email.FocusAttachments, - }) - check("composer", map[string]string{ - "external_editor": kb.Composer.ExternalEditor, - "next_field": kb.Composer.NextField, - "prev_field": kb.Composer.PrevField, - keyDelete: kb.Composer.Delete, - // spell_* bindings intentionally excluded from this conflict - // check — spell_accept reusing "tab" with next_field, and - // spell_dismiss reusing "esc" with cancel, are deliberate: the - // spellcheck popup intercepts before those handlers fire. - }) - check("folder", map[string]string{ - "next_folder": kb.Folder.NextFolder, - "prev_folder": kb.Folder.PrevFolder, - "move": kb.Folder.Move, - "focus_preview": kb.Folder.FocusPreview, - "focus_inbox": kb.Folder.FocusInbox, - }) - check("drafts", map[string]string{ - "open": kb.Drafts.Open, - keyDelete: kb.Drafts.Delete, - }) - - return conflicts } diff --git a/go.mod b/go.mod index b6d373506809272c20769e37388dcbc7fba8ce0a..6f6adc47610db062c87f785918d23517753018e7 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/floatpane/bubble-overlay v0.0.1 github.com/floatpane/go-icalendar v0.0.1 + github.com/floatpane/go-keybind v0.0.1 github.com/floatpane/go-openpgp-card-hl v0.0.1 github.com/floatpane/go-secretbox v0.1.0 github.com/floatpane/go-uds-jsonrpc v0.0.1 diff --git a/go.sum b/go.sum index b533e7576532bcee1777a36f448b0de91574585a..0115e47b8ec3e6da071335c0edab9a9af265e6ea 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/floatpane/bubble-overlay v0.0.1 h1:5xU8cNigDPYegvgGMfOG23fIDXhrqXPvLT github.com/floatpane/bubble-overlay v0.0.1/go.mod h1:Csi1byxb9L8EAb8X13XdWF5aX5YiBD5C9WEWACyGa8A= github.com/floatpane/go-icalendar v0.0.1 h1:lF9NhEI4TobX8valDakAFfCnBhM2GDITWMVymhXzD8c= github.com/floatpane/go-icalendar v0.0.1/go.mod h1:LSy9G+LwUZtfNIAjLlEVRXkuc2A+hq6+pVCIFOiEAyE= +github.com/floatpane/go-keybind v0.0.1 h1:UrzPQ4ldR9sKQt/efpUfcs6gH8Mwy2NO5vwFTVf0dy0= +github.com/floatpane/go-keybind v0.0.1/go.mod h1:B8l43ypYOcjknyaHgU0EXUAbrUvfnV2HOzYSzyqcPJI= github.com/floatpane/go-openpgp-card-hl v0.0.1 h1:1DYmzwGDb8eneZxbc/xtwjXeFY8DFL3eYnUooMT0L0w= github.com/floatpane/go-openpgp-card-hl v0.0.1/go.mod h1:Mrx+ukCnpEpMAxyB0p8Ch2gu78Q3Ir40BxBybb2jirw= github.com/floatpane/go-secretbox v0.1.0 h1:xNryazmCP0oR/yVxIkHRc5bcV56YrbisY+bMl8BBfwU=