river: add position and dimensions rules

This commit adds position and dimensions rules for configuring
the initial position and dimensions of views.

When a view is not matched by any position rules, it is centered
in the avaliable output space matching the current behavior. If
the provided position rule places the view outside of the output,
the view's position is clamped to the output bounds (with respect
to borders).

When a view is not matched by any dimensions rules, no default
dimensions is set by the server. If the provided dimensions rule
exceeds the minimum or maximum width/height constraints of the view,
the view's width/height is clamped to the constraints.

Position and dimensions rules have no effect if a view is started
fullscreen or is not floating. A view must be matched by a float
rule in order for them to take effect.
This commit is contained in:
polykernel 2023-08-29 17:54:13 -04:00
parent 18a440b606
commit a0ea456ab2
No known key found for this signature in database
7 changed files with 104 additions and 12 deletions

View File

@ -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" ;;

View File

@ -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"

View File

@ -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
;;

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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();