1package config
2
3import (
4 _ "embed"
5 "encoding/json"
6
7 keybind "github.com/floatpane/go-keybind"
8)
9
10const keyDelete = "delete" // used in ValidateKeybinds action map keys
11
12//go:embed default_keybinds.json
13var defaultKeybindsJSON []byte
14
15// Keybinds is the active keybind configuration. Initialized to defaults at
16// package init; overwritten by LoadKeybindsFromDir when config is loaded.
17var Keybinds = defaultKeybinds()
18
19// KeybindsConfig holds all configurable key bindings organized by area.
20type KeybindsConfig struct {
21 Global GlobalKeys `json:"global"`
22 Inbox InboxKeys `json:"inbox"`
23 Email EmailKeys `json:"email"`
24 Composer ComposerKeys `json:"composer"`
25 Folder FolderKeys `json:"folder"`
26 Drafts DraftsKeys `json:"drafts"`
27}
28
29type GlobalKeys struct {
30 Quit string `json:"quit"`
31 Cancel string `json:"cancel"`
32 NavUp string `json:"nav_up"`
33 NavDown string `json:"nav_down"`
34}
35
36type InboxKeys struct {
37 VisualMode string `json:"visual_mode"`
38 ToggleThreaded string `json:"toggle_threaded"`
39 Delete string `json:"delete"`
40 Archive string `json:"archive"`
41 Refresh string `json:"refresh"`
42 Search string `json:"search"`
43 Filter string `json:"filter"`
44 Open string `json:"open"`
45 NextTab string `json:"next_tab"`
46 PrevTab string `json:"prev_tab"`
47}
48
49type EmailKeys struct {
50 Reply string `json:"reply"`
51 Forward string `json:"forward"`
52 Delete string `json:"delete"`
53 Archive string `json:"archive"`
54 ToggleImages string `json:"toggle_images"`
55 RsvpAccept string `json:"rsvp_accept"`
56 RsvpDecline string `json:"rsvp_decline"`
57 RsvpTentative string `json:"rsvp_tentative"`
58 FocusAttachments string `json:"focus_attachments"`
59}
60
61type ComposerKeys struct {
62 ExternalEditor string `json:"external_editor"`
63 NextField string `json:"next_field"`
64 PrevField string `json:"prev_field"`
65 Delete string `json:"delete"`
66 SpellNext string `json:"spell_next"`
67 SpellPrev string `json:"spell_prev"`
68 SpellAccept string `json:"spell_accept"`
69 SpellDismiss string `json:"spell_dismiss"`
70 UndoSend string `json:"undo_send"`
71}
72
73type FolderKeys struct {
74 NextFolder string `json:"next_folder"`
75 PrevFolder string `json:"prev_folder"`
76 Move string `json:"move"`
77 FocusPreview string `json:"focus_preview"`
78 FocusInbox string `json:"focus_inbox"`
79}
80
81type DraftsKeys struct {
82 Open string `json:"open"`
83 Delete string `json:"delete"`
84}
85
86func defaultKeybinds() KeybindsConfig {
87 var kb KeybindsConfig
88 if err := json.Unmarshal(defaultKeybindsJSON, &kb); err != nil {
89 panic("matcha: malformed default_keybinds.json: " + err.Error())
90 }
91 return kb
92}
93
94// LoadKeybindsFromDir reads keybinds.json from cfgDir, writing defaults if
95// the file does not exist, then updates the package-level Keybinds var.
96func LoadKeybindsFromDir(cfgDir string) error {
97 kb, err := keybind.Load(cfgDir, "keybinds.json", defaultKeybinds())
98 if err != nil {
99 return err
100 }
101 Keybinds = kb
102 return nil
103}
104
105// ValidateKeybinds returns a list of conflict descriptions where two different
106// actions within the same area are mapped to the same key. Cross-area
107// duplicates are intentional (e.g. "d" = delete in both inbox and email view).
108func ValidateKeybinds(kb KeybindsConfig) []string {
109 return keybind.Validate(map[string]map[string]string{
110 "global": {
111 "quit": kb.Global.Quit,
112 "cancel": kb.Global.Cancel,
113 "nav_up": kb.Global.NavUp,
114 "nav_down": kb.Global.NavDown,
115 },
116 "inbox": {
117 "visual_mode": kb.Inbox.VisualMode,
118 "toggle_threaded": kb.Inbox.ToggleThreaded,
119 keyDelete: kb.Inbox.Delete,
120 "archive": kb.Inbox.Archive,
121 "refresh": kb.Inbox.Refresh,
122 "search": kb.Inbox.Search,
123 "filter": kb.Inbox.Filter,
124 "open": kb.Inbox.Open,
125 "next_tab": kb.Inbox.NextTab,
126 "prev_tab": kb.Inbox.PrevTab,
127 },
128 "email": {
129 "reply": kb.Email.Reply,
130 "forward": kb.Email.Forward,
131 keyDelete: kb.Email.Delete,
132 "archive": kb.Email.Archive,
133 "toggle_images": kb.Email.ToggleImages,
134 "rsvp_accept": kb.Email.RsvpAccept,
135 "rsvp_decline": kb.Email.RsvpDecline,
136 "rsvp_tentative": kb.Email.RsvpTentative,
137 "focus_attachments": kb.Email.FocusAttachments,
138 },
139 "composer": {
140 "undo_send": kb.Composer.UndoSend,
141 "external_editor": kb.Composer.ExternalEditor,
142 "next_field": kb.Composer.NextField,
143 "prev_field": kb.Composer.PrevField,
144 keyDelete: kb.Composer.Delete,
145 // spell_* bindings intentionally excluded — spell_accept reusing
146 // "tab" with next_field and spell_dismiss reusing "esc" with cancel
147 // are deliberate: the spellcheck popup intercepts before those handlers.
148 },
149 "folder": {
150 "next_folder": kb.Folder.NextFolder,
151 "prev_folder": kb.Folder.PrevFolder,
152 "move": kb.Folder.Move,
153 "focus_preview": kb.Folder.FocusPreview,
154 "focus_inbox": kb.Folder.FocusInbox,
155 },
156 "drafts": {
157 "open": kb.Drafts.Open,
158 keyDelete: kb.Drafts.Delete,
159 },
160 })
161}