feat(core): appstate

This commit is contained in:
2025-05-19 08:48:50 +02:00
parent 4eda4d335b
commit cbeed4003a
14 changed files with 454 additions and 103 deletions

View File

@@ -0,0 +1,4 @@
pub const Action = union(enum) {
GoUp,
Enter,
};

View File

@@ -0,0 +1,17 @@
pub fn handle(action: Action, appState: *AppState) !void {
switch (action) {
.GoUp => {
if (appState.currentTab.currentLocation) |currentLocation| {
const parent = currentLocation.item.parent;
if (parent) |p| {
try appState.currentTab.setCurrentLocation(.{ .path = p.path.path });
} else return error.NoParent;
} else return error.NoCurrentLocation;
},
.Enter => unreachable,
}
}
const Action = @import("action.zig").Action;
const AppState = @import("../app_state.zig").AppState;
const RootProvider = @import("../provider/root.zig").RootProvider;

50
src/core/app_state.zig Normal file
View File

@@ -0,0 +1,50 @@
pub const UnknownTabError = error.UnknownTab;
pub const AppState = struct {
currentTab: *Tab = undefined,
tabs: std.ArrayList(Tab),
currentTabChanged: Observable(*Tab),
tabPreCurrentItemsUnload: Observable(*Tab),
pub fn init(allocator: std.mem.Allocator) AppState {
return .{
.tabs = std.ArrayList(Tab).init(allocator),
.currentTabChanged = Observable(*Tab).init(allocator),
.tabPreCurrentItemsUnload = Observable(*Tab).init(allocator),
};
}
pub fn addTab(self: *AppState, tab: Tab) void {
self.tabs.append(tab);
// tab.preCurrentItemsUnload.attach(.{
// .ctx = @ptrCast(@alignCast(self)),
// .update = preCurrentItemsUnload,
// });
}
pub fn setCurrentTab(self: *AppState, newTab: *Tab) !void {
blk: {
for (self.tabs.items) |item| {
if (item == newTab) break :blk;
}
return UnknownTabError;
}
self.currentTab = newTab;
self.currentTabChanged.notify(newTab);
}
pub fn deinit(self: *AppState) void {
self.currentTabChanged.deinit();
self.tabs.deinit();
}
fn preCurrentItemsUnload(ctx: *anyopaque, tab: *Tab) void {
const self: AppState = @ptrCast(@alignCast(ctx));
self.tabPreCurrentItemsUnload.notify(tab);
}
};
const std = @import("std");
const Tab = @import("tab/tab.zig").Tab;
const Observable = @import("observable.zig").Observable;

View File

@@ -54,18 +54,26 @@ pub const Container = struct {
self.item.allocator.free(itemFullName.path);
}
self.children.deinit();
self.item.allocator.destroy(self);
}
};
pub const FullName = struct {
path: []const u8,
pub fn getChild(self: *FullName, childName: []const u8, allocator: std.mem.Allocator) !FullName {
pub fn getChild(self: *const FullName, childName: []const u8, allocator: std.mem.Allocator) !FullName {
const path = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ self.path, consts.PathSeparator, childName });
return FullName{ .path = path };
}
pub fn getParent(self: *const FullName, allocator: std.mem.Allocator) !?FullName {
const lastIndex = std.mem.lastIndexOf(u8, self.path, consts.PathSeparator) orelse return null;
if (lastIndex == 0) return null;
const path = try std.fmt.allocPrint(allocator, "{s}", .{self.path[0..lastIndex]});
return FullName{ .path = path };
}
};
pub const NativePath = struct {
@@ -73,7 +81,7 @@ pub const NativePath = struct {
};
pub const AbsolutePath = struct {
path: []const u8,
path: FullName,
type: AbsolutePathType,
};
@@ -81,7 +89,7 @@ pub const Error = struct {
content: []const u8,
};
pub const AbsolutePathType = enum {
Container,
Item,
pub const AbsolutePathType = union(enum) {
container,
item,
};

41
src/core/observable.zig Normal file
View File

@@ -0,0 +1,41 @@
pub fn Observable(T: type) type {
return struct {
observers: std.ArrayList(*const Observer(T)),
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.observers = std.ArrayList(*const Observer(T)).init(allocator),
};
}
pub fn deinit(self: *Self) void {
self.observers.deinit();
}
pub fn attach(self: *Self, obs: *const Observer(T)) !void {
try self.observers.append(obs);
}
pub fn detach(self: *Self, obs: *const Observer(T)) void {
if (std.mem.indexOfScalar(*const Observer, self.observers.items, obs)) |index| {
_ = self.observers.swapRemove(index);
}
}
pub fn notify(self: Self, args: T) void {
for (self.observers.items) |obs| {
obs.update(obs.ctx, args);
}
}
};
}
pub fn Observer(T: type) type {
return struct {
ctx: *anyopaque,
update: *const fn (ctx: *anyopaque, args: T) void,
};
}
const std = @import("std");

