diff options
| -rw-r--r-- | build.zig | 10 | ||||
| -rw-r--r-- | common/globber.zig | 223 | ||||
| -rw-r--r-- | completions/bash/riverctl | 7 | ||||
| -rw-r--r-- | completions/fish/riverctl.fish | 9 | ||||
| -rw-r--r-- | completions/zsh/_riverctl | 9 | ||||
| -rw-r--r-- | doc/riverctl.1.scd | 94 | ||||
| -rw-r--r-- | river/Config.zig | 69 | ||||
| -rw-r--r-- | river/Cursor.zig | 2 | ||||
| -rw-r--r-- | river/LayoutDemand.zig | 2 | ||||
| -rw-r--r-- | river/RuleList.zig | 106 | ||||
| -rw-r--r-- | river/View.zig | 15 | ||||
| -rw-r--r-- | river/XdgDecoration.zig | 21 | ||||
| -rw-r--r-- | river/XdgToplevel.zig | 10 | ||||
| -rw-r--r-- | river/XwaylandView.zig | 24 | ||||
| -rw-r--r-- | river/command.zig | 21 | ||||
| -rw-r--r-- | river/command/filter.zig | 147 | ||||
| -rw-r--r-- | river/command/rule.zig | 164 |
17 files changed, 662 insertions, 271 deletions
@@ -163,6 +163,7 @@ pub fn build(b: *zbs.Builder) !void { river.linkSystemLibrary("wlroots"); river.addPackagePath("flags", "common/flags.zig"); + river.addPackagePath("globber", "common/globber.zig"); river.addCSourceFile("river/wlroots_log_wrapper.c", &[_][]const u8{ "-std=c99", "-O2" }); // TODO: remove when zig issue #131 is implemented @@ -254,6 +255,15 @@ pub fn build(b: *zbs.Builder) !void { if (fish_completion) { b.installFile("completions/fish/riverctl.fish", "share/fish/vendor_completions.d/riverctl.fish"); } + + { + const globber_test = b.addTest("common/globber.zig"); + globber_test.setTarget(target); + globber_test.setBuildMode(mode); + + const test_step = b.step("test", "Run the tests"); + test_step.dependOn(&globber_test.step); + } } const ScdocStep = struct { diff --git a/common/globber.zig b/common/globber.zig new file mode 100644 index 0000000..ead0fe7 --- /dev/null +++ b/common/globber.zig @@ -0,0 +1,223 @@ +// Basic prefix, suffix, and substring glob matching. +// +// Released under the Zero Clause BSD (0BSD) license: +// +// Copyright 2023 Isaac Freund +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +const std = @import("std"); +const mem = std.mem; + +/// Validate a glob, returning error.InvalidGlob if it is empty, "**" or has a +/// '*' at any position other than the first and/or last byte. +pub fn validate(glob: []const u8) error{InvalidGlob}!void { + switch (glob.len) { + 0 => return error.InvalidGlob, + 1 => {}, + 2 => if (glob[0] == '*' and glob[1] == '*') return error.InvalidGlob, + else => if (mem.indexOfScalar(u8, glob[1 .. glob.len - 1], '*') != null) { + return error.InvalidGlob; + }, + } +} + +test validate { + const testing = std.testing; + + _ = try validate("*"); + _ = try validate("a"); + _ = try validate("*a"); + _ = try validate("a*"); + _ = try validate("*a*"); + _ = try validate("ab"); + _ = try validate("*ab"); + _ = try validate("ab*"); + _ = try validate("*ab*"); + _ = try validate("abc"); + _ = try validate("*abc"); + _ = try validate("abc*"); + _ = try validate("*abc*"); + + try testing.expectError(error.InvalidGlob, validate("")); + try testing.expectError(error.InvalidGlob, validate("**")); + try testing.expectError(error.InvalidGlob, validate("***")); + try testing.expectError(error.InvalidGlob, validate("a*c")); + try testing.expectError(error.InvalidGlob, validate("ab*c*")); + try testing.expectError(error.InvalidGlob, validate("*ab*c")); + try testing.expectError(error.InvalidGlob, validate("ab*c")); + try testing.expectError(error.InvalidGlob, validate("a*bc*")); + try testing.expectError(error.InvalidGlob, validate("**a")); + try testing.expectError(error.InvalidGlob, validate("abc**")); +} + +/// Return true if s is matched by glob. +/// Asserts that the glob is valid, see `validate()`. +pub fn match(s: []const u8, glob: []const u8) bool { + if (std.debug.runtime_safety) { + validate(glob) catch unreachable; + } + + if (glob.len == 1) { + return glob[0] == '*' or mem.eql(u8, s, glob); + } + + const suffix_match = glob[0] == '*'; + const prefix_match = glob[glob.len - 1] == '*'; + + if (suffix_match and prefix_match) { + return mem.indexOf(u8, s, glob[1 .. glob.len - 1]) != null; + } else if (suffix_match) { + return mem.endsWith(u8, s, glob[1..]); + } else if (prefix_match) { + return mem.startsWith(u8, s, glob[0 .. glob.len - 1]); + } else { + return mem.eql(u8, s, glob); + } +} + +test match { + const testing = std.testing; + + try testing.expect(match("", "*")); + + try testing.expect(match("a", "*")); + try testing.expect(match("a", "*a*")); + try testing.expect(match("a", "a*")); + try testing.expect(match("a", "*a")); + try testing.expect(match("a", "a")); + + try testing.expect(!match("a", "b")); + try testing.expect(!match("a", "*b*")); + try testing.expect(!match("a", "b*")); + try testing.expect(!match("a", "*b")); + + try testing.expect(match("ab", "*")); + try testing.expect(match("ab", "*a*")); + try testing.expect(match("ab", "*b*")); + try testing.expect(match("ab", "a*")); + try testing.expect(match("ab", "*b")); + try testing.expect(match("ab", "*ab*")); + try testing.expect(match("ab", "ab*")); + try testing.expect(match("ab", "*ab")); + try testing.expect(match("ab", "ab")); + + try testing.expect(!match("ab", "b*")); + try testing.expect(!match("ab", "*a")); + try testing.expect(!match("ab", "*c*")); + try testing.expect(!match("ab", "c*")); + try testing.expect(!match("ab", "*c")); + try testing.expect(!match("ab", "ac")); + try testing.expect(!match("ab", "*ac*")); + try testing.expect(!match("ab", "ac*")); + try testing.expect(!match("ab", "*ac")); + + try testing.expect(match("abc", "*")); + try testing.expect(match("abc", "*a*")); + try testing.expect(match("abc", "*b*")); + try testing.expect(match("abc", "*c*")); + try testing.expect(match("abc", "a*")); + try testing.expect(match("abc", "*c")); + try testing.expect(match("abc", "*ab*")); + try testing.expect(match("abc", "ab*")); + try testing.expect(match("abc", "*bc*")); + try testing.expect(match("abc", "*bc")); + try testing.expect(match("abc", "*abc*")); + try testing.expect(match("abc", "abc*")); + try testing.expect(match("abc", "*abc")); + try testing.expect(match("abc", "abc")); + + try testing.expect(!match("abc", "*a")); + try testing.expect(!match("abc", "*b")); + try testing.expect(!match("abc", "b*")); + try testing.expect(!match("abc", "c*")); + try testing.expect(!match("abc", "*ab")); + try testing.expect(!match("abc", "bc*")); + try testing.expect(!match("abc", "*d*")); + try testing.expect(!match("abc", "d*")); + try testing.expect(!match("abc", "*d")); +} + +/// Returns .lt if a is less general than b. +/// Returns .gt if a is more general than b. +/// Returns .eq if a and b are equally general. +/// Both a and b must be valid globs, see `validate()`. +pub fn order(a: []const u8, b: []const u8) std.math.Order { + if (std.debug.runtime_safety) { + validate(a) catch unreachable; + validate(b) catch unreachable; + } + + if (mem.eql(u8, a, "*") and mem.eql(u8, b, "*")) { + return .eq; + } else if (mem.eql(u8, a, "*")) { + return .gt; + } else if (mem.eql(u8, b, "*")) { + return .lt; + } + + const count_a = @as(u2, @boolToInt(a[0] == '*')) + @boolToInt(a[a.len - 1] == '*'); + const count_b = @as(u2, @boolToInt(b[0] == '*')) + @boolToInt(b[b.len - 1] == '*'); + + if (count_a == 0 and count_b == 0) { + return .eq; + } else if (count_a == count_b) { + // This may look backwards since e.g. "c*" is more general than "cc*" + return std.math.order(b.len, a.len); + } else { + return std.math.order(count_a, count_b); + } +} + +test order { + const testing = std.testing; + const Order = std.math.Order; + + try testing.expectEqual(Order.eq, order("*", "*")); + try testing.expectEqual(Order.eq, order("*a*", "*b*")); + try testing.expectEqual(Order.eq, order("a*", "*b")); + try testing.expectEqual(Order.eq, order("*a", "*b")); + try testing.expectEqual(Order.eq, order("*a", "b*")); + try testing.expectEqual(Order.eq, order("a*", "b*")); + + const descending = [_][]const u8{ + "*", + "*a*", + "*b*", + "*a*", + "*ab*", + "*bab*", + "*a", + "b*", + "*b", + "*a", + "a", + "bababab", + "b", + "a", + }; + + for (descending) |a, i| { + for (descending[i..]) |b| { + try testing.expect(order(a, b) != .lt); + } + } + + var ascending = descending; + mem.reverse([]const u8, &ascending); + + for (ascending) |a, i| { + for (ascending[i..]) |b| { + try testing.expect(order(a, b) != .gt); + } + } +} diff --git a/completions/bash/riverctl b/completions/bash/riverctl index f50ac3b..d1de796 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -8,9 +8,7 @@ function __riverctl_completion () keyboard-group-add \ keyboard-group-remove \ keyboard-layout \ - csd-filter-add \ exit \ - float-filter-add \ focus-output \ focus-view \ input \ @@ -18,6 +16,9 @@ function __riverctl_completion () list-input-configs \ move \ resize \ + rule-add \ + rule-del \ + list-rules \ snap \ send-to-output \ spawn \ @@ -61,6 +62,8 @@ function __riverctl_completion () "focus-output"|"focus-view"|"send-to-output"|"swap") OPTS="next previous" ;; "move"|"snap") OPTS="up down left right" ;; "resize") OPTS="horizontal vertical" ;; + "rule-add"|"rule-del") OPTS="float no-float ssd csd" ;; + "list-rules") OPTS="float ssd" ;; "map") OPTS="-release -repeat -layout" ;; "unmap") OPTS="-release" ;; "attach-mode") OPTS="top bottom" ;; diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index 35db6e8..a207156 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -12,9 +12,7 @@ end # Actions complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'close' -d 'Close the focued view' -complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'csd-filter-add' -d 'Add app-id to the CSD filter list' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'exit' -d 'Exit the compositor, terminating the Wayland session' -complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'float-filter-add' -d 'Add app-id to the float filter list' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'focus-output' -d 'Focus the next or previous output' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'focus-view' -d 'Focus the next or previous view in the stack' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'input' -d 'Create a configuration rule for an input device' @@ -49,6 +47,10 @@ complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'map-switch ' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'unmap' -d 'Remove the mapping defined by the arguments' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'unmap-pointer' -d 'Remove the pointer mapping defined by the arguments' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'unmap-switch' -d 'Remove the switch mapping defined by the arguments' +# Rules +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'rule-add' -d 'Apply an action to matching views' +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'rule-del' -d 'Delete a rule added with rule-add' +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'list-rules' -d 'Print rules in a given list' # Configuration complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'attach-mode' -d 'Configure where new views should attach to the view stack' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'background-color' -d 'Set the background color' @@ -81,6 +83,9 @@ complete -c riverctl -x -n '__fish_seen_subcommand_from unmap' -a complete -c riverctl -x -n '__fish_seen_subcommand_from attach-mode' -a 'top bottom' complete -c riverctl -x -n '__fish_seen_subcommand_from focus-follows-cursor' -a 'disabled normal always' complete -c riverctl -x -n '__fish_seen_subcommand_from set-cursor-warp' -a 'disabled on-output-change on-focus-change' +complete -c riverctl -x -n '__fish_seen_subcommand_from rule-add' -a 'float no-float ssd csd' +complete -c riverctl -x -n '__fish_seen_subcommand_from rule-del' -a 'float no-float ssd csd' +complete -c riverctl -x -n '__fish_seen_subcommand_from list-rules' -a 'float ssd' # Subcommands for 'input' complete -c riverctl -x -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 2' -a "(__riverctl_list_input_devices)" diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl index 90e39d2..53b2194 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -9,9 +9,7 @@ _riverctl_subcommands() riverctl_subcommands=( # Actions 'close:Close the focused view' - 'csd-filter-add:Add app-id to the CSD filter list' 'exit:Exit the compositor, terminating the Wayland session' - 'float-filter-add:Add app-id to the float filter list' 'focus-output:Focus the next or previous output' 'focus-view:Focus the next or previous view in the stack' 'move:Move the focused view in the specified direction' @@ -43,6 +41,10 @@ _riverctl_subcommands() 'unmap:Remove the mapping defined by the arguments' 'unmap-pointer:Remove the pointer mapping defined by the arguments' 'unmap-switch:Remove the switch mapping defined by the arguments' + # Rules + 'rule-add:Apply an action to matching views' + 'rule-del:Delete a rule added with rule-add' + 'list-rules:Print rules in a given list' # Configuration 'attach-mode:Configure where new views should attach to the view stack' 'background-color:Set the background color' @@ -181,6 +183,9 @@ _riverctl() focus-follows-cursor) _alternative 'arguments:args:(disabled normal always)' ;; set-cursor-warp) _alternative 'arguments:args:(disabled on-output-change on-focus-change)' ;; hide-cursor) _riverctl_hide_cursor ;; + rule-add) _alternative 'arguments:args:(float no-float ssd csd)' ;; + rule-del) _alternative 'arguments:args:(float no-float ssd csd)' ;; + list-rules) _alternative 'arguments:args:(float ssd)' ;; *) return 0 ;; esac ;; diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 2b6bfd7..9809b4c 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -28,28 +28,9 @@ over the Wayland protocol. *close* Close the focused view. -*csd-filter-add* *app-id*|*title* _pattern_ - Add _pattern_ to the CSD filter list. Views with this _pattern_ are told to - use client side decoration instead of the default server side decoration. - Note that this affects new views as well as already existing ones. Title - updates are not taken into account. - -*csd-filter-remove* *app-id*|*title* _pattern_ - Remove _pattern_ from the CSD filter list. Note that this affects new views - as well as already existing ones. - *exit* Exit the compositor, terminating the Wayland session. -*float-filter-add* *app-id*|*title* _pattern_ - Add a pattern to the float filter list. Note that this affects only new - views, not already existing ones. Title updates are also not taken into - account. - -*float-filter-remove* *app-id*|*title* _pattern_ - Remove an app-id or title from the float filter list. Note that this - affects only new views, not already existing ones. - *focus-output* *next*|*previous*|*up*|*right*|*down*|*left*|_name_ Focus the next or previous output, the closest output in any direction or an output by name. @@ -192,18 +173,18 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ *enter-mode* _name_ Switch to given mode if it exists. -*map* [_-release_|_-repeat_|_-layout_ _index_] _mode_ _modifiers_ _key_ _command_ +*map* [*-release*|*-repeat*|*-layout* _index_] _mode_ _modifiers_ _key_ _command_ Run _command_ when _key_ is pressed while _modifiers_ are held down and in the specified _mode_. - - _-release_: if passed activate on key release instead of key press - - _-repeat_: if passed activate repeatedly until key release; may not - be used with -release - - _-layout_: if passed, a specific layout is pinned to the mapping. + - *-release*: if passed activate on key release instead of key press + - *-repeat*: if passed activate repeatedly until key release; may not + be used with *-release* + - *-layout*: if passed, a specific layout is pinned to the mapping. When the mapping is checked against a pressed key, this layout is used to translate the key independent of the active layout - _index_: zero-based index of a layout set with the *keyboard-layout* - command. If the index is out of range, the _-layout_ option will + command. If the index is out of range, the *-layout* option will have no effect - _mode_: name of the mode for which to create the mapping - _modifiers_: one or more of the modifiers listed above, separated @@ -239,10 +220,10 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ - off - _command_: any command that may be run with riverctl -*unmap* [_-release_] _mode_ _modifiers_ _key_ +*unmap* [*-release*] _mode_ _modifiers_ _key_ Remove the mapping defined by the arguments: - - _-release_: if passed unmap the key release instead of the key press + - *-release*: if passed unmap the key release instead of the key press - _mode_: name of the mode for which to remove the mapping - _modifiers_: one or more of the modifiers listed above, separated by a plus sign (+). @@ -263,6 +244,65 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ - _lid_|_tablet_: the switch for which to remove the mapping - _state_: a state as listed above +## RULES + +Rules match either the app-id and title of views against a _glob_ pattern. +A _glob_ is a string that may optionally have an _\*_ at the beginning and/or +end. A _\*_ in a _glob_ matches zero or more arbitrary characters in the +app-id or title. + +For example, _abc_ is matched by _a\*_, _\*a\*_, _\*b\*_, _\*c_, _abc_, and +_\*_ but not matched by _\*a_, _b\*_, _\*b_, _\*c_, or _ab_. Note that _\*_ +matches everything while _\*\*_ and the empty string are invalid. + +*rule-add* _action_ [*-app-id* _glob_|*-title* _glob_] + Add a rule that applies an _action_ to views with *app-id* and *title* + matched by the respective _glob_. Omitting *-app-id* or *-title* + is equivalent to passing *-app-id* _\*_ or *-title* _\*_. + + The supported _action_ types are: + + - *float*: Make the view floating. Applies only to new views. + - *no-float*: Don't make the view floating. Applies only to + new views. + - *ssd*: Use server-side decorations for the view. Applies to new + and existing views. + - *csd*: Use client-side decorations for the view. Applies to new + and existing views. + + Both *float* and *no-float* rules are added to the same list, + which means that adding a *no-float* rule with the same arguments + as a *float* rule will overwrite it. The same holds for *ssd* and + *csd* rules. + + If multiple rules in a list match a given view the most specific + rule will be applied. For example with the following rules + ``` + action app-id title + ssd foo bar + csd foo * + csd * bar + ssd * baz + ``` + a view with app-id 'foo' and title 'bar' would get ssd despite matching + two csd rules as the first rule is most specific. Furthermore a view + with app-id 'foo' and title 'baz' would get csd despite matching the + last rule in the list since app-id specificity takes priority over + title specificity. + + If a view is not matched by any rule, river will respect the csd/ssd + wishes of the client and may start the view floating based on simple + heuristics intended to catch popup-like views. + +*rule-del* _action_ [*-app-id* _glob_|*-title* _glob_] + Delete a rule created using *rule-add* with the given arguments. + +*list-rules* *float*|*ssd* + Print the specified rule list. The output is ordered from most specific + to least specific, the same order in which views are checked against + when searching for a match. Only the first matching rule in the list + has an effect on a given view. + ## CONFIGURATION *attach-mode* *top*|*bottom* diff --git a/river/Config.zig b/river/Config.zig index 08bca49..5a14529 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -18,13 +18,14 @@ const Self = @This(); const std = @import("std"); const mem = std.mem; +const globber = @import("globber"); const xkb = @import("xkbcommon"); const util = @import("util.zig"); const Server = @import("Server.zig"); const Mode = @import("Mode.zig"); -const View = @import("View.zig"); +const RuleList = @import("RuleList.zig"); pub const AttachMode = enum { top, @@ -72,13 +73,8 @@ mode_to_id: std.StringHashMap(u32), /// All user-defined keymap modes, indexed by mode id modes: std.ArrayListUnmanaged(Mode), -/// Sets of app_ids and titles which will be started floating -float_filter_app_ids: std.StringHashMapUnmanaged(void) = .{}, -float_filter_titles: std.StringHashMapUnmanaged(void) = .{}, - -/// Sets of app_ids and titles which are allowed to use client side decorations -csd_filter_app_ids: std.StringHashMapUnmanaged(void) = .{}, -csd_filter_titles: std.StringHashMapUnmanaged(void) = .{}, +float_rules: RuleList = .{}, +ssd_rules: RuleList = .{}, /// The selected focus_follows_cursor mode focus_follows_cursor: FocusFollowsCursorMode = .disabled, @@ -152,64 +148,11 @@ pub fn deinit(self: *Self) void { for (self.modes.items) |*mode| mode.deinit(); self.modes.deinit(util.gpa); - { - var it = self.float_filter_app_ids.keyIterator(); - while (it.next()) |key| util.gpa.free(key.*); - self.float_filter_app_ids.deinit(util.gpa); - } - - { - var it = self.float_filter_titles.keyIterator(); - while (it.next()) |key| util.gpa.free(key.*); - self.float_filter_titles.deinit(util.gpa); - } - - { - var it = self.csd_filter_app_ids.keyIterator(); - while (it.next()) |key| util.gpa.free(key.*); - self.csd_filter_app_ids.deinit(util.gpa); - } - - { - var it = self.csd_filter_titles.keyIterator(); - while (it.next()) |key| util.gpa.free(key.*); - self.csd_filter_titles.deinit(util.gpa); - } + self.float_rules.deinit(); + self.ssd_rules.deinit(); util.gpa.free(self.default_layout_namespace); self.keymap.unref(); self.xkb_context.unref(); } - -pub fn shouldFloat(self: Self, view: *View) bool { - if (view.getAppId()) |app_id| { - if (self.float_filter_app_ids.contains(std.mem.span(app_id))) { - return true; - } - } - - if (view.getTitle()) |title| { - if (self.float_filter_titles.contains(std.mem.span(title))) { - return true; - } - } - - return false; -} - -pub fn csdAllowed(self: Self, view: *View) bool { - if (view.getAppId()) |app_id| { - if (self.csd_filter_app_ids.contains(std.mem.span(app_id))) { - return true; - } - } - - if (view.getTitle()) |title| { - if (self.csd_filter_titles.contains(std.mem.span(title))) { - return true; - } - } - - return false; -} diff --git a/river/Cursor.zig b/river/Cursor.zig index e0949c5..d7497f9 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -877,7 +877,7 @@ fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, { // Modify the pending box, taking constraints into account - const border_width = if (data.view.pending.borders) server.config.border_width else 0; + const border_width = if (data.view.pending.ssd) server.config.border_width else 0; var output_width: i32 = undefined; var output_height: i32 = undefined; diff --git a/river/LayoutDemand.zig b/river/LayoutDemand.zig index a6f8ffa..a127938 100644 --- a/river/LayoutDemand.zig +++ b/river/LayoutDemand.zig @@ -132,7 +132,7 @@ pub fn apply(self: *Self, layout: *Layout) void { // Here we apply the offset to align the coords with the origin of the // usable area and shrink the dimensions to accommodate the border size. - const border_width = if (view.inflight.borders) server.config.border_width else 0; + const border_width = if (view.inflight.ssd) server.config.border_width else 0; view.inflight.box = .{ .x = proposed.x + output.usable_box.x + border_width, .y = proposed.y + output.usable_box.y + border_width, diff --git a/river/RuleList.zig b/river/RuleList.zig new file mode 100644 index 0000000..db765e7 --- /dev/null +++ b/river/RuleList.zig @@ -0,0 +1,106 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2023 The River Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +const RuleList = @This(); + +const std = @import("std"); +const mem = std.mem; + +const globber = @import("globber"); +const util = @import("util.zig"); + +const View = @import("View.zig"); + +const Rule = struct { + app_id_glob: []const u8, + title_glob: []const u8, + value: bool, +}; + +/// Ordered from most specific to most general. +/// Ordered first by app-id generality then by title generality. +rules: std.ArrayListUnmanaged(Rule) = .{}, + +pub fn deinit(list: *RuleList) void { + for (list.rules.items) |rule| { + util.gpa.free(rule.app_id_glob); + util.gpa.free(rule.title_glob); + } + list.rules.deinit(util.gpa); +} + +pub fn add(list: *RuleList, rule: Rule) error{OutOfMemory}!void { + const index = for (list.rules.items) |*existing, i| { + if (mem.eql(u8, rule.app_id_glob, existing.app_id_glob) and + mem.eql(u8, rule.title_glob, existing.title_glob)) + { + existing.value = rule.value; + return; + } + + switch (globber.order(rule.app_id_glob, existing.app_id_glob)) { + .lt => break i, + .eq => { + if (globber.order(rule.title_glob, existing.title_glob) == .lt) { + break i; + } + }, + .gt => {}, + } + } else list.rules.items.len; + + const owned_app_id_glob = try util.gpa.dupe(u8, rule.app_id_glob); + errdefer util.gpa.free(owned_app_id_glob); + + const owned_title_glob = try util.gpa.dupe(u8, rule.title_glob); + errdefer util.gpa.free(owned_title_glob); + + try list.rules.insert(util.gpa, index, .{ + .app_id_glob = owned_app_id_glob, + .title_glob = owned_title_glob, + .value = rule.value, + }); +} + +pub fn del(list: *RuleList, rule: Rule) void { + for (list.rules.items) |existing, i| { + if (mem.eql(u8, rule.app_id_glob, existing.app_id_glob) and + mem.eql(u8, rule.title_glob, existing.title_glob)) + { + util.gpa.free(existing.app_id_glob); + util.gpa.free(existing.title_glob); + _ = list.rules.orderedRemove(i); + return; + } + } +} + +/// Returns the value of the most specific rule matching the view. +/// Returns null if no rule matches. +pub fn match(list: *RuleList, view: *View) ?bool { + const app_id = mem.sliceTo(view.getAppId(), 0) orelse ""; + const title = mem.sliceTo(view.getTitle(), 0) orelse ""; + + for (list.rules.items) |rule| { + if (globber.match(app_id, rule.app_id_glob) and + globber.match(title, rule.title_glob)) + { + return rule.value; + } + } + + return null; +} diff --git a/river/View.zig b/river/View.zig index a436903..24934dd 100644 --- a/river/View.zig +++ b/river/View.zig @@ -72,13 +72,13 @@ pub const State = struct { float: bool = false, fullscreen: bool = false, urgent: bool = false, - borders: bool = true, + ssd: bool = false, resizing: bool = false, /// Modify the x/y of the given state by delta_x/delta_y, clamping to the /// bounds of the output. pub fn move(state: *State, delta_x: i32, delta_y: i32) void { - const border_width = if (state.borders) server.config.border_width else 0; + const border_width = if (state.ssd) server.config.border_width else 0; var output_width: i32 = math.maxInt(i32); var output_height: i32 = math.maxInt(i32); @@ -106,7 +106,7 @@ pub const State = struct { var output_height: i32 = undefined; output.wlr_output.effectiveResolution(&output_width, &output_height); - const border_width = if (state.borders) server.config.border_width else 0; + const border_width = if (state.ssd) server.config.border_width else 0; state.box.width = math.min(state.box.width, output_width - (2 * border_width)); state.box.height = math.min(state.box.height, output_height - (2 * border_width)); @@ -265,7 +265,7 @@ pub fn updateCurrent(view: *Self) void { view.tree.node.setPosition(box.x, box.y); view.popup_tree.node.setPosition(box.x, box.y); - const enable_borders = view.current.borders and !view.current.fullscreen; + const enable_borders = view.current.ssd and !view.current.fullscreen; const border_width: c_int = config.border_width; view.borders.left.node.setEnabled(enable_borders); @@ -428,7 +428,12 @@ pub fn map(view: *Self) !void { view.foreign_toplevel_handle.map(); - view.pending.borders = !server.config.csdAllowed(view); + if (server.config.float_rules.match(view)) |float| { + view.pending.float = float; + } + if (server.config.ssd_rules.match(view)) |ssd| { + view.pending.ssd = ssd; + } if (server.input_manager.defaultSeat().focused_output) |output| { // Center the initial pending box on the output diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig index 8eb44b8..39c28c7 100644 --- a/river/XdgDecoration.zig +++ b/river/XdgDecoration.zig @@ -42,7 +42,14 @@ pub fn init(wlr_decoration: *wlr.XdgToplevelDecorationV1) void { wlr_decoration.events.destroy.add(&decoration.destroy); wlr_decoration.events.request_mode.add(&decoration.request_mode); - handleRequestMode(&decoration.request_mode, decoration.wlr_decoration); + const ssd = server.config.ssd_rules.match(xdg_toplevel.view) orelse + (decoration.wlr_decoration.requested_mode != .client_side); + + // TODO(wlroots): make sure this is properly batched in a single configure + // with all other initial state when wlroots makes this possible. + _ = wlr_decoration.setMode(if (ssd) .server_side else .client_side); + + xdg_toplevel.view.pending.ssd = ssd; } // TODO(wlroots): remove this function when updating to 0.17.0 @@ -72,9 +79,13 @@ fn handleRequestMode( const decoration = @fieldParentPtr(XdgDecoration, "request_mode", listener); const xdg_toplevel = @intToPtr(*XdgToplevel, decoration.wlr_decoration.surface.data); - if (server.config.csdAllowed(xdg_toplevel.view)) { - _ = decoration.wlr_decoration.setMode(.client_side); - } else { - _ = decoration.wlr_decoration.setMode(.server_side); + const view = xdg_toplevel.view; + + const ssd = server.config.ssd_rules.match(xdg_toplevel.view) orelse + (decoration.wlr_decoration.requested_mode != .client_side); + + if (view.pending.ssd != ssd) { + view.pending.ssd = ssd; + server.root.applyPending(); } } diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index 64c3f99..188be4b 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -115,6 +115,7 @@ pub fn configure(self: *Self) bool { (inflight.focus != 0) == (current.focus != 0) and inflight_fullscreen == current_fullscreen and inflight_float == current_float and + inflight.ssd == current.ssd and inflight.resizing == current.resizing) { return false; @@ -130,6 +131,10 @@ pub fn configure(self: *Self) bool { _ = self.xdg_toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); } + if (self.decoration) |decoration| { + _ = decoration.wlr_decoration.setMode(if (inflight.ssd) .server_side else .client_side); + } + _ = self.xdg_toplevel.setResizing(inflight.resizing); // Only track configures with the transaction system if they affect the dimensions of the view. @@ -226,9 +231,8 @@ fn handleMap(listener: *wl.Listener(void)) void { (state.min_width == state.max_width or state.min_height == state.max_height); if (self.xdg_toplevel.parent != null or has_fixed_size) { - // If the self.xdg_toplevel has a parent or has a fixed size make it float - view.pending.float = true; - } else if (server.config.shouldFloat(view)) { + // If the self.xdg_toplevel has a parent or has a fixed size make it float. + // This will be overwritten in View.map() if the view is matched by a rule. view.pending.float = true; } diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig index 274e8ed..fe90a10 100644 --- a/river/XwaylandView.zig +++ b/river/XwaylandView.zig @@ -51,6 +51,8 @@ set_override_redirect: wl.Listener(*wlr.XwaylandSurface) = // Listeners that are only active while the view is mapped set_title: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleSetTitle), set_class: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleSetClass), +set_decorations: wl.Listener(*wlr.XwaylandSurface) = + wl.Listener(*wlr.XwaylandSurface).init(handleSetDecorations), request_fullscreen: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleRequestFullscreen), request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) = @@ -168,6 +170,7 @@ pub fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: // Add listeners that are only active while mapped xwayland_surface.events.set_title.add(&self.set_title); xwayland_surface.events.set_class.add(&self.set_class); + xwayland_surface.events.set_decorations.add(&self.set_decorations); xwayland_surface.events.request_fullscreen.add(&self.request_fullscreen); xwayland_surface.events.request_minimize.add(&self.request_minimize); @@ -194,12 +197,14 @@ pub fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: false; if (self.xwayland_surface.parent != null or has_fixed_size) { - // If the toplevel has a parent or has a fixed size make it float - view.pending.float = true; - } else if (server.config.shouldFloat(view)) { + // If the toplevel has a parent or has a fixed size make it float by default. + // This will be overwritten in View.map() if the view is matched by a rule. view.pending.float = true; } + // This will be overwritten in View.map() if the view is matched by a rule. + view.pending.ssd = !xwayland_surface.decorations.no_border; + view.pending.fullscreen = xwayland_surface.fullscreen; view.map() catch { @@ -276,6 +281,19 @@ fn handleSetClass(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.Xwayland self.view.notifyAppId(); } +fn handleSetDecorations(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void { + const self = @fieldParentPtr(Self, "set_decorations", listener); + const view = self.view; + + const ssd = server.config.ssd_rules.match(view) orelse + !self.xwayland_surface.decorations.no_border; + + if (view.pending.ssd != ssd) { + view.pending.ssd = ssd; + server.root.applyPending(); + } +} + fn handleRequestFullscreen(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void { const self = @fieldParentPtr(Self, "request_fullscreen", listener); if (self.view.pending.fullscreen != xwayland_surface.fullscreen) { diff --git a/river/command.zig b/river/command.zig index 8e541d7..ede1704 100644 --- a/river/command.zig +++ b/river/command.zig @@ -47,28 +47,32 @@ const command_impls = std.ComptimeStringMap( .{ "border-color-urgent", @import("command/config.zig").borderColorUrgent }, .{ "border-width", @import("command/config.zig").borderWidth }, .{ "close", @import("command/close.zig").close }, - .{ "csd-filter-add", @import("command/filter.zig").csdFilterAdd }, - .{ "csd-filter-remove", @import("command/filter.zig").csdFilterRemove }, .{ "declare-mode", @import("command/declare_mode.zig").declareMode }, .{ "default-layout", @import("command/layout.zig").defaultLayout }, .{ "enter-mode", @import("command/enter_mode.zig").enterMode }, .{ "exit", @import("command/exit.zig").exit }, - .{ "float-filter-add", @import("command/filter.zig").floatFilterAdd }, - .{ "float-filter-remove", @import("command/filter.zig").floatFilterRemove }, .{ "focus-follows-cursor", @import("command/focus_follows_cursor.zig").focusFollowsCursor }, .{ "focus-output", @import("command/output.zig").focusOutput }, .{ "focus-previous-tags", @import("command/tags.zig").focusPreviousTags }, .{ "focus-view", @import("command/focus_view.zig").focusView }, .{ "hide-cursor", @import("command/cursor.zig").cursor }, .{ "input", @import("command/input.zig").input }, + .{ "keyboard-group-add", @import("command/keyboard_group.zig").keyboardGroupAdd }, + .{ "keyboard-group-create", @import("command/keyboard_group.zig").keyboardGroupCreate }, + .{ "keyboard-group-destroy", @import("command/keyboard_group.zig").keyboardGroupDestroy }, + .{ "keyboard-group-remove", @import("command/keyboard_group.zig").keyboardGroupRemove }, + .{ "keyboard-layout", @import("command/keyboard.zig").keyboardLayout }, .{ "list-input-configs", @import("command/input.zig").listInputConfigs}, .{ "list-inputs", @import("command/input.zig").listInputs }, + .{ "list-rules", @import("command/rule.zig").listRules}, .{ "map", @import("command/map.zig").map }, .{ "map-pointer", @import("command/map.zig").mapPointer }, .{ "map-switch", @import("command/map.zig").mapSwitch }, .{ "move", @import("command/move.zig").move }, .{ "output-layout", @import("command/layout.zig").outputLayout }, .{ "resize", @import("command/move.zig").resize }, + .{ "rule-add", @import("command/rule.zig").ruleAdd }, + .{ "rule-del", @import("command/rule.zig").ruleDel }, .{ "send-layout-cmd", @import("command/layout.zig").sendLayoutCmd }, .{ "send-to-output", @import("command/output.zig").sendToOutput }, .{ "send-to-previous-tags", @import("command/tags.zig").sendToPreviousTags }, @@ -89,11 +93,6 @@ const command_impls = std.ComptimeStringMap( .{ "unmap-switch", @import("command/map.zig").unmapSwitch }, .{ "xcursor-theme", @import("command/xcursor_theme.zig").xcursorTheme }, .{ "zoom", @import("command/zoom.zig").zoom }, - .{ "keyboard-layout", @import("command/keyboard.zig").keyboardLayout }, - .{ "keyboard-group-create", @import("command/keyboard_group.zig").keyboardGroupCreate }, - .{ "keyboard-group-destroy", @import("command/keyboard_group.zig").keyboardGroupDestroy }, - .{ "keyboard-group-add", @import("command/keyboard_group.zig").keyboardGroupAdd }, - .{ "keyboard-group-remove", @import("command/keyboard_group.zig").keyboardGroupRemove }, }, ); // zig fmt: on @@ -107,6 +106,7 @@ pub const Error = error{ InvalidButton, InvalidCharacter, InvalidDirection, + InvalidGlob, InvalidPhysicalDirection, InvalidOutputIndicator, InvalidOrientation, @@ -136,7 +136,7 @@ pub fn run( try impl_fn(seat, args, out); } -/// Return a short error message for the given error. Passing Error.Other is UB +/// Return a short error message for the given error. Passing Error.Other is invalid. pub fn errToMsg(err: Error) [:0]const u8 { return switch (err) { Error.NoCommand => "no command given", @@ -149,6 +149,7 @@ pub fn errToMsg(err: Error) [:0]const u8 { Error.InvalidButton => "invalid button", Error.InvalidCharacter => "invalid character in argument", Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'", + Error.InvalidGlob => "invalid glob. '*' is only allowed as the first and/or last character", Error.InvalidPhysicalDirection => "invalid direction. Must be 'up', 'down', 'left' or 'right'", Error.InvalidOutputIndicator => "invalid indicator for an output. Must be 'next', 'previous', 'up', 'down', 'left', 'right' or a valid output name", Error.InvalidOrientation => "invalid orientation. Must be 'horizontal', or 'vertical'", diff --git a/river/command/filter.zig b/river/command/filter.zig deleted file mode 100644 index cb07c7d..0000000 --- a/river/command/filter.zig +++ /dev/null @@ -1,147 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 The River Developers -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 3. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -const std = @import("std"); -const assert = std.debug.assert; -const mem = std.mem; - -const server = &@import("../main.zig").server; -const util = @import("../util.zig"); - -const View = @import("../View.zig"); -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -const FilterKind = enum { - @"app-id", - title, -}; - -pub fn floatFilterAdd( - _: *Seat, - args: []const [:0]const u8, - _: *?[]const u8, -) Error!void { - if (args.len < 3) return Error.NotEnoughArguments; - if (args.len > 3) return Error.TooManyArguments; - - const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption; - const map = switch (kind) { - .@"app-id" => &server.config.float_filter_app_ids, - .title => &server.config.float_filter_titles, - }; - - const key = args[2]; - const gop = try map.getOrPut(util.gpa, key); - if (gop.found_existing) return; - errdefer assert(map.remove(key)); - gop.key_ptr.* = try util.gpa.dupe(u8, key); -} - -pub fn floatFilterRemove( - _: *Seat, - args: []const [:0]const u8, - _: *?[]const u8, -) Error!void { - if (args.len < 3) return Error.NotEnoughArguments; - if (args.len > 3) return Error.TooManyArguments; - - const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption; - const map = switch (kind) { - .@"app-id" => &server.config.float_filter_app_ids, - .title => &server.config.float_filter_titles, - }; - - const key = args[2]; - if (map.fetchRemove(key)) |kv| util.gpa.free(kv.key); -} - -pub fn csdFilterAdd( - _: *Seat, - args: []const [:0]const u8, - _: *?[]const u8, -) Error!void { - if (args.len < 3) return Error.NotEnoughArguments; - if (args.len > 3) return Error.TooManyArguments; - - const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption; - const map = switch (kind) { - .@"app-id" => &server.config.csd_filter_app_ids, - .title => &server.config.csd_filter_titles, - }; - - const key = args[2]; - const gop = try map.getOrPut(util.gpa, key); - if (gop.found_existing) return; - errdefer assert(map.remove(key)); - gop.key_ptr.* = try util.gpa.dupe(u8, key); - - csdFilterUpdateViews(kind, key, .add); - server.root.applyPending(); -} - -pub fn csdFilterRemove( - _: *Seat, - args: []const [:0]const u8, - _: *?[]const u8, -) Error!void { - if (args.len < 3) return Error.NotEnoughArguments; - if (args.len > 3) return Error.TooManyArguments; - - const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption; - const map = switch (kind) { - .@"app-id" => &server.config.csd_filter_app_ids, - .title => &server.config.csd_filter_titles, - }; - - const key = args[2]; - if (map.fetchRemove(key)) |kv| { - util.gpa.free(kv.key); - csdFilterUpdateViews(kind, key, .remove); - server.root.applyPending(); - } -} - -fn csdFilterUpdateViews(kind: FilterKind, pattern: []const u8, operation: enum { add, remove }) void { - var it = server.root.views.iterator(.forward); - while (it.next()) |view| { - if (view.impl == .xdg_toplevel) { - if (view.impl.xdg_toplevel.decoration) |decoration| { - if (viewMatchesPattern(kind, pattern, view)) { - switch (operation) { - .add => { - _ = decoration.wlr_decoration.setMode(.client_side); - view.pending.borders = false; - }, - .remove => { - _ = decoration.wlr_decoration.setMode(.server_side); - view.pending.borders = true; - }, - } - } - } - } - } -} - -fn viewMatchesPattern(kind: FilterKind, pattern: []const u8, view: *View) bool { - const p = switch (kind) { - .@"app-id" => mem.span(view.getAppId()), - .title => mem.span(view.getTitle()), - } orelse return false; - - return mem.eql(u8, pattern, p); -} diff --git a/river/command/rule.zig b/river/command/rule.zig new file mode 100644 index 0000000..c9665af --- /dev/null +++ b/river/command/rule.zig @@ -0,0 +1,164 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2023 The River Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +const std = @import("std"); +const fmt = std.fmt; + +const globber = @import("globber"); +const flags = @import("flags"); + +const server = &@import("../main.zig").server; +const util = @import("../util.zig"); + +const Error = @import("../command.zig").Error; +const RuleList = @import("../RuleList.zig"); +const Seat = @import("../Seat.zig"); +const View = @import("../View.zig"); + +const Action = enum { + float, + @"no-float", + ssd, + csd, +}; + +pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + + const result = flags.parser([:0]const u8, &.{ + .{ .name = "app-id", .kind = .arg }, + .{ .name = "title", .kind = .arg }, + }).parse(args[2..]) catch { + return error.InvalidValue; + }; + + if (result.args.len > 0) return Error.TooManyArguments; + + const action = std.meta.stringToEnum(Action, args[1]) orelse return Error.UnknownOption; + const app_id_glob = result.flags.@"app-id" orelse "*"; + const title_glob = result.flags.title orelse "*"; + + try globber.validate(app_id_glob); + try globber.validate(title_glob); + + switch (action) { + .float, .@"no-float" => { + try server.config.float_rules.add(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = (action == .float), + }); + }, + .ssd, .csd => { + try server.config.ssd_rules.add(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = (action == .ssd), + }); + apply_ssd_rules(); + server.root.applyPending(); + }, + } +} + +pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + + const result = flags.parser([:0]const u8, &.{ + .{ .name = "app-id", .kind = .arg }, + .{ .name = "title", .kind = .arg }, + }).parse(args[2..]) catch { + return error.InvalidValue; + }; + + if (result.args.len > 0) return Error.TooManyArguments; + + const action = std.meta.stringToEnum(Action, args[1]) orelse return Error.UnknownOption; + const app_id_glob = result.flags.@"app-id" orelse "*"; + const title_glob = result.flags.title orelse "*"; + + switch (action) { + .float, .@"no-float" => { + server.config.float_rules.del(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = (action == .float), + }); + }, + .ssd, .csd => { + server.config.ssd_rules.del(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = (action == .ssd), + }); + apply_ssd_rules(); + server.root.applyPending(); + }, + } +} + +fn apply_ssd_rules() void { + var it = server.root.views.iterator(.forward); + while (it.next()) |view| { + if (server.config.ssd_rules.match(view)) |ssd| { + view.pending.ssd = ssd; + } + } +} + +pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void { + if (args.len < 2) return error.NotEnoughArguments; + if (args.len > 2) return error.TooManyArguments; + + const list = std.meta.stringToEnum(enum { float, ssd }, args[1]) orelse return Error.UnknownOption; + + const rules = switch (list) { + .float => server.config.float_rules.rules.items, + .ssd => server.config.ssd_rules.rules.items, + }; + + var action_column_max = "action".len; + var app_id_column_max = "app-id".len; + for (rules) |rule| { + const action = switch (list) { + .float => if (rule.value) "float" else "no-float", + .ssd => if (rule.value) "ssd" else "csd", + }; + action_column_max = @max(action_column_max, action.len); + app_id_column_max = @max(app_id_column_max, rule.app_id_glob.len); + } + action_column_max += 2; + app_id_column_max += 2; + + var buffer = std.ArrayList(u8).init(util.gpa); + const writer = buffer.writer(); + + try fmt.formatBuf("action", .{ .width = action_column_max, .alignment = .Left }, writer); + try fmt.formatBuf("app-id", .{ .width = app_id_column_max, .alignment = .Left }, writer); + try writer.writeAll("title\n"); + + for (rules) |rule| { + const action = switch (list) { + .float => if (rule.value) "float" else "no-float", + .ssd => if (rule.value) "ssd" else "csd", + }; + try fmt.formatBuf(action, .{ .width = action_column_max, .alignment = .Left }, writer); + try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .Left }, writer); + try writer.print("{s}\n", .{rule.title_glob}); + } + + out.* = buffer.toOwnedSlice(); +} |
