diff --git a/src/app_common/Model.zig b/src/app_common/Model.zig index c46574b..8886238 100644 --- a/src/app_common/Model.zig +++ b/src/app_common/Model.zig @@ -1,6 +1,5 @@ running: bool = true, usage_number: locked(u16) = .{ .data = 0 }, -current_items: locked(?[]Arc(*models.Item).Weak) = .{ .data = null }, allocator: std.mem.Allocator, appState: AppState, @@ -12,66 +11,8 @@ pub fn init(model: *Self, allocator: std.mem.Allocator, appState: AppState) !voi } -pub fn updateCurrentItems(self: *Self, tab_current_items: *std.ArrayList(Arc(*models.Item))) !void { - self.resetCurrentItems(); - - var items = try self.allocator.alloc(Arc(*models.Item).Weak, tab_current_items.items.len); - errdefer { - self.allocator.free(items); - self.current_items.data = null; - } - for (tab_current_items.items, 0..) |*item, i| { - items[i] = item.downgrade(); - } - - std.mem.sort(Arc(*models.Item).Weak, items, {}, struct { - fn sort(_: void, lhs_weak: Arc(*models.Item).Weak, rhs_weak: Arc(*models.Item).Weak) bool { - const lhs_arc = @constCast(&lhs_weak).upgrade(); - defer if (lhs_arc) |l1| if (l1.releaseUnwrap()) |l2| l2.deinit(); - - const rhs_arc = @constCast(&rhs_weak).upgrade(); - defer if (rhs_arc) |r1| if (r1.releaseUnwrap()) |r2| r2.deinit(); - - const lhs = if (lhs_arc) |l| - l.value.* - else - return false; - - const rhs = if (rhs_arc) |r| - r.value.* - else - return false; - - if (lhs.item == .container and rhs.item == .element) return true; - if (lhs.item == .element and rhs.item == .container) return false; - return std.mem.order(u8, lhs.displayName, rhs.displayName) == .lt; - } - }.sort); - - self.current_items.mutex.lock(); - defer self.current_items.mutex.unlock(); - self.current_items.data = items; -} - -fn resetCurrentItems(self: *@This()) void { - self.current_items.mutex.lock(); - defer self.current_items.mutex.unlock(); - - const data = self.current_items.data; - // self.current_items.data = null; - - if (data) |currentItems| { - for (currentItems) |item| { - item.release(); - } - self.allocator.free(currentItems); - } -} - pub fn deinit(self: *@This()) void { self.appState.deinit(); - - self.resetCurrentItems(); } const std = @import("std"); diff --git a/src/app_common/root.zig b/src/app_common/root.zig deleted file mode 100644 index 6676f3c..0000000 --- a/src/app_common/root.zig +++ /dev/null @@ -1,26 +0,0 @@ -const std = @import("std"); -const models = @import("../core/models.zig"); -const Model = @import("Model.zig"); - -pub fn data_loop(vm: *Model) void { - vm.usage_number.data += 1; - while (vm.running) { - inner_loop(vm) catch {}; - std.Thread.sleep(100 * std.time.ns_per_ms); - } - vm.usage_number.data -= 1; -} -fn inner_loop(appCommonModel: *Model) !void { - const tab = appCommonModel.appState.currentTab; - tab.currentItems.mutex.lock(); - defer tab.currentItems.mutex.unlock(); - - if (tab.currentItemsChanged) { - std.Thread.sleep(10 * std.time.ns_per_ms); - - if (tab.currentItems.data) |*tab_current_items| { - try appCommonModel.updateCurrentItems(tab_current_items); - tab.currentItemsChanged = false; - } - } -} diff --git a/src/console/action_map.zig b/src/console/action_map.zig index b228739..8ebf4fa 100644 --- a/src/console/action_map.zig +++ b/src/console/action_map.zig @@ -1,11 +1,18 @@ -pub fn handle_key(key: vaxis.Key, appState: *AppState, arena: std.mem.Allocator) !void { +pub fn handle_key(key: vaxis.Key, appState: *AppState, arena: std.mem.Allocator) !ActionResult { if (key.matches(vaxis.Key.left, .{})) { - try handle_action(Action.GoUp, appState, arena); + return try handle_action(Action.GoUp, appState, arena); + } else if (key.matches(vaxis.Key.up, .{})) { + return try handle_action(Action.SelectPrevious, appState, arena); + } else if (key.matches(vaxis.Key.down, .{})) { + return try handle_action(Action.SelectNext, appState, arena); } + + return ActionResult.None; } const std = @import("std"); const vaxis = @import("vaxis"); +const ActionResult = @import("../core/action/action.zig").ActionResult; const handle_action = @import("../core/action/action_handler.zig").handle; const Action = @import("../core/action/action.zig").Action; const AppState = @import("../core/app_state.zig").AppState; diff --git a/src/console/main.zig b/src/console/main.zig index c9c5c90..a2b24aa 100644 --- a/src/console/main.zig +++ b/src/console/main.zig @@ -1,8 +1,8 @@ -/// Our main application state const Model = struct { crash: bool = false, allocator: std.mem.Allocator, current_items_view: *vxfw.ListView, + current_items_view_widget: vxfw.Widget, app_common_model: *AppCommonModel, root_provider: *RootProvider, @@ -11,15 +11,15 @@ const Model = struct { return .{ .userdata = self, .eventHandler = Model.typeErasedEventHandler, + .captureHandler = Model.typeErasedCaptureHandler, .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 vm: *Model = @ptrCast(@alignCast(ptr)); switch (event) { - .init => return ctx.requestFocus(vm.current_items_view.widget()), + .init => return ctx.requestFocus(vm.current_items_view_widget), .key_press => |key| { if (key.matches('c', .{ .ctrl = true })) { ctx.quit = true; @@ -28,18 +28,27 @@ const Model = struct { if (key.matches('r', .{})) { // vm.crash = true; ctx.redraw = true; - return ctx.requestFocus(vm.current_items_view.widget()); + return ctx.requestFocus(vm.current_items_view_widget); } - // if (key.matches(vaxis.Key.left, .{})) { - // ctx.redraw = true; - // return ctx.requestFocus(vm.current_items_view.widget()); - // } - + }, + .focus_in => return ctx.requestFocus(vm.current_items_view_widget), + else => {}, + } + } + fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { + const vm: *Model = @ptrCast(@alignCast(ptr)); + switch (event) { + .key_press => |key| { var arena = std.heap.ArenaAllocator.init(vm.allocator); defer arena.deinit(); - handle_key(key, &vm.app_common_model.appState, arena.allocator()) catch {}; + + const result = handle_key(key, &vm.app_common_model.appState, arena.allocator()) catch null; + if (result) |r| { + if (r == ActionResult.Handled) { + ctx.consumeAndRedraw(); + } + } }, - .focus_in => return ctx.requestFocus(vm.current_items_view.widget()), else => {}, } } @@ -80,8 +89,7 @@ const Model = struct { const currentItemsTop = 1; const list_surface: vxfw.SubSurface = .{ .origin = .{ .row = currentItemsTop, .col = parentItemsWidth }, - .surface = try vm.current_items_view - .widget() + .surface = try vm.current_items_view_widget .draw(ctx.withConstraints(ctx.min, .{ .width = currentItemsWidth, .height = ctx.max.height.? - currentItemsTop, @@ -90,23 +98,77 @@ const Model = struct { }; try rootWidgets.append(list_surface); - const current_location_text = if (vm.app_common_model.appState.currentTab.currentLocation) |loc| loc.item.fullName.path else ""; - const current_location_text_element = try ctx.arena.create(vxfw.Text); - current_location_text_element.* = vxfw.Text{ - .text = current_location_text, - .overflow = .clip, - .softwrap = false, - .style = .{ - .fg = .{ .index = 4 }, - }, - }; - const current_location_text_surface: vxfw.SubSurface = .{ - .origin = .{ .row = 0, .col = 0 }, - .surface = try current_location_text_element.widget().draw(ctx), - // .surface = try vm.current_items.widget().draw(ctx), - }; + const header = try ctx.arena.create(std.ArrayList(vxfw.Widget)); + header.* = std.ArrayList(vxfw.Widget).init(ctx.arena); - try rootWidgets.append(current_location_text_surface); + { + const current_location_text = if (vm.app_common_model.appState.currentTab.currentLocation) |loc| + try std.fmt.allocPrint(ctx.arena, "{s} ", .{loc.item.fullName.path}) + else + ""; + const current_location_text_element = try ctx.arena.create(vxfw.Text); + current_location_text_element.* = vxfw.Text{ + .text = current_location_text, + .overflow = .clip, + .softwrap = false, + .style = .{ + .fg = .{ .index = 4 }, + }, + }; + try header.append(current_location_text_element.widget()); + } + + { + const currentTab = vm.app_common_model.appState.currentTab; + if (currentTab.currentItem) |currentItem| { + currentTab.currentItems.mutex.lock(); + defer currentTab.currentItems.mutex.unlock(); + + if (currentTab.currentItems.data) |currentItems| { + const current_item_text = + for (currentItems.items) |item| { + const arc_item = item.retain(); + defer if (arc_item.releaseUnwrap()) |i| i.deinit(); + + if (models.FullName.eql(&arc_item.value.*.fullName, ¤tItem)) { + break try std.fmt.allocPrint(ctx.arena, "{s} ", .{arc_item.value.*.displayName}); + } + } else ""; + + const current_item_text_element = try ctx.arena.create(vxfw.Text); + current_item_text_element.* = vxfw.Text{ + .text = current_item_text, + .overflow = .clip, + .softwrap = false, + .style = .{ + .fg = .{ .index = 4 }, + }, + }; + try header.append(current_item_text_element.widget()); + } + } + } + + { + const head_items = try ctx.arena.alloc(vxfw.FlexItem, header.items.len); + + for (0.., header.items) |i, item| { + head_items[i] = vxfw.FlexItem{ + .widget = item, + .flex = 0, + }; + } + + const header_element = try ctx.arena.create(vxfw.FlexRow); + header_element.* = .{ .children = head_items }; + + const flex_column_surface: vxfw.SubSurface = .{ + .origin = .{ .row = 0, .col = 0 }, + .surface = try header_element.widget().draw(ctx), + }; + + try rootWidgets.append(flex_column_surface); + } return .{ .size = max_size, @@ -120,20 +182,32 @@ const Model = struct { if (vm.crash) @panic("asd123"); const text_items = blk2: { - vm.app_common_model.current_items.mutex.lock(); - defer vm.app_common_model.current_items.mutex.unlock(); + const currentTab = vm.app_common_model.appState.currentTab; + const current_full_name = if (currentTab.currentItem) |currentItem| + models.FullName{ .path = try ctx.arena.dupe(u8, currentItem.path) } + else + null; - break :blk2 if (vm.app_common_model.current_items.data) |items| blk: { + currentTab.currentItems.mutex.lock(); + defer currentTab.currentItems.mutex.unlock(); + + break :blk2 if (currentTab.currentItems.data) |items| blk: { const children = try ctx.arena.create(std.ArrayList(*vxfw.Text)); children.* = std.ArrayList(*vxfw.Text).init(ctx.arena); + // const current_index = vm.current_items_view.cursor; + // const children = try ctx.arena.alloc(*vxfw.Text, items.len); - for (0.., items) |i, *weak_child| { - const arc_child = weak_child.upgrade() orelse continue; + for (items.items) |*original_arc_child| { + const arc_child = original_arc_child.retain(); defer if (arc_child.releaseUnwrap()) |item| item.deinit(); const child = arc_child.value.*; - const is_active = i == vm.current_items_view.cursor; + // const is_active = i == current_index; + const is_active = if (current_full_name) |c_full_name| + models.FullName.eql(&c_full_name, &child.fullName) + else + false; const fg, const bg = colors: { var fg: vaxis.Color = .default; @@ -271,6 +345,10 @@ pub fn main() !void { .draw_cursor = false, .children = .{ .slice = items }, }; + var list_widget = list.widget(); + list_widget.eventHandler = struct { + fn a(_: *anyopaque, _: *vxfw.EventContext, _: vxfw.Event) anyerror!void {} + }.a; var appState: AppState = AppState.init(allocator); try appState.addTab(tab1); @@ -287,13 +365,12 @@ pub fn main() !void { .allocator = allocator, .app_common_model = &app_common_model, .current_items_view = &list, + .current_items_view_widget = list_widget, .root_provider = &rootProvider, }; model.app_common_model.usage_number.data += 1; - try pool.spawn(core.data_loop, .{model.app_common_model}); - var app = try vxfw.App.init(allocator); defer app.deinit(); @@ -313,12 +390,12 @@ const vaxis = @import("vaxis"); const vxfw = vaxis.vxfw; const models = @import("../core/models.zig"); +const ActionResult = @import("../core/action/action.zig").ActionResult; const provider = @import("../core/provider/provider.zig"); const RootProvider = @import("../core/provider/root.zig").RootProvider; const local_provider = @import("../core/provider/local.zig"); const Tab = @import("../core/tab/tab.zig").Tab; const locked = @import("../core/sync.zig").locked; const AppCommonModel = @import("../app_common/Model.zig"); -const core = @import("../app_common/root.zig"); const AppState = @import("../core/app_state.zig").AppState; const handle_key = @import("./action_map.zig").handle_key; diff --git a/src/core/action/action.zig b/src/core/action/action.zig index 0699e2f..7d33177 100644 --- a/src/core/action/action.zig +++ b/src/core/action/action.zig @@ -1,4 +1,11 @@ +pub const ActionResult = union(enum) { + None, + Handled, +}; + pub const Action = union(enum) { GoUp, Enter, + SelectNext, + SelectPrevious, }; diff --git a/src/core/action/action_handler.zig b/src/core/action/action_handler.zig index d8ba7b5..92b07b7 100644 --- a/src/core/action/action_handler.zig +++ b/src/core/action/action_handler.zig @@ -1,4 +1,4 @@ -pub fn handle(action: Action, appState: *AppState, arena: std.mem.Allocator) !void { +pub fn handle(action: Action, appState: *AppState, arena: std.mem.Allocator) !ActionResult { switch (action) { .GoUp => { if (appState.currentTab.currentLocation) |currentLocation| { @@ -6,14 +6,76 @@ pub fn handle(action: Action, appState: *AppState, arena: std.mem.Allocator) !vo if (parent) |p| { const path = try arena.dupe(u8, p.path.path); try appState.currentTab.setCurrentLocation(.{ .path = path }); + return ActionResult.Handled; } else return error.NoParent; } else return error.NoCurrentLocation; }, - .Enter => unreachable, + .Enter => {}, + .SelectNext => { + return selectNextIndex(appState.currentTab, .Next); + }, + .SelectPrevious => { + return selectNextIndex(appState.currentTab, .Previous); + }, } + + return ActionResult.None; +} + +const SelectStep = enum { + Next, + Previous, + NextPage, + PreviousPage, +}; + +fn selectNextIndex(currentTab: *Tab, step: SelectStep) ActionResult { + const index = blk: { + currentTab.currentItems.mutex.lock(); + defer currentTab.currentItems.mutex.unlock(); + + const currentItems = currentTab.currentItems.data orelse return ActionResult.None; + + if (currentTab.currentItem) |*currentItem| { + const index = indexOf(Arc(*models.Item), models.FullName, currentItems.items, currentItem, arcFullNameEql); + if (index) |i| { + break :blk switch (step) { + .Next => i +| 1, + .Previous => i -| 1, + .NextPage => i +| 8, + .PreviousPage => i +| 8, + }; + } + } + + break :blk 0; + }; + + currentTab.selectItem(index) catch { + //TODO + }; + return ActionResult.Handled; +} + +fn arcFullNameEql(arcLhs: *const Arc(*models.Item), rhs: *const models.FullName) bool { + const arcLhs2 = arcLhs.retain(); + defer if (arcLhs2.releaseUnwrap()) |i| i.deinit(); + + return models.FullName.eql(&arcLhs2.value.*.fullName, rhs); +} + +fn indexOf(comptime TSlice: type, comptime TItem: type, slice: []const TSlice, value: *const TItem, compare: *const fn (a: *const TSlice, b: *const TItem) bool) ?usize { + for (0.., slice) |i, *item| { + if (compare(item, value)) return i; + } + return null; } const std = @import("std"); const Action = @import("action.zig").Action; +const ActionResult = @import("action.zig").ActionResult; const AppState = @import("../app_state.zig").AppState; const RootProvider = @import("../provider/root.zig").RootProvider; +const models = @import("../models.zig"); +const Arc = @import("zigrc").Arc; +const Tab = @import("../tab/tab.zig").Tab; diff --git a/src/core/models.zig b/src/core/models.zig index 32d310f..72221a9 100644 --- a/src/core/models.zig +++ b/src/core/models.zig @@ -80,6 +80,10 @@ pub const FullName = struct { return FullName{ .path = path }; } + + pub fn eql(self: *const FullName, value: *const FullName) bool { + return std.mem.eql(u8, self.path, value.path); + } }; pub const NativePath = struct { diff --git a/src/core/tab/tab.zig b/src/core/tab/tab.zig index 0fb9a90..dcb2eb9 100644 --- a/src/core/tab/tab.zig +++ b/src/core/tab/tab.zig @@ -3,7 +3,7 @@ pub const Tab = struct { currentLocation: ?*Container, currentItems: locked(?std.ArrayList(Arc(*Item))), currentLocationChanged: Observable(?*Container), - currentItemsChanged: bool = false, + currentItem: ?models.FullName, threadPool: *std.Thread.Pool, rootProvider: *RootProvider, @@ -18,6 +18,7 @@ pub const Tab = struct { .currentItems = .{ .data = null }, .currentLocationChanged = Observable(?*Container).init(allocator), .currentLocation = null, + .currentItem = null, .threadPool = threadPool, .rootProvider = rootProvider, }; @@ -28,26 +29,54 @@ pub const Tab = struct { c.item.deinit(); } - errdefer { - self.currentLocation = null; - self.currentLocationChanged.notify(null); + if (self.currentItem) |i| { + self.allocator.free(i.path); + self.currentItem = null; } - const newLocation = try self.rootProvider.getItemByFullName(newLocationFullName, &.{}, self.allocator); - errdefer { - newLocation.deinit(); - } + const newLocationContainer = blk: { + const newLocation = try self.rootProvider.getItemByFullName(newLocationFullName, &.{}, self.allocator); + errdefer { + newLocation.deinit(); + self.currentLocation = null; + self.currentLocationChanged.notify(null); + } - const newLocationContainer = switch (newLocation.item) { - .container => |c| c, - .element => return error.NotContainer, + const newLocationContainer = switch (newLocation.item) { + .container => |c| c, + .element => return error.NotContainer, + }; + + self.currentLocation = newLocationContainer; + break :blk newLocationContainer; }; - - self.currentLocation = newLocationContainer; self.currentLocationChanged.notify(newLocationContainer); //TODO: Proper error handling std.Thread.Pool.spawn(self.threadPool, loadItemsWrapper, .{ self, newLocationContainer }) catch unreachable; + + self.selectItem(0) catch { + self.currentItem = null; + }; + } + + pub fn selectItem(self: *Tab, index: usize) !void { + self.currentItems.mutex.lock(); + defer self.currentItems.mutex.unlock(); + + const currentItems = self.currentItems.data orelse return error.Error; + + if (index >= currentItems.items.len) { + return error.OutOfRange; + } + + if (self.currentItem) |currentItem| { + self.allocator.free(currentItem.path); + } + + self.currentItem = models.FullName{ + .path = try self.allocator.dupe(u8, currentItems.items[index].value.*.fullName.path), + }; } fn loadItemsWrapper(self: *Tab, location: *Container) void { @@ -84,25 +113,25 @@ pub const Tab = struct { std.Thread.sleep(1 * std.time.ns_per_ms); } - for (location.children.items) |item| { - const resolvedItem = location.item.provider.getItemByFullName(item, &.{ .skipChildInit = true }, self.allocator) catch |e| { + for (location.children.items) |item_fullname| { + const resolvedItem = location.item.provider.getItemByFullName(item_fullname, &.{ .skipChildInit = true }, self.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_fullname.path }); continue; }; self.currentItems.mutex.lock(); defer self.currentItems.mutex.unlock(); - // const arc = try Arc(*Item).init(self.allocator, resolvedItem); - // try self.currentItems.data.?.append(arc); - try self.currentItems.data.?.append(try Arc(*Item).init(self.allocator, resolvedItem)); - } - - { - self.currentItems.mutex.lock(); - defer self.currentItems.mutex.unlock(); - self.currentItemsChanged = true; + const index = for (self.currentItems.data.?.items, 0..) |item, i| { + if (compareByDisplayName(resolvedItem, item.value.*)) break i; + } else null; + const arc = try Arc(*Item).init(self.allocator, resolvedItem); + if (index) |i| { + try self.currentItems.data.?.insert(i, arc); + } else { + try self.currentItems.data.?.append(arc); + } } } @@ -122,8 +151,18 @@ pub const Tab = struct { currentItems.deinit(); } + if (self.currentItem) |currentItem| { + self.allocator.free(currentItem.path); + } + self.allocator.destroy(self); } + + fn compareByDisplayName(lhs: *models.Item, rhs: *models.Item) bool { + if (lhs.item == .container and rhs.item == .element) return true; + if (lhs.item == .element and rhs.item == .container) return false; + return std.mem.order(u8, lhs.displayName, rhs.displayName) == .lt; + } }; const std = @import("std"); diff --git a/src/gui/main.zig b/src/gui/main.zig index 926548a..e217d74 100644 --- a/src/gui/main.zig +++ b/src/gui/main.zig @@ -73,7 +73,6 @@ pub fn main() !void { }; defer model.core_model.deinit(); - try pool.spawn(core.data_loop, .{&model.core_model}); // init Raylib backend (creates OS window) // initWindow() means the backend calls CloseWindow for you in deinit() @@ -138,12 +137,14 @@ pub fn main() !void { fn dvui_frame(model: *Model) !void { { - model.core_model.current_items.mutex.lock(); - defer model.core_model.current_items.mutex.unlock(); + const currentTab = model.core_model.appState.currentTab; - if (model.core_model.current_items.data) |current_items| { - for (0.., current_items) |i, *weak_item| { - const arc_item = weak_item.upgrade() orelse continue; + currentTab.currentItems.mutex.lock(); + defer currentTab.currentItems.mutex.unlock(); + + if (currentTab.currentItems.data) |current_items| { + for (0.., current_items.items) |i, *source_arc_item| { + const arc_item = source_arc_item.retain(); defer if (arc_item.releaseUnwrap()) |item| item.deinit(); const item = arc_item.value.*; @@ -205,7 +206,6 @@ const provider = @import("../core/provider/provider.zig"); const RootProvider = @import("../core/provider/root.zig").RootProvider; const local_provider = @import("../core/provider/local.zig"); const Tab = @import("../core/tab/tab.zig").Tab; -const core = @import("../app_common/root.zig"); const AppState = @import("../core/app_state.zig").AppState; const dvui = @import("dvui");