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=]* + keybindings for this context and account, where matches + the account name you provided in *accounts.conf*. + +Example: +``` +[messages:account=Mailbox] +c = :cf path:mailbox/** and + +[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: