From 33384e4d5d608c5f44840d4ebf4317b1f31d9052 Mon Sep 17 00:00:00 2001 From: purian23 Date: Sat, 4 Jul 2026 22:25:53 -0400 Subject: [PATCH] fix(niri): update custom keyboard shortcut parsing properties Fixes #2187 --- core/internal/keybinds/providers/niri.go | 44 ++++++++++++---- core/internal/keybinds/providers/niri_test.go | 52 +++++++++++++++++++ 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/core/internal/keybinds/providers/niri.go b/core/internal/keybinds/providers/niri.go index a89468a67..252f6a63b 100644 --- a/core/internal/keybinds/providers/niri.go +++ b/core/internal/keybinds/providers/niri.go @@ -19,6 +19,20 @@ type NiriProvider struct { parsed bool } +type niriActionPart struct { + value string + quoted bool +} + +var niriActionPropertyOrder = []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} +var niriActionProperties = map[string]struct{}{ + "focus": {}, + "show-pointer": {}, + "write-to-disk": {}, + "skip-confirmation": {}, + "delay-ms": {}, +} + func NewNiriProvider(configDir string) *NiriProvider { if configDir == "" { configDir = defaultNiriConfigDir() @@ -353,7 +367,7 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string { } if actionNode.Properties != nil { - for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} { + for _, propName := range niriActionPropertyOrder { if val, ok := actionNode.Properties.Get(propName); ok { parts = append(parts, propName+"="+val.String()) } @@ -441,10 +455,10 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node { return node } - node.SetName(parts[0]) + node.SetName(parts[0].value) for _, arg := range parts[1:] { - if strings.Contains(arg, "=") { - kv := strings.SplitN(arg, "=", 2) + if n.isNiriActionPropertyToken(arg) { + kv := strings.SplitN(arg.value, "=", 2) switch kv[1] { case "true": node.AddProperty(kv[0], true, "") @@ -455,13 +469,25 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node { } continue } - node.AddArgument(arg, "") + node.AddArgument(arg.value, "") } return node } -func (n *NiriProvider) parseActionParts(action string) []string { - var parts []string +func (n *NiriProvider) isNiriActionPropertyToken(part niriActionPart) bool { + if part.quoted || !strings.Contains(part.value, "=") { + return false + } + key, _, ok := strings.Cut(part.value, "=") + if !ok { + return false + } + _, ok = niriActionProperties[key] + return ok +} + +func (n *NiriProvider) parseActionParts(action string) []niriActionPart { + var parts []niriActionPart var current strings.Builder var inQuote, escaped, wasQuoted bool @@ -477,7 +503,7 @@ func (n *NiriProvider) parseActionParts(action string) []string { inQuote = !inQuote case r == ' ' && !inQuote: if current.Len() > 0 || wasQuoted { - parts = append(parts, current.String()) + parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted}) current.Reset() wasQuoted = false } @@ -486,7 +512,7 @@ func (n *NiriProvider) parseActionParts(action string) []string { } } if current.Len() > 0 || wasQuoted { - parts = append(parts, current.String()) + parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted}) } return parts } diff --git a/core/internal/keybinds/providers/niri_test.go b/core/internal/keybinds/providers/niri_test.go index 1dc1b1fb2..5ece050b0 100644 --- a/core/internal/keybinds/providers/niri_test.go +++ b/core/internal/keybinds/providers/niri_test.go @@ -234,6 +234,58 @@ func TestNiriGenerateBindsContent(t *testing.T) { expected: `binds { Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } } +`, + }, + { + name: "spawn with equals arg", + binds: map[string]*overrideBind{ + "Mod+B": { + Key: "Mod+B", + Action: `spawn /opt/browser --profile-directory=Default`, + }, + }, + expected: `binds { + Mod+B { spawn "/opt/browser" "--profile-directory=Default"; } +} +`, + }, + { + name: "spawn shell command with quoted equals args", + binds: map[string]*overrideBind{ + "Mod+C": { + Key: "Mod+C", + Action: `spawn sh -c "chrome --profile-directory=Default --app=x"`, + }, + }, + expected: `binds { + Mod+C { spawn "sh" "-c" "chrome --profile-directory=Default --app=x"; } +} +`, + }, + { + name: "spawn env assignment stays arg", + binds: map[string]*overrideBind{ + "Mod+E": { + Key: "Mod+E", + Action: `spawn env FOO=bar mycmd`, + }, + }, + expected: `binds { + Mod+E { spawn "env" "FOO=bar" "mycmd"; } +} +`, + }, + { + name: "niri action property remains property", + binds: map[string]*overrideBind{ + "Print": { + Key: "Print", + Action: `screenshot show-pointer=false`, + }, + }, + expected: `binds { + Print { screenshot show-pointer=false; } +} `, }, {