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}
71
72type FolderKeys struct {
73 NextFolder string `json:"next_folder"`
74 PrevFolder string `json:"prev_folder"`
75 Move string `json:"move"`
76 FocusPreview string `json:"focus_preview"`
77 FocusInbox string `json:"focus_inbox"`
78}
79
80type DraftsKeys struct {
81 Open string `json:"open"`
82 Delete string `json:"delete"`
83}
84
85func defaultKeybinds() KeybindsConfig {
86 var kb KeybindsConfig
87 if err := json.Unmarshal(defaultKeybindsJSON, &kb); err != nil {
88 panic("matcha: malformed default_keybinds.json: " + err.Error())
89 }
90 return kb
91}
92
93// LoadKeybindsFromDir reads keybinds.json from cfgDir, writing defaults if
94// the file does not exist, then updates the package-level Keybinds var.
95func LoadKeybindsFromDir(cfgDir string) error {
96 kb, err := keybind.Load(cfgDir, "keybinds.json", defaultKeybinds())
97 if err != nil {
98 return err
99 }
100 Keybinds = kb
101 return nil
102}
103
104// ValidateKeybinds returns a list of conflict descriptions where two different
105// actions within the same area are mapped to the same key. Cross-area
106// duplicates are intentional (e.g. "d" = delete in both inbox and email view).
107func ValidateKeybinds(kb KeybindsConfig) []string {
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 },
158 })
159}