river: Allow applying CSD based on window titles

This extends the `csd-filter-add` command to allow matching on window
titles as well, using a `csd-filter-add kind pattern` syntax. The
following kinds are supported:

  * `title`, which matches window titles
  * `app-id`, which matches app ids

Only exact matches are considered.

As an example following configuration applies client-side decorations to
all windows with the title 'asdf with spaces'.

    riverctl csd-filter-add title 'asdf with spaces'
This commit is contained in:
Ben Fiedler 2021-09-06 15:28:05 +02:00 committed by Isaac Freund
parent 98aed8d47e
commit 5f6428bafe
6 changed files with 79 additions and 33 deletions

View file

@ -28,15 +28,15 @@ over the Wayland protocol.
*close*
Close the focused view.
*csd-filter-add* _app-id_
Add _app-id_ to the CSD filter list. Views with this _app-id_ are
told to use client side decoration instead of the default server
side decoration. Note that this affects both new views, as well as already
existing ones.
*csd-filter-add* *app-id*|*title* _pattern_
Add _pattern_ to the CSD filter list. Views with this _pattern_ are told to
use client side decoration instead of the default server side decoration.
Note that this affects new views as well as already existing ones. Title
updates are not taken into account.
*csd-filter-remove* _app-id_
Remove an _app-id_ from the CSD filter list. Note that this affects both new
views, as well as already existing ones.
*csd-filter-remove* *app-id*|*title* _pattern_
Remove _pattern_ from the CSD filter list. Note that this affects new views
as well as already existing ones.
*exit*
Exit the compositor, terminating the Wayland session.

View file

@ -152,8 +152,8 @@ riverctl set-repeat 50 300
riverctl float-filter-add app-id float
riverctl float-filter-add title "popup title with spaces"
# Set app-ids of views which should use client side decorations
riverctl csd-filter-add "gedit"
# Set app-ids and titles of views which should use client side decorations
riverctl csd-filter-add app-id "gedit"
# Set and exec into the default layout generator, rivertile.
# River will send the process group of the init executable SIGTERM on exit.

View file

@ -62,8 +62,9 @@ modes: std.ArrayList(Mode),
float_filter_app_ids: std.StringHashMapUnmanaged(void) = .{},
float_filter_titles: std.StringHashMapUnmanaged(void) = .{},
/// Set of app_ids which are allowed to use client side decorations
csd_filter: std.StringHashMapUnmanaged(void) = .{},
/// Sets of app_ids and titles which are allowed to use client side decorations
csd_filter_app_ids: std.StringHashMapUnmanaged(void) = .{},
csd_filter_titles: std.StringHashMapUnmanaged(void) = .{},
/// The selected focus_follows_cursor mode
focus_follows_cursor: FocusFollowsCursorMode = .disabled,
@ -133,9 +134,15 @@ pub fn deinit(self: *Self) void {
}
{
var it = self.csd_filter.keyIterator();
var it = self.csd_filter_app_ids.keyIterator();
while (it.next()) |key| util.gpa.free(key.*);
self.csd_filter.deinit(util.gpa);
self.csd_filter_app_ids.deinit(util.gpa);
}
{
var it = self.csd_filter_titles.keyIterator();
while (it.next()) |key| util.gpa.free(key.*);
self.csd_filter_titles.deinit(util.gpa);
}
util.gpa.free(self.default_layout_namespace);
@ -156,3 +163,19 @@ pub fn shouldFloat(self: Self, view: *View) bool {
return false;
}
pub fn csdAllowed(self: Self, view: *View) bool {
if (view.getAppId()) |app_id| {
if (self.csd_filter_app_ids.contains(std.mem.span(app_id))) {
return true;
}
}
if (view.getTitle()) |title| {
if (self.csd_filter_titles.contains(std.mem.span(title))) {
return true;
}
}
return false;
}

View file

