Add context-specific keybindings

This commit is contained in:
Drew DeVault 2019-03-21 17:36:42 -04:00
parent 79b459ecb0
commit f5bf4a9324
4 changed files with 152 additions and 20 deletions

View File

@ -44,6 +44,16 @@ func NewKeyBindings() *KeyBindings {
} }
} }
func MergeBindings(bindings ...*KeyBindings) *KeyBindings {
merged := NewKeyBindings()
for _, b := range bindings {
merged.bindings = append(merged.bindings, b.bindings...)
}
merged.ExKey = bindings[0].ExKey
merged.Globals = bindings[0].Globals
return merged
}
func (bindings *KeyBindings) Add(binding *Binding) { func (bindings *KeyBindings) Add(binding *Binding) {
// TODO: Search for conflicts? // TODO: Search for conflicts?
bindings.bindings = append(bindings.bindings, binding) bindings.bindings = append(bindings.bindings, binding)

32
config/binds.conf Normal file
View File

@ -0,0 +1,32 @@
# Binds are of the form <key sequence> = <command to run>
# To use '=' in a key sequence, substitute it with "Eq": "<Ctrl+Eq>"
# If you wish to bind #, you can wrap the key sequence in quotes: "#" = quit
q = :quit<Enter>
L = :next-tab<Enter>
H = :prev-tab<Enter>
<C-t> = :term<Enter>
[messages]
j = :next-message<Enter>
<Down> = :next-message<Enter>
<C-d> = :next-message 50%<Enter>
<C-f> = :next-message 100%<Enter>
<PgDn> = :next-message -s 100%<Enter>
k = :prev-message<Enter>
<Up> = :prev-message<Enter>
<C-u> = :prev-message 50%<Enter>
<C-b> = :prev-message 100%<Enter>
<PgUp> = :prev-message -s 100%<Enter>
g = :select-message 0<Enter>
G = :select-message -1<Enter>
J = :next-folder<Enter>
K = :prev-folder<Enter>
<Enter> = :view-message<Enter>
d = :confirm 'Really delete this message?' ':delete-message<Enter>'<Enter>
D = :delete-message<Enter>
c = :cf<space>
$ = :term<space>

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"path" "path"
"strings" "strings"
@ -29,8 +30,16 @@ type AccountConfig struct {
Params map[string]string Params map[string]string
} }
type BindingConfig struct {
Global *KeyBindings
Compose *KeyBindings
MessageList *KeyBindings
MessageView *KeyBindings
Terminal *KeyBindings
}
type AercConfig struct { type AercConfig struct {
Lbinds *KeyBindings Bindings BindingConfig
Ini *ini.File `ini:"-"` Ini *ini.File `ini:"-"`
Accounts []AccountConfig `ini:"-"` Accounts []AccountConfig `ini:"-"`
Ui UIConfig Ui UIConfig
@ -98,7 +107,13 @@ func LoadConfig(root *string) (*AercConfig, error) {
} }
file.NameMapper = mapName file.NameMapper = mapName
config := &AercConfig{ config := &AercConfig{
Lbinds: NewKeyBindings(), Bindings: BindingConfig{
Global: NewKeyBindings(),
Compose: NewKeyBindings(),
MessageList: NewKeyBindings(),
MessageView: NewKeyBindings(),
Terminal: NewKeyBindings(),
},
Ini: file, Ini: file,
Ui: UIConfig{ Ui: UIConfig{
@ -121,20 +136,65 @@ func LoadConfig(root *string) (*AercConfig, error) {
return nil, err return nil, err
} }
} }
if lbinds, err := file.GetSection("lbinds"); err == nil {
for key, value := range lbinds.KeysHash() {
binding, err := ParseBinding(key, value)
if err != nil {
return nil, err
}
config.Lbinds.Add(binding)
}
}
accountsPath := path.Join(*root, "accounts.conf") accountsPath := path.Join(*root, "accounts.conf")
if accounts, err := loadAccountConfig(accountsPath); err != nil { if accounts, err := loadAccountConfig(accountsPath); err != nil {
return nil, err return nil, err
} else { } else {
config.Accounts = accounts config.Accounts = accounts
} }
binds, err := ini.Load(path.Join(*root, "binds.conf"))
if err != nil {
return nil, err
}
groups := map[string]**KeyBindings{
"default": &config.Bindings.Global,
"compose": &config.Bindings.Compose,
"messages": &config.Bindings.MessageList,
"terminal": &config.Bindings.Terminal,
"view": &config.Bindings.MessageView,
}
for _, name := range binds.SectionStrings() {
sec, err := binds.GetSection(name)
if err != nil {
return nil, err
}
group, ok := groups[strings.ToLower(name)]
if !ok {
return nil, errors.New("Unknown keybinding group " + name)
}
bindings := NewKeyBindings()
for key, value := range sec.KeysHash() {
if key == "$ex" {
strokes, err := ParseKeyStrokes(value)
if err != nil {
return nil, err
}
if len(strokes) != 1 {
return nil, errors.New(
"Error: only one keystroke supported for $ex")
}
bindings.ExKey = strokes[0]
continue
}
if key == "$noinherit" {
if value == "false" {
continue
}
if value != "true" {
return nil, errors.New(
"Error: expected 'true' or 'false' for $noinherit")
}
bindings.Globals = false
}
binding, err := ParseBinding(key, value)
if err != nil {
return nil, err
}
bindings.Add(binding)
}
*group = MergeBindings(bindings, *group)
}
// Globals can't inherit from themselves
config.Bindings.Global.Globals = false
return config, nil return config, nil
} }

View File

@ -94,6 +94,24 @@ func (aerc *Aerc) Draw(ctx *libui.Context) {
aerc.grid.Draw(ctx) aerc.grid.Draw(ctx)
} }
func (aerc *Aerc) getBindings() *config.KeyBindings {
switch aerc.SelectedTab().(type) {
case *AccountView:
return aerc.conf.Bindings.MessageList
default:
return aerc.conf.Bindings.Global
}
}
func (aerc *Aerc) simulate(strokes []config.KeyStroke) {
aerc.pendingKeys = []config.KeyStroke{}
for _, stroke := range strokes {
simulated := tcell.NewEventKey(
stroke.Key, stroke.Rune, tcell.ModNone)
aerc.Event(simulated)
}
}
func (aerc *Aerc) Event(event tcell.Event) bool { func (aerc *Aerc) Event(event tcell.Event) bool {
if aerc.focused != nil { if aerc.focused != nil {
return aerc.focused.Event(event) return aerc.focused.Event(event)
@ -105,18 +123,30 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
Key: event.Key(), Key: event.Key(),
Rune: event.Rune(), Rune: event.Rune(),
}) })
result, output := aerc.conf.Lbinds.GetBinding(aerc.pendingKeys) bindings := aerc.getBindings()
incomplete := false
result, strokes := bindings.GetBinding(aerc.pendingKeys)
switch result { switch result {
case config.BINDING_FOUND: case config.BINDING_FOUND:
aerc.pendingKeys = []config.KeyStroke{} aerc.simulate(strokes)
for _, stroke := range output { return true
simulated := tcell.NewEventKey(
stroke.Key, stroke.Rune, tcell.ModNone)
aerc.Event(simulated)
}
case config.BINDING_INCOMPLETE: case config.BINDING_INCOMPLETE:
return false incomplete = true
case config.BINDING_NOT_FOUND: case config.BINDING_NOT_FOUND:
}
if bindings.Globals {
result, strokes = aerc.conf.Bindings.Global.
GetBinding(aerc.pendingKeys)
switch result {
case config.BINDING_FOUND:
aerc.simulate(strokes)
return true
case config.BINDING_INCOMPLETE:
incomplete = true
case config.BINDING_NOT_FOUND:
}
}
if !incomplete {
aerc.pendingKeys = []config.KeyStroke{} aerc.pendingKeys = []config.KeyStroke{}
if event.Rune() == ':' { if event.Rune() == ':' {
aerc.BeginExCommand() aerc.BeginExCommand()