aboutsummaryrefslogblamecommitdiffstats
path: root/config/binds.go
blob: 5271e0b08c4d055ef66cd6cffb0e07e43d713058 (plain) (tree)
1
2
3
4
5
6
7
8
9
10






                


                

                 
                                    
                                     
                               

 











                                           






                                                  
                                  
                                    

                                  

 
                       
                               

                           






                          
                         
                           



                                                               









                                                         
 








                            
                                          
                                              

                                                     
                                                        














                                                         
 
                                    

                                                                        
                                                                                   



                                                                           
                                                                          





                                               







                                                                   















                                                                                    
                                                                      





                                          
                                            



































                                                                         
                                                                                 







                                                               





                                  














                                                                      


                                             

                                                       
                                        

















                                                                                          
                                                           





                                                                                         
                                                                                         

                                        
                                                                        


                                                                                       
                                                                       


                                                                                          

                                                                              




                  
                                    
                            



                                                                               
         

 


                                                           
                                                                        





                                            

                                                 
                












                                                                


                                                              


                                                             


                                                             
 

                
 





                                                                      

 

                                                    
                                                              


                                        

                                      


                                                                        
                                                   



                                                    


                                                                           





                                                                      















                                                            


                                                                                   
                                                   


























                                                                                                               

                                                 
                                                      

                                                    













                                                             









































































































































































































                                                                                    















                                                                                      

                                           
                                                                       
                                        
                                               
                                         



                                                                             
                                                              
                                
                                                                                


                                                                       







                                                    

                                                            
                                                         

                                                         



















                                                           
package config

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"regexp"
	"strings"

	"git.sr.ht/~rjarry/aerc/log"
	"github.com/gdamore/tcell/v2"
	"github.com/go-ini/ini"
)

type BindingConfig struct {
	Global                 *KeyBindings
	AccountWizard          *KeyBindings
	Compose                *KeyBindings
	ComposeEditor          *KeyBindings
	ComposeReview          *KeyBindings
	MessageList            *KeyBindings
	MessageView            *KeyBindings
	MessageViewPassthrough *KeyBindings
	Terminal               *KeyBindings
}

type bindsContextType int

const (
	bindsContextFolder bindsContextType = iota
	bindsContextAccount
)

type BindingConfigContext struct {
	ContextType bindsContextType
	Regex       *regexp.Regexp
	Bindings    *KeyBindings
}

type KeyStroke struct {
	Modifiers tcell.ModMask
	Key       tcell.Key
	Rune      rune
}

type Binding struct {
	Output []KeyStroke
	Input  []KeyStroke
}

type KeyBindings struct {
	Bindings []*Binding
	// If false, disable global keybindings in this context
	Globals bool
	// Which key opens the ex line (default is :)
	ExKey KeyStroke

	// private
	contextualBinds  []*BindingConfigContext
	contextualCounts map[bindsContextType]int
	contextualCache  map[bindsContextKey]*KeyBindings
}

type bindsContextKey struct {
	ctxType bindsContextType
	value   string
}

const (
	BINDING_FOUND = iota
	BINDING_INCOMPLETE
	BINDING_NOT_FOUND
)

type BindingSearchResult int

func defaultBindsConfig() *BindingConfig {
	// These bindings are not configurable
	wizard := NewKeyBindings()
	wizard.ExKey = KeyStroke{Key: tcell.KeyCtrlE}
	quit, _ := ParseBinding("<C-q>", ":quit<Enter>")
	wizard.Add(quit)
	return &BindingConfig{
		Global:                 NewKeyBindings(),
		AccountWizard:          wizard,
		Compose:                NewKeyBindings(),
		ComposeEditor:          NewKeyBindings(),
		ComposeReview:          NewKeyBindings(),
		MessageList:            NewKeyBindings(),
		MessageView:            NewKeyBindings(),
		MessageViewPassthrough: NewKeyBindings(),
		Terminal:               NewKeyBindings(),
	}
}

var Binds = defaultBindsConfig()

