mirror of
https://github.com/Misterio77/Foundry.git
synced 2026-08-24 10:04:09 -05:00
Patch aerc to load nested $include paths before the including binds file, with relative and home path expansion plus cycle detection. Include the packaged defaults from Home Manager so custom bindings override them without duplication or IFD. Assisted-by: pi (gpt-5.6-sol)
172 lines
5.1 KiB
Diff
172 lines
5.1 KiB
Diff
diff --git a/config/binds.go b/config/binds.go
|
|
index 3b9510cd..9591ee49 100644
|
|
--- a/config/binds.go
|
|
+++ b/config/binds.go
|
|
@@ -7,12 +7,14 @@ import (
|
|
"io"
|
|
"os"
|
|
"path"
|
|
+ "path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
+ "git.sr.ht/~rjarry/aerc/lib/xdg"
|
|
"git.sr.ht/~rockorager/vaxis"
|
|
"github.com/go-ini/ini"
|
|
)
|
|
@@ -104,7 +106,24 @@ func defaultBindsConfig() *BindingConfig {
|
|
|
|
var Binds = defaultBindsConfig()
|
|
|
|
-func parseBindsFromFile(root string, filename string) error {
|
|
+func parseBindsFromFile(_ string, filename string) error {
|
|
+ filename = xdg.ExpandHome(filename)
|
|
+ absolute, err := filepath.Abs(filename)
|
|
+ if err != nil {
|
|
+ return err
|
|
+ }
|
|
+ return parseBindsFile(absolute, make(map[string]bool))
|
|
+}
|
|
+
|
|
+func parseBindsFile(filename string, stack map[string]bool) error {
|
|
+ filename = filepath.Clean(filename)
|
|
+
|
|
+ if stack[filename] {
|
|
+ return fmt.Errorf("cyclic key bindings include: %s", filename)
|
|
+ }
|
|
+ stack[filename] = true
|
|
+ defer delete(stack, filename)
|
|
+
|
|
log.Debugf("Parsing key bindings configuration from %s", filename)
|
|
binds, err := ini.LoadSources(ini.LoadOptions{
|
|
KeyValueDelimiters: "=",
|
|
@@ -117,6 +136,26 @@ func parseBindsFromFile(root string, filename string) error {
|
|
return err
|
|
}
|
|
|
|
+ defaultSection, err := binds.GetSection(ini.DefaultSection)
|
|
+ if err != nil {
|
|
+ return err
|
|
+ }
|
|
+ if include, err := defaultSection.GetKey("$include"); err == nil {
|
|
+ for _, name := range strings.Split(include.String(), ",") {
|
|
+ name = strings.TrimSpace(name)
|
|
+ if name == "" {
|
|
+ continue
|
|
+ }
|
|
+ name = xdg.ExpandHome(name)
|
|
+ if !filepath.IsAbs(name) {
|
|
+ name = filepath.Join(filepath.Dir(filename), name)
|
|
+ }
|
|
+ if err := parseBindsFile(name, stack); err != nil {
|
|
+ return fmt.Errorf("include %s from %s: %w", name, filename, err)
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
baseGroups := map[string]**KeyBindings{
|
|
"default": &Binds.Global,
|
|
"compose": &Binds.Compose,
|
|
@@ -178,6 +217,8 @@ func LoadBindingSection(sec *ini.Section) (*KeyBindings, error) {
|
|
value, annotation, _ := strings.Cut(k.String(), " # ")
|
|
value = strings.TrimSpace(value)
|
|
switch key {
|
|
+ case "$include":
|
|
+ continue
|
|
case "$ex":
|
|
strokes, err := ParseKeyStrokes(value)
|
|
if err != nil {
|
|
diff --git a/config/binds_test.go b/config/binds_test.go
|
|
index a47da41c..e3182537 100644
|
|
--- a/config/binds_test.go
|
|
+++ b/config/binds_test.go
|
|
@@ -2,12 +2,57 @@ package config
|
|
|
|
import (
|
|
"fmt"
|
|
+ "os"
|
|
+ "path/filepath"
|
|
"testing"
|
|
|
|
"git.sr.ht/~rockorager/vaxis"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
+func TestBindsInclude(t *testing.T) {
|
|
+ oldBinds := Binds
|
|
+ t.Cleanup(func() { Binds = oldBinds })
|
|
+ Binds = defaultBindsConfig()
|
|
+
|
|
+ dir := t.TempDir()
|
|
+ write := func(name, content string) {
|
|
+ err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600)
|
|
+ assert.NoError(t, err)
|
|
+ }
|
|
+ write("base.conf", "a = :base<Enter>\n[messages]\nq = :base<Enter>\n")
|
|
+ write("extra.conf", "a = :extra<Enter>\n[messages]\nq = :extra<Enter>\n")
|
|
+ write("binds.conf", "$include = base.conf, extra.conf\na = :local<Enter>\n")
|
|
+
|
|
+ assert.NoError(t, parseBindsFromFile(dir, filepath.Join(dir, "binds.conf")))
|
|
+ assertBinding := func(bindings *KeyBindings, input, output string) {
|
|
+ in, err := ParseKeyStrokes(input)
|
|
+ assert.NoError(t, err)
|
|
+ want, err := ParseKeyStrokes(output)
|
|
+ assert.NoError(t, err)
|
|
+ result, got := bindings.GetBinding(in)
|
|
+ assert.Equal(t, BindingSearchResult(BINDING_FOUND), result)
|
|
+ assert.Equal(t, want, got)
|
|
+ }
|
|
+ assertBinding(Binds.Global, "a", ":local<Enter>")
|
|
+ assertBinding(Binds.MessageList, "q", ":extra<Enter>")
|
|
+}
|
|
+
|
|
+func TestBindsIncludeCycle(t *testing.T) {
|
|
+ oldBinds := Binds
|
|
+ t.Cleanup(func() { Binds = oldBinds })
|
|
+ Binds = defaultBindsConfig()
|
|
+
|
|
+ dir := t.TempDir()
|
|
+ assert.NoError(t, os.WriteFile(
|
|
+ filepath.Join(dir, "one.conf"), []byte("$include = two.conf\n"), 0o600))
|
|
+ assert.NoError(t, os.WriteFile(
|
|
+ filepath.Join(dir, "two.conf"), []byte("$include = one.conf\n"), 0o600))
|
|
+
|
|
+ err := parseBindsFromFile(dir, filepath.Join(dir, "one.conf"))
|
|
+ assert.ErrorContains(t, err, "cyclic key bindings include")
|
|
+}
|
|
+
|
|
func TestGetBinding(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
diff --git a/doc/aerc-binds.5.scd b/doc/aerc-binds.5.scd
|
|
index 1f508ef8..68155c7a 100644
|
|
--- a/doc/aerc-binds.5.scd
|
|
+++ b/doc/aerc-binds.5.scd
|
|
@@ -30,6 +30,23 @@ in a similar fashion.
|
|
You may configure different keybindings for different contexts by writing them
|
|
into different *[sections]* of the ini file.
|
|
|
|
+# INCLUDES
|
|
+
|
|
+Additional binding files can be loaded with the *$include* special option in the
|
|
+global section:
|
|
+
|
|
+```
|
|
+$include = /usr/share/aerc/binds.conf
|
|
+```
|
|
+
|
|
+Multiple files can be separated with commas. Relative paths are resolved from
|
|
+the directory containing the file with the *$include* option. Paths beginning
|
|
+with _~/_ are expanded to the user's home directory. Included files are loaded
|
|
+in the order listed, before the including file. Therefore, bindings in the
|
|
+including file override bindings from included files.
|
|
+
|
|
+Includes may be nested. Cyclic includes are rejected.
|
|
+
|
|
# CONTEXTS
|
|
|
|
The available contexts are:
|