aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--completions/bash/riverctl4
-rw-r--r--completions/fish/riverctl.fish4
-rw-r--r--completions/zsh/_riverctl4
-rw-r--r--doc/riverctl.1.scd19
-rw-r--r--river/Config.zig16
-rw-r--r--river/View.zig16
-rw-r--r--river/command/rule.zig53
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();