Merge 3c90eea992 into 5758e14931
commit
04b7f7b151
|
|
@ -3007,8 +3007,23 @@ fn maybeHandleBinding(
|
|||
}
|
||||
|
||||
// If we have the performable flag and the action was not performed,
|
||||
// then we act as though a binding didn't exist.
|
||||
// execute the else branch if present, otherwise act as though a
|
||||
// binding didn't exist.
|
||||
if (leaf.flags.performable and !performed) {
|
||||
if (leaf.else_actions.len > 0) {
|
||||
for (leaf.else_actions) |else_action| {
|
||||
if (self.performBindingAction(else_action)) |_| {} else |err| {
|
||||
log.info(
|
||||
"else binding action failed action={t} err={}",
|
||||
.{ else_action, err },
|
||||
);
|
||||
}
|
||||
}
|
||||
self.endKeySequence(.drop, .retain);
|
||||
self.keyboard.last_trigger = event.bindingHash();
|
||||
return .consumed;
|
||||
}
|
||||
|
||||
// If we're in a sequence, we treat this as if we pressed a key
|
||||
// that doesn't exist in the sequence. Reset our sequence and flush
|
||||
// any queued events.
|
||||
|
|
|
|||
|
|
@ -7275,9 +7275,11 @@ pub const Keybinds = struct {
|
|||
return;
|
||||
}
|
||||
|
||||
// Chains are only allowed at the root level. Their target is
|
||||
// Chains and else are only allowed at the root level. Their target is
|
||||
// tracked globally by parse order in `self.chain_target`.
|
||||
if (std.mem.startsWith(u8, binding, "chain=")) {
|
||||
if (std.mem.startsWith(u8, binding, "chain=") or
|
||||
std.mem.startsWith(u8, binding, "else="))
|
||||
{
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
|
||||
|
|
@ -7287,7 +7289,9 @@ pub const Keybinds = struct {
|
|||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, value, "chain=")) {
|
||||
if (std.mem.startsWith(u8, value, "chain=") or
|
||||
std.mem.startsWith(u8, value, "else="))
|
||||
{
|
||||
switch (self.chain_target) {
|
||||
.root => try self.set.parseAndPut(alloc, value),
|
||||
.table => |table_name| {
|
||||
|
|
@ -7392,6 +7396,14 @@ pub const Keybinds = struct {
|
|||
a2,
|
||||
)) return false;
|
||||
}
|
||||
if (self_chain.else_actions.items.len != other_chain.else_actions.items.len) return false;
|
||||
for (self_chain.else_actions.items, other_chain.else_actions.items) |a1, a2| {
|
||||
if (!deepEqual(
|
||||
inputpkg.Binding.Action,
|
||||
a1,
|
||||
a2,
|
||||
)) return false;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ pub const Parser = struct {
|
|||
action: Action,
|
||||
flags: Flags = .{},
|
||||
chain: bool,
|
||||
is_else: bool,
|
||||
|
||||
pub const Elem = union(enum) {
|
||||
/// A leader trigger in a sequence.
|
||||
|
|
@ -89,6 +90,11 @@ pub const Parser = struct {
|
|||
/// invalid actions for chains such as `unbind`. We expect downstream
|
||||
/// consumers to validate that the action is valid for chaining.
|
||||
chain: Action,
|
||||
|
||||
/// An else action `else=<action>` that should be used as the
|
||||
/// fallback when a performable binding's action cannot be performed.
|
||||
/// Like chain, any action is parsed and downstream consumers validate.
|
||||
@"else": Action,
|
||||
};
|
||||
|
||||
pub fn init(raw_input: []const u8) Error!Parser {
|
||||
|
|
@ -125,9 +131,10 @@ pub const Parser = struct {
|
|||
return Error.InvalidFormat;
|
||||
};
|
||||
|
||||
// Detect chains. Chains must not have flags.
|
||||
// Detect chains and else. Neither can have flags.
|
||||
const chain = std.mem.eql(u8, input[0..eql_idx], "chain");
|
||||
if (chain and start_idx > 0) return Error.InvalidFormat;
|
||||
const is_else = std.mem.eql(u8, input[0..eql_idx], "else");
|
||||
if ((chain or is_else) and start_idx > 0) return Error.InvalidFormat;
|
||||
|
||||
// Sequence iterator goes up to the equal, action is after. We can
|
||||
// parse the action now.
|
||||
|
|
@ -137,11 +144,12 @@ pub const Parser = struct {
|
|||
// for chained inputs. The `next` will never yield this
|
||||
// because we have chain set. When we find a nicer way to
|
||||
// do this we can remove it, the e2e is tested.
|
||||
.input = if (chain) "a" else input[0..eql_idx],
|
||||
.input = if (chain or is_else) "a" else input[0..eql_idx],
|
||||
},
|
||||
.action = try .parse(input[eql_idx + 1 ..]),
|
||||
.flags = flags,
|
||||
.chain = chain,
|
||||
.is_else = is_else,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -197,8 +205,9 @@ pub const Parser = struct {
|
|||
return .{ .leader = trigger };
|
||||
}
|
||||
|
||||
// If we're a chain then return it as-is.
|
||||
// If we're a chain or else then return it as-is.
|
||||
if (self.chain) return .{ .chain = self.action };
|
||||
if (self.is_else) return .{ .@"else" = self.action };
|
||||
|
||||
// Out of triggers, yield the final action.
|
||||
return .{ .binding = .{
|
||||
|
|
@ -255,6 +264,7 @@ fn parseSingle(raw_input: []const u8) (Error || error{
|
|||
.leader => error.UnexpectedSequence,
|
||||
.binding => elem.binding,
|
||||
.chain => error.UnexpectedChain,
|
||||
.@"else" => error.UnexpectedChain,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2092,6 +2102,8 @@ pub const Set = struct {
|
|||
key_ptr: *Trigger,
|
||||
value_ptr: *Value,
|
||||
set: *Set,
|
||||
/// When true, subsequent chain= appends to the else branch.
|
||||
in_else: bool = false,
|
||||
};
|
||||
|
||||
/// The entry type for the forward mapping of trigger to action.
|
||||
|
|
@ -2169,8 +2181,21 @@ pub const Set = struct {
|
|||
.leaf => |leaf| {
|
||||
// When we get to the leaf, the buffer_stream contains
|
||||
// the full sequence of keys needed to reach this action.
|
||||
const pos = buffer.end;
|
||||
buffer.print("={f}", .{leaf.action}) catch return error.OutOfMemory;
|
||||
try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]);
|
||||
|
||||
// Format else actions if present.
|
||||
for (leaf.else_actions.items, 0..) |else_action, i| {
|
||||
buffer.end = 0;
|
||||
if (i == 0) {
|
||||
buffer.print("else={f}", .{else_action}) catch return error.OutOfMemory;
|
||||
} else {
|
||||
buffer.print("chain={f}", .{else_action}) catch return error.OutOfMemory;
|
||||
}
|
||||
try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]);
|
||||
buffer.end = pos;
|
||||
}
|
||||
},
|
||||
|
||||
.leaf_chained => |leaf| {
|
||||
|
|
@ -2185,6 +2210,18 @@ pub const Set = struct {
|
|||
try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]);
|
||||
buffer.end = pos;
|
||||
}
|
||||
|
||||
// Format else actions if present.
|
||||
for (leaf.else_actions.items, 0..) |else_action, i| {
|
||||
buffer.end = 0;
|
||||
if (i == 0) {
|
||||
buffer.print("else={f}", .{else_action}) catch return error.OutOfMemory;
|
||||
} else {
|
||||
buffer.print("chain={f}", .{else_action}) catch return error.OutOfMemory;
|
||||
}
|
||||
try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]);
|
||||
buffer.end = pos;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -2195,17 +2232,38 @@ pub const Set = struct {
|
|||
pub const Leaf = struct {
|
||||
action: Action,
|
||||
flags: Flags,
|
||||
else_actions: std.ArrayList(Action) = .empty,
|
||||
|
||||
pub fn clone(
|
||||
self: Leaf,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!Leaf {
|
||||
var cloned_else = try self.else_actions.clone(alloc);
|
||||
errdefer cloned_else.deinit(alloc);
|
||||
for (cloned_else.items) |*action| {
|
||||
action.* = try action.clone(alloc);
|
||||
}
|
||||
return .{
|
||||
.action = try self.action.clone(alloc),
|
||||
.flags = self.flags,
|
||||
.else_actions = cloned_else,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Leaf, alloc: Allocator) void {
|
||||
self.else_actions.deinit(alloc);
|
||||
}
|
||||
|
||||
pub fn equal(self: Leaf, other: Leaf) bool {
|
||||
if (self.flags != other.flags) return false;
|
||||
if (!deepEqual(Action, self.action, other.action)) return false;
|
||||
if (self.else_actions.items.len != other.else_actions.items.len) return false;
|
||||
for (self.else_actions.items, other.else_actions.items) |a1, a2| {
|
||||
if (!deepEqual(Action, a1, a2)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn hash(self: Leaf) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
self.action.hash(&hasher);
|
||||
|
|
@ -2217,6 +2275,7 @@ pub const Set = struct {
|
|||
return .{
|
||||
.flags = self.flags,
|
||||
.actions = .{ .single = .{self.action} },
|
||||
.else_actions = self.else_actions.items,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -2225,6 +2284,7 @@ pub const Set = struct {
|
|||
pub const LeafChained = struct {
|
||||
actions: std.ArrayList(Action),
|
||||
flags: Flags,
|
||||
else_actions: std.ArrayList(Action) = .empty,
|
||||
|
||||
pub fn clone(
|
||||
self: LeafChained,
|
||||
|
|
@ -2235,20 +2295,28 @@ pub const Set = struct {
|
|||
for (cloned_actions.items) |*action| {
|
||||
action.* = try action.clone(alloc);
|
||||
}
|
||||
var cloned_else = try self.else_actions.clone(alloc);
|
||||
errdefer cloned_else.deinit(alloc);
|
||||
for (cloned_else.items) |*action| {
|
||||
action.* = try action.clone(alloc);
|
||||
}
|
||||
return .{
|
||||
.actions = cloned_actions,
|
||||
.flags = self.flags,
|
||||
.else_actions = cloned_else,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *LeafChained, alloc: Allocator) void {
|
||||
self.actions.deinit(alloc);
|
||||
self.else_actions.deinit(alloc);
|
||||
}
|
||||
|
||||
pub fn generic(self: *const LeafChained) GenericLeaf {
|
||||
return .{
|
||||
.flags = self.flags,
|
||||
.actions = .{ .many = self.actions.items },
|
||||
.else_actions = self.else_actions.items,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -2261,6 +2329,7 @@ pub const Set = struct {
|
|||
single: [1]Action,
|
||||
many: []const Action,
|
||||
},
|
||||
else_actions: []const Action = &.{},
|
||||
|
||||
pub fn actionsSlice(self: *const GenericLeaf) []const Action {
|
||||
return switch (self.actions) {
|
||||
|
|
@ -2284,7 +2353,7 @@ pub const Set = struct {
|
|||
|
||||
.leaf_chained => |*l| l.deinit(alloc),
|
||||
|
||||
.leaf => {},
|
||||
.leaf => |*l| l.deinit(alloc),
|
||||
};
|
||||
|
||||
self.bindings.deinit(alloc);
|
||||
|
|
@ -2333,6 +2402,12 @@ pub const Set = struct {
|
|||
// If we had an invalid action for a chain (e.g. unbind).
|
||||
error.InvalidChainAction => return error.InvalidFormat,
|
||||
|
||||
// If else was used on a non-performable binding.
|
||||
error.ElseRequiresPerformable => return error.InvalidFormat,
|
||||
|
||||
// If else was used more than once on the same binding.
|
||||
error.DuplicateElse => return error.InvalidFormat,
|
||||
|
||||
// Unrecoverable
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
}
|
||||
|
|
@ -2360,6 +2435,8 @@ pub const Set = struct {
|
|||
NoChainParent,
|
||||
UnexpectedEndOfInput,
|
||||
InvalidChainAction,
|
||||
ElseRequiresPerformable,
|
||||
DuplicateElse,
|
||||
};
|
||||
|
||||
/// Returns the set that was ultimately updated if a binding was
|
||||
|
|
@ -2399,6 +2476,8 @@ pub const Set = struct {
|
|||
error.NoChainParent,
|
||||
error.UnexpectedEndOfInput,
|
||||
error.InvalidChainAction,
|
||||
error.ElseRequiresPerformable,
|
||||
error.DuplicateElse,
|
||||
error.OutOfMemory,
|
||||
=> err,
|
||||
},
|
||||
|
|
@ -2460,6 +2539,8 @@ pub const Set = struct {
|
|||
error.NoChainParent,
|
||||
error.UnexpectedEndOfInput,
|
||||
error.InvalidChainAction,
|
||||
error.ElseRequiresPerformable,
|
||||
error.DuplicateElse,
|
||||
error.OutOfMemory,
|
||||
=> return err,
|
||||
};
|
||||
|
|
@ -2490,6 +2571,15 @@ pub const Set = struct {
|
|||
try set.appendChain(alloc, action);
|
||||
return set;
|
||||
},
|
||||
|
||||
.@"else" => |action| {
|
||||
// Else can only happen on the root.
|
||||
assert(set == root);
|
||||
// Unbind is not valid for else.
|
||||
if (action == .unbind) return error.InvalidChainAction;
|
||||
try set.appendElse(alloc, action);
|
||||
return set;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2546,14 +2636,17 @@ pub const Set = struct {
|
|||
|
||||
// If we have an existing binding for this trigger, we have to
|
||||
// update the reverse mapping to remove the old action.
|
||||
.leaf => if (track_reverse) {
|
||||
const t_hash = t.hash();
|
||||
for (0.., self.reverse.values()) |i, *value| {
|
||||
if (t_hash == value.hash()) {
|
||||
self.reverse.swapRemoveAt(i);
|
||||
break;
|
||||
.leaf => |*leaf| {
|
||||
if (track_reverse) {
|
||||
const t_hash = t.hash();
|
||||
for (0.., self.reverse.values()) |i, *value| {
|
||||
if (t_hash == value.hash()) {
|
||||
self.reverse.swapRemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
leaf.deinit(alloc);
|
||||
},
|
||||
|
||||
// Chained leaves aren't in the reverse mapping so we just
|
||||
|
|
@ -2592,6 +2685,17 @@ pub const Set = struct {
|
|||
assert(action != .unbind);
|
||||
|
||||
const parent = self.chain_parent orelse return error.NoChainParent;
|
||||
|
||||
// If we're in the else branch, append to else_actions instead.
|
||||
if (parent.in_else) {
|
||||
switch (parent.value_ptr.*) {
|
||||
.leader => unreachable,
|
||||
.leaf => |*leaf| try leaf.else_actions.append(alloc, action),
|
||||
.leaf_chained => |*leaf| try leaf.else_actions.append(alloc, action),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (parent.value_ptr.*) {
|
||||
// Leader can never be a chain parent. Verified through various
|
||||
// assertions and unit tests.
|
||||
|
|
@ -2623,6 +2727,7 @@ pub const Set = struct {
|
|||
parent.value_ptr.* = .{ .leaf_chained = .{
|
||||
.actions = actions,
|
||||
.flags = leaf.flags,
|
||||
.else_actions = leaf.else_actions,
|
||||
} };
|
||||
|
||||
// Clean up our reverse mapping. Chained actions are not
|
||||
|
|
@ -2637,6 +2742,44 @@ pub const Set = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Append an else fallback action to the prior set action.
|
||||
///
|
||||
/// The else action is used as a fallback when a performable binding's
|
||||
/// action cannot be performed. It is an error to use else on a
|
||||
/// non-performable binding, or to use else more than once.
|
||||
pub fn appendElse(
|
||||
self: *Set,
|
||||
alloc: Allocator,
|
||||
action: Action,
|
||||
) (Allocator.Error || error{ NoChainParent, ElseRequiresPerformable, DuplicateElse })!void {
|
||||
assert(action != .unbind);
|
||||
|
||||
const parent = &(self.chain_parent orelse return error.NoChainParent);
|
||||
|
||||
// Cannot use else if already in the else branch (duplicate else).
|
||||
if (parent.in_else) return error.DuplicateElse;
|
||||
|
||||
switch (parent.value_ptr.*) {
|
||||
.leader => unreachable,
|
||||
|
||||
.leaf => |*leaf| {
|
||||
if (!leaf.flags.performable) return error.ElseRequiresPerformable;
|
||||
// First else on a leaf — else_actions must be empty.
|
||||
assert(leaf.else_actions.items.len == 0);
|
||||
try leaf.else_actions.append(alloc, action);
|
||||
},
|
||||
|
||||
.leaf_chained => |*leaf| {
|
||||
if (!leaf.flags.performable) return error.ElseRequiresPerformable;
|
||||
assert(leaf.else_actions.items.len == 0);
|
||||
try leaf.else_actions.append(alloc, action);
|
||||
},
|
||||
}
|
||||
|
||||
// Switch to else branch so subsequent chains append there.
|
||||
parent.in_else = true;
|
||||
}
|
||||
|
||||
/// Get a binding for a given trigger.
|
||||
pub fn get(self: Set, t: Trigger) ?Entry {
|
||||
return self.bindings.getEntry(t);
|
||||
|
|
@ -2718,10 +2861,13 @@ pub const Set = struct {
|
|||
},
|
||||
|
||||
// For an action we need to fix up the reverse mapping.
|
||||
.leaf => |leaf| self.fixupReverseForAction(
|
||||
leaf.action,
|
||||
t,
|
||||
),
|
||||
.leaf => |*leaf| {
|
||||
self.fixupReverseForAction(
|
||||
leaf.action,
|
||||
t,
|
||||
);
|
||||
leaf.deinit(alloc);
|
||||
},
|
||||
|
||||
// Chained leaves are never in our reverse mapping so no
|
||||
// cleanup is required.
|
||||
|
|
@ -4880,3 +5026,536 @@ test "set: formatEntries leaf_chained with text action" {
|
|||
;
|
||||
try testing.expectEqualStrings(expected, output.written());
|
||||
}
|
||||
|
||||
test "parse: else" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Valid
|
||||
{
|
||||
var p = try Parser.init("else=new_tab");
|
||||
try testing.expectEqual(Parser.Elem{
|
||||
.@"else" = .new_tab,
|
||||
}, try p.next());
|
||||
try testing.expect(try p.next() == null);
|
||||
}
|
||||
|
||||
// Else can't have flags
|
||||
try testing.expectError(error.InvalidFormat, Parser.init("global:else=ignore"));
|
||||
try testing.expectError(error.InvalidFormat, Parser.init("performable:else=ignore"));
|
||||
}
|
||||
|
||||
test "set: parseAndPut else on performable" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
// Should still be a leaf with else_actions
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
const leaf = entry.leaf;
|
||||
try testing.expect(leaf.action == .new_window);
|
||||
try testing.expect(leaf.flags.performable);
|
||||
try testing.expectEqual(@as(usize, 1), leaf.else_actions.items.len);
|
||||
try testing.expect(leaf.else_actions.items[0] == .new_tab);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: parseAndPut else on non-performable is error" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "a=new_window");
|
||||
try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "else=new_tab"));
|
||||
}
|
||||
|
||||
test "set: parseAndPut else without parent is error" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "else=new_tab"));
|
||||
}
|
||||
|
||||
test "set: parseAndPut duplicate else is error" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "else=close_surface"));
|
||||
}
|
||||
|
||||
test "set: parseAndPut chain then else" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
// performable action, chain on performed branch, then else branch
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf_chained);
|
||||
const chained = entry.leaf_chained;
|
||||
try testing.expect(chained.flags.performable);
|
||||
try testing.expectEqual(@as(usize, 2), chained.actions.items.len);
|
||||
try testing.expect(chained.actions.items[0] == .new_window);
|
||||
try testing.expect(chained.actions.items[1] == .close_surface);
|
||||
try testing.expectEqual(@as(usize, 1), chained.else_actions.items.len);
|
||||
try testing.expect(chained.else_actions.items[0] == .new_tab);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: parseAndPut else then chain appends to else branch" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
// performable action, else branch, chain on else branch
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
const leaf = entry.leaf;
|
||||
try testing.expect(leaf.action == .new_window);
|
||||
try testing.expect(leaf.flags.performable);
|
||||
try testing.expectEqual(@as(usize, 2), leaf.else_actions.items.len);
|
||||
try testing.expect(leaf.else_actions.items[0] == .new_tab);
|
||||
try testing.expect(leaf.else_actions.items[1] == .close_surface);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: parseAndPut else with unbind is error" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "else=unbind"));
|
||||
}
|
||||
|
||||
test "set: parseAndPut chain on both branches" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
// Full config: performed branch with chains, then else branch with chains
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try s.parseAndPut(alloc, "chain=toggle_fullscreen");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf_chained);
|
||||
const chained = entry.leaf_chained;
|
||||
|
||||
// Performed branch: new_window, close_surface
|
||||
try testing.expectEqual(@as(usize, 2), chained.actions.items.len);
|
||||
try testing.expect(chained.actions.items[0] == .new_window);
|
||||
try testing.expect(chained.actions.items[1] == .close_surface);
|
||||
|
||||
// Else branch: new_tab, toggle_fullscreen
|
||||
try testing.expectEqual(@as(usize, 2), chained.else_actions.items.len);
|
||||
try testing.expect(chained.else_actions.items[0] == .new_tab);
|
||||
try testing.expect(chained.else_actions.items[1] == .toggle_fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: clone with else_actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
var cloned = try s.clone(alloc);
|
||||
defer cloned.deinit(alloc);
|
||||
|
||||
const entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
try testing.expectEqual(@as(usize, 1), entry.leaf.else_actions.items.len);
|
||||
try testing.expect(entry.leaf.else_actions.items[0] == .new_tab);
|
||||
}
|
||||
|
||||
test "set: formatEntries with else" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const formatterpkg = @import("../config/formatter.zig");
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?;
|
||||
|
||||
var output: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer output.deinit();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
try entry.key_ptr.format(&writer);
|
||||
try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer));
|
||||
|
||||
// Note: the trigger format does not include flag prefixes like performable:
|
||||
// so the serialized output won't have it. This is an existing limitation.
|
||||
const expected =
|
||||
\\keybind = a=new_window
|
||||
\\keybind = else=new_tab
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output.written());
|
||||
}
|
||||
|
||||
test "set: formatEntries with chain and else" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const formatterpkg = @import("../config/formatter.zig");
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try s.parseAndPut(alloc, "chain=toggle_fullscreen");
|
||||
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?;
|
||||
|
||||
var output: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer output.deinit();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
try entry.key_ptr.format(&writer);
|
||||
try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer));
|
||||
|
||||
const expected =
|
||||
\\keybind = a=new_window
|
||||
\\keybind = chain=close_surface
|
||||
\\keybind = else=new_tab
|
||||
\\keybind = chain=toggle_fullscreen
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output.written());
|
||||
}
|
||||
|
||||
test "set: parseAndPut multiple chains on else branch" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "chain=toggle_fullscreen");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
const leaf = entry.leaf;
|
||||
|
||||
// Primary branch: just new_window (no chains before else)
|
||||
try testing.expect(leaf.action == .new_window);
|
||||
|
||||
// Else branch: new_tab, close_surface, toggle_fullscreen
|
||||
try testing.expectEqual(@as(usize, 3), leaf.else_actions.items.len);
|
||||
try testing.expect(leaf.else_actions.items[0] == .new_tab);
|
||||
try testing.expect(leaf.else_actions.items[1] == .close_surface);
|
||||
try testing.expect(leaf.else_actions.items[2] == .toggle_fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: parseAndPut multiple chains on performed branch before else" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "chain=toggle_fullscreen");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf_chained);
|
||||
const chained = entry.leaf_chained;
|
||||
|
||||
// Performed branch: new_window, close_surface, toggle_fullscreen
|
||||
try testing.expectEqual(@as(usize, 3), chained.actions.items.len);
|
||||
try testing.expect(chained.actions.items[0] == .new_window);
|
||||
try testing.expect(chained.actions.items[1] == .close_surface);
|
||||
try testing.expect(chained.actions.items[2] == .toggle_fullscreen);
|
||||
|
||||
// Else branch: just new_tab
|
||||
try testing.expectEqual(@as(usize, 1), chained.else_actions.items.len);
|
||||
try testing.expect(chained.else_actions.items[0] == .new_tab);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: parseAndPut else with text actions" {
|
||||
const testing = std.testing;
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var s: Set = .{};
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=text:hello");
|
||||
try s.parseAndPut(alloc, "else=text:world");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
try testing.expectEqualStrings("hello", entry.leaf.action.text);
|
||||
try testing.expectEqual(@as(usize, 1), entry.leaf.else_actions.items.len);
|
||||
try testing.expectEqualStrings("world", entry.leaf.else_actions.items[0].text);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: clone with leaf_chained and else_actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try s.parseAndPut(alloc, "chain=toggle_fullscreen");
|
||||
|
||||
var cloned = try s.clone(alloc);
|
||||
defer cloned.deinit(alloc);
|
||||
|
||||
const entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf_chained);
|
||||
|
||||
// Performed branch preserved
|
||||
try testing.expectEqual(@as(usize, 2), entry.leaf_chained.actions.items.len);
|
||||
try testing.expect(entry.leaf_chained.actions.items[0] == .new_window);
|
||||
try testing.expect(entry.leaf_chained.actions.items[1] == .close_surface);
|
||||
|
||||
// Else branch preserved
|
||||
try testing.expectEqual(@as(usize, 2), entry.leaf_chained.else_actions.items.len);
|
||||
try testing.expect(entry.leaf_chained.else_actions.items[0] == .new_tab);
|
||||
try testing.expect(entry.leaf_chained.else_actions.items[1] == .toggle_fullscreen);
|
||||
}
|
||||
|
||||
test "set: clone with text else_actions has independent memory" {
|
||||
const testing = std.testing;
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var s: Set = .{};
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=text:hello");
|
||||
try s.parseAndPut(alloc, "else=text:world");
|
||||
|
||||
const cloned = try s.clone(alloc);
|
||||
|
||||
const orig_entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
const cloned_entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
|
||||
// Verify the cloned else action has the same content
|
||||
try testing.expectEqualStrings("world", cloned_entry.leaf.else_actions.items[0].text);
|
||||
|
||||
// Verify the pointers are different (independent allocation)
|
||||
try testing.expect(orig_entry.leaf.else_actions.items[0].text.ptr !=
|
||||
cloned_entry.leaf.else_actions.items[0].text.ptr);
|
||||
}
|
||||
|
||||
test "set: overwrite binding with else_actions cleans up" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
// Create a binding with else_actions
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
// Overwrite with a new binding — old else_actions must be freed
|
||||
try s.parseAndPut(alloc, "a=close_surface");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
try testing.expect(entry.leaf.action == .close_surface);
|
||||
// New binding should have no else_actions
|
||||
try testing.expectEqual(@as(usize, 0), entry.leaf.else_actions.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: new binding after else resets chain context" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
// First binding with else
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
// New binding should start fresh, not in else mode
|
||||
try s.parseAndPut(alloc, "b=close_surface");
|
||||
try s.parseAndPut(alloc, "chain=toggle_fullscreen");
|
||||
|
||||
{
|
||||
// Verify first binding unchanged
|
||||
const a_entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(a_entry == .leaf);
|
||||
try testing.expectEqual(@as(usize, 1), a_entry.leaf.else_actions.items.len);
|
||||
|
||||
// Verify second binding has chain on primary (not else)
|
||||
const b_entry = s.get(.{ .key = .{ .unicode = 'b' } }).?.value_ptr.*;
|
||||
try testing.expect(b_entry == .leaf_chained);
|
||||
try testing.expectEqual(@as(usize, 2), b_entry.leaf_chained.actions.items.len);
|
||||
try testing.expect(b_entry.leaf_chained.actions.items[0] == .close_surface);
|
||||
try testing.expect(b_entry.leaf_chained.actions.items[1] == .toggle_fullscreen);
|
||||
// No else on second binding
|
||||
try testing.expectEqual(@as(usize, 0), b_entry.leaf_chained.else_actions.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: else after unbind is error" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "a=unbind");
|
||||
|
||||
// chain_parent cleared by unbind, so else should fail
|
||||
try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "else=new_tab"));
|
||||
}
|
||||
|
||||
test "set: generic leaf exposes else_actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
const generic = entry.leaf.generic();
|
||||
|
||||
// Primary actions
|
||||
const actions = generic.actionsSlice();
|
||||
try testing.expectEqual(@as(usize, 1), actions.len);
|
||||
try testing.expect(actions[0] == .new_window);
|
||||
|
||||
// Else actions accessible via generic
|
||||
try testing.expectEqual(@as(usize, 2), generic.else_actions.len);
|
||||
try testing.expect(generic.else_actions[0] == .new_tab);
|
||||
try testing.expect(generic.else_actions[1] == .close_surface);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: generic leaf_chained exposes else_actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
try s.parseAndPut(alloc, "chain=close_surface");
|
||||
try s.parseAndPut(alloc, "else=new_tab");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf_chained);
|
||||
const generic = entry.leaf_chained.generic();
|
||||
|
||||
// Primary actions
|
||||
const actions = generic.actionsSlice();
|
||||
try testing.expectEqual(@as(usize, 2), actions.len);
|
||||
|
||||
// Else actions accessible via generic
|
||||
try testing.expectEqual(@as(usize, 1), generic.else_actions.len);
|
||||
try testing.expect(generic.else_actions[0] == .new_tab);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: performable without else has empty else_actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "performable:a=new_window");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
const generic = entry.leaf.generic();
|
||||
try testing.expectEqual(@as(usize, 0), generic.else_actions.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: non-performable binding has empty else_actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.parseAndPut(alloc, "a=new_window");
|
||||
|
||||
{
|
||||
const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*;
|
||||
try testing.expect(entry == .leaf);
|
||||
try testing.expectEqual(@as(usize, 0), entry.leaf.else_actions.items.len);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue