From 620e44ef024286d84626dd677cf595a054868591 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Mon, 20 Jan 2025 06:03:34 -0700 Subject: [PATCH 01/20] win32 gui: fix high cpu usage, validate window in WM_PAINT Looks like I probably accidently removed the calls to BeginPaint and EndPaint in WM_PAINT, which, would cause the OS to think that the window contents are never validated and will continuously send us WM_PAINT messages. I've added these back in and now flow is back to low CPU usage especially when idle. --- src/win32/gui.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 7dc5456..f334ca3 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1040,6 +1040,10 @@ fn WndProc( const font = getFont(dpi, getFontSize(), getFontFace()); const client_size = getClientSize(u32, hwnd); + var ps: win32.PAINTSTRUCT = undefined; + _ = win32.BeginPaint(hwnd, &ps) orelse return fatalWin32("BeginPaint", win32.GetLastError()); + defer if (0 == win32.EndPaint(hwnd, &ps)) fatalWin32("EndPaint", win32.GetLastError()); + global.render_cells.resize( global.render_cells_arena.allocator(), global.screen.buf.len, From 5b5fd26bdf8d841f159e473e03e2cb49788e3561 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 15:07:42 +0100 Subject: [PATCH 02/20] feat(win32 gui): add tracy zones to win32 gui renderer --- build.zig | 1 + src/win32/gui.zig | 61 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 05582a5..516e660 100644 --- a/build.zig +++ b/build.zig @@ -333,6 +333,7 @@ pub fn build_exe( .{ .name = "vaxis", .module = vaxis_mod }, .{ .name = "color", .module = color_mod }, .{ .name = "gui_config", .module = gui_config_mod }, + .{ .name = "tracy", .module = tracy_mod }, }, }); gui_mod.addIncludePath(b.path("src/win32")); diff --git a/src/win32/gui.zig b/src/win32/gui.zig index f334ca3..a2a7451 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const tracy = @import("tracy"); const build_options = @import("build_options"); const root = @import("root"); @@ -92,6 +93,8 @@ const window_style_ex = win32.WINDOW_EX_STYLE{ const window_style = win32.WS_OVERLAPPEDWINDOW; pub fn init() void { + const frame = tracy.initZone(@src(), .{ .name = "gui init" }); + defer frame.deinit(); std.debug.assert(!global.init_called); global.init_called = true; render.init(.{}); @@ -102,6 +105,8 @@ const Icons = struct { large: win32.HICON, }; fn getIcons(dpi: XY(u32)) Icons { + const frame = tracy.initZone(@src(), .{ .name = "gui getIcons" }); + defer frame.deinit(); const small_x = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CXSMICON), dpi.x); const small_y = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CYSMICON), dpi.y); const large_x = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CXICON), dpi.x); @@ -190,6 +195,8 @@ fn getFontSize() f32 { } fn getFont(dpi: u32, size: f32, face: *const FontFace) render.Font { + const frame = tracy.initZone(@src(), .{ .name = "gui getFont" }); + defer frame.deinit(); if (global.font) |*font| { if (font.dpi == dpi and font.size == size and font.face.eql(face)) return font.render_object; @@ -251,6 +258,8 @@ fn calcWindowPlacement( initial_window_x: u16, initial_window_y: u16, ) WindowPlacement { + const frame = tracy.initZone(@src(), .{ .name = "gui calcWindowPlacement" }); + defer frame.deinit(); var result = WindowPlacement.default; const monitor = maybe_monitor orelse return result; @@ -531,6 +540,8 @@ fn updateWindowSize( edge: ?win32.WPARAM, bounds_ref: *?WindowBounds, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui updateWindowSize" }); + defer frame.deinit(); const dpi = win32.dpiFromHwnd(hwnd); const font = getFont(dpi, getFontSize(), getFontFace()); const cell_size = font.getCellSize(i32); @@ -564,6 +575,8 @@ fn updateWindowSize( } fn getFontFaces(state: *State) void { + const frame = tracy.initZone(@src(), .{ .name = "gui getFontFaces" }); + defer frame.deinit(); const fonts = render.Fonts.init(); defer fonts.deinit(); var buf: [FontFace.max * 2]u8 = undefined; @@ -631,6 +644,8 @@ fn sendMouse( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui sendMouse" }); + defer frame.deinit(); const point = win32ext.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); @@ -682,6 +697,8 @@ fn sendMouseWheel( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui sendMouseWheel" }); + defer frame.deinit(); const point = win32ext.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); @@ -731,6 +748,8 @@ fn sendKey( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui sendKey" }); + defer frame.deinit(); const state = stateFromHwnd(hwnd); var keyboard_state: [256]u8 = undefined; @@ -988,6 +1007,8 @@ fn WndProc( wparam: win32.WPARAM, lparam: win32.LPARAM, ) callconv(std.os.windows.WINAPI) win32.LRESULT { + const frame = tracy.initZone(@src(), .{ .name = "gui WndProc" }); + defer frame.deinit(); var msg_node: windowmsg.MessageNode = undefined; msg_node.init(&global_msg_tail, hwnd, msg, wparam, lparam); defer msg_node.deinit(); @@ -1035,6 +1056,8 @@ fn WndProc( return 0; }, win32.WM_PAINT => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_PAINT" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); const font = getFont(dpi, getFontSize(), getFontFace()); @@ -1074,6 +1097,8 @@ fn WndProc( return 0; }, win32.WM_GETDPISCALEDSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_GETDPISCALEDSIZE" }); + defer frame_.deinit(); const inout_size: *win32.SIZE = @ptrFromInt(@as(usize, @bitCast(lparam))); const new_dpi: u32 = @intCast(0xffffffff & wparam); // we don't want to update the font with the new dpi until after @@ -1109,6 +1134,8 @@ fn WndProc( return 1; }, win32.WM_DPICHANGED => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_DPICHANGED" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); if (dpi != win32.hiword(wparam)) @panic("unexpected hiword dpi"); @@ -1119,10 +1146,14 @@ fn WndProc( return 0; }, win32.WM_WINDOWPOSCHANGED => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_WINDOWPOSCHANGED" }); + defer frame_.deinit(); sendResize(hwnd); return 0; }, win32.WM_SIZING => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_SIZING" }); + defer frame_.deinit(); const rect: *win32.RECT = @ptrFromInt(@as(usize, @bitCast(lparam))); const dpi = win32.dpiFromHwnd(hwnd); const font = getFont(dpi, getFontSize(), getFontFace()); @@ -1137,6 +1168,8 @@ fn WndProc( return 0; }, win32.WM_DISPLAYCHANGE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_DISPLAYCHANGE" }); + defer frame_.deinit(); win32.invalidateHwnd(hwnd); return 0; }, @@ -1150,6 +1183,8 @@ fn WndProc( return WM_APP_EXIT_RESULT; }, WM_APP_SET_BACKGROUND => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_SET_BACKGROUND" }); + defer frame_.deinit(); const rgb = RGB.from_u24(@intCast(0xffffff & wparam)); render.setBackground( &stateFromHwnd(hwnd).render_state, @@ -1159,6 +1194,8 @@ fn WndProc( return WM_APP_SET_BACKGROUND_RESULT; }, WM_APP_ADJUST_FONTSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_ADJUST_FONTSIZE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const amount: f32 = @bitCast(@as(u32, @intCast(0xFFFFFFFFF & wparam))); global.fontsize = @max(getFontSize() + amount, 1.0); @@ -1167,6 +1204,8 @@ fn WndProc( return WM_APP_ADJUST_FONTSIZE_RESULT; }, WM_APP_SET_FONTSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_SET_FONTSIZE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const fontsize: f32 = @bitCast(@as(u32, @intCast(0xFFFFFFFFF & wparam))); global.fontsize = @max(fontsize, 1.0); @@ -1175,6 +1214,8 @@ fn WndProc( return WM_APP_SET_FONTSIZE_RESULT; }, WM_APP_RESET_FONTSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_RESET_FONTSIZE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); global.fontsize = null; updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); @@ -1182,6 +1223,8 @@ fn WndProc( return WM_APP_SET_FONTSIZE_RESULT; }, WM_APP_SET_FONTFACE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_SET_FONTFACE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); setFontFace(@ptrFromInt(wparam)); updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); @@ -1189,6 +1232,8 @@ fn WndProc( return WM_APP_SET_FONTFACE_RESULT; }, WM_APP_RESET_FONTFACE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_RESET_FONTFACE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); global.fontface = null; updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); @@ -1196,11 +1241,15 @@ fn WndProc( return WM_APP_SET_FONTFACE_RESULT; }, WM_APP_GET_FONTFACES => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_GET_FONTFACES" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); getFontFaces(state); return WM_APP_GET_FONTFACES_RESULT; }, WM_APP_UPDATE_SCREEN => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_UPDATE_SCREEN" }); + defer frame_.deinit(); const screen: *const vaxis.Screen = @ptrFromInt(wparam); _ = global.screen_arena.reset(.retain_capacity); const buf = global.screen_arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); @@ -1229,6 +1278,8 @@ fn WndProc( return WM_APP_UPDATE_SCREEN_RESULT; }, win32.WM_CREATE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_CREATE" }); + defer frame_.deinit(); std.debug.assert(global.state == null); const create_struct: *win32.CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lparam))); const create_args: *CreateWindowArgs = @alignCast(@ptrCast(create_struct.lpCreateParams)); @@ -1246,13 +1297,19 @@ fn WndProc( // hwnd reference @panic("gui window erroneously destroyed"); }, - else => return win32.DefWindowProcW(hwnd, msg, wparam, lparam), + else => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui DefWindowProcW" }); + defer frame_.deinit(); + return win32.DefWindowProcW(hwnd, msg, wparam, lparam); + }, } } fn sendResize( hwnd: win32.HWND, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui sendResize" }); + defer frame.deinit(); const dpi = win32.dpiFromHwnd(hwnd); const state = stateFromHwnd(hwnd); @@ -1309,6 +1366,8 @@ fn calcWindowRect( maybe_edge: ?win32.WPARAM, cell_size: XY(i32), ) win32.RECT { + const frame = tracy.initZone(@src(), .{ .name = "gui calcWindowRect" }); + defer frame.deinit(); const client_inset = getClientInset(dpi); const bounding_client_size: XY(i32) = .{ .x = (bounding_rect.right - bounding_rect.left) - client_inset.x, From 51e8e9c1abbc55c93744f61f76325492859c7b0f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 15:33:52 +0100 Subject: [PATCH 03/20] build: update zig-tracy to support win32 --- build.zig.zon | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index e38c3f8..f0151dd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -10,16 +10,16 @@ .hash = "1220930a42f8da3fb7f723e3ad3f6dcc6db76327dd8d26274566423192d53e91b2bb", }, .tracy = .{ - .url = "https://github.com/neurocyte/zig-tracy/archive/58999b786089e5319dd0707f6afbfca04c6340e7.tar.gz", - .hash = "1220a2c8f8db1b5265458ac967ea1f7cc0a8ddcd1d774df3b73d86c4f529aadbfb94", + .url = "https://github.com/neurocyte/zig-tracy/archive/e04e31c64498149a324491b8534758e6af43a5c2.tar.gz", + .hash = "1220d0fb2bff7b453dbb39d1db3eb472b6680e2564f2b23b0e947671be47bbdd188f", }, .dizzy = .{ .url = "https://github.com/neurocyte/dizzy/archive/455d18369cbb2a0458ba70be919cd378338d695e.tar.gz", .hash = "1220220dbc7fe91c1c54438193ca765cebbcb7d58f35cdcaee404a9d2245a42a4362", }, .thespian = .{ - .url = "https://github.com/neurocyte/thespian/archive/b7ace533875217f2b002f0ac8dfe32169cab7138.tar.gz", - .hash = "1220d25045c26cc8d2d46995d670ce10b64d0e3a3fa23cfded9a35d353b4d08cff08", + .url = "https://github.com/neurocyte/thespian/archive/7668befc06610a76689842854f514fccf02f4c60.tar.gz", + .hash = "1220b25919a37c7a5dc266056a3668a754024ecb391807fa9f80466238269df6aa73", }, .themes = .{ .url = "https://github.com/neurocyte/flow-themes/releases/download/master-770924d1aba9cc33440b66e865bd4f7dca871074/flow-themes.tar.gz", From 29f8b4680de09a3fe9618b2c880587c448500f78 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 15:49:16 +0100 Subject: [PATCH 04/20] fix: disable mouse_idle_timer It's not very important and it's too spammy which makes debug builds slow. --- src/tui/tui.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index faa3216..198362e 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -56,6 +56,7 @@ mouse_idle_timer: ?tp.Cancellable = null, default_cursor: keybind.CursorShape = .default, fontface: []const u8 = "", fontfaces: ?std.ArrayList([]const u8) = null, +enable_mouse_idle_timer: bool = false, const keepalive = std.time.us_per_day * 365; // one year const idle_frames = 0; @@ -222,6 +223,7 @@ fn listen_sigwinch(self: *Self) tp.result { } fn update_mouse_idle_timer(self: *Self) void { + if (!self.enable_mouse_idle_timer) return; const delay = std.time.us_per_ms * @as(u64, mouse_idle_time_milliseconds); if (self.mouse_idle_timer) |*t| { t.cancel() catch {}; From feca5e8fd9337a2c2ab51f726d10280dc8b9e087 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 16:40:46 +0100 Subject: [PATCH 05/20] build: always optimize dependencies (again) --- build.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 516e660..86bc076 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,8 @@ const std = @import("std"); const builtin = @import("builtin"); +const optimize_deps = .ReleaseFast; + pub fn build(b: *std.Build) void { const release = b.option(bool, "package_release", "Build all release targets") orelse false; const tracy_enabled = b.option(bool, "enable_tracy", "Enable tracy client library (default: no)") orelse false; @@ -227,14 +229,14 @@ pub fn build_exe( const syntax_dep = b.dependency("syntax", .{ .target = target, - .optimize = optimize, + .optimize = optimize_deps, .use_tree_sitter = use_tree_sitter, }); const syntax_mod = syntax_dep.module("syntax"); const thespian_dep = b.dependency("thespian", .{ .target = target, - .optimize = optimize, + .optimize = optimize_deps, .enable_tracy = tracy_enabled, }); From 102a00098e92b8ba235b30973897ce5a0509f214 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 19:45:44 +0100 Subject: [PATCH 06/20] build: update thespian for minor performance improvements --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index f0151dd..6e5cfb0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -18,8 +18,8 @@ .hash = "1220220dbc7fe91c1c54438193ca765cebbcb7d58f35cdcaee404a9d2245a42a4362", }, .thespian = .{ - .url = "https://github.com/neurocyte/thespian/archive/7668befc06610a76689842854f514fccf02f4c60.tar.gz", - .hash = "1220b25919a37c7a5dc266056a3668a754024ecb391807fa9f80466238269df6aa73", + .url = "https://github.com/neurocyte/thespian/archive/d2b65df7c59b58f5e86b1ce3403cd3589d06a367.tar.gz", + .hash = "122040a2dcb9569ee26ae9c205f944db4ee9e3eb7c9d5ec610c7d84cf82ab6909fa9", }, .themes = .{ .url = "https://github.com/neurocyte/flow-themes/releases/download/master-770924d1aba9cc33440b66e865bd4f7dca871074/flow-themes.tar.gz", From 91eba390196dc54999b9d4297e3e14303f2cc11c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 19:46:22 +0100 Subject: [PATCH 07/20] fix(win32 gui): prevent index out of bounds in d3d11.paint --- src/win32/d3d11.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig index 21b6caa..7dbe105 100644 --- a/src/win32/d3d11.zig +++ b/src/win32/d3d11.zig @@ -309,7 +309,7 @@ pub fn paint( for (0..shader_row_count) |row| { const src_row = blk: { const r = top + row; - break :blk r - if (r >= row_count) row_count else 0; + break :blk if (r < row_count) r else 0; }; const src_row_offset = src_row * col_count; const dst_row_offset = row * shader_col_count; From 48420021365a169452acc2a6685db9ab7800f808 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 19:55:18 +0100 Subject: [PATCH 08/20] fix(win32 gui): translate mouse wheel events from screen to window coordinates --- src/win32/gui.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/win32/gui.zig b/src/win32/gui.zig index a2a7451..2faa947 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -699,7 +699,8 @@ fn sendMouseWheel( ) void { const frame = tracy.initZone(@src(), .{ .name = "gui sendMouseWheel" }); defer frame.deinit(); - const point = win32ext.pointFromLparam(lparam); + var point = win32ext.pointFromLparam(lparam); + _ = win32.ScreenToClient(hwnd, &point); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); const cell_size = getFont(dpi, getFontSize(), getFontFace()).getCellSize(i32); From 142f6e51aeef857386d2d404135f2e5ab048cb2b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 20 Jan 2025 20:30:24 +0100 Subject: [PATCH 09/20] feat(tui): show screen dimentions on resize --- src/tui/tui.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 198362e..7677103 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -311,6 +311,8 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { if (try m.match(.{"resize"})) { self.resize(); + const box = self.screen(); + message("{d}x{d}", .{ box.w, box.h }); return; } @@ -1174,3 +1176,8 @@ pub fn is_cursor_beam(self: *Self) bool { else => false, }; } + +pub fn message(comptime fmt: anytype, args: anytype) void { + var buf: [256]u8 = undefined; + tp.self_pid().send(.{ "message", std.fmt.bufPrint(&buf, fmt, args) catch @panic("too large") }) catch {}; +} From 99aefc8d22943a8f9d97e99ee72b77a23e41cb92 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Mon, 20 Jan 2025 14:57:03 -0700 Subject: [PATCH 10/20] remove direct2d dependency and update zigwin32 Since we removed the direct2d renderer we no longer need to reference the direct2d-zig repository. Instead we now directly reference the zigwin32 repository. I've also updated that repository with a few fixes and additions which allowed us to remove some code from flow. --- build.zig | 7 ++---- build.zig.zon | 6 ++--- src/renderer/win32/renderer.zig | 23 +++---------------- src/win32/gui.zig | 40 ++++++++++++++------------------- src/win32/win32ext.zig | 11 --------- 5 files changed, 25 insertions(+), 62 deletions(-) diff --git a/build.zig b/build.zig index 86bc076..58b92d0 100644 --- a/build.zig +++ b/build.zig @@ -318,16 +318,13 @@ pub fn build_exe( const renderer_mod = blk: { if (gui) switch (target.result.os.tag) { .windows => { - const direct2d_dep = b.lazyDependency("direct2d", .{}) orelse break :blk tui_renderer_mod; - - const win32_dep = direct2d_dep.builder.dependency("win32", .{}); - const win32_mod = win32_dep.module("zigwin32"); + const win32_dep = b.lazyDependency("win32", .{}) orelse break :blk tui_renderer_mod; + const win32_mod = win32_dep.module("win32"); const gui_mod = b.createModule(.{ .root_source_file = b.path("src/win32/gui.zig"), .imports = &.{ .{ .name = "build_options", .module = options_mod }, .{ .name = "win32", .module = win32_mod }, - .{ .name = "ddui", .module = direct2d_dep.module("ddui") }, .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, .{ .name = "input", .module = input_mod }, diff --git a/build.zig.zon b/build.zig.zon index 6e5cfb0..d04bd53 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,9 +37,9 @@ .url = "https://github.com/rockorager/zeit/archive/9cca8ec620a54c3b07cd249f25e5bcb3153d03d7.tar.gz", .hash = "1220755ea2a5aa6bb3713437aaafefd44812169fe43f1da755c3ee6101b85940f441", }, - .direct2d = .{ - .url = "https://github.com/marler8997/direct2d-zig/archive/0d031389a26653bb71f81c2340d1b8ba6bd339c3.tar.gz", - .hash = "122069b40656962c6ba9b9b3f9f882ba2e9cf4c5e1afebac7b7501404129e6bb4705", + .win32 = .{ + .url = "https://github.com/marlersoft/zigwin32/archive/259b6f353a48968d7e3171573db4fd898b046188.tar.gz", + .hash = "1220925614447b54ccc9894bbba8b202c6a8b750267890edab7732064867e46f3217", .lazy = true, }, }, diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index ac0f899..6c63388 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -19,6 +19,8 @@ const gui = @import("gui"); const DropWriter = gui.DropWriter; pub const style = StyleBits; +pub const panic = win32.messageBoxThenPanic(.{ .title = "Flow Panic" }); + allocator: std.mem.Allocator, vx: vaxis.Vaxis, @@ -84,25 +86,6 @@ pub fn deinit(self: *Self) void { self.title_buf.deinit(); } -threadlocal var thread_is_panicing = false; - -pub fn panic( - msg: []const u8, - error_return_trace: ?*std.builtin.StackTrace, - ret_addr: ?usize, -) noreturn { - if (!thread_is_panicing) { - thread_is_panicing = true; - const msg_z: [:0]const u8 = if (std.fmt.allocPrintZ( - std.heap.page_allocator, - "{s}", - .{msg}, - )) |msg_z| msg_z else |_| "failed allocate error message"; - _ = win32.MessageBoxA(null, msg_z, "Flow Panic", .{ .ICONASTERISK = 1 }); - } - std.builtin.default_panic(msg, error_return_trace, ret_addr); -} - pub fn run(self: *Self) !void { if (self.thread) |_| return; @@ -353,7 +336,7 @@ fn update_window_title(self: *Self) void { const title = self.title_buf.toOwnedSliceSentinel(0) catch @panic("OOM:update_window_title"); if (win32.SetWindowTextW(hwnd, title) == 0) { - std.log.warn("SetWindowText failed with {}", .{win32.GetLastError().fmt()}); + std.log.warn("SetWindowText failed, error={}", .{win32.GetLastError()}); self.title_buf = std.ArrayList(u16).fromOwnedSlice(self.allocator, title); } else { self.allocator.free(title); diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 2faa947..71761eb 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -123,7 +123,7 @@ fn getIcons(dpi: XY(u32)) Icons { small_x, small_y, win32.LR_SHARED, - ) orelse fatalWin32("LoadImage for small icon", win32.GetLastError()); + ) orelse win32.panicWin32("LoadImage for small icon", win32.GetLastError()); const large = win32.LoadImageW( win32.GetModuleHandleW(null), @ptrFromInt(c.ID_ICON_FLOW), @@ -131,7 +131,7 @@ fn getIcons(dpi: XY(u32)) Icons { large_x, large_y, win32.LR_SHARED, - ) orelse fatalWin32("LoadImage for large icon", win32.GetLastError()); + ) orelse win32.panicWin32("LoadImage for large icon", win32.GetLastError()); return .{ .small = @ptrCast(small), .large = @ptrCast(large) }; } @@ -268,7 +268,7 @@ fn calcWindowPlacement( var info: win32.MONITORINFO = undefined; info.cbSize = @sizeOf(win32.MONITORINFO); if (0 == win32.GetMonitorInfoW(monitor, &info)) { - std.log.warn("GetMonitorInfo failed with {}", .{win32.GetLastError().fmt()}); + std.log.warn("GetMonitorInfo failed, error={}", .{win32.GetLastError()}); return result; } break :blk info.rcWork; @@ -335,7 +335,7 @@ fn entry(pid: thespian.pid) !void { }, win32.MONITOR_DEFAULTTOPRIMARY, ) orelse { - std.log.warn("MonitorFromPoint failed with {}", .{win32.GetLastError().fmt()}); + std.log.warn("MonitorFromPoint failed, error={}", .{win32.GetLastError()}); break :blk null; }; }; @@ -384,7 +384,7 @@ fn entry(pid: thespian.pid) !void { .lpszClassName = CLASS_NAME, .hIconSm = global.icons.small, }; - if (0 == win32.RegisterClassExW(&wc)) fatalWin32( + if (0 == win32.RegisterClassExW(&wc)) win32.panicWin32( "RegisterClass for main window", win32.GetLastError(), ); @@ -403,7 +403,7 @@ fn entry(pid: thespian.pid) !void { null, // Menu win32.GetModuleHandleW(null), @ptrCast(&create_args), - ) orelse fatalWin32("CreateWindow", win32.GetLastError()); + ) orelse win32.panicWin32("CreateWindow", win32.GetLastError()); // NEVER DESTROY THE WINDOW! // This allows us to send the hwnd to other thread/parts // of the app and it will always be valid. @@ -430,7 +430,7 @@ fn entry(pid: thespian.pid) !void { ); } - if (0 == win32.UpdateWindow(hwnd)) fatalWin32("UpdateWindow", win32.GetLastError()); + if (0 == win32.UpdateWindow(hwnd)) win32.panicWin32("UpdateWindow", win32.GetLastError()); _ = win32.ShowWindow(hwnd, win32.SW_SHOWNORMAL); // try some things to bring our window to the top @@ -547,7 +547,7 @@ fn updateWindowSize( const cell_size = font.getCellSize(i32); var window_rect: win32.RECT = undefined; - if (0 == win32.GetWindowRect(hwnd, &window_rect)) fatalWin32( + if (0 == win32.GetWindowRect(hwnd, &window_rect)) win32.panicWin32( "GetWindowRect", win32.GetLastError(), ); @@ -646,7 +646,7 @@ fn sendMouse( ) void { const frame = tracy.initZone(@src(), .{ .name = "gui sendMouse" }); defer frame.deinit(); - const point = win32ext.pointFromLparam(lparam); + const point = win32.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); const cell_size = getFont(dpi, getFontSize(), getFontFace()).getCellSize(i32); @@ -699,7 +699,7 @@ fn sendMouseWheel( ) void { const frame = tracy.initZone(@src(), .{ .name = "gui sendMouseWheel" }); defer frame.deinit(); - var point = win32ext.pointFromLparam(lparam); + var point = win32.pointFromLparam(lparam); _ = win32.ScreenToClient(hwnd, &point); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); @@ -754,7 +754,7 @@ fn sendKey( const state = stateFromHwnd(hwnd); var keyboard_state: [256]u8 = undefined; - if (0 == win32.GetKeyboardState(&keyboard_state)) fatalWin32( + if (0 == win32.GetKeyboardState(&keyboard_state)) win32.panicWin32( "GetKeyboardState", win32.GetLastError(), ); @@ -1065,8 +1065,8 @@ fn WndProc( const client_size = getClientSize(u32, hwnd); var ps: win32.PAINTSTRUCT = undefined; - _ = win32.BeginPaint(hwnd, &ps) orelse return fatalWin32("BeginPaint", win32.GetLastError()); - defer if (0 == win32.EndPaint(hwnd, &ps)) fatalWin32("EndPaint", win32.GetLastError()); + _ = win32.BeginPaint(hwnd, &ps) orelse return win32.panicWin32("BeginPaint", win32.GetLastError()); + defer if (0 == win32.EndPaint(hwnd, &ps)) win32.panicWin32("EndPaint", win32.GetLastError()); global.render_cells.resize( global.render_cells_arena.allocator(), @@ -1343,19 +1343,13 @@ fn renderColorFromVaxis(color: vaxis.Color) render.Color { }; } -fn fatalWin32(what: []const u8, err: win32.WIN32_ERROR) noreturn { - std.debug.panic("{s} failed with {}", .{ what, err.fmt() }); -} -fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { - std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); -} fn deleteObject(obj: ?win32.HGDIOBJ) void { - if (0 == win32.DeleteObject(obj)) fatalWin32("DeleteObject", win32.GetLastError()); + if (0 == win32.DeleteObject(obj)) win32.panicWin32("DeleteObject", win32.GetLastError()); } fn getClientSize(comptime T: type, hwnd: win32.HWND) XY(T) { var rect: win32.RECT = undefined; if (0 == win32.GetClientRect(hwnd, &rect)) - fatalWin32("GetClientRect", win32.GetLastError()); + win32.panicWin32("GetClientRect", win32.GetLastError()); std.debug.assert(rect.left == 0); std.debug.assert(rect.top == 0); return .{ .x = @intCast(rect.right), .y = @intCast(rect.bottom) }; @@ -1428,7 +1422,7 @@ fn getClientInset(dpi: u32) XY(i32) { 0, window_style_ex, dpi, - )) fatalWin32( + )) win32.panicWin32( "AdjustWindowRect", win32.GetLastError(), ); @@ -1456,5 +1450,5 @@ fn setWindowPosRect(hwnd: win32.HWND, rect: win32.RECT) void { rect.right - rect.left, rect.bottom - rect.top, .{ .NOZORDER = 1 }, - )) fatalWin32("SetWindowPos", win32.GetLastError()); + )) win32.panicWin32("SetWindowPos", win32.GetLastError()); } diff --git a/src/win32/win32ext.zig b/src/win32/win32ext.zig index f70d244..fc93085 100644 --- a/src/win32/win32ext.zig +++ b/src/win32/win32ext.zig @@ -1,17 +1,6 @@ const std = @import("std"); const win32 = @import("win32").everything; -// todo: these should be available in zigwin32 -fn xFromLparam(lparam: win32.LPARAM) i16 { - return @bitCast(win32.loword(lparam)); -} -fn yFromLparam(lparam: win32.LPARAM) i16 { - return @bitCast(win32.hiword(lparam)); -} -pub fn pointFromLparam(lparam: win32.LPARAM) win32.POINT { - return win32.POINT{ .x = xFromLparam(lparam), .y = yFromLparam(lparam) }; -} - // TODO: update zigwin32 with a way to get the corresponding IID for any COM interface pub fn queryInterface(obj: anytype, comptime Interface: type) *Interface { const obj_basename_start: usize = comptime if (std.mem.lastIndexOfScalar(u8, @typeName(@TypeOf(obj)), '.')) |i| (i + 1) else 0; From 672f85a58c91bb47b089dede8a45070c931158ee Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 12:50:21 +0100 Subject: [PATCH 11/20] build: make contrib/make_release callable from a different destdir --- contrib/make_release | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contrib/make_release b/contrib/make_release index 6d57d73..437c332 100755 --- a/contrib/make_release +++ b/contrib/make_release @@ -1,12 +1,13 @@ #!/bin/bash set -e +DESTDIR="$(pwd)/release" BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" APPNAME="$(basename "$BASEDIR")" cd "$BASEDIR" -if [ -e "release" ]; then +if [ -e "$DESTDIR" ]; then echo directory \"release\" already exists exit 1 fi @@ -17,9 +18,9 @@ echo running tests... echo building... -./zig build -Dpackage_release --prefix release/build +./zig build -Dpackage_release --prefix "$DESTDIR/build" -cd release/build +cd "$DESTDIR/build" VERSION=$(/bin/cat version) TARGETS=$(/bin/ls) @@ -44,7 +45,7 @@ for tarfile in $TARFILES; do sha256sum -b "$tarfile" > "${tarfile}.sha256" done -echo "done making release $VERSION" +echo "done making release $VERSION @ $DESTDIR" echo /bin/ls -lah From ba5f15dab830ca5b0694b4eb1e89ca694b84f388 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 20:23:47 +0100 Subject: [PATCH 12/20] feat(buffers): define explicit errors for Buffer load functions --- src/buffer/Buffer.zig | 75 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 964f483..38912d4 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -143,7 +143,7 @@ pub const Leaf = struct { bol: bool = true, eol: bool = true, - fn new(allocator: Allocator, piece: []const u8, bol: bool, eol: bool) !*const Node { + fn new(allocator: Allocator, piece: []const u8, bol: bool, eol: bool) error{OutOfMemory}!*const Node { if (piece.len == 0) return if (!bol and !eol) &empty_leaf else if (bol and !eol) &empty_bol_leaf else if (!bol and eol) &empty_eol_leaf else &empty_line_leaf; const node = try allocator.create(Node); @@ -1043,7 +1043,7 @@ const Node = union(enum) { } }; -pub fn create(allocator: Allocator) !*Self { +pub fn create(allocator: Allocator) error{OutOfMemory}!*Self { const self = try allocator.create(Self); const arena_a = if (builtin.is_test) allocator else std.heap.page_allocator; self.* = .{ @@ -1062,12 +1062,23 @@ pub fn deinit(self: *Self) void { self.external_allocator.destroy(self); } -fn new_file(self: *const Self, file_exists: *bool) !Root { +fn new_file(self: *const Self, file_exists: *bool) error{OutOfMemory}!Root { file_exists.* = false; return Leaf.new(self.allocator, "", true, false); } -pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode, utf8_sanitized: *bool) !Root { +pub fn LoadError(comptime reader_error: anytype) type { + return error{ + OutOfMemory, + BufferUnderrun, + DanglingSurrogateHalf, + ExpectedSecondSurrogateHalf, + UnexpectedSecondSurrogateHalf, + Unexpected, + } || reader_error; +} + +pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode, utf8_sanitized: *bool) LoadError(@TypeOf(reader).Error)!Root { const lf = '\n'; const cr = '\r'; var buf = try self.external_allocator.alloc(u8, size); @@ -1117,12 +1128,14 @@ pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode, return Node.merge_in_place(leaves, self.allocator); } -pub fn load_from_string(self: *const Self, s: []const u8, eol_mode: *EolMode, utf8_sanitized: *bool) !Root { +pub const LoadFromStringError = LoadError(error{}); + +pub fn load_from_string(self: *const Self, s: []const u8, eol_mode: *EolMode, utf8_sanitized: *bool) LoadFromStringError!Root { var stream = std.io.fixedBufferStream(s); return self.load(stream.reader(), s.len, eol_mode, utf8_sanitized); } -pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []const u8) !void { +pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []const u8) LoadFromStringError!void { self.root = try self.load_from_string(s, &self.file_eol_mode, &self.file_utf8_sanitized); self.file_path = try self.allocator.dupe(u8, file_path); self.last_save = self.root; @@ -1130,7 +1143,53 @@ pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []cons self.file_exists = false; } -pub fn load_from_file(self: *const Self, file_path: []const u8, file_exists: *bool, eol_mode: *EolMode, utf8_sanitized: *bool) !Root { +pub const LoadFromFileError = error{ + OutOfMemory, + Unexpected, + FileTooBig, + NoSpaceLeft, + DeviceBusy, + AccessDenied, + SystemResources, + WouldBlock, + IsDir, + SharingViolation, + PathAlreadyExists, + FileNotFound, + PipeBusy, + NameTooLong, + InvalidUtf8, + InvalidWtf8, + BadPathName, + NetworkNotFound, + AntivirusInterference, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + NotDir, + FileLocksNotSupported, + FileBusy, + InputOutput, + BrokenPipe, + OperationAborted, + ConnectionResetByPeer, + ConnectionTimedOut, + NotOpenForReading, + SocketNotConnected, + BufferUnderrun, + DanglingSurrogateHalf, + ExpectedSecondSurrogateHalf, + UnexpectedSecondSurrogateHalf, +}; + +pub fn load_from_file( + self: *const Self, + file_path: []const u8, + file_exists: *bool, + eol_mode: *EolMode, + utf8_sanitized: *bool, +) LoadFromFileError!Root { const file = cwd().openFile(file_path, .{ .mode = .read_only }) catch |e| switch (e) { error.FileNotFound => return self.new_file(file_exists), else => return e, @@ -1142,7 +1201,7 @@ pub fn load_from_file(self: *const Self, file_path: []const u8, file_exists: *bo return self.load(file.reader(), @intCast(stat.size), eol_mode, utf8_sanitized); } -pub fn load_from_file_and_update(self: *Self, file_path: []const u8) !void { +pub fn load_from_file_and_update(self: *Self, file_path: []const u8) LoadFromFileError!void { var file_exists: bool = false; var eol_mode: EolMode = .lf; var utf8_sanitized: bool = false; From e404c830c901c293bf00750da7be492c37e26ab2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 21:40:27 +0100 Subject: [PATCH 13/20] feat(buffers): define explicit errors for Buffer store functions --- src/buffer/Buffer.zig | 47 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 38912d4..1eb1294 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1220,7 +1220,7 @@ pub fn store_to_string(self: *const Self, allocator: Allocator, eol_mode: EolMod return s.toOwnedSlice(); } -fn store_to_file_const(self: *const Self, file: anytype) !void { +fn store_to_file_const(self: *const Self, file: anytype) StoreToFileError!void { const buffer_size = 4096 * 16; // 64KB const BufferedWriter = std.io.BufferedWriter(buffer_size, std.fs.File.Writer); const Writer = std.io.Writer(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write); @@ -1232,7 +1232,46 @@ fn store_to_file_const(self: *const Self, file: anytype) !void { try buffered_writer.flush(); } -pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) !void { +pub const StoreToFileError = error{ + AccessDenied, + AntivirusInterference, + BadPathName, + BrokenPipe, + ConnectionResetByPeer, + DeviceBusy, + DiskQuota, + FileBusy, + FileLocksNotSupported, + FileNotFound, + FileTooBig, + InputOutput, + InvalidArgument, + InvalidUtf8, + InvalidWtf8, + IsDir, + LinkQuotaExceeded, + LockViolation, + NameTooLong, + NetworkNotFound, + NoDevice, + NoSpaceLeft, + NotDir, + NotOpenForWriting, + OperationAborted, + PathAlreadyExists, + PipeBusy, + ProcessFdQuotaExceeded, + ReadOnlyFileSystem, + RenameAcrossMountPoints, + SharingViolation, + SymLinkLoop, + SystemFdQuotaExceeded, + SystemResources, + Unexpected, + WouldBlock, +}; + +pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) StoreToFileError!void { const stat = try cwd().statFile(file_path); var atomic = try cwd().atomicFile(file_path, .{ .mode = stat.mode }); defer atomic.deinit(); @@ -1240,13 +1279,13 @@ pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) !v try atomic.finish(); } -pub fn store_to_new_file_const(self: *const Self, file_path: []const u8) !void { +pub fn store_to_new_file_const(self: *const Self, file_path: []const u8) StoreToFileError!void { const file = try cwd().createFile(file_path, .{ .read = true, .truncate = true }); defer file.close(); try self.store_to_file_const(file); } -pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) !void { +pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) StoreToFileError!void { self.store_to_existing_file_const(file_path) catch |e| switch (e) { error.FileNotFound => try self.store_to_new_file_const(file_path), else => return e, From fbeaefe7ff062a3980923b0e3b0a35421d6440ac Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 21:40:51 +0100 Subject: [PATCH 14/20] feat(buffers): add reset_to_last_saved buffer function --- src/buffer/Buffer.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 1eb1294..f3a17c7 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1214,6 +1214,13 @@ pub fn load_from_file_and_update(self: *Self, file_path: []const u8) LoadFromFil self.last_save_eol_mode = eol_mode; } +pub fn reset_to_last_saved(self: *Self) void { + if (self.last_save) |last_save| { + self.store_undo(&[_]u8{}) catch {}; + self.root = last_save; + } +} + pub fn store_to_string(self: *const Self, allocator: Allocator, eol_mode: EolMode) ![]u8 { var s = try ArrayList(u8).initCapacity(allocator, self.root.weights_sum().len); try self.root.store(s.writer(), eol_mode); From aa1e0674ccc032082071c3491e33c45076950f1b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 21:42:36 +0100 Subject: [PATCH 15/20] feat(buffers): move buffer lifetime management to new Buffer.Manager module --- src/buffer/Buffer.zig | 1 + src/buffer/Manager.zig | 79 +++++++++++++++++++++++++++++++++ src/tui/editor.zig | 51 +++++++-------------- src/tui/mainview.zig | 21 ++++----- src/tui/mode/mini/open_file.zig | 1 - src/tui/tui.zig | 4 ++ 6 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 src/buffer/Manager.zig diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index f3a17c7..b5779e4 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -10,6 +10,7 @@ const max_imbalance = 7; pub const Root = *const Node; pub const unicode = @import("unicode.zig"); +pub const Manager = @import("Manager.zig"); pub const Cursor = @import("Cursor.zig"); pub const View = @import("View.zig"); pub const Selection = @import("Selection.zig"); diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig new file mode 100644 index 0000000..910e389 --- /dev/null +++ b/src/buffer/Manager.zig @@ -0,0 +1,79 @@ +const std = @import("std"); +const Buffer = @import("Buffer.zig"); + +const Self = @This(); + +allocator: std.mem.Allocator, +buffers: std.StringHashMapUnmanaged(*Buffer), + +pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .allocator = allocator, + .buffers = .{}, + }; +} + +pub fn deinit(self: *Self) void { + var i = self.buffers.iterator(); + while (i.next()) |p| { + self.allocator.free(p.key_ptr.*); + p.value_ptr.*.deinit(); + } + self.buffers.deinit(self.allocator); +} + +pub fn open_file(self: *Self, file_path: []const u8) Buffer.LoadFromFileError!*Buffer { + if (self.buffers.get(file_path)) |buffer| { + return buffer; + } else { + var buffer = try Buffer.create(self.allocator); + errdefer buffer.deinit(); + try buffer.load_from_file_and_update(file_path); + try self.buffers.put(self.allocator, try self.allocator.dupe(u8, file_path), buffer); + return buffer; + } +} + +pub fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) Buffer.LoadFromStringError!*Buffer { + if (self.buffers.get(file_path)) |buffer| { + return buffer; + } else { + var buffer = try Buffer.create(self.allocator); + errdefer buffer.deinit(); + try buffer.load_from_string_and_update(file_path, content); + buffer.file_exists = true; + try self.buffers.put(self.allocator, try self.allocator.dupe(u8, file_path), buffer); + return buffer; + } +} + +pub fn retire(self: *Self, buffer: *Buffer) void { + _ = self; + _ = buffer; +} + +pub fn list(self: *Self, allocator: std.mem.Allocator) []*const Buffer { + _ = self; + _ = allocator; + unreachable; +} + +pub fn is_dirty(self: *const Self) bool { + var i = self.buffers.iterator(); + while (i.next()) |kv| + if (kv.value_ptr.*.is_dirty()) + return true; + return false; +} + +pub fn is_buffer_dirty(self: *const Self, file_path: []const u8) bool { + return if (self.buffers.get(file_path)) |buffer| buffer.is_dirty() else false; +} + +pub fn save_all(self: *const Self) Buffer.StoreToFileError!void { + var i = self.buffers.iterator(); + while (i.next()) |kv| { + const buffer = kv.value_ptr.*; + try buffer.store_to_file_and_clean(buffer.file_path); + } +} diff --git a/src/tui/editor.zig b/src/tui/editor.zig index c9a64b3..b5665dc 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -218,6 +218,7 @@ pub const Editor = struct { file_path: ?[]const u8, buffer: ?*Buffer, + buffer_manager: *Buffer.Manager, lsp_version: usize = 1, pause_undo: bool = false, @@ -358,7 +359,7 @@ pub const Editor = struct { self.clamp(); } - fn init(self: *Self, allocator: Allocator, n: Plane) void { + fn init(self: *Self, allocator: Allocator, n: Plane, buffer_manager: *Buffer.Manager) void { const logger = log.logger("editor"); const frame_rate = tp.env.get().num("frame-rate"); const indent_size = tui.current().config.indent_size; @@ -372,6 +373,7 @@ pub const Editor = struct { .logger = logger, .file_path = null, .buffer = null, + .buffer_manager = buffer_manager, .handlers = EventHandler.List.init(allocator), .animation_lag = get_animation_max_lag(), .animation_frame_rate = frame_rate, @@ -393,7 +395,7 @@ pub const Editor = struct { self.matches.deinit(); self.handlers.deinit(); self.logger.deinit(); - if (self.buffer) |p| p.deinit(); + if (self.buffer) |p| self.buffer_manager.retire(p); if (self.case_data) |cd| cd.deinit(); } @@ -459,28 +461,16 @@ pub const Editor = struct { self.view.cols = pos.w; } - pub fn is_dirty(self: *Self) bool { - const b = self.buffer orelse return false; - return b.is_dirty(); - } - fn open(self: *Self, file_path: []const u8) !void { - var new_buf = try Buffer.create(self.allocator); - errdefer new_buf.deinit(); - try new_buf.load_from_file_and_update(file_path); - return self.open_buffer(file_path, new_buf); + return self.open_buffer(file_path, try self.buffer_manager.open_file(file_path)); } fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) !void { - var new_buf = try Buffer.create(self.allocator); - errdefer new_buf.deinit(); - try new_buf.load_from_string_and_update(file_path, content); - new_buf.file_exists = true; - return self.open_buffer(file_path, new_buf); + return self.open_buffer(file_path, try self.buffer_manager.open_scratch(file_path, content)); } fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer) !void { - errdefer new_buf.deinit(); + errdefer self.buffer_manager.retire(new_buf); self.cancel_all_selections(); self.get_primary().reset(); self.file_path = try self.allocator.dupe(u8, file_path); @@ -519,17 +509,7 @@ pub const Editor = struct { } fn close(self: *Self) !void { - return self.close_internal(false); - } - - fn close_dirty(self: *Self) !void { - return self.close_internal(true); - } - - fn close_internal(self: *Self, allow_dirty_close: bool) !void { - const b = self.buffer orelse return error.Stop; - if (!allow_dirty_close and b.is_dirty()) return tp.exit("unsaved changes"); - if (self.buffer) |b_mut| b_mut.deinit(); + if (self.buffer) |b_mut| self.buffer_manager.retire(b_mut); self.buffer = null; self.plane.erase(); self.plane.home(); @@ -3770,7 +3750,8 @@ pub const Editor = struct { pub fn close_file_without_saving(self: *Self, _: Context) Result { self.cancel_all_selections(); - try self.close_dirty(); + if (self.buffer) |buffer| buffer.reset_to_last_saved(); + try self.close(); } pub const close_file_without_saving_meta = .{ .description = "Close file without saving" }; @@ -4759,8 +4740,8 @@ pub const Editor = struct { pub const set_file_type_meta = .{ .arguments = &.{.string} }; }; -pub fn create(allocator: Allocator, parent: Widget) !Widget { - return EditorWidget.create(allocator, parent); +pub fn create(allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !Widget { + return EditorWidget.create(allocator, parent, buffer_manager); } pub const EditorWidget = struct { @@ -4782,10 +4763,10 @@ pub const EditorWidget = struct { const Self = @This(); const Commands = command.Collection(Editor); - fn create(allocator: Allocator, parent: Widget) !Widget { + fn create(allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !Widget { const container = try WidgetList.createH(allocator, parent, "editor.container", .dynamic); const self: *Self = try allocator.create(Self); - try self.init(allocator, container.widget()); + try self.init(allocator, container.widget(), buffer_manager); try self.commands.init(&self.editor); const editorWidget = Widget.to(self); try container.add(try editor_gutter.create(allocator, container.widget(), editorWidget, &self.editor)); @@ -4794,7 +4775,7 @@ pub const EditorWidget = struct { return container.widget(); } - fn init(self: *Self, allocator: Allocator, parent: Widget) !void { + fn init(self: *Self, allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !void { var n = try Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*); errdefer n.deinit(); @@ -4803,7 +4784,7 @@ pub const EditorWidget = struct { .plane = n, .editor = undefined, }; - self.editor.init(allocator, n); + self.editor.init(allocator, n, buffer_manager); errdefer self.editor.deinit(); try self.editor.push_cursor(); } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 9727898..d1cdcdf 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -14,6 +14,7 @@ const build_options = @import("build_options"); const Plane = @import("renderer").Plane; const input = @import("input"); const command = @import("command"); +const BufferManager = @import("Buffer").Manager; const tui = @import("tui.zig"); const Box = @import("Box.zig"); @@ -47,6 +48,7 @@ active_view: ?usize = 0, panels: ?*WidgetList = null, last_match_text: ?[]const u8 = null, location_history: location_history, +buffer_manager: BufferManager, file_stack: std.ArrayList([]const u8), find_in_files_state: enum { init, adding, done } = .done, file_list_type: FileListType = .find_in_files, @@ -70,6 +72,7 @@ pub fn create(allocator: std.mem.Allocator) !Widget { .file_stack = std.ArrayList([]const u8).init(allocator), .views = undefined, .views_widget = undefined, + .buffer_manager = BufferManager.init(allocator), }; try self.commands.init(self); const w = Widget.to(self); @@ -104,6 +107,7 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { self.commands.deinit(); self.widgets.deinit(allocator); self.floating_views.deinit(); + self.buffer_manager.deinit(); allocator.destroy(self); } @@ -237,15 +241,8 @@ fn toggle_view(self: *Self, view: anytype) !void { } fn check_all_not_dirty(self: *const Self) command.Result { - for (self.editors.items) |editor| - if (editor.is_dirty()) - return tp.exit("unsaved changes"); -} - -fn check_active_not_dirty(self: *const Self) command.Result { - if (self.active_editor) |idx| - if (self.editors.items[idx].is_dirty()) - return tp.exit("unsaved changes"); + if (self.buffer_manager.is_dirty()) + return tp.exit("unsaved changes"); } const cmds = struct { @@ -296,6 +293,8 @@ const cmds = struct { self.clear_find_in_files_results(.diagnostics); if (self.file_list_type == .diagnostics and self.is_panel_view_showing(filelist_view)) try self.toggle_panel_view(filelist_view, false); + self.buffer_manager.deinit(); + self.buffer_manager = BufferManager.init(self.allocator); try project_manager.open(project_dir); const project = tp.env.get().str("project"); tui.current().rdr.set_terminal_working_directory(project); @@ -352,7 +351,6 @@ const cmds = struct { if (!same_file) { if (self.get_active_editor()) |editor| { - try self.check_active_not_dirty(); editor.send_editor_jump_source() catch {}; } try self.create_editor(); @@ -395,7 +393,6 @@ const cmds = struct { pub const open_gui_config_meta = .{ .description = "Edit gui configuration file" }; pub fn create_scratch_buffer(self: *Self, ctx: Ctx) Result { - try self.check_all_not_dirty(); tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", ctx); @@ -863,7 +860,7 @@ fn create_editor(self: *Self) !void { if (self.get_active_file_path()) |file_path| self.push_file_stack(file_path) catch {}; try self.delete_active_view(); command.executeName("enter_mode_default", .{}) catch {}; - var editor_widget = try ed.create(self.allocator, Widget.to(self)); + var editor_widget = try ed.create(self.allocator, Widget.to(self), &self.buffer_manager); errdefer editor_widget.deinit(self.allocator); const editor = editor_widget.get("editor") orelse @panic("mainview editor not found"); if (self.top_bar) |bar| editor.subscribe(EventHandler.to_unowned(bar)) catch @panic("subscribe unsupported"); diff --git a/src/tui/mode/mini/open_file.zig b/src/tui/mode/mini/open_file.zig index 407ffeb..de60c07 100644 --- a/src/tui/mode/mini/open_file.zig +++ b/src/tui/mode/mini/open_file.zig @@ -11,7 +11,6 @@ pub const create = Type.create; pub fn load_entries(self: *Type) error{ Exit, OutOfMemory }!void { const editor = tui.get_active_editor() orelse return; - if (editor.is_dirty()) return tp.exit("unsaved changes"); if (editor.file_path) |old_path| if (std.mem.lastIndexOf(u8, old_path, "/")) |pos| try self.file_path.appendSlice(old_path[0 .. pos + 1]); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 7677103..2b1d28c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -991,6 +991,10 @@ pub fn get_active_selection(allocator: std.mem.Allocator) ?[]u8 { return editor.get_selection(sel, allocator) catch null; } +pub fn get_buffer_manager() ?*@import("Buffer").Manager { + return if (current().mainview.dynamic_cast(mainview)) |mv_| &mv_.buffer_manager else null; +} + fn context_check() void { if (instance_ == null) @panic("tui call out of context"); } From 661808f31658a1c3b350ca6f5740ca21f2320c68 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 21:43:35 +0100 Subject: [PATCH 16/20] feat(buffers): render buffer dirty state in open recent files palette --- src/tui/mode/overlay/open_recent.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 0c4c5b7..cf490c1 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -10,6 +10,7 @@ const keybind = @import("keybind"); const project_manager = @import("project_manager"); const command = @import("command"); const EventHandler = @import("EventHandler"); +const BufferManager = @import("Buffer").Manager; const tui = @import("../../tui.zig"); const MessageFilter = @import("../../MessageFilter.zig"); @@ -34,6 +35,7 @@ need_reset: bool = false, need_select_first: bool = true, longest: usize = 0, commands: Commands = undefined, +buffer_manager: ?*BufferManager, pub fn create(allocator: std.mem.Allocator) !tui.Mode { const mv = tui.current().mainview.dynamic_cast(mainview) orelse return error.NotFound; @@ -51,6 +53,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .ctx = self, .label = "Search files by name", }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, + .buffer_manager = tui.get_buffer_manager(), }; try self.commands.init(self); try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager)); @@ -93,7 +96,7 @@ inline fn max_menu_width() usize { return @max(15, width - (width / 5)); } -fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { +fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { const style_base = theme.editor_widget; const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base; @@ -110,7 +113,8 @@ fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *c if (!(cbor.matchString(&iter, &file_path) catch false)) file_path = "#ERROR#"; button.plane.set_style(style_keybind); - const pointer = if (selected) "⏵" else " "; + const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " "; + const pointer = if (selected) "⏵" else dirty; _ = button.plane.print("{s}", .{pointer}) catch {}; var buf: [std.fs.max_path_bytes]u8 = undefined; var removed_prefix: usize = 0; From 4dddadb46d9952dda71e12c491256fc9d515c2a3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 21:44:05 +0100 Subject: [PATCH 17/20] feat(buffers): add home screen save all command --- src/tui/home.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tui/home.zig b/src/tui/home.zig index afba9f0..079e67a 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -245,6 +245,12 @@ const cmds = struct { const Ctx = command.Context; const Result = command.Result; + pub fn save_all(_: *Self, _: Ctx) Result { + if (tui.get_buffer_manager()) |bm| + bm.save_all() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const save_all_meta = .{ .description = "Save all changed files" }; + pub fn home_menu_down(self: *Self, _: Ctx) Result { self.menu.select_down(); } From efb3ab42fde4c43de11121819e62732515e3df5e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 21:59:55 +0100 Subject: [PATCH 18/20] feat(buffers): add tracking of last modified and last used times for buffers --- src/buffer/Buffer.zig | 15 +++++++++++++++ src/buffer/Manager.zig | 42 +++++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index b5779e4..215a179 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -44,6 +44,9 @@ undo_history: ?*UndoNode = null, redo_history: ?*UndoNode = null, curr_history: ?*UndoNode = null, +mtime: i64, +utime: i64, + pub const EolMode = enum { lf, crlf }; pub const EolModeTag = @typeInfo(EolMode).Enum.tag_type; @@ -1052,6 +1055,8 @@ pub fn create(allocator: Allocator) error{OutOfMemory}!*Self { .allocator = self.arena.allocator(), .external_allocator = allocator, .root = try Node.new(self.allocator, &empty_leaf, &empty_leaf), + .mtime = std.time.milliTimestamp(), + .utime = std.time.milliTimestamp(), }; return self; } @@ -1063,6 +1068,10 @@ pub fn deinit(self: *Self) void { self.external_allocator.destroy(self); } +pub fn update_last_used_time(self: *Self) void { + self.utime = std.time.milliTimestamp(); +} + fn new_file(self: *const Self, file_exists: *bool) error{OutOfMemory}!Root { file_exists.* = false; return Leaf.new(self.allocator, "", true, false); @@ -1142,6 +1151,7 @@ pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []cons self.last_save = self.root; self.last_save_eol_mode = self.file_eol_mode; self.file_exists = false; + self.mtime = std.time.milliTimestamp(); } pub const LoadFromFileError = error{ @@ -1213,12 +1223,14 @@ pub fn load_from_file_and_update(self: *Self, file_path: []const u8) LoadFromFil self.file_eol_mode = eol_mode; self.file_utf8_sanitized = utf8_sanitized; self.last_save_eol_mode = eol_mode; + self.mtime = std.time.milliTimestamp(); } pub fn reset_to_last_saved(self: *Self) void { if (self.last_save) |last_save| { self.store_undo(&[_]u8{}) catch {}; self.root = last_save; + self.mtime = std.time.milliTimestamp(); } } @@ -1319,6 +1331,7 @@ pub fn version(self: *const Self) usize { pub fn update(self: *Self, root: Root) void { self.root = root; + self.mtime = std.time.milliTimestamp(); } pub fn store_undo(self: *Self, meta: []const u8) !void { @@ -1369,6 +1382,7 @@ pub fn undo(self: *Self, meta: []const u8) error{Stop}![]const u8 { self.curr_history = h; self.root = h.root; self.push_redo(r); + self.mtime = std.time.milliTimestamp(); return h.meta; } @@ -1380,5 +1394,6 @@ pub fn redo(self: *Self) error{Stop}![]const u8 { self.curr_history = h; self.root = h.root; self.push_undo(u); + self.mtime = std.time.milliTimestamp(); return h.meta; } diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 910e389..26a1448 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -23,39 +23,47 @@ pub fn deinit(self: *Self) void { } pub fn open_file(self: *Self, file_path: []const u8) Buffer.LoadFromFileError!*Buffer { - if (self.buffers.get(file_path)) |buffer| { - return buffer; - } else { + const buffer = if (self.buffers.get(file_path)) |buffer| buffer else blk: { var buffer = try Buffer.create(self.allocator); errdefer buffer.deinit(); try buffer.load_from_file_and_update(file_path); try self.buffers.put(self.allocator, try self.allocator.dupe(u8, file_path), buffer); - return buffer; - } + break :blk buffer; + }; + buffer.update_last_used_time(); + return buffer; } pub fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) Buffer.LoadFromStringError!*Buffer { - if (self.buffers.get(file_path)) |buffer| { - return buffer; - } else { + const buffer = if (self.buffers.get(file_path)) |buffer| buffer else blk: { var buffer = try Buffer.create(self.allocator); errdefer buffer.deinit(); try buffer.load_from_string_and_update(file_path, content); buffer.file_exists = true; try self.buffers.put(self.allocator, try self.allocator.dupe(u8, file_path), buffer); - return buffer; - } + break :blk buffer; + }; + buffer.update_last_used_time(); + return buffer; } -pub fn retire(self: *Self, buffer: *Buffer) void { - _ = self; - _ = buffer; +pub fn retire(_: *Self, buffer: *Buffer) void { + buffer.update_last_used_time(); } -pub fn list(self: *Self, allocator: std.mem.Allocator) []*const Buffer { - _ = self; - _ = allocator; - unreachable; +pub fn list(self: *Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*const Buffer { + var buffers: std.ArrayListUnmanaged([]*const Buffer) = .{}; + var i = self.buffers.iterator(); + while (i.next()) |kv| + (try buffers.addOne()).* = kv.value_ptr.*; + + std.mem.sort(*Buffer, buffers.items, {}, struct { + fn less_fn(_: void, lhs: *Buffer, rhs: *Buffer) bool { + return lhs.mtime > rhs.mtime; + } + }.less_fn); + + return buffers.toOwnedSlice(allocator); } pub fn is_dirty(self: *const Self) bool { From 3f06f6b19c53d3f1580fbcc1291e14190c1578cd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 22:29:39 +0100 Subject: [PATCH 19/20] feat(buffers): add buffer switcher --- src/buffer/Manager.zig | 8 ++--- src/keybind/builtin/flow.json | 2 ++ src/tui/mode/overlay/buffer_palette.zig | 44 +++++++++++++++++++++++++ src/tui/tui.zig | 5 +++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/tui/mode/overlay/buffer_palette.zig diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 26a1448..6b68e78 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -51,15 +51,15 @@ pub fn retire(_: *Self, buffer: *Buffer) void { buffer.update_last_used_time(); } -pub fn list(self: *Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*const Buffer { - var buffers: std.ArrayListUnmanaged([]*const Buffer) = .{}; +pub fn list_most_recently_used(self: *Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*Buffer { + var buffers: std.ArrayListUnmanaged(*Buffer) = .{}; var i = self.buffers.iterator(); while (i.next()) |kv| - (try buffers.addOne()).* = kv.value_ptr.*; + (try buffers.addOne(allocator)).* = kv.value_ptr.*; std.mem.sort(*Buffer, buffers.items, {}, struct { fn less_fn(_: void, lhs: *Buffer, rhs: *Buffer) bool { - return lhs.mtime > rhs.mtime; + return lhs.utime > rhs.utime; } }.less_fn); diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 33c5ec9..6876aee 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -1,6 +1,7 @@ { "project": { "press": [ + ["ctrl+shift+e", "switch_buffers"], ["ctrl+0", "reset_fontsize"], ["ctrl+plus", "adjust_fontsize", 1.0], ["ctrl+minus", "adjust_fontsize", -1.0], @@ -250,6 +251,7 @@ ["ctrl+p", "palette_menu_up"], ["ctrl+n", "palette_menu_down"], ["ctrl+e", "palette_menu_down"], + ["ctrl+shift+e", "palette_menu_down"], ["ctrl+r", "palette_menu_down"], ["ctrl+t", "palette_menu_down"], ["ctrl+v", "system_paste"], diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig new file mode 100644 index 0000000..a8cff8e --- /dev/null +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const root = @import("root"); +const command = @import("command"); + +const tui = @import("../../tui.zig"); +pub const Type = @import("palette.zig").Create(@This()); + +pub const label = "Switch buffers"; +pub const name = " buffer"; +pub const description = "buffer"; + +pub const Entry = struct { + label: []const u8, +}; + +pub fn load_entries(palette: *Type) !usize { + const buffer_manager = tui.get_buffer_manager() orelse return 0; + const buffers = try buffer_manager.list_most_recently_used(palette.allocator); + defer palette.allocator.free(buffers); + for (buffers) |buffer| + (try palette.entries.addOne()).* = .{ .label = buffer.file_path }; + return if (palette.entries.items.len == 0) label.len else 0; +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.allocator); + defer value.deinit(); + const writer = value.writer(); + try cbor.writeValue(writer, entry.label); + try cbor.writeValue(writer, matches orelse &[_]usize{}); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { + var file_path: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &file_path) catch false)) return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); + tp.self_pid().send(.{ "cmd", "navigate", .{} }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); + tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 2b1d28c..fc385ea 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -815,6 +815,11 @@ const cmds = struct { } pub const open_recent_project_meta = .{ .description = "Open recent project" }; + pub fn switch_buffers(self: *Self, _: Ctx) Result { + return self.enter_overlay_mode(@import("mode/overlay/buffer_palette.zig").Type); + } + pub const switch_buffers_meta = .{ .description = "Switch buffers" }; + pub fn change_theme(self: *Self, _: Ctx) Result { return self.enter_overlay_mode(@import("mode/overlay/theme_palette.zig").Type); } From 84655c4ff8b38b01deb0b194ff2a19e945b6e702 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 21 Jan 2025 22:53:09 +0100 Subject: [PATCH 20/20] feat(buffers): add dirty state indicator to buffer switcher --- src/tui/mode/overlay/buffer_palette.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig index a8cff8e..96751a9 100644 --- a/src/tui/mode/overlay/buffer_palette.zig +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -10,18 +10,22 @@ pub const Type = @import("palette.zig").Create(@This()); pub const label = "Switch buffers"; pub const name = " buffer"; pub const description = "buffer"; +const dirty_indicator = ""; pub const Entry = struct { label: []const u8, + hint: []const u8, }; pub fn load_entries(palette: *Type) !usize { const buffer_manager = tui.get_buffer_manager() orelse return 0; const buffers = try buffer_manager.list_most_recently_used(palette.allocator); defer palette.allocator.free(buffers); - for (buffers) |buffer| - (try palette.entries.addOne()).* = .{ .label = buffer.file_path }; - return if (palette.entries.items.len == 0) label.len else 0; + for (buffers) |buffer| { + const hint = if (buffer.is_dirty()) dirty_indicator else ""; + (try palette.entries.addOne()).* = .{ .label = buffer.file_path, .hint = hint }; + } + return if (palette.entries.items.len == 0) label.len else 2; } pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { @@ -29,6 +33,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v defer value.deinit(); const writer = value.writer(); try cbor.writeValue(writer, entry.label); + try cbor.writeValue(writer, entry.hint); try cbor.writeValue(writer, matches orelse &[_]usize{}); try palette.menu.add_item_with_handler(value.items, select); palette.items += 1;