~sircmpwn/aerc

Reworked account specific bindings v4 PROPOSED

Jonathan Bartlett: 1
 Reworked account specific bindings

 4 files changed, 166 insertions(+), 158 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/22742/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH v4] Reworked account specific bindings Export this patch

Account specific bindings now follows the same structure as contextual
UI configuration. It also supports regular expressions and provides
a base for further binding subcontexts in the future.
---
 config/bindings.go    |  22 ++++
 config/config.go      | 240 ++++++++++++++++++++++--------------------
 doc/aerc-config.5.scd |  10 +-
 widgets/aerc.go       |  52 ++-------
 4 files changed, 166 insertions(+), 158 deletions(-)

diff --git a/config/bindings.go b/config/bindings.go
index 9956b41..23a082e 100644
--- a/config/bindings.go
+++ b/config/bindings.go
@@ -54,6 +54,28 @@ func MergeBindings(bindings ...*KeyBindings) *KeyBindings {
	return merged
}

func (config AercConfig) MergeContextualBinds(baseBinds *KeyBindings,
	contextType ContextType, reg string, bindCtx string) *KeyBindings {

	bindings := baseBinds
	for _, contextualBind := range config.ContextualBinds {
		if contextualBind.ContextType != contextType {
			continue
		}

		if !contextualBind.Regex.Match([]byte(reg)) {
			continue
		}

		if contextualBind.BindContext != bindCtx {
			continue
		}

		bindings = MergeBindings(bindings, contextualBind.Bindings)
	}
	return bindings
}

