1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
/// OSC52 Clipboard backend
/// Fullfils `Clip` interface
pub const OSC52 = @This();
const std = @import("std");
alloc: std.mem.Allocator,
io: std.Io,
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, 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,
};
}
pub fn free(self: *OSC52) void {
self.base64buffer.clearAndFree(self.alloc);
}
pub fn writeCopyBuffer(self: *OSC52, buf: []const u8) anyerror!void {
if (buf.len == 0) return;
// Start of buffer after dealing with leftover bytes
const buf_idx = 3 - self.leftover_count;
// When we dont fill the leftover buffer, just copy data
if (buf_idx > buf.len) {
const end = self.leftover_count + buf.len;
@memcpy(self.leftover_bytes[self.leftover_count..end], buf[0..buf.len]);
self.leftover_count = @intCast(end);
return;
}
@memcpy(self.leftover_bytes[self.leftover_count..3], buf[0..buf_idx]);
var temp: [5]u8 = undefined;
var 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[buf_idx..chunkable_length], 3, 3);
while (chunker.next()) |chunk| {
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 encodeRemaining(self: *OSC52) anyerror!void {
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);
}
pub fn writePasteboard(self: *OSC52) anyerror!void {
try self.encodeRemaining();
// https://ghostty.org/docs/vt/osc/52
// Write data to stdout
const stdout = std.Io.File.stdout();
try stdout.writeStreamingAll(self.io, self.osc_header());
try stdout.writeStreamingAll(self.io, self.base64buffer.items);
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, false);
defer osc.free();
try osc.writeCopyBuffer("part1");
try osc.encodeRemaining();
try std.testing.expectEqualSlices(u8, "cGFydDE=", osc.base64buffer.items);
}
test "multi part base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free();
try osc.writeCopyBuffer("part1");
try osc.writeCopyBuffer("part2");
try osc.encodeRemaining();
try std.testing.expectEqualSlices(u8, "cGFydDFwYXJ0Mg==", osc.base64buffer.items);
}
test "tiny buffers base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free();
try osc.writeCopyBuffer("part1");
try osc.writeCopyBuffer("part2");
try osc.writeCopyBuffer("1");
try osc.writeCopyBuffer("2");
try osc.writeCopyBuffer("3");
try osc.writeCopyBuffer("45");
try osc.writeCopyBuffer("6");
try osc.writeCopyBuffer("78");
try osc.writeCopyBuffer("90fin");
try osc.encodeRemaining();
try std.testing.expectEqualSlices(u8, "cGFydDFwYXJ0MjEyMzQ1Njc4OTBmaW4=", osc.base64buffer.items);
}
test "big buffers base64 encode" {
var osc = try OSC52.init(std.testing.allocator, std.testing.io, 1024, false);
defer osc.free();
try osc.writeCopyBuffer("part1part1part1part1part1part1part1part1part1|");
try osc.writeCopyBuffer("part2part2part2part2part2part2part2part2part2");
try osc.encodeRemaining();
try std.testing.expectEqualSlices(u8, "cGFydDFwYXJ0MXBhcnQxcGFydDFwYXJ0MXBhcnQxcGFydDFwYXJ0MXBhcnQxfHBhcnQycGFydDJwYXJ0MnBhcnQycGFydDJwYXJ0MnBhcnQycGFydDJwYXJ0Mg==", osc.base64buffer.items);
}
|