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:
parent
18a440b606
commit
a0ea456ab2
@ -1,6 +1,6 @@
|
|||||||
function __riverctl_completion ()
|
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 ]
|
if [ "${COMP_CWORD}" -eq 1 ]
|
||||||
then
|
then
|
||||||
OPTS=" \
|
OPTS=" \
|
||||||
@ -66,7 +66,7 @@ function __riverctl_completion ()
|
|||||||
"move"|"snap") OPTS="up down left right" ;;
|
"move"|"snap") OPTS="up down left right" ;;
|
||||||
"resize") OPTS="horizontal vertical" ;;
|
"resize") OPTS="horizontal vertical" ;;
|
||||||
"rule-add"|"rule-del") OPTS="-app-id -title $rule_actions" ;;
|
"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" ;;
|
"map") OPTS="-release -repeat -layout" ;;
|
||||||
"unmap") OPTS="-release" ;;
|
"unmap") OPTS="-release" ;;
|
||||||
"attach-mode") OPTS="top bottom" ;;
|
"attach-mode") OPTS="top bottom" ;;
|
||||||
|
@ -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 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 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 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'
|
# 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 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 '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"
|
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"
|
||||||
|
@ -183,9 +183,9 @@ _riverctl()
|
|||||||
# In case of a new rule added in river, we just need
|
# In case of a new rule added in river, we just need
|
||||||
# to add it to the third option between '()',
|
# to add it to the third option between '()',
|
||||||
# i.e (float no-float <new-option>)
|
# 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 ;;
|
*) return 0 ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
@ -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 _\*_
|
_\*_ but not matched by _\*a_, _b\*_, _\*b_, _c\*_, or _ab_. Note that _\*_
|
||||||
matches everything while _\*\*_ and the empty string are invalid.
|
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*
|
Add a rule that applies an _action_ to views with *app-id* and *title*
|
||||||
matched by the respective _glob_. Omitting *-app-id* or *-title*
|
matched by the respective _glob_. Omitting *-app-id* or *-title*
|
||||||
is equivalent to passing *-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:
|
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
|
with make: _HP Inc._, model: _HP 22w_, and serial: _CNC93720WF_, the
|
||||||
identifier would be: _HP Inc. HP 22w CNC93720WF_. If the make, model, or
|
identifier would be: _HP Inc. HP 22w CNC93720WF_. If the make, model, or
|
||||||
serial is unknown, the word "Unknown" is used instead.
|
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,
|
Both *float* and *no-float* rules are added to the same list,
|
||||||
which means that adding a *no-float* rule with the same arguments
|
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
|
wishes of the client and may start the view floating based on simple
|
||||||
heuristics intended to catch popup-like views.
|
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_
|
*rule-del* [*-app-id* _glob_|*-title* _glob_] _action_
|
||||||
Delete a rule created using *rule-add* with the given arguments.
|
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
|
Print the specified rule list. The output is ordered from most specific
|
||||||
to least specific, the same order in which views are checked against
|
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
|
when searching for a match. Only the first matching rule in the list
|
||||||
|
@ -55,6 +55,16 @@ pub const HideCursorWhenTypingMode = enum {
|
|||||||
enabled,
|
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)
|
/// 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
|
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) = .{},
|
ssd_rules: RuleList(bool) = .{},
|
||||||
tag_rules: RuleList(u32) = .{},
|
tag_rules: RuleList(u32) = .{},
|
||||||
output_rules: RuleList([]const u8) = .{},
|
output_rules: RuleList([]const u8) = .{},
|
||||||
|
position_rules: RuleList(Position) = .{},
|
||||||
|
dimensions_rules: RuleList(Dimensions) = .{},
|
||||||
|
fullscreen_rules: RuleList(bool) = .{},
|
||||||
|
|
||||||
/// The selected focus_follows_cursor mode
|
/// The selected focus_follows_cursor mode
|
||||||
focus_follows_cursor: FocusFollowsCursorMode = .disabled,
|
focus_follows_cursor: FocusFollowsCursorMode = .disabled,
|
||||||
@ -161,6 +174,9 @@ pub fn deinit(self: *Self) void {
|
|||||||
util.gpa.free(rule.value);
|
util.gpa.free(rule.value);
|
||||||
}
|
}
|
||||||
self.output_rules.deinit();
|
self.output_rules.deinit();
|
||||||
|
self.position_rules.deinit();
|
||||||
|
self.dimensions_rules.deinit();
|
||||||
|
self.fullscreen_rules.deinit();
|
||||||
|
|
||||||
util.gpa.free(self.default_layout_namespace);
|
util.gpa.free(self.default_layout_namespace);
|
||||||
|
|
||||||
|
@ -494,9 +494,19 @@ pub fn map(view: *Self) !void {
|
|||||||
|
|
||||||
const focused_output = server.input_manager.defaultSeat().focused_output;
|
const focused_output = server.input_manager.defaultSeat().focused_output;
|
||||||
if (try server.config.outputRuleMatch(view) orelse focused_output) |output| {
|
if (try server.config.outputRuleMatch(view) orelse focused_output) |output| {
|
||||||
// Center the initial pending box on the output
|
if (server.config.position_rules.match(view)) |position| {
|
||||||
view.pending.box.x = @divTrunc(@max(0, output.usable_box.width - view.pending.box.width), 2);
|
view.pending.box.x = position.x;
|
||||||
view.pending.box.y = @divTrunc(@max(0, output.usable_box.height - view.pending.box.height), 2);
|
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: {
|
view.pending.tags = blk: {
|
||||||
if (server.config.tag_rules.match(view)) |tags| break :blk tags;
|
if (server.config.tag_rules.match(view)) |tags| break :blk tags;
|
||||||
|
@ -34,6 +34,8 @@ const Action = enum {
|
|||||||
csd,
|
csd,
|
||||||
tag,
|
tag,
|
||||||
output,
|
output,
|
||||||
|
position,
|
||||||
|
dimensions,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
|
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) {
|
const positional_arguments_count: u8 = switch (action) {
|
||||||
.float, .@"no-float", .ssd, .csd => 1,
|
.float, .@"no-float", .ssd, .csd => 1,
|
||||||
.tag, .output => 2,
|
.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.TooManyArguments;
|
||||||
if (result.args.len < positional_arguments_count) return Error.NotEnoughArguments;
|
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,
|
.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);
|
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,
|
ssd,
|
||||||
tag,
|
tag,
|
||||||
output,
|
output,
|
||||||
|
position,
|
||||||
|
dimensions,
|
||||||
}, args[1]) orelse return Error.UnknownOption;
|
}, args[1]) orelse return Error.UnknownOption;
|
||||||
const max_glob_len = switch (list) {
|
const max_glob_len = switch (list) {
|
||||||
.float => server.config.float_rules.getMaxGlobLen(),
|
.float => server.config.float_rules.getMaxGlobLen(),
|
||||||
.ssd => server.config.ssd_rules.getMaxGlobLen(),
|
.ssd => server.config.ssd_rules.getMaxGlobLen(),
|
||||||
.tag => server.config.tag_rules.getMaxGlobLen(),
|
.tag => server.config.tag_rules.getMaxGlobLen(),
|
||||||
.output => server.config.output_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 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);
|
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});
|
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();
|
out.* = try buffer.toOwnedSlice();
|
||||||
|
Loading…
Reference in New Issue
Block a user