~sircmpwn/aerc

f5bf4a93243c62b5b30ed0f1d15c124739444c79 — Drew DeVault 1 year, 8 months ago 79b459e
Add context-specific keybindings
4 files changed, 152 insertions(+), 20 deletions(-)

M config/bindings.go
A config/binds.conf
M config/config.go
M widgets/aerc.go
M config/bindings.go => config/bindings.go +10 -0
@@ 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) {
	// TODO: Search for conflicts?
	bindings.bindings = append(bindings.bindings, binding)

A config/binds.conf => config/binds.conf +32 -0
@@ 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>

M config/config.go => config/config.go +72 -12
@@ 1,6 1,7 @@
package config

import (
	"errors"
	"fmt"
	"path"
	"strings"


@@ 29,8 30,16 @@ type AccountConfig struct {
	Params  map[string]string
}

type BindingConfig struct {
	Global      *KeyBindings
	Compose     *KeyBindings
	MessageList *KeyBindings
	MessageView *KeyBindings
	Terminal    *KeyBindings
}

type AercConfig struct {
	Lbinds   *KeyBindings
	Bindings BindingConfig
	Ini      *ini.File       `ini:"-"`
	Accounts []AccountConfig `ini:"-"`
	Ui       UIConfig


@@ 98,8 107,14 @@ func LoadConfig(root *string) (*AercConfig, error) {
	}
	file.NameMapper = mapName
	config := &AercConfig{
		Lbinds: NewKeyBindings(),
		Ini:    file,
		Bindings: BindingConfig{
			Global:      NewKeyBindings(),
			Compose:     NewKeyBindings(),
			MessageList: NewKeyBindings(),
			MessageView: NewKeyBindings(),
			Terminal:    NewKeyBindings(),
		},
		Ini: file,

		Ui: UIConfig{
			IndexFormat:     "%4C %Z %D %-17.17n %s",


@@ 121,20 136,65 @@ func LoadConfig(root *string) (*AercConfig, error) {
			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")
	if accounts, err := loadAccountConfig(accountsPath); err != nil {
		return nil, err
	} else {
		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
}

M widgets/aerc.go => widgets/aerc.go +38 -8
@@ 94,6 94,24 @@ func (aerc *Aerc) Draw(ctx *libui.Context) {
	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 {
	if aerc.focused != nil {
		return aerc.focused.Event(event)


@@ 105,18 123,30 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
			Key:  event.Key(),
			Rune: event.Rune(),
		})
		result, output := aerc.conf.Lbinds.GetBinding(aerc.pendingKeys)
		bindings := aerc.getBindings()
		incomplete := false
		result, strokes := bindings.GetBinding(aerc.pendingKeys)
		switch result {
		case config.BINDING_FOUND:
			aerc.pendingKeys = []config.KeyStroke{}
			for _, stroke := range output {
				simulated := tcell.NewEventKey(
					stroke.Key, stroke.Rune, tcell.ModNone)
				aerc.Event(simulated)
			}
			aerc.simulate(strokes)
			return true
		case config.BINDING_INCOMPLETE:
			return false
			incomplete = true
		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{}
			if event.Rune() == ':' {
				aerc.BeginExCommand()