about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorJonathan Bartlett <jonathan@jonnobrow.co.uk>2021-12-10 21:27:29 +0000
committerRobin Jarry <robin@jarry.cc>2021-12-11 21:45:41 +0100
commit175d0efeb22eb61ce40a1b25969886a66fcdf83e (patch)
tree8aaab1ec68acbccf5183419832d69c436f66a5d9
parentb84374a572b4f23611fb85415142d385e4a4228c (diff)
downloadaerc-175d0efeb22eb61ce40a1b25969886a66fcdf83e.tar.gz
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>
-rw-r--r--config/bindings.go22
-rw-r--r--config/config.go206
-rw-r--r--doc/aerc-config.5.scd15
-rw-r--r--widgets/aerc.go14
4 files changed, 199 insertions, 58 deletions
diff --git a/config/bindings.go b/config/bindings.go
index eff48a6..62956d7 100644
--- a/config/bindings.go
+++ b/config/bindings.go
@@ -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)
diff --git a/config/config.go b/config/config.go
index cbd5860..cf0ded6 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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 {
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index e95a86c..ae03074 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -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
diff --git a/widgets/aerc.go b/widgets/aerc.go
index cbde56c..b84dd87 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -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: