river: Allow floating based on window titles

This extends the `float-filter-add` command to allow matching on window
titles as well, using a `float-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 floats all windows with the title
'asdf with spaces'.

    riverctl float-filter-add title 'asdf with spaces'
This commit is contained in:
Ben Fiedler 2021-08-31 21:26:17 +02:00 committed by Isaac Freund
parent e59c2a73d7
commit 546252aecf
5 changed files with 70 additions and 37 deletions

View file

@ -41,14 +41,14 @@ over the Wayland protocol.
*exit* *exit*
Exit the compositor, terminating the Wayland session. Exit the compositor, terminating the Wayland session.
*float-filter-add* _app-id_ *float-filter-add* *app-id*|*title* _pattern_
Add _app-id_ to the float filter list. Views with this _app-id_ Add a pattern to the float filter list. Note that this affects only new
will start floating. Note that this affects only new views, not already views, not already existing ones. Title updates are also not taken into
existing ones. account.
*float-filter-remove* _app-id_ *float-filter-remove* *app-id*|*title* _pattern_
Remove an _app-id_ from the float filter list. Note that this affects only Remove an app-id or title from the float filter list. Note that this
new views, not already existing ones. affects only new views, not already existing ones.
*focus-output* *next*|*previous*|*up*|*right*|*down*|*left* *focus-output* *next*|*previous*|*up*|*right*|*down*|*left*
Focus the next or previous output or the closest output in any direction. Focus the next or previous output or the closest output in any direction.

View file

@ -24,6 +24,7 @@ const util = @import("util.zig");
const Server = @import("Server.zig"); const Server = @import("Server.zig");
const Mode = @import("Mode.zig"); const Mode = @import("Mode.zig");
const AttachMode = @import("view_stack.zig").AttachMode; const AttachMode = @import("view_stack.zig").AttachMode;
const View = @import("View.zig");
pub const FocusFollowsCursorMode = enum { pub const FocusFollowsCursorMode = enum {
disabled, disabled,
@ -57,8 +58,9 @@ mode_to_id: std.StringHashMap(usize),
/// All user-defined keymap modes, indexed by mode id /// All user-defined keymap modes, indexed by mode id
modes: std.ArrayList(Mode), modes: std.ArrayList(Mode),
/// Set of app_ids which will be started floating /// Sets of app_ids and titles which will be started floating
float_filter: std.StringHashMapUnmanaged(void) = .{}, 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 /// Set of app_ids which are allowed to use client side decorations
csd_filter: std.StringHashMapUnmanaged(void) = .{}, csd_filter: std.StringHashMapUnmanaged(void) = .{},
@ -119,9 +121,15 @@ pub fn deinit(self: *Self) void {
self.modes.deinit(); self.modes.deinit();
{ {
var it = self.float_filter.keyIterator(); var it = self.float_filter_app_ids.keyIterator();
while (it.next()) |key| util.gpa.free(key.*); while (it.next()) |key| util.gpa.free(key.*);
self.float_filter.deinit(util.gpa); self.float_filter_app_ids.deinit(util.gpa);
}
{
var it = self.float_filter_titles.keyIterator();
while (it.next()) |key| util.gpa.free(key.*);
self.float_filter_titles.deinit(util.gpa);
} }
{ {
@ -132,3 +140,19 @@ pub fn deinit(self: *Self) void {
util.gpa.free(self.default_layout_namespace); util.gpa.free(self.default_layout_namespace);
} }
pub fn shouldFloat(self: Self, view: *View) bool {
if (view.getAppId()) |app_id| {
if (self.float_filter_app_ids.contains(std.mem.span(app_id))) {
return true;
}
}
if (view.getTitle()) |title| {
if (self.float_filter_titles.contains(std.mem.span(title))) {
return true;
}
}
return false;
}

View file

@ -212,16 +212,11 @@ fn handleMap(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurfa
view.current.float = true; view.current.float = true;
view.pending.float = true; view.pending.float = true;
view.pending.box = view.float_box; view.pending.box = view.float_box;
} else { } else if (server.config.shouldFloat(view)) {
// Make views with app_ids listed in the float filter float
if (toplevel.app_id) |app_id| {
if (server.config.float_filter.contains(mem.span(app_id))) {
view.current.float = true; view.current.float = true;
view.pending.float = true; view.pending.float = true;
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 toplevel has an app_id which is not configured to use client side
// decorations, inform it that it is tiled. // decorations, inform it that it is tiled.

View file

@ -196,16 +196,11 @@ fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wl
view.current.float = true; view.current.float = true;
view.pending.float = true; view.pending.float = true;
view.pending.box = view.float_box; view.pending.box = view.float_box;
} else { } else if (server.config.shouldFloat(view)) {
// Make views with app_ids listed in the float filter float
if (self.xwayland_surface.class) |app_id| {
if (server.config.float_filter.contains(std.mem.span(app_id))) {
view.current.float = true; view.current.float = true;
view.pending.float = true; view.pending.float = true;
view.pending.box = view.float_box; view.pending.box = view.float_box;
} }
}
}
view.map() catch { view.map() catch {
std.log.crit("out of memory", .{}); std.log.crit("out of memory", .{});

View file

@ -27,19 +27,31 @@ const ViewStack = @import("view_stack.zig").ViewStack;
const Error = @import("../command.zig").Error; const Error = @import("../command.zig").Error;
const Seat = @import("../Seat.zig"); const Seat = @import("../Seat.zig");
const FilterKind = enum {
@"app-id",
title,
};
pub fn floatFilterAdd( pub fn floatFilterAdd(
allocator: *mem.Allocator, allocator: *mem.Allocator,
seat: *Seat, seat: *Seat,
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.float_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.float_filter_app_ids,
.title => &server.config.float_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.float_filter.remove(args[1])); errdefer assert(map.remove(args[1]));
gop.key_ptr.* = try std.mem.dupe(util.gpa, u8, args[1]); gop.key_ptr.* = try std.mem.dupe(util.gpa, u8, key);
} }
pub fn floatFilterRemove( pub fn floatFilterRemove(
@ -48,10 +60,17 @@ pub fn floatFilterRemove(
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.float_filter.fetchRemove(args[1])) |kv| util.gpa.free(kv.key); const kind = std.meta.stringToEnum(FilterKind, args[1]) orelse return Error.UnknownOption;
const map = switch (kind) {
.@"app-id" => &server.config.float_filter_app_ids,
.title => &server.config.float_filter_titles,
};
const key = args[2];
if (map.fetchRemove(key)) |kv| util.gpa.free(kv.key);
} }
pub fn csdFilterAdd( pub fn csdFilterAdd(