diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 68cbbb6..dcb2148 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -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. diff --git a/example/init b/example/init index d3b9dc3..7ddd8eb 100755 --- a/example/init +++ b/example/init @@ -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. diff --git a/river/Config.zig b/river/Config.zig index 113264c..56d2bbe 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -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; +} diff --git a/river/Decoration.zig b/river/Decoration.zig index 4159bc6..45263ac 100644 --- a/river/Decoration.zig +++ b/river/Decoration.zig @@ -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); diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index b837c0d..1606e6a 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -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 }); diff --git a/river/command/filter.zig b/river/command/filter.zig index 6d6ac35..298b56d 100644 --- a/river/command/filter.zig +++ b/river/command/filter.zig @@ -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); +}