diff --git a/packages/core/src/zig/terminal.zig b/packages/core/src/zig/terminal.zig index 92dce32c8..0a96caf9c 100644 --- a/packages/core/src/zig/terminal.zig +++ b/packages/core/src/zig/terminal.zig @@ -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, @@ -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; @@ -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 { @@ -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; } @@ -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 { diff --git a/packages/core/src/zig/tests/renderer_test.zig b/packages/core/src/zig/tests/renderer_test.zig index 33e2159aa..14f96dfea 100644 --- a/packages/core/src/zig/tests/renderer_test.zig +++ b/packages/core/src/zig/tests/renderer_test.zig @@ -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")); diff --git a/packages/core/src/zig/tests/terminal_test.zig b/packages/core/src/zig/tests/terminal_test.zig index 36a9acac9..41eb5c997 100644 --- a/packages/core/src/zig/tests/terminal_test.zig +++ b/packages/core/src/zig/tests/terminal_test.zig @@ -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" { @@ -1426,7 +1426,7 @@ 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); @@ -1434,7 +1434,7 @@ test "writeClipboard - Screen framing crosses the 252-byte boundary" { 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); @@ -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" { @@ -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;