Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions packages/core/src/zig/terminal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const NOTIFICATION_QUERY_ID = "opentui-notifications";
pub const SCREEN_PASSTHROUGH_CHUNK_SIZE = 252;
pub const CLIPBOARD_PAYLOAD_SIZE_MAX = std.math.maxInt(u32);
const OSC52_FRAMING_SIZE = "\x1b]52;c;".len + "\x1b\\".len;
const PASSTHROUGH_ESCAPED_OSC52_SIZE = OSC52_FRAMING_SIZE + 2;
const SCREEN_OSC52_FRAMING_SIZE = "\x1b]52;c;".len + "\x07".len;

pub const MouseLevel = enum {
none,
Expand Down Expand Up @@ -1584,10 +1584,10 @@ pub fn clipboardSequenceSize(self: *Terminal, payload_len: usize) !usize {
}

if (self.isInScreen()) {
const escaped_len = try std.math.add(usize, encoded_len, PASSTHROUGH_ESCAPED_OSC52_SIZE);
const chunk_count = @divFloor(escaped_len - 1, SCREEN_PASSTHROUGH_CHUNK_SIZE) + 1;
const screen_sequence_len = try std.math.add(usize, encoded_len, SCREEN_OSC52_FRAMING_SIZE);
const chunk_count = @divFloor(screen_sequence_len - 1, SCREEN_PASSTHROUGH_CHUNK_SIZE) + 1;
const envelopes_len = try std.math.mul(usize, chunk_count, ansi.ANSI.screenDcsStart.len + ansi.ANSI.screenDcsEnd.len);
return std.math.add(usize, escaped_len, envelopes_len);
return std.math.add(usize, screen_sequence_len, envelopes_len);
}

return sequence_len;
Expand All @@ -1603,19 +1603,19 @@ pub fn writeClipboard(self: *Terminal, tty: anytype, target: ClipboardTarget, te

if (self.isInTmux()) {
try tty.writeAll(ansi.ANSI.tmuxDcsStart);
try writeClipboardSequence(tty, target, text_utf8, true);
try writeClipboardSequence(tty, target, text_utf8, true, "\x1b\\");
try tty.writeAll(ansi.ANSI.tmuxDcsEnd);
return;
}

if (self.isInScreen()) {
var screen_writer = ScreenPassthroughWriter(@TypeOf(tty)).init(tty);
try writeClipboardSequence(&screen_writer, target, text_utf8, false);
try writeClipboardSequence(&screen_writer, target, text_utf8, false, "\x07");
try screen_writer.finish();
return;
}

try writeClipboardSequence(tty, target, text_utf8, false);
try writeClipboardSequence(tty, target, text_utf8, false, "\x1b\\");
}

fn ScreenPassthroughWriter(comptime Writer: type) type {
Expand All @@ -1635,13 +1635,7 @@ fn ScreenPassthroughWriter(comptime Writer: type) type {
}

pub fn writeByte(self: *Self, byte: u8) !void {
const encoded_length: usize = if (byte == '\x1b') 2 else 1;
if (self.length + encoded_length > self.buffer.len) try self.flush();

if (byte == '\x1b') {
self.buffer[self.length] = '\x1b';
self.length += 1;
}
if (self.length == self.buffer.len) try self.flush();
self.buffer[self.length] = byte;
self.length += 1;
}
Expand All @@ -1660,12 +1654,12 @@ fn ScreenPassthroughWriter(comptime Writer: type) type {
};
}

fn writeClipboardSequence(writer: anytype, target: ClipboardTarget, text_utf8: []const u8, escape: bool) !void {
fn writeClipboardSequence(writer: anytype, target: ClipboardTarget, text_utf8: []const u8, escape: bool, terminator: []const u8) !void {
try writeClipboardBytes(writer, "\x1b]52;", escape);
try writer.writeByte(target.toChar());
try writer.writeByte(';');
try writeClipboardBase64(writer, text_utf8);
try writeClipboardBytes(writer, "\x1b\\", escape);
try writeClipboardBytes(writer, terminator, escape);
}

fn writeClipboardBase64(writer: anytype, source: []const u8) !void {
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/zig/tests/renderer_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1025,13 +1025,13 @@ test "renderer - clipboard chunks OSC 52 in screen DCS passthrough" {
const payload = [_]u8{'A'} ** 2048;
try std.testing.expect(test_renderer.renderer.copyToClipboardOSC52(.clipboard, &payload));

// Escaped sequence: base64 (2732) + OSC 52 framing (9) + doubled ESC bytes
// (+2) = 2743 bytes, split into 11 chunks of at most 252 bytes, each wrapped
// in "\x1bP" .. "\x1b\\" (+4 per chunk) = 2787 total.
// Sequence: base64 (2732) + OSC 52 framing with BEL (8) = 2740 bytes,
// split into 11 chunks of at most 252 bytes, each wrapped in
// "\x1bP" .. "\x1b\\" (+4 per chunk) = 2784 total.
const output = test_renderer.lastOutput();
try std.testing.expectEqual(@as(usize, 2787), output.len);
try std.testing.expect(std.mem.startsWith(u8, output, "\x1bP\x1b\x1b]52;c;QUFB"));
try std.testing.expect(std.mem.endsWith(u8, output, "QUE=\x1b\x1b\\\x1b\\"));
try std.testing.expectEqual(@as(usize, 2784), output.len);
try std.testing.expect(std.mem.startsWith(u8, output, "\x1bP\x1b]52;c;QUFB"));
try std.testing.expect(std.mem.endsWith(u8, output, "QUE=\x07\x1b\\"));

// ESC only precedes 'P' at envelope starts, so this counts the chunks.
try std.testing.expectEqual(@as(usize, 11), std.mem.count(u8, output, "\x1bP"));
Expand Down
55 changes: 34 additions & 21 deletions packages/core/src/zig/tests/terminal_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1324,16 +1324,16 @@ test "writeClipboard - chunks large payload through GNU Screen passthrough" {
try term.writeClipboard(&writer, .clipboard, payload);

const output = writer.getWritten();
try testing.expect(countSubstring(output, ansi.ANSI.screenDcsStart) > 1);
try testing.expect(std.mem.endsWith(u8, output, ansi.ANSI.screenDcsEnd));

var frame_start: usize = 0;
while (std.mem.indexOfPos(u8, output, frame_start, ansi.ANSI.screenDcsStart)) |start| {
const content_start = start + ansi.ANSI.screenDcsStart.len;
const next_start = std.mem.indexOfPos(u8, output, content_start, ansi.ANSI.screenDcsStart) orelse output.len;
try testing.expect(next_start - content_start <= Terminal.SCREEN_PASSTHROUGH_CHUNK_SIZE + ansi.ANSI.screenDcsEnd.len);
frame_start = next_start;
}
const forwarded_buffer = try testing.allocator.alloc(u8, output.len);
defer testing.allocator.free(forwarded_buffer);
const forwarded = try forwardScreenDcs(output, forwarded_buffer);
const encoded_len = std.base64.standard.Encoder.calcSize(payload.len);
const expected = try testing.allocator.alloc(u8, "\x1b]52;c;".len + encoded_len + 1);
defer testing.allocator.free(expected);
@memcpy(expected[0.."\x1b]52;c;".len], "\x1b]52;c;");
_ = std.base64.standard.Encoder.encode(expected["\x1b]52;c;".len .. expected.len - 1], payload);
expected[expected.len - 1] = '\x07';
try testing.expectEqualSlices(u8, expected, forwarded);
}

test "writeClipboard - base64 encodes raw UTF-8 bytes" {
Expand Down Expand Up @@ -1426,15 +1426,15 @@ test "writeClipboard - Screen framing crosses the 252-byte boundary" {
try env.put("STY", "12345.pts-0.hostname");
var term = Terminal.init(.{ .env_map = &env });

const payload_one_chunk = [_]u8{'A'} ** 180;
const payload_one_chunk = [_]u8{'A'} ** 183;
var writer = TestWriter.init(testing.allocator);
defer writer.deinit();
try term.writeClipboard(&writer, .clipboard, &payload_one_chunk);
try testing.expectEqual(@as(usize, 1), countSubstring(writer.getWritten(), ansi.ANSI.screenDcsStart));
try testing.expectEqual(try term.clipboardSequenceSize(payload_one_chunk.len), writer.getWritten().len);

writer.reset();
const payload_two_chunks = [_]u8{'A'} ** 181;
const payload_two_chunks = [_]u8{'A'} ** 184;
try term.writeClipboard(&writer, .clipboard, &payload_two_chunks);
try testing.expectEqual(@as(usize, 2), countSubstring(writer.getWritten(), ansi.ANSI.screenDcsStart));
try testing.expectEqual(try term.clipboardSequenceSize(payload_two_chunks.len), writer.getWritten().len);
Expand Down Expand Up @@ -1472,21 +1472,13 @@ test "writeClipboard - wraps in DCS passthrough for GNU Screen" {
try env.put("STY", "12345.pts-0.hostname");

var term = Terminal.init(.{ .env_map = &env });
term.caps.osc52 = true;

var writer = TestWriter.init(testing.allocator);
defer writer.deinit();

try term.writeClipboard(&writer, .clipboard, "test");

const output = writer.getWritten();
// Should start with DCS (but not tmux prefix)
try testing.expect(std.mem.startsWith(u8, output, "\x1bP"));
try testing.expect(!std.mem.startsWith(u8, output, "\x1bPtmux;"));
// Should end with DCS terminator
try testing.expect(std.mem.endsWith(u8, output, "\x1b\\"));
// Should have doubled ESC characters
try testing.expect(std.mem.indexOf(u8, output, "\x1b\x1b") != null);
try testing.expectEqualStrings("\x1bP\x1b]52;c;dGVzdA==\x07\x1b\\", writer.getWritten());
}

test "writeClipboard - handles tmux sessions" {
Expand Down Expand Up @@ -1537,6 +1529,27 @@ fn countSubstring(haystack: []const u8, needle: []const u8) usize {
return count;
}

fn forwardScreenDcs(input: []const u8, output: []u8) ![]const u8 {
var input_offset: usize = 0;
var output_offset: usize = 0;
while (input_offset < input.len) {
if (!std.mem.startsWith(u8, input[input_offset..], ansi.ANSI.screenDcsStart)) {
return error.InvalidScreenDcsStart;
}
const content_start = input_offset + ansi.ANSI.screenDcsStart.len;
const end_offset = std.mem.indexOf(u8, input[content_start..], ansi.ANSI.screenDcsEnd) orelse {
return error.MissingScreenDcsEnd;
};
const content = input[content_start .. content_start + end_offset];
if (content.len > Terminal.SCREEN_PASSTHROUGH_CHUNK_SIZE) return error.ScreenDcsPayloadTooLarge;
if (output_offset + content.len > output.len) return error.ScreenDcsOutputTooSmall;
@memcpy(output[output_offset .. output_offset + content.len], content);
output_offset += content.len;
input_offset = content_start + end_offset + ansi.ANSI.screenDcsEnd.len;
}
return output[0..output_offset];
}

test "queryTerminalSend - skips OSC 66 queries when OPENTUI_FORCE_EXPLICIT_WIDTH=false" {
if (builtin.os.tag == .windows) return error.SkipZigTest;

Expand Down
Loading