Create initial window rules set up
At least tag rules seem to be working (but they're not frame perfect). I might need to investigate more for float/no_float. Rules are ANDed and only apply during window's first manage sequence, so changing an appid/title doesn't affect anything.
This commit is contained in:
parent
11cef4b2f8
commit
6b8350e7b6
6 changed files with 410 additions and 25 deletions
|
|
@ -42,10 +42,13 @@ tag_binds: std.ArrayList(Keybind) = .{},
|
|||
keybinds: keybind.Map = .{},
|
||||
pointer_binds: std.ArrayList(PointerBind) = .{},
|
||||
input_configs: std.ArrayList(InputConfig) = .{},
|
||||
window_rules: std.ArrayList(WindowRule) = .{},
|
||||
|
||||
// Re-exports
|
||||
pub const Keybind = keybind.Keybind;
|
||||
pub const PointerBind = pointer_bind.PointerBind;
|
||||
pub const WindowRule = window_rule.Rule;
|
||||
pub const WindowRuleAction = window_rule.Action;
|
||||
|
||||
pub const AttachMode = enum {
|
||||
top,
|
||||
|
|
@ -66,6 +69,7 @@ const NodeName = enum {
|
|||
keybinds,
|
||||
pointer_binds,
|
||||
tag_overlay,
|
||||
window_rules,
|
||||
};
|
||||
|
||||
pub fn create() !*Config {
|
||||
|
|
@ -103,6 +107,11 @@ pub fn create() !*Config {
|
|||
if (ic.name) |name| utils.gpa.free(name);
|
||||
}
|
||||
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 (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);
|
||||
}
|
||||
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 (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| {
|
||||
config.focus_follows_pointer = focus_follows_pointer;
|
||||
logDebugSettingNode(name, focus_follows_pointer_str);
|
||||
} else {
|
||||
} else |_| {
|
||||
logWarnInvalidNodeArg(name, focus_follows_pointer_str);
|
||||
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| {
|
||||
config.pointer_warp_on_focus_change = pointer_warp_on_focus_change;
|
||||
logDebugSettingNode(name, pointer_warp_on_focus_change_str);
|
||||
} else {
|
||||
} else |_| {
|
||||
logWarnInvalidNodeArg(name, pointer_warp_on_focus_change_str);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -238,8 +252,6 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
|||
};
|
||||
logDebugSettingNode(name, path_str);
|
||||
},
|
||||
.bar => next_child_block = .bar,
|
||||
.borders => next_child_block = .borders,
|
||||
.input => {
|
||||
pending_input_name = if (node.prop(&parser, "name")) |n|
|
||||
try utils.gpa.dupe(u8, utils.stripQuotes(n))
|
||||
|
|
@ -247,15 +259,13 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
|||
null;
|
||||
next_child_block = .input;
|
||||
},
|
||||
.keybinds => {
|
||||
next_child_block = .keybinds;
|
||||
},
|
||||
.pointer_binds => {
|
||||
next_child_block = .pointer_binds;
|
||||
},
|
||||
.tag_overlay => {
|
||||
next_child_block = .tag_overlay;
|
||||
},
|
||||
inline .bar,
|
||||
.borders,
|
||||
.keybinds,
|
||||
.pointer_binds,
|
||||
.tag_overlay,
|
||||
.window_rules,
|
||||
=> |n| next_child_block = n,
|
||||
}
|
||||
} else {
|
||||
helpers.logWarnInvalidNode(node.name);
|
||||
|
|
@ -273,6 +283,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
|||
pending_input_name = null; // ownership transferred
|
||||
},
|
||||
.tag_overlay => try TagOverlayConfig.load(config, &parser, hostname),
|
||||
.window_rules => try window_rule.load(config, &parser, hostname),
|
||||
else => {
|
||||
// Nothing else should ever be marked as a next_child_block
|
||||
unreachable;
|
||||
|
|
@ -322,6 +333,7 @@ const border = @import("config/border.zig");
|
|||
const helpers = @import("config/helpers.zig");
|
||||
const keybind = @import("config/keybind.zig");
|
||||
const pointer_bind = @import("config/pointer_bind.zig");
|
||||
const window_rule = @import("config/window_rule.zig");
|
||||
const BarConfig = @import("config/BarConfig.zig");
|
||||
const InputConfig = @import("config/InputConfig.zig");
|
||||
const TagOverlayConfig = @import("config/TagOverlayConfig.zig");
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ context: *Context,
|
|||
river_window_v1: *river.WindowV1,
|
||||
river_node_v1: *river.NodeV1,
|
||||
|
||||
app_id: ?[]const u8 = null,
|
||||
title: ?[]const u8 = null,
|
||||
|
||||
rect: utils.Rect = .{},
|
||||
|
||||
fullscreen: bool = false,
|
||||
|
|
@ -80,8 +83,11 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Out
|
|||
}
|
||||
|
||||
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_window_v1.destroy();
|
||||
|
||||
utils.gpa.destroy(window);
|
||||
}
|
||||
|
|
@ -139,7 +145,18 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
|||
window.pending_manage.dimensions = .{ .width = @intCast(ev.width), .height = @intCast(ev.height) };
|
||||
},
|
||||
.dimensions_hint => {
|
||||
// TODO: Maybe could use this for floating windows
|
||||
// TODO: Use this for clamping windows on resize
|
||||
},
|
||||
.app_id => |ev| {
|
||||
if (window.app_id) |app_id| utils.gpa.free(app_id);
|
||||
window.app_id = utils.gpa.dupe(u8, std.mem.span(ev.app_id.?)) catch @panic("Out of memory");
|
||||
},
|
||||
.title => |ev| {
|
||||
if (window.title) |title| utils.gpa.free(title);
|
||||
window.title = utils.gpa.dupe(u8, std.mem.span(ev.title.?)) catch @panic("Out of memory");
|
||||
},
|
||||
.parent => {
|
||||
// TODO: float this window directly over its parent
|
||||
},
|
||||
else => |ev| {
|
||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||
|
|
@ -160,6 +177,13 @@ pub fn manage(window: *Window) void {
|
|||
|
||||
river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = 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
|
||||
else
|
||||
window.pending_manage.floating = false;
|
||||
}
|
||||
|
||||
// Updating state since the last manage event
|
||||
|
|
@ -280,6 +304,37 @@ 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);
|
||||
}
|
||||
|
||||
// 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 assert = std.debug.assert;
|
||||
const DoublyLinkedList = std.DoublyLinkedList;
|
||||
|
|
@ -288,9 +343,11 @@ const wayland = @import("wayland");
|
|||
const wl = wayland.client.wl;
|
||||
const river = wayland.client.river;
|
||||
|
||||
const globber = @import("globber.zig");
|
||||
const utils = @import("utils.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const Seat = @import("Seat.zig");
|
||||
const WindowRule = @import("Config.zig").WindowRule;
|
||||
|
||||
const log = std.log.scoped(.Window);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/// if arg_str in ["#true", "true"], return true
|
||||
/// if arg_str in ["#false", "false"], return false
|
||||
/// 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
|
||||
mem.eql(u8, arg_str, "true"))
|
||||
{
|
||||
|
|
@ -17,7 +17,7 @@ pub fn boolFromKdlStr(arg_str: []const u8) ?bool {
|
|||
{
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
return error.NotABool;
|
||||
}
|
||||
|
||||
pub fn parseButton(s: []const u8) ?u32 {
|
||||
|
|
@ -96,16 +96,16 @@ const testing = std.testing;
|
|||
|
||||
test "boolFromKdlStr" {
|
||||
// True valid
|
||||
try testing.expectEqual(@as(?bool, true), boolFromKdlStr("#true"));
|
||||
try testing.expectEqual(@as(?bool, true), boolFromKdlStr("true"));
|
||||
try testing.expectEqual(true, try boolFromKdlStr("#true"));
|
||||
try testing.expectEqual(true, try boolFromKdlStr("true"));
|
||||
// False valid
|
||||
try testing.expectEqual(@as(?bool, false), boolFromKdlStr("#false"));
|
||||
try testing.expectEqual(@as(?bool, false), boolFromKdlStr("false"));
|
||||
try testing.expectEqual(false, try boolFromKdlStr("#false"));
|
||||
try testing.expectEqual(false, try boolFromKdlStr("false"));
|
||||
// Invalid
|
||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("yes"));
|
||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("1"));
|
||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr(""));
|
||||
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("TRUE"));
|
||||
try testing.expectError(error.NotABool, boolFromKdlStr("yes"));
|
||||
try testing.expectError(error.NotABool, boolFromKdlStr("1"));
|
||||
try testing.expectError(error.NotABool, boolFromKdlStr(""));
|
||||
try testing.expectError(error.NotABool, boolFromKdlStr("TRUE"));
|
||||
}
|
||||
|
||||
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 {
|
||||
_ = @import("utils.zig");
|
||||
_ = @import("Config.zig");
|
||||
_ = @import("globber.zig");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue