~rjarry/aerc-devel

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
15 3

[PATCH v8] Account Specific Bindings

Details
Message ID
<20211210201832.279900-1-jonathan@jonnobrow.co.uk>
DKIM signature
pass
Download raw message
Patch: +200 -58
---
This version allows global binds to be overriden on a per account basis.

 config/bindings.go    |  22 +++++
 config/config.go      | 207 +++++++++++++++++++++++++++++++-----------
 doc/aerc-config.5.scd |  15 +++
 widgets/aerc.go       |  14 ++-
 4 files changed, 200 insertions(+), 58 deletions(-)

diff --git a/config/bindings.go b/config/bindings.go
index 9956b41..0ce9b9d 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(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 af9c63b..cd83dbe 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"os/exec"
@@ -56,6 +57,7 @@ const (
	UI_CONTEXT_FOLDER ContextType = iota
	UI_CONTEXT_ACCOUNT
	UI_CONTEXT_SUBJECT
	BIND_CONTEXT_ACCOUNT
)

type UIConfigContext struct {
@@ -100,6 +102,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:"-"`
@@ -134,17 +143,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
@@ -345,11 +355,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
@@ -488,6 +500,9 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			MessageView:   NewKeyBindings(),
			Terminal:      NewKeyBindings(),
		},

		ContextualBinds: []BindingConfigContext{},

		Ini: file,

		Ui: UIConfig{
@@ -565,6 +580,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,63 +591,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 d4de883..c89115f 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: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 6df0c95..5661260 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:
-- 
2.34.0
Details
Message ID
<CGBWVCP1SWMN.3H346GYVJNREY@diabtop>
In-Reply-To
<20211210201832.279900-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
missing
Download raw message
Jonathan Bartlett, Dec 10, 2021 at 21:18:
> ---
> This version allows global binds to be overriden on a per account basis.

Hi, this patch does not apply on the current master:

  ~$ git log -n1 --pretty=short -1
  commit 5dfeff75f3681429446329e2d644811414100e7c (HEAD -> master, origin/master)
  Author: Robin Jarry <robin@jarry.cc>

      imap: add tcp connection options
  ~$ git am < account-specific-bindings.patch
  Applying: Account Specific Bindings
  error: patch failed: config/config.go:345
  error: config/config.go: patch does not apply
  Patch failed at 0001 Account Specific Bindings

Could you rebase it please?

Also, please put the commit description before the --- line. The text
after the --- line should be used for non-permanent information.

See this thread for more details:
https://lists.sr.ht/~rjarry/aerc-devel/%3CCGB2IRC8OL8Y.3EGLXYFUMZ45W%40diabtop%3E

Thanks!

[PATCH v9] Account Specific Bindings

Details
Message ID
<20211210204513.285753-1-jonathan@jonnobrow.co.uk>
In-Reply-To
<20211210201832.279900-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
pass
Download raw message
Patch: +200 -58
---
I had not merged the fork into my working copy so I believe v8 did not work.

 config/bindings.go    |  22 +++++
 config/config.go      | 207 +++++++++++++++++++++++++++++++-----------
 doc/aerc-config.5.scd |  15 +++
 widgets/aerc.go       |  14 ++-
 4 files changed, 200 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..7d78cbd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"os/exec"
@@ -64,6 +65,7 @@ const (
	UI_CONTEXT_FOLDER ContextType = iota
	UI_CONTEXT_ACCOUNT
	UI_CONTEXT_SUBJECT
	BIND_CONTEXT_ACCOUNT
)

type UIConfigContext struct {
@@ -109,6 +111,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 +152,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 +367,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 +376,7 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
			return err
		}
	}

	for _, sectionName := range file.SectionStrings() {
		if !strings.Contains(sectionName, "ui:") {
			continue
@@ -526,6 +538,9 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			MessageView:   NewKeyBindings(),
			Terminal:      NewKeyBindings(),
		},

		ContextualBinds: []BindingConfigContext{},

		Ini: file,

		Ui: UIConfig{
@@ -609,6 +624,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 +635,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:
-- 
2.34.0

Re: [PATCH v9] Account Specific Bindings

Details
Message ID
<CGBX6K3UID37.BTKEH5YA378U@diabtop>
In-Reply-To
<20211210204513.285753-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
missing
Download raw message
Jonathan Bartlett, Dec 10, 2021 at 21:45:
> ---
> I had not merged the fork into my working copy so I believe v8 did not work.

Now, the patch applies but it does not build :)

# git.sr.ht/~rjarry/aerc/config
config/config.go:7:2: imported and not used: "log"

Could you check it works before sending a v10? Also, please add a more
detailed explanation about what your patch does and why is it useful. It
may be obvious to you but to review it, I need to understand your
thought process and why you did things in a certain way.

Read this for inspiration:

https://www.kernel.org/doc/html/latest/process/submitting-patches.html#describe-your-changes

Thanks.

[PATCH v10] binds: Add Account Specific Bindings

Details
Message ID
<20211210212729.306156-1-jonathan@jonnobrow.co.uk>
In-Reply-To
<20211210204513.285753-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
pass
Download raw message
Patch: +199 -58
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>
---
Apologies for the spam, it's been over 6 months since I last used the email flow
and I'm a little rusty :)

v9 -> v10:
    * Code now builds
    * Add detailed commit description with example

 config/bindings.go    |  22 +++++
 config/config.go      | 206 +++++++++++++++++++++++++++++++-----------
 doc/aerc-config.5.scd |  15 +++
 widgets/aerc.go       |  14 ++-
 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:
-- 
2.34.0

Re: [PATCH v10] binds: Add Account Specific Bindings

Details
Message ID
<CGCRNINBCSC1.3KHEXRWIFQJUC@diabtop>
In-Reply-To
<20211210212729.306156-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
missing
Download raw message
Jonathan Bartlett, Dec 10, 2021 at 22:27:
> 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>

Applied on master.

Thanks!

Re: [PATCH v10] binds: Add Account Specific Bindings

Details
Message ID
<CGKE3BKS7MFA.3K6HM6SRZFYYR@bisio>
In-Reply-To
<20211210212729.306156-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
I found a small problem with this patch: when bindings are defined for account
foo and this account does not exist in the `accounts.conf`, aerc fails with the
message "Failed to load config: Invalid Account Name: foo". Could it be better
to just ignore these account-specific bindings in favor of the global bindings?
Maybe produce a message "Bindings defined for nonexistent account "foo"". 

I find that preventing aerc from running for this reason is a bit too harsh,
especially given that the rest of accounts would indeed work as expected...


On Fri Dec 10, 2021 at 10:27 PM CET, Jonathan Bartlett wrote:
> 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>
> ---
> Apologies for the spam, it's been over 6 months since I last used the
> email flow
> and I'm a little rusty :)
>
> v9 -> v10:
> * Code now builds
> * Add detailed commit description with example
>
> config/bindings.go | 22 +++++
> config/config.go | 206 +++++++++++++++++++++++++++++++-----------
> doc/aerc-config.5.scd | 15 +++
> widgets/aerc.go | 14 ++-
> 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:
> --
> 2.34.0

Re: [PATCH v10] binds: Add Account Specific Bindings

Details
Message ID
<CGR6N3XDKMT4.1IA6HJ30H4PCX@diabtop>
In-Reply-To
<CGKE3BKS7MFA.3K6HM6SRZFYYR@bisio> (view parent)
DKIM signature
missing
Download raw message
inwit, Dec 20, 2021 at 20:52:
> I found a small problem with this patch: when bindings are defined for
> account foo and this account does not exist in the `accounts.conf`,
> aerc fails with the message "Failed to load config: Invalid Account
> Name: foo". Could it be better to just ignore these account-specific
> bindings in favor of the global bindings? Maybe produce a message
> "Bindings defined for nonexistent account "foo"". 
>
> I find that preventing aerc from running for this reason is a bit too
> harsh, especially given that the rest of accounts would indeed work as
> expected...

Sure, a warning would be more than enough.

Please submit a patch.

Re: [PATCH v10] binds: Add Account Specific Bindings

Details
Message ID
<CHAQB0VIE23Q.2ST787PE5G7OV@bisio>
In-Reply-To
<CGR6N3XDKMT4.1IA6HJ30H4PCX@diabtop> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
On Tue Dec 28, 2021 at 8:30 PM CET, Robin Jarry wrote:
> inwit, Dec 20, 2021 at 20:52:
> > I found a small problem with this patch: when bindings are defined for
> > account foo and this account does not exist in the `accounts.conf`,
> > aerc fails with the message "Failed to load config: Invalid Account
> > Name: foo". Could it be better to just ignore these account-specific
> > bindings in favor of the global bindings? Maybe produce a message
> > "Bindings defined for nonexistent account "foo"". 
> >
> > I find that preventing aerc from running for this reason is a bit too
> > harsh, especially given that the rest of accounts would indeed work as
> > expected...
>
> Sure, a warning would be more than enough.
Where/how should we write such a warning? Would it be ok to send it to
the log (`mv.acct.Logger().Printf`) or would it be better to show it in
the message area?


>
> Please submit a patch.

Re: [PATCH v10] binds: Add Account Specific Bindings

Details
Message ID
<CHARNYAGY9HH.YX43NTFIQHXD@diabtop>
In-Reply-To
<CHAQB0VIE23Q.2ST787PE5G7OV@bisio> (view parent)
DKIM signature
missing
Download raw message
inwit, Jan 20, 2022 at 19:55:
> Where/how should we write such a warning? Would it be ok to send it to
> the log (`mv.acct.Logger().Printf`) or would it be better to show it in
> the message area?

mv.acct.Logger().Printf is enough(). No need to alert the user about
this.

[PATCH aerc] Warning on unexistent account bindings

Details
Message ID
<20220123121607.1245305-1-inwit@sindominio.net>
In-Reply-To
<20211210212729.306156-1-jonathan@jonnobrow.co.uk> (view parent)
DKIM signature
pass
Download raw message
Patch: +5 -4
After commit 175d0ef ("binds: add account specific bindings"), when
bindings are defined for an account not defined in accounts.conf, aerc
quits with an error. After this commit, a warning is logged and aerc
ignores those bindings.

Signed-off-by: inwit <inwit@sindominio.net>
---
 config/config.go | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/config/config.go b/config/config.go
index 2e1d589..d45f6b1 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"os/exec"
@@ -502,7 +503,7 @@ func validateBorderChars(section *ini.Section, config *UIConfig) error {
	return nil
}

func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
func LoadConfigFromFile(root *string, sharedir string, logger *log.Logger) (*AercConfig, error) {
	if root == nil {
		_root := path.Join(xdg.ConfigHome(), "aerc")
		root = &_root
@@ -661,7 +662,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
		}

		if baseOnly {
			err = config.LoadBinds(binds, baseSectionName, group)
			err = config.LoadBinds(binds, baseSectionName, group, logger)
			if err != nil {
				return nil, err
			}
@@ -711,7 +712,7 @@ func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
	return bindings, nil
}

func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings, logger *log.Logger) error {

	if sec, err := binds.GetSection(baseName); err == nil {
		binds, err := LoadBindingSection(sec)
@@ -766,7 +767,7 @@ func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup
				}
			}
			if !valid {
				return fmt.Errorf("Invalid Account Name: %s", acctName)
				logger.Printf("Tried to define binds for unexistent     account: %v\n", acctName)
			}
			contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
		default:
-- 
2.32.0

Re: [PATCH aerc] Warning on unexistent account bindings

Details
Message ID
<CHDS129UXOUB.3BMKA5R37PG5T@diabtop>
In-Reply-To
<20220123121607.1245305-1-inwit@sindominio.net> (view parent)
DKIM signature
missing
Download raw message
inwit, Jan 23, 2022 at 13:16:
> After commit 175d0ef ("binds: add account specific bindings"), when
> bindings are defined for an account not defined in accounts.conf, aerc
> quits with an error. After this commit, a warning is logged and aerc
> ignores those bindings.
>
> Signed-off-by: inwit <inwit@sindominio.net>

This does not compile:

# git.sr.ht/~rjarry/aerc
./aerc.go:161:40: not enough arguments in call to config.LoadConfigFromFile
        have (nil, string)
        want (*string, string, *log.Logger)
make: *** [Makefile:36: aerc] Error 2

[PATCH aerc v2] binds: Warning on unexistent account bindings

Details
Message ID
<20220124100457.3677565-1-inwit@sindominio.net>
In-Reply-To
<20220123121607.1245305-1-inwit@sindominio.net> (view parent)
DKIM signature
pass
Download raw message
Patch: +6 -5
After commit 175d0ef ("binds: add account specific bindings"), when
bindings are defined for an account not defined in accounts.conf, aerc
quits with an error. After this commit, a warning is logged and aerc
ignores those bindings.

Signed-off-by: inwit <inwit@sindominio.net>
---
 aerc.go          | 2 +-
 config/config.go | 9 +++++----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/aerc.go b/aerc.go
index b3338ba..9d83d2c 100644
--- a/aerc.go
+++ b/aerc.go
@@ -158,7 +158,7 @@ func main() {
	logger = log.New(logOut, "", log.LstdFlags)
	logger.Println("Starting up aerc")

	conf, err := config.LoadConfigFromFile(nil, ShareDir)
	conf, err := config.LoadConfigFromFile(nil, ShareDir, logger)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
		os.Exit(1)
diff --git a/config/config.go b/config/config.go
index 2e1d589..831981b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"os/exec"
@@ -502,7 +503,7 @@ func validateBorderChars(section *ini.Section, config *UIConfig) error {
	return nil
}

func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
func LoadConfigFromFile(root *string, sharedir string, logger *log.Logger) (*AercConfig, error) {
	if root == nil {
		_root := path.Join(xdg.ConfigHome(), "aerc")
		root = &_root
@@ -661,7 +662,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
		}

		if baseOnly {
			err = config.LoadBinds(binds, baseSectionName, group)
			err = config.LoadBinds(binds, baseSectionName, group, logger)
			if err != nil {
				return nil, err
			}
@@ -711,7 +712,7 @@ func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
	return bindings, nil
}

func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings, logger *log.Logger) error {

	if sec, err := binds.GetSection(baseName); err == nil {
		binds, err := LoadBindingSection(sec)
@@ -766,7 +767,7 @@ func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup
				}
			}
			if !valid {
				return fmt.Errorf("Invalid Account Name: %s", acctName)
				logger.Printf("Tried to define binds for unexistent account: %v\n", acctName)
			}
			contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
		default:
-- 
2.32.0

Re: [PATCH aerc v2] binds: Warning on unexistent account bindings

Details
Message ID
<CHDU5X8GW3AU.3HUOB9NKAT2QA@diabtop>
In-Reply-To
<20220124100457.3677565-1-inwit@sindominio.net> (view parent)
DKIM signature
missing
Download raw message
inwit, Jan 24, 2022 at 11:04:
> After commit 175d0ef ("binds: add account specific bindings"), when
> bindings are defined for an account not defined in accounts.conf, aerc
> quits with an error. After this commit, a warning is logged and aerc
> ignores those bindings.
>
> Signed-off-by: inwit <inwit@sindominio.net>
[snip]
> diff --git a/config/config.go b/config/config.go
> index 2e1d589..831981b 100644
> --- a/config/config.go
> +++ b/config/config.go
[snip]
> -func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
> +func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings, logger *log.Logger) error {
>  
>  	if sec, err := binds.GetSection(baseName); err == nil {
>  		binds, err := LoadBindingSection(sec)
> @@ -766,7 +767,7 @@ func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup
>  				}
>  			}
>  			if !valid {
> -				return fmt.Errorf("Invalid Account Name: %s", acctName)
> +				logger.Printf("Tried to define binds for unexistent account: %v\n", acctName)

Shouldn't we continue here, and not append the invalid binding to the
list.

[PATCH aerc v3] binds: Warning on unexistent account bindings

Details
Message ID
<20220124104535.3837691-1-inwit@sindominio.net>
In-Reply-To
<CHDU5X8GW3AU.3HUOB9NKAT2QA@diabtop> (view parent)
DKIM signature
pass
Download raw message
Patch: +7 -5
After commit 175d0ef ("binds: add account specific bindings"), when
bindings are defined for an account not defined in accounts.conf, aerc
quits with an error. After this commit, a warning is logged and aerc
ignores those bindings.

Signed-off-by: inwit <inwit@sindominio.net>
---
 aerc.go          |  2 +-
 config/config.go | 10 ++++++----
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/aerc.go b/aerc.go
index b3338ba..9d83d2c 100644
--- a/aerc.go
+++ b/aerc.go
@@ -158,7 +158,7 @@ func main() {
	logger = log.New(logOut, "", log.LstdFlags)
	logger.Println("Starting up aerc")

	conf, err := config.LoadConfigFromFile(nil, ShareDir)
	conf, err := config.LoadConfigFromFile(nil, ShareDir, logger)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
		os.Exit(1)
diff --git a/config/config.go b/config/config.go
index 2e1d589..374fd5c 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"os/exec"
@@ -502,7 +503,7 @@ func validateBorderChars(section *ini.Section, config *UIConfig) error {
	return nil
}

func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
func LoadConfigFromFile(root *string, sharedir string, logger *log.Logger) (*AercConfig, error) {
	if root == nil {
		_root := path.Join(xdg.ConfigHome(), "aerc")
		root = &_root
@@ -661,7 +662,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
		}

		if baseOnly {
			err = config.LoadBinds(binds, baseSectionName, group)
			err = config.LoadBinds(binds, baseSectionName, group, logger)
			if err != nil {
				return nil, err
			}
@@ -711,7 +712,7 @@ func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
	return bindings, nil
}

func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings, logger *log.Logger) error {

	if sec, err := binds.GetSection(baseName); err == nil {
		binds, err := LoadBindingSection(sec)
@@ -766,7 +767,8 @@ func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup
				}
			}
			if !valid {
				return fmt.Errorf("Invalid Account Name: %s", acctName)
				logger.Printf("Tried to define binds for unexistent account: %v\n", acctName)
				continue
			}
			contextualBind.ContextType = BIND_CONTEXT_ACCOUNT
		default:
-- 
2.32.0

Re: [PATCH aerc v3] binds: Warning on unexistent account bindings

Details
Message ID
<CHDUHWX0CST1.3M6FIXBSLZ58T@diabtop>
In-Reply-To
<20220124104535.3837691-1-inwit@sindominio.net> (view parent)
DKIM signature
missing
Download raw message
inwit, Jan 24, 2022 at 11:45:
> After commit 175d0ef ("binds: add account specific bindings"), when
> bindings are defined for an account not defined in accounts.conf, aerc
> quits with an error. After this commit, a warning is logged and aerc
> ignores those bindings.
>
> Signed-off-by: inwit <inwit@sindominio.net>

To git.sr.ht:~rjarry/aerc
   cb3090956cfd..d922d7325c7b  master -> master

Thanks!
Reply to thread Export thread (mbox)