~sircmpwn/aerc

Account Specific Bindings v5 PROPOSED

Jonathan Bartlett: 1
 Account Specific Bindings

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

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

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

[PATCH v5] Account Specific Bindings Export this patch

---
 config/bindings.go    |  22 ++++++
 config/config.go      | 178 ++++++++++++++++++++++++++++++------------
 doc/aerc-config.5.scd |  15 ++++
 widgets/aerc.go       |  14 ++--
 4 files changed, 173 insertions(+), 56 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 af9c63b..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 {
@@ -100,6 +101,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 +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
@@ -345,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
@@ -488,6 +499,9 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			MessageView:   NewKeyBindings(),
			Terminal:      NewKeyBindings(),
		},

		ContextualBinds: []BindingConfigContext{},

		Ini: file,

		Ui: UIConfig{
@@ -565,6 +579,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,61 +590,122 @@ 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)

	// Base Bindings
	for name, group := range baseGroups {
		err = config.LoadBinds(binds, name, group)
		if err != nil {
			return nil, err
		}
		group, ok := groups[strings.ToLower(name)]
		if !ok {
			return nil, errors.New("Unknown keybinding group " + name)
	}

	config.Bindings.Global.Globals = false
	for _, contextBind := range config.ContextualBinds {
		if contextBind.BindContext == "default" {
			contextBind.Bindings.Globals = false
		}
		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
	}

	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 key == "$noinherit" {
				if value == "false" {
					continue
				}
				if value != "true" {
					return nil, errors.New(
						"Error: expected 'true' or 'false' for $noinherit")
				}
				bindings.Globals = false
			if len(strokes) != 1 {
				return nil, errors.New("Invalid binding")
			}
			bindings.ExKey = strokes[0]
			continue
		}
		if key == "$noinherit" {
			if value == "false" {
				continue
			}
			binding, err := ParseBinding(key, value)
			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(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)
		}
		*group = MergeBindings(bindings, *group)

		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)
	}
	// Globals can't inherit from themselves
	config.Bindings.Global.Globals = false
	return config, nil

	return nil
}

// checkConfigPerms checks for too open permissions
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index d4de883..fb032cb 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=regex]*
	keybindings for this context and account, where *regex* matches
	the account name 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.31.1