terminal/kitty: preserve chunked response identifiers

Chunked image responses used the final command even though only the
initial chunk carries the image and placement identifiers. Successful
replies lost image numbers and placement IDs. Final validation errors
could also be suppressed entirely.

Save the initial response identifiers with the in-progress image and use
them when the final chunk completes. Continue replacing the response ID
with the generated image ID after a successful load. Cover successful
image-number replies and invalid final payloads with unit tests.
This commit is contained in:
Mitchell Hashimoto
2026-08-18 12:09:40 -07:00
parent 2ced1e5c8e
commit 86d94f150a
2 changed files with 84 additions and 5 deletions

View File

@@ -160,11 +160,15 @@ fn transmit(
cmd: *const Command,
) Response {
const t = cmd.transmission().?;
var result: Response = .{
.id = t.image_id,
.image_number = t.image_number,
.placement_id = t.placement_id,
};
const storage = &terminal.screens.active.kitty_images;
var result: Response = if (storage.loading) |loading|
loading.response
else
.{
.id = t.image_id,
.image_number = t.image_number,
.placement_id = t.placement_id,
};
const load = loadAndAddImage(io, alloc, terminal, cmd) catch |err| {
encodeError(&result, err);
@@ -594,6 +598,72 @@ test "kittygfx conflicting identifiers are rejected before mutation" {
}
}
test "kittygfx chunked success response uses initial identifiers" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;
var t = try Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
{
const cmd = try command.Parser.parseString(
alloc,
"a=t,f=24,s=1,v=2,I=93,p=7,m=1;AAAA",
);
defer cmd.deinit(alloc);
try testing.expect(execute(io, alloc, &t, &cmd) == null);
}
{
const cmd = try command.Parser.parseString(alloc, "m=0;AAAA");
defer cmd.deinit(alloc);
const resp = execute(io, alloc, &t, &cmd).?;
try testing.expect(resp.ok());
try testing.expectEqual(@as(u32, 2147483647), resp.id);
try testing.expectEqual(@as(u32, 93), resp.image_number);
try testing.expectEqual(@as(u32, 7), resp.placement_id);
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try resp.encode(&writer);
try testing.expectEqualStrings(
"\x1b_Gi=2147483647,I=93,p=7;OK\x1b\\",
writer.buffered(),
);
}
}
test "kittygfx chunked error response uses initial identifiers" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;
var t = try Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
{
const cmd = try command.Parser.parseString(
alloc,
"a=t,f=24,s=1,v=1,i=41,p=7,m=1;AA==",
);
defer cmd.deinit(alloc);
try testing.expect(execute(io, alloc, &t, &cmd) == null);
}
{
const cmd = try command.Parser.parseString(alloc, "m=0;AA==");
defer cmd.deinit(alloc);
const resp = execute(io, alloc, &t, &cmd).?;
try testing.expect(!resp.ok());
try testing.expectEqual(@as(u32, 41), resp.id);
try testing.expectEqual(@as(u32, 7), resp.placement_id);
try testing.expectEqualStrings("EINVAL: invalid data", resp.message);
}
}
test "kittygfx more chunks with q=1" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@@ -39,6 +39,10 @@ pub const LoadingImage = struct {
/// used if q isn't set on subsequent chunks.
quiet: command.Command.Quiet,
/// Response identifiers from the initial load command. Subsequent chunks
/// omit these, so completion responses must use the saved values.
response: command.Response = .{},
/// The temporary directory for file transmission (null means that
/// temporary directory transmission is disabled).
temporary_directory: ?[]const u8,
@@ -104,6 +108,11 @@ pub const LoadingImage = struct {
.display = cmd.display(),
.quiet = cmd.quiet,
.response = .{
.id = t.image_id,
.image_number = t.image_number,
.placement_id = t.placement_id,
},
.temporary_directory = switch (limits.temporary_file) {
.enabled => |d| d.directory,
.disabled => null,