refactor: split OSC52 off into different interface

This commit is contained in:
Grace Yoder 2026-06-24 17:48:16 -06:00
parent bcf298696b
commit d7feea3f86
Signed by: grace
SSH key fingerprint: SHA256:oG9v0ZlohsoMAg083HAwBGOcaPLF3nIuOlAW4MtASMo
3 changed files with 97 additions and 47 deletions

1
src/Clip.zig Normal file
View file

@ -0,0 +1 @@
pub const OSC52 = @import("Clip/OSC52.zig");

89
src/Clip/OSC52.zig Normal file
View file

@ -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);
}

View file

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const File = std.Io.File; const File = std.Io.File;
const Clip = @import("Clip.zig");
pub fn main(init: std.process.Init) !void { pub fn main(init: std.process.Init) !void {
const io = init.io; 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 // We want to avoid doing as many allocations as possible so having raw
// pages with it being larger should be totally fine // 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 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) { while (true) {
// Read as much of stdin there is // 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) { if (err == File.ReadStreamingError.EndOfStream) {
break; break;
} else { } else {
return err; return err;
} }
}; };
const length = copy_index + bytes;
// Passthrough to stdout // Passthrough to stdout
_ = try File.stdout().writeStreamingAll(io, buf[copy_index..length]); _ = try File.stdout().writeStreamingAll(io, buf[0..bytes]);
// Base64 encode // Write to clip
const chunkable_length = length - (length % 3); try clip.writeCopyBuffer(buf[0..bytes]);
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]);
}
} }
// 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 try clip.writePasteboard();
// 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 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", .{}); std.debug.print("\x1b[0;32mCopied To Pasteboard!\x1b[0m\n", .{});
} }