From 634a18cb5685a3c3fcfc08301306e628d33c3256 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 13 Oct 2025 19:50:04 +0200 Subject: [PATCH] feat: add clipboard history palette --- src/tui/mainview.zig | 5 ++ src/tui/mode/overlay/clipboard_palette.zig | 73 ++++++++++++++++++++++ src/tui/tui.zig | 10 +++ 3 files changed, 88 insertions(+) create mode 100644 src/tui/mode/overlay/clipboard_palette.zig diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 9fe0104..3b3dcb8 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1018,6 +1018,11 @@ const cmds = struct { } pub const system_paste_meta: Meta = .{ .description = "Paste from system clipboard" }; + pub fn paste_history(_: *Self, _: Ctx) Result { + return try tui.open_overlay(@import("mode/overlay/clipboard_palette.zig").Type); + } + pub const paste_history_meta: Meta = .{ .description = "Paste from clipboard history" }; + pub fn find_in_files_query(self: *Self, ctx: Ctx) Result { var query: []const u8 = undefined; if (!try ctx.args.match(.{tp.extract(&query)})) return error.InvalidFindInFilesQueryArgument; diff --git a/src/tui/mode/overlay/clipboard_palette.zig b/src/tui/mode/overlay/clipboard_palette.zig new file mode 100644 index 0000000..1830065 --- /dev/null +++ b/src/tui/mode/overlay/clipboard_palette.zig @@ -0,0 +1,73 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const root = @import("soft_root").root; +const command = @import("command"); + +const tui = @import("../../tui.zig"); +pub const Type = @import("palette.zig").Create(@This()); +const module_name = @typeName(@This()); + +pub const label = "Clipboard history"; +pub const name = " clipboard"; +pub const description = "clipboard"; +pub const icon = " "; + +pub const Entry = struct { + label: []const u8, + idx: usize, +}; + +pub fn load_entries(palette: *Type) !usize { + const history = tui.clipboard_get_history() orelse &.{}; + + if (history.len > 0) { + var idx = history.len - 1; + while (true) : (idx -= 1) { + (try palette.entries.addOne(palette.allocator)).* = .{ + .label = history[idx], + .idx = idx, + }; + if (idx == 0) break; + } + } + return if (palette.entries.items.len == 0) label.len + 3 else 4; +} + +pub fn clear_entries(palette: *Type) void { + palette.entries.clearRetainingCapacity(); +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value: std.Io.Writer.Allocating = .init(palette.allocator); + defer value.deinit(); + const writer = &value.writer; + try cbor.writeValue(writer, entry.label); + try cbor.writeValue(writer, entry.idx); + try cbor.writeValue(writer, matches orelse &[_]usize{}); + try palette.menu.add_item_with_handler(value.written(), select); + palette.items += 1; +} + +fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { + var unused: []const u8 = undefined; + var idx: usize = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &unused) catch false)) return; + if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); + + const history = tui.clipboard_get_history() orelse return; + if (history.len <= idx) return; + tp.self_pid().send(.{ "cmd", "paste", .{history[idx]} }) catch {}; +} + +pub fn delete_item(menu: *Type.MenuType, button: *Type.ButtonType) bool { + var unused: []const u8 = undefined; + var idx: usize = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &unused) catch false)) return false; + if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) return false; + command.executeName("clipboard_delete", command.fmt(.{idx})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + return true; //refresh list +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 54f8aee..ffe3ed7 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1275,6 +1275,16 @@ const cmds = struct { @import("mode/helix.zig").deinit(); } pub const exit_helix_mode_meta: Meta = .{}; + + pub fn clipboard_delete(self: *Self, ctx: Ctx) Result { + var idx: usize = 0; + if (!try ctx.args.match(.{tp.extract(&idx)})) + return error.InvalidClipboardDeleteArgument; + const clipboard = if (self.clipboard) |*clipboard| clipboard else return; + const removed = clipboard.orderedRemove(idx); + self.allocator.free(removed); + } + pub const clipboard_delete_meta: Meta = .{}; }; pub const MiniMode = struct {