feat: add support for groups in clipboard history

This introduces the concept of clipboard history groups. A group is created
for each high level clipboard operation. Cut, copy, etc. Single cursor
operations will create a group with just one entry. Multi-cursor operations
on the other hand will create groups with multiple clipboard history entries.
This makes for very powerful clipboard history integration with multi-cursor
support.

This commit also adds the ability to apply integer parmeters to the paste
command to select a clipboard group to paste.

Also, pasting from the system clipboard will detect if the system clipboard is
equivalent to the top most clipboard group, and if so use the group instead.
This allows much better multi-cursor support when using the system copy & paste
commands.
This commit is contained in:
CJ van den Berg 2025-10-31 22:53:14 +01:00
parent 6f57578925
commit 4d375d2d9b
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
5 changed files with 174 additions and 73 deletions

View file

@ -2675,6 +2675,7 @@ pub const Editor = struct {
var all_stop = true; var all_stop = true;
var root = root_; var root = root_;
tui.clipboard_start_group();
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
if (cursel.selection) |_| { if (cursel.selection) |_| {
const cut_text, root = self.cut_selection(root, cursel, tui.clipboard_allocator()) catch continue; const cut_text, root = self.cut_selection(root, cursel, tui.clipboard_allocator()) catch continue;
@ -2696,6 +2697,7 @@ pub const Editor = struct {
const primary = self.get_primary(); const primary = self.get_primary();
const b = self.buf_for_update() catch return; const b = self.buf_for_update() catch return;
var root = b.root; var root = b.root;
tui.clipboard_start_group();
if (self.cursels.items.len == 1 and primary.selection == null) if (self.cursels.items.len == 1 and primary.selection == null)
try self.select_line_at_cursor(root, primary, .include_eol); try self.select_line_at_cursor(root, primary, .include_eol);
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
@ -2713,15 +2715,14 @@ pub const Editor = struct {
var root = b.root; var root = b.root;
if (self.cursels.items.len == 1 and primary.selection == null) if (self.cursels.items.len == 1 and primary.selection == null)
try self.select_line_at_cursor(root, primary, .include_eol); try self.select_line_at_cursor(root, primary, .include_eol);
var count: usize = 0; tui.clipboard_start_group();
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
count += 1;
const cut_text, root = try self.cut_selection(root, cursel, tui.clipboard_allocator()); const cut_text, root = try self.cut_selection(root, cursel, tui.clipboard_allocator());
tui.clipboard_add_chunk(cut_text); tui.clipboard_add_chunk(cut_text);
}; };
try self.update_buf(root); try self.update_buf(root);
self.clamp(); self.clamp();
try tui.clipboard_send_to_system(count); try tui.clipboard_send_to_system();
} }
pub const cut_meta: Meta = .{ .description = "Cut selection or current line to clipboard" }; pub const cut_meta: Meta = .{ .description = "Cut selection or current line to clipboard" };
@ -2735,18 +2736,16 @@ pub const Editor = struct {
try move_cursor_end(root, &sel.end, self.metrics); try move_cursor_end(root, &sel.end, self.metrics);
try move_cursor_right(root, &sel.end, self.metrics); try move_cursor_right(root, &sel.end, self.metrics);
}; };
var count: usize = 0; tui.clipboard_start_group();
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| {
count += 1;
tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics)); tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics));
}; };
return tui.clipboard_send_to_system(count); return tui.clipboard_send_to_system();
} }
pub const copy_meta: Meta = .{ .description = "Copy selection to clipboard" }; pub const copy_meta: Meta = .{ .description = "Copy selection to clipboard" };
fn copy_cursel_file_name(self: *const Self) error{OutOfMemory}!usize { fn copy_cursel_file_name(self: *const Self) error{OutOfMemory}!void {
tui.clipboard_add_chunk(try tui.clipboard_allocator().dupe(u8, self.file_path orelse "*")); tui.clipboard_add_chunk(try tui.clipboard_allocator().dupe(u8, self.file_path orelse "*"));
return 1;
} }
fn copy_cursel_file_name_and_location(self: *const Self, cursel: *const CurSel) error{ WriteFailed, OutOfMemory }!void { fn copy_cursel_file_name_and_location(self: *const Self, cursel: *const CurSel) error{ WriteFailed, OutOfMemory }!void {
@ -2779,23 +2778,20 @@ pub const Editor = struct {
tui.clipboard_add_chunk(try buffer.toOwnedSlice()); tui.clipboard_add_chunk(try buffer.toOwnedSlice());
} }
fn copy_cursels_file_name_and_location(self: *const Self) error{OutOfMemory}!usize { fn copy_cursels_file_name_and_location(self: *const Self) error{OutOfMemory}!void {
var count: usize = 0; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
count += 1;
self.copy_cursel_file_name_and_location(cursel) catch return error.OutOfMemory; self.copy_cursel_file_name_and_location(cursel) catch return error.OutOfMemory;
};
return count;
} }
pub fn copy_file_name(self: *Self, ctx: Context) Result { pub fn copy_file_name(self: *Self, ctx: Context) Result {
var mode: enum { all, file_name_only } = .all; var mode: enum { all, file_name_only } = .all;
_ = ctx.args.match(.{tp.extract(&mode)}) catch false; _ = ctx.args.match(.{tp.extract(&mode)}) catch false;
const n = switch (mode) { tui.clipboard_start_group();
switch (mode) {
.file_name_only => try self.copy_cursel_file_name(), .file_name_only => try self.copy_cursel_file_name(),
.all => try self.copy_cursels_file_name_and_location(), .all => try self.copy_cursels_file_name_and_location(),
}; }
return tui.clipboard_send_to_system(n); return tui.clipboard_send_to_system();
} }
pub const copy_file_name_meta: Meta = .{ pub const copy_file_name_meta: Meta = .{
.description = "Copy file name and location to clipboard", .description = "Copy file name and location to clipboard",
@ -2803,6 +2799,7 @@ pub const Editor = struct {
pub fn copy_internal_vim(self: *Self, _: Context) Result { pub fn copy_internal_vim(self: *Self, _: Context) Result {
const root = self.buf_root() catch return; const root = self.buf_root() catch return;
tui.clipboard_start_group();
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel|
tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics)); tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics));
} }
@ -2817,49 +2814,69 @@ pub const Editor = struct {
try move_cursor_end(root, &sel.end, self.metrics); try move_cursor_end(root, &sel.end, self.metrics);
try move_cursor_right(root, &sel.end, self.metrics); try move_cursor_right(root, &sel.end, self.metrics);
} }
tui.clipboard_start_group();
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel|
tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics)); tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics));
} }
pub const copy_line_internal_vim_meta: Meta = .{ .description = "Copy line to internal clipboard (vim)" }; pub const copy_line_internal_vim_meta: Meta = .{ .description = "Copy line to internal clipboard (vim)" };
pub fn paste(self: *Self, ctx: Context) Result { pub fn paste(self: *Self, ctx: Context) Result {
var text_: []const u8 = undefined; var group_idx: usize = 0;
const clipboard: []const []const u8 = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) var text_: []const u8 = &.{};
&[_][]const u8{text_} const clipboard: []const tui.ClipboardEntry = if (try ctx.args.match(.{tp.extract(&group_idx)})) blk: {
self.logger.print("paste: pasting group {d}", .{group_idx});
break :blk tui.clipboard_get_group(group_idx);
} else if (try ctx.args.match(.{tp.extract(&text_)}))
&[_]tui.ClipboardEntry{.{ .text = text_ }}
else else
tui.clipboard_get_history() orelse return; tui.clipboard_get_group(0);
const b = try self.buf_for_update(); const b = try self.buf_for_update();
var root = b.root; var root = b.root;
var bytes: usize = 0; var bytes: usize = 0;
var cursel_idx = self.cursels.items.len - 1;
var idx = clipboard.len - 1; if (clipboard.len == 0) {
while (true) { self.logger.print("paste: nothing to paste", .{});
const cursel_ = &self.cursels.items[cursel_idx]; return;
if (cursel_.*) |*cursel| {
const text = clipboard[idx];
root = try self.insert(root, cursel, text, b.allocator);
idx = if (idx == 0) clipboard.len - 1 else idx - 1;
bytes += text.len;
}
if (cursel_idx == 0) break;
cursel_idx -= 1;
} }
if (clipboard.len > 1 and self.cursels.items.len == 1) {
const cursel = self.get_primary();
for (clipboard) |item| {
root = try self.insert(root, cursel, item.text, b.allocator);
if (item.text[item.text.len - 1] != '\n')
root = try self.insert(root, cursel, "\n", b.allocator);
}
} else {
var cursel_idx = self.cursels.items.len - 1;
var idx = clipboard.len - 1;
while (true) {
const cursel_ = &self.cursels.items[cursel_idx];
if (cursel_.*) |*cursel| {
const text = clipboard[idx].text;
root = try self.insert(root, cursel, text, b.allocator);
idx = if (idx == 0) clipboard.len - 1 else idx - 1;
bytes += text.len;
}
if (cursel_idx == 0) break;
cursel_idx -= 1;
}
}
self.logger.print("paste: {d} bytes", .{bytes}); self.logger.print("paste: {d} bytes", .{bytes});
try self.update_buf(root); try self.update_buf(root);
self.clamp(); self.clamp();
self.need_render(); self.need_render();
} }
pub const paste_meta: Meta = .{ .description = "Paste from internal clipboard" }; pub const paste_meta: Meta = .{ .description = "Paste from internal clipboard", .arguments = &.{.integer} };
pub fn paste_internal_vim(self: *Self, ctx: Context) Result { pub fn paste_internal_vim(self: *Self, ctx: Context) Result {
var text_: []const u8 = undefined; var text_: []const u8 = undefined;
const clipboard: []const []const u8 = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) const clipboard: []const tui.ClipboardEntry = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)}))
&[_][]const u8{text_} &[_]tui.ClipboardEntry{.{ .text = text_ }}
else else
tui.clipboard_get_history() orelse return; tui.clipboard_get_group(0);
const b = try self.buf_for_update(); const b = try self.buf_for_update();
var root = b.root; var root = b.root;
@ -2870,7 +2887,7 @@ pub const Editor = struct {
while (true) { while (true) {
const cursel_ = &self.cursels.items[cursel_idx]; const cursel_ = &self.cursels.items[cursel_idx];
if (cursel_.*) |*cursel| { if (cursel_.*) |*cursel| {
const text = clipboard[idx]; const text = clipboard[idx].text;
root = try self.insert_line_vim(root, cursel, text, b.allocator); root = try self.insert_line_vim(root, cursel, text, b.allocator);
idx = if (idx == 0) clipboard.len - 1 else idx - 1; idx = if (idx == 0) clipboard.len - 1 else idx - 1;
bytes += text.len; bytes += text.len;

View file

@ -1409,7 +1409,11 @@ pub fn write_restore_info(self: *Self) void {
if (tui.clipboard_get_history()) |clipboard| { if (tui.clipboard_get_history()) |clipboard| {
cbor.writeArrayHeader(writer, clipboard.len) catch return; cbor.writeArrayHeader(writer, clipboard.len) catch return;
for (clipboard) |item| cbor.writeValue(writer, item) catch return; for (clipboard) |item| {
cbor.writeArrayHeader(writer, 2) catch return;
cbor.writeValue(writer, item.group) catch return;
cbor.writeValue(writer, item.text) catch return;
}
} else { } else {
cbor.writeValue(writer, null) catch return; cbor.writeValue(writer, null) catch return;
} }
@ -1443,11 +1447,18 @@ fn read_restore_info(self: *Self) !void {
tui.clipboard_clear_all(); tui.clipboard_clear_all();
var len = try cbor.decodeArrayHeader(&iter); var len = try cbor.decodeArrayHeader(&iter);
var prev_group: usize = 0;
const clipboard_allocator = tui.clipboard_allocator(); const clipboard_allocator = tui.clipboard_allocator();
while (len > 0) : (len -= 1) { while (len > 0) : (len -= 1) {
var chunk: []const u8 = undefined; const len_ = try cbor.decodeArrayHeader(&iter);
if (!try cbor.matchValue(&iter, cbor.extract(&chunk))) return error.Stop; if (len_ != 2) return error.Stop;
tui.clipboard_add_chunk(try clipboard_allocator.dupe(u8, chunk)); var group: usize = 0;
var text: []const u8 = undefined;
if (!try cbor.matchValue(&iter, cbor.extract(&group))) return error.Stop;
if (!try cbor.matchValue(&iter, cbor.extract(&text))) return error.Stop;
if (prev_group != group) tui.clipboard_start_group();
prev_group = group;
tui.clipboard_add_chunk(try clipboard_allocator.dupe(u8, text));
} }
try self.buffer_manager.extract_state(&iter); try self.buffer_manager.extract_state(&iter);

View file

@ -307,7 +307,7 @@ const cmds_ = struct {
const mv = tui.mainview() orelse return; const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return; const ed = mv.get_active_editor() orelse return;
const b = try ed.buf_for_update(); const b = try ed.buf_for_update();
tui.clipboard_clear_all(); tui.clipboard_start_group();
const root = try ed.cut_to(move_noop, b.root); const root = try ed.cut_to(move_noop, b.root);
try ed.update_buf(root); try ed.update_buf(root);
ed.clamp(); ed.clamp();
@ -418,7 +418,7 @@ const cmds_ = struct {
const ed = mv.get_active_editor() orelse return; const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return; const root = ed.buf_root() catch return;
tui.clipboard_clear_all(); tui.clipboard_start_group();
for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel|
tui.clipboard_add_chunk(try Editor.copy_selection(root, sel, tui.clipboard_allocator(), ed.metrics)); tui.clipboard_add_chunk(try Editor.copy_selection(root, sel, tui.clipboard_allocator(), ed.metrics));
@ -890,10 +890,10 @@ fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result
const ed = mv.get_active_editor() orelse return; const ed = mv.get_active_editor() orelse return;
var text_: []const u8 = undefined; var text_: []const u8 = undefined;
const clipboard: []const []const u8 = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) const clipboard: []const tui.ClipboardEntry = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)}))
&[_][]const u8{text_} &[_]tui.ClipboardEntry{.{ .text = text_ }}
else else
tui.clipboard_get_history() orelse return; tui.clipboard_get_group(0);
const b = try ed.buf_for_update(); const b = try ed.buf_for_update();
var root = b.root; var root = b.root;
@ -905,11 +905,11 @@ fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result
var bytes: usize = 0; var bytes: usize = 0;
for (ed.cursels.items, 0..) |*cursel_, idx| if (cursel_.*) |*cursel| { for (ed.cursels.items, 0..) |*cursel_, idx| if (cursel_.*) |*cursel| {
if (idx < clipboard.len) { if (idx < clipboard.len) {
root = try do_paste(ed, root, cursel, clipboard[idx], b.allocator); root = try do_paste(ed, root, cursel, clipboard[idx].text, b.allocator);
bytes += clipboard[idx].len; bytes += clipboard[idx].text.len;
} else { } else {
bytes += clipboard[clipboard.len - 1].len; bytes += clipboard[clipboard.len - 1].text.len;
root = try do_paste(ed, root, cursel, clipboard[clipboard.len - 1], b.allocator); root = try do_paste(ed, root, cursel, clipboard[clipboard.len - 1].text, b.allocator);
} }
}; };
ed.logger.print("paste: {d} bytes", .{bytes}); ed.logger.print("paste: {d} bytes", .{bytes});

View file

@ -16,6 +16,7 @@ pub const icon = " ";
pub const Entry = struct { pub const Entry = struct {
label: []const u8, label: []const u8,
idx: usize, idx: usize,
group: usize,
}; };
pub fn load_entries(palette: *Type) !usize { pub fn load_entries(palette: *Type) !usize {
@ -24,7 +25,8 @@ pub fn load_entries(palette: *Type) !usize {
if (history.len > 0) { if (history.len > 0) {
var idx = history.len - 1; var idx = history.len - 1;
while (true) : (idx -= 1) { while (true) : (idx -= 1) {
var label_ = history[idx]; const entry = &history[idx];
var label_ = entry.text;
while (label_.len > 0) switch (label_[0]) { while (label_.len > 0) switch (label_[0]) {
' ', '\t', '\n' => label_ = label_[1..], ' ', '\t', '\n' => label_ = label_[1..],
else => break, else => break,
@ -32,6 +34,7 @@ pub fn load_entries(palette: *Type) !usize {
(try palette.entries.addOne(palette.allocator)).* = .{ (try palette.entries.addOne(palette.allocator)).* = .{
.label = label_, .label = label_,
.idx = idx, .idx = idx,
.group = entry.group,
}; };
if (idx == 0) break; if (idx == 0) break;
} }
@ -51,7 +54,11 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v
var hint: std.Io.Writer.Allocating = .init(palette.allocator); var hint: std.Io.Writer.Allocating = .init(palette.allocator);
defer hint.deinit(); defer hint.deinit();
const item = if (tui.clipboard_get_history()) |clipboard| clipboard[entry.idx] else &.{}; const clipboard_ = tui.clipboard_get_history();
const clipboard = clipboard_ orelse &.{};
const clipboard_entry: tui.ClipboardEntry = if (clipboard_) |_| clipboard[entry.idx] else .{};
const group_idx = tui.clipboard_current_group() - clipboard_entry.group;
const item = clipboard_entry.text;
var line_count: usize = 1; var line_count: usize = 1;
for (0..item.len) |i| if (item[i] == '\n') { for (0..item.len) |i| if (item[i] == '\n') {
line_count += 1; line_count += 1;
@ -60,6 +67,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v
try hint.writer.print(" {d} lines", .{line_count}) try hint.writer.print(" {d} lines", .{line_count})
else else
try hint.writer.print(" {d} {s}", .{ item.len, if (item.len == 1) "byte " else "bytes" }); try hint.writer.print(" {d} {s}", .{ item.len, if (item.len == 1) "byte " else "bytes" });
try hint.writer.print(":{d}", .{group_idx});
try cbor.writeValue(writer, hint.written()); try cbor.writeValue(writer, hint.written());
try cbor.writeValue(writer, matches orelse &[_]usize{}); try cbor.writeValue(writer, matches orelse &[_]usize{});

View file

@ -76,10 +76,16 @@ fontfaces_: std.ArrayListUnmanaged([]const u8) = .{},
enable_mouse_idle_timer: bool = false, enable_mouse_idle_timer: bool = false,
query_cache_: *syntax.QueryCache, query_cache_: *syntax.QueryCache,
frames_rendered_: usize = 0, frames_rendered_: usize = 0,
clipboard: ?std.ArrayList([]const u8) = null, clipboard: ?std.ArrayList(ClipboardEntry) = null,
clipboard_current_group_number: usize = 0,
color_scheme: enum { dark, light } = .dark, color_scheme: enum { dark, light } = .dark,
color_scheme_locked: bool = false, color_scheme_locked: bool = false,
pub const ClipboardEntry = struct {
text: []const u8 = &.{},
group: usize = 0,
};
const keepalive = std.time.us_per_day * 365; // one year const keepalive = std.time.us_per_day * 365; // one year
const idle_frames = 0; const idle_frames = 0;
const mouse_idle_time_milliseconds = 3000; const mouse_idle_time_milliseconds = 3000;
@ -377,10 +383,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
var text: []const u8 = undefined; var text: []const u8 = undefined;
if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.dispatch_flush_input_event(); try self.dispatch_flush_input_event();
return if (command.get_id("mini_mode_paste")) |id| return self.handle_system_clipboard(text);
command.execute(id, command.fmt(.{text}))
else
command.executeName("paste", command.fmt(.{text}));
} }
if (try m.match(.{ "system_clipboard", tp.null_ })) if (try m.match(.{ "system_clipboard", tp.null_ }))
@ -605,6 +608,20 @@ fn dispatch_event(ctx: *anyopaque, cbor_msg: []const u8) void {
tp.self_pid().send_raw(m) catch |e| self.logger.err("dispatch event", e); tp.self_pid().send_raw(m) catch |e| self.logger.err("dispatch event", e);
} }
fn handle_system_clipboard(self: *Self, text: []const u8) !void {
if (command.get_id("mini_mode_paste")) |id|
return command.execute(id, command.fmt(.{text}));
{
const text_ = try clipboard_system_clipboard_text(self.allocator);
defer self.allocator.free(text_);
if (std.mem.eql(u8, text_, text))
return command.executeName("paste", command.fmt(.{0}));
}
return command.executeName("paste", command.fmt(.{text}));
}
fn find_coord_widget(self: *Self, y: usize, x: usize) ?*Widget { fn find_coord_widget(self: *Self, y: usize, x: usize) ?*Widget {
const Ctx = struct { const Ctx = struct {
widget: ?*Widget = null, widget: ?*Widget = null,
@ -1302,7 +1319,7 @@ const cmds = struct {
return error.InvalidClipboardDeleteArgument; return error.InvalidClipboardDeleteArgument;
const clipboard = if (self.clipboard) |*clipboard| clipboard else return; const clipboard = if (self.clipboard) |*clipboard| clipboard else return;
const removed = clipboard.orderedRemove(idx); const removed = clipboard.orderedRemove(idx);
self.allocator.free(removed); self.allocator.free(removed.text);
} }
pub const clipboard_delete_meta: Meta = .{}; pub const clipboard_delete_meta: Meta = .{};
}; };
@ -1901,7 +1918,7 @@ fn widget_type_config_variable(widget_type: WidgetType) *ConfigWidgetStyle {
fn clipboard_deinit(self: *Self) void { fn clipboard_deinit(self: *Self) void {
if (self.clipboard) |*clipboard| { if (self.clipboard) |*clipboard| {
for (clipboard.items) |chunk| for (clipboard.items) |chunk|
self.allocator.free(chunk); self.allocator.free(chunk.text);
clipboard.deinit(self.allocator); clipboard.deinit(self.allocator);
} }
self.clipboard = null; self.clipboard = null;
@ -1912,7 +1929,7 @@ pub fn clipboard_allocator() Allocator {
return self.allocator; return self.allocator;
} }
pub fn clipboard_get_history() ?[]const []const u8 { pub fn clipboard_get_history() ?[]const ClipboardEntry {
const self = current(); const self = current();
return if (self.clipboard) |clipboard| clipboard.items else null; return if (self.clipboard) |clipboard| clipboard.items else null;
} }
@ -1920,12 +1937,55 @@ pub fn clipboard_get_history() ?[]const []const u8 {
pub fn clipboard_peek_chunk() ?[]const u8 { pub fn clipboard_peek_chunk() ?[]const u8 {
const self = current(); const self = current();
const clipboard = self.clipboard orelse return null; const clipboard = self.clipboard orelse return null;
return clipboard[clipboard.len - 1]; return clipboard[clipboard.len - 1].text;
}
pub fn clipboard_start_group() void {
const self = current();
self.clipboard_current_group_number += 1;
}
pub fn clipboard_current_group() usize {
const self = current();
return self.clipboard_current_group_number;
}
pub fn clipboard_group_size(group: usize) usize {
const self = current();
const clipboard = self.clipboard orelse return 0;
var count: usize = 0;
var idx: usize = clipboard.len - 1;
while (clipboard[idx].group != group) {
if (idx == 0) return 0 else idx -= 1;
}
while (clipboard[idx].group == group) {
count += 1;
if (idx == 0) break else idx -= 1;
}
return count;
}
pub fn clipboard_get_group(group_idx: usize) []const ClipboardEntry {
const self = current();
const clipboard = (self.clipboard orelse return &.{}).items;
const group = self.clipboard_current_group_number - group_idx;
if (clipboard.len == 0) return &.{};
var group_end: usize = clipboard.len;
while (clipboard[group_end - 1].group != group) {
if (group_end == 1) return &.{} else group_end -= 1;
}
var group_begin: usize = group_end;
while (clipboard[group_begin - 1].group == group) {
group_begin -= 1;
if (group_begin == 0) break;
}
return clipboard[group_begin..group_end];
} }
pub fn clipboard_clear_all() void { pub fn clipboard_clear_all() void {
const self = current(); const self = current();
self.clipboard_deinit(); self.clipboard_deinit();
self.clipboard_current_group_number = 0;
} }
pub fn clipboard_add_chunk(text: []const u8) void { pub fn clipboard_add_chunk(text: []const u8) void {
@ -1935,23 +1995,28 @@ pub fn clipboard_add_chunk(text: []const u8) void {
break :blk &self.clipboard.?; break :blk &self.clipboard.?;
}; };
const chunk = clipboard.addOne(self.allocator) catch @panic("OOM clipboard_add_chunk"); const chunk = clipboard.addOne(self.allocator) catch @panic("OOM clipboard_add_chunk");
chunk.* = text; chunk.text = text;
chunk.group = self.clipboard_current_group_number;
} }
pub fn clipboard_send_to_system(n_chunks: usize) error{ Stop, WriteFailed }!void { fn clipboard_system_clipboard_text(allocator: std.mem.Allocator) error{ Stop, WriteFailed, OutOfMemory }![]const u8 {
const self = current(); var buffer: std.Io.Writer.Allocating = .init(allocator);
var buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer buffer.deinit(); defer buffer.deinit();
const writer = &buffer.writer; const writer = &buffer.writer;
const clipboard = if (self.clipboard) |clipboard| clipboard.items else return error.Stop; const clipboard = clipboard_get_group(0);
if (clipboard.len < n_chunks) return error.Stop;
if (n_chunks == 1) return self.clipboard_send_to_system_internal(clipboard[clipboard.len - 1]);
var first = true; var first = true;
const chunks = clipboard[clipboard.len - n_chunks ..]; for (clipboard) |chunk| {
for (chunks) |chunk| {
if (first) first = false else try writer.writeByte('\n'); if (first) first = false else try writer.writeByte('\n');
try writer.writeAll(chunk); try writer.writeAll(chunk.text);
} }
return buffer.toOwnedSlice();
}
pub fn clipboard_send_to_system() error{ Stop, WriteFailed, OutOfMemory }!void {
const self = current();
const text = try clipboard_system_clipboard_text(self.allocator);
defer self.allocator.free(text);
return self.clipboard_send_to_system_internal(text);
} }
fn clipboard_send_to_system_internal(self: *Self, text: []const u8) void { fn clipboard_send_to_system_internal(self: *Self, text: []const u8) void {