fix(config): keep a rule when an invalid duplicate key follows a valid one
- commit
- 628294425cb3db6caf1722f4cc1a79db29844d18
- author
- brice · · 2026-10-08 20:15 UTC
- parent
- 12a6410
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +8 -3
M docs/config.md +1 -1
| @@ -100,7 +100,7 @@ rule.calc.floating = on | |||
| 100 | 100 | - A named workspace stays on its own monitor: `monitor` is ignored next to a named `workspace`. A name nobody declared warns and the window opens as usual. | |
| 101 | 101 | - A window started for a slot (`workspace.<name>.column.N`) goes to its slot, and a dialog stays over its parent: rules do not apply to them. | |
| 102 | 102 | - `width` is ignored under `fixed` and `cascade` overflow, where columns share the width. | |
| 103 | - | - An invalid value, a bad regular expression or a missing `app-id` logs a warning with the line, and the rule is ignored. | |
| 103 | + | - An invalid value, a bad regular expression or a missing `app-id` logs a warning with the line, and the rule is ignored. An invalid value that repeats a key already set keeps the earlier value, like any other key. | |
| 104 | 104 | ||
| 105 | 105 | To find an app ID, look at `app_id` in `neferwl state | jq '.windows[] | {id, app_id}'`, or at the `window mapped` line in the log. X11 apps run through xwayland-satellite get their app ID from `WM_CLASS`; it may be empty, and an app that sets it after opening is not matched. | |
| 106 | 106 | ||
| @@ -100,7 +100,7 @@ rule.calc.floating = on | |||||
| 100 | - A named workspace stays on its own monitor: `monitor` is ignored next to a named `workspace`. A name nobody declared warns and the window opens as usual. | 100 | - A named workspace stays on its own monitor: `monitor` is ignored next to a named `workspace`. A name nobody declared warns and the window opens as usual. | ||
| 101 | - A window started for a slot (`workspace.<name>.column.N`) goes to its slot, and a dialog stays over its parent: rules do not apply to them. | 101 | - A window started for a slot (`workspace.<name>.column.N`) goes to its slot, and a dialog stays over its parent: rules do not apply to them. | ||
| 102 | - `width` is ignored under `fixed` and `cascade` overflow, where columns share the width. | 102 | - `width` is ignored under `fixed` and `cascade` overflow, where columns share the width. | ||
| 103 | - | - An invalid value, a bad regular expression or a missing `app-id` logs a warning with the line, and the rule is ignored. | 103 | + | - An invalid value, a bad regular expression or a missing `app-id` logs a warning with the line, and the rule is ignored. An invalid value that repeats a key already set keeps the earlier value, like any other key. |
| 104 | 104 | ||||
| 105 | To find an app ID, look at `app_id` in `neferwl state | jq '.windows[] | {id, app_id}'`, or at the `window mapped` line in the log. X11 apps run through xwayland-satellite get their app ID from `WM_CLASS`; it may be empty, and an app that sets it after opening is not matched. | 105 | To find an app ID, look at `app_id` in `neferwl state | jq '.windows[] | {id, app_id}'`, or at the `window mapped` line in the log. X11 apps run through xwayland-satellite get their app ID from `WM_CLASS`; it may be empty, and an app that sets it after opening is not matched. | ||
| 106 | 106 | ||||
M internal/adapters/config/config.go +4 -2
| @@ -531,7 +531,8 @@ func parse(r io.Reader) (ports.Config, map[string]string, []Warning, error) { | |||
| 531 | 531 | } | |
| 532 | 532 | if err := setRule(&e.rule, field, value); err != nil { | |
| 533 | 533 | warn("%s: %v", key, err) | |
| 534 | - | if !errors.Is(err, errUnknownRuleField) { | |
| 534 | + | // Like any key, an invalid duplicate keeps the earlier value. | |
| 535 | + | if _, dup := seen[key]; !dup && !errors.Is(err, errUnknownRuleField) { | |
| 535 | 536 | e.dropped = true | |
| 536 | 537 | } | |
| 537 | 538 | continue | |
| @@ -1016,7 +1017,8 @@ var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) | |||
| 1016 | 1017 | type ruleEntry struct { | |
| 1017 | 1018 | rule ports.WindowRule | |
| 1018 | 1019 | line int // of its first key | |
| 1019 | - | // dropped is set by an invalid value: the whole rule is ignored. | |
| 1020 | + | // dropped is set by an invalid value with no valid one before it: the | |
| 1021 | + | // whole rule is ignored. | |
| 1020 | 1022 | dropped bool | |
| 1021 | 1023 | } | |
| 1022 | 1024 | ||
| @@ -531,7 +531,8 @@ func parse(r io.Reader) (ports.Config, map[string]string, []Warning, error) { | |||||
| 531 | } | 531 | } | ||
| 532 | if err := setRule(&e.rule, field, value); err != nil { | 532 | if err := setRule(&e.rule, field, value); err != nil { | ||
| 533 | warn("%s: %v", key, err) | 533 | warn("%s: %v", key, err) | ||
| 534 | - | if !errors.Is(err, errUnknownRuleField) { | 534 | + | // Like any key, an invalid duplicate keeps the earlier value. |
| 535 | + | if _, dup := seen[key]; !dup && !errors.Is(err, errUnknownRuleField) { | |||
| 535 | e.dropped = true | 536 | e.dropped = true | ||
| 536 | } | 537 | } | ||
| 537 | continue | 538 | continue | ||
| @@ -1016,7 +1017,8 @@ var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) | |||||
| 1016 | type ruleEntry struct { | 1017 | type ruleEntry struct { | ||
| 1017 | rule ports.WindowRule | 1018 | rule ports.WindowRule | ||
| 1018 | line int // of its first key | 1019 | line int // of its first key | ||
| 1019 | - | // dropped is set by an invalid value: the whole rule is ignored. | 1020 | + | // dropped is set by an invalid value with no valid one before it: the |
| 1021 | + | // whole rule is ignored. | |||
| 1020 | dropped bool | 1022 | dropped bool | ||
| 1021 | } | 1023 | } | ||
| 1022 | 1024 | ||||
M internal/adapters/config/rules_test.go +3 -0
| @@ -66,6 +66,9 @@ func TestRuleWarnings(t *testing.T) { | |||
| 66 | 66 | {"missing app-id", "rule.a.floating = on\n", "rule.a: no app-id, rule ignored", 1, 0}, | |
| 67 | 67 | {"unknown field", "rule.a.app-id = foo\nrule.a.title = x\n", "rule.a.title: unknown key", 2, 1}, | |
| 68 | 68 | {"bad floating", "rule.a.app-id = foo\nrule.a.floating = maybe\n", "rule.a.floating: must be on or off", 2, 0}, | |
| 69 | + | // An invalid duplicate keeps the earlier value and the rule. | |
| 70 | + | {"bad duplicate floating", "rule.a.app-id = foo\nrule.a.floating = on\nrule.a.floating = maybe\n", "rule.a.floating: must be on or off", 3, 1}, | |
| 71 | + | {"bad duplicate app-id", "rule.a.app-id = foo\nrule.a.app-id = (\n", "rule.a.app-id: invalid regular expression", 2, 1}, | |
| 69 | 72 | {"bad width", "rule.a.app-id = foo\nrule.a.width = wide\n", "rule.a.width: must be a width", 2, 0}, | |
| 70 | 73 | {"bad workspace number", "rule.a.app-id = foo\nrule.a.workspace = 0\n", "rule.a.workspace: workspace number", 2, 0}, | |
| 71 | 74 | {"bad workspace name", "rule.a.app-id = foo\nrule.a.workspace = a b\n", "rule.a.workspace: must be", 2, 0}, | |
| @@ -66,6 +66,9 @@ func TestRuleWarnings(t *testing.T) { | |||||
| 66 | {"missing app-id", "rule.a.floating = on\n", "rule.a: no app-id, rule ignored", 1, 0}, | 66 | {"missing app-id", "rule.a.floating = on\n", "rule.a: no app-id, rule ignored", 1, 0}, | ||
| 67 | {"unknown field", "rule.a.app-id = foo\nrule.a.title = x\n", "rule.a.title: unknown key", 2, 1}, | 67 | {"unknown field", "rule.a.app-id = foo\nrule.a.title = x\n", "rule.a.title: unknown key", 2, 1}, | ||
| 68 | {"bad floating", "rule.a.app-id = foo\nrule.a.floating = maybe\n", "rule.a.floating: must be on or off", 2, 0}, | 68 | {"bad floating", "rule.a.app-id = foo\nrule.a.floating = maybe\n", "rule.a.floating: must be on or off", 2, 0}, | ||
| 69 | + | // An invalid duplicate keeps the earlier value and the rule. | |||
| 70 | + | {"bad duplicate floating", "rule.a.app-id = foo\nrule.a.floating = on\nrule.a.floating = maybe\n", "rule.a.floating: must be on or off", 3, 1}, | |||
| 71 | + | {"bad duplicate app-id", "rule.a.app-id = foo\nrule.a.app-id = (\n", "rule.a.app-id: invalid regular expression", 2, 1}, | |||
| 69 | {"bad width", "rule.a.app-id = foo\nrule.a.width = wide\n", "rule.a.width: must be a width", 2, 0}, | 72 | {"bad width", "rule.a.app-id = foo\nrule.a.width = wide\n", "rule.a.width: must be a width", 2, 0}, | ||
| 70 | {"bad workspace number", "rule.a.app-id = foo\nrule.a.workspace = 0\n", "rule.a.workspace: workspace number", 2, 0}, | 73 | {"bad workspace number", "rule.a.app-id = foo\nrule.a.workspace = 0\n", "rule.a.workspace: workspace number", 2, 0}, | ||
| 71 | {"bad workspace name", "rule.a.app-id = foo\nrule.a.workspace = a b\n", "rule.a.workspace: must be", 2, 0}, | 74 | {"bad workspace name", "rule.a.app-id = foo\nrule.a.workspace = a b\n", "rule.a.workspace: must be", 2, 0}, | ||