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*
Close the focused view. Close the focused view.
*csd-filter-add* _app-id_ *csd-filter-add* *app-id*|*title* _pattern_
Add _app-id_ to the CSD filter list. Views with this _app-id_ are Add _pattern_ to the CSD filter list. Views with this _pattern_ are told to
told to use client side decoration instead of the default server use client side decoration instead of the default server side decoration.
side decoration. Note that this affects both new views, as well as already Note that this affects new views as well as already existing ones. Title
existing ones. updates are not taken into account.
*csd-filter-remove* _app-id_ *csd-filter-remove* *app-id*|*title* _pattern_
Remove an _app-id_ from the CSD filter list. Note that this affects both new Remove _pattern_ from the CSD filter list. Note that this affects new views
views, as well as already existing ones. as well as already existing ones.
*exit* *exit*
Exit the compositor, terminating the Wayland session. 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 app-id float
riverctl float-filter-add title "popup title with spaces" riverctl float-filter-add title "popup title with spaces"
# Set app-ids of views which should use client side decorations # Set app-ids and titles of views which should use client side decorations
riverctl csd-filter-add "gedit" riverctl csd-filter-add app-id "gedit"
# Set and exec into the default layout generator, rivertile. # Set and exec into the default layout generator, rivertile.
# River will send the process group of the init executable SIGTERM on exit. # 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_app_ids: std.StringHashMapUnmanaged(void) = .{},
float_filter_titles: std.StringHashMapUnmanaged(void) = .{}, float_filter_titles: std.StringHashMapUnmanaged(void) = .{},
/// Set of app_ids which are allowed to use client side decorations /// Sets of app_ids and titles which are allowed to use client side decorations
csd_filter: std.StringHashMapUnmanaged(void) = .{}, csd_filter_app_ids: std.StringHashMapUnmanaged(void) = .{},
csd_filter_titles: std.StringHashMapUnmanaged(void) = .{},
/// The selected focus_follows_cursor mode /// The selected focus_follows_cursor mode
focus_follows_cursor: FocusFollowsCursorMode = .disabled, 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.*); 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); util.gpa.free(self.default_layout_namespace);
@ -156,3 +163,19 @@ pub fn shouldFloat(self: Self, view: *View) bool {
return false; 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 util = @import("util.zig");
const Server = @import("Server.zig"); const Server = @import("Server.zig");
const View = @import("View.zig");
xdg_toplevel_decoration: *wlr.XdgToplevelDecorationV1, xdg_toplevel_decoration: *wlr.XdgToplevelDecorationV1,
@ -62,8 +63,8 @@ fn handleRequestMode(
) void { ) void {
const self = @fieldParentPtr(Self, "request_mode", listener); const self = @fieldParentPtr(Self, "request_mode", listener);
const toplevel = self.xdg_toplevel_decoration.surface.role_data.toplevel; const view = @intToPtr(*View, self.xdg_toplevel_decoration.surface.data);
if (toplevel.app_id != null and server.config.csd_filter.contains(mem.span(toplevel.app_id.?))) { if (server.config.csdAllowed(view)) {
_ = self.xdg_toplevel_decoration.setMode(.client_side); _ = self.xdg_toplevel_decoration.setMode(.client_side);
} else { } else {
_ = self.xdg_toplevel_decoration.setMode(.server_side); _ = 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; view.pending.box = view.float_box;
} }
// If the toplevel has an app_id which is not configured to use client side // If the view has an app_id or title which is not configured to use client
// decorations, inform it that it is tiled. // 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 (server.config.csdAllowed(view)) {
view.draw_borders = false; view.draw_borders = false;
} else { } else {
_ = toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); _ = toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });

View file

@ -79,15 +79,22 @@ pub fn csdFilterAdd(
args: []const [:0]const u8, args: []const [:0]const u8,
out: *?[]const u8, out: *?[]const u8,
) Error!void { ) Error!void {
if (args.len < 2) return Error.NotEnoughArguments; if (args.len < 3) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments; 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; if (gop.found_existing) return;
errdefer assert(server.config.csd_filter.remove(args[1])); errdefer assert(map.remove(key));
gop.key_ptr.* = try std.mem.dupe(util.gpa, u8, args[1]); gop.key_ptr.* = try std.mem.dupe(util.gpa, u8, key);
csdFilterUpdateViews(args[1], .add); csdFilterUpdateViews(kind, key, .add);
} }
pub fn csdFilterRemove( pub fn csdFilterRemove(
@ -96,23 +103,29 @@ pub fn csdFilterRemove(
args: []const [:0]const u8, args: []const [:0]const u8,
out: *?[]const u8, out: *?[]const u8,
) Error!void { ) Error!void {
if (args.len < 2) return Error.NotEnoughArguments; if (args.len < 3) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments; 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); 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; var decoration_it = server.decoration_manager.decorations.first;
while (decoration_it) |decoration_node| : (decoration_it = decoration_node.next) { while (decoration_it) |decoration_node| : (decoration_it = decoration_node.next) {
const xdg_toplevel_decoration = decoration_node.data.xdg_toplevel_decoration; 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; const toplevel = view.impl.xdg_toplevel.xdg_surface.role_data.toplevel;
switch (operation) { switch (operation) {
.add => { .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);
}