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:
parent
b84374a572
commit
175d0efeb2
4 changed files with 199 additions and 58 deletions
|
@ -55,6 +55,28 @@ func MergeBindings(bindings ...*KeyBindings) *KeyBindings {
|
||||||
return merged
|
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) {
|
func (bindings *KeyBindings) Add(binding *Binding) {
|
||||||
// TODO: Search for conflicts?
|
// TODO: Search for conflicts?
|
||||||
bindings.bindings = append(bindings.bindings, binding)
|
bindings.bindings = append(bindings.bindings, binding)
|
||||||
|
|
130
config/config.go
130
config/config.go
|
@ -64,6 +64,7 @@ const (
|
||||||
UI_CONTEXT_FOLDER ContextType = iota
|
UI_CONTEXT_FOLDER ContextType = iota
|
||||||
UI_CONTEXT_ACCOUNT
|
UI_CONTEXT_ACCOUNT
|
||||||
UI_CONTEXT_SUBJECT
|
UI_CONTEXT_SUBJECT
|
||||||
|
BIND_CONTEXT_ACCOUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
type UIConfigContext struct {
|
type UIConfigContext struct {
|
||||||
|
@ -109,6 +110,13 @@ type BindingConfig struct {
|
||||||
Terminal *KeyBindings
|
Terminal *KeyBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BindingConfigContext struct {
|
||||||
|
ContextType ContextType
|
||||||
|
Regex *regexp.Regexp
|
||||||
|
Bindings *KeyBindings
|
||||||
|
BindContext string
|
||||||
|
}
|
||||||
|
|
||||||
type ComposeConfig struct {
|
type ComposeConfig struct {
|
||||||
Editor string `ini:"editor"`
|
Editor string `ini:"editor"`
|
||||||
HeaderLayout [][]string `ini:"-"`
|
HeaderLayout [][]string `ini:"-"`
|
||||||
|
@ -144,6 +152,7 @@ type TemplateConfig struct {
|
||||||
|
|
||||||
type AercConfig struct {
|
type AercConfig struct {
|
||||||
Bindings BindingConfig
|
Bindings BindingConfig
|
||||||
|
ContextualBinds []BindingConfigContext
|
||||||
Compose ComposeConfig
|
Compose ComposeConfig
|
||||||
Ini *ini.File `ini:"-"`
|
Ini *ini.File `ini:"-"`
|
||||||
Accounts []AccountConfig `ini:"-"`
|
Accounts []AccountConfig `ini:"-"`
|
||||||
|
@ -357,6 +366,7 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui, err := file.GetSection("ui"); err == nil {
|
if ui, err := file.GetSection("ui"); err == nil {
|
||||||
if err := ui.MapTo(&config.Ui); err != nil {
|
if err := ui.MapTo(&config.Ui); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -365,6 +375,7 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sectionName := range file.SectionStrings() {
|
for _, sectionName := range file.SectionStrings() {
|
||||||
if !strings.Contains(sectionName, "ui:") {
|
if !strings.Contains(sectionName, "ui:") {
|
||||||
continue
|
continue
|
||||||
|
@ -526,6 +537,9 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
MessageView: NewKeyBindings(),
|
MessageView: NewKeyBindings(),
|
||||||
Terminal: NewKeyBindings(),
|
Terminal: NewKeyBindings(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ContextualBinds: []BindingConfigContext{},
|
||||||
|
|
||||||
Ini: file,
|
Ini: file,
|
||||||
|
|
||||||
Ui: UIConfig{
|
Ui: UIConfig{
|
||||||
|
@ -609,6 +623,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
} else {
|
} else {
|
||||||
config.Accounts = accounts
|
config.Accounts = accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = path.Join(*root, "binds.conf")
|
filename = path.Join(*root, "binds.conf")
|
||||||
binds, err := ini.Load(filename)
|
binds, err := ini.Load(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -619,25 +634,49 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groups := map[string]**KeyBindings{
|
|
||||||
|
baseGroups := map[string]**KeyBindings{
|
||||||
"default": &config.Bindings.Global,
|
"default": &config.Bindings.Global,
|
||||||
"compose": &config.Bindings.Compose,
|
"compose": &config.Bindings.Compose,
|
||||||
"messages": &config.Bindings.MessageList,
|
"messages": &config.Bindings.MessageList,
|
||||||
"terminal": &config.Bindings.Terminal,
|
"terminal": &config.Bindings.Terminal,
|
||||||
"view": &config.Bindings.MessageView,
|
"view": &config.Bindings.MessageView,
|
||||||
|
|
||||||
"compose::editor": &config.Bindings.ComposeEditor,
|
"compose::editor": &config.Bindings.ComposeEditor,
|
||||||
"compose::review": &config.Bindings.ComposeReview,
|
"compose::review": &config.Bindings.ComposeReview,
|
||||||
}
|
}
|
||||||
for _, name := range binds.SectionStrings() {
|
|
||||||
sec, err := binds.GetSection(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 " + sectionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseOnly {
|
||||||
|
err = config.LoadBinds(binds, baseSectionName, group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
|
||||||
bindings := NewKeyBindings()
|
bindings := NewKeyBindings()
|
||||||
for key, value := range sec.KeysHash() {
|
for key, value := range sec.KeysHash() {
|
||||||
if key == "$ex" {
|
if key == "$ex" {
|
||||||
|
@ -646,8 +685,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(strokes) != 1 {
|
if len(strokes) != 1 {
|
||||||
return nil, errors.New(
|
return nil, errors.New("Invalid binding")
|
||||||
"Error: only one keystroke supported for $ex")
|
|
||||||
}
|
}
|
||||||
bindings.ExKey = strokes[0]
|
bindings.ExKey = strokes[0]
|
||||||
continue
|
continue
|
||||||
|
@ -657,8 +695,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if value != "true" {
|
if value != "true" {
|
||||||
return nil, errors.New(
|
return nil, errors.New("Invalid binding")
|
||||||
"Error: expected 'true' or 'false' for $noinherit")
|
|
||||||
}
|
}
|
||||||
bindings.Globals = false
|
bindings.Globals = false
|
||||||
continue
|
continue
|
||||||
|
@ -669,11 +706,74 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
}
|
}
|
||||||
bindings.Add(binding)
|
bindings.Add(binding)
|
||||||
}
|
}
|
||||||
*group = MergeBindings(bindings, *group)
|
return bindings, nil
|
||||||
}
|
}
|
||||||
// Globals can't inherit from themselves
|
|
||||||
config.Bindings.Global.Globals = false
|
func (config *AercConfig) LoadBinds(binds *ini.File, baseName string, baseGroup **KeyBindings) error {
|
||||||
return config, nil
|
|
||||||
|
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
|
// checkConfigPerms checks for too open permissions
|
||||||
|
|
|
@ -528,6 +528,21 @@ are:
|
||||||
*[terminal]*
|
*[terminal]*
|
||||||
keybindings for terminal tabs
|
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
|
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*
|
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
|
option specified, the _key_ is the keystrokes pressed (in order) to invoke this
|
||||||
|
|
|
@ -182,22 +182,26 @@ func (aerc *Aerc) Draw(ctx *ui.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (aerc *Aerc) getBindings() *config.KeyBindings {
|
func (aerc *Aerc) getBindings() *config.KeyBindings {
|
||||||
|
selectedAccountName := ""
|
||||||
|
if aerc.SelectedAccount() != nil {
|
||||||
|
selectedAccountName = aerc.SelectedAccount().acct.Name
|
||||||
|
}
|
||||||
switch view := aerc.SelectedTab().(type) {
|
switch view := aerc.SelectedTab().(type) {
|
||||||
case *AccountView:
|
case *AccountView:
|
||||||
return aerc.conf.Bindings.MessageList
|
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.MessageList, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "messages")
|
||||||
case *AccountWizard:
|
case *AccountWizard:
|
||||||
return aerc.conf.Bindings.AccountWizard
|
return aerc.conf.Bindings.AccountWizard
|
||||||
case *Composer:
|
case *Composer:
|
||||||
switch view.Bindings() {
|
switch view.Bindings() {
|
||||||
case "compose::editor":
|
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":
|
case "compose::review":
|
||||||
return aerc.conf.Bindings.ComposeReview
|
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.ComposeReview, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose::review")
|
||||||
default:
|
default:
|
||||||
return aerc.conf.Bindings.Compose
|
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.Compose, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "compose")
|
||||||
}
|
}
|
||||||
case *MessageViewer:
|
case *MessageViewer:
|
||||||
return aerc.conf.Bindings.MessageView
|
return aerc.conf.MergeContextualBinds(aerc.conf.Bindings.MessageView, config.BIND_CONTEXT_ACCOUNT, selectedAccountName, "view")
|
||||||
case *Terminal:
|
case *Terminal:
|
||||||
return aerc.conf.Bindings.Terminal
|
return aerc.conf.Bindings.Terminal
|
||||||
default:
|
default:
|
||||||
|
|
Loading…
Reference in a new issue