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.
This commit is contained in:
		
							
								
								
									
										10
									
								
								build.zig
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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 { | ||||
|  | ||||
							
								
								
									
										223
									
								
								common/globber.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								common/globber.zig
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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" ;; | ||||
|  | ||||
| @ -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)" | ||||
|  | ||||
| @ -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 | ||||
|         ;; | ||||
|  | ||||
| @ -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* | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
							
								
								
									
										106
									
								
								river/RuleList.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								river/RuleList.zig
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| } | ||||
| @ -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 | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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'", | ||||
|  | ||||
| @ -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); | ||||
| } | ||||
							
								
								
									
										164
									
								
								river/command/rule.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								river/command/rule.zig
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user