From d7feea3f86358bea2fc3c2c15875706183530d58 Mon Sep 17 00:00:00 2001 From: Grace Yoder Date: Wed, 24 Jun 2026 17:48:16 -0600 Subject: [PATCH] refactor: split OSC52 off into different interface --- src/Clip.zig | 1 + src/Clip/OSC52.zig | 89 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 54 ++++------------------------ 3 files changed, 97 insertions(+), 47 deletions(-) create mode 100644 src/Clip.zig create mode 100644 src/Clip/OSC52.zig diff --git a/src/Clip.zig b/src/Clip.zig new file mode 100644 index 0000000..7d2f6db --- /dev/null +++ b/src/Clip.zig @@ -0,0 +1 @@ +pub const OSC52 = @import("Clip/OSC52.zig"); diff --git a/src/Clip/OSC52.zig b/src/Clip/OSC52.zig new file mode 100644 index 0000000..44d92bf --- /dev/null +++ b/src/Clip/OSC52.zig @@ -0,0 +1,89 @@ +/// OSC52 Clipboard backend +/// Fullfils `Clip` interface +pub const OSC52 = @This(); + +const std = @import("std"); +const Io = std.Io; + +alloc: std.mem.Allocator, +io: std.Io, + +base64buffer: std.ArrayList(u8), + +leftover_bytes: [3]u8, +leftover_count: u8, + +const base64encoder = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, null); + +pub fn init(alloc: std.mem.Allocator, io: Io, buf_size: usize) !OSC52 { + return .{ + .alloc = alloc, + .io = io, + .base64buffer = try std.ArrayList(u8).initCapacity(alloc, buf_size), + .leftover_bytes = undefined, + .leftover_count = 0, + }; +} + +pub fn writeCopyBuffer(self: *OSC52, buf: []u8) !void { + if (buf.len == 0) return; + + // Start of buffer after dealing with leftover bytes + const buf_idx = 3 - self.leftover_count; + + if (buf.len + self.leftover_count < 3) { + const len = buf.len + self.leftover_count; + @memcpy(self.leftover_bytes[self.leftover_count..len], buf[0..buf.len]); + self.leftover_count = @intCast(len); + return; + } + + if (self.leftover_count != 0) { + @memcpy(self.leftover_bytes[self.leftover_count..3], buf[0..buf_idx]); + var temp: [5]u8 = undefined; + const s = OSC52.base64encoder.encode(&temp, &self.leftover_bytes); + try self.base64buffer.appendSlice(self.alloc, s); + } + + const chunkable_length = buf.len - ((buf.len - buf_idx) % 3); + var chunker = std.mem.window(u8, buf[0..chunkable_length], 3, 3); + while (chunker.next()) |chunk| { + var temp: [5]u8 = undefined; + const s = OSC52.base64encoder.encode(&temp, chunk); + try self.base64buffer.appendSlice(self.alloc, s); + } + + self.leftover_count = @intCast(buf.len - chunkable_length); + if (self.leftover_count != 0) { + @memmove(self.leftover_bytes[0..self.leftover_count], buf[chunkable_length..]); + } +} + +pub fn writePasteboard(self: *OSC52) !void { + + // https://ghostty.org/docs/vt/osc/52 + const osc_header = &[_]u8{ + 0x1b, // ESC + 0x5d, // ']' + 0x35, 0x32, // Code 52 + 0x3b, // ';' + // 0x70, // 'p' Primary Clipboard + 0x63, // 'c' Standard Clipboard + 0x3b, // ';' + }; + const osc_footer = &[_]u8{ + 0x1b, // ESC + 0x5c, // '\' + }; + + // Write leftover bytes + var temp: [5]u8 = undefined; + const s = OSC52.base64encoder.encode(&temp, self.leftover_bytes[0..self.leftover_count]); + try self.base64buffer.appendSlice(self.alloc, s); + + // Write data to stdout + const stdout = Io.File.stdout(); + try stdout.writeStreamingAll(self.io, osc_header); + try stdout.writeStreamingAll(self.io, self.base64buffer.items); + try stdout.writeStreamingAll(self.io, osc_footer); +} diff --git a/src/main.zig b/src/main.zig index cd716ab..5c1a339 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,6 @@ const std = @import("std"); const File = std.Io.File; +const Clip = @import("Clip.zig"); pub fn main(init: std.process.Init) !void { const io = init.io; @@ -14,67 +15,26 @@ pub fn main(init: std.process.Init) !void { // We want to avoid doing as many allocations as possible so having raw // pages with it being larger should be totally fine - const alloc = std.heap.page_allocator; + var clip = try Clip.OSC52.init(std.heap.page_allocator, init.io, std.heap.pageSize()); var buf: [4096]u8 = undefined; - var copy_index: u8 = 0; - - var encoded_buffer = try std.ArrayList(u8).initCapacity(alloc, std.heap.pageSize()); - var encoder = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, null); - while (true) { // Read as much of stdin there is - const bytes = File.stdin().readStreaming(io, &.{buf[copy_index..]}) catch |err| { + const bytes = File.stdin().readStreaming(io, &.{&buf}) catch |err| { if (err == File.ReadStreamingError.EndOfStream) { break; } else { return err; } }; - const length = copy_index + bytes; - // Passthrough to stdout - _ = try File.stdout().writeStreamingAll(io, buf[copy_index..length]); + _ = try File.stdout().writeStreamingAll(io, buf[0..bytes]); - // Base64 encode - const chunkable_length = length - (length % 3); - var chunker = std.mem.window(u8, buf[0..chunkable_length], 3, 3); - while (chunker.next()) |chunk| { - var temp: [5]u8 = undefined; - const s = encoder.encode(&temp, chunk); - try encoded_buffer.appendSlice(alloc, s); - } - - // Move left over bits to the start of the array - copy_index = @intCast(length - chunkable_length); - if (copy_index != 0) { - @memmove(buf[0..copy_index], buf[chunkable_length..length]); - } + // Write to clip + try clip.writeCopyBuffer(buf[0..bytes]); } - // Encode last bytes - var temp: [5]u8 = undefined; - const s = encoder.encode(&temp, buf[0..copy_index]); - try encoded_buffer.appendSlice(alloc, s); - // OSC 52 Copying — written as one contiguous buffer so the terminal - // receives the complete sequence in a single write. - // https://ghostty.org/docs/vt/osc/52 - const osc_header = &[_]u8{ - 0x1b, // ESC - 0x5d, // ']' - 0x35, 0x32, // Code 52 - 0x3b, // ';' - // 0x70, // 'p' Primary Clipboard - 0x63, // 'c' Standard Clipboard - 0x3b, // ';' - }; - const osc_footer = &[_]u8{ - 0x1b, // ESC - 0x5c, // '\' - }; + try clip.writePasteboard(); - _ = try File.stdout().writeStreamingAll(io, osc_header); - _ = try File.stdout().writeStreamingAll(io, encoded_buffer.items); - _ = try File.stdout().writeStreamingAll(io, osc_footer); std.debug.print("\x1b[0;32mCopied To Pasteboard!\x1b[0m\n", .{}); }