~sircmpwn/aerc

Account Specific Bindings v4 PROPOSED

Jonathan Bartlett: 3
 Account Specific Bindings
 Formatting
 Reworked account specific bindings

 9 files changed, 458 insertions(+), 341 deletions(-)
Hi,
Next
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/22743/mbox | git am -3
Learn more about email & git

[PATCH v4 1/3] Account Specific Bindings Export this patch

---
 config/config.go      | 148 +++++++++++++++++++++++++++++-------------
 doc/aerc-config.5.scd |  15 +++++
 widgets/aerc.go       |  54 ++++++++++++---
 3 files changed, 163 insertions(+), 54 deletions(-)

diff --git a/config/config.go b/config/config.go
index af9c63b..3ecf2b7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -90,14 +90,19 @@ type AccountConfig struct {
}

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

type BindingGroup struct {
    Base          *KeyBindings
    Account       map[string]*KeyBindings
}

type ComposeConfig struct {
@@ -478,16 +483,10 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
	}
	file.NameMapper = mapName
	config := &AercConfig{
		Bindings: BindingConfig{
			Global:        NewKeyBindings(),
			AccountWizard: NewKeyBindings(),
			Compose:       NewKeyBindings(),
			ComposeEditor: NewKeyBindings(),
			ComposeReview: NewKeyBindings(),
			MessageList:   NewKeyBindings(),
			MessageView:   NewKeyBindings(),
			Terminal:      NewKeyBindings(),
		},
        Bindings: BindingConfig{
            AccountWizard: &BindingGroup{Base: NewKeyBindings()},
        },

		Ini: file,

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

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

	if err = config.LoadConfig(file); err != nil {
		return nil, err
@@ -565,6 +564,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
	} else {
		config.Accounts = accounts
	}

	filename = path.Join(*root, "binds.conf")
	binds, err := ini.Load(filename)
	if err != nil {
@@ -575,25 +575,84 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			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,

		"compose::editor": &config.Bindings.ComposeEditor,
		"compose::review": &config.Bindings.ComposeReview,
	}
    bindSectionMap := map[string][]string {
        "default": []string{},
        "compose": []string{},
        "messages": []string{},
        "terminal": []string{},
        "view": []string{},
        "compose::editor": []string{},
        "compose::review": []string{},
    }
	for _, name := range binds.SectionStrings() {
		sec, err := binds.GetSection(name)
        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)
        }
        section = append(section, name)
        bindSectionMap[strings.ToLower(base)] = section
	}

    acctNames := []string{}
    for _, acctConf := range config.Accounts {
        acctNames = append(acctNames, acctConf.Name)
    }
    
    bindingGroups := make(map[string]*BindingGroup)
    for baseName, secNames := range bindSectionMap {
        group, err := NewBindingGroup(binds, acctNames, secNames, baseName)
        if err != nil {
            return nil, err
        }
        bindingGroups[baseName] = group
    }
    
    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"]

    // Globals can't inherit from themselves
	config.Bindings.Global.Base.Globals = false
    for _, acctBinds := range config.Bindings.Global.Account {
        acctBinds.Globals = false
    }
	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, "//")

        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)
		}

        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)
        }

		bindings := NewKeyBindings()
		for key, value := range sec.KeysHash() {
			if key == "$ex" {
@@ -602,8 +661,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
					return nil, err
				}
				if len(strokes) != 1 {
					return nil, errors.New(
						"Error: only one keystroke supported for $ex")
					return nil, errors.New("Invalid binding")
				}
				bindings.ExKey = strokes[0]
				continue
@@ -613,8 +671,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
					continue
				}
				if value != "true" {
					return nil, errors.New(
						"Error: expected 'true' or 'false' for $noinherit")
					return nil, errors.New("Invalid binding")
				}
				bindings.Globals = false
				continue
@@ -625,11 +682,14 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			}
			bindings.Add(binding)
		}
		*group = MergeBindings(bindings, *group)
	}
	// Globals can't inherit from themselves
	config.Bindings.Global.Globals = false
	return config, nil
        
        if len(parts) == 1 {
            group.Base = MergeBindings(bindings, subgroup)
        } else {
            group.Account[strings.ToLower(parts[1])] = MergeBindings(bindings, subgroup)
        }
    }
    return group, nil   
}

// checkConfigPerms checks for too open permissions
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index d4de883..511864c 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -487,6 +487,21 @@ are:
*[terminal]*
	keybindings for terminal tabs

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*

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

