Add generic session panel stack
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Generic transient panel stack state, independent of rendering and concrete panel types.
|
||||
// req: ui/001, ui/002, session/003, testing/001, testing/002, testing/003, testing/004
|
||||
|
||||
test {
|
||||
_ = Stack;
|
||||
}
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPanelTitle,
|
||||
NoPanelOpen,
|
||||
};
|
||||
|
||||
pub const Panel = struct {
|
||||
title: []u8,
|
||||
};
|
||||
|
||||
pub const State = struct {
|
||||
depth: usize,
|
||||
active_index: ?usize,
|
||||
active_title: ?[]const u8,
|
||||
};
|
||||
|
||||
pub const Stack = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
panels: std.ArrayList(Panel) = .empty,
|
||||
active_index: ?usize = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Stack {
|
||||
return .{ .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Stack) void {
|
||||
for (self.panels.items) |panel| self.allocator.free(panel.title);
|
||||
self.panels.deinit(self.allocator);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub fn open(self: *Stack, title: []const u8) !void {
|
||||
try validateTitle(title);
|
||||
const owned = try self.allocator.dupe(u8, title);
|
||||
errdefer self.allocator.free(owned);
|
||||
try self.panels.append(self.allocator, .{ .title = owned });
|
||||
self.active_index = self.panels.items.len - 1;
|
||||
}
|
||||
|
||||
pub fn closeActive(self: *Stack) !void {
|
||||
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||
const removed = self.panels.orderedRemove(index);
|
||||
self.allocator.free(removed.title);
|
||||
if (self.panels.items.len == 0) {
|
||||
self.active_index = null;
|
||||
} else if (index >= self.panels.items.len) {
|
||||
self.active_index = self.panels.items.len - 1;
|
||||
} else {
|
||||
self.active_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(self: *Stack) !void {
|
||||
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||
self.active_index = (index + 1) % self.panels.items.len;
|
||||
}
|
||||
|
||||
pub fn previous(self: *Stack) !void {
|
||||
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||
self.active_index = if (index == 0) self.panels.items.len - 1 else index - 1;
|
||||
}
|
||||
|
||||
pub fn state(self: *const Stack) State {
|
||||
const index = self.active_index;
|
||||
return .{
|
||||
.depth = self.panels.items.len,
|
||||
.active_index = index,
|
||||
.active_title = if (index) |i| self.panels.items[i].title else null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pathAlloc(self: *const Stack, allocator: std.mem.Allocator) ![]u8 {
|
||||
if (self.panels.items.len == 0) return allocator.dupe(u8, "-");
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
for (self.panels.items, 0..) |panel, index| {
|
||||
if (index > 0) try out.append(allocator, '>');
|
||||
if (self.active_index != null and self.active_index.? == index) {
|
||||
try out.append(allocator, '[');
|
||||
try out.appendSlice(allocator, panel.title);
|
||||
try out.append(allocator, ']');
|
||||
} else {
|
||||
try out.appendSlice(allocator, panel.title);
|
||||
}
|
||||
}
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
fn validateTitle(title: []const u8) !void {
|
||||
if (title.len == 0) return Error.InvalidPanelTitle;
|
||||
if (!std.unicode.utf8ValidateSlice(title)) return Error.InvalidPanelTitle;
|
||||
for (title) |byte| {
|
||||
if (byte <= 0x20 or byte == '>' or byte == '[' or byte == ']') return Error.InvalidPanelTitle;
|
||||
}
|
||||
}
|
||||
|
||||
test "regular: opens nested panels and tracks active path" {
|
||||
var stack = Stack.init(std.testing.allocator);
|
||||
defer stack.deinit();
|
||||
|
||||
try stack.open("files");
|
||||
try stack.open("diagnostics");
|
||||
|
||||
const state = stack.state();
|
||||
try std.testing.expectEqual(@as(usize, 2), state.depth);
|
||||
try std.testing.expectEqual(@as(?usize, 1), state.active_index);
|
||||
try std.testing.expectEqualStrings("diagnostics", state.active_title.?);
|
||||
|
||||
const path = try stack.pathAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(path);
|
||||
try std.testing.expectEqualStrings("files>[diagnostics]", path);
|
||||
}
|
||||
|
||||
test "regular: next previous and close preserve stack order" {
|
||||
var stack = Stack.init(std.testing.allocator);
|
||||
defer stack.deinit();
|
||||
|
||||
try stack.open("files");
|
||||
try stack.open("git");
|
||||
try stack.open("jobs");
|
||||
try stack.previous();
|
||||
try std.testing.expectEqualStrings("git", stack.state().active_title.?);
|
||||
try stack.next();
|
||||
try std.testing.expectEqualStrings("jobs", stack.state().active_title.?);
|
||||
try stack.closeActive();
|
||||
try std.testing.expectEqualStrings("git", stack.state().active_title.?);
|
||||
|
||||
const path = try stack.pathAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(path);
|
||||
try std.testing.expectEqualStrings("files>[git]", path);
|
||||
}
|
||||
|
||||
test "adversarial: empty stack operations fail clearly" {
|
||||
var stack = Stack.init(std.testing.allocator);
|
||||
defer stack.deinit();
|
||||
|
||||
try std.testing.expectError(Error.NoPanelOpen, stack.closeActive());
|
||||
try std.testing.expectError(Error.NoPanelOpen, stack.next());
|
||||
try std.testing.expectError(Error.NoPanelOpen, stack.previous());
|
||||
|
||||
const path = try stack.pathAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(path);
|
||||
try std.testing.expectEqualStrings("-", path);
|
||||
}
|
||||
|
||||
test "adversarial: invalid panel titles are rejected" {
|
||||
var stack = Stack.init(std.testing.allocator);
|
||||
defer stack.deinit();
|
||||
|
||||
try std.testing.expectError(Error.InvalidPanelTitle, stack.open(""));
|
||||
try std.testing.expectError(Error.InvalidPanelTitle, stack.open("two words"));
|
||||
try std.testing.expectError(Error.InvalidPanelTitle, stack.open("bad>path"));
|
||||
const bad_utf8 = [_]u8{ 0xc3, 0x28 };
|
||||
try std.testing.expectError(Error.InvalidPanelTitle, stack.open(&bad_utf8));
|
||||
try std.testing.expectEqual(@as(usize, 0), stack.state().depth);
|
||||
}
|
||||
Reference in New Issue
Block a user