diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/stream_terminal.zig | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff) | |
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/stream_terminal.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/stream_terminal.zig | 509 |
1 files changed, 493 insertions, 16 deletions
diff --git a/vendor/libghostty-vt/src/terminal/stream_terminal.zig b/vendor/libghostty-vt/src/terminal/stream_terminal.zig index f68f088b..91cfb21c 100644 --- a/vendor/libghostty-vt/src/terminal/stream_terminal.zig +++ b/vendor/libghostty-vt/src/terminal/stream_terminal.zig @@ -2,13 +2,16 @@ const std = @import("std"); const build_options = @import("terminal_options"); const testing = std.testing; const apc = @import("apc.zig"); +const clipboard = @import("clipboard.zig"); const csi = @import("csi.zig"); const device_attributes = @import("device_attributes.zig"); const device_status = @import("device_status.zig"); const stream = @import("stream.zig"); const Action = stream.Action; const Screen = @import("Screen.zig"); +const color = @import("color.zig"); const modes = @import("modes.zig"); +const osc = @import("osc.zig"); const osc_color = @import("osc/parsers/color.zig"); const kitty_color = @import("kitty/color.zig"); const size_report = @import("size_report.zig"); @@ -43,6 +46,11 @@ pub const Handler = struct { /// the kitty graphics protocol. apc_handler: apc.Handler = .{}, + /// Default cursor style used by DECSCUSR reset (CSI 0 q). + default_cursor: bool = true, + default_cursor_style: Screen.CursorStyle = .block, + default_cursor_blink: bool = false, + pub const Effects = struct { /// Called when the terminal needs to write data back to the pty, /// e.g. in response to a DECRQM query. The data is only valid @@ -78,6 +86,25 @@ pub const Handler = struct { /// handler.terminal.getTitle(). title_changed: ?*const fn (*Handler) void, + /// Called when the terminal pwd changes via escape sequences + /// (e.g. OSC 7). The new pwd can be queried via + /// handler.terminal.getPwd(). + pwd_changed: ?*const fn (*Handler) void, + + /// Called when the running program writes to a clipboard. The write + /// has a normalized destination and one or more decoded MIME + /// representations. All request, MIME, and data memory is borrowed + /// and only valid for the duration of the callback. + /// + /// A write with no contents clears the destination. A content entry + /// with empty data is a distinct empty representation. + /// + /// Clipboard read requests (OSC 52 with a "?" payload) are never + /// forwarded: answering one would let any program running in the + /// terminal silently read the user's clipboard, and a VT state + /// library has no way to mediate that with user consent. + clipboard_write: ?*const fn (*Handler, clipboard.Write) clipboard.WriteResult, + /// Called in response to an XTVERSION query. Returns the version /// string to report (e.g. "ghostty 1.2.3"). The returned memory /// must be valid for the lifetime of the call. The maximum length @@ -89,11 +116,13 @@ pub const Handler = struct { /// effects beyond that. pub const readonly: Effects = .{ .bell = null, + .clipboard_write = null, .color_scheme = null, .device_attributes = null, .enquiry = null, .size = null, .title_changed = null, + .pwd_changed = null, .write_pty = null, .xtversion = null, }; @@ -126,6 +155,7 @@ pub const Handler = struct { ) !void { switch (action) { .print => try self.terminal.print(value.cp), + .print_slice => try self.terminal.printSlice(value.cps), .print_repeat => try self.terminal.printRepeat(value), .backspace => self.terminal.backspace(), .carriage_return => self.terminal.carriageReturn(), @@ -152,12 +182,19 @@ pub const Handler = struct { self.terminal.screens.active.cursor.x + 1, ), .cursor_style => { + self.default_cursor = false; + const blink = switch (value) { - .default, .steady_block, .steady_bar, .steady_underline => false, + .default => self.default_cursor_blink, + .steady_block, .steady_bar, .steady_underline => false, .blinking_block, .blinking_bar, .blinking_underline => true, }; const style: Screen.CursorStyle = switch (value) { - .default, .blinking_block, .steady_block => .block, + .default => style: { + self.default_cursor = true; + break :style self.default_cursor_style; + }, + .blinking_block, .steady_block => .block, .blinking_bar, .steady_bar => .bar, .blinking_underline, .steady_underline => .underline, }; @@ -228,17 +265,23 @@ pub const Handler = struct { }, .active_status_display => self.terminal.status_display = value, .decaln => try self.terminal.decaln(), - .full_reset => self.terminal.fullReset(), + .full_reset => { + self.terminal.fullReset(); + self.default_cursor = true; + self.terminal.modes.set(.cursor_blinking, self.default_cursor_blink); + self.terminal.screens.active.cursor.cursor_style = self.default_cursor_style; + }, .start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id), .end_hyperlink => self.terminal.screens.active.endHyperlink(), .semantic_prompt => try self.terminal.semanticPrompt(value), .mouse_shape => self.terminal.mouse_shape = value, - .color_operation => try self.colorOperation(value.op, &value.requests), + .color_operation => try self.colorOperation(&value.requests, value.terminator), .kitty_color_report => try self.kittyColorOperation(value), // APC .apc_start => self.apc_handler.start(), .apc_put => self.apc_handler.feed(self.terminal.gpa(), value), + .apc_put_slice => self.apc_handler.feedSlice(self.terminal.gpa(), value.bytes), .apc_end => self.apcEnd(), // Effect-based handlers @@ -251,7 +294,9 @@ pub const Handler = struct { .request_mode_unknown => self.requestModeUnknown(value.mode, value.ansi), .size_report => self.reportSize(value), .window_title => self.windowTitle(value.title), + .report_pwd => self.reportPwd(value.url), .xtversion => self.reportXtversion(), + .clipboard_contents => try self.clipboardContents(value.kind, value.data), // No supported DCS commands have any terminal-modifying effects, // but they may in the future. For now we just ignore it. @@ -261,10 +306,8 @@ pub const Handler = struct { => {}, // Have no terminal-modifying effect - .report_pwd, .show_desktop_notification, .progress_report, - .clipboard_contents, .title_push, .title_pop, => {}, @@ -281,6 +324,44 @@ pub const Handler = struct { func(self); } + fn clipboardContents(self: *Handler, kind: u8, data: []const u8) !void { + const func = self.effects.clipboard_write orelse return; + + // Read requests are deliberately not forwarded; see the effect docs. + if (data.len == 1 and data[0] == '?') return; + + const location: clipboard.Location = switch (kind) { + 's' => .selection, + 'p' => .primary, + else => .standard, + }; + + // OSC 52 uses an empty payload to clear the selected clipboard. + if (data.len == 0) { + _ = func(self, .{ + .location = location, + .contents = &.{}, + }); + return; + } + + const decoder = std.base64.standard.Decoder; + const decoded_len = try decoder.calcSizeForSlice(data); + const alloc = self.terminal.gpa(); + const decoded = try alloc.alloc(u8, decoded_len); + defer alloc.free(decoded); + try decoder.decode(decoded, data); + + const contents = [_]clipboard.Content{.{ + .mime = "text/plain", + .data = decoded, + }}; + _ = func(self, .{ + .location = location, + .contents = &contents, + }); + } + fn reportDeviceAttributes(self: *Handler, req: device_attributes.Req) void { const func = self.effects.device_attributes orelse return; const attrs = func(self); @@ -325,10 +406,11 @@ pub const Handler = struct { .color_scheme => { const func = self.effects.color_scheme orelse return; const scheme = func(self) orelse return; - self.writePty(switch (scheme) { - .dark => "\x1B[?997;1n", - .light => "\x1B[?997;2n", - }); + var buf: [device_status.max_color_scheme_report_encode_size + 1]u8 = undefined; + var writer: std.Io.Writer = .fixed(buf[0..device_status.max_color_scheme_report_encode_size]); + device_status.encodeColorSchemeReport(&writer, scheme) catch return; + buf[writer.end] = 0; + self.writePty(buf[0..writer.end :0]); }, } } @@ -420,6 +502,29 @@ pub const Handler = struct { func(self); } + fn reportPwd(self: *Handler, url_raw: []const u8) void { + // Prevent DoS attacks by limiting url length. Headroom for + // Linux PATH_MAX (4096) plus URI scheme/host and percent-encoding. + const max_url_len = 4096; + const url = if (url_raw.len > max_url_len) url: { + log.warn("pwd url length {d} exceeds max length {d}, truncating", .{ + url_raw.len, + max_url_len, + }); + break :url url_raw[0..max_url_len]; + } else url_raw; + + // We store the raw payload unparsed. Embedders read it via + // getPwd() and are responsible for decoding any URI scheme. + self.terminal.setPwd(url) catch |err| { + log.warn("error setting pwd err={}", .{err}); + return; + }; + + const func = self.effects.pwd_changed orelse return; + func(self); + } + fn requestMode(self: *Handler, mode: modes.Mode) void { const report = self.terminal.modes.getReport(.fromMode(mode)); self.sendModeReport(report); @@ -550,12 +655,17 @@ pub const Handler = struct { fn colorOperation( self: *Handler, - op: osc_color.Operation, requests: *const osc_color.List, + terminator: osc.Terminator, ) !void { - _ = op; if (requests.count() == 0) return; + var stack = std.heap.stackFallback(1024, self.terminal.gpa()); + const alloc = stack.get(); + var response: std.Io.Writer.Allocating = .init(alloc); + defer response.deinit(); + const writer = &response.writer; + var it = requests.constIterator(0); while (it.next()) |req| { switch (req.*) { @@ -613,17 +723,67 @@ pub const Handler = struct { mask.* = .initEmpty(); }, - .query, - .reset_special, - => {}, + .query => |target| { + if (self.effects.write_pty == null) continue; + const c = self.terminal.colorForXterm(target) orelse continue; + try writeXtermColorReport(writer, target, c, terminator); + }, + + .reset_special => {}, } } + + if (response.written().len > 0) { + const resp = try response.toOwnedSliceSentinel(0); + defer alloc.free(resp); + self.writePty(resp); + } + } + + fn writeXtermColorReport( + writer: *std.Io.Writer, + target: osc_color.Target, + c: color.RGB, + terminator: osc.Terminator, + ) !void { + switch (target) { + .palette => |i| { + try writer.print("\x1b]4;{d};", .{i}); + try c.encodeRgb16(writer); + try writer.writeAll(terminator.string()); + }, + .dynamic => |dynamic| switch (dynamic) { + .foreground, + .background, + .cursor, + => { + try writer.print("\x1b]{d};", .{@intFromEnum(dynamic)}); + try c.encodeRgb16(writer); + try writer.writeAll(terminator.string()); + }, + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => {}, + }, + .special => {}, + } } fn kittyColorOperation( self: *Handler, request: kitty_color.OSC, ) !void { + var stack = std.heap.stackFallback(1024, self.terminal.gpa()); + const alloc = stack.get(); + var response: std.Io.Writer.Allocating = .init(alloc); + defer response.deinit(); + const writer = &response.writer; + for (request.list.items) |item| { switch (item) { .set => |v| switch (v.key) { @@ -650,9 +810,28 @@ pub const Handler = struct { else => {}, }, }, - .query => {}, + .query => |key| { + if (self.effects.write_pty == null) continue; + const c = self.terminal.colorForKitty(key) orelse { + if (!key.hasTerminalQueryColor()) continue; + if (response.written().len == 0) try writer.writeAll("\x1b]21"); + try writer.print(";{f}=", .{key}); + continue; + }; + + if (response.written().len == 0) try writer.writeAll("\x1b]21"); + try writer.print(";{f}=", .{key}); + try c.encodeRgb8(writer); + }, } } + + if (response.written().len > 0) { + try writer.writeAll(request.terminator.string()); + const resp = try response.toOwnedSliceSentinel(0); + defer alloc.free(resp); + self.writePty(resp); + } } fn apcEnd(self: *Handler) void { @@ -679,6 +858,25 @@ pub const Handler = struct { if (final.len > 3) self.writePty(final[0 .. final.len - 1 :0]); } }, + + .glyph => |*glyph_req| { + const resp = self.terminal.glyphProtocol(alloc, glyph_req); + if (resp) |r| resp_block: { + // Don't waste time encoding if we can't write responses + // anyways. + if (self.effects.write_pty == null) break :resp_block; + + // Glyph responses are short and bounded by the protocol + // fields we emit, so this matches the Kitty response + // buffer size above with ample headroom. + var buf: [apc.glyph.Response.max_wire_bytes]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + r.formatWire(&writer) catch return; + writer.writeByte(0) catch return; + const final = writer.buffered(); + self.writePty(final[0 .. final.len - 1 :0]); + } + }, } } }; @@ -900,6 +1098,8 @@ test "full reset" { s.nextSlice("\x1B[10;20H"); s.nextSlice("\x1B[5;20r"); // Set scroll region s.nextSlice("\x1B[?7l"); // Disable wraparound + s.nextSlice("\x1B_25a1;r;cp=e0a0;AAAAAAAAAAAAAA==\x1B\\"); + try testing.expect(t.glyph_glossary.contains(0xE0A0)); // Full reset s.nextSlice("\x1Bc"); @@ -910,6 +1110,35 @@ test "full reset" { try testing.expectEqual(@as(usize, 0), t.scrolling_region.top); try testing.expectEqual(@as(usize, 23), t.scrolling_region.bottom); try testing.expect(t.modes.get(.wraparound)); + try testing.expect(!t.glyph_glossary.contains(0xE0A0)); +} + +test "glyph protocol APC with write_pty callback" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); + defer t.deinit(testing.allocator); + + const S = struct { + var last_response: ?[:0]const u8 = null; + fn writePty(_: *Handler, data: [:0]const u8) void { + if (last_response) |old| testing.allocator.free(old); + last_response = testing.allocator.dupeZ(u8, data) catch @panic("OOM"); + } + }; + S.last_response = null; + defer if (S.last_response) |old| testing.allocator.free(old); + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + s.nextSlice("\x1B_25a1;s\x1B\\"); + try testing.expectEqualStrings("\x1B_25a1;s;fmt=glyf\x1B\\", S.last_response.?); + + s.nextSlice("\x1B_25a1;r;cp=e0a0;AAAAAAAAAAAAAA==\x1B\\"); + try testing.expectEqualStrings("\x1B_25a1;r;cp=e0a0;status=0\x1B\\", S.last_response.?); + try testing.expect(t.glyph_glossary.contains(0xE0A0)); } test "ignores query actions" { @@ -923,6 +1152,8 @@ test "ignores query actions" { s.nextSlice("\x1B[c"); // Device attributes s.nextSlice("\x1B[5n"); // Device status report s.nextSlice("\x1B[6n"); // Cursor position report + s.nextSlice("\x1B]4;0;?\x1B\\"); // OSC color query + s.nextSlice("\x1B]21;foreground=?\x1B\\"); // Kitty color query // Terminal should still be functional s.nextSlice("Test"); @@ -1039,6 +1270,63 @@ test "OSC 12 set and reset cursor color" { // After reset, cursor might be null (using default) } +test "OSC color query responses" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + const S = struct { + var last_response: ?[:0]const u8 = null; + + fn reset() void { + if (last_response) |old| testing.allocator.free(old); + last_response = null; + } + + fn writePty(_: *Handler, data: [:0]const u8) void { + reset(); + last_response = testing.allocator.dupeZ(u8, data) catch @panic("OOM"); + } + }; + S.last_response = null; + defer S.reset(); + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + s.nextSlice("\x1b]10;?\x1b\\"); + try testing.expect(S.last_response == null); + + s.nextSlice("\x1b]11;?\x1b\\"); + try testing.expect(S.last_response == null); + + s.nextSlice("\x1b]4;2;rgb:12/34/56;2;?\x1b\\"); + try testing.expectEqualStrings( + "\x1b]4;2;rgb:1212/3434/5656\x1b\\", + S.last_response.?, + ); + + s.nextSlice("\x1b]10;rgb:01/02/03\x1b\\"); + s.nextSlice("\x1b]11;rgb:04/05/06\x1b\\"); + s.nextSlice("\x1b]12;rgb:07/08/09\x1b\\"); + s.nextSlice("\x1b]10;?;?;?\x1b\\"); + try testing.expectEqualStrings( + "\x1b]10;rgb:0101/0202/0303\x1b\\" ++ + "\x1b]11;rgb:0404/0505/0606\x1b\\" ++ + "\x1b]12;rgb:0707/0808/0909\x1b\\", + S.last_response.?, + ); + + s.nextSlice("\x1b]112\x1b\\"); + s.nextSlice("\x1b]12;?\x07"); + try testing.expectEqualStrings( + "\x1b]12;rgb:0101/0202/0303\x07", + S.last_response.?, + ); +} + test "kitty color protocol set palette" { var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator); @@ -1133,6 +1421,46 @@ test "kitty color protocol reset foreground" { try testing.expect(t.colors.foreground.get() == null); } +test "kitty color protocol query responses" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + const S = struct { + var last_response: ?[:0]const u8 = null; + + fn reset() void { + if (last_response) |old| testing.allocator.free(old); + last_response = null; + } + + fn writePty(_: *Handler, data: [:0]const u8) void { + reset(); + last_response = testing.allocator.dupeZ(u8, data) catch @panic("OOM"); + } + }; + S.last_response = null; + defer S.reset(); + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + s.nextSlice("\x1b]21;background=?\x1b\\"); + try testing.expectEqualStrings( + "\x1b]21;background=\x1b\\", + S.last_response.?, + ); + + s.nextSlice("\x1b]21;foreground=rgb:12/34/56;2=rgb:aa/bb/cc\x1b\\"); + s.nextSlice("\x1b]21;foreground=?;background=?;2=?\x1b\\"); + try testing.expectEqualStrings( + "\x1b]21;foreground=rgb:12/34/56;background=;2=rgb:aa/bb/cc\x1b\\", + S.last_response.?, + ); +} + test "palette dirty flag set on color change" { var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator); @@ -1321,6 +1649,155 @@ test "bell effect callback" { } } +test "clipboard_write effect callback" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); + defer t.deinit(testing.allocator); + + // A null callback (the default readonly effects) silently ignores writes. + { + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + s.nextSlice("\x1B]52;c;aGVsbG8=\x1B\\"); + + // Terminal should still be functional after the ignored sequence + s.nextSlice("AfterClipboard"); + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("AfterClipboard", str); + } + + t.fullReset(); + + const S = struct { + var count: usize = 0; + var result: clipboard.WriteResult = .success; + var last_location: clipboard.Location = .standard; + var last_contents_len: usize = 0; + var last_mime: ?[]u8 = null; + var last_data: ?[]u8 = null; + + fn clearCapture() void { + if (last_mime) |value| testing.allocator.free(value); + if (last_data) |value| testing.allocator.free(value); + last_mime = null; + last_data = null; + last_contents_len = 0; + } + + fn clipboardWrite(_: *Handler, write: clipboard.Write) clipboard.WriteResult { + clearCapture(); + count += 1; + last_location = write.location; + last_contents_len = write.contents.len; + if (write.contents.len > 0) { + last_mime = testing.allocator.dupe(u8, write.contents[0].mime) catch + @panic("failed to capture clipboard MIME type"); + last_data = testing.allocator.dupe(u8, write.contents[0].data) catch + @panic("failed to capture clipboard data"); + } + return result; + } + }; + S.count = 0; + S.result = .denied; + S.clearCapture(); + defer S.clearCapture(); + + var handler: Handler = .init(&t); + handler.effects.clipboard_write = &S.clipboardWrite; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + // Selectors are normalized and payloads are decoded before the callback. + const cases = [_]struct { + sequence: []const u8, + location: clipboard.Location, + data: []const u8, + }{ + .{ .sequence = "\x1B]52;c;aGVsbG8=\x1B\\", .location = .standard, .data = "hello" }, + .{ .sequence = "\x1B]52;s;d29ybGQ=\x07", .location = .selection, .data = "world" }, + .{ .sequence = "\x1B]52;p;cHJpbWFyeQ==\x1B\\", .location = .primary, .data = "primary" }, + .{ .sequence = "\x1B]52;0;Y3V0\x1B\\", .location = .standard, .data = "cut" }, + .{ .sequence = "\x1B]52;x;ZmFsbGJhY2s=\x1B\\", .location = .standard, .data = "fallback" }, + .{ .sequence = "\x1B]52;c;YQBi\x1B\\", .location = .standard, .data = "a\x00b" }, + }; + + for (cases, 1..) |case, expected_count| { + s.nextSlice(case.sequence); + try testing.expectEqual(expected_count, S.count); + try testing.expectEqual(case.location, S.last_location); + try testing.expectEqual(@as(usize, 1), S.last_contents_len); + try testing.expectEqualStrings("text/plain", S.last_mime.?); + try testing.expectEqualSlices(u8, case.data, S.last_data.?); + } + + // Empty data is a clear, represented by an empty contents slice. + s.nextSlice("\x1B]52;s;\x1B\\"); + try testing.expectEqual(@as(usize, cases.len + 1), S.count); + try testing.expectEqual(clipboard.Location.selection, S.last_location); + try testing.expectEqual(@as(usize, 0), S.last_contents_len); + try testing.expect(S.last_mime == null); + try testing.expect(S.last_data == null); + + // Reads and malformed base64 are ignored. + s.nextSlice("\x1B]52;c;?\x1B\\"); + s.nextSlice("\x1B]52;c;***\x1B\\"); + try testing.expectEqual(@as(usize, cases.len + 1), S.count); + + // OSC 1337 Copy shares the normalized clipboard write path. + s.nextSlice("\x1B]1337;Copy=:aVRlcm0y\x1B\\"); + try testing.expectEqual(@as(usize, cases.len + 2), S.count); + try testing.expectEqual(clipboard.Location.standard, S.last_location); + try testing.expectEqualStrings("text/plain", S.last_mime.?); + try testing.expectEqualStrings("iTerm2", S.last_data.?); + + // Parsing across write boundaries still invokes exactly one atomic write. + s.nextSlice("\x1B]52;p;ZnJh"); + s.nextSlice("Z21lbnRlZA==\x1B"); + s.nextSlice("\\"); + try testing.expectEqual(@as(usize, cases.len + 3), S.count); + try testing.expectEqual(clipboard.Location.primary, S.last_location); + try testing.expectEqualStrings("text/plain", S.last_mime.?); + try testing.expectEqualStrings("fragmented", S.last_data.?); + + // Callback results are intentionally ignored for protocols without a + // write acknowledgement. The denied result above did not stop later writes. + try testing.expectEqual(clipboard.WriteResult.denied, S.result); +} + +test "clipboard_write allocation failure is ignored" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); + defer t.deinit(testing.allocator); + + const S = struct { + var count: usize = 0; + + fn clipboardWrite(_: *Handler, _: clipboard.Write) clipboard.WriteResult { + count += 1; + return .success; + } + }; + S.count = 0; + + var handler: Handler = .init(&t); + handler.effects.clipboard_write = &S.clipboardWrite; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + // Only the decoded scratch data uses the terminal allocator here. Swap in + // an allocator that always fails, then restore it before terminal teardown. + { + const alloc = t.screens.active.alloc; + t.screens.active.alloc = testing.failing_allocator; + defer t.screens.active.alloc = alloc; + s.nextSlice("\x1B]52;c;aGVsbG8=\x1B\\"); + } + try testing.expectEqual(@as(usize, 0), S.count); +} + test "request mode DECRQM with write_pty callback" { var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); defer t.deinit(testing.allocator); |
