Merge pull request 'Implement window rules' (#14) from window-rules into main
Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/14
This commit is contained in:
commit
507b16521d
11 changed files with 674 additions and 120 deletions
|
|
@ -71,6 +71,57 @@ borders {
|
||||||
|
|
||||||
Colors are specified in `0xRRGGBB` or `0xRRGGBBAA` hex format.
|
Colors are specified in `0xRRGGBB` or `0xRRGGBBAA` hex format.
|
||||||
|
|
||||||
|
## Window Rules
|
||||||
|
|
||||||
|
Window rules let you set certain properties on windows when they first appear,
|
||||||
|
based on their `app_id` and/or `title`. Crucially, you can override them any time after
|
||||||
|
that, it's only when the window is first created. Rules are placed inside a `window_rules` block:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
window_rules {
|
||||||
|
// Float Firefox picture-in-picture windows
|
||||||
|
float app_id="firefox" title="Picture-in-Picture"
|
||||||
|
// Float any window with "Preferences" in the title
|
||||||
|
float title="*Preferences*"
|
||||||
|
// Keep mpv windows tiled
|
||||||
|
no_float app_id="mpv"
|
||||||
|
// Send Slack to tag 3 (1<<2 = 0x0004)
|
||||||
|
tags 0x0004 app_id="Slack"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rule Types
|
||||||
|
|
||||||
|
| Rule | Argument | Description |
|
||||||
|
|------------|------------------|------------------------------------------|
|
||||||
|
| `float` | | Make matching windows float |
|
||||||
|
| `no_float` | | Keep matching windows tiled |
|
||||||
|
| `tags` | u32 (bitmask) | Assign matching windows to specific tags |
|
||||||
|
|
||||||
|
### Matching
|
||||||
|
|
||||||
|
Each rule can have an `app_id=` and/or `title=` property. Both support glob patterns:
|
||||||
|
|
||||||
|
| Pattern | Matches |
|
||||||
|
|---------------|--------------------------------|
|
||||||
|
| `"foo"` | Exact match only |
|
||||||
|
| `"foo*"` | Starts with "foo" |
|
||||||
|
| `"*foo"` | Ends with "foo" |
|
||||||
|
| `"*foo*"` | Contains "foo" |
|
||||||
|
| `"*"` | Everything |
|
||||||
|
|
||||||
|
A rule with no `app_id=` or `title=` property matches all windows. If both are
|
||||||
|
specified, both must match.
|
||||||
|
|
||||||
|
### Evaluation
|
||||||
|
|
||||||
|
Rules are evaluated top-to-bottom. Each matching rule applies only the properties it
|
||||||
|
specifies, so multiple rules can contribute different properties to the same window.
|
||||||
|
For the same property, later rules override earlier ones.
|
||||||
|
|
||||||
|
Rules are applied once during window initialization. Title changes after initialization
|
||||||
|
do not re-trigger rules.
|
||||||
|
|
||||||
## Bar
|
## Bar
|
||||||
|
|
||||||
The bar is an optional widget that shows the time on your screen. Right now, that's it.
|
The bar is an optional widget that shows the time on your screen. Right now, that's it.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
These are in rough order of my priority, though no promises I do them in this order.
|
These are in rough order of my priority, though no promises I do them in this order.
|
||||||
|
|
||||||
- [ ] Support window rules (float/tags/SSD by app-id/title)
|
- [ ] Move orphan handling out of .output and .seat events; into manage()
|
||||||
|
- [ ] Add focused window title to bar
|
||||||
- [ ] Support overriding config location
|
- [ ] Support overriding config location
|
||||||
- [ ] Support configuring primary vs secondary stack side
|
- [ ] Support configuring primary vs secondary stack side
|
||||||
- [ ] Support switch handling (e.g. lid close)
|
- [ ] Support switch handling (e.g. lid close)
|
||||||
|
|
@ -31,3 +32,5 @@ These are in rough order of my priority, though no promises I do them in this or
|
||||||
- [x] Add options to the tag overlay
|
- [x] Add options to the tag overlay
|
||||||
- [x] Add options to the bar
|
- [x] Add options to the bar
|
||||||
- [x] Make a Rect struct to combine x, y, width, and height
|
- [x] Make a Rect struct to combine x, y, width, and height
|
||||||
|
- [x] Support window rules (float/tags by app-id/title)
|
||||||
|
- [x] Fix resizing size when you pop a window out, basically, it start with its current size but then when you try resize it goes to 75%
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ borders {
|
||||||
color_focused "0x89b4fa"
|
color_focused "0x89b4fa"
|
||||||
color_unfocused "0x1e1e2e"
|
color_unfocused "0x1e1e2e"
|
||||||
}
|
}
|
||||||
// Bar widget - shows the time
|
// Bar widget; shows the time
|
||||||
bar {
|
bar {
|
||||||
position top
|
position top
|
||||||
}
|
}
|
||||||
// Tag overlay widget - shown briefly when switching tags
|
// Tag overlay widget; shown briefly when switching tags
|
||||||
// Remove this block to disable the overlay entirely
|
// Remove this block to disable the overlay entirely
|
||||||
tag_overlay {
|
tag_overlay {
|
||||||
tag_amount 10
|
tag_amount 10
|
||||||
|
|
@ -35,6 +35,18 @@ tag_overlay {
|
||||||
square_inactive_border_color "0x6c7086"
|
square_inactive_border_color "0x6c7086"
|
||||||
square_inactive_occupied_color "0xcdd6f4"
|
square_inactive_occupied_color "0xcdd6f4"
|
||||||
}
|
}
|
||||||
|
// Window rules; applied once when a window first appears
|
||||||
|
// Rules are evaluated top-to-bottom; later rules override earlier ones
|
||||||
|
window_rules {
|
||||||
|
// Float Firefox picture-in-picture windows
|
||||||
|
float app_id="firefox" title="Picture-in-Picture"
|
||||||
|
// Float any window with "Preferences" in the title
|
||||||
|
float title="*Preferences*"
|
||||||
|
// Keep mpv windows tiled
|
||||||
|
no_float app_id="mpv"
|
||||||
|
// Send Slack to tag 3 (1<<2 = 0x0004)
|
||||||
|
tags 0x0004 app_id="Slack"
|
||||||
|
}
|
||||||
keybinds {
|
keybinds {
|
||||||
// Swap a window
|
// Swap a window
|
||||||
spawn Mod4 T foot
|
spawn Mod4 T foot
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,13 @@ tag_binds: std.ArrayList(Keybind) = .{},
|
||||||
keybinds: keybind.Map = .{},
|
keybinds: keybind.Map = .{},
|
||||||
pointer_binds: std.ArrayList(PointerBind) = .{},
|
pointer_binds: std.ArrayList(PointerBind) = .{},
|
||||||
input_configs: std.ArrayList(InputConfig) = .{},
|
input_configs: std.ArrayList(InputConfig) = .{},
|
||||||
|
window_rules: std.ArrayList(WindowRule) = .{},
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub const Keybind = keybind.Keybind;
|
pub const Keybind = keybind.Keybind;
|
||||||
pub const PointerBind = pointer_bind.PointerBind;
|
pub const PointerBind = pointer_bind.PointerBind;
|
||||||
|
pub const WindowRule = window_rule.Rule;
|
||||||
|
pub const WindowRuleAction = window_rule.Action;
|
||||||
|
|
||||||
pub const AttachMode = enum {
|
pub const AttachMode = enum {
|
||||||
top,
|
top,
|
||||||
|
|
@ -66,6 +69,7 @@ const NodeName = enum {
|
||||||
keybinds,
|
keybinds,
|
||||||
pointer_binds,
|
pointer_binds,
|
||||||
tag_overlay,
|
tag_overlay,
|
||||||
|
window_rules,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn create() !*Config {
|
pub fn create() !*Config {
|
||||||
|
|
@ -103,6 +107,11 @@ pub fn create() !*Config {
|
||||||
if (ic.name) |name| utils.gpa.free(name);
|
if (ic.name) |name| utils.gpa.free(name);
|
||||||
}
|
}
|
||||||
config.input_configs.clearAndFree(utils.gpa);
|
config.input_configs.clearAndFree(utils.gpa);
|
||||||
|
for (config.window_rules.items) |rule| {
|
||||||
|
if (rule.app_id_glob) |app_id_glob| utils.gpa.free(app_id_glob);
|
||||||
|
if (rule.title_glob) |title_glob| utils.gpa.free(title_glob);
|
||||||
|
}
|
||||||
|
config.window_rules.clearAndFree(utils.gpa);
|
||||||
if (config.bar_config) |bc| {
|
if (config.bar_config) |bc| {
|
||||||
if (bc.fonts) |fonts| utils.gpa.free(fonts);
|
if (bc.fonts) |fonts| utils.gpa.free(fonts);
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +142,11 @@ pub fn destroy(config: *Config) void {
|
||||||
if (ic.name) |name| utils.gpa.free(name);
|
if (ic.name) |name| utils.gpa.free(name);
|
||||||
}
|
}
|
||||||
config.input_configs.deinit(utils.gpa);
|
config.input_configs.deinit(utils.gpa);
|
||||||
|
for (config.window_rules.items) |rule| {
|
||||||
|
if (rule.app_id_glob) |app_id_glob| utils.gpa.free(app_id_glob);
|
||||||
|
if (rule.title_glob) |title_glob| utils.gpa.free(title_glob);
|
||||||
|
}
|
||||||
|
config.window_rules.deinit(utils.gpa);
|
||||||
if (config.bar_config) |bc| {
|
if (config.bar_config) |bc| {
|
||||||
if (bc.fonts) |fonts| utils.gpa.free(fonts);
|
if (bc.fonts) |fonts| utils.gpa.free(fonts);
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +224,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
if (helpers.boolFromKdlStr(focus_follows_pointer_str)) |focus_follows_pointer| {
|
if (helpers.boolFromKdlStr(focus_follows_pointer_str)) |focus_follows_pointer| {
|
||||||
config.focus_follows_pointer = focus_follows_pointer;
|
config.focus_follows_pointer = focus_follows_pointer;
|
||||||
logDebugSettingNode(name, focus_follows_pointer_str);
|
logDebugSettingNode(name, focus_follows_pointer_str);
|
||||||
} else {
|
} else |_| {
|
||||||
logWarnInvalidNodeArg(name, focus_follows_pointer_str);
|
logWarnInvalidNodeArg(name, focus_follows_pointer_str);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +234,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
if (helpers.boolFromKdlStr(pointer_warp_on_focus_change_str)) |pointer_warp_on_focus_change| {
|
if (helpers.boolFromKdlStr(pointer_warp_on_focus_change_str)) |pointer_warp_on_focus_change| {
|
||||||
config.pointer_warp_on_focus_change = pointer_warp_on_focus_change;
|
config.pointer_warp_on_focus_change = pointer_warp_on_focus_change;
|
||||||
logDebugSettingNode(name, pointer_warp_on_focus_change_str);
|
logDebugSettingNode(name, pointer_warp_on_focus_change_str);
|
||||||
} else {
|
} else |_| {
|
||||||
logWarnInvalidNodeArg(name, pointer_warp_on_focus_change_str);
|
logWarnInvalidNodeArg(name, pointer_warp_on_focus_change_str);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -238,8 +252,6 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
};
|
};
|
||||||
logDebugSettingNode(name, path_str);
|
logDebugSettingNode(name, path_str);
|
||||||
},
|
},
|
||||||
.bar => next_child_block = .bar,
|
|
||||||
.borders => next_child_block = .borders,
|
|
||||||
.input => {
|
.input => {
|
||||||
pending_input_name = if (node.prop(&parser, "name")) |n|
|
pending_input_name = if (node.prop(&parser, "name")) |n|
|
||||||
try utils.gpa.dupe(u8, utils.stripQuotes(n))
|
try utils.gpa.dupe(u8, utils.stripQuotes(n))
|
||||||
|
|
@ -247,15 +259,13 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
null;
|
null;
|
||||||
next_child_block = .input;
|
next_child_block = .input;
|
||||||
},
|
},
|
||||||
.keybinds => {
|
inline .bar,
|
||||||
next_child_block = .keybinds;
|
.borders,
|
||||||
},
|
.keybinds,
|
||||||
.pointer_binds => {
|
.pointer_binds,
|
||||||
next_child_block = .pointer_binds;
|
.tag_overlay,
|
||||||
},
|
.window_rules,
|
||||||
.tag_overlay => {
|
=> |n| next_child_block = n,
|
||||||
next_child_block = .tag_overlay;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
helpers.logWarnInvalidNode(node.name);
|
helpers.logWarnInvalidNode(node.name);
|
||||||
|
|
@ -273,6 +283,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
pending_input_name = null; // ownership transferred
|
pending_input_name = null; // ownership transferred
|
||||||
},
|
},
|
||||||
.tag_overlay => try TagOverlayConfig.load(config, &parser, hostname),
|
.tag_overlay => try TagOverlayConfig.load(config, &parser, hostname),
|
||||||
|
.window_rules => try window_rule.load(config, &parser, hostname),
|
||||||
else => {
|
else => {
|
||||||
// Nothing else should ever be marked as a next_child_block
|
// Nothing else should ever be marked as a next_child_block
|
||||||
unreachable;
|
unreachable;
|
||||||
|
|
@ -322,6 +333,7 @@ const border = @import("config/border.zig");
|
||||||
const helpers = @import("config/helpers.zig");
|
const helpers = @import("config/helpers.zig");
|
||||||
const keybind = @import("config/keybind.zig");
|
const keybind = @import("config/keybind.zig");
|
||||||
const pointer_bind = @import("config/pointer_bind.zig");
|
const pointer_bind = @import("config/pointer_bind.zig");
|
||||||
|
const window_rule = @import("config/window_rule.zig");
|
||||||
const BarConfig = @import("config/BarConfig.zig");
|
const BarConfig = @import("config/BarConfig.zig");
|
||||||
const InputConfig = @import("config/InputConfig.zig");
|
const InputConfig = @import("config/InputConfig.zig");
|
||||||
const TagOverlayConfig = @import("config/TagOverlayConfig.zig");
|
const TagOverlayConfig = @import("config/TagOverlayConfig.zig");
|
||||||
|
|
|
||||||
|
|
@ -576,8 +576,11 @@ pub fn manage(output: *Output) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate layout before managing windows
|
// Calculate layout before managing windows, but only if output dimensions are initialized
|
||||||
output.calculatePrimaryStackLayout();
|
if (output.usable_geometry.width > 0 and output.usable_geometry.height > 0) {
|
||||||
|
output.calculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
var it = output.windows.iterator(.forward);
|
var it = output.windows.iterator(.forward);
|
||||||
while (it.next()) |window| {
|
while (it.next()) |window| {
|
||||||
window.manage();
|
window.manage();
|
||||||
|
|
@ -611,12 +614,17 @@ pub fn render(output: *Output) void {
|
||||||
/// Calculate primary/stack layout positions for all windows.
|
/// Calculate primary/stack layout positions for all windows.
|
||||||
/// - Single window: maximized
|
/// - Single window: maximized
|
||||||
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
|
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
|
||||||
fn calculatePrimaryStackLayout(output: *Output) void {
|
fn calculateLayout(output: *Output) void {
|
||||||
|
// Shouldn't be called if height/width are not positive
|
||||||
|
assert(output.geometry.width > 0 and output.geometry.height > 0);
|
||||||
// Get a list of active windows
|
// Get a list of active windows
|
||||||
var active_list: DoublyLinkedList = .{};
|
var active_list: DoublyLinkedList = .{};
|
||||||
var active_count: u31 = 0;
|
var active_count: u31 = 0;
|
||||||
var it = output.windows.iterator(.forward);
|
var it = output.windows.iterator(.forward);
|
||||||
while (it.next()) |window| {
|
while (it.next()) |window| {
|
||||||
|
// Initialize new windows before checking tags/float so that
|
||||||
|
// window rules are reflected in the first frame's layout.
|
||||||
|
window.initialize();
|
||||||
if (output.tags & window.tags != 0x0000) {
|
if (output.tags & window.tags != 0x0000) {
|
||||||
// Floating windows should be shown but not included in this layout generation
|
// Floating windows should be shown but not included in this layout generation
|
||||||
const will_float = window.pending_manage.floating orelse window.floating;
|
const will_float = window.pending_manage.floating orelse window.floating;
|
||||||
|
|
@ -711,8 +719,14 @@ fn calculatePrimaryStackLayout(output: *Output) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make space for borders
|
// Make space for borders
|
||||||
|
if (window.pending_manage.dimensions.?.height > 2 * border_width and
|
||||||
|
window.pending_manage.dimensions.?.width > 2 * border_width)
|
||||||
|
{
|
||||||
window.pending_manage.dimensions.?.height -= 2 * border_width;
|
window.pending_manage.dimensions.?.height -= 2 * border_width;
|
||||||
window.pending_manage.dimensions.?.width -= 2 * border_width;
|
window.pending_manage.dimensions.?.width -= 2 * border_width;
|
||||||
|
} else {
|
||||||
|
log.warn("Can't add borders to some window; {s}'s dimensions are too small.", .{output.name orelse "some output"});
|
||||||
|
}
|
||||||
window.pending_render.position.?.x += border_width;
|
window.pending_render.position.?.x += border_width;
|
||||||
window.pending_render.position.?.y += border_width;
|
window.pending_render.position.?.y += border_width;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
141
src/Window.zig
141
src/Window.zig
|
|
@ -9,6 +9,10 @@ context: *Context,
|
||||||
river_window_v1: *river.WindowV1,
|
river_window_v1: *river.WindowV1,
|
||||||
river_node_v1: *river.NodeV1,
|
river_node_v1: *river.NodeV1,
|
||||||
|
|
||||||
|
app_id: ?[]const u8 = null,
|
||||||
|
title: ?[]const u8 = null,
|
||||||
|
parent: ?*river.WindowV1 = null,
|
||||||
|
|
||||||
rect: utils.Rect = .{},
|
rect: utils.Rect = .{},
|
||||||
|
|
||||||
fullscreen: bool = false,
|
fullscreen: bool = false,
|
||||||
|
|
@ -19,6 +23,7 @@ output: ?*Output,
|
||||||
|
|
||||||
floating: bool = false,
|
floating: bool = false,
|
||||||
floating_rect: utils.Rect = .{},
|
floating_rect: utils.Rect = .{},
|
||||||
|
dimensions_hint: DimensionsHint = .{},
|
||||||
|
|
||||||
initialized: bool = false,
|
initialized: bool = false,
|
||||||
|
|
||||||
|
|
@ -61,6 +66,29 @@ pub const PendingRender = struct {
|
||||||
show: ?bool = null,
|
show: ?bool = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const DimensionsHint = struct {
|
||||||
|
min_width: u31 = 0,
|
||||||
|
min_height: u31 = 0,
|
||||||
|
max_width: u31 = 0,
|
||||||
|
max_height: u31 = 0,
|
||||||
|
|
||||||
|
fn clampWidth(hint: DimensionsHint, width: u31) u31 {
|
||||||
|
return math.clamp(
|
||||||
|
width,
|
||||||
|
if (hint.min_width != 0) hint.min_width else 1,
|
||||||
|
if (hint.max_width != 0) hint.max_width else math.maxInt(u31),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clampHeight(hint: DimensionsHint, height: u31) u31 {
|
||||||
|
return math.clamp(
|
||||||
|
height,
|
||||||
|
if (hint.min_height != 0) hint.min_height else 1,
|
||||||
|
if (hint.max_height != 0) hint.max_height else math.maxInt(u31),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Output) !*Window {
|
pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Output) !*Window {
|
||||||
var window = try utils.gpa.create(Window);
|
var window = try utils.gpa.create(Window);
|
||||||
errdefer utils.gpa.destroy(window);
|
errdefer utils.gpa.destroy(window);
|
||||||
|
|
@ -80,8 +108,11 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(window: *Window) void {
|
pub fn destroy(window: *Window) void {
|
||||||
window.river_window_v1.destroy();
|
if (window.app_id) |app_id| utils.gpa.free(app_id);
|
||||||
|
if (window.title) |title| utils.gpa.free(title);
|
||||||
|
|
||||||
window.river_node_v1.destroy();
|
window.river_node_v1.destroy();
|
||||||
|
window.river_window_v1.destroy();
|
||||||
|
|
||||||
utils.gpa.destroy(window);
|
utils.gpa.destroy(window);
|
||||||
}
|
}
|
||||||
|
|
@ -111,8 +142,8 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
||||||
seat.pending_manage.pointer_resize_request = null;
|
seat.pending_manage.pointer_resize_request = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If there's no output, we don't really care about focus and can skip this event
|
if (window.output) |output| {
|
||||||
const output = if (window.output) |output| output else return;
|
// Get a new window for the wm to focus
|
||||||
var it = window.context.wm.seats.iterator(.forward);
|
var it = window.context.wm.seats.iterator(.forward);
|
||||||
while (it.next()) |seat| {
|
while (it.next()) |seat| {
|
||||||
if (seat.focused_window == window) {
|
if (seat.focused_window == window) {
|
||||||
|
|
@ -130,6 +161,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
window.link.remove();
|
window.link.remove();
|
||||||
window.destroy();
|
window.destroy();
|
||||||
},
|
},
|
||||||
|
|
@ -138,8 +170,39 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
||||||
assert(ev.width > 0 and ev.height > 0);
|
assert(ev.width > 0 and ev.height > 0);
|
||||||
window.pending_manage.dimensions = .{ .width = @intCast(ev.width), .height = @intCast(ev.height) };
|
window.pending_manage.dimensions = .{ .width = @intCast(ev.width), .height = @intCast(ev.height) };
|
||||||
},
|
},
|
||||||
.dimensions_hint => {
|
.dimensions_hint => |ev| {
|
||||||
// TODO: Maybe could use this for floating windows
|
window.dimensions_hint = .{
|
||||||
|
.min_width = @intCast(ev.min_width),
|
||||||
|
.min_height = @intCast(ev.min_height),
|
||||||
|
.max_width = @intCast(ev.max_width),
|
||||||
|
.max_height = @intCast(ev.max_height),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.app_id => |ev| {
|
||||||
|
if (window.app_id) |app_id| utils.gpa.free(app_id);
|
||||||
|
window.app_id = if (ev.app_id) |aid|
|
||||||
|
utils.gpa.dupe(u8, std.mem.span(aid)) catch @panic("Out of memory")
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
},
|
||||||
|
.title => |ev| {
|
||||||
|
if (window.title) |title| utils.gpa.free(title);
|
||||||
|
window.title = if (ev.title) |t|
|
||||||
|
utils.gpa.dupe(u8, std.mem.span(t)) catch @panic("Out of memory")
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
},
|
||||||
|
.parent => |ev| {
|
||||||
|
const parent = ev.parent orelse return;
|
||||||
|
window.parent = parent;
|
||||||
|
|
||||||
|
// Make window float on top of its parent
|
||||||
|
window.pending_manage.floating = true;
|
||||||
|
const parent_window: *Window = @ptrCast(@alignCast(parent.getUserData() orelse return));
|
||||||
|
window.pending_render.position = .{
|
||||||
|
.x = parent_window.rect.x + @divTrunc(parent_window.rect.width, 2),
|
||||||
|
.y = parent_window.rect.y + @divTrunc(parent_window.rect.height, 2),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
else => |ev| {
|
else => |ev| {
|
||||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||||
|
|
@ -147,25 +210,38 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn manage(window: *Window) void {
|
/// Apply one-time initialization for newly created windows.
|
||||||
const river_window_v1 = window.river_window_v1;
|
/// Called before calculatePrimaryStackLayout() so that tag and float
|
||||||
if (!window.initialized) {
|
/// rules are reflected in the first frame's layout.
|
||||||
// Only happens once per Window
|
pub fn initialize(window: *Window) void {
|
||||||
|
if (window.initialized) {
|
||||||
@branchHint(.unlikely);
|
@branchHint(.unlikely);
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.initialized = true;
|
window.initialized = true;
|
||||||
|
|
||||||
|
const river_window_v1 = window.river_window_v1;
|
||||||
|
|
||||||
// TODO: We might want to think about paying attention to the decoration_hint event
|
// TODO: We might want to think about paying attention to the decoration_hint event
|
||||||
// If we do, this would need to move, I think?
|
// If we do, this would need to move, I think?
|
||||||
river_window_v1.useSsd();
|
river_window_v1.useSsd();
|
||||||
|
|
||||||
river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true });
|
river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true });
|
||||||
river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
||||||
|
|
||||||
|
const res = window.applyRules();
|
||||||
|
if (res.tags) |tags| window.tags = tags;
|
||||||
|
if (res.float) |should_float|
|
||||||
|
window.pending_manage.floating = should_float;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn manage(window: *Window) void {
|
||||||
// Updating state since the last manage event
|
// Updating state since the last manage event
|
||||||
defer window.pending_manage = .{};
|
defer window.pending_manage = .{};
|
||||||
const pending_manage = window.pending_manage;
|
const pending_manage = window.pending_manage;
|
||||||
|
const river_window_v1 = window.river_window_v1;
|
||||||
// Floating status
|
// Floating status
|
||||||
|
var became_floating = false;
|
||||||
if (pending_manage.floating) |floating| blk: {
|
if (pending_manage.floating) |floating| blk: {
|
||||||
// This needs to be before proposing the new dimensions since we want to save the current ones!
|
// This needs to be before proposing the new dimensions since we want to save the current ones!
|
||||||
// Skip the rest of the block if floating matches what is already set
|
// Skip the rest of the block if floating matches what is already set
|
||||||
|
|
@ -173,14 +249,16 @@ pub fn manage(window: *Window) void {
|
||||||
|
|
||||||
window.floating = floating;
|
window.floating = floating;
|
||||||
if (floating) {
|
if (floating) {
|
||||||
|
became_floating = true;
|
||||||
// Let the window know it isn't tiled
|
// Let the window know it isn't tiled
|
||||||
river_window_v1.setTiled(.{});
|
river_window_v1.setTiled(.{});
|
||||||
|
|
||||||
if (window.floating_rect.width == 0) {
|
if (window.floating_rect.width == 0) {
|
||||||
// Never floated before; use 75% of usable area, centered on output
|
// This window has never floated before, let's give it floating dimensions.
|
||||||
|
// Use 75% of the output's usable size, clamped to the window's dimension hints.
|
||||||
if (window.output) |output| {
|
if (window.output) |output| {
|
||||||
window.floating_rect.width = @divFloor(output.usable_geometry.width * 3, 4);
|
window.floating_rect.width = window.dimensions_hint.clampWidth(@divFloor(output.usable_geometry.width * 3, 4));
|
||||||
window.floating_rect.height = @divFloor(output.usable_geometry.height * 3, 4);
|
window.floating_rect.height = window.dimensions_hint.clampHeight(@divFloor(output.usable_geometry.height * 3, 4));
|
||||||
window.floating_rect.x = output.usable_geometry.x + @divFloor(output.usable_geometry.width, 2) - @divFloor(window.floating_rect.width, 2);
|
window.floating_rect.x = output.usable_geometry.x + @divFloor(output.usable_geometry.width, 2) - @divFloor(window.floating_rect.width, 2);
|
||||||
window.floating_rect.y = output.usable_geometry.y + @divFloor(output.usable_geometry.height, 2) - @divFloor(window.floating_rect.height, 2);
|
window.floating_rect.y = output.usable_geometry.y + @divFloor(output.usable_geometry.height, 2) - @divFloor(window.floating_rect.height, 2);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -202,12 +280,15 @@ pub fn manage(window: *Window) void {
|
||||||
window.floating_rect.y = window.rect.y;
|
window.floating_rect.y = window.rect.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Layout (pre-computed by WindowManager.calculatePrimaryStackLayout())
|
// Layout (pre-computed by WindowManager.caluclateLayout())
|
||||||
if (pending_manage.dimensions) |dimensions| {
|
if (pending_manage.dimensions) |dimensions| {
|
||||||
window.rect.width = dimensions.width;
|
window.rect.width = dimensions.width;
|
||||||
window.rect.height = dimensions.height;
|
window.rect.height = dimensions.height;
|
||||||
|
if (!became_floating) {
|
||||||
|
// We want to skip this if the floating block above already proposed dimensions
|
||||||
window.river_window_v1.proposeDimensions(dimensions.width, dimensions.height);
|
window.river_window_v1.proposeDimensions(dimensions.width, dimensions.height);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Fullscreen and maximize operations
|
// Fullscreen and maximize operations
|
||||||
if (pending_manage.fullscreen) |fullscreen| blk: {
|
if (pending_manage.fullscreen) |fullscreen| blk: {
|
||||||
window.fullscreen = fullscreen;
|
window.fullscreen = fullscreen;
|
||||||
|
|
@ -280,17 +361,51 @@ fn applyBorders(window: *Window, color: utils.RiverColor) void {
|
||||||
window.river_window_v1.setBorders(all_sides, border_width, color.red, color.green, color.blue, color.alpha);
|
window.river_window_v1.setBorders(all_sides, border_width, color.red, color.green, color.blue, color.alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iterate over all window rules and apply any that match.
|
||||||
|
// Later rules in the list overwrite earlier ones.
|
||||||
|
fn applyRules(window: *Window) struct {
|
||||||
|
float: ?bool = null,
|
||||||
|
tags: ?u32 = null,
|
||||||
|
} {
|
||||||
|
var float: ?bool = null;
|
||||||
|
var tags: ?u32 = null;
|
||||||
|
for (window.context.config.window_rules.items) |rule| {
|
||||||
|
const app_id_matches = if (rule.app_id_glob) |glob|
|
||||||
|
if (window.app_id) |app_id| globber.match(app_id, glob) else false
|
||||||
|
else
|
||||||
|
true;
|
||||||
|
const title_matches = if (rule.title_glob) |glob|
|
||||||
|
if (window.title) |title| globber.match(title, glob) else false
|
||||||
|
else
|
||||||
|
true;
|
||||||
|
if (app_id_matches and title_matches) {
|
||||||
|
switch (rule.action) {
|
||||||
|
.float => |should_float| float = should_float,
|
||||||
|
.tags => |tagmask| tags = tagmask,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.float = float,
|
||||||
|
.tags = tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
const math = std.math;
|
||||||
const DoublyLinkedList = std.DoublyLinkedList;
|
const DoublyLinkedList = std.DoublyLinkedList;
|
||||||
|
|
||||||
const wayland = @import("wayland");
|
const wayland = @import("wayland");
|
||||||
const wl = wayland.client.wl;
|
const wl = wayland.client.wl;
|
||||||
const river = wayland.client.river;
|
const river = wayland.client.river;
|
||||||
|
|
||||||
|
const globber = @import("globber.zig");
|
||||||
const utils = @import("utils.zig");
|
const utils = @import("utils.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
const Output = @import("Output.zig");
|
const Output = @import("Output.zig");
|
||||||
const Seat = @import("Seat.zig");
|
const Seat = @import("Seat.zig");
|
||||||
|
const WindowRule = @import("Config.zig").WindowRule;
|
||||||
|
|
||||||
const log = std.log.scoped(.Window);
|
const log = std.log.scoped(.Window);
|
||||||
|
|
|
||||||
|
|
@ -72,21 +72,17 @@ pub fn prevOutput(wm: *WindowManager, current: *Output) ?*Output {
|
||||||
return @fieldParentPtr("link", prev_link);
|
return @fieldParentPtr("link", prev_link);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_start(wm: *WindowManager) void {
|
fn initialize(wm: *WindowManager) void {
|
||||||
const river_window_manager_v1 = wm.river_window_manager_v1;
|
if (wm.context.initialized) return;
|
||||||
const context = wm.context;
|
|
||||||
|
|
||||||
// This gets used shortly, so it goes at the beginning
|
// We need a seat to intitialize this stuff, so let's just not do it right now.
|
||||||
context.manage();
|
// The WM can run fine without it, though, it won't be fully usuable.
|
||||||
|
const seat = wm.seats.first() orelse return;
|
||||||
if (!context.initialized) {
|
|
||||||
// This code runs during initial startup and after config reloads.
|
|
||||||
@branchHint(.cold);
|
|
||||||
context.initialized = true;
|
|
||||||
|
|
||||||
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
|
||||||
const river_seat_v1 = seat.river_seat_v1;
|
const river_seat_v1 = seat.river_seat_v1;
|
||||||
|
|
||||||
|
const context = wm.context;
|
||||||
|
context.initialized = true;
|
||||||
|
|
||||||
// Tag bindings
|
// Tag bindings
|
||||||
for (context.config.tag_binds.items) |tag_bind| {
|
for (context.config.tag_binds.items) |tag_bind| {
|
||||||
comptime var i: u8 = 1;
|
comptime var i: u8 = 1;
|
||||||
|
|
@ -132,6 +128,19 @@ fn manage_start(wm: *WindowManager) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn manage(wm: *WindowManager) void {
|
||||||
|
const river_window_manager_v1 = wm.river_window_manager_v1;
|
||||||
|
const context = wm.context;
|
||||||
|
|
||||||
|
// This gets used shortly, so it goes at the beginning
|
||||||
|
context.manage();
|
||||||
|
|
||||||
|
if (!context.initialized) {
|
||||||
|
// This code runs during initial startup and after config reloads.
|
||||||
|
@branchHint(.cold);
|
||||||
|
wm.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var it = wm.outputs.iterator(.forward);
|
var it = wm.outputs.iterator(.forward);
|
||||||
while (it.next()) |output| {
|
while (it.next()) |output| {
|
||||||
|
|
@ -147,7 +156,7 @@ fn manage_start(wm: *WindowManager) void {
|
||||||
river_window_manager_v1.manageFinish();
|
river_window_manager_v1.manageFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_start(wm: *WindowManager) void {
|
fn render(wm: *WindowManager) void {
|
||||||
const river_window_manager_v1 = wm.river_window_manager_v1;
|
const river_window_manager_v1 = wm.river_window_manager_v1;
|
||||||
{
|
{
|
||||||
var it = wm.seats.iterator(.forward);
|
var it = wm.seats.iterator(.forward);
|
||||||
|
|
@ -171,8 +180,8 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
||||||
.unavailable => {
|
.unavailable => {
|
||||||
fatal("Window manager unavailable (some other wm instance is running). Exiting", .{});
|
fatal("Window manager unavailable (some other wm instance is running). Exiting", .{});
|
||||||
},
|
},
|
||||||
.manage_start => wm.manage_start(),
|
.manage_start => wm.manage(),
|
||||||
.render_start => wm.render_start(),
|
.render_start => wm.render(),
|
||||||
.output => |ev| {
|
.output => |ev| {
|
||||||
const output = Output.create(context, ev.id) catch @panic("Out of memory");
|
const output = Output.create(context, ev.id) catch @panic("Out of memory");
|
||||||
wm.outputs.append(output);
|
wm.outputs.append(output);
|
||||||
|
|
@ -212,12 +221,32 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
||||||
// If there was already an output, but no seats, set the first output as focused
|
// If there was already an output, but no seats, set the first output as focused
|
||||||
if (wm.outputs.first()) |output| {
|
if (wm.outputs.first()) |output| {
|
||||||
seat.pending_manage.output = .{ .output = output };
|
seat.pending_manage.output = .{ .output = output };
|
||||||
|
|
||||||
|
// Adopt any orphan windows that arrived before we had a seat
|
||||||
|
var it = wm.orphan_windows.iterator(.forward);
|
||||||
|
while (it.next()) |window| {
|
||||||
|
window.pending_manage.pending_output = .{ .output = output };
|
||||||
|
}
|
||||||
|
if (wm.orphan_windows.first()) |first| {
|
||||||
|
seat.pending_manage.window = .{ .window = first };
|
||||||
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
|
}
|
||||||
|
output.windows.appendList(&wm.orphan_windows);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.window => |ev| {
|
.window => |ev| {
|
||||||
// TODO: Support multiple seats
|
// TODO: Support multiple seats
|
||||||
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
const seat = wm.seats.first();
|
||||||
const focused_output = seat.focused_output;
|
const focused_output = if (seat) |s|
|
||||||
|
s.focused_output orelse if (s.pending_manage.output) |pending_output|
|
||||||
|
switch (pending_output) {
|
||||||
|
.output => |output| output,
|
||||||
|
.clear_focus => null,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
wm.outputs.first()
|
||||||
|
else
|
||||||
|
wm.outputs.first();
|
||||||
const window_list = if (focused_output) |output|
|
const window_list = if (focused_output) |output|
|
||||||
&output.windows
|
&output.windows
|
||||||
else
|
else
|
||||||
|
|
@ -228,8 +257,10 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
||||||
.top => window_list.prepend(window),
|
.top => window_list.prepend(window),
|
||||||
.bottom => window_list.append(window),
|
.bottom => window_list.append(window),
|
||||||
}
|
}
|
||||||
seat.pending_manage.window = .{ .window = window };
|
if (seat) |s| {
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
s.pending_manage.window = .{ .window = window };
|
||||||
|
s.pending_manage.should_warp_pointer = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
else => |ev| {
|
else => |ev| {
|
||||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
/// if arg_str in ["#true", "true"], return true
|
/// if arg_str in ["#true", "true"], return true
|
||||||
/// if arg_str in ["#false", "false"], return false
|
/// if arg_str in ["#false", "false"], return false
|
||||||
/// else, return null
|
/// else, return null
|
||||||
pub fn boolFromKdlStr(arg_str: []const u8) ?bool {
|
pub fn boolFromKdlStr(arg_str: []const u8) !bool {
|
||||||
if (mem.eql(u8, arg_str, "#true") or
|
if (mem.eql(u8, arg_str, "#true") or
|
||||||
mem.eql(u8, arg_str, "true"))
|
mem.eql(u8, arg_str, "true"))
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +17,7 @@ pub fn boolFromKdlStr(arg_str: []const u8) ?bool {
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return null;
|
return error.NotABool;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parseButton(s: []const u8) ?u32 {
|
pub fn parseButton(s: []const u8) ?u32 {
|
||||||
|
|
@ -96,16 +96,16 @@ const testing = std.testing;
|
||||||
|
|
||||||
test "boolFromKdlStr" {
|
test "boolFromKdlStr" {
|
||||||
// True valid
|
// True valid
|
||||||
try testing.expectEqual(@as(?bool, true), boolFromKdlStr("#true"));
|
try testing.expectEqual(true, try boolFromKdlStr("#true"));
|
||||||
try testing.expectEqual(@as(?bool, true), boolFromKdlStr("true"));
|
try testing.expectEqual(true, try boolFromKdlStr("true"));
|
||||||
// False valid
|
// False valid
|
||||||
try testing.expectEqual(@as(?bool, false), boolFromKdlStr("#false"));
|
try testing.expectEqual(false, try boolFromKdlStr("#false"));
|
||||||
try testing.expectEqual(@as(?bool, false), boolFromKdlStr("false"));
|
try testing.expectEqual(false, try boolFromKdlStr("false"));
|
||||||
// Invalid
|
// Invalid
|
||||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("yes"));
|
try testing.expectError(error.NotABool, boolFromKdlStr("yes"));
|
||||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("1"));
|
try testing.expectError(error.NotABool, boolFromKdlStr("1"));
|
||||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr(""));
|
try testing.expectError(error.NotABool, boolFromKdlStr(""));
|
||||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("TRUE"));
|
try testing.expectError(error.NotABool, boolFromKdlStr("TRUE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseButton named buttons" {
|
test "parseButton named buttons" {
|
||||||
|
|
|
||||||
104
src/config/window_rule.zig
Normal file
104
src/config/window_rule.zig
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
const NodeName = enum {
|
||||||
|
float,
|
||||||
|
no_float,
|
||||||
|
tags,
|
||||||
|
// TODO: Add more of riverctl's rule options such as ssd/csd
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Rule = struct {
|
||||||
|
// if app_id/title are null, they match all values
|
||||||
|
app_id_glob: ?[]const u8 = null,
|
||||||
|
title_glob: ?[]const u8 = null,
|
||||||
|
action: Action,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Action = union(enum) {
|
||||||
|
float: bool,
|
||||||
|
tags: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
||||||
|
while (try parser.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.node => |node| {
|
||||||
|
const node_name = std.meta.stringToEnum(NodeName, node.name);
|
||||||
|
if (node_name) |name| {
|
||||||
|
if (!helpers.hostMatches(node, parser, hostname)) {
|
||||||
|
log.debug("Skipping \"window_rule.{s}\" (host mismatch)", .{@tagName(name)});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const app_id_glob = if (node.prop(parser, "app_id")) |raw_app_id| blk: {
|
||||||
|
const app_id = utils.stripQuotes(raw_app_id);
|
||||||
|
globber.validate(app_id) catch {
|
||||||
|
log.warn("Invalid glob for app_id \"{s}\"", .{app_id});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
break :blk try utils.gpa.dupe(u8, app_id);
|
||||||
|
} else null;
|
||||||
|
errdefer if (app_id_glob) |app_id| utils.gpa.free(app_id);
|
||||||
|
const title_glob = if (node.prop(parser, "title")) |raw_title| blk: {
|
||||||
|
const title = utils.stripQuotes(raw_title);
|
||||||
|
globber.validate(title) catch {
|
||||||
|
log.warn("Invalid glob for title \"{s}\"", .{title});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
break :blk try utils.gpa.dupe(u8, title);
|
||||||
|
} else null;
|
||||||
|
errdefer if (title_glob) |title| utils.gpa.free(title);
|
||||||
|
|
||||||
|
const action: Action = sw: switch (name) {
|
||||||
|
.float => .{ .float = true },
|
||||||
|
.no_float => .{ .float = false },
|
||||||
|
.tags => {
|
||||||
|
const tags_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
||||||
|
const tags = fmt.parseInt(u32, tags_str, 0) catch {
|
||||||
|
logWarnInvalidNodeArg(name, tags_str);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
break :sw .{ .tags = tags };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try config.window_rules.append(utils.gpa, .{
|
||||||
|
.app_id_glob = app_id_glob,
|
||||||
|
.title_glob = title_glob,
|
||||||
|
.action = action,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
helpers.logWarnInvalidNode(node.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.child_block_begin => {
|
||||||
|
// window_rules should never have a nested child block
|
||||||
|
try helpers.skipChildBlock(parser);
|
||||||
|
},
|
||||||
|
.child_block_end => {
|
||||||
|
// Done parsing the window_rules block; return
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn logWarnInvalidNodeArg(node_name: NodeName, node_value: []const u8) void {
|
||||||
|
log.warn("Invalid \"window_rule.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value });
|
||||||
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const fmt = std.fmt;
|
||||||
|
const mem = std.mem;
|
||||||
|
|
||||||
|
const kdl = @import("kdl");
|
||||||
|
|
||||||
|
const globber = @import("../globber.zig");
|
||||||
|
const utils = @import("../utils.zig");
|
||||||
|
const Config = @import("../Config.zig");
|
||||||
|
const XkbBindings = @import("../XkbBindings.zig");
|
||||||
|
|
||||||
|
const helpers = @import("helpers.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.config_window_rule);
|
||||||
211
src/globber.zig
Normal file
211
src/globber.zig
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Copyright 2023 Isaac Freund
|
||||||
|
// SPDX-FileCopyrightText: 2023 Isaac Freund
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
|
||||||
|
/// Validate a glob, returning error.InvalidGlob if it is empty, "**" or has a
|
||||||
|
/// '*' at any position other than the first and/or last byte.
|
||||||
|
pub fn validate(glob: []const u8) error{InvalidGlob}!void {
|
||||||
|
switch (glob.len) {
|
||||||
|
0 => return error.InvalidGlob,
|
||||||
|
1 => {},
|
||||||
|
2 => if (glob[0] == '*' and glob[1] == '*') return error.InvalidGlob,
|
||||||
|
else => if (mem.indexOfScalar(u8, glob[1 .. glob.len - 1], '*') != null) {
|
||||||
|
return error.InvalidGlob;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test validate {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
try validate("*");
|
||||||
|
try validate("a");
|
||||||
|
try validate("*a");
|
||||||
|
try validate("a*");
|
||||||
|
try validate("*a*");
|
||||||
|
try validate("ab");
|
||||||
|
try validate("*ab");
|
||||||
|
try validate("ab*");
|
||||||
|
try validate("*ab*");
|
||||||
|
try validate("abc");
|
||||||
|
try validate("*abc");
|
||||||
|
try validate("abc*");
|
||||||
|
try validate("*abc*");
|
||||||
|
|
||||||
|
try testing.expectError(error.InvalidGlob, validate(""));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("**"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("***"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("a*c"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("ab*c*"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("*ab*c"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("ab*c"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("a*bc*"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("**a"));
|
||||||
|
try testing.expectError(error.InvalidGlob, validate("abc**"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if s is matched by glob.
|
||||||
|
/// Asserts that the glob is valid, see `validate()`.
|
||||||
|
pub fn match(s: []const u8, glob: []const u8) bool {
|
||||||
|
if (std.debug.runtime_safety) {
|
||||||
|
validate(glob) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glob.len == 1) {
|
||||||
|
return glob[0] == '*' or mem.eql(u8, s, glob);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix_match = glob[0] == '*';
|
||||||
|
const prefix_match = glob[glob.len - 1] == '*';
|
||||||
|
|
||||||
|
if (suffix_match and prefix_match) {
|
||||||
|
return mem.indexOf(u8, s, glob[1 .. glob.len - 1]) != null;
|
||||||
|
} else if (suffix_match) {
|
||||||
|
return mem.endsWith(u8, s, glob[1..]);
|
||||||
|
} else if (prefix_match) {
|
||||||
|
return mem.startsWith(u8, s, glob[0 .. glob.len - 1]);
|
||||||
|
} else {
|
||||||
|
return mem.eql(u8, s, glob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test match {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
try testing.expect(match("", "*"));
|
||||||
|
|
||||||
|
try testing.expect(match("a", "*"));
|
||||||
|
try testing.expect(match("a", "*a*"));
|
||||||
|
try testing.expect(match("a", "a*"));
|
||||||
|
try testing.expect(match("a", "*a"));
|
||||||
|
try testing.expect(match("a", "a"));
|
||||||
|
|
||||||
|
try testing.expect(!match("a", "b"));
|
||||||
|
try testing.expect(!match("a", "*b*"));
|
||||||
|
try testing.expect(!match("a", "b*"));
|
||||||
|
try testing.expect(!match("a", "*b"));
|
||||||
|
|
||||||
|
try testing.expect(match("ab", "*"));
|
||||||
|
try testing.expect(match("ab", "*a*"));
|
||||||
|
try testing.expect(match("ab", "*b*"));
|
||||||
|
try testing.expect(match("ab", "a*"));
|
||||||
|
try testing.expect(match("ab", "*b"));
|
||||||
|
try testing.expect(match("ab", "*ab*"));
|
||||||
|
try testing.expect(match("ab", "ab*"));
|
||||||
|
try testing.expect(match("ab", "*ab"));
|
||||||
|
try testing.expect(match("ab", "ab"));
|
||||||
|
|
||||||
|
try testing.expect(!match("ab", "b*"));
|
||||||
|
try testing.expect(!match("ab", "*a"));
|
||||||
|
try testing.expect(!match("ab", "*c*"));
|
||||||
|
try testing.expect(!match("ab", "c*"));
|
||||||
|
try testing.expect(!match("ab", "*c"));
|
||||||
|
try testing.expect(!match("ab", "ac"));
|
||||||
|
try testing.expect(!match("ab", "*ac*"));
|
||||||
|
try testing.expect(!match("ab", "ac*"));
|
||||||
|
try testing.expect(!match("ab", "*ac"));
|
||||||
|
|
||||||
|
try testing.expect(match("abc", "*"));
|
||||||
|
try testing.expect(match("abc", "*a*"));
|
||||||
|
try testing.expect(match("abc", "*b*"));
|
||||||
|
try testing.expect(match("abc", "*c*"));
|
||||||
|
try testing.expect(match("abc", "a*"));
|
||||||
|
try testing.expect(match("abc", "*c"));
|
||||||
|
try testing.expect(match("abc", "*ab*"));
|
||||||
|
try testing.expect(match("abc", "ab*"));
|
||||||
|
try testing.expect(match("abc", "*bc*"));
|
||||||
|
try testing.expect(match("abc", "*bc"));
|
||||||
|
try testing.expect(match("abc", "*abc*"));
|
||||||
|
try testing.expect(match("abc", "abc*"));
|
||||||
|
try testing.expect(match("abc", "*abc"));
|
||||||
|
try testing.expect(match("abc", "abc"));
|
||||||
|
|
||||||
|
try testing.expect(!match("abc", "*a"));
|
||||||
|
try testing.expect(!match("abc", "*b"));
|
||||||
|
try testing.expect(!match("abc", "b*"));
|
||||||
|
try testing.expect(!match("abc", "c*"));
|
||||||
|
try testing.expect(!match("abc", "*ab"));
|
||||||
|
try testing.expect(!match("abc", "bc*"));
|
||||||
|
try testing.expect(!match("abc", "*d*"));
|
||||||
|
try testing.expect(!match("abc", "d*"));
|
||||||
|
try testing.expect(!match("abc", "*d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns .lt if a is less general than b.
|
||||||
|
/// Returns .gt if a is more general than b.
|
||||||
|
/// Returns .eq if a and b are equally general.
|
||||||
|
/// Both a and b must be valid globs, see `validate()`.
|
||||||
|
pub fn order(a: []const u8, b: []const u8) std.math.Order {
|
||||||
|
if (std.debug.runtime_safety) {
|
||||||
|
validate(a) catch unreachable;
|
||||||
|
validate(b) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mem.eql(u8, a, "*") and mem.eql(u8, b, "*")) {
|
||||||
|
return .eq;
|
||||||
|
} else if (mem.eql(u8, a, "*")) {
|
||||||
|
return .gt;
|
||||||
|
} else if (mem.eql(u8, b, "*")) {
|
||||||
|
return .lt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count_a = @as(u2, @intFromBool(a[0] == '*')) + @intFromBool(a[a.len - 1] == '*');
|
||||||
|
const count_b = @as(u2, @intFromBool(b[0] == '*')) + @intFromBool(b[b.len - 1] == '*');
|
||||||
|
|
||||||
|
if (count_a == 0 and count_b == 0) {
|
||||||
|
return .eq;
|
||||||
|
} else if (count_a == count_b) {
|
||||||
|
// This may look backwards since e.g. "c*" is more general than "cc*"
|
||||||
|
return std.math.order(b.len, a.len);
|
||||||
|
} else {
|
||||||
|
return std.math.order(count_a, count_b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test order {
|
||||||
|
const testing = std.testing;
|
||||||
|
const Order = std.math.Order;
|
||||||
|
|
||||||
|
try testing.expectEqual(Order.eq, order("*", "*"));
|
||||||
|
try testing.expectEqual(Order.eq, order("*a*", "*b*"));
|
||||||
|
try testing.expectEqual(Order.eq, order("a*", "*b"));
|
||||||
|
try testing.expectEqual(Order.eq, order("*a", "*b"));
|
||||||
|
try testing.expectEqual(Order.eq, order("*a", "b*"));
|
||||||
|
try testing.expectEqual(Order.eq, order("a*", "b*"));
|
||||||
|
|
||||||
|
const descending = [_][]const u8{
|
||||||
|
"*",
|
||||||
|
"*a*",
|
||||||
|
"*b*",
|
||||||
|
"*a*",
|
||||||
|
"*ab*",
|
||||||
|
"*bab*",
|
||||||
|
"*a",
|
||||||
|
"b*",
|
||||||
|
"*b",
|
||||||
|
"*a",
|
||||||
|
"a",
|
||||||
|
"bababab",
|
||||||
|
"b",
|
||||||
|
"a",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (descending, 0..) |a, i| {
|
||||||
|
for (descending[i..]) |b| {
|
||||||
|
try testing.expect(order(a, b) != .lt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ascending = descending;
|
||||||
|
mem.reverse([]const u8, &ascending);
|
||||||
|
|
||||||
|
for (ascending, 0..) |a, i| {
|
||||||
|
for (ascending[i..]) |b| {
|
||||||
|
try testing.expect(order(a, b) != .gt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -357,4 +357,5 @@ const log = std.log.scoped(.main);
|
||||||
test {
|
test {
|
||||||
_ = @import("utils.zig");
|
_ = @import("utils.zig");
|
||||||
_ = @import("Config.zig");
|
_ = @import("Config.zig");
|
||||||
|
_ = @import("globber.zig");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue