const std = @import("std"); const testing = std.testing; const act = @import("action"); const input = @import("input"); const layout = @import("layout.zig"); const Box = layout.Box; comptime { _ = act; _ = input; _ = @import("color.zig"); } const area: Box = .{ .x = 0, .y = 0, .width = 1000, .height = 600 }; fn params(nmaster: u32, mfact: f32) layout.Params { return .{ .area = area, .nmaster = nmaster, .mfact = mfact }; } // ─── master/stack tiling ───────────────────────────────────────────────────── test "single window fills the whole area" { var cells: [1]Box = undefined; _ = layout.arrange(.master, params(1, 0.55), &cells); try testing.expectEqual(area, cells[0]); } test "two windows split at mfact" { var cells: [2]Box = undefined; _ = layout.arrange(.master, params(1, 0.55), &cells); try testing.expectEqual(@as(i32, 550), cells[0].width); try testing.expectEqual(@as(i32, 600), cells[0].height); try testing.expectEqual(@as(i32, 550), cells[1].x); try testing.expectEqual(@as(i32, 450), cells[1].width); try testing.expectEqual(@as(i32, 600), cells[1].height); } test "stack column divides height with no gap or overlap" { // Three windows, one master: the stack column holds two. var cells: [3]Box = undefined; _ = layout.arrange(.master, params(1, 0.5), &cells); try testing.expectEqual(@as(i32, 600), cells[0].height); try testing.expectEqual(cells[1].y + cells[1].height, cells[2].y); try testing.expectEqual(area.height, cells[1].height + cells[2].height); } test "odd heights are absorbed rather than leaving a gap at the bottom" { // 600 / 7 does not divide evenly; the last window must still end exactly // at the bottom edge. var cells: [7]Box = undefined; _ = layout.arrange(.master, params(0, 0.55), &cells); var y: i32 = area.y; for (cells) |cell| { try testing.expectEqual(y, cell.y); y += cell.height; } try testing.expectEqual(area.y + area.height, y); } test "nmaster zero puts every window in the stack column" { var cells: [3]Box = undefined; _ = layout.arrange(.master, params(0, 0.55), &cells); for (cells) |cell| { try testing.expectEqual(@as(i32, 0), cell.x); try testing.expectEqual(area.width, cell.width); } } test "windows all fit in master when count does not exceed nmaster" { var cells: [2]Box = undefined; _ = layout.arrange(.master, params(3, 0.55), &cells); // No stack column, so master spans the full width. for (cells) |cell| { try testing.expectEqual(area.width, cell.width); } try testing.expectEqual(area.height, cells[0].height + cells[1].height); } test "master column also divides height when nmaster exceeds one" { var cells: [4]Box = undefined; _ = layout.arrange(.master, params(2, 0.5), &cells); try testing.expectEqual(@as(i32, 500), cells[0].width); try testing.expectEqual(@as(i32, 500), cells[1].width); try testing.expectEqual(area.height, cells[0].height + cells[1].height); try testing.expectEqual(area.height, cells[2].height + cells[3].height); } // ─── stacking layouts ──────────────────────────────────────────────────────── test "monocle gives every window the full area and reports stacked" { var cells: [3]Box = undefined; const result = layout.arrange(.monocle, params(1, 0.55), &cells); try testing.expect(result.stacked); try testing.expect(result.tabbar == null); for (cells) |cell| try testing.expectEqual(area, cell); } test "master does not report stacked" { var cells: [2]Box = undefined; var p = params(1, 0.55); p.tabbar_height = 22; try testing.expect(!layout.arrange(.master, p, &cells).stacked); } test "tabbed reserves the bar strip above the windows" { var cells: [3]Box = undefined; var p = params(1, 0.55); p.tabbar_height = 22; const result = layout.arrange(.tabbed, p, &cells); try testing.expect(result.stacked); const bar = result.tabbar.?; try testing.expectEqual(@as(i32, 22), bar.height); try testing.expectEqual(area.y, bar.y); // Windows start below the bar and the two together cover the area exactly. for (cells) |cell| { try testing.expectEqual(area.y + 22, cell.y); try testing.expectEqual(area.height - 22, cell.height); } } test "tabbed drops the bar rather than crushing the windows" { var cells: [2]Box = undefined; var p: layout.Params = .{ .area = .{ .x = 0, .y = 0, .width = 400, .height = 30 }, .nmaster = 1, .mfact = 0.55, .tabbar_height = 22, }; const result = layout.arrange(.tabbed, p, &cells); try testing.expect(result.tabbar == null); try testing.expectEqual(@as(i32, 30), cells[0].height); p.tabbar_height = 0; } test "no windows is not a crash" { var cells: [0]Box = undefined; const result = layout.arrange(.master, params(1, 0.55), &cells); try testing.expect(result.tabbar == null); } // ─── gaps ──────────────────────────────────────────────────────────────────── test "outer gap insets the whole area" { var cells: [1]Box = undefined; var p = params(1, 0.55); p.outer_gap = 10; _ = layout.arrange(.master, p, &cells); try testing.expectEqual(@as(i32, 10), cells[0].x); try testing.expectEqual(@as(i32, 10), cells[0].y); try testing.expectEqual(@as(i32, 980), cells[0].width); try testing.expectEqual(@as(i32, 580), cells[0].height); } test "inner gap separates adjacent windows" { var cells: [2]Box = undefined; var p = params(1, 0.5); p.gap = 10; _ = layout.arrange(.master, p, &cells); // Each cell shrinks by half the gap per side, leaving a full gap between. const right_of_master = cells[0].x + cells[0].width; try testing.expect(cells[1].x - right_of_master == 10); } // ─── tab rectangles ────────────────────────────────────────────────────────── test "tab rects tile the bar exactly with no rounding gap" { const bar: Box = .{ .x = 5, .y = 0, .width = 101, .height = 22 }; var rects: [4]Box = undefined; layout.tabRects(bar, 4, &rects); try testing.expectEqual(bar.x, rects[0].x); var total: i32 = 0; for (rects, 0..) |rect, i| { total += rect.width; if (i > 0) { try testing.expectEqual(rects[i - 1].x + rects[i - 1].width, rect.x); } } try testing.expectEqual(bar.width, total); try testing.expectEqual(bar.x + bar.width, rects[3].x + rects[3].width); } test "single tab spans the bar" { const bar: Box = .{ .x = 0, .y = 0, .width = 300, .height = 22 }; var rects: [1]Box = undefined; layout.tabRects(bar, 1, &rects); try testing.expectEqual(@as(i32, 300), rects[0].width); } // ─── Box helpers ───────────────────────────────────────────────────────────── test "inset never produces negative dimensions" { const tiny: Box = .{ .x = 0, .y = 0, .width = 4, .height = 4 }; const r = tiny.inset(10); try testing.expectEqual(@as(i32, 0), r.width); try testing.expectEqual(@as(i32, 0), r.height); } test "contains is half open on the far edges" { const b: Box = .{ .x = 10, .y = 10, .width = 100, .height = 50 }; try testing.expect(b.contains(10, 10)); try testing.expect(b.contains(109, 59)); try testing.expect(!b.contains(110, 30)); try testing.expect(!b.contains(9, 30)); } // ─── per-tag settings slots ────────────────────────────────────────────────── test "each single tag gets its own settings slot" { for (0..act.tag_count) |i| { const mask = @as(u32, 1) << @intCast(i); try testing.expectEqual(i + 1, act.tagSlot(mask)); } } test "views of more than one tag share the shared slot" { try testing.expectEqual(@as(usize, 0), act.tagSlot(0b11)); try testing.expectEqual(@as(usize, 0), act.tagSlot(0b101)); try testing.expectEqual(@as(usize, 0), act.tagSlot(act.all_tags)); } test "an empty view falls back to the shared slot" { try testing.expectEqual(@as(usize, 0), act.tagSlot(0)); } test "bits above the tag range do not affect the slot" { // A single valid tag stays on its own slot even with junk in the high bits, // so a mask that survived a sloppy IPC caller cannot index past the array. const junk: u32 = ~act.all_tags; try testing.expectEqual(@as(usize, 1), act.tagSlot(0b1 | junk)); try testing.expectEqual(act.tag_count, act.tagSlot(@as(u32, 1) << (act.tag_count - 1))); } // ─── command parsing ───────────────────────────────────────────────────────── fn parseOk(argv: []const []const u8) act.Action { return act.parse(argv) catch unreachable; } test "tag arguments accept indices, masks and all" { try testing.expectEqual(@as(u32, 1), parseOk(&.{ "view", "1" }).view); try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "3" }).view); try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "0x4" }).view); try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "mask:4" }).view); try testing.expectEqual(act.all_tags, parseOk(&.{ "view", "all" }).view); } test "tag indices outside the range are rejected" { try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "0" })); try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "10" })); try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "nope" })); try testing.expectError(error.MissingArgument, act.parse(&.{"view"})); } test "masks are clamped to the valid tag range" { try testing.expectEqual(act.all_tags, parseOk(&.{ "view", "0xffffffff" }).view); } test "deltas distinguish relative from absolute" { switch (parseOk(&.{ "mfact", "+0.05" }).mfact) { .relative => |v| try testing.expectApproxEqAbs(@as(f32, 0.05), v, 1e-6), .absolute => return error.TestUnexpectedResult, } switch (parseOk(&.{ "mfact", "0.5" }).mfact) { .absolute => |v| try testing.expectApproxEqAbs(@as(f32, 0.5), v, 1e-6), .relative => return error.TestUnexpectedResult, } switch (parseOk(&.{ "nmaster", "-1" }).nmaster) { .relative => |v| try testing.expectEqual(@as(i32, -1), v), .absolute => return error.TestUnexpectedResult, } } test "delta application respects relative and absolute" { const rel = act.Delta(i32){ .relative = -1 }; const abs = act.Delta(i32){ .absolute = 3 }; try testing.expectEqual(@as(i32, 4), rel.apply(5)); try testing.expectEqual(@as(i32, 3), abs.apply(5)); } test "unknown commands are rejected rather than guessed at" { try testing.expectError(error.UnknownCommand, act.parse(&.{"nonsense"})); try testing.expectError(error.UnknownCommand, act.parse(&.{})); } test "spawn keeps its whole argv" { const a = parseOk(&.{ "spawn", "foot", "-e", "htop" }); try testing.expectEqual(@as(usize, 3), a.spawn.len); try testing.expectEqualStrings("htop", a.spawn[2]); try testing.expectError(error.MissingArgument, act.parse(&.{"spawn"})); } test "directions parse both spellings" { try testing.expectEqual(act.Direction.next, parseOk(&.{ "focus", "next" }).focus); try testing.expectEqual(act.Direction.prev, parseOk(&.{ "focus", "prev" }).focus); try testing.expectEqual(act.Direction.prev, parseOk(&.{ "focus", "previous" }).focus); try testing.expectError(error.InvalidArgument, act.parse(&.{ "focus", "sideways" })); } test "window commands keep the identifier verbatim" { try testing.expectEqualStrings("w-17", parseOk(&.{ "focus-window", "w-17" }).focus_window); try testing.expectEqualStrings("w-17", parseOk(&.{ "close-window", "w-17" }).close_window); // An identifier is opaque, so a direction-looking one is still an id. try testing.expectEqualStrings("next", parseOk(&.{ "focus-window", "next" }).focus_window); try testing.expectError(error.MissingArgument, act.parse(&.{"focus-window"})); try testing.expectError(error.InvalidArgument, act.parse(&.{ "focus-window", "" })); } test "layouts parse by name" { try testing.expectEqual(act.Layout.monocle, parseOk(&.{ "layout", "monocle" }).set_layout); try testing.expectEqual(act.Layout.tabbed, parseOk(&.{ "layout", "tabbed" }).set_layout); try testing.expectError(error.InvalidArgument, act.parse(&.{ "layout", "spiral" })); } test "only navigation-style actions repeat on key hold" { try testing.expect(parseOk(&.{ "focus", "next" }).repeats()); try testing.expect(parseOk(&.{ "mfact", "+0.05" }).repeats()); try testing.expect(!parseOk(&.{"zoom"}).repeats()); try testing.expect(!parseOk(&.{"close"}).repeats()); try testing.expect(!parseOk(&.{ "view", "1" }).repeats()); } // ─── input device rules ────────────────────────────────────────────────────── test "device name globs match the way a config author expects" { const touchpad = "ELAN0501:00 04F3:3060 Touchpad"; try testing.expect(input.matches("*Touchpad*", touchpad)); try testing.expect(input.matches("*Touchpad", touchpad)); try testing.expect(input.matches("ELAN*", touchpad)); try testing.expect(input.matches("*", touchpad)); try testing.expect(input.matches(touchpad, touchpad)); try testing.expect(!input.matches("*Trackpoint*", touchpad)); try testing.expect(!input.matches("Touchpad", touchpad)); // A literal pattern must match the whole name, not merely a prefix. try testing.expect(!input.matches("ELAN0501", touchpad)); } test "globs handle empty runs and repeated stars" { try testing.expect(input.matches("", "")); try testing.expect(input.matches("*", "")); try testing.expect(input.matches("***", "")); try testing.expect(!input.matches("a", "")); // The backtracking case: each star has to be willing to give ground. try testing.expect(input.matches("*a*b*c*", "xxaxxbxxcxx")); try testing.expect(!input.matches("*a*b*c*", "xxaxxcxxbxx")); // Only the last 'a' lets the rest of the pattern through. try testing.expect(input.matches("*aab", "aaab")); } test "rules match on name and type independently" { const rule: input.Rule = .{ .name = "*Touchpad*", .type = .pointer, .tap = true }; try testing.expect(rule.matchesDevice("Foo Touchpad", .pointer)); // Right name, wrong kind of device. try testing.expect(!rule.matchesDevice("Foo Touchpad", .touch)); try testing.expect(!rule.matchesDevice("Foo Keyboard", .pointer)); // A rule with neither selector applies to everything. const catch_all: input.Rule = .{ .natural_scroll = true }; try testing.expect(catch_all.matchesDevice("anything", .tablet)); try testing.expect(catch_all.matchesDevice("", .keyboard)); } test "later rules override earlier ones field by field" { const broad: input.Rule = .{ .name = "*", .tap = true, .natural_scroll = true }; const narrow: input.Rule = .{ .name = "*Touchpad*", .tap = false, .click_method = .clickfinger }; const merged = (input.Rule{}).merge(broad).merge(narrow); // Stated twice: the later rule wins. try testing.expectEqual(@as(?bool, false), merged.tap); // Stated only by the broad rule: survives. try testing.expectEqual(@as(?bool, true), merged.natural_scroll); // Stated only by the narrow rule: applied. try testing.expectEqual(@as(?input.ClickMethod, .clickfinger), merged.click_method); // Stated by neither: still null, so the device keeps libinput's default. try testing.expectEqual(@as(?bool, null), merged.middle_emulation); } test "merging leaves the selectors alone" { // Otherwise a merged rule would claim to be about whichever device matched // last, which is not a thing anything should be able to read back out. const merged = (input.Rule{ .name = "a", .type = .pointer }) .merge(.{ .name = "b", .type = .keyboard, .tap = true }); try testing.expectEqualStrings("a", merged.name.?); try testing.expectEqual(@as(?input.Type, .pointer), merged.type); try testing.expectEqual(@as(?bool, true), merged.tap); } test "an unset keymap is recognised as the default" { try testing.expect((input.Keymap{}).isDefault()); try testing.expect(!(input.Keymap{ .layout = "us" }).isDefault()); try testing.expect(!(input.Keymap{ .options = "caps:escape" }).isDefault()); } test "map_to_output merges like any other setting" { // It is a string rather than a scalar, so worth pinning that the generic // merge handles it and that a rule silent about it does not clear it. const merged = (input.Rule{}) .merge(.{ .type = .touch, .map_to_output = "eDP-1" }) .merge(.{ .name = "*", .tap = true }); try testing.expectEqualStrings("eDP-1", merged.map_to_output.?); // And that a later rule naming a different output does win. const moved = merged.merge(.{ .map_to_output = "DP-2" }); try testing.expectEqualStrings("DP-2", moved.map_to_output.?); }