aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsaac Freund <mail@isaacfreund.com>2023-03-12 15:40:42 +0100
committerIsaac Freund <mail@isaacfreund.com>2023-03-12 16:44:19 +0100
commitb2b2c9ed1397d345004fc2369217307b44bdbd88 (patch)
tree8021cdfbedb90a33a001da2ac90693eca89387dc
parent05eac54b076f2069469aa48377cae54f0cd311aa (diff)
downloadriver-b2b2c9ed1397d345004fc2369217307b44bdbd88.tar.gz
river-b2b2c9ed1397d345004fc2369217307b44bdbd88.tar.xz
river: add rules system
This is a breaking change and replaces the previous csd-filter-add/remove and float-filter-add/remove commands. See the riverctl(1) man page for documentation on the new system.
-rw-r--r--build.zig10
-rw-r--r--common/globber.zig223
-rw-r--r--completions/bash/riverctl7
-rw-r--r--completions/fish/riverctl.fish9
-rw-r--r--completions/zsh/_riverctl9
-rw-r--r--doc/riverctl.1.scd94
-rw-r--r--river/Config.zig69
-rw-r--r--river/Cursor.zig2
-rw-r--r--river/LayoutDemand.zig2
-rw-r--r--river/RuleList.zig106
-rw-r--r--river/View.zig15
-rw-r--r--river/XdgDecoration.zig21
-rw-r--r--river/XdgToplevel.zig10
-rw-r--r--river/XwaylandView.zig24
-rw-r--r--river/command.zig21
-rw-r--r--river/command/filter.zig147
-rw-r--r--river/command/rule.zig164
17 files changed, 662 insertions, 271 deletions
diff --git a/build.zig b/build.zig
index a3f1bde..1dd65eb 100644
--- a/build.zig
+++ b/build.zig
@@ -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();
+}