func parseBinds(root string) error {
	filename := path.Join(root, "binds.conf")
	if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
		fmt.Printf("%s not found, installing the system default", filename)
		if err := installTemplate(root, "binds.conf"); err != nil {
			return err
		}
	}
	log.Debugf("Parsing key bindings configuration from %s", filename)
	binds, err := ini.Load(filename)
	if err != nil {
		return err
	}

	baseGroups := map[string]**KeyBindings{
		"default":           &Binds.Global,
		"compose":           &Binds.Compose,
		"messages":          &Binds.MessageList,
		"terminal":          &Binds.Terminal,
		"view":              &Binds.MessageView,
		"view::passthrough": &Binds.MessageViewPassthrough,
		"compose::editor":   &Binds.ComposeEditor,
		"compose::review":   &Binds.ComposeReview,
	}

	// Base Bindings
	for _, sectionName := range binds.SectionStrings() {
		// Handle :: delimeter
		baseSectionName := strings.ReplaceAll(sectionName, "::", "////")
		sections := strings.Split(baseSectionName, ":")
		baseOnly := len(sections) == 1
		baseSectionName = strings.ReplaceAll(sections[0], "////", "::")

		group, ok := baseGroups[strings.ToLower(baseSectionName)]
		if !ok {
			return errors.New("Unknown keybinding group " + sectionName)
		}

		if baseOnly {
			err = LoadBinds(binds, baseSectionName, group)
			if err != nil {
				return err
			}
		}
	}

	log.Debugf("binds.conf: %#v", Binds)
	return nil
}

func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
	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("Invalid binding")
			}
			bindings.ExKey = strokes[0]
			continue
		}
		if key == "$noinherit" {
			if value == "false" {
				continue
			}
			if value != "true" {
				return nil, errors.New("Invalid binding")
			}
			bindings.Globals = false
			continue
		}
		binding, err := ParseBinding(key, value)
		if err != nil {
			return nil, err
		}
		bindings.Add(binding)
	}
	return bindings, nil
}

func LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
	if sec, err := binds.GetSection(baseName); err == nil {
		binds, err := LoadBindingSection(sec)
		if err != nil {
			return err
		}
		*baseGroup = MergeBindings(binds, *baseGroup)
	}

	b := *baseGroup

	if baseName == "default" {
		b.Globals = false
	}

	for _, sectionName := range binds.SectionStrings() {
		if !strings.Contains(sectionName, baseName+":") ||
			strings.Contains(sectionName, baseName+"::") {
			continue
		}

		bindSection, err := binds.GetSection(sectionName)
		if err != nil {
			return err
		}

		binds, err := LoadBindingSection(bindSection)
		if err != nil {
			return err
		}
		if baseName == "default" {
			binds.Globals = false
		}

		contextualBind := BindingConfigContext{
			Bindings: binds,
		}

		var index int
		if strings.Contains(sectionName, "=") {
			index = strings.Index(sectionName, "=")
			value := string(sectionName[index+1:])
			contextualBind.Regex, err = regexp.Compile(value)
			if err != nil {
				return err
			}
		} else {
			return fmt.Errorf("Invalid Bind Context regex in %s", sectionName)
		}

		switch sectionName[len(baseName)+1 : index] {
		case "account":
			acctName := sectionName[index+1:]
			valid := false
			for _, acctConf := range Accounts {
				matches := contextualBind.Regex.FindString(acctConf.Name)
				if matches != "" {
					valid = true
				}
			}
			if !valid {
				log.Warnf("binds.conf: unexistent account: %s", acctName)
				continue
			}
			contextualBind.ContextType = bindsContextAccount
		case "folder":
			// No validation needed. If the folder doesn't exist, the binds
			// never get used
			contextualBind.ContextType = bindsContextFolder
		default:
			return fmt.Errorf("Unknown Context Bind Section: %s", sectionName)
		}
		b.contextualBinds = append(b.contextualBinds, &contextualBind)
		b.contextualCounts[contextualBind.ContextType]++
	}

	return nil
}

