From 00720df5960be39333eefb5ab977902c23dc10ee Mon Sep 17 00:00:00 2001 From: vantroy Date: Mon, 29 Jun 2026 14:22:10 +1000 Subject: [PATCH] feat(image): add image_quiet option to set the kitty graphics q flag Suppresses the terminal's responses to image transmit, placement, and delete commands so they cannot leak to the shell when an app exits before its reader drains them. Defaults to .off (behavior unchanged). --- src/Vaxis.zig | 116 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 12 deletions(-) diff --git a/src/Vaxis.zig b/src/Vaxis.zig index a354fc8b..f520c52e 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -47,6 +47,16 @@ pub const Options = struct { /// requests. If not supplied, it won't be possible to request the system /// clipboard system_clipboard_allocator: ?std.mem.Allocator = null, + /// Sets the Kitty graphics quietness (`q`) flag on image transmit, + /// placement, and delete commands + image_quiet: enum { + /// The terminal reports both successes and failures (default) + off, + /// The terminal suppresses success reports, but still reports failures + no_ok, + /// The terminal suppresses both success and failure reports + silent, + } = .off, }; io: std.Io, @@ -437,7 +447,7 @@ pub fn render(self: *Vaxis, tty: *std.Io.Writer) !void { reposition_ptr.* = true; // Clear all images if (vx.caps.kitty_graphics) - try io.writeAll(ctlseqs.kitty_graphics_clear); + try io.print("\x1b_Ga=d{s}\x1b\\", .{vx.imageQuietSeq()}); } }; @@ -577,6 +587,7 @@ pub fn render(self: *Vaxis, tty: *std.Io.Writer) !void { if (img.options.z_index) |z| { try tty.print(",z={d}", .{z}); } + try tty.writeAll(self.imageQuietSeq()); try tty.writeAll(ctlseqs.kitty_graphics_closing); } @@ -924,6 +935,14 @@ pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse { return result; } +fn imageQuietSeq(self: *const Vaxis) []const u8 { + return switch (self.opts.image_quiet) { + .off => "", + .no_ok => ",q=1", + .silent => ",q=2", + }; +} + /// Transmit an image using the local filesystem. Allocates only for base64 encoding pub fn transmitLocalImagePath( self: *Vaxis, @@ -957,20 +976,20 @@ pub fn transmitLocalImagePath( switch (format) { .rgb => { try tty.print( - "\x1b_Gf=24,s={d},v={d},i={d},t={c};{s}\x1b\\", - .{ width, height, id, medium_char, encoded }, + "\x1b_Gf=24,s={d},v={d},i={d}{s},t={c};{s}\x1b\\", + .{ width, height, id, self.imageQuietSeq(), medium_char, encoded }, ); }, .rgba => { try tty.print( - "\x1b_Gf=32,s={d},v={d},i={d},t={c};{s}\x1b\\", - .{ width, height, id, medium_char, encoded }, + "\x1b_Gf=32,s={d},v={d},i={d}{s},t={c};{s}\x1b\\", + .{ width, height, id, self.imageQuietSeq(), medium_char, encoded }, ); }, .png => { try tty.print( - "\x1b_Gf=100,i={d},t={c};{s}\x1b\\", - .{ id, medium_char, encoded }, + "\x1b_Gf=100,i={d}{s},t={c};{s}\x1b\\", + .{ id, self.imageQuietSeq(), medium_char, encoded }, ); }, } @@ -1005,12 +1024,13 @@ pub fn transmitPreEncodedImage( if (bytes.len < 4096) { try tty.print( - "\x1b_Gf={d},s={d},v={d},i={d};{s}\x1b\\", + "\x1b_Gf={d},s={d},v={d},i={d}{s};{s}\x1b\\", .{ fmt, width, height, id, + self.imageQuietSeq(), bytes, }, ); @@ -1018,8 +1038,8 @@ pub fn transmitPreEncodedImage( var n: usize = 4096; try tty.print( - "\x1b_Gf={d},s={d},v={d},i={d},m=1;{s}\x1b\\", - .{ fmt, width, height, id, bytes[0..n] }, + "\x1b_Gf={d},s={d},v={d},i={d}{s},m=1;{s}\x1b\\", + .{ fmt, width, height, id, self.imageQuietSeq(), bytes[0..n] }, ); while (n < bytes.len) : (n += 4096) { const end: usize = @min(n + 4096, bytes.len); @@ -1096,8 +1116,8 @@ pub fn loadImage( } /// deletes an image from the terminal's memory -pub fn freeImage(_: Vaxis, tty: *std.Io.Writer, id: u32) void { - tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { +pub fn freeImage(self: Vaxis, tty: *std.Io.Writer, id: u32) void { + tty.print("\x1b_Ga=d,d=I,i={d}{s};\x1b\\", .{ id, self.imageQuietSeq() }) catch |err| { log.err("couldn't delete image {d}: {}", .{ id, err }); return; }; @@ -1320,6 +1340,7 @@ pub fn prettyPrint(self: *Vaxis, tty: *std.Io.Writer) !void { if (img.options.z_index) |z| { try tty.print(",z={d}", .{z}); } + try tty.writeAll(self.imageQuietSeq()); try tty.writeAll(ctlseqs.kitty_graphics_closing); } @@ -1523,3 +1544,74 @@ test "render: no output when no changes" { defer std.testing.allocator.free(output); try std.testing.expectEqual(@as(usize, 0), output.len); } + +test "image_quiet: silent sets q=2 on transmit" { + const io = std.testing.io; + var env_map = try std.testing.environ.createMap(std.testing.allocator); + defer env_map.deinit(); + var vx = try Vaxis.init(io, std.testing.allocator, &env_map, .{ .image_quiet = .silent }); + var deinit_writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer deinit_writer.deinit(); + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); + vx.caps.kitty_graphics = true; + + var writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer writer.deinit(); + _ = try vx.transmitPreEncodedImage(&writer.writer, "AAAA", 1, 1, .rgba); + const output = try writer.toOwnedSlice(); + defer std.testing.allocator.free(output); + try std.testing.expect(std.mem.indexOf(u8, output, ",q=2") != null); +} + +test "image_quiet: off leaves transmit unchanged" { + const io = std.testing.io; + var env_map = try std.testing.environ.createMap(std.testing.allocator); + defer env_map.deinit(); + var vx = try Vaxis.init(io, std.testing.allocator, &env_map, .{}); + var deinit_writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer deinit_writer.deinit(); + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); + vx.caps.kitty_graphics = true; + + var writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer writer.deinit(); + _ = try vx.transmitPreEncodedImage(&writer.writer, "AAAA", 1, 1, .rgba); + const output = try writer.toOwnedSlice(); + defer std.testing.allocator.free(output); + try std.testing.expect(std.mem.indexOf(u8, output, "q=") == null); +} + +test "image_quiet: no_ok sets q=1 on transmit" { + const io = std.testing.io; + var env_map = try std.testing.environ.createMap(std.testing.allocator); + defer env_map.deinit(); + var vx = try Vaxis.init(io, std.testing.allocator, &env_map, .{ .image_quiet = .no_ok }); + var deinit_writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer deinit_writer.deinit(); + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); + vx.caps.kitty_graphics = true; + + var writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer writer.deinit(); + _ = try vx.transmitPreEncodedImage(&writer.writer, "AAAA", 1, 1, .rgba); + const output = try writer.toOwnedSlice(); + defer std.testing.allocator.free(output); + try std.testing.expect(std.mem.indexOf(u8, output, ",q=1") != null); +} + +test "image_quiet: silent sets q=2 on freeImage" { + const io = std.testing.io; + var env_map = try std.testing.environ.createMap(std.testing.allocator); + defer env_map.deinit(); + var vx = try Vaxis.init(io, std.testing.allocator, &env_map, .{ .image_quiet = .silent }); + var deinit_writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer deinit_writer.deinit(); + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); + + var writer: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer writer.deinit(); + vx.freeImage(&writer.writer, 1); + const output = try writer.toOwnedSlice(); + defer std.testing.allocator.free(output); + try std.testing.expect(std.mem.indexOf(u8, output, ",q=2") != null); +}