feat(console): lazy load items

This commit is contained in:
2025-04-24 17:51:33 +02:00
parent 693167bd1d
commit c23ee52f05
3 changed files with 121 additions and 41 deletions

View File

@@ -5,15 +5,19 @@ const vxfw = vaxis.vxfw;
const models = @import("../core/models.zig"); const models = @import("../core/models.zig");
const provider = @import("../core/provider/provider.zig"); const provider = @import("../core/provider/provider.zig");
const local_provider = @import("../core/provider/local.zig"); const local_provider = @import("../core/provider/local.zig");
const tab = @import("../core/tab/tab.zig"); const Tab = @import("../core/tab/tab.zig").Tab;
const Tab = tab.Tab; const locked = @import("../core/sync.zig").locked;
/// Our main application state /// Our main application state
const Model = struct { const Model = struct {
current_items: vxfw.ListView, usage_number: locked(u16) = .{ .data = 0 },
running: bool = true,
allocator: std.mem.Allocator,
current_items: *vxfw.ListView,
current_items_allocator: ?std.heap.ArenaAllocator = null,
tab: *Tab, tab: *Tab,
/// State of the counter /// State of the counter
count: u32 = 0, count: usize = 0,
/// The button. This widget is stateful and must live between frames /// The button. This widget is stateful and must live between frames
button: vxfw.Button, button: vxfw.Button,
@@ -28,12 +32,12 @@ const Model = struct {
/// This function will be called from the vxfw runtime. /// This function will be called from the vxfw runtime.
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
const self: *Model = @ptrCast(@alignCast(ptr)); const vm: *Model = @ptrCast(@alignCast(ptr));
switch (event) { switch (event) {
// The root widget is always sent an init event as the first event. Users of the // 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 // library can also send this event to other widgets they create if they need to do
// some initialization. // some initialization.
.init => return ctx.requestFocus(self.current_items.widget()), .init => return ctx.requestFocus(vm.current_items.widget()),
.key_press => |key| { .key_press => |key| {
if (key.matches('c', .{ .ctrl = true })) { if (key.matches('c', .{ .ctrl = true })) {
ctx.quit = true; ctx.quit = true;
@@ -44,7 +48,7 @@ const Model = struct {
// our button. Having focus means that key events will be sent up the widget tree to // 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 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 // the runtime the event was handled and the capture or bubble phase will stop
.focus_in => return ctx.requestFocus(self.current_items.widget()), .focus_in => return ctx.requestFocus(vm.current_items.widget()),
else => {}, else => {},
} }
} }
@@ -55,6 +59,7 @@ const Model = struct {
/// which don't change state (ie mouse motion, unhandled key events, etc) /// 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 { fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
const vm: *Model = @ptrCast(@alignCast(ptr)); const vm: *Model = @ptrCast(@alignCast(ptr));
// The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum // 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 // 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 // maximum constraint can have null width and/or height - meaning there is no constraint in
@@ -129,6 +134,59 @@ const Model = struct {
} }
}; };
fn updateChildren(vm: *Model) !void {
if (vm.current_items_allocator) |a| {
a.deinit();
}
vm.current_items_allocator = std.heap.ArenaAllocator.init(vm.allocator);
const arena_allocator = vm.current_items_allocator.?.allocator();
const tab = vm.tab;
var listView = vm.current_items;
tab.currentItems.mutex.lock();
defer tab.currentItems.mutex.unlock();
const children = if (tab.currentItems.data) |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 &.{};
listView.children.slice = children;
}
fn loop1(vm: *Model) void {
vm.usage_number.data += 1;
while (vm.running) {
inner_loop(vm) catch {};
std.Thread.sleep(2000 * std.time.ns_per_ms);
}
vm.usage_number.data -= 1;
}
fn inner_loop(vm: *Model) !void {
if (vm.tab.currentItemsChanged) {
std.Thread.sleep(2000 * std.time.ns_per_ms);
try updateChildren(vm);
vm.tab.currentItemsChanged = false;
}
vm.tab.currentItems.mutex.lock();
defer vm.tab.currentItems.mutex.unlock();
vm.count = if (vm.tab.currentItems.data) |c| c.items.len else 999;
}
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){}; var gpa = std.heap.DebugAllocator(.{}){};
const gp_allocator = gpa.allocator(); const gp_allocator = gpa.allocator();
@@ -148,9 +206,6 @@ pub fn main() !void {
var localContentProvider = local_provider.LocalContentProvider{ .threadPool = &pool }; 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 homeFullName: models.FullName = .{ .path = "/home/adam" };
const homeItem = try localContentProvider.getItemByFullName(homeFullName, &.{}, allocator); const homeItem = try localContentProvider.getItemByFullName(homeFullName, &.{}, allocator);
// defer homeItem.deinit(); // defer homeItem.deinit();
@@ -167,33 +222,17 @@ pub fn main() !void {
tab1.setCurrentLocation(c); 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); const model = try allocator.create(Model);
defer allocator.destroy(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); 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); const list = try allocator.create(vxfw.ListView);
text_c.* = vxfw.Text{ .text = text1, .overflow = .clip, .softwrap = false, .style = .{ .bold = true } }; list.* = .{
children[i] = text_c.widget(); .children = .{ .slice = &.{} },
}
break :blk children;
} else &.{};
const list: vxfw.ListView = .{
.children = .{ .slice = children1 },
}; };
// Set the initial state of our button
model.* = .{ model.* = .{
.allocator = allocator,
.current_items = list, .current_items = list,
.tab = tab1, .tab = tab1,
.count = 0, .count = 0,
@@ -204,6 +243,24 @@ pub fn main() !void {
}, },
}; };
model.usage_number.data += 1;
try pool.spawn(loop1, .{model});
var app = try vxfw.App.init(allocator);
defer app.deinit();
try app.run(model.widget(), .{}); try app.run(model.widget(), .{});
std.Thread.sleep(1 * std.time.ns_per_s); // std.Thread.sleep(10 * std.time.ns_per_s);
model.usage_number.data -= 1;
model.running = false;
while (model.usage_number.data > 0) {
std.Thread.sleep(100 * std.time.ns_per_ms);
}
if (model.current_items_allocator) |a| {
a.deinit();
}
} }