func (bindings *KeyBindings) Add(binding *Binding) {
	// TODO: Search for conflicts?
	bindings.bindings = append(bindings.bindings, binding)
diff --git a/config/config.go b/config/config.go
index 4f39f29..cfc1757 100644
--- a/config/config.go
+++ b/config/config.go
@@ -56,6 +56,7 @@ const (
	UI_CONTEXT_FOLDER ContextType = iota
	UI_CONTEXT_ACCOUNT
	UI_CONTEXT_SUBJECT
	BIND_CONTEXT_ACCOUNT
)

type UIConfigContext struct {
@@ -90,19 +91,21 @@ type AccountConfig struct {
}

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

type BindingGroup struct {
	Base    *KeyBindings
	Account map[string]*KeyBindings
type BindingConfigContext struct {
	ContextType ContextType
	Regex       *regexp.Regexp
	Bindings    *KeyBindings
	BindContext string
}

type ComposeConfig struct {
@@ -139,17 +142,18 @@ type TemplateConfig struct {
}

type AercConfig struct {
	Bindings      BindingConfig
	Compose       ComposeConfig
	Ini           *ini.File       `ini:"-"`
	Accounts      []AccountConfig `ini:"-"`
	Filters       []FilterConfig  `ini:"-"`
	Viewer        ViewerConfig    `ini:"-"`
	Triggers      TriggersConfig  `ini:"-"`
	Ui            UIConfig
	ContextualUis []UIConfigContext
	General       GeneralConfig
	Templates     TemplateConfig
	Bindings        BindingConfig
	ContextualBinds []BindingConfigContext
	Compose         ComposeConfig
	Ini             *ini.File       `ini:"-"`
	Accounts        []AccountConfig `ini:"-"`
	Filters         []FilterConfig  `ini:"-"`
	Viewer          ViewerConfig    `ini:"-"`
	Triggers        TriggersConfig  `ini:"-"`
	Ui              UIConfig
	ContextualUis   []UIConfigContext
	General         GeneralConfig
	Templates       TemplateConfig
}

// Input: TimestampFormat
@@ -350,11 +354,13 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
			}
		}
	}

	if ui, err := file.GetSection("ui"); err == nil {
		if err := ui.MapTo(&config.Ui); err != nil {
			return err
		}
	}

	for _, sectionName := range file.SectionStrings() {
		if !strings.Contains(sectionName, "ui:") {
			continue
@@ -484,9 +490,18 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
	file.NameMapper = mapName
	config := &AercConfig{
		Bindings: BindingConfig{
			AccountWizard: &BindingGroup{Base: NewKeyBindings()},
			Global:        NewKeyBindings(),
			AccountWizard: NewKeyBindings(),
			Compose:       NewKeyBindings(),
			ComposeEditor: NewKeyBindings(),
			ComposeReview: NewKeyBindings(),
			MessageList:   NewKeyBindings(),
			MessageView:   NewKeyBindings(),
			Terminal:      NewKeyBindings(),
		},

		ContextualBinds: []BindingConfigContext{},

		Ini: file,

		Ui: UIConfig{
@@ -542,11 +557,11 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
	}

	// These bindings are not configurable
	config.Bindings.AccountWizard.Base.ExKey = KeyStroke{
	config.Bindings.AccountWizard.ExKey = KeyStroke{
		Key: tcell.KeyCtrlE,
	}
	quit, _ := ParseBinding("<C-q>", ":quit<Enter>")
	config.Bindings.AccountWizard.Base.Add(quit)
	config.Bindings.AccountWizard.Add(quit)

	if err = config.LoadConfig(file); err != nil {
		return nil, err
@@ -576,120 +591,121 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
		}
	}

	bindSectionMap := map[string][]string{
		"default":         []string{},
		"compose":         []string{},
		"messages":        []string{},
		"terminal":        []string{},
		"view":            []string{},
		"compose::editor": []string{},
		"compose::review": []string{},
	baseGroups := map[string]**KeyBindings{
		"default":         &config.Bindings.Global,
		"compose":         &config.Bindings.Compose,
		"messages":        &config.Bindings.MessageList,
		"terminal":        &config.Bindings.Terminal,
		"view":            &config.Bindings.MessageView,
		"compose::editor": &config.Bindings.ComposeEditor,
		"compose::review": &config.Bindings.ComposeReview,
	}
	for _, name := range binds.SectionStrings() {
		parts := strings.Split(name, "//")
		base := parts[0]
		section, ok := bindSectionMap[strings.ToLower(base)]
		if !ok {
			return nil, errors.New("Invalid section name " + name + ", Base was: " + base)

	// Base Bindings
	for name, group := range baseGroups {
		err = config.LoadBinds(binds, name, group)
		if err != nil {
			return nil, err
		}
		section = append(section, name)
		bindSectionMap[strings.ToLower(base)] = section
	}

	acctNames := []string{}
	for _, acctConf := range config.Accounts {
		acctNames = append(acctNames, acctConf.Name)
	config.Bindings.Global.Globals = false
	for _, contextBind := range config.ContextualBinds {
		if contextBind.BindContext == "default" {
			contextBind.Bindings.Globals = false
		}
	}

	bindingGroups := make(map[string]*BindingGroup)
	for baseName, secNames := range bindSectionMap {
		group, err := NewBindingGroup(binds, acctNames, secNames, baseName)
	return config, 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
		}
		bindingGroups[baseName] = group
		bindings.Add(binding)
	}
	return bindings, nil
}

	config.Bindings.Global = bindingGroups["default"]
	config.Bindings.Compose = bindingGroups["compose"]
	config.Bindings.ComposeEditor = bindingGroups["compose::editor"]
	config.Bindings.ComposeReview = bindingGroups["compose::review"]
	config.Bindings.MessageList = bindingGroups["messages"]
	config.Bindings.MessageView = bindingGroups["view"]
	config.Bindings.Terminal = bindingGroups["terminal"]
func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {

	// Globals can't inherit from themselves
	config.Bindings.Global.Base.Globals = false
	for _, acctBinds := range config.Bindings.Global.Account {
		acctBinds.Globals = false
	if sec, err := binds.GetSection(baseName); err == nil {
		binds, err := LoadBindingSection(sec)
		if err != nil {
			return err
		}
		*baseGroup = MergeBindings(binds, *baseGroup)
	}
	return config, nil
}

func NewBindingGroup(binds *ini.File, accountNames []string, secNames []string, baseName string) (*BindingGroup, error) {
	group := &BindingGroup{
		Base: NewKeyBindings(), Account: make(map[string]*KeyBindings)}
	for _, name := range accountNames {
		group.Account[strings.ToLower(name)] = NewKeyBindings()
	}
	for _, name := range secNames {
		parts := strings.Split(name, "//")
	for _, sectionName := range binds.SectionStrings() {
		if !strings.Contains(sectionName, baseName+":") ||
			strings.Contains(sectionName, baseName+"::") {
			continue
		}

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

		subgroup := &KeyBindings{}
		if len(parts) == 2 {
			sub, ok := group.Account[strings.ToLower(parts[1])]
			if !ok {
				return nil, errors.New("Invalid binding subgroup name " + name)
			}
			subgroup = sub
		} else if len(parts) == 1 {
			subgroup = group.Base
		} else {
			return nil, errors.New("Invalid binding group name " + name)
		binds, err := LoadBindingSection(bindSection)
		if err != nil {
			return err
		}

		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
		contextualBind :=
			BindingConfigContext{
				Bindings:    binds,
				BindContext: baseName,
			}
			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)

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

		if len(parts) == 1 {
			group.Base = MergeBindings(bindings, subgroup)
		} else {
			group.Account[strings.ToLower(parts[1])] = MergeBindings(bindings, subgroup)
		switch sectionName[len(baseName)+1 : index] {
		case "account":
			contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
		default:
			return fmt.Errorf("Unknown Context Bind Section: %s", sectionName)
		}
		config.ContextualBinds = append(config.ContextualBinds, contextualBind)
	}
	return group, nil

	return nil
}

// checkConfigPerms checks for too open permissions
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 511864c..fb032cb 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -489,16 +489,16 @@ are:

You may also configure account specific key bindings for each context:

*[context//accountname]*
	keybindings for this context and account, where *accountname* matches
	the name provided in *accounts.conf*
*[context:account=regex]*
	keybindings for this context and account, where *regex* matches
	the account name provided in *accounts.conf*

Example:
```
[messages//mailbox]
[messages:account=Mailbox]
c = :cf path:mailbox/** and<space>

[compose::editor//mailbox2]
[compose::editor:account=Mailbox2]
...
```

diff --git a/widgets/aerc.go b/widgets/aerc.go
index 35efa3b..5661260 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -184,58 +184,28 @@ func (aerc *Aerc) Draw(ctx *ui.Context) {
func (aerc *Aerc) getBindings() *config.KeyBindings {
	selectedAccountName := ""
	if aerc.SelectedAccount() != nil {
		selectedAccountName = strings.ToLower(aerc.SelectedAccount().acct.Name)
		selectedAccountName = aerc.SelectedAccount().acct.Name
	}
	switch view := aerc.SelectedTab().(type) {
	case *AccountView:
		acctBinds, ok := aerc.conf.Bindings.MessageList.Account[selectedAccountName]
		if !ok {
			return aerc.conf.Bindings.MessageList.Base
		} else {
			return config.MergeBindings(aerc.conf.Bindings.MessageList.Base, acctBinds)
		}
		return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.MessageList, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "messages")
	case *AccountWizard:
		return aerc.conf.Bindings.AccountWizard.Base
		return aerc.conf.Bindings.AccountWizard
	case *Composer:
		switch view.Bindings() {
		case "compose::editor":
			acctBinds, ok := aerc.conf.Bindings.ComposeEditor.Account[selectedAccountName]
			if !ok {
				return aerc.conf.Bindings.ComposeEditor.Base
			} else {
				return config.MergeBindings(aerc.conf.Bindings.ComposeEditor.Base, acctBinds)
			}
			return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.ComposeEditor, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose::editor")
		case "compose::review":
			acctBinds, ok := aerc.conf.Bindings.ComposeReview.Account[selectedAccountName]
			if !ok {
				return aerc.conf.Bindings.ComposeReview.Base
			} else {
				return config.MergeBindings(aerc.conf.Bindings.ComposeReview.Base, acctBinds)
			}
			return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.ComposeReview, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose::review")
		default:
			acctBinds, ok := aerc.conf.Bindings.Compose.Account[selectedAccountName]
			if !ok {
				return aerc.conf.Bindings.Compose.Base
			} else {
				return config.MergeBindings(aerc.conf.Bindings.Compose.Base, acctBinds)
			}
			return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.Compose, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose")
		}
	case *MessageViewer:
		acctBinds, ok := aerc.conf.Bindings.MessageView.Account[selectedAccountName]
		if !ok {
			return aerc.conf.Bindings.MessageView.Base
		} else {
			return config.MergeBindings(aerc.conf.Bindings.MessageView.Base, acctBinds)
		}
		return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.MessageView, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "view")
	case *Terminal:
		return aerc.conf.Bindings.Terminal.Base
		return aerc.conf.Bindings.Terminal
	default:
		acctBinds, ok := aerc.conf.Bindings.Global.Account[selectedAccountName]
		if !ok {
			return aerc.conf.Bindings.Global.Base
		} else {
			return config.MergeBindings(aerc.conf.Bindings.Global.Base, acctBinds)
		}
		return aerc.conf.Bindings.Global
	}
}

@@ -279,7 +249,7 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
		case config.BINDING_NOT_FOUND:
		}
		if bindings.Globals {
			result, strokes = aerc.conf.Bindings.Global.Base.
			result, strokes = aerc.conf.Bindings.Global.
				GetBinding(aerc.pendingKeys)
			switch result {
			case config.BINDING_FOUND:
@@ -295,7 +265,7 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
			exKey := bindings.ExKey
			if aerc.simulating > 0 {
				// Keybindings still use : even if you change the ex key
				exKey = aerc.conf.Bindings.Global.Base.ExKey
				exKey = aerc.conf.Bindings.Global.ExKey
			}
			if event.Key() == exKey.Key && event.Rune() == exKey.Rune {
				aerc.BeginExCommand("")
-- 
2.31.1