From c47ce8702cb89842aef1f71098908505453c8116 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 5 Nov 2025 14:41:36 +0100 Subject: [PATCH 1/6] fix: don't leak project names in project_manager.request_recent_projects --- src/project_manager.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/project_manager.zig b/src/project_manager.zig index c5d380c..6917e91 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -521,6 +521,7 @@ const Process = struct { try cbor.writeArrayHeader(writer, 2); try cbor.writeValue(writer, project.name); try cbor.writeValue(writer, if (self.projects.get(project.name)) |_| true else false); + self.allocator.free(project.name); } from.send_raw(.{ .buf = message.written() }) catch return error.ClientFailed; self.logger.print("{d} projects found", .{recent_projects.items.len}); From 089a835160070313f5c175f03bd1a78b315cfcf3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 5 Nov 2025 14:42:39 +0100 Subject: [PATCH 2/6] refactor: remove active project last_used override in project_manager.load_recent_projects Makes more sense to update last_used after loading as we may need to add the entry. --- src/project_manager.zig | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/project_manager.zig b/src/project_manager.zig index 6917e91..53d71d4 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -756,7 +756,7 @@ const Process = struct { return stream.toOwnedSlice(); } - fn load_recent_projects(self: *Process, recent_projects: *std.ArrayList(RecentProject), project_directory: []const u8) !void { + fn load_recent_projects(self: *Process, recent_projects: *std.ArrayList(RecentProject)) !void { var path: std.Io.Writer.Allocating = .init(self.allocator); defer path.deinit(); const writer = &path.writer; @@ -769,7 +769,7 @@ const Process = struct { var iter = dir.iterate(); while (try iter.next()) |entry| { if (entry.kind != .file) continue; - try self.read_project_name(path.written(), entry.name, recent_projects, project_directory); + try self.read_project_name(path.written(), entry.name, recent_projects); } } @@ -778,7 +778,6 @@ const Process = struct { state_dir: []const u8, file_path: []const u8, recent_projects: *std.ArrayList(RecentProject), - project_directory: []const u8, ) !void { var path: std.Io.Writer.Allocating = .init(self.allocator); defer path.deinit(); @@ -796,10 +795,8 @@ const Process = struct { var iter: []const u8 = buffer; var name: []const u8 = undefined; - if (cbor.matchValue(&iter, tp.extract(&name)) catch return) { - const last_used = if (std.mem.eql(u8, project_directory, name)) std.math.maxInt(@TypeOf(stat.mtime)) else stat.mtime; - (try recent_projects.addOne(self.allocator)).* = .{ .name = try self.allocator.dupe(u8, name), .last_used = last_used }; - } + if (cbor.matchValue(&iter, tp.extract(&name)) catch return) + (try recent_projects.addOne(self.allocator)).* = .{ .name = try self.allocator.dupe(u8, name), .last_used = stat.mtime }; } fn sort_projects_by_last_used(_: *Process, recent_projects: *std.ArrayList(RecentProject)) void { From 03d07d682f397658c575ba36cc26b305a9949153 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 5 Nov 2025 14:43:55 +0100 Subject: [PATCH 3/6] feat: always list active project first in project_manager.request_recent_projects --- src/project_manager.zig | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/project_manager.zig b/src/project_manager.zig index 53d71d4..c658385 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -505,10 +505,21 @@ const Process = struct { return project.request_new_or_modified_files(from, max); } - fn request_recent_projects(self: *Process, from: tp.pid_ref, project_directory: []const u8) (ProjectError || Project.ClientError)!void { + fn request_recent_projects(self: *Process, from: tp.pid_ref, active_project: []const u8) (ProjectError || Project.ClientError)!void { var recent_projects: std.ArrayList(RecentProject) = .empty; defer recent_projects.deinit(self.allocator); - self.load_recent_projects(&recent_projects, project_directory) catch {}; + self.load_recent_projects(&recent_projects) catch {}; + for (recent_projects.items) |*recent_project| { + if (std.mem.eql(u8, active_project, recent_project.name)) { + recent_project.last_used = std.math.maxInt(i128); + break; + } + } else { + (try recent_projects.addOne(self.allocator)).* = .{ + .name = try self.allocator.dupe(u8, active_project), + .last_used = std.math.maxInt(i128), + }; + } self.sort_projects_by_last_used(&recent_projects); var message: std.Io.Writer.Allocating = .init(self.allocator); defer message.deinit(); From 7744bdf6c42d0979dc1ef04e26c3c036b301d409 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 5 Nov 2025 16:33:12 +0100 Subject: [PATCH 4/6] fix: update Project.last_used when switching projects --- src/Project.zig | 2 ++ src/project_manager.zig | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index a12ff0f..e1ce93e 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -31,6 +31,7 @@ persistent: bool = false, logger: log.Logger, logger_lsp: log.Logger, logger_git: log.Logger, +last_used: i128, workspace: ?[]const u8 = null, @@ -100,6 +101,7 @@ pub fn init(allocator: std.mem.Allocator, name: []const u8) OutOfMemoryError!Sel .logger = log.logger("project"), .logger_lsp = log.logger("lsp"), .logger_git = log.logger("git"), + .last_used = std.time.nanoTimestamp(), }; } diff --git a/src/project_manager.zig b/src/project_manager.zig index c658385..ea77ced 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -463,15 +463,16 @@ const Process = struct { } fn open(self: *Process, project_directory: []const u8) (SpawnError || std.fs.Dir.OpenError)!void { - if (self.projects.get(project_directory) == null) { + if (self.projects.get(project_directory)) |project| { + project.last_used = std.time.nanoTimestamp(); + self.logger.print("switched to: {s}", .{project_directory}); + } else { self.logger.print("opening: {s}", .{project_directory}); const project = try self.allocator.create(Project); project.* = try Project.init(self.allocator, project_directory); try self.projects.put(self.allocator, try self.allocator.dupe(u8, project_directory), project); self.restore_project(project) catch |e| self.logger.err("restore_project", e); project.query_git(); - } else { - self.logger.print("switched to: {s}", .{project_directory}); } } From 983e518f692eadeeaad42d94edf024d5c1e249a9 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 5 Nov 2025 16:33:42 +0100 Subject: [PATCH 5/6] fix: add not-yet-persisted projects to recent_projecsts list --- src/project_manager.zig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/project_manager.zig b/src/project_manager.zig index ea77ced..4cd1146 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -521,6 +521,20 @@ const Process = struct { .last_used = std.math.maxInt(i128), }; } + var iter = self.projects.iterator(); + while (iter.next()) |item| { + for (recent_projects.items) |*recent_project| { + if (std.mem.eql(u8, item.value_ptr.*.name, recent_project.name)) { + recent_project.last_used = item.value_ptr.*.last_used; + break; + } + } else { + (try recent_projects.addOne(self.allocator)).* = .{ + .name = try self.allocator.dupe(u8, item.value_ptr.*.name), + .last_used = item.value_ptr.*.last_used, + }; + } + } self.sort_projects_by_last_used(&recent_projects); var message: std.Io.Writer.Allocating = .init(self.allocator); defer message.deinit(); From 8b50c7a3affc453c43ab980f7ce47dd2bb1eaf45 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 5 Nov 2025 16:39:35 +0100 Subject: [PATCH 6/6] fix: fully deinit keybind.Mode to avoid race when switching modes --- src/keybind/keybind.zig | 23 ++++++++++++++++++----- src/tui/tui.zig | 6 +++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index 92f8a3f..4fe2d67 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -100,7 +100,7 @@ const Handler = struct { pub const Mode = struct { allocator: std.mem.Allocator, - input_handler: EventHandler, + input_handler: ?EventHandler, event_handler: ?EventHandler = null, mode: []const u8, @@ -121,11 +121,24 @@ pub const Mode = struct { } pub fn deinit(self: *Mode) void { - if (self.deinit_command) |deinit_command| - deinit_command.execute_const(); - self.allocator.free(self.mode); - self.input_handler.deinit(); + if (self.deinit_command) |deinit_command| deinit_command.execute_const(); if (self.event_handler) |eh| eh.deinit(); + if (self.input_handler) |ih| ih.deinit(); + self.allocator.free(self.mode); + + self.deinit_command = null; + self.event_handler = null; + self.input_handler = null; + self.mode = &.{}; + + self.name = ""; + self.line_numbers = .inherit; + self.keybind_hints = &.{}; + self.cursor_shape = null; + self.selection_style = .normal; + self.init_command = null; + self.deinit_command = null; + self.initialized = false; } }; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 917fc50..a465633 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -554,7 +554,7 @@ fn active_event_handler(self: *Self) ?EventHandler { fn dispatch_flush_input_event(self: *Self) error{ Exit, NoSpaceLeft }!void { var buf: [32]u8 = undefined; const mode = self.input_mode_ orelse return; - try mode.input_handler.send(tp.self_pid(), try tp.message.fmtbuf(&buf, .{"F"})); + if (mode.input_handler) |ih| try ih.send(tp.self_pid(), try tp.message.fmtbuf(&buf, .{"F"})); if (mode.event_handler) |eh| try eh.send(tp.self_pid(), try tp.message.fmtbuf(&buf, .{"F"})); } @@ -577,8 +577,8 @@ fn dispatch_input(ctx: *anyopaque, cbor_msg: []const u8) void { break :ret false; }) return; - if (self.input_mode_) |mode| - mode.input_handler.send(from, m) catch |e| self.logger.err("input handler", e); + if (self.input_mode_) |mode| if (mode.input_handler) |ih| + ih.send(from, m) catch |e| self.logger.err("input handler", e); } fn dispatch_mouse(ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void {