diff --git a/build.zig.zon b/build.zig.zon index 0f1d653..e0a2a4b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .tpb, - .version = "0.1.0", + .version = "0.1.2", .fingerprint = 0x203a601c46df0265, .minimum_zig_version = "0.16.0", .paths = .{ diff --git a/src/Clip.zig b/src/Clip.zig index 299d4c6..7a379a3 100644 --- a/src/Clip.zig +++ b/src/Clip.zig @@ -7,6 +7,8 @@ pub const OSC52 = @import("Clip/OSC52.zig"); pub const NSPasteboard = if (builtin.target.os.tag == .macos) @import("Clip/NSPasteboard.zig") else void; +pub const ForkOut = @import("Clip/ForkOut.zig"); + // I am not sure if this is super idiomatic Zig or if this is bad practice, but // by ensuring that all Clip backends implement the same methods, we are able // to use the `anytype` type and we are able to have an interface that is able diff --git a/src/Clip/ForkOut.zig b/src/Clip/ForkOut.zig new file mode 100644 index 0000000..97c5377 --- /dev/null +++ b/src/Clip/ForkOut.zig @@ -0,0 +1,24 @@ +/// ForkOut Clipboard backend +/// Fulfills `Clip` interface +pub const ForkOut = @This(); + +const std = @import("std"); + +extern fn sendPB(text: [*:0]const u8) void; +extern fn initPB() void; + +io: std.Io, + +child: std.process.Child, + +pub fn init(io: std.Io, argv: []const []const u8) !ForkOut { + return .{ .child = try std.process.spawn(io, .{ .argv = argv, .stdin = .pipe }), .io = io }; +} + +pub fn writeCopyBuffer(self: *ForkOut, buf: []const u8) anyerror!void { + try self.child.stdin.?.writeStreamingAll(self.io, buf); +} + +pub fn writePasteboard(self: *ForkOut) anyerror!void { + self.child.stdin.?.close(self.io); +} diff --git a/src/Clip/OSC52.zig b/src/Clip/OSC52.zig index 8b751f5..e52c6fc 100644 --- a/src/Clip/OSC52.zig +++ b/src/Clip/OSC52.zig @@ -12,16 +12,29 @@ base64buffer: std.ArrayList(u8), leftover_bytes: [3]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 = 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 .{ .alloc = alloc, .io = io, .base64buffer = try std.ArrayList(u8).initCapacity(alloc, buf_size), .leftover_bytes = undefined, .leftover_count = 0, + .is_tmux = is_tmux, }; } @@ -71,29 +84,58 @@ pub fn writePasteboard(self: *OSC52) anyerror!void { try self.encodeRemaining(); // 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 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, 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" { - 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(); try osc.writeCopyBuffer("part1"); try osc.encodeRemaining(); @@ -101,7 +143,7 @@ test "basic 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(); try osc.writeCopyBuffer("part1"); try osc.writeCopyBuffer("part2"); @@ -110,7 +152,7 @@ test "multi part 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(); try osc.writeCopyBuffer("part1"); try osc.writeCopyBuffer("part2"); @@ -126,7 +168,7 @@ test "tiny 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(); try osc.writeCopyBuffer("part1part1part1part1part1part1part1part1part1|"); try osc.writeCopyBuffer("part2part2part2part2part2part2part2part2part2"); diff --git a/src/main.zig b/src/main.zig index bb0aa14..9ae524b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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_TTY") != null; + const is_tmux = + init.environ_map.get("TMUX") != null; + 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(); try run(io, &clip); return; @@ -42,8 +45,26 @@ pub fn main(init: std.process.Init) !void { return; } + // I don't like this but it will stay until I review how dispatch works + // with zig since I will probably be refactoring there + linux: { + if (comptime os == .linux) { + const xdg_type = init.environ_map.get("XDG_SESSION_TYPE") orelse ""; + + if (std.mem.eql(u8, xdg_type, "wayland")) { + var clip = Clip.ForkOut.init(io, &[_][]const u8{"wl-copy"}) catch break :linux; + try run(io, &clip); + return; + } else if (std.mem.eql(u8, xdg_type, "x11")) { + var clip = Clip.ForkOut.init(io, &[_][]const u8{ "xclip", "-selection", "clipboard" }) catch break :linux; + try run(io, &clip); + return; + } + } + } + // 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(); try run(io, &clip); }