binds: add account specific bindings

When using aerc for multiple accounts often bindings might differ
slightly between accounts. For example:

* Account A archives to one directory (:archive)
* Account B archives to monthly directories (:archive month)

Add account specific bindings to allow the user to add a "context" to a
binding group using a context specifier and a regular expression.

Currently the only context specifier is 'account'.

The regular expression is validated against the accounts loaded from
accounts.conf and the configuration fails to load if there are no
matches.

Contextual bindings are merged with global bindings, with contextual
bindings taking precedence, when that context is active.

Bindings are be configured using a generic pattern of
'view:context=regexp'. E.g.:

    # Globally Applicable Archiving
    [messages]
    A = :read<Enter>:archive<Enter>

    # Monthly Archiving for 'Mailbox' Account
    [messages:account=Mailbox$]
    A = :read<Enter>:archive month<Enter>

In the above example all accounts matching the regular expression will
archive in the monthly format - all others will use the global binding.

Signed-off-by: Jonathan Bartlett <jonathan@jonnobrow.co.uk>
This commit is contained in:
Jonathan Bartlett 2021-12-10 21:27:29 +00:00 committed by Robin Jarry
parent b84374a572
commit 175d0efeb2
4 changed files with 199 additions and 58 deletions

View File

@ -55,6 +55,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(contextualBind.Bindings, bindings)
}
return bindings
}
func (bindings *KeyBindings) Add(binding *Binding) {
// TODO: Search for conflicts?
bindings.bindings = append(bindings.bindings, binding)

View File

@ -64,6 +64,7 @@ const (
UI_CONTEXT_FOLDER ContextType = iota
UI_CONTEXT_ACCOUNT
UI_CONTEXT_SUBJECT
BIND_CONTEXT_ACCOUNT
)
type UIConfigContext struct {
@ -109,6 +110,13 @@ type BindingConfig struct {
Terminal *KeyBindings
}
type BindingConfigContext struct {
ContextType ContextType
Regex *regexp.Regexp
Bindings *KeyBindings
BindContext string
}
type ComposeConfig struct {
Editor string `ini:"editor"`
HeaderLayout [][]string `ini:"-"`
@ -143,17 +151,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
@ -357,6 +366,7 @@ 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
@ -365,6 +375,7 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
return err
}
}
for _, sectionName := range file.SectionStrings() {
if !strings.Contains(sectionName, "ui:") {
continue
@ -526,6 +537,9 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
MessageView: NewKeyBindings(),
Terminal: NewKeyBindings(),
},
ContextualBinds: []BindingConfigContext{},
Ini: file,
Ui: UIConfig{
@ -609,6 +623,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 {
@ -619,63 +634,148 @@ 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,
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() {
sec, err := binds.GetSection(name)
if err != nil {
return nil, err
}
group, ok := groups[strings.ToLower(name)]
// Base Bindings
for _, sectionName := range binds.SectionStrings() {
// Handle :: delimeter
baseSectionName := strings.Replace(sectionName, "::", "////", -1)
sections := strings.Split(baseSectionName, ":")
baseOnly := len(sections) == 1
baseSectionName = strings.Replace(sections[0], "////", "::", -1)
group, ok := baseGroups[strings.ToLower(baseSectionName)]
if !ok {
return nil, errors.New("Unknown keybinding group " + name)
return nil, errors.New("Unknown keybinding group " + sectionName)
}
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
continue
}
binding, err := ParseBinding(key, value)
if baseOnly {
err = config.LoadBinds(binds, baseSectionName, group)
if err != nil {
return nil, err
}
bindings.Add(binding)
}
*group = MergeBindings(bindings, *group)
}
// Globals can't inherit from themselves
config.Bindings.Global.Globals = false
for _, contextBind := range config.ContextualBinds {
if contextBind.BindContext == "default" {
contextBind.Bindings.Globals = false
}
}
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
}
bindings.Add(binding)
}
return bindings, nil
}
func (config *AercConfig) 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)
}
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
}
contextualBind :=
BindingConfigContext{
Bindings: binds,
BindContext: baseName,
}
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 config.Accounts {
matches := contextualBind.Regex.FindString(acctConf.Name)
if matches != "" {
valid = true
}
}
if !valid {
return fmt.Errorf("Invalid Account Name: %s", acctName)
}
contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
default:
return fmt.Errorf("Unknown Context Bind Section: %s", sectionName)
}
config.ContextualBinds = append(config.ContextualBinds, contextualBind)
}
return nil
}
// checkConfigPerms checks for too open permissions
// printing the fix on stdout and returning an error
func checkConfigPerms(filename string) error {

View File

@ -528,6 +528,21 @@ are:
*[terminal]*
keybindings for terminal tabs
You may also configure account specific key bindings for each context:
*[context:account=<AccountName>]*
keybindings for this context and account, where <AccountName> matches
the account name you provided in *accounts.conf*.
Example:
```
[messages:account=Mailbox]
c = :cf path:mailbox/** and<space>
[compose::editor:account=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

View File

@ -182,22 +182,26 @@ func (aerc *Aerc) Draw(ctx *ui.Context) {
}
func (aerc *Aerc) getBindings() *config.KeyBindings {
selectedAccountName := ""
if aerc.SelectedAccount() != nil {
selectedAccountName = aerc.SelectedAccount().acct.Name
}
switch view := aerc.SelectedTab().(type) {
case *AccountView:
return aerc.conf.Bindings.MessageList
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.MessageList, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "messages")
case *AccountWizard:
return aerc.conf.Bindings.AccountWizard
case *Composer:
switch view.Bindings() {
case "compose::editor":
return aerc.conf.Bindings.ComposeEditor
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.ComposeEditor, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose::editor")
case "compose::review":
return aerc.conf.Bindings.ComposeReview
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.ComposeReview, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose::review")
default:
return aerc.conf.Bindings.Compose
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.Compose, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose")
}
case *MessageViewer:
return aerc.conf.Bindings.MessageView
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.MessageView, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "view")
case *Terminal:
return aerc.conf.Bindings.Terminal
default: