Compare commits

...

3 commits
0.1.1 ... main

Author SHA1 Message Date
2178c4d839
feat(OSC52): added tmux autodetect
All checks were successful
ci/crow/push/release Pipeline was successful
ci/crow/tag/build Pipeline was successful
2026-07-06 17:15:47 -06:00
130e668dd5
feat(OSC52): added tmux mode to use tmux passthrough 2026-07-06 17:15:47 -06:00
ec524f039c
fix: added os comptime check for running ForkOut 2026-07-06 17:15:47 -06:00
3 changed files with 79 additions and 32 deletions

View file

@ -1,6 +1,6 @@
.{ .{
.name = .tpb, .name = .tpb,
.version = "0.1.1", .version = "0.1.2",
.fingerprint = 0x203a601c46df0265, .fingerprint = 0x203a601c46df0265,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.paths = .{ .paths = .{

View file

@ -12,16 +12,29 @@ base64buffer: std.ArrayList(u8),
leftover_bytes: [3]u8, leftover_bytes: [3]u8,
leftover_count: u8, leftover_count: u8,
/// This is to wrap the OSC buffer in the tmux passthrough sequence. This lets
/// OSC52 clipboard requests go directly to the host instead of going to a tmux
/// buffer.
/// https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
/// Since tmux v3.7, there is a `get-clipboard` option and when it is set to
/// `request` any OSC52 query that comes to it will cause the tmux buffer to
/// update before sending the information.
/// https://raw.githubusercontent.com/tmux/tmux/3.7b/CHANGES
/// This still wraps the tmux session in an escape whenever one is detected
/// since this option is still incredibly new.
is_tmux: bool,
const base64encoder = const base64encoder =
std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '='); std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '=');
pub fn init(alloc: std.mem.Allocator, io: std.Io, buf_size: usize) !OSC52 { pub fn init(alloc: std.mem.Allocator, io: std.Io, buf_size: usize, is_tmux: bool) !OSC52 {
return .{ return .{
.alloc = alloc, .alloc = alloc,
.io = io, .io = io,
.base64buffer = try std.ArrayList(u8).initCapacity(alloc, buf_size), .base64buffer = try std.ArrayList(u8).initCapacity(alloc, buf_size),
.leftover_bytes = undefined, .leftover_bytes = undefined,
.leftover_count = 0, .leftover_count = 0,
.is_tmux = is_tmux,
}; };
} }
@ -71,29 +84,58 @@ pub fn writePasteboard(self: *OSC52) anyerror!void {
try self.encodeRemaining(); try self.encodeRemaining();
// https://ghostty.org/docs/vt/osc/52 // 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 data to stdout // Write data to stdout
const stdout = std.Io.File.stdout(); const stdout = std.Io.File.stdout();
try stdout.writeStreamingAll(self.io, osc_header); try stdout.writeStreamingAll(self.io, self.osc_header());
try stdout.writeStreamingAll(self.io, self.base64buffer.items); try stdout.writeStreamingAll(self.io, self.base64buffer.items);
try stdout.writeStreamingAll(self.io, osc_footer); try stdout.writeStreamingAll(self.io, self.osc_footer());
}
fn osc_header(self: *OSC52) []const u8 {
if (self.is_tmux) {
return &[_]u8{
0x1b, // ESC
'P', 't', 'm', 'u', 'x', ';', // tmux escape
0x1b, 0x1b, // ESC ESC
0x5d, // ']'
0x35, 0x32, // Code 52
0x3b, // ';'
// 0x70, // 'p' Primary Clipboard
0x63, // 'c' Standard Clipboard
0x3b, // ';'
};
} else {
return &[_]u8{
0x1b, // ESC
0x5d, // ']'
0x35, 0x32, // Code 52
0x3b, // ';'
// 0x70, // 'p' Primary Clipboard
0x63, // 'c' Standard Clipboard
0x3b, // ';'
};
}
}
fn osc_footer(self: *OSC52) []const u8 {
if (self.is_tmux) {
return &[_]u8{
0x1b, 0x1b, // ESC ESC
0x5c, // '\'
0x1b, // ESC
0x5c, // '\'
};
} else {
return &[_]u8{
0x1b, // ESC
0x5c, // '\'
};
}
} }
test "basic base64 encode" { test "basic base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024); var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free(); defer osc.free();
try osc.writeCopyBuffer("part1"); try osc.writeCopyBuffer("part1");
try osc.encodeRemaining(); try osc.encodeRemaining();
@ -101,7 +143,7 @@ test "basic base64 encode" {
} }
test "multi part base64 encode" { test "multi part base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024); var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free(); defer osc.free();
try osc.writeCopyBuffer("part1"); try osc.writeCopyBuffer("part1");
try osc.writeCopyBuffer("part2"); try osc.writeCopyBuffer("part2");
@ -110,7 +152,7 @@ test "multi part base64 encode" {
} }
test "tiny buffers base64 encode" { test "tiny buffers base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024); var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free(); defer osc.free();
try osc.writeCopyBuffer("part1"); try osc.writeCopyBuffer("part1");
try osc.writeCopyBuffer("part2"); try osc.writeCopyBuffer("part2");
@ -126,7 +168,7 @@ test "tiny buffers base64 encode" {
} }
test "big buffers base64 encode" { test "big buffers base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024); var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free(); defer osc.free();
try osc.writeCopyBuffer("part1part1part1part1part1part1part1part1part1|"); try osc.writeCopyBuffer("part1part1part1part1part1part1part1part1part1|");
try osc.writeCopyBuffer("part2part2part2part2part2part2part2part2part2"); try osc.writeCopyBuffer("part2part2part2part2part2part2part2part2part2");

View file

@ -28,8 +28,11 @@ pub fn main(init: std.process.Init) !void {
init.environ_map.get("SSH_CLIENT") != null or init.environ_map.get("SSH_CLIENT") != null or
init.environ_map.get("SSH_TTY") != null; init.environ_map.get("SSH_TTY") != null;
const is_tmux =
init.environ_map.get("TMUX") != null;
if (is_ssh) { if (is_ssh) {
var clip = try Clip.OSC52.init(alloc, io, size); var clip = try Clip.OSC52.init(alloc, io, size, is_tmux);
defer clip.free(); defer clip.free();
try run(io, &clip); try run(io, &clip);
return; return;
@ -45,21 +48,23 @@ pub fn main(init: std.process.Init) !void {
// I don't like this but it will stay until I review how dispatch works // I don't like this but it will stay until I review how dispatch works
// with zig since I will probably be refactoring there // with zig since I will probably be refactoring there
linux: { linux: {
const xdg_type = init.environ_map.get("XDG_SESSION_TYPE") orelse ""; if (comptime os == .linux) {
const xdg_type = init.environ_map.get("XDG_SESSION_TYPE") orelse "";
if (std.mem.eql(u8, xdg_type, "wayland")) { if (std.mem.eql(u8, xdg_type, "wayland")) {
var clip = Clip.ForkOut.init(io, &[_][]const u8{"wl-copy"}) catch break :linux; var clip = Clip.ForkOut.init(io, &[_][]const u8{"wl-copy"}) catch break :linux;
try run(io, &clip); try run(io, &clip);
return; return;
} else if (std.mem.eql(u8, xdg_type, "x11")) { } else if (std.mem.eql(u8, xdg_type, "x11")) {
var clip = Clip.ForkOut.init(io, &[_][]const u8{ "xclip", "-selection", "clipboard" }) catch break :linux; var clip = Clip.ForkOut.init(io, &[_][]const u8{ "xclip", "-selection", "clipboard" }) catch break :linux;
try run(io, &clip); try run(io, &clip);
return; return;
}
} }
} }
// Fallback Clipboard // Fallback Clipboard
var clip = try Clip.OSC52.init(alloc, io, size); var clip = try Clip.OSC52.init(alloc, io, size, is_tmux);
defer clip.free(); defer clip.free();
try run(io, &clip); try run(io, &clip);
} }