diff options
| -rw-r--r-- | completions/bash/riverctl | 4 | ||||
| -rw-r--r-- | completions/fish/riverctl.fish | 4 | ||||
| -rw-r--r-- | completions/zsh/_riverctl | 4 | ||||
| -rw-r--r-- | doc/riverctl.1.scd | 19 | ||||
| -rw-r--r-- | river/Config.zig | 16 | ||||
| -rw-r--r-- | river/View.zig | 16 | ||||
| -rw-r--r-- | river/command/rule.zig | 53 |
7 files changed, 104 insertions, 12 deletions
diff --git a/completions/bash/riverctl b/completions/bash/riverctl index 4b19d4c..c92e001 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -1,6 +1,6 @@ function __riverctl_completion () { - local rule_actions="float no-float ssd csd tag output" + local rule_actions="float no-float ssd csd tag output position dimensions" if [ "${COMP_CWORD}" -eq 1 ] then OPTS=" \ @@ -66,7 +66,7 @@ function __riverctl_completion () "move"|"snap") OPTS="up down left right" ;; "resize") OPTS="horizontal vertical" ;; "rule-add"|"rule-del") OPTS="-app-id -title $rule_actions" ;; - "list-rules") OPTS="float ssd tag output" ;; + "list-rules") OPTS="float ssd tag output position dimensions" ;; "map") OPTS="-release -repeat -layout" ;; "unmap") OPTS="-release" ;; "attach-mode") OPTS="top bottom" ;; diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index c69b232..5f596b9 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -88,10 +88,10 @@ complete -c riverctl -n '__fish_seen_subcommand_from unmap' complete -c riverctl -n '__fish_seen_subcommand_from attach-mode' -n '__fish_riverctl_complete_arg 2' -a 'top bottom' 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 tag output' +complete -c riverctl -n '__fish_seen_subcommand_from list-rules' -n '__fish_riverctl_complete_arg 2' -a 'float ssd tag output position dimensions' # Options and subcommands for 'rule-add' and 'rule-del' -set -l rule_actions float no-float ssd csd tag output +set -l rule_actions float no-float ssd csd tag output position dimensions 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 d2cf8ee..2a50abd 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -183,9 +183,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 <new-option>) - _arguments '1: :(-app-id -title)' '2: : ' ':: :(float no-float ssd csd tag output)' + _arguments '1: :(-app-id -title)' '2: : ' ':: :(float no-float ssd csd tag output position dimensions)' ;; - list-rules) _alternative 'arguments:args:(float ssd tag output)' ;; + list-rules) _alternative 'arguments:args:(float ssd tag output position dimensions)' ;; *) return 0 ;; esac ;; diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index a418322..803f56c 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -274,11 +274,11 @@ 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* [*-app-id* _glob_|*-title* _glob_] _action_ [_argument_] +*rule-add* [*-app-id* _glob_|*-title* _glob_] _action_ [_arguments_] 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* _\*_. - Some actions require an _argument_. + Some actions require one or more _arguments_. The supported _action_ types are: @@ -298,6 +298,15 @@ matches everything while _\*\*_ and the empty string are invalid. with make: _HP Inc._, model: _HP 22w_, and serial: _CNC93720WF_, the identifier would be: _HP Inc. HP 22w CNC93720WF_. If the make, model, or serial is unknown, the word "Unknown" is used instead. + - *position*: Set the initial position of the view, clamping to the + bounds of the output. Requires x and y coordinates of the view as + arguments, both of which must be non-negative. Applies only to new views. + - *dimensions*: Set the initial dimensions of the view, clamping to the + constraints of the view. Requires width and height of the view as + arguments, both of which must be non-negative. Applies only to new views. + - *fullscreen*: Make the view fullscreen. Applies only to new views. + - *no-fullscreen*: Don't make the view fullscreen. Applies only to + new 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 @@ -323,10 +332,14 @@ matches everything while _\*\*_ and the empty string are invalid. wishes of the client and may start the view floating based on simple heuristics intended to catch popup-like views. + If a view is started fullscreen or is not floating, then *position* and + *dimensions* rules will have no effect A view must be matched by a *float* + rule in order for them to take effect. + *rule-del* [*-app-id* _glob_|*-title* _glob_] _action_ Delete a rule created using *rule-add* with the given arguments. -*list-rules* *float*|*ssd*|*tag* +*list-rules* *float*|*ssd*|*tag*|*position*|*dimensions* 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 2a3fd90..a765d54 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -55,6 +55,16 @@ pub const HideCursorWhenTypingMode = enum { enabled, }; +pub const Position = struct { + x: u31, + y: u31, +}; + +pub const Dimensions = struct { + width: u31, + height: u31, +}; + /// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions) background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03 @@ -81,6 +91,9 @@ float_rules: RuleList(bool) = .{}, ssd_rules: RuleList(bool) = .{}, tag_rules: RuleList(u32) = .{}, output_rules: RuleList([]const u8) = .{}, +position_rules: RuleList(Position) = .{}, +dimensions_rules: RuleList(Dimensions) = .{}, +fullscreen_rules: RuleList(bool) = .{}, /// The selected focus_follows_cursor mode focus_follows_cursor: FocusFollowsCursorMode = .disabled, @@ -161,6 +174,9 @@ pub fn deinit(self: *Self) void { util.gpa.free(rule.value); } self.output_rules.deinit(); + self.position_rules.deinit(); + self.dimensions_rules.deinit(); + self.fullscreen_rules.deinit(); util.gpa.free(self.default_layout_namespace); diff --git a/river/View.zig b/river/View.zig index 25f44ea..36d1e12 100644 --- a/river/View.zig +++ b/river/View.zig @@ -494,9 +494,19 @@ pub fn map(view: *Self) !void { const focused_output = server.input_manager.defaultSeat().focused_output; if (try server.config.outputRuleMatch(view) orelse focused_output) |output| { - // Center the initial pending box on the output - view.pending.box.x = @divTrunc(@max(0, output.usable_box.width - view.pending.box.width), 2); - view.pending.box.y = @divTrunc(@max(0, output.usable_box.height - view.pending.box.height), 2); + if (server.config.position_rules.match(view)) |position| { + view.pending.box.x = position.x; + view.pending.box.y = position.y; + } else { + // Center the initial pending box on the output + view.pending.box.x = @divTrunc(@max(0, output.usable_box.width - view.pending.box.width), 2); + view.pending.box.y = @divTrunc(@max(0, output.usable_box.height - view.pending.box.height), 2); + } + + if (server.config.dimensions_rules.match(view)) |dimensions| { + view.pending.box.width = dimensions.width; + view.pending.box.height = dimensions.height; + } view.pending.tags = blk: { if (server.config.tag_rules.match(view)) |tags| break :blk tags; diff --git a/river/command/rule.zig b/river/command/rule.zig index 63d2bb9..1ccc1ce 100644 --- a/river/command/rule.zig +++ b/river/command/rule.zig @@ -34,6 +34,8 @@ const Action = enum { csd, tag, output, + position, + dimensions, }; pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void { @@ -51,6 +53,7 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void const positional_arguments_count: u8 = switch (action) { .float, .@"no-float", .ssd, .csd => 1, .tag, .output => 2, + .position, .dimensions => 3, }; if (result.args.len > positional_arguments_count) return Error.TooManyArguments; if (result.args.len < positional_arguments_count) return Error.NotEnoughArguments; @@ -95,6 +98,30 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void .value = output_name, }); }, + .position => { + const x = try fmt.parseInt(u31, result.args[1], 10); + const y = try fmt.parseInt(u31, result.args[2], 10); + try server.config.position_rules.add(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = .{ + .x = x, + .y = y, + }, + }); + }, + .dimensions => { + const width = try fmt.parseInt(u31, result.args[1], 10); + const height = try fmt.parseInt(u31, result.args[2], 10); + try server.config.dimensions_rules.add(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = .{ + .width = width, + .height = height, + }, + }); + }, } } @@ -132,6 +159,12 @@ pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void util.gpa.free(output_rule); } }, + .position => { + _ = server.config.position_rules.del(rule); + }, + .dimensions => { + _ = server.config.dimensions_rules.del(rule); + }, } } @@ -153,12 +186,16 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! ssd, tag, output, + position, + dimensions, }, args[1]) orelse return Error.UnknownOption; const max_glob_len = switch (list) { .float => server.config.float_rules.getMaxGlobLen(), .ssd => server.config.ssd_rules.getMaxGlobLen(), .tag => server.config.tag_rules.getMaxGlobLen(), .output => server.config.output_rules.getMaxGlobLen(), + .position => server.config.position_rules.getMaxGlobLen(), + .dimensions => server.config.dimensions_rules.getMaxGlobLen(), }; const app_id_column_max = 2 + @max("app-id".len, max_glob_len.app_id); const title_column_max = 2 + @max("title".len, max_glob_len.title); @@ -203,6 +240,22 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! try writer.print("{s}\n", .{rule.value}); } }, + .position => { + const rules = server.config.position_rules.rules.items; + for (rules) |rule| { + try fmt.formatBuf(rule.title_glob, .{ .width = title_column_max, .alignment = .left }, writer); + try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .left }, writer); + try writer.print("{d},{d}\n", .{ rule.value.x, rule.value.y }); + } + }, + .dimensions => { + const rules = server.config.dimensions_rules.rules.items; + for (rules) |rule| { + try fmt.formatBuf(rule.title_glob, .{ .width = title_column_max, .alignment = .left }, writer); + try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .left }, writer); + try writer.print("{d}x{d}\n", .{ rule.value.width, rule.value.height }); + } + }, } out.* = try buffer.toOwnedSlice(); |
