@@ -3,12 +3,11 @@ package config
33import (
44 _ "embed"
55 "encoding/json"
6- "fmt"
7- "os"
8- "path/filepath"
6+
7+ keybind "github.com/floatpane/go-keybind"
98)
109
11- const keyDelete = "delete"
10+ const keyDelete = "delete" // used in ValidateKeybinds action map keys
1211
1312//go:embed default_keybinds.json
1413var defaultKeybindsJSON []byte
@@ -94,27 +93,9 @@ func defaultKeybinds() KeybindsConfig {
9493// LoadKeybindsFromDir reads keybinds.json from cfgDir, writing defaults if
9594// the file does not exist, then updates the package-level Keybinds var.
9695func LoadKeybindsFromDir (cfgDir string ) error {
97- path := filepath .Join (cfgDir , "keybinds.json" )
98-
99- data , err := os .ReadFile (path )
96+ kb , err := keybind .Load (cfgDir , "keybinds.json" , defaultKeybinds ())
10097 if err != nil {
101- if ! os .IsNotExist (err ) {
102- return fmt .Errorf ("keybinds: read %s: %w" , path , err )
103- }
104- // File missing — write defaults.
105- if err := os .MkdirAll (cfgDir , 0700 ); err != nil {
106- return fmt .Errorf ("keybinds: mkdir %s: %w" , cfgDir , err )
107- }
108- if err := os .WriteFile (path , defaultKeybindsJSON , 0600 ); err != nil {
109- return fmt .Errorf ("keybinds: write defaults to %s: %w" , path , err )
110- }
111- Keybinds = defaultKeybinds ()
112- return nil
113- }
114-
115- kb := defaultKeybinds ()
116- if err := json .Unmarshal (data , & kb ); err != nil {
117- return fmt .Errorf ("keybinds: parse %s: %w" , path , err )
98+ return err
11899 }
119100 Keybinds = kb
120101 return nil
@@ -124,73 +105,55 @@ func LoadKeybindsFromDir(cfgDir string) error {
124105// actions within the same area are mapped to the same key. Cross-area
125106// duplicates are intentional (e.g. "d" = delete in both inbox and email view).
126107func ValidateKeybinds (kb KeybindsConfig ) []string {
127- var conflicts []string
128-
129- check := func (area string , bindings map [string ]string ) {
130- seen := make (map [string ]string ) // key → action name
131- for action , key := range bindings {
132- if key == "" {
133- continue
134- }
135- if prev , ok := seen [key ]; ok {
136- conflicts = append (conflicts ,
137- fmt .Sprintf ("conflict in %s: key %q used for both %q and %q" , area , key , prev , action ))
138- } else {
139- seen [key ] = action
140- }
141- }
142- }
143-
144- check ("global" , map [string ]string {
145- "quit" : kb .Global .Quit ,
146- "cancel" : kb .Global .Cancel ,
147- "nav_up" : kb .Global .NavUp ,
148- "nav_down" : kb .Global .NavDown ,
108+ return keybind .Validate (map [string ]map [string ]string {
109+ "global" : {
110+ "quit" : kb .Global .Quit ,
111+ "cancel" : kb .Global .Cancel ,
112+ "nav_up" : kb .Global .NavUp ,
113+ "nav_down" : kb .Global .NavDown ,
114+ },
115+ "inbox" : {
116+ "visual_mode" : kb .Inbox .VisualMode ,
117+ "toggle_threaded" : kb .Inbox .ToggleThreaded ,
118+ keyDelete : kb .Inbox .Delete ,
119+ "archive" : kb .Inbox .Archive ,
120+ "refresh" : kb .Inbox .Refresh ,
121+ "search" : kb .Inbox .Search ,
122+ "filter" : kb .Inbox .Filter ,
123+ "open" : kb .Inbox .Open ,
124+ "next_tab" : kb .Inbox .NextTab ,
125+ "prev_tab" : kb .Inbox .PrevTab ,
126+ },
127+ "email" : {
128+ "reply" : kb .Email .Reply ,
129+ "forward" : kb .Email .Forward ,
130+ keyDelete : kb .Email .Delete ,
131+ "archive" : kb .Email .Archive ,
132+ "toggle_images" : kb .Email .ToggleImages ,
133+ "rsvp_accept" : kb .Email .RsvpAccept ,
134+ "rsvp_decline" : kb .Email .RsvpDecline ,
135+ "rsvp_tentative" : kb .Email .RsvpTentative ,
136+ "focus_attachments" : kb .Email .FocusAttachments ,
137+ },
138+ "composer" : {
139+ "external_editor" : kb .Composer .ExternalEditor ,
140+ "next_field" : kb .Composer .NextField ,
141+ "prev_field" : kb .Composer .PrevField ,
142+ keyDelete : kb .Composer .Delete ,
143+ // spell_* bindings intentionally excluded — spell_accept reusing
144+ // "tab" with next_field and spell_dismiss reusing "esc" with cancel
145+ // are deliberate: the spellcheck popup intercepts before those handlers.
146+ },
147+ "folder" : {
148+ "next_folder" : kb .Folder .NextFolder ,
149+ "prev_folder" : kb .Folder .PrevFolder ,
150+ "move" : kb .Folder .Move ,
151+ "focus_preview" : kb .Folder .FocusPreview ,
152+ "focus_inbox" : kb .Folder .FocusInbox ,
153+ },
154+ "drafts" : {
155+ "open" : kb .Drafts .Open ,
156+ keyDelete : kb .Drafts .Delete ,
157+ },
149158 })
150- check ("inbox" , map [string ]string {
151- "visual_mode" : kb .Inbox .VisualMode ,
152- "toggle_threaded" : kb .Inbox .ToggleThreaded ,
153- keyDelete : kb .Inbox .Delete ,
154- "archive" : kb .Inbox .Archive ,
155- "refresh" : kb .Inbox .Refresh ,
156- "search" : kb .Inbox .Search ,
157- "filter" : kb .Inbox .Filter ,
158- "open" : kb .Inbox .Open ,
159- "next_tab" : kb .Inbox .NextTab ,
160- "prev_tab" : kb .Inbox .PrevTab ,
161- })
162- check ("email" , map [string ]string {
163- "reply" : kb .Email .Reply ,
164- "forward" : kb .Email .Forward ,
165- keyDelete : kb .Email .Delete ,
166- "archive" : kb .Email .Archive ,
167- "toggle_images" : kb .Email .ToggleImages ,
168- "rsvp_accept" : kb .Email .RsvpAccept ,
169- "rsvp_decline" : kb .Email .RsvpDecline ,
170- "rsvp_tentative" : kb .Email .RsvpTentative ,
171- "focus_attachments" : kb .Email .FocusAttachments ,
172- })
173- check ("composer" , map [string ]string {
174- "external_editor" : kb .Composer .ExternalEditor ,
175- "next_field" : kb .Composer .NextField ,
176- "prev_field" : kb .Composer .PrevField ,
177- keyDelete : kb .Composer .Delete ,
178- // spell_* bindings intentionally excluded from this conflict
179- // check — spell_accept reusing "tab" with next_field, and
180- // spell_dismiss reusing "esc" with cancel, are deliberate: the
181- // spellcheck popup intercepts before those handlers fire.
182- })
183- check ("folder" , map [string ]string {
184- "next_folder" : kb .Folder .NextFolder ,
185- "prev_folder" : kb .Folder .PrevFolder ,
186- "move" : kb .Folder .Move ,
187- "focus_preview" : kb .Folder .FocusPreview ,
188- "focus_inbox" : kb .Folder .FocusInbox ,
189- })
190- check ("drafts" , map [string ]string {
191- "open" : kb .Drafts .Open ,
192- keyDelete : kb .Drafts .Delete ,
193- })
194-
195- return conflicts
196159}
0 commit comments