diff --git a/core/internal/keybinds/providers/niri.go b/core/internal/keybinds/providers/niri.go index 90d2880a..19424ffd 100644 --- a/core/internal/keybinds/providers/niri.go +++ b/core/internal/keybinds/providers/niri.go @@ -166,7 +166,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co } if source == "dms-default" && conflicts != nil { - if conflictKb, ok := conflicts[keyStr]; ok { + if conflictKb, ok := conflicts[normalizeNiriBindKey(keyStr)]; ok { bind.Conflict = &keybinds.Keybind{ Key: keyStr, Description: conflictKb.Description, @@ -249,7 +249,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri existingBinds = make(map[string]*overrideBind) } - existingBinds[key] = &overrideBind{ + existingBinds[normalizeNiriBindKey(key)] = &overrideBind{ Key: key, Action: action, Description: description, @@ -265,7 +265,7 @@ func (n *NiriProvider) RemoveBind(key string) error { return nil } - delete(existingBinds, key) + delete(existingBinds, normalizeNiriBindKey(key)) return n.writeOverrideBinds(existingBinds) } @@ -316,7 +316,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) { action = n.formatRawAction(kb.Action, kb.Args) } - binds[keyStr] = &overrideBind{ + binds[normalizeNiriBindKey(keyStr)] = &overrideBind{ Key: keyStr, Action: action, Description: kb.Description, diff --git a/core/internal/keybinds/providers/niri_parser.go b/core/internal/keybinds/providers/niri_parser.go index b273df24..d042b65c 100644 --- a/core/internal/keybinds/providers/niri_parser.go +++ b/core/internal/keybinds/providers/niri_parser.go @@ -162,6 +162,14 @@ func NewNiriParser(configDir string) *NiriParser { } } +func normalizeNiriBindKey(key string) string { + parts := strings.Split(key, "+") + for i := range parts { + parts[i] = strings.ToLower(strings.TrimSpace(parts[i])) + } + return strings.Join(parts, "+") +} + func (p *NiriParser) Parse() (*NiriSection, error) { dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl") if _, err := os.Stat(dmsBindsPath); err == nil { @@ -213,24 +221,25 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding { func (p *NiriParser) addBind(kb *NiriKeyBinding) { key := p.formatBindKey(kb) + normalizedKey := normalizeNiriBindKey(key) isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl") if isDMSBind { - p.dmsBindKeys[key] = true - p.dmsBindMap[key] = kb - } else if p.dmsBindKeys[key] { + p.dmsBindKeys[normalizedKey] = true + p.dmsBindMap[normalizedKey] = kb + } else if p.dmsBindKeys[normalizedKey] { p.bindsAfterDMS++ - p.conflictingConfigs[key] = kb - p.configBindKeys[key] = true + p.conflictingConfigs[normalizedKey] = kb + p.configBindKeys[normalizedKey] = true return } else { - p.configBindKeys[key] = true + p.configBindKeys[normalizedKey] = true } - if _, exists := p.bindMap[key]; !exists { - p.bindOrder = append(p.bindOrder, key) + if _, exists := p.bindMap[normalizedKey]; !exists { + p.bindOrder = append(p.bindOrder, normalizedKey) } - p.bindMap[key] = kb + p.bindMap[normalizedKey] = kb } func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string { diff --git a/core/internal/keybinds/providers/niri_parser_test.go b/core/internal/keybinds/providers/niri_parser_test.go index a665f95d..d7a94b00 100644 --- a/core/internal/keybinds/providers/niri_parser_test.go +++ b/core/internal/keybinds/providers/niri_parser_test.go @@ -526,6 +526,85 @@ binds { } } +func TestNiriKeyIdentityIsCaseInsensitive(t *testing.T) { + tmpDir := t.TempDir() + dmsDir := filepath.Join(tmpDir, "dms") + if err := os.MkdirAll(dmsDir, 0o755); err != nil { + t.Fatalf("Failed to create dms dir: %v", err) + } + + config := `binds { + Alt+Space hotkey-overlay-title="Spotlight Bar" { spawn "dms" "ipc" "call" "spotlight-bar" "toggle"; } +} +include "dms/binds.kdl" +` + if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil { + t.Fatalf("Failed to write config: %v", err) + } + include := `binds { + Alt+space hotkey-overlay-title="Default Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } +} +` + if err := os.WriteFile(filepath.Join(dmsDir, "binds.kdl"), []byte(include), 0o644); err != nil { + t.Fatalf("Failed to write binds include: %v", err) + } + + result, err := ParseNiriKeys(tmpDir) + if err != nil { + t.Fatalf("ParseNiriKeys failed: %v", err) + } + + var altSpaceBinds []NiriKeyBinding + parser := NewNiriParser("") + for _, kb := range result.Section.Keybinds { + if normalizeNiriBindKey(parser.formatBindKey(&kb)) == "alt+space" { + altSpaceBinds = append(altSpaceBinds, kb) + } + } + + if len(altSpaceBinds) != 1 { + t.Fatalf("Expected one Alt+Space identity, got %d", len(altSpaceBinds)) + } + if got := altSpaceBinds[0].Args; len(got) < 5 || got[3] != "spotlight" || got[4] != "toggle" { + t.Fatalf("Expected later DMS include to win with spotlight toggle, got action=%s args=%v", altSpaceBinds[0].Action, got) + } +} + +func TestNiriOverrideBindsUseCaseInsensitiveKeys(t *testing.T) { + tmpDir := t.TempDir() + dmsDir := filepath.Join(tmpDir, "dms") + if err := os.MkdirAll(dmsDir, 0o755); err != nil { + t.Fatalf("Failed to create dms dir: %v", err) + } + path := filepath.Join(dmsDir, "binds.kdl") + if err := os.WriteFile(path, []byte(`binds { + Alt+space hotkey-overlay-title="Default Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } +} +`), 0o644); err != nil { + t.Fatalf("Failed to write override binds: %v", err) + } + + provider := NewNiriProvider(tmpDir) + if err := provider.SetBind("Alt+Space", "spawn dms ipc call spotlight-bar toggle", "Spotlight Bar", nil); err != nil { + t.Fatalf("SetBind failed: %v", err) + } + + binds, err := provider.loadOverrideBinds() + if err != nil { + t.Fatalf("loadOverrideBinds failed: %v", err) + } + if len(binds) != 1 { + t.Fatalf("Expected one normalized override bind, got %d", len(binds)) + } + bind := binds["alt+space"] + if bind == nil { + t.Fatal("Expected normalized alt+space bind") + } + if bind.Key != "Alt+Space" || bind.Action != "spawn dms ipc call spotlight-bar toggle" { + t.Fatalf("Unexpected bind after SetBind: %+v", bind) + } +} + func TestNiriParseMultipleArgs(t *testing.T) { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.kdl") diff --git a/core/internal/keybinds/providers/niri_test.go b/core/internal/keybinds/providers/niri_test.go index b04c5b2d..0976e8cd 100644 --- a/core/internal/keybinds/providers/niri_test.go +++ b/core/internal/keybinds/providers/niri_test.go @@ -367,7 +367,7 @@ func TestNiriEmptyArgsPreservation(t *testing.T) { } for key, expected := range binds { - loaded, ok := loadedBinds[key] + loaded, ok := loadedBinds[normalizeNiriBindKey(key)] if !ok { t.Errorf("Missing bind for key %s", key) continue