diff --git a/common/globber.zig b/common/globber.zig index c92e99c..35b6942 100644 --- a/common/globber.zig +++ b/common/globber.zig @@ -18,12 +18,11 @@ 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. +/// Validate a glob, returning error.InvalidGlob if is "**" 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 => {}, + 0, 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; @@ -34,6 +33,7 @@ pub fn validate(glob: []const u8) error{InvalidGlob}!void { test validate { const testing = std.testing; + try validate(""); try validate("*"); try validate("a"); try validate("*a"); @@ -48,7 +48,6 @@ test validate { 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")); @@ -67,7 +66,9 @@ pub fn match(s: []const u8, glob: []const u8) bool { validate(glob) catch unreachable; } - if (glob.len == 1) { + if (glob.len == 0) { + return s.len == 0; + } else if (glob.len == 1) { return glob[0] == '*' or mem.eql(u8, s, glob); } @@ -89,6 +90,9 @@ test match { const testing = std.testing; try testing.expect(match("", "*")); + try testing.expect(match("", "")); + try testing.expect(!match("a", "")); + try testing.expect(!match("", "a")); try testing.expect(match("a", "*")); try testing.expect(match("a", "*a*")); @@ -165,8 +169,10 @@ pub fn order(a: []const u8, b: []const u8) std.math.Order { return .lt; } - const count_a = @as(u2, @intFromBool(a[0] == '*')) + @intFromBool(a[a.len - 1] == '*'); - const count_b = @as(u2, @intFromBool(b[0] == '*')) + @intFromBool(b[b.len - 1] == '*'); + const count_a = if (a.len != 0) @as(u2, @intFromBool(a[0] == '*')) + + @intFromBool(a[a.len - 1] == '*') else 0; + const count_b = if (b.len != 0) @as(u2, @intFromBool(b[0] == '*')) + + @intFromBool(b[b.len - 1] == '*') else 0; if (count_a == 0 and count_b == 0) { return .eq; @@ -182,6 +188,7 @@ test order { const testing = std.testing; const Order = std.math.Order; + try testing.expectEqual(Order.eq, order("", "")); try testing.expectEqual(Order.eq, order("*", "*")); try testing.expectEqual(Order.eq, order("*a*", "*b*")); try testing.expectEqual(Order.eq, order("a*", "*b")); @@ -204,6 +211,7 @@ test order { "bababab", "b", "a", + "", }; for (descending, 0..) |a, i| { diff --git a/completions/bash/riverctl b/completions/bash/riverctl index 35e46df..c122f0f 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -1,6 +1,6 @@ function __riverctl_completion () { - local rule_actions="float no-float ssd csd tags output position relative-position dimensions fullscreen no-fullscreen" + local rule_actions="float no-float ssd csd tags output position relative-position dimensions fullscreen no-fullscreen warp no-warp" if [ "${COMP_CWORD}" -eq 1 ] then OPTS=" \ diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index d443f92..1eac46e 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -91,10 +91,10 @@ complete -c riverctl -n '__fish_seen_subcommand_from default-attach-mode' complete -c riverctl -n '__fish_seen_subcommand_from output-attach-mode' -n '__fish_riverctl_complete_arg 2' -a 'top bottom above below after' complete -c riverctl -n '__fish_seen_subcommand_from focus-follows-cursor' -n '__fish_riverctl_complete_arg 2' -a 'disabled normal always' complete -c riverctl -n '__fish_seen_subcommand_from set-cursor-warp' -n '__fish_riverctl_complete_arg 2' -a 'disabled on-output-change on-focus-change' -complete -c riverctl -n '__fish_seen_subcommand_from list-rules' -n '__fish_riverctl_complete_arg 2' -a 'float ssd tags output position dimensions fullscreen' +complete -c riverctl -n '__fish_seen_subcommand_from list-rules' -n '__fish_riverctl_complete_arg 2' -a 'float ssd tags output position dimensions fullscreen warp' # Options and subcommands for 'rule-add' and 'rule-del' -set -l rule_actions float no-float ssd csd tags output position relative-position dimensions fullscreen no-fullscreen +set -l rule_actions float no-float ssd csd tags output position relative-position dimensions fullscreen no-fullscreen warp no-warp complete -c riverctl -n '__fish_seen_subcommand_from rule-add rule-del' -n "not __fish_seen_subcommand_from $rule_actions" -n 'not __fish_seen_argument -o app-id' -o 'app-id' -r complete -c riverctl -n '__fish_seen_subcommand_from rule-add rule-del' -n "not __fish_seen_subcommand_from $rule_actions" -n 'not __fish_seen_argument -o title' -o 'title' -r complete -c riverctl -n '__fish_seen_subcommand_from rule-add rule-del' -n "not __fish_seen_subcommand_from $rule_actions" -n 'test (math (count (commandline -opc)) % 2) -eq 0' -a "$rule_actions" diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl index e597dd3..4892dbe 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -207,9 +207,9 @@ _riverctl() # In case of a new rule added in river, we just need # to add it to the third option between '()', # i.e (float no-float ) - _arguments '1: :(-app-id -title)' '2: : ' ':: :(float no-float ssd csd tags output position relative-position dimensions fullscreen no-fullscreen)' + _arguments '1: :(-app-id -title)' '2: : ' ':: :(float no-float ssd csd tags output position relative-position dimensions fullscreen no-fullscreen warp no-warp)' ;; - list-rules) _alternative 'arguments:args:(float ssd tags output position dimensions fullscreen)' ;; + list-rules) _alternative 'arguments:args:(float ssd tags output position dimensions fullscreen warp)' ;; *) return 0 ;; esac ;; diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 6112dcb..8d1fff5 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -315,12 +315,16 @@ matches everything while _\*\*_ and the empty string are invalid. view's preference. Applies to new and existing views. - *no-tearing*: Disable tearing for the view regardless of the view's preference. Applies to new and existing views. + - *warp*: Always warp the cursor when switching to this view, regardless of + the _set-cursor-warp_ setting. Applies to new and existing views. + - *no-warp*: Never warp the cursor when switching to this view, regardless + of the _set-cursor-warp_ setting. 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*, *fullscreen* and *no-fullscreen*, *tearing* and - *no-tearing* rules. + *no-tearing*, *warp* and *no-warp* rules. If multiple rules in a list match a given view the most specific rule will be applied. For example with the following rules @@ -348,7 +352,7 @@ matches everything while _\*\*_ and the empty string are invalid. *rule-del* [*-app-id* _glob_|*-title* _glob_] _action_ Delete a rule created using *rule-add* with the given arguments. -*list-rules* *float*|*ssd*|*tags*|*position*|*dimensions*|*fullscreen* +*list-rules* *float*|*ssd*|*tags*|*position*|*dimensions*|*fullscreen*|*warp* 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 diff --git a/river/Config.zig b/river/Config.zig index dac8ae1..6d2b4ea 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -108,6 +108,7 @@ rules: struct { dimensions: RuleList(Dimensions) = .{}, fullscreen: RuleList(bool) = .{}, tearing: RuleList(bool) = .{}, + warp: RuleList(bool) = .{}, } = .{}, /// The selected focus_follows_cursor mode @@ -192,6 +193,7 @@ pub fn deinit(config: *Config) void { config.rules.position.deinit(); config.rules.dimensions.deinit(); config.rules.fullscreen.deinit(); + config.rules.warp.deinit(); util.gpa.free(config.default_layout_namespace); diff --git a/river/Cursor.zig b/river/Cursor.zig index 235f29c..ab7fb36 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -1178,10 +1178,18 @@ fn warp(cursor: *Cursor) void { const focused_output = cursor.seat.focused_output orelse return; + var mode = server.config.warp_cursor; + if (cursor.seat.focused == .view) { + const view = cursor.seat.focused.view; + if (server.config.rules.warp.match(view)) |w| { + mode = if (w) .@"on-focus-change" else .disabled; + } + } + // Warp pointer to center of the focused view/output (In layout coordinates) if enabled. var output_layout_box: wlr.Box = undefined; server.root.output_layout.getBox(focused_output.wlr_output, &output_layout_box); - const target_box = switch (server.config.warp_cursor) { + const target_box = switch (mode) { .disabled => return, .@"on-output-change" => output_layout_box, .@"on-focus-change" => switch (cursor.seat.focused) { diff --git a/river/command/rule.zig b/river/command/rule.zig index 6891618..9fbaa9f 100644 --- a/river/command/rule.zig +++ b/river/command/rule.zig @@ -42,6 +42,8 @@ const Action = enum { @"no-fullscreen", tearing, @"no-tearing", + warp, + @"no-warp", }; pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void { @@ -57,7 +59,7 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption; const positional_arguments_count: u8 = switch (action) { - .float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen", .tearing, .@"no-tearing" => 1, + .float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen", .tearing, .@"no-tearing", .warp, .@"no-warp" => 1, .tags, .output => 2, .position, .dimensions => 3, .@"relative-position" => 4, @@ -162,6 +164,13 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void .value = (action == .fullscreen), }); }, + .warp, .@"no-warp" => { + try server.config.rules.warp.add(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = (action == .warp), + }); + }, } } @@ -212,6 +221,9 @@ pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void _ = server.config.rules.tearing.del(rule); apply_tearing_rules(); }, + .warp, .@"no-warp" => { + _ = server.config.rules.warp.del(rule); + }, } } @@ -250,6 +262,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! dimensions, fullscreen, tearing, + warp, }, args[1]) orelse return Error.UnknownOption; const max_glob_len = switch (rule_list) { inline else => |list| @field(server.config.rules, @tagName(list)).getMaxGlobLen(), @@ -265,13 +278,14 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! try writer.writeAll("action\n"); switch (rule_list) { - inline .float, .ssd, .output, .fullscreen, .tearing => |list| { + inline .float, .ssd, .output, .fullscreen, .tearing, .warp => |list| { const rules = switch (list) { .float => server.config.rules.float.rules.items, .ssd => server.config.rules.ssd.rules.items, .output => server.config.rules.output.rules.items, .fullscreen => server.config.rules.fullscreen.rules.items, .tearing => server.config.rules.tearing.rules.items, + .warp => server.config.rules.warp.rules.items, else => unreachable, }; for (rules) |rule| { @@ -283,6 +297,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! .output => rule.value, .fullscreen => if (rule.value) "fullscreen" else "no-fullscreen", .tearing => if (rule.value) "tearing" else "no-tearing", + .warp => if (rule.value) "warp" else "no-warp", else => unreachable, }}); }