func NewKeyBindings() *KeyBindings {
	return &KeyBindings{
		ExKey:            KeyStroke{tcell.ModNone, tcell.KeyRune, ':'},
		Globals:          true,
		contextualCache:  make(map[bindsContextKey]*KeyBindings),
		contextualCounts: make(map[bindsContextType]int),
	}
}

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 (base *KeyBindings) contextual(
	contextType bindsContextType, reg string,
) *KeyBindings {
	if base.contextualCounts[contextType] == 0 {
		// shortcut if no contextual binds for that type
		return base
	}

	key := bindsContextKey{ctxType: contextType, value: reg}
	c, found := base.contextualCache[key]
	if found {
		return c
	}

	c = base
	for _, contextualBind := range base.contextualBinds {
		if contextualBind.ContextType != contextType {
			continue
		}
		if !contextualBind.Regex.Match([]byte(reg)) {
			continue
		}
		c = MergeBindings(contextualBind.Bindings, c)
	}
	base.contextualCache[key] = c

	return c
}

func (bindings *KeyBindings) ForAccount(account string) *KeyBindings {
	return bindings.contextual(bindsContextAccount, account)
}

func (bindings *KeyBindings) ForFolder(folder string) *KeyBindings {
	return bindings.contextual(bindsContextFolder, folder)
}

func (bindings *KeyBindings) Add(binding *Binding) {
	// TODO: Search for conflicts?
	bindings.Bindings = append(bindings.Bindings, binding)
}

func (bindings *KeyBindings) GetBinding(
	input []KeyStroke,
) (BindingSearchResult, []KeyStroke) {
	incomplete := false
	// TODO: This could probably be a sorted list to speed things up
	// TODO: Deal with bindings that share a prefix
	for _, binding := range bindings.Bindings {
		if len(binding.Input) < len(input) {
			continue
		}
		for i, stroke := range input {
			if stroke.Modifiers != binding.Input[i].Modifiers {
				goto next
			}
			if stroke.Key != binding.Input[i].Key {
				goto next
			}
			if stroke.Key == tcell.KeyRune &&
				stroke.Rune != binding.Input[i].Rune {

				goto next
			}
		}
		if len(binding.Input) != len(input) {
			incomplete = true
		} else {
			return BINDING_FOUND, binding.Output
		}
	next:
	}
	if incomplete {
		return BINDING_INCOMPLETE, nil
	}
	return BINDING_NOT_FOUND, nil
}

func (bindings *KeyBindings) GetReverseBindings(output []KeyStroke) [][]KeyStroke {
	var inputs [][]KeyStroke

	for _, binding := range bindings.Bindings {
		if len(binding.Output) != len(output) {
			continue
		}
		for i, stroke := range output {
			if stroke.Modifiers != binding.Output[i].Modifiers {
				goto next
			}
			if stroke.Key != binding.Output[i].Key {
				goto next
			}
			if stroke.Key == tcell.KeyRune && stroke.Rune != binding.Output[i].Rune {
				goto next
			}
		}
		inputs = append(inputs, binding.Input)
	next:
	}
	return inputs
}

func FormatKeyStrokes(keystrokes []KeyStroke) string {
	var sb strings.Builder

	for _, stroke := range keystrokes {
		s := ""
		for name, ks := range keyNames {
			if ks.Modifiers == stroke.Modifiers && ks.Key == stroke.Key && ks.Rune == stroke.Rune {
				switch name {
				case "cr", "c-m":
					name = "enter"
				case "c-i":
					name = "tab"
				}
				s = fmt.Sprintf("<%s>", name)
				break
			}
		}
		if s == "" && stroke.Key == tcell.KeyRune {
			s = string(stroke.Rune)
		}
		sb.WriteString(s)
	}

	return sb.String()
}