[compose::editor//mailbox2]
...
```

You may also configure global keybindings by placing them at the beginning of
the file, before specifying any context-specific sections. For each *key=value*
option specified, the _key_ is the keystrokes pressed (in order) to invoke this
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 6df0c95..d515536 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -182,26 +182,60 @@ 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);
    }
	switch view := aerc.SelectedTab().(type) {
	case *AccountView:
		return aerc.conf.Bindings.MessageList
        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)
        }
	case *AccountWizard:
		return aerc.conf.Bindings.AccountWizard
		return aerc.conf.Bindings.AccountWizard.Base
	case *Composer:
		switch view.Bindings() {
		case "compose::editor":
			return aerc.conf.Bindings.ComposeEditor
            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)
            }
		case "compose::review":
			return aerc.conf.Bindings.ComposeReview
            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)
            }
		default:
			return aerc.conf.Bindings.Compose
            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)
            }
		}
	case *MessageViewer:
		return aerc.conf.Bindings.MessageView
        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)
        }
	case *Terminal:
		return aerc.conf.Bindings.Terminal
        return aerc.conf.Bindings.Terminal.Base
	default:
		return aerc.conf.Bindings.Global
        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)
        }
	}
}

@@ -245,7 +279,7 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
		case config.BINDING_NOT_FOUND:
		}
		if bindings.Globals {
			result, strokes = aerc.conf.Bindings.Global.
			result, strokes = aerc.conf.Bindings.Global.Base.
				GetBinding(aerc.pendingKeys)
			switch result {
			case config.BINDING_FOUND:
@@ -261,7 +295,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.ExKey
				exKey = aerc.conf.Bindings.Global.Base.ExKey
			}
			if event.Key() == exKey.Key && event.Rune() == exKey.Rune {
				aerc.BeginExCommand("")
-- 
2.31.1
Hi,
You seem to have a bit of a problem as to how the email patch submission works.
Maybe follow that tutorial here: https://git-send-email.io/ ?

Please rebase your changes, as explained in that tutorial.

Also, not sure why you tend to send multiple patches in very short time increments...
Maybe slow down a tad on the submission?

Cheers,
Reto

[PATCH v4 2/3] Formatting Export this patch

---
 config/config.go | 176 +++++++++++++++++++++++------------------------
 widgets/aerc.go  |  82 +++++++++++-----------
 2 files changed, 129 insertions(+), 129 deletions(-)

diff --git a/config/config.go b/config/config.go
index 3ecf2b7..4f39f29 100644
--- a/config/config.go
+++ b/config/config.go
@@ -90,19 +90,19 @@ type AccountConfig struct {
}

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

type BindingGroup struct {
    Base          *KeyBindings
    Account       map[string]*KeyBindings
	Base    *KeyBindings
	Account map[string]*KeyBindings
}

type ComposeConfig struct {
@@ -483,9 +483,9 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
	}
	file.NameMapper = mapName
	config := &AercConfig{
        Bindings: BindingConfig{
            AccountWizard: &BindingGroup{Base: NewKeyBindings()},
        },
		Bindings: BindingConfig{
			AccountWizard: &BindingGroup{Base: NewKeyBindings()},
		},

		Ini: file,

@@ -541,7 +541,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
		},
	}

    // These bindings are not configurable
	// These bindings are not configurable
	config.Bindings.AccountWizard.Base.ExKey = KeyStroke{
		Key: tcell.KeyCtrlE,
	}
@@ -576,82 +576,82 @@ 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{},
    }
	bindSectionMap := map[string][]string{
		"default":         []string{},
		"compose":         []string{},
		"messages":        []string{},
		"terminal":        []string{},
		"view":            []string{},
		"compose::editor": []string{},
		"compose::review": []string{},
	}
	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)
        }
        section = append(section, name)
        bindSectionMap[strings.ToLower(base)] = section
	}

    acctNames := []string{}
    for _, acctConf := range config.Accounts {
        acctNames = append(acctNames, acctConf.Name)
    }
    
    bindingGroups := make(map[string]*BindingGroup)
    for baseName, secNames := range bindSectionMap {
        group, err := NewBindingGroup(binds, acctNames, secNames, baseName)
        if err != nil {
            return nil, err
        }
        bindingGroups[baseName] = group
    }
    
    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"]

    // Globals can't inherit from themselves
		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)
		}
		section = append(section, name)
		bindSectionMap[strings.ToLower(base)] = section
	}

	acctNames := []string{}
	for _, acctConf := range config.Accounts {
		acctNames = append(acctNames, acctConf.Name)
	}

	bindingGroups := make(map[string]*BindingGroup)
	for baseName, secNames := range bindSectionMap {
		group, err := NewBindingGroup(binds, acctNames, secNames, baseName)
		if err != nil {
			return nil, err
		}
		bindingGroups[baseName] = group
	}

	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"]

	// Globals can't inherit from themselves
	config.Bindings.Global.Base.Globals = false
    for _, acctBinds := range config.Bindings.Global.Account {
        acctBinds.Globals = false
    }
	for _, acctBinds := range config.Bindings.Global.Account {
		acctBinds.Globals = false
	}
	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, "//")

        sec, err := binds.GetSection(name)
	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, "//")

		sec, err := binds.GetSection(name)
		if err != nil {
			return nil, 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)
        }
		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)
		}

		bindings := NewKeyBindings()
		for key, value := range sec.KeysHash() {
@@ -682,14 +682,14 @@ func NewBindingGroup(binds *ini.File, accountNames []string, secNames []string,
			}
			bindings.Add(binding)
		}
        
        if len(parts) == 1 {
            group.Base = MergeBindings(bindings, subgroup)
        } else {
            group.Account[strings.ToLower(parts[1])] = MergeBindings(bindings, subgroup)
        }
    }
    return group, nil   

		if len(parts) == 1 {
			group.Base = MergeBindings(bindings, subgroup)
		} else {
			group.Account[strings.ToLower(parts[1])] = MergeBindings(bindings, subgroup)
		}
	}
	return group, nil
}

// checkConfigPerms checks for too open permissions
diff --git a/widgets/aerc.go b/widgets/aerc.go
index d515536..35efa3b 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -182,60 +182,60 @@ 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 := ""
	if aerc.SelectedAccount() != nil {
		selectedAccountName = strings.ToLower(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)
        }
		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)
		}
	case *AccountWizard:
		return aerc.conf.Bindings.AccountWizard.Base
	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)
            }
			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)
			}
		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)
            }
			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)
			}
		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)
            }
			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)
			}
		}
	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)
        }
		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)
		}
	case *Terminal:
        return aerc.conf.Bindings.Terminal.Base
		return aerc.conf.Bindings.Terminal.Base
	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)
        }
		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)
		}
	}
}

-- 
2.31.1

[PATCH v4 3/3] 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