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:
parent
05eac54b07
commit
b2b2c9ed13
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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user