var keyNames = map[string]KeyStroke{
	"space":     {tcell.ModNone, tcell.KeyRune, ' '},
	"semicolon": {tcell.ModNone, tcell.KeyRune, ';'},
	"enter":     {tcell.ModNone, tcell.KeyEnter, 0},
	"c-enter":   {tcell.ModCtrl, tcell.KeyEnter, 0},
	"a-enter":   {tcell.ModAlt, tcell.KeyEnter, 0},
	"up":        {tcell.ModNone, tcell.KeyUp, 0},
	"c-up":      {tcell.ModCtrl, tcell.KeyUp, 0},
	"a-up":      {tcell.ModAlt, tcell.KeyUp, 0},
	"down":      {tcell.ModNone, tcell.KeyDown, 0},
	"c-down":    {tcell.ModCtrl, tcell.KeyDown, 0},
	"a-down":    {tcell.ModAlt, tcell.KeyDown, 0},
	"right":     {tcell.ModNone, tcell.KeyRight, 0},
	"c-right":   {tcell.ModCtrl, tcell.KeyRight, 0},
	"a-right":   {tcell.ModAlt, tcell.KeyRight, 0},
	"left":      {tcell.ModNone, tcell.KeyLeft, 0},
	"c-left":    {tcell.ModCtrl, tcell.KeyLeft, 0},
	"a-left":    {tcell.ModAlt, tcell.KeyLeft, 0},
	"upleft":    {tcell.ModNone, tcell.KeyUpLeft, 0},
	"upright":   {tcell.ModNone, tcell.KeyUpRight, 0},
	"downleft":  {tcell.ModNone, tcell.KeyDownLeft, 0},
	"downright": {tcell.ModNone, tcell.KeyDownRight, 0},
	"center":    {tcell.ModNone, tcell.KeyCenter, 0},
	"pgup":      {tcell.ModNone, tcell.KeyPgUp, 0},
	"c-pgup":    {tcell.ModCtrl, tcell.KeyPgUp, 0},
	"a-pgup":    {tcell.ModAlt, tcell.KeyPgUp, 0},
	"pgdn":      {tcell.ModNone, tcell.KeyPgDn, 0},
	"c-pgdn":    {tcell.ModCtrl, tcell.KeyPgDn, 0},
	"a-pgdn":    {tcell.ModAlt, tcell.KeyPgDn, 0},
	"home":      {tcell.ModNone, tcell.KeyHome, 0},
	"end":       {tcell.ModNone, tcell.KeyEnd, 0},
	"insert":    {tcell.ModNone, tcell.KeyInsert, 0},
	"delete":    {tcell.ModNone, tcell.KeyDelete, 0},
	"help":      {tcell.ModNone, tcell.KeyHelp, 0},
	"exit":      {tcell.ModNone, tcell.KeyExit, 0},
	"clear":     {tcell.ModNone, tcell.KeyClear, 0},
	"cancel":    {tcell.ModNone, tcell.KeyCancel, 0},
	"print":     {tcell.ModNone, tcell.KeyPrint, 0},
	"pause":     {tcell.ModNone, tcell.KeyPause, 0},
	"backtab":   {tcell.ModNone, tcell.KeyBacktab, 0},
	"f1":        {tcell.ModNone, tcell.KeyF1, 0},
	"f2":        {tcell.ModNone, tcell.KeyF2, 0},
	"f3":        {tcell.ModNone, tcell.KeyF3, 0},
	"f4":        {tcell.ModNone, tcell.KeyF4, 0},
	"f5":        {tcell.ModNone, tcell.KeyF5, 0},
	"f6":        {tcell.ModNone, tcell.KeyF6, 0},
	"f7":        {tcell.ModNone, tcell.KeyF7, 0},
	"f8":        {tcell.ModNone, tcell.KeyF8, 0},
	"f9":        {tcell.ModNone, tcell.KeyF9, 0},
	"f10":       {tcell.ModNone, tcell.KeyF10, 0},
	"f11":       {tcell.ModNone, tcell.KeyF11, 0},
	"f12":       {tcell.ModNone, tcell.KeyF12, 0},
	"f13":       {tcell.ModNone, tcell.KeyF13, 0},
	"f14":       {tcell.ModNone, tcell.KeyF14, 0},
	"f15":       {tcell.ModNone, tcell.KeyF15, 0},
	"f16":       {tcell.ModNone, tcell.KeyF16, 0},
	"f17":       {tcell.ModNone, tcell.KeyF17, 0},
	"f18":       {tcell.ModNone, tcell.KeyF18, 0},
	"f19":       {tcell.ModNone, tcell.KeyF19, 0},
	"f20":       {tcell.ModNone, tcell.KeyF20, 0},
	"f21":       {tcell.ModNone, tcell.KeyF21, 0},
	"f22":       {tcell.ModNone, tcell.KeyF22, 0},
	"f23":       {tcell.ModNone, tcell.KeyF23, 0},
	"f24":       {tcell.ModNone, tcell.KeyF24, 0},
	"f25":       {tcell.ModNone, tcell.KeyF25, 0},
	"f26":       {tcell.ModNone, tcell.KeyF26, 0},
	"f27":       {tcell.ModNone, tcell.KeyF27, 0},
	"f28":       {tcell.ModNone, tcell.KeyF28, 0},
	"f29":       {tcell.ModNone, tcell.KeyF29, 0},
	"f30":       {tcell.ModNone, tcell.KeyF30, 0},
	"f31":       {tcell.ModNone, tcell.KeyF31, 0},
	"f32":       {tcell.ModNone, tcell.KeyF32, 0},
	"f33":       {tcell.ModNone, tcell.KeyF33, 0},
	"f34":       {tcell.ModNone, tcell.KeyF34, 0},
	"f35":       {tcell.ModNone, tcell.KeyF35, 0},
	"f36":       {tcell.ModNone, tcell.KeyF36, 0},
	"f37":       {tcell.ModNone, tcell.KeyF37, 0},
	"f38":       {tcell.ModNone, tcell.KeyF38, 0},
	"f39":       {tcell.ModNone, tcell.KeyF39, 0},
	"f40":       {tcell.ModNone, tcell.KeyF40, 0},
	"f41":       {tcell.ModNone, tcell.KeyF41, 0},
	"f42":       {tcell.ModNone, tcell.KeyF42, 0},
	"f43":       {tcell.ModNone, tcell.KeyF43, 0},
	"f44":       {tcell.ModNone, tcell.KeyF44, 0},
	"f45":       {tcell.ModNone, tcell.KeyF45, 0},
	"f46":       {tcell.ModNone, tcell.KeyF46, 0},
	"f47":       {tcell.ModNone, tcell.KeyF47, 0},
	"f48":       {tcell.ModNone, tcell.KeyF48, 0},
	"f49":       {tcell.ModNone, tcell.KeyF49, 0},
	"f50":       {tcell.ModNone, tcell.KeyF50, 0},
	"f51":       {tcell.ModNone, tcell.KeyF51, 0},
	"f52":       {tcell.ModNone, tcell.KeyF52, 0},
	"f53":       {tcell.ModNone, tcell.KeyF53, 0},
	"f54":       {tcell.ModNone, tcell.KeyF54, 0},
	"f55":       {tcell.ModNone, tcell.KeyF55, 0},
	"f56":       {tcell.ModNone, tcell.KeyF56, 0},
	"f57":       {tcell.ModNone, tcell.KeyF57, 0},
	"f58":       {tcell.ModNone, tcell.KeyF58, 0},
	"f59":       {tcell.ModNone, tcell.KeyF59, 0},
	"f60":       {tcell.ModNone, tcell.KeyF60, 0},
	"f61":       {tcell.ModNone, tcell.KeyF61, 0},
	"f62":       {tcell.ModNone, tcell.KeyF62, 0},
	"f63":       {tcell.ModNone, tcell.KeyF63, 0},
	"f64":       {tcell.ModNone, tcell.KeyF64, 0},
	"c-space":   {tcell.ModCtrl, tcell.KeyCtrlSpace, 0},
	"c-a":       {tcell.ModCtrl, tcell.KeyCtrlA, 0},
	"c-b":       {tcell.ModCtrl, tcell.KeyCtrlB, 0},
	"c-c":       {tcell.ModCtrl, tcell.KeyCtrlC, 0},
	"c-d":       {tcell.ModCtrl, tcell.KeyCtrlD, 0},
	"c-e":       {tcell.ModCtrl, tcell.KeyCtrlE, 0},
	"c-f":       {tcell.ModCtrl, tcell.KeyCtrlF, 0},
	"c-g":       {tcell.ModCtrl, tcell.KeyCtrlG, 0},
	"c-h":       {tcell.ModNone, tcell.KeyCtrlH, 0},
	"c-i":       {tcell.ModNone, tcell.KeyCtrlI, 0},
	"c-j":       {tcell.ModCtrl, tcell.KeyCtrlJ, 0},
	"c-k":       {tcell.ModCtrl, tcell.KeyCtrlK, 0},
	"c-l":       {tcell.ModCtrl, tcell.KeyCtrlL, 0},
	"c-m":       {tcell.ModNone, tcell.KeyCtrlM, 0},
	"c-n":       {tcell.ModCtrl, tcell.KeyCtrlN, 0},
	"c-o":       {tcell.ModCtrl, tcell.KeyCtrlO, 0},
	"c-p":       {tcell.ModCtrl, tcell.KeyCtrlP, 0},
	"c-q":       {tcell.ModCtrl, tcell.KeyCtrlQ, 0},
	"c-r":       {tcell.ModCtrl, tcell.KeyCtrlR, 0},
	"c-s":       {tcell.ModCtrl, tcell.KeyCtrlS, 0},
	"c-t":       {tcell.ModCtrl, tcell.KeyCtrlT, 0},
	"c-u":       {tcell.ModCtrl, tcell.KeyCtrlU, 0},
	"c-v":       {tcell.ModCtrl, tcell.KeyCtrlV, 0},
	"c-w":       {tcell.ModCtrl, tcell.KeyCtrlW, 0},
	"c-x":       {tcell.ModCtrl, tcell.KeyCtrlX, rune(tcell.KeyCAN)},
	"c-y":       {tcell.ModCtrl, tcell.KeyCtrlY, 0}, // TODO: runes for the rest
	"c-z":       {tcell.ModCtrl, tcell.KeyCtrlZ, 0},
	"c-]":       {tcell.ModCtrl, tcell.KeyCtrlRightSq, 0},
	"c-\\":      {tcell.ModCtrl, tcell.KeyCtrlBackslash, 0},
	"c-[":       {tcell.ModCtrl, tcell.KeyCtrlLeftSq, 0},
	"c-^":       {tcell.ModCtrl, tcell.KeyCtrlCarat, 0},
	"c-_":       {tcell.ModCtrl, tcell.KeyCtrlUnderscore, 0},
	"a-space":   {tcell.ModAlt, tcell.KeyRune, ' '},
	"a-a":       {tcell.ModAlt, tcell.KeyRune, 'a'},
	"a-b":       {tcell.ModAlt, tcell.KeyRune, 'b'},
	"a-c":       {tcell.ModAlt, tcell.KeyRune, 'c'},
	"a-d":       {tcell.ModAlt, tcell.KeyRune, 'd'},
	"a-e":       {tcell.ModAlt, tcell.KeyRune, 'e'},
	"a-f":       {tcell.ModAlt, tcell.KeyRune, 'f'},
	"a-g":       {tcell.ModAlt, tcell.KeyRune, 'g'},
	"a-h":       {tcell.ModAlt, tcell.KeyRune, 'h'},
	"a-i":       {tcell.ModAlt, tcell.KeyRune, 'i'},
	"a-j":       {tcell.ModAlt, tcell.KeyRune, 'j'},
	"a-k":       {tcell.ModAlt, tcell.KeyRune, 'k'},
	"a-l":       {tcell.ModAlt, tcell.KeyRune, 'l'},
	"a-m":       {tcell.ModAlt, tcell.KeyRune, 'm'},
	"a-n":       {tcell.ModAlt, tcell.KeyRune, 'n'},
	"a-o":       {tcell.ModAlt, tcell.KeyRune, 'o'},
	"a-p":       {tcell.ModAlt, tcell.KeyRune, 'p'},
	"a-q":       {tcell.ModAlt, tcell.KeyRune, 'q'},
	"a-r":       {tcell.ModAlt, tcell.KeyRune, 'r'},
	"a-s":       {tcell.ModAlt, tcell.KeyRune, 's'},
	"a-t":       {tcell.ModAlt, tcell.KeyRune, 't'},
	"a-u":       {tcell.ModAlt, tcell.KeyRune, 'u'},
	"a-v":       {tcell.ModAlt, tcell.KeyRune, 'v'},
	"a-w":       {tcell.ModAlt, tcell.KeyRune, 'w'},
	"a-x":       {tcell.ModAlt, tcell.KeyRune, 'x'},
	"a-y":       {tcell.ModAlt, tcell.KeyRune, 'y'},
	"a-z":       {tcell.ModAlt, tcell.KeyRune, 'z'},
	"a-]":       {tcell.ModAlt, tcell.KeyRune, ']'},
	"a-\\":      {tcell.ModAlt, tcell.KeyRune, '\\'},
	"a-[":       {tcell.ModAlt, tcell.KeyRune, '['},
	"a-^":       {tcell.ModAlt, tcell.KeyRune, '^'},
	"a-_":       {tcell.ModAlt, tcell.KeyRune, '_'},
	"nul":       {tcell.ModNone, tcell.KeyNUL, 0},
	"soh":       {tcell.ModNone, tcell.KeySOH, 0},
	"stx":       {tcell.ModNone, tcell.KeySTX, 0},
	"etx":       {tcell.ModNone, tcell.KeyETX, 0},
	"eot":       {tcell.ModNone, tcell.KeyEOT, 0},
	"enq":       {tcell.ModNone, tcell.KeyENQ, 0},
	"ack":       {tcell.ModNone, tcell.KeyACK, 0},
	"bel":       {tcell.ModNone, tcell.KeyBEL, 0},
	"bs":        {tcell.ModNone, tcell.KeyBS, 0},
	"tab":       {tcell.ModNone, tcell.KeyTAB, 0},
	"lf":        {tcell.ModNone, tcell.KeyLF, 0},
	"vt":        {tcell.ModNone, tcell.KeyVT, 0},
	"ff":        {tcell.ModNone, tcell.KeyFF, 0},
	"cr":        {tcell.ModNone, tcell.KeyCR, 0},
	"so":        {tcell.ModNone, tcell.KeySO, 0},
	"si":        {tcell.ModNone, tcell.KeySI, 0},
	"dle":       {tcell.ModNone, tcell.KeyDLE, 0},
	"dc1":       {tcell.ModNone, tcell.KeyDC1, 0},
	"dc2":       {tcell.ModNone, tcell.KeyDC2, 0},
	"dc3":       {tcell.ModNone, tcell.KeyDC3, 0},
	"dc4":       {tcell.ModNone, tcell.KeyDC4, 0},
	"nak":       {tcell.ModNone, tcell.KeyNAK, 0},
	"syn":       {tcell.ModNone, tcell.KeySYN, 0},
	"etb":       {tcell.ModNone, tcell.KeyETB, 0},
	"can":       {tcell.ModNone, tcell.KeyCAN, 0},
	"em":        {tcell.ModNone, tcell.KeyEM, 0},
	"sub":       {tcell.ModNone, tcell.KeySUB, 0},
	"esc":       {tcell.ModNone, tcell.KeyESC, 0},
	"fs":        {tcell.ModNone, tcell.KeyFS, 0},
	"gs":        {tcell.ModNone, tcell.KeyGS, 0},
	"rs":        {tcell.ModNone, tcell.KeyRS, 0},
	"us":        {tcell.ModNone, tcell.KeyUS, 0},
	"del":       {tcell.ModNone, tcell.KeyDEL, 0},
}

