diff --git a/config/config.go b/config/config.go index 617968f..ce8f9b0 100644 --- a/config/config.go +++ b/config/config.go @@ -29,6 +29,9 @@ import ( "maunium.net/go/mautrix/id" "maunium.net/go/mautrix/pushrules" + "github.com/3nprob/cbind" + "maunium.net/go/tcell" + "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/rooms" ) @@ -56,6 +59,26 @@ type UserPreferences struct { AltEnterToSend bool `yaml:"alt_enter_to_send"` } +type Keybind struct { + Mod tcell.ModMask + Key tcell.Key + Ch rune +} + +type Keybindings struct { + Main map[Keybind]string `yaml:"main,omitempty"` + Room map[Keybind]string `yaml:"room,omitempty"` + Modal map[Keybind]string `yaml:"modal,omitempty"` + Visual map[Keybind]string `yaml:"visual,omitempty"` +} + +type KeybindingsConfig struct { + Main map[string]string `yaml:"main,omitempty"` + Room map[string]string `yaml:"room,omitempty"` + Modal map[string]string `yaml:"modal,omitempty"` + Visual map[string]string `yaml:"visual,omitempty"` +} + // Config contains the main config of gomuks. type Config struct { UserID id.UserID `yaml:"mxid"` @@ -85,6 +108,7 @@ type Config struct { AuthCache AuthCache `yaml:"-"` Rooms *rooms.RoomCache `yaml:"-"` PushRules *pushrules.PushRuleset `yaml:"-"` + Keybindings Keybindings `yaml:"-"` nosave bool } @@ -152,6 +176,7 @@ func (config *Config) LoadAll() { config.LoadAuthCache() config.LoadPushRules() config.LoadPreferences() + config.LoadKeybindings() err := config.Rooms.LoadList() if err != nil { panic(err) @@ -189,6 +214,70 @@ func (config *Config) SavePreferences() { config.save("user preferences", config.CacheDir, "preferences.yaml", &config.Preferences) } +func (config *Config) LoadKeybindings() { + cfg := KeybindingsConfig{} + config.load("keybindings", config.Dir, "keybindings.yaml", &cfg) + config.Keybindings.Main = make(map[Keybind]string) + config.Keybindings.Room = make(map[Keybind]string) + config.Keybindings.Modal = make(map[Keybind]string) + config.Keybindings.Visual = make(map[Keybind]string) + + for k, v := range cfg.Main { + mod, key, ch, err := cbind.Decode(k) + if err != nil { + // todo + } + kb := Keybind{ + Mod: mod, + Key: key, + Ch: ch, + } + config.Keybindings.Main[kb] = v + } + for k, v := range cfg.Room { + mod, key, ch, err := cbind.Decode(k) + if err != nil { + // todo + } + kb := Keybind{ + Mod: mod, + Key: key, + Ch: ch, + } + config.Keybindings.Room[kb] = v + } + + for k, v := range cfg.Modal { + mod, key, ch, err := cbind.Decode(k) + if err != nil { + // todo + } + kb := Keybind{ + Mod: mod, + Key: key, + Ch: ch, + } + config.Keybindings.Modal[kb] = v + } + + for k, v := range cfg.Visual { + mod, key, ch, err := cbind.Decode(k) + if err != nil { + // todo + } + kb := Keybind{ + Mod: mod, + Key: key, + Ch: ch, + } + config.Keybindings.Visual[kb] = v + } +} + +func (config *Config) SaveKeybindings() { + config.save("keybindings", config.Dir, "keybindings.yaml", &config.Keybindings) +} + func (config *Config) LoadAuthCache() { config.load("auth cache", config.CacheDir, "auth-cache.yaml", &config.AuthCache) } diff --git a/go.mod b/go.mod index b1313e3..de5e918 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module maunium.net/go/gomuks go 1.14 require ( + github.com/3nprob/cbind v0.0.0-20211207125121-3a585abdeddb github.com/alecthomas/chroma v0.8.2 github.com/disintegration/imaging v1.6.2 github.com/gabriel-vasile/mimetype v1.2.0 diff --git a/go.sum b/go.sum index c6cb721..f74bec7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/3nprob/cbind v0.0.0-20211207125121-3a585abdeddb h1:1MTRfYbQB9Ew5k3feAgtTFZwBfOkWdNaupWWJKa4+Vc= +github.com/3nprob/cbind v0.0.0-20211207125121-3a585abdeddb/go.mod h1:qfgvjR6/wfSG5BBbzVHSWoJuSZhEIu4GXlBArOqYssI= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= @@ -119,13 +121,16 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/keybindings.sample.yaml b/keybindings.sample.yaml new file mode 100644 index 0000000..5f41da2 --- /dev/null +++ b/keybindings.sample.yaml @@ -0,0 +1,40 @@ +--- +main: + 'Ctrl+Down': next_room + 'Ctrl+Up': prev_room + 'Ctrl+k': search_rooms + 'Ctrl+Home': scroll_up + 'Ctrl+End': scroll_down + 'Ctrl+Enter': add_newline + 'Ctrl+a': next_active_room + 'Ctrl+l': show_bare + 'Alt+Down': next_room + 'Alt+Up': prev_room + 'Alt+k': search_rooms + 'Alt+Home': scroll_up + 'Alt+End': scroll_down + 'Alt+Enter': add_newline + 'Alt+a': next_active_room + 'Alt+l': show_bare + +modal: + 'Tab': select_next + 'Backtab': select_prev + 'Enter': confirm + 'Escape': cancel + +visual: + 'Escape': clear + 'h': clear + 'Up': select_prev + 'k': select_prev + 'Down': select_next + 'j': select_next + 'Enter': confirm + 'l': confirm + +room: + 'Escape': clear + 'PageUp': scroll_up + 'PageDown': scroll_down + 'Enter': send diff --git a/ui/fuzzy-search-modal.go b/ui/fuzzy-search-modal.go index b96f758..d22826c 100644 --- a/ui/fuzzy-search-modal.go +++ b/ui/fuzzy-search-modal.go @@ -27,6 +27,7 @@ import ( "maunium.net/go/mauview" "maunium.net/go/tcell" + "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/rooms" ) @@ -120,12 +121,17 @@ func (fs *FuzzySearchModal) changeHandler(str string) { func (fs *FuzzySearchModal) OnKeyEvent(event mauview.KeyEvent) bool { highlights := fs.results.GetHighlights() - switch event.Key() { - case tcell.KeyEsc: + kb := config.Keybind{ + Key: event.Key(), + Ch: event.Rune(), + Mod: event.Modifiers(), + } + switch fs.parent.config.Keybindings.Modal[kb] { + case "cancel": // Close room finder fs.parent.HideModal() return true - case tcell.KeyTab: + case "select_next": // Cycle highlighted area to next match if len(highlights) > 0 { fs.selected = (fs.selected + 1) % len(fs.matches) @@ -133,7 +139,7 @@ func (fs *FuzzySearchModal) OnKeyEvent(event mauview.KeyEvent) bool { fs.results.ScrollToHighlight() } return true - case tcell.KeyBacktab: + case "select_prev": if len(highlights) > 0 { fs.selected = (fs.selected - 1) % len(fs.matches) if fs.selected < 0 { @@ -143,7 +149,7 @@ func (fs *FuzzySearchModal) OnKeyEvent(event mauview.KeyEvent) bool { fs.results.ScrollToHighlight() } return true - case tcell.KeyEnter: + case "confirm": // Switch room to currently selected room if len(highlights) > 0 { debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].OriginalIndex].GetTitle()) diff --git a/ui/help-modal.go b/ui/help-modal.go index 36b57c6..b3bf657 100644 --- a/ui/help-modal.go +++ b/ui/help-modal.go @@ -1,6 +1,7 @@ package ui import ( + "maunium.net/go/gomuks/config" "maunium.net/go/tcell" "maunium.net/go/mauview" @@ -94,7 +95,12 @@ func NewHelpModal(parent *MainView) *HelpModal { } func (hm *HelpModal) OnKeyEvent(event mauview.KeyEvent) bool { - if event.Key() == tcell.KeyEscape || event.Rune() == 'q' { + kb := config.Keybind{ + Key: event.Key(), + Ch: event.Rune(), + Mod: event.Modifiers(), + } + if hm.parent.config.Keybindings.Room[kb] == "cancel" { hm.parent.HideModal() return true } diff --git a/ui/room-view.go b/ui/room-view.go index 116448c..dd0e25b 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -37,7 +37,7 @@ import ( "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/interface" + ifc "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/open" "maunium.net/go/gomuks/lib/util" "maunium.net/go/gomuks/matrix/muksevt" @@ -339,41 +339,44 @@ func (view *RoomView) ClearAllContext() { func (view *RoomView) OnKeyEvent(event mauview.KeyEvent) bool { msgView := view.MessageView() + kb := config.Keybind{ + Key: event.Key(), + Ch: event.Rune(), + Mod: event.Modifiers(), + } + if view.selecting { - k := event.Key() - c := event.Rune() - switch { - case k == tcell.KeyEscape || c == 'h': + switch view.config.Keybindings.Visual[kb] { + case "clear": view.ClearAllContext() - case k == tcell.KeyUp || c == 'k': + case "select_prev": view.SelectPrevious() - case k == tcell.KeyDown || c == 'j': + case "select_next": view.SelectNext() - case k == tcell.KeyEnter || c == 'l': + case "confirm": view.OnSelect(msgView.selected) default: return false } return true } - switch event.Key() { - case tcell.KeyEscape: + + switch view.config.Keybindings.Room[kb] { + case "clear": view.ClearAllContext() return true - case tcell.KeyPgUp: + case "scroll_up": if msgView.IsAtTop() { go view.parent.LoadHistory(view.Room.ID) } msgView.AddScrollOffset(+msgView.Height() / 2) return true - case tcell.KeyPgDn: + case "scroll_down": msgView.AddScrollOffset(-msgView.Height() / 2) return true - case tcell.KeyEnter: - if (event.Modifiers()&tcell.ModShift == 0 && event.Modifiers()&tcell.ModCtrl == 0) != (view.config.Preferences.AltEnterToSend) { - view.InputSubmit(view.input.GetText()) - return true - } + case "send": + view.InputSubmit(view.input.GetText()) + return true } return view.input.OnKeyEvent(event) } diff --git a/ui/verification-modal.go b/ui/verification-modal.go index 8c7714f..86ea90a 100644 --- a/ui/verification-modal.go +++ b/ui/verification-modal.go @@ -27,6 +27,7 @@ import ( "maunium.net/go/mauview" "maunium.net/go/tcell" + "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/event" @@ -207,8 +208,13 @@ func (vm *VerificationModal) OnSuccess() { } func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { + kb := config.Keybind{ + Key: event.Key(), + Ch: event.Rune(), + Mod: event.Modifiers(), + } if vm.done { - if event.Key() == tcell.KeyEnter || event.Key() == tcell.KeyEsc { + if vm.parent.config.Keybindings.Modal[kb] == "cancel" || vm.parent.config.Keybindings.Modal[kb] == "confirm" { vm.parent.HideModal() return true } @@ -217,7 +223,7 @@ func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { debug.Print("Ignoring pre-emoji key event") return false } - if event.Key() == tcell.KeyEnter { + if vm.parent.config.Keybindings.Modal[kb] == "confirm" { text := strings.ToLower(strings.TrimSpace(vm.inputBar.GetText())) if text == "yes" { debug.Print("Confirming verification") diff --git a/ui/view-main.go b/ui/view-main.go index fc1b9c9..df28742 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -33,7 +33,7 @@ import ( "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/interface" + ifc "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/notification" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/widget" @@ -170,33 +170,34 @@ func (view *MainView) OnKeyEvent(event mauview.KeyEvent) bool { return view.modal.OnKeyEvent(event) } - k := event.Key() - c := event.Rune() - if event.Modifiers() == tcell.ModCtrl || event.Modifiers() == tcell.ModAlt { - switch { - case k == tcell.KeyDown: - view.SwitchRoom(view.roomList.Next()) - case k == tcell.KeyUp: - view.SwitchRoom(view.roomList.Previous()) - case c == 'k' || k == tcell.KeyCtrlK: - view.ShowModal(NewFuzzySearchModal(view, 42, 12)) - case k == tcell.KeyHome: - msgView := view.currentRoom.MessageView() - msgView.AddScrollOffset(msgView.TotalHeight()) - case k == tcell.KeyEnd: - msgView := view.currentRoom.MessageView() - msgView.AddScrollOffset(-msgView.TotalHeight()) - case k == tcell.KeyEnter: - return view.flex.OnKeyEvent(tcell.NewEventKey(tcell.KeyEnter, '\n', event.Modifiers()|tcell.ModShift, "")) - case c == 'a': - view.SwitchRoom(view.roomList.NextWithActivity()) - case c == 'l' || k == tcell.KeyCtrlL: - view.ShowBare(view.currentRoom) - default: - goto defaultHandler - } - return true + kb := config.Keybind{ + Key: event.Key(), + Ch: event.Rune(), + Mod: event.Modifiers(), } + switch view.config.Keybindings.Main[kb] { + case "next_room": + view.SwitchRoom(view.roomList.Next()) + case "prev_room": + view.SwitchRoom(view.roomList.Previous()) + case "search_rooms": + view.ShowModal(NewFuzzySearchModal(view, 42, 12)) + case "scroll_up": + msgView := view.currentRoom.MessageView() + msgView.AddScrollOffset(msgView.TotalHeight()) + case "scroll_down": + msgView := view.currentRoom.MessageView() + msgView.AddScrollOffset(-msgView.TotalHeight()) + case "add_newline": + return view.flex.OnKeyEvent(tcell.NewEventKey(tcell.KeyEnter, '\n', event.Modifiers()|tcell.ModShift, "")) + case "next_active_room": + view.SwitchRoom(view.roomList.NextWithActivity()) + case "show_bare": + view.ShowBare(view.currentRoom) + default: + goto defaultHandler + } + return true defaultHandler: if view.config.Preferences.HideRoomList { return view.roomView.OnKeyEvent(event)