// zwanzig-disable: file-as-struct // Pure-function module keeps the PascalCase name per project convention. // SPDX-FileCopyrightText: © 2026 mtmn // // SPDX-License-Identifier: BSD-3-Clause 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; }, } } /// Return true if text is matched by glob. The glob must be valid, /// see validate(). pub fn match(text: []const u8, glob: []const u8) bool { if (glob.len == 1) { return glob[0] == '*' or mem.eql(u8, text, glob); } const suffix_match = glob[0] == '*'; const prefix_match = glob[glob.len - 1] == '*'; if (suffix_match and prefix_match) { return mem.indexOf(u8, text, glob[1 .. glob.len - 1]) != null; } else if (suffix_match) { return mem.endsWith(u8, text, glob[1..]); } else if (prefix_match) { return mem.startsWith(u8, text, glob[0 .. glob.len - 1]); } else { return mem.eql(u8, text, glob); } } pub const Action = enum { float, fullscreen, csd, tags, }; pub const Rule = struct { app_id_glob: []const u8, title_glob: []const u8, action: Action, tags: u32 = 0, }; test validate { const testing = std.testing; try validate("*"); try validate("a"); try validate("*a"); try validate("a*"); try validate("*a*"); try validate("ab"); 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("**a")); try testing.expectError(error.InvalidGlob, validate("abc**")); } 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", "*ab*")); try testing.expect(match("ab", "*b")); try testing.expect(match("ab", "a*")); try testing.expect(!match("ab", "b*")); try testing.expect(!match("ab", "*a")); try testing.expect(!match("ab", "ac")); try testing.expect(!match("ab", "*ac*")); } test "rule matches app id and title together" { const testing = std.testing; try testing.expect(match("float-term", "float*")); try testing.expect(!match("other", "float*")); try testing.expect(match("foo", "*")); }