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:
		
				
					committed by
					
						 Isaac Freund
						Isaac Freund
					
				
			
			
				
	
			
			
			
						parent
						
							98aed8d47e
						
					
				
				
					commit
					5f6428bafe
				
			| @ -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. | ||||||
|  | |||||||
| @ -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. | ||||||
|  | |||||||
| @ -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; | ||||||
|  | } | ||||||
|  | |||||||
| @ -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); | ||||||
|  | |||||||
| @ -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 }); | ||||||
|  | |||||||
| @ -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); | ||||||
|  | } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user