// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Showing a PNG with the kitty graphics protocol. //! //! The image is transmitted and placed in one go (`a=T`) at the cursor, at //! its own size in pixels, so the terminal draws it without resampling. The //! data travels in the escape codes themselves, base64 encoded, since over //! SSH the terminal has no way to read a file of ours. Replies are //! suppressed (`q=2`): nothing reads them after the banner is shown. const std = @import("std"); const Io = std.Io; /// The most base64 the protocol allows in one escape code. const chunk_len = 4096; /// Writes the escape codes that show `png` at the cursor, then moves the /// cursor to the start of the line below the image. pub fn writeImage(w: *Io.Writer, png: []const u8) Io.Writer.Error!void { const encoder = std.base64.standard.Encoder; // Whole groups of three bytes, so that no chunk but the last is padded. const raw_per_chunk = chunk_len / 4 * 3; var rest = png; var first = true; while (true) { const take = @min(rest.len, raw_per_chunk); const more = rest.len > take; try w.writeAll("\x1b_G"); if (first) try w.writeAll("a=T,f=100,q=2,"); try w.print("m={d};", .{@intFromBool(more)}); var buf: [chunk_len]u8 = undefined; try w.writeAll(encoder.encode(&buf, rest[0..take])); try w.writeAll("\x1b\\"); rest = rest[take..]; first = false; if (!more) break; } // Placing an image leaves the cursor just after its bottom-right cell. try w.writeAll("\r\n"); } const testing = std.testing; test "a small image is one escape code" { var buf: [256]u8 = undefined; var w: Io.Writer = .fixed(&buf); try writeImage(&w, "abc"); try testing.expectEqualStrings("\x1b_Ga=T,f=100,q=2,m=0;YWJj\x1b\\\r\n", w.buffered()); } test "a large image is split into chunks" { const gpa = testing.allocator; const data = try gpa.alloc(u8, 3072 * 2 + 1); defer gpa.free(data); @memset(data, 0); var out: Io.Writer.Allocating = .init(gpa); defer out.deinit(); try writeImage(&out.writer, data); const s = out.written(); try testing.expectEqual(@as(usize, 3), std.mem.count(u8, s, "\x1b_G")); try testing.expect(std.mem.startsWith(u8, s, "\x1b_Ga=T,f=100,q=2,m=1;")); try testing.expectEqual(@as(usize, 1), std.mem.count(u8, s, "\x1b_Gm=1;")); try testing.expectEqual(@as(usize, 1), std.mem.count(u8, s, "\x1b_Gm=0;AA==\x1b\\")); }