feat(console+core): tui, core

This commit is contained in:
2025-04-24 14:49:18 +02:00
parent 8a39c36aa8
commit 693167bd1d
9 changed files with 305 additions and 188 deletions

View File

@@ -1,75 +1,43 @@
const std = @import("std"); const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
// We will also create a module for our other entry point, 'main.zig'. const console_exe_mod = b.createModule(.{
const exe_mod = b.createModule(.{ .root_source_file = b.path("src/console_main.zig"),
// `root_source_file` is the Zig "entry point" of the module. If a module .target = target,
// only contains e.g. external object files, you can make this `null`. .optimize = optimize,
// In this case the main source file is merely a path, however, in more });
// complicated build scripts, this could be a generated file. const console_exe = b.addExecutable(.{
.root_source_file = b.path("src/main.zig"), .name = "FileTime3",
.root_module = console_exe_mod,
});
const vaxis = b.dependency("vaxis", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
// This creates another `std.Build.Step.Compile`, but this one builds an executable console_exe.root_module.addImport("vaxis", vaxis.module("vaxis"));
// rather than a static library. b.installArtifact(console_exe);
const exe = b.addExecutable(.{
.name = "FileTime3",
.root_module = exe_mod,
});
// This declares intent for the executable to be installed into the const run_console_cmd = b.addRunArtifact(console_exe);
// standard location when the user invokes the "install" step (the default run_console_cmd.step.dependOn(b.getInstallStep());
// step when running `zig build`).
b.installArtifact(exe);
// This *creates* a Run step in the build graph, to be executed when another
// step is evaluated that depends on it. The next line below will establish
// such a dependency.
const run_cmd = b.addRunArtifact(exe);
// By making the run step depend on the install step, it will be run from the
// installation directory rather than directly from within the cache directory.
// This is not necessary, however, if the application depends on other installed
// files, this ensures they will be present and in the expected location.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| { if (b.args) |args| {
run_cmd.addArgs(args); run_console_cmd.addArgs(args);
} }
// This creates a build step. It will be visible in the `zig build --help` menu, const run_console_step = b.step("run", "Run the console app");
// and can be selected like this: `zig build run` run_console_step.dependOn(&run_console_cmd.step);
// This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_unit_tests = b.addTest(.{ const console_unit_tests = b.addTest(.{
.root_module = exe_mod, .root_module = console_exe_mod,
}); });
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); const run_console_unit_tests = b.addRunArtifact(console_unit_tests);
// Similar to creating the run step earlier, this exposes a `test` step to const test_step = b.step("test", "Run unit console tests");
// the `zig build --help` menu, providing a way for the user to request test_step.dependOn(&run_console_unit_tests.step);
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
} }

View File

@@ -36,45 +36,11 @@
// Once all dependencies are fetched, `zig build` no longer requires // Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity. // internet connectivity.
.dependencies = .{ .dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies. .vaxis = .{
//.example = .{ .url = "git+https://github.com/rockorager/libvaxis.git#ae71b6545c099e73d45df7e5dd7c7a3081839468",
// // When updating this field to a new URL, be sure to delete the corresponding .hash = "vaxis-0.1.0-BWNV_JEMCQBskZQsnlzh6GoyHSDgOi41bCoZIB2pW-E7",
// // `hash`, otherwise you are communicating that you expect to find the old hash at },
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
}, },
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{ .paths = .{
"build.zig", "build.zig",
"build.zig.zon", "build.zig.zon",

209
src/console/main.zig Normal file
View File

@@ -0,0 +1,209 @@
const std = @import("std");
const vaxis = @import("vaxis");
const vxfw = vaxis.vxfw;
const models = @import("../core/models.zig");
const provider = @import("../core/provider/provider.zig");
const local_provider = @import("../core/provider/local.zig");
const tab = @import("../core/tab/tab.zig");
const Tab = tab.Tab;
/// Our main application state
const Model = struct {
current_items: vxfw.ListView,
tab: *Tab,
/// State of the counter
count: u32 = 0,
/// The button. This widget is stateful and must live between frames
button: vxfw.Button,
/// Helper function to return a vxfw.Widget struct
pub fn widget(self: *Model) vxfw.Widget {
return .{
.userdata = self,
.eventHandler = Model.typeErasedEventHandler,
.drawFn = Model.typeErasedDrawFn,
};
}
/// This function will be called from the vxfw runtime.
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
const self: *Model = @ptrCast(@alignCast(ptr));
switch (event) {
// The root widget is always sent an init event as the first event. Users of the
// library can also send this event to other widgets they create if they need to do
// some initialization.
.init => return ctx.requestFocus(self.current_items.widget()),
.key_press => |key| {
if (key.matches('c', .{ .ctrl = true })) {
ctx.quit = true;
return;
}
},
// We can request a specific widget gets focus. In this case, we always want to focus
// our button. Having focus means that key events will be sent up the widget tree to
// the focused widget, and then bubble back down the tree to the root. Users can tell
// the runtime the event was handled and the capture or bubble phase will stop
.focus_in => return ctx.requestFocus(self.current_items.widget()),
else => {},
}
}
/// This function is called from the vxfw runtime. It will be called on a regular interval, and
/// only when any event handler has marked the redraw flag in EventContext as true. By
/// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
/// which don't change state (ie mouse motion, unhandled key events, etc)
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
const vm: *Model = @ptrCast(@alignCast(ptr));
// The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
// constraint. The minimum constraint will always be set, even if it is set to 0x0. The
// maximum constraint can have null width and/or height - meaning there is no constraint in
// that direction and the widget should take up as much space as it needs. By calling size()
// on the max, we assert that it has some constrained size. This is *always* the case for
// the root widget - the maximum size will always be the size of the terminal screen.
const max_size = ctx.max.size();
// The DrawContext also contains an arena allocator that can be used for each frame. The
// lifetime of this allocation is until the next time we draw a frame. This is useful for
// temporary allocations such as the one below: we have an integer we want to print as text.
// We can safely allocate this with the ctx arena since we only need it for this frame.
const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{vm.count});
const text: vxfw.Text = .{ .text = count_text };
// Each widget returns a Surface from it's draw function. A Surface contains the rectangular
// area of the widget, as well as some information about the surface or widget: can we focus
// it? does it handle the mouse?
//
// It DOES NOT contain the location it should be within it's parent. Only the parent can set
// this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
// has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
// with an offset and a z-index - the offset can be negative. This lets a parent draw a
// child and place it within itself
const text_child: vxfw.SubSurface = .{
.origin = .{ .row = 0, .col = 0 },
.surface = try text.draw(ctx),
};
const button_child: vxfw.SubSurface = .{
.origin = .{ .row = 2, .col = 0 },
.surface = try vm.button.draw(ctx.withConstraints(
ctx.min,
// Here we explicitly set a new maximum size constraint for the Button. A Button will
// expand to fill it's area and must have some hard limit in the maximum constraint
.{ .width = 16, .height = 3 },
)),
};
const list_surface: vxfw.SubSurface = .{
.origin = .{ .row = 12, .col = 30 },
.surface = try vm.current_items.widget().draw(ctx.withConstraints(ctx.min, .{ .width = 30, .height = 10 })),
};
// We also can use our arena to allocate the slice for our SubSurfaces. This slice only
// needs to live until the next frame, making this safe.
const children = try ctx.arena.alloc(vxfw.SubSurface, 3);
children[0] = text_child;
children[1] = button_child;
// children[2] = splitView_surface;
children[2] = list_surface;
return .{
// A Surface must have a size. Our root widget is the size of the screen
.size = max_size,
.widget = vm.widget(),
// We didn't actually need to draw anything for the root. In this case, we can set
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
// assert that it's length is equal to the size.width * size.height.
.buffer = &.{},
.children = children,
};
}
/// The onClick callback for our button. This is also called if we press enter while the button
/// has focus
fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
const ptr = maybe_ptr orelse return;
const self: *Model = @ptrCast(@alignCast(ptr));
self.count +|= 1;
return ctx.consumeAndRedraw();
}
};
pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
const gp_allocator = gpa.allocator();
defer {
_ = gpa.detectLeaks();
_ = gpa.deinit();
}
var tsa = std.heap.ThreadSafeAllocator{ .child_allocator = gp_allocator };
const allocator = tsa.allocator();
var pool: std.Thread.Pool = undefined;
try pool.init(.{
.allocator = allocator,
});
defer pool.deinit();
var localContentProvider = local_provider.LocalContentProvider{ .threadPool = &pool };
var app = try vxfw.App.init(allocator);
defer app.deinit();
const homeFullName: models.FullName = .{ .path = "/home/adam" };
const homeItem = try localContentProvider.getItemByFullName(homeFullName, &.{}, allocator);
// defer homeItem.deinit();
const c = switch (homeItem.item) {
.container => |c| c,
.element => unreachable,
};
var tab1 = try allocator.create(Tab);
defer allocator.destroy(tab1);
tab1.init(&pool, allocator);
defer tab1.deinit();
tab1.setCurrentLocation(c);
// We heap allocate our model because we will require a stable pointer to it in our Button
// widget
const model = try allocator.create(Model);
defer allocator.destroy(model);
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const arena_allocator = arena.allocator();
std.Thread.sleep(1 * std.time.ns_per_s);
const children1 = if (tab1.currentItems) |items| blk: {
const children = try arena_allocator.alloc(vxfw.Widget, items.items.len);
for (0.., items.items[0..children.len]) |i, child| {
const text1 = try arena_allocator.dupe(u8, child.fullName.path);
const text_c = try arena_allocator.create(vxfw.Text);
text_c.* = vxfw.Text{ .text = text1, .overflow = .clip, .softwrap = false, .style = .{ .bold = true } };
children[i] = text_c.widget();
}
break :blk children;
} else &.{};
const list: vxfw.ListView = .{
.children = .{ .slice = children1 },
};
// Set the initial state of our button
model.* = .{
.current_items = list,
.tab = tab1,
.count = 0,
.button = .{
.label = "Click me!",
.onClick = Model.onClick,
.userdata = model,
},
};
try app.run(model.widget(), .{});
std.Thread.sleep(1 * std.time.ns_per_s);
}

5
src/console_main.zig Normal file
View File

@@ -0,0 +1,5 @@
const run = @import("console/main.zig").main;
pub fn main() !void {
try run();
}

View File

@@ -1,42 +0,0 @@
const std = @import("std");
const Mutex = std.Thread.Mutex;
const Error = error{
Deinitialized,
};
pub const DeinitTracker = struct {
allocator: std.mem.Allocator,
deinited: bool,
usageCount: u16,
mutex: Mutex,
pub fn init(self: *DeinitTracker, allocator: std.mem.Allocator) void {
self.* = .{
.allocator = allocator,
.deinited = false,
.usageCount = 1,
.mutex = Mutex{},
};
}
pub fn use(self: *DeinitTracker) Error!void {
self.mutex.lock();
defer self.mutex.unlock();
if (self.deinited) return Error.Deinitialized;
self.usageCount += 1;
}
pub fn unuse(self: *DeinitTracker) void {
self.mutex.lock();
defer self.mutex.unlock();
self.usageCount -= 1;
if (self.usageCount == 0) {
self.allocator.destroy(self);
}
}
};

View File

@@ -1,12 +1,10 @@
const std = @import("std"); const std = @import("std");
const Provider = @import("provider/provider.zig").Provider; const Provider = @import("provider/provider.zig").Provider;
const DeinitTracker = @import("deinit.zig").DeinitTracker;
const consts = @import("consts.zig"); const consts = @import("consts.zig");
pub const Item = struct { pub const Item = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
provider: Provider, provider: Provider,
deinitTracker: *DeinitTracker,
name: []const u8, name: []const u8,
displayName: []const u8, displayName: []const u8,
fullName: FullName, fullName: FullName,
@@ -20,8 +18,6 @@ pub const Item = struct {
self.allocator.free(self.displayName); self.allocator.free(self.displayName);
self.allocator.free(self.fullName.path); self.allocator.free(self.fullName.path);
self.allocator.free(self.nativePath.path); self.allocator.free(self.nativePath.path);
self.deinitTracker.deinited = true;
self.deinitTracker.unuse();
for (self.errors.items) |e| { for (self.errors.items) |e| {
self.allocator.free(e.content); self.allocator.free(e.content);
} }

View File

@@ -3,7 +3,7 @@ const models = @import("../models.zig");
const Provider = @import("provider.zig").Provider; const Provider = @import("provider.zig").Provider;
const ProviderVTable = @import("provider.zig").VTable; const ProviderVTable = @import("provider.zig").VTable;
const GetItemsError = @import("provider.zig").GetItemsError; const GetItemsError = @import("provider.zig").GetItemsError;
const DeinitTracker = @import("../deinit.zig").DeinitTracker; const InitContext = @import("provider.zig").InitContext;
const FullName = models.FullName; const FullName = models.FullName;
const Item = models.Item; const Item = models.Item;
@@ -11,21 +11,14 @@ const ItemEnum = models.ItemEnum;
const Element = models.Element; const Element = models.Element;
const Container = models.Container; const Container = models.Container;
fn loadChildren(container: *Container, deinitTracker: *DeinitTracker) void { // TODO: the container might be freed while this runs
// Tab should hold something at pass it here
fn loadChildren(container: *Container) void {
defer { defer {
if (!deinitTracker.deinited) container.childrenLoading = false;
container.childrenLoading = false;
} }
deinitTracker.use() catch {
std.debug.print("already deinitialized", .{});
return;
};
defer deinitTracker.unuse();
var dir = std.fs.cwd().openDir(container.item.nativePath.path, .{ .iterate = true }) catch { var dir = std.fs.cwd().openDir(container.item.nativePath.path, .{ .iterate = true }) catch {
if (deinitTracker.deinited) return;
const errorContent = std.fmt.allocPrint(container.item.allocator, "Could not open directory '{s}'.", .{container.item.nativePath.path}) catch return; const errorContent = std.fmt.allocPrint(container.item.allocator, "Could not open directory '{s}'.", .{container.item.nativePath.path}) catch return;
container.item.errors.append(.{ .content = errorContent }) catch return; container.item.errors.append(.{ .content = errorContent }) catch return;
return; return;
@@ -36,7 +29,6 @@ fn loadChildren(container: *Container, deinitTracker: *DeinitTracker) void {
while (it.next() catch return) |entry| { while (it.next() catch return) |entry| {
const child = container.item.fullName.getChild(entry.name, container.item.allocator) catch return; const child = container.item.fullName.getChild(entry.name, container.item.allocator) catch return;
if (deinitTracker.deinited) return;
container.children.append(child) catch return; container.children.append(child) catch return;
} }
} }
@@ -48,11 +40,11 @@ const VTable: ProviderVTable = .{
pub fn getItemByFullNameGeneric( pub fn getItemByFullNameGeneric(
context: *anyopaque, context: *anyopaque,
fullName: FullName, fullName: FullName,
initContext: *const InitContext,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
globalAllocator: std.mem.Allocator, ) GetItemsError!*Item {
) GetItemsError!Item {
const self: *LocalContentProvider = @ptrCast(@alignCast(context)); const self: *LocalContentProvider = @ptrCast(@alignCast(context));
return self.getItemByFullName(fullName, allocator, globalAllocator); return self.getItemByFullName(fullName, initContext, allocator);
} }
pub const LocalContentProvider = struct { pub const LocalContentProvider = struct {
@@ -61,9 +53,9 @@ pub const LocalContentProvider = struct {
pub fn getItemByFullName( pub fn getItemByFullName(
self: *LocalContentProvider, self: *LocalContentProvider,
fullName: FullName, fullName: FullName,
initContext: *const InitContext,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
globalAllocator: std.mem.Allocator, ) GetItemsError!*Item {
) GetItemsError!Item {
const stat = std.fs.cwd().statFile(fullName.path) catch return GetItemsError.NotExists; const stat = std.fs.cwd().statFile(fullName.path) catch return GetItemsError.NotExists;
return switch (stat.kind) { return switch (stat.kind) {
@@ -84,12 +76,15 @@ pub const LocalContentProvider = struct {
val, val,
fullName, fullName,
allocator, allocator,
globalAllocator,
); );
try self.threadPool.spawn(loadChildren, .{ container, container.item.deinitTracker }); if (!initContext.skipChildInit) {
try self.threadPool.spawn(loadChildren, .{container});
} else {
container.childrenLoading = false;
}
break :blk container.item; break :blk &container.item;
}, },
.file => blk: { .file => blk: {
const element = try allocator.create(Element); const element = try allocator.create(Element);
@@ -107,10 +102,9 @@ pub const LocalContentProvider = struct {
val, val,
fullName, fullName,
allocator, allocator,
globalAllocator,
); );
break :blk element.item; break :blk &element.item;
}, },
else => @panic( else => @panic(
"Unsupported file type\n", "Unsupported file type\n",
@@ -124,29 +118,17 @@ pub const LocalContentProvider = struct {
innerItem: ItemEnum, innerItem: ItemEnum,
fullName: FullName, fullName: FullName,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
globalAllocator: std.mem.Allocator,
) !void { ) !void {
var deinitTracker = try globalAllocator.create(DeinitTracker);
deinitTracker.init(globalAllocator);
const basename = std.fs.path.basename(fullName.path); const basename = std.fs.path.basename(fullName.path);
const name = try allocator.alloc(u8, basename.len); const name = try allocator.dupe(u8, basename);
@memcpy(name, basename); const displayName = try allocator.dupe(u8, basename);
const fullName2 = try allocator.dupe(u8, fullName.path);
const displayName = try allocator.alloc(u8, basename.len); const nativePath = try allocator.dupe(u8, fullName.path);
@memcpy(displayName, basename);
const fullName2 = try allocator.alloc(u8, fullName.path.len);
@memcpy(fullName2, fullName.path);
const nativePath = try allocator.alloc(u8, fullName.path.len);
@memcpy(nativePath, fullName.path);
item.* = Item{ item.* = Item{
.allocator = allocator, .allocator = allocator,
.provider = contentProvider.provider(), .provider = contentProvider.provider(),
.deinitTracker = deinitTracker,
.name = name, .name = name,
.displayName = displayName, .displayName = displayName,
.fullName = .{ .path = fullName2 }, .fullName = .{ .path = fullName2 },

View File

@@ -2,9 +2,9 @@ pub const VTable = struct {
getItemByFullName: *const fn ( getItemByFullName: *const fn (
self: *anyopaque, self: *anyopaque,
fullName: FullName, fullName: FullName,
initContext: *const InitContext,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
globalAllocator: std.mem.Allocator, ) GetItemsError!*Item,
) GetItemsError!Item,
}; };
pub const GetItemsError = error{ pub const GetItemsError = error{
@@ -19,13 +19,17 @@ pub const Provider = struct {
pub inline fn getItemByFullName( pub inline fn getItemByFullName(
self: *const Provider, self: *const Provider,
fullName: FullName, fullName: FullName,
initContext: *const InitContext,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
globalAllocator: std.mem.Allocator, ) GetItemsError!*Item {
) GetItemsError!Item { return self.vtable.getItemByFullName(self.object, fullName, initContext, allocator);
return &self.vtable.getItemByFullName(self.object, fullName, allocator, globalAllocator);
} }
}; };
pub const InitContext = struct {
skipChildInit: bool = false,
};
const std = @import("std"); const std = @import("std");
const models = @import("../models.zig"); const models = @import("../models.zig");
const Item = models.Item; const Item = models.Item;

View File

@@ -9,7 +9,8 @@ const Item = models.Item;
pub const Tab = struct { pub const Tab = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
currentLocation: ?*Container, currentLocation: ?*Container,
currentItems: ?std.ArrayList(Item), currentItems: ?std.ArrayList(*Item),
currentItemsChanged: bool = false,
threadPool: *std.Thread.Pool, threadPool: *std.Thread.Pool,
_private: Private, _private: Private,
@@ -41,17 +42,46 @@ pub const Tab = struct {
self.currentLocation = newLocation; self.currentLocation = newLocation;
//TODO: Proper error handling //TODO: Proper error handling
std.Thread.Pool.spawn(self.threadPool, loadItems, .{ self, newLocation }) catch unreachable; std.Thread.Pool.spawn(self.threadPool, loadItemsWrapper, .{ self, newLocation }) catch unreachable;
} }
fn loadItems(self: *Tab, location: *Container) void { fn loadItemsWrapper(self: *Tab, location: *Container) void {
loadItems(self, location) catch return;
}
fn loadItems(self: *Tab, location: *Container) !void {
if (self._private.currentItemsAllocator) |arena| { if (self._private.currentItemsAllocator) |arena| {
arena.deinit(); arena.deinit();
} }
if (self.currentItems) |items| {
items.deinit();
}
self._private.currentItemsAllocator = std.heap.ArenaAllocator.init(self.allocator); self._private.currentItemsAllocator = std.heap.ArenaAllocator.init(self.allocator);
const arenaAllocator = &self._private.currentItemsAllocator.?;
const arena = arenaAllocator.allocator();
var threadSafeAllocator = std.heap.ThreadSafeAllocator{.child_allocator = arena};
const allocator = threadSafeAllocator.allocator();
errdefer {
arenaAllocator.deinit();
self._private.currentItemsAllocator = null;
}
self.currentItems = std.ArrayList(*Item).init(allocator);
errdefer {
self.currentItems.?.deinit();
self.currentItems = null;
}
while (location.childrenLoading) {
std.Thread.sleep(1 * std.time.ns_per_ms);
}
for (location.children.items) |item| { for (location.children.items) |item| {
_ = item; const resolvedItem = try location.item.provider.getItemByFullName(item, &.{ .skipChildInit = false }, allocator);
try self.currentItems.?.append(resolvedItem);
self.currentItemsChanged = true;
} }
} }
@@ -62,6 +92,5 @@ pub const Tab = struct {
if (self._private.currentItemsAllocator) |arena| { if (self._private.currentItemsAllocator) |arena| {
arena.deinit(); arena.deinit();
} }
self.allocator.destroy(self);
} }
}; };