View File

@@ -1,15 +1,4 @@
const std = @import("std");
const models = @import("../models.zig");
const Provider = @import("provider.zig").Provider;
const ProviderVTable = @import("provider.zig").VTable;
const GetItemsError = @import("provider.zig").GetItemsError;
const InitContext = @import("provider.zig").InitContext;
const FullName = models.FullName;
const Item = models.Item;
const ItemEnum = models.ItemEnum;
const Element = models.Element;
const Container = models.Container;
pub const LocalProviderId = "local";
// TODO: the container might be freed while this runs
// Tab should hold something at pass it here
@@ -18,6 +7,7 @@ fn loadChildren(container: *Container) void {
container.childrenLoading = false;
}
std.debug.print("load children {s}", .{container.item.nativePath.path});
var dir = std.fs.cwd().openDir(container.item.nativePath.path, .{ .iterate = true }) catch {
// 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;
@@ -35,6 +25,8 @@ fn loadChildren(container: *Container) void {
const VTable: ProviderVTable = .{
.getItemByFullName = getItemByFullNameGeneric,
.canHandle = canHandleGeneric,
.deinit = deinitGeneric,
};
pub fn getItemByFullNameGeneric(
@@ -47,6 +39,43 @@ pub fn getItemByFullNameGeneric(
return self.getItemByFullName(fullName, initContext, allocator);
}
pub fn canHandleGeneric(
context: *anyopaque,
fullName: FullName,
) bool {
const self: *LocalContentProvider = @ptrCast(@alignCast(context));
return self.canHandle(fullName);
}
pub fn deinitGeneric(_: *anyopaque) void {}
pub fn getFullNameByNativePath(allocator: std.mem.Allocator, nativePath: NativePath) !FullName {
const correct_sep = try std.mem.replaceOwned(u8, allocator, nativePath.path, std.fs.path.sep_str, "/");
defer allocator.free(correct_sep);
const asd = if (std.mem.startsWith(u8, correct_sep, "/"))
correct_sep[1..]
else
correct_sep;
const full_name_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ LocalProviderId, asd });
return .{ .path = full_name_path };
}
pub fn getNativePathByFullName(allocator: std.mem.Allocator, fullName: FullName) ![]u8 {
const fullNameWithoutId = fullName.path[LocalProviderId.len + 1 ..];
var native_path = try std.mem.replaceOwned(u8, allocator, fullNameWithoutId, "/", std.fs.path.sep_str);
if (native_os == .linux) {
const linux_native_path = try std.fmt.allocPrint(allocator, "/{s}", .{native_path});
allocator.free(native_path);
native_path = linux_native_path;
}
return native_path;
}
pub const LocalContentProvider = struct {
threadPool: *std.Thread.Pool,
@@ -56,7 +85,7 @@ pub const LocalContentProvider = struct {
initContext: *const InitContext,
allocator: std.mem.Allocator,
) GetItemsError!*Item {
const native_path = try std.mem.replaceOwned(u8, allocator, fullName.path, "/", std.fs.path.sep_str);
const native_path = try getNativePathByFullName(allocator, fullName);
defer allocator.free(native_path);
const kind: union(enum) { directory, file } = blk: {
@@ -139,7 +168,10 @@ pub const LocalContentProvider = struct {
const name = try allocator.dupe(u8, basename);
const displayName = try allocator.dupe(u8, basename);
const fullName2 = try allocator.dupe(u8, fullName.path);
const nativePath = try allocator.dupe(u8, fullName.path);
const nativePath = try getNativePathByFullName(allocator, fullName);
const parentFullName = try fullName.getParent(allocator);
const parent = if (parentFullName) |p| models.AbsolutePath{ .path = p, .type = .container } else null;
item.* = Item{
.allocator = allocator,
@@ -148,12 +180,19 @@ pub const LocalContentProvider = struct {
.displayName = displayName,
.fullName = .{ .path = fullName2 },
.nativePath = models.NativePath{ .path = nativePath },
.parent = null,
.parent = parent,
.item = innerItem,
.errors = std.ArrayList(models.Error).init(allocator),
};
}
pub fn canHandle(
_: *LocalContentProvider,
fullName: FullName,
) bool {
return std.mem.startsWith(u8, fullName.path, "local/");
}
pub fn provider(self: *LocalContentProvider) Provider {
return Provider{
.object = self,
@@ -161,3 +200,18 @@ pub const LocalContentProvider = struct {
};
}
};
const std = @import("std");
const models = @import("../models.zig");
const Provider = @import("provider.zig").Provider;
const ProviderVTable = @import("provider.zig").VTable;
const GetItemsError = @import("provider.zig").GetItemsError;
const InitContext = @import("provider.zig").InitContext;
const FullName = models.FullName;
const NativePath = models.NativePath;
const Item = models.Item;
const ItemEnum = models.ItemEnum;
const Element = models.Element;
const Container = models.Container;
const native_os = @import("builtin").os.tag;

View File

@@ -5,6 +5,13 @@ pub const VTable = struct {
initContext: *const InitContext,
allocator: std.mem.Allocator,
) GetItemsError!*Item,
canHandle: *const fn (
self: *anyopaque,
fullName: FullName,
) bool,
deinit: *const fn (self: *anyopaque) void,
};
pub const GetItemsError = error{
@@ -25,6 +32,17 @@ pub const Provider = struct {
) GetItemsError!*Item {
return self.vtable.getItemByFullName(self.object, fullName, initContext, allocator);
}
pub inline fn canHandle(
self: *const Provider,
fullName: FullName,
) bool {
return self.vtable.canHandle(self.object, fullName);
}
pub inline fn deinit(self: *const Provider) void {
return self.vtable.deinit(self.object);
}
};
pub const InitContext = struct {

View File

@@ -0,0 +1,45 @@
pub const RootGetItemsError = GetItemsError || error{ProviderNotFound};
pub const RootProvider = struct {
providers: std.ArrayList(*Provider),
pub fn init(allocator: std.mem.Allocator) RootProvider {
return .{
.providers = std.ArrayList(*Provider).init(allocator),
};
}
pub fn getItemByFullName(
self: *const RootProvider,
fullName: FullName,
initContext: *const InitContext,
allocator: std.mem.Allocator,
) RootGetItemsError!*Item {
const provider = for (self.providers.items) |provider| {
if (provider.canHandle(fullName)) {
break provider;
}
} else return error.ProviderNotFound;
return provider.getItemByFullName(fullName, initContext, allocator);
}
pub fn addProvider(self: *Provider, provider: *Provider) !void {
try self.providers.append(provider);
}
pub fn deinit(
self: *const RootProvider,
) void {
for (self.providers.items) |p| {
p.deinit();
}
self.providers.deinit();
}
};
const std = @import("std");
const Provider = @import("./provider.zig").Provider;
const InitContext = @import("./provider.zig").InitContext;
const GetItemsError = @import("./provider.zig").GetItemsError;
const models = @import("../models.zig");
const FullName = models.FullName;
const Item = models.Item;

View File

@@ -1,19 +1,12 @@
const std = @import("std");
const assert = std.debug.assert;
const Mutex = std.Thread.Mutex;
const models = @import("../models.zig");
const Container = models.Container;
const Item = models.Item;
const locked = @import("../sync.zig").locked;
pub const Tab = struct {
allocator: std.mem.Allocator,
currentLocation: ?*Container,
currentItems: locked(?std.ArrayList(*Item)),
currentLocationChanged: Observable(?*Container),
currentItemsChanged: bool = false,
preCurrentItemsUnload: Observable(*Tab),
threadPool: *std.Thread.Pool,
rootProvider: *RootProvider,
_private: Private,
const Private = struct {
@@ -23,44 +16,67 @@ pub const Tab = struct {
pub fn init(
self: *Tab,
threadPool: *std.Thread.Pool,
rootProvider: *RootProvider,
allocator: std.mem.Allocator,
) void {
self.* = Tab{
.allocator = allocator,
.currentItems = .{ .data = null },
.currentLocationChanged = Observable(?*Container).init(allocator),
.currentLocation = null,
.threadPool = threadPool,
.rootProvider = rootProvider,
.preCurrentItemsUnload = Observable(*Tab).init(allocator),
._private = .{
.currentItemsAllocator = std.heap.ArenaAllocator.init(allocator),
},
};
}
pub fn setCurrentLocation(self: *Tab, newLocation: *Container) void {
pub fn setCurrentLocation(self: *Tab, newLocationFullName: models.FullName) !void {
if (self.currentLocation) |c| {
c.item.deinit();
self.allocator.destroy(c);
}
self.currentLocation = newLocation;
errdefer {
self.currentLocation = null;
self.currentLocationChanged.notify(null);
}
const newLocation = try self.rootProvider.getItemByFullName(newLocationFullName, &.{}, self.allocator);
const newLocationContainer = switch (newLocation.item) {
.container => |c| c,
.element => return error.NotContainer,
};
self.currentLocation = newLocationContainer;
self.currentLocationChanged.notify(newLocationContainer);
//TODO: Proper error handling
std.Thread.Pool.spawn(self.threadPool, loadItemsWrapper, .{ self, newLocation }) catch unreachable;
std.Thread.Pool.spawn(self.threadPool, loadItemsWrapper, .{ self, newLocationContainer }) catch unreachable;
}
fn loadItemsWrapper(self: *Tab, location: *Container) void {
loadItems(self, location) catch return;
}
fn loadItems(self: *Tab, location: *Container) !void {
_ = self._private.currentItemsAllocator.reset(.retain_capacity);
{
self.currentItems.mutex.lock();
defer self.currentItems.mutex.unlock();
self.preCurrentItemsUnload.notify(self);
if (self.currentItems.data) |items| {
items.deinit();
}
self.currentItems.data = null;
}
_ = self._private.currentItemsAllocator.reset(.retain_capacity);
const arenaAllocator = &self._private.currentItemsAllocator;
const arena = arenaAllocator.allocator();
@@ -90,7 +106,7 @@ pub const Tab = struct {
for (location.children.items) |item| {
const resolvedItem = location.item.provider.getItemByFullName(item, &.{ .skipChildInit = true }, allocator) catch |e| {
//TODO: save error to container errors
std.debug.print("error {} {s}\r\n", .{e, item.path});
std.debug.print("error {} {s}\r\n", .{ e, item.path });
continue;
};
@@ -98,6 +114,11 @@ pub const Tab = struct {
defer self.currentItems.mutex.unlock();
try self.currentItems.data.?.append(resolvedItem);
}
{
self.currentItems.mutex.lock();
defer self.currentItems.mutex.unlock();
self.currentItemsChanged = true;
}
}
@@ -109,3 +130,15 @@ pub const Tab = struct {
self._private.currentItemsAllocator.deinit();
}
};
const std = @import("std");
const assert = std.debug.assert;
const Mutex = std.Thread.Mutex;
const models = @import("../models.zig");
const Container = models.Container;
const Item = models.Item;
const locked = @import("../sync.zig").locked;
const Observable = @import("../observable.zig").Observable;
const RootProvider = @import("../provider/root.zig").RootProvider;