fix(windowrules): preserve niri exclude clauses

fixes #2996
port 1.5

(cherry picked from commit 8e2168ecf8)
This commit is contained in:
bbedward authored and dms-ci[bot] committed 2026-08-08 17:12:41 +00:00
1 parent a34037f49c
commit 8200c72f8c
4 files changed
+72 -5

No files matched your search

@@ -37,6 +37,7 @@ type NiriWindowRule struct {
MatchIsUrgent *bool
MatchAtStartup *bool
Matches []NiriMatch
Excludes []NiriMatch
Opacity *float64
OpenFloating *bool
OpenMaximized *bool
@@ -212,6 +213,8 @@ func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
switch childName {
case "match":
rule.Matches = append(rule.Matches, p.parseMatchNode(child))
case "exclude":
rule.Excludes = append(rule.Excludes, p.parseMatchNode(child))
case "opacity":
if len(child.Arguments) > 0 {
val := child.Arguments[0].ResolvedValue()
@@ -597,7 +600,8 @@ func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.Win
IsUrgent: nr.MatchIsUrgent,
AtStartup: nr.MatchAtStartup,
},
Matches: convertNiriMatches(nr.Matches),
Matches: convertNiriMatches(nr.Matches),
Excludes: convertNiriMatches(nr.Excludes),
Actions: windowrules.Actions{
Opacity: nr.Opacity,
OpenFloating: nr.OpenFloating,
@@ -810,7 +814,8 @@ func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error)
IsUrgent: nr.MatchIsUrgent,
AtStartup: nr.MatchAtStartup,
},
Matches: convertNiriMatches(nr.Matches),
Matches: convertNiriMatches(nr.Matches),
Excludes: convertNiriMatches(nr.Excludes),
Actions: windowrules.Actions{
Opacity: nr.Opacity,
OpenFloating: nr.OpenFloating,
@@ -874,7 +879,7 @@ func (p *NiriWritableProvider) writeDMSRules(rules []windowrules.WindowRule) err
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
}
func formatNiriMatchLine(m windowrules.MatchCriteria) (string, bool) {
func formatNiriMatchLine(keyword string, m windowrules.MatchCriteria) (string, bool) {
var matchProps []string
if m.AppID != "" {
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
@@ -906,7 +911,7 @@ func formatNiriMatchLine(m windowrules.MatchCriteria) (string, bool) {
if len(matchProps) == 0 {
return "", false
}
return " match " + strings.Join(matchProps, " "), true
return " " + keyword + " " + strings.Join(matchProps, " "), true
}
func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
@@ -919,7 +924,12 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
matches = []windowrules.MatchCriteria{rule.MatchCriteria}
}
for _, m := range matches {
if line, ok := formatNiriMatchLine(m); ok {
if line, ok := formatNiriMatchLine("match", m); ok {
lines = append(lines, line)
}
}
for _, m := range rule.Excludes {
if line, ok := formatNiriMatchLine("exclude", m); ok {
lines = append(lines, line)
}
}
@@ -333,3 +333,56 @@ window-rule {
t.Error("DMSStatus.Exists should be false when dms rules file doesn't exist")
}
}
func TestNiriExcludesSurviveEditOfOtherRule(t *testing.T) {
tmpDir := t.TempDir()
provider := NewNiriWritableProvider(tmpDir)
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
t.Fatal(err)
}
existing := `// @id=thunderbird @name=Thunderbird
window-rule {
match app-id="^org.mozilla.Thunderbird$"
open-floating true
exclude title="^Mozilla Thunderbird$"
exclude title="^Verfassen:.*"
}
// @id=firefox @name=Firefox
window-rule {
match app-id="^firefox$"
open-floating true
}
`
if err := os.WriteFile(filepath.Join(dmsDir, "windowrules.kdl"), []byte(existing), 0644); err != nil {
t.Fatal(err)
}
edited := newTestWindowRule("firefox", "Firefox", "^firefox$")
edited.Actions.OpenFloating = boolPtr(false)
if err := provider.SetRule(edited); err != nil {
t.Fatalf("SetRule failed: %v", err)
}
rules, err := provider.LoadDMSRules()
if err != nil {
t.Fatalf("LoadDMSRules failed: %v", err)
}
if len(rules) != 2 {
t.Fatalf("expected 2 rules, got %d", len(rules))
}
if len(rules[0].Excludes) != 2 {
t.Fatalf("expected 2 excludes on untouched rule, got %d", len(rules[0].Excludes))
}
if rules[0].Excludes[0].Title != "^Mozilla Thunderbird$" {
t.Errorf("Excludes[0].Title = %q", rules[0].Excludes[0].Title)
}
if rules[0].Excludes[1].Title != "^Verfassen:.*" {
t.Errorf("Excludes[1].Title = %q", rules[0].Excludes[1].Title)
}
if rules[1].Actions.OpenFloating == nil || *rules[1].Actions.OpenFloating {
t.Error("edited rule should have open-floating false")
}
}
+1
View File
@@ -77,6 +77,7 @@ type WindowRule struct {
Enabled bool `json:"enabled"`
MatchCriteria MatchCriteria `json:"matchCriteria"`
Matches []MatchCriteria `json:"matches,omitempty"`
Excludes []MatchCriteria `json:"excludes,omitempty"`
Actions Actions `json:"actions"`
Source string `json:"source,omitempty"`
}
+3
View File
@@ -457,6 +457,9 @@ FloatingWindow {
};
if (isNiri && extraMatchModel.count > 0)
ruleData.matches = matches;
// No exclude editor yet — carry existing excludes through so an edit doesn't delete them (#2996)
if (isEditMode && (editingRule.excludes?.length ?? 0) > 0)
ruleData.excludes = editingRule.excludes;
submitting = true;