func ParseKeyStrokes(keystrokes string) ([]KeyStroke, error) {
	var strokes []KeyStroke
	buf := bytes.NewBufferString(keystrokes)
	for {
		tok, _, err := buf.ReadRune()
		if err == io.EOF {
			break
		} else if err != nil {
			return nil, err
		}
		// TODO: make it possible to bind to < or > themselves (and default to
		// switching accounts)
		switch tok {
		case '<':
			name, err := buf.ReadString(byte('>'))
			switch {
			case err == io.EOF:
				return nil, errors.New("Expecting '>'")
			case err != nil:
				return nil, err
			case name == ">":
				return nil, errors.New("Expected a key name")
			}
			name = name[:len(name)-1]
			if key, ok := keyNames[strings.ToLower(name)]; ok {
				strokes = append(strokes, key)
			} else {
				return nil, fmt.Errorf("Unknown key '%s'", name)
			}
		case '>':
			return nil, errors.New("Found '>' without '<'")
		case '\\':
			tok, _, err = buf.ReadRune()
			if err == io.EOF {
				tok = '\\'
			} else if err != nil {
				return nil, err
			}
			fallthrough
		default:
			strokes = append(strokes, KeyStroke{
				Modifiers: tcell.ModNone,
				Key:       tcell.KeyRune,
				Rune:      tok,
			})
		}
	}
	return strokes, nil
}

func ParseBinding(input, output string) (*Binding, error) {
	in, err := ParseKeyStrokes(input)
	if err != nil {
		return nil, err
	}
	out, err := ParseKeyStrokes(output)
	if err != nil {
		return nil, err
	}
	return &Binding{
		Input:  in,
		Output: out,
	}, nil
}