@ -26,6 +26,7 @@ const server = &@import("main.zig").server;
const util = @import("util.zig");
const Server = @import("Server.zig");
const View = @import("View.zig");
xdg_toplevel_decoration: *wlr.XdgToplevelDecorationV1,
@ -62,8 +63,8 @@ fn handleRequestMode(
) void {
const self = @fieldParentPtr(Self, "request_mode", listener);
const toplevel = self.xdg_toplevel_decoration.surface.role_data.toplevel;
if (toplevel.app_id != null and server.config.csd_filter.contains(mem.span(toplevel.app_id.?))) {
const view = @intToPtr(*View, self.xdg_toplevel_decoration.surface.data);
if (server.config.csdAllowed(view)) {
_ = self.xdg_toplevel_decoration.setMode(.client_side);
} else {
_ = self.xdg_toplevel_decoration.setMode(.server_side);

View file

@ -218,9 +218,9 @@ fn handleMap(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurfa
view.pending.box = view.float_box;
}
// If the toplevel has an app_id which is not configured to use client side
// decorations, inform it that it is tiled.
if (toplevel.app_id != null and server.config.csd_filter.contains(mem.span(toplevel.app_id.?))) {
// If the view has an app_id or title which is not configured to use client
// side decorations, inform it that it is tiled.
if (server.config.csdAllowed(view)) {
view.draw_borders = false;
} else {
_ = toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });

View file

@ -79,15 +79,22 @@ pub fn csdFilterAdd(
args: []const [:0]const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
if (args.len < 3) return Error.NotEnoughArguments;
if (args.len > 3) return Error.TooManyArguments;
const gop = try server.config.csd_filter.getOrPut(util.gpa, args[1]);
const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption;
const map = switch (kind) {
.@"app-id" => &server.config.csd_filter_app_ids,
.title => &server.config.csd_filter_titles,
};
const key = args[2];
const gop = try map.getOrPut(util.gpa, key);
if (gop.found_existing) return;
errdefer assert(server.config.csd_filter.remove(args[1]));
gop.key_ptr.* = try std.mem.dupe(util.gpa, u8, args[1]);
errdefer assert(map.remove(key));
gop.key_ptr.* = try std.mem.dupe(util.gpa, u8, key);
csdFilterUpdateViews(args[1], .add);
csdFilterUpdateViews(kind, key, .add);
}
pub fn csdFilterRemove(
@ -96,23 +103,29 @@ pub fn csdFilterRemove(
args: []const [:0]const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
if (args.len < 3) return Error.NotEnoughArguments;
if (args.len > 3) return Error.TooManyArguments;
if (server.config.csd_filter.fetchRemove(args[1])) |kv| {
const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption;
const map = switch (kind) {
.@"app-id" => &server.config.csd_filter_app_ids,
.title => &server.config.csd_filter_titles,
};
const key = args[2];
if (map.fetchRemove(key)) |kv| {
util.gpa.free(kv.key);
csdFilterUpdateViews(args[1], .remove);
csdFilterUpdateViews(kind, key, .remove);
}
}
fn csdFilterUpdateViews(app_id: []const u8, operation: enum { add, remove }) void {
fn csdFilterUpdateViews(kind: FilterKind, pattern: []const u8, operation: enum { add, remove }) void {
var decoration_it = server.decoration_manager.decorations.first;
while (decoration_it) |decoration_node| : (decoration_it = decoration_node.next) {
const xdg_toplevel_decoration = decoration_node.data.xdg_toplevel_decoration;
const view = @intToPtr(*View, xdg_toplevel_decoration.surface.data);
const view_app_id = mem.span(view.getAppId()) orelse continue;
if (mem.eql(u8, app_id, view_app_id)) {
const view = @intToPtr(*View, xdg_toplevel_decoration.surface.data);
if (viewMatchesPattern(kind, pattern, view)) {
const toplevel = view.impl.xdg_toplevel.xdg_surface.role_data.toplevel;
switch (operation) {
.add => {
@ -129,3 +142,12 @@ fn csdFilterUpdateViews(app_id: []const u8, operation: enum { add, remove }) voi
}
}
}
fn viewMatchesPattern(kind: FilterKind, pattern: []const u8, view: *View) bool {
const p = switch (kind) {
.@"app-id" => mem.span(view.getAppId()),
.title => mem.span(view.getTitle()),
} orelse return false;
return mem.eql(u8, pattern, p);
}