7
src/core/sync.zig Normal file
View File

@@ -0,0 +1,7 @@
const std = @import("std");
pub fn locked(comptime T: type) type {
return struct {
data: T,
mutex: std.Thread.Mutex = .{},
};
}

View File

@@ -6,10 +6,12 @@ const models = @import("../models.zig");
const Container = models.Container; const Container = models.Container;
const Item = models.Item; const Item = models.Item;
const locked = @import("../sync.zig").locked;
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: locked(?std.ArrayList(*Item)),
currentItemsChanged: bool = false, currentItemsChanged: bool = false,
threadPool: *std.Thread.Pool, threadPool: *std.Thread.Pool,
_private: Private, _private: Private,
@@ -25,7 +27,7 @@ pub const Tab = struct {
) void { ) void {
self.* = Tab{ self.* = Tab{
.allocator = allocator, .allocator = allocator,
.currentItems = null, .currentItems = .{ .data = null },
.currentLocation = null, .currentLocation = null,
.threadPool = threadPool, .threadPool = threadPool,
._private = .{ ._private = .{
@@ -52,15 +54,20 @@ pub const Tab = struct {
if (self._private.currentItemsAllocator) |arena| { if (self._private.currentItemsAllocator) |arena| {
arena.deinit(); arena.deinit();
} }
if (self.currentItems) |items| {
items.deinit(); {
self.currentItems.mutex.lock();
defer self.currentItems.mutex.unlock();
if (self.currentItems.data) |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 arenaAllocator = &self._private.currentItemsAllocator.?;
const arena = arenaAllocator.allocator(); const arena = arenaAllocator.allocator();
var threadSafeAllocator = std.heap.ThreadSafeAllocator{.child_allocator = arena}; var threadSafeAllocator = std.heap.ThreadSafeAllocator{ .child_allocator = arena };
const allocator = threadSafeAllocator.allocator(); const allocator = threadSafeAllocator.allocator();
errdefer { errdefer {
@@ -68,10 +75,15 @@ pub const Tab = struct {
self._private.currentItemsAllocator = null; self._private.currentItemsAllocator = null;
} }
self.currentItems = std.ArrayList(*Item).init(allocator); {
errdefer { self.currentItems.mutex.lock();
self.currentItems.?.deinit(); defer self.currentItems.mutex.unlock();
self.currentItems = null;
self.currentItems.data = std.ArrayList(*Item).init(allocator);
errdefer {
self.currentItems.data.?.deinit();
self.currentItems.data = null;
}
} }
while (location.childrenLoading) { while (location.childrenLoading) {
@@ -80,7 +92,11 @@ pub const Tab = struct {
for (location.children.items) |item| { for (location.children.items) |item| {
const resolvedItem = try location.item.provider.getItemByFullName(item, &.{ .skipChildInit = false }, allocator); const resolvedItem = try location.item.provider.getItemByFullName(item, &.{ .skipChildInit = false }, allocator);
try self.currentItems.?.append(resolvedItem);
self.currentItems.mutex.lock();
defer self.currentItems.mutex.unlock();
try self.currentItems.data.?.append(resolvedItem);
self.currentItemsChanged = true; self.currentItemsChanged = true;
} }
} }