mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-03 06:09:04 +00:00
This commit represents the majority of the work necessary to upgrade Ghostty to use Zig 0.16.0. Key parts: * In addition to its previous responsibilities, the global state now houses state for global I/O implementations and the process environment. It is now also utilized in the main application along with the C library. Where necessary, global state is isolated from key parts of the implementation (e.g., in libghostty subsystems), and it's expected that this list will grow. * We currently manage our own C translation layer where necessary. In these cases, cImport has been removed in favor of the new external translate-c package. Due to fixes that have needed be made to properly translate the dependencies that were swapped out, as mentioned, we have had to backport fixes from the current translate-c package (and the upstream Arocc dependency). We will host this ourselves until Zig 0.17.0 is released with these fixes. * Where necessary (only a small number of cases), some stdlib code from 0.15.2 (and even from 0.17.0) has been taken, adopted, and vendored in lib/compat. Co-authored-by: Leah Amelia Chen <hi@pluie.me>
676 lines
21 KiB
Zig
676 lines
21 KiB
Zig
const std = @import("std");
|
|
const assert = @import("../../quirks.zig").inlineAssert;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const Terminal = @import("../Terminal.zig");
|
|
const command = @import("graphics_command.zig");
|
|
const image = @import("graphics_image.zig");
|
|
const Command = command.Command;
|
|
const Response = command.Response;
|
|
const LoadingImage = image.LoadingImage;
|
|
const Image = image.Image;
|
|
const ImageStorage = @import("graphics_storage.zig").ImageStorage;
|
|
|
|
const log = std.log.scoped(.kitty_gfx);
|
|
|
|
/// Execute a Kitty graphics command against the given terminal. This
|
|
/// will never fail, but the response may indicate an error and the
|
|
/// terminal state may not be updated to reflect the command. This will
|
|
/// never put the terminal in an unrecoverable state, however.
|
|
///
|
|
/// The allocator must be the same allocator that was used to build
|
|
/// the command.
|
|
pub fn execute(
|
|
io: std.Io,
|
|
alloc: Allocator,
|
|
terminal: *Terminal,
|
|
cmd: *const Command,
|
|
) ?Response {
|
|
// If storage is disabled then we disable the full protocol. This means
|
|
// we don't even respond to queries so the terminal completely acts as
|
|
// if this feature is not supported.
|
|
if (!terminal.screens.active.kitty_images.enabled()) {
|
|
log.debug("kitty graphics requested but disabled", .{});
|
|
return null;
|
|
}
|
|
|
|
log.debug("executing kitty graphics command: quiet={} control={}", .{
|
|
cmd.quiet,
|
|
cmd.control,
|
|
});
|
|
|
|
// The quiet settings used to control the response. We have to make this
|
|
// a var because in certain special cases (namely chunked transmissions)
|
|
// this can change.
|
|
var quiet = cmd.quiet;
|
|
|
|
const resp_: ?Response = switch (cmd.control) {
|
|
.query => query(io, alloc, terminal, cmd),
|
|
.display => display(io, alloc, terminal, cmd),
|
|
.delete => delete(io, alloc, terminal, cmd),
|
|
|
|
.transmit, .transmit_and_display => resp: {
|
|
// If we're transmitting, then our `q` setting value is complicated.
|
|
// The `q` setting inherits the value from the starting command
|
|
// unless `q` is set >= 1 on this command. If it is, then we save
|
|
// that as the new `q` setting.
|
|
const storage = &terminal.screens.active.kitty_images;
|
|
if (storage.loading) |loading| switch (cmd.quiet) {
|
|
// q=0 we use whatever the start command value is
|
|
.no => quiet = loading.quiet,
|
|
|
|
// q>=1 we use the new value, but we should already be set to it
|
|
inline .ok, .failures => |tag| {
|
|
assert(quiet == tag);
|
|
loading.quiet = tag;
|
|
},
|
|
};
|
|
|
|
break :resp transmit(io, alloc, terminal, cmd);
|
|
},
|
|
|
|
.transmit_animation_frame,
|
|
.control_animation,
|
|
.compose_animation,
|
|
=> .{ .message = "ERROR: unimplemented action" },
|
|
};
|
|
|
|
// Handle the quiet settings
|
|
if (resp_) |resp| {
|
|
if (!resp.ok()) {
|
|
log.warn("erroneous kitty graphics response: {s}", .{resp.message});
|
|
}
|
|
|
|
return switch (quiet) {
|
|
.no => if (resp.empty()) null else resp,
|
|
.ok => if (resp.ok()) null else resp,
|
|
.failures => null,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
/// Execute a "query" command.
|
|
///
|
|
/// This command is used to attempt to load an image and respond with
|
|
/// success/error but does not persist any of the command to the terminal
|
|
/// state.
|
|
fn query(
|
|
io: std.Io,
|
|
alloc: Allocator,
|
|
terminal: *const Terminal,
|
|
cmd: *const Command,
|
|
) Response {
|
|
const t = cmd.control.query;
|
|
|
|
// Query requires image ID. We can't actually send a response without
|
|
// an image ID either but we return an error and this will be logged
|
|
// downstream.
|
|
if (t.image_id == 0) {
|
|
return .{ .message = "EINVAL: image ID required" };
|
|
}
|
|
|
|
// Build a partial response to start
|
|
var result: Response = .{
|
|
.id = t.image_id,
|
|
.image_number = t.image_number,
|
|
.placement_id = t.placement_id,
|
|
};
|
|
|
|
// Attempt to load the image. If we cannot, then set an appropriate error.
|
|
const storage = &terminal.screens.active.kitty_images;
|
|
var loading = LoadingImage.init(io, alloc, cmd, storage.image_limits) catch |err| {
|
|
encodeError(&result, err);
|
|
return result;
|
|
};
|
|
loading.deinit(alloc);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Transmit image data.
|
|
///
|
|
/// This loads the image, validates it, and puts it into the terminal
|
|
/// screen storage. It does not display the image.
|
|
fn transmit(
|
|
io: std.Io,
|
|
alloc: Allocator,
|
|
terminal: *Terminal,
|
|
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,
|
|
};
|
|
if (t.image_id > 0 and t.image_number > 0) {
|
|
return .{ .message = "EINVAL: image ID and number are mutually exclusive" };
|
|
}
|
|
|
|
const load = loadAndAddImage(io, alloc, terminal, cmd) catch |err| {
|
|
encodeError(&result, err);
|
|
return result;
|
|
};
|
|
errdefer load.image.deinit(alloc);
|
|
|
|
// If we're also displaying, then do that now. This function does
|
|
// both transmit and transmit and display. The display might also be
|
|
// deferred if it is multi-chunk.
|
|
if (load.display) |d| {
|
|
assert(!load.more);
|
|
var d_copy = d;
|
|
d_copy.image_id = load.image.id;
|
|
result = display(io, alloc, terminal, &.{
|
|
.control = .{ .display = d_copy },
|
|
.quiet = cmd.quiet,
|
|
});
|
|
}
|
|
|
|
// If there are more chunks expected we do not respond.
|
|
if (load.more) return .{};
|
|
|
|
// If the loaded image was assigned its ID automatically, not based
|
|
// on a number or explicitly specified ID, then we don't respond.
|
|
if (load.image.implicit_id) return .{};
|
|
|
|
// After the image is added, set the ID in case it changed.
|
|
// The resulting image number and placement ID never change.
|
|
result.id = load.image.id;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Display a previously transmitted image.
|
|
fn display(
|
|
io: std.Io,
|
|
alloc: Allocator,
|
|
terminal: *Terminal,
|
|
cmd: *const Command,
|
|
) Response {
|
|
const d = cmd.display().?;
|
|
|
|
// Display requires image ID or number.
|
|
if (d.image_id == 0 and d.image_number == 0) {
|
|
return .{ .message = "EINVAL: image ID or number required" };
|
|
}
|
|
|
|
// Build up our response
|
|
var result: Response = .{
|
|
.id = d.image_id,
|
|
.image_number = d.image_number,
|
|
.placement_id = d.placement_id,
|
|
};
|
|
|
|
// Verify the requested image exists if we have an ID
|
|
const storage = &terminal.screens.active.kitty_images;
|
|
const img_: ?Image = if (d.image_id != 0)
|
|
storage.imageById(d.image_id)
|
|
else
|
|
storage.imageByNumber(d.image_number);
|
|
const img = img_ orelse {
|
|
result.message = "ENOENT: image not found";
|
|
return result;
|
|
};
|
|
|
|
// Make sure our response has the image id in case we looked up by number
|
|
result.id = img.id;
|
|
|
|
// Location where the placement will go.
|
|
const location: ImageStorage.Placement.Location = location: {
|
|
// Virtual placements are not tracked
|
|
if (d.virtual_placement) {
|
|
if (d.parent_id > 0) {
|
|
result.message = "EINVAL: virtual placement cannot refer to a parent";
|
|
return result;
|
|
}
|
|
|
|
break :location .{ .virtual = {} };
|
|
}
|
|
|
|
// Track a new pin for our cursor. The cursor is always tracked but we
|
|
// don't want this one to move with the cursor.
|
|
const pin = terminal.screens.active.pages.trackPin(
|
|
terminal.screens.active.cursor.page_pin.*,
|
|
) catch |err| {
|
|
log.warn("failed to create pin for Kitty graphics err={}", .{err});
|
|
result.message = "EINVAL: failed to prepare terminal state";
|
|
return result;
|
|
};
|
|
break :location .{ .pin = pin };
|
|
};
|
|
|
|
// Add the placement
|
|
const p: ImageStorage.Placement = .{
|
|
.location = location,
|
|
.x_offset = d.x_offset,
|
|
.y_offset = d.y_offset,
|
|
.source_x = d.x,
|
|
.source_y = d.y,
|
|
.source_width = d.width,
|
|
.source_height = d.height,
|
|
.columns = d.columns,
|
|
.rows = d.rows,
|
|
.z = d.z,
|
|
};
|
|
storage.addPlacement(
|
|
io,
|
|
alloc,
|
|
img.id,
|
|
result.placement_id,
|
|
p,
|
|
) catch |err| {
|
|
p.deinit(terminal.screens.active);
|
|
encodeError(&result, err);
|
|
return result;
|
|
};
|
|
|
|
// Apply cursor movement setting. This only applies to pin placements.
|
|
switch (p.location) {
|
|
.virtual => {},
|
|
.pin => |pin| switch (d.cursor_movement) {
|
|
.none => {},
|
|
.after => {
|
|
// We use terminal.index to properly handle scroll regions.
|
|
const size = p.gridSize(img, terminal);
|
|
for (0..size.rows) |_| terminal.index() catch |err| {
|
|
log.warn("failed to move cursor: {}", .{err});
|
|
break;
|
|
};
|
|
|
|
terminal.setCursorPos(
|
|
terminal.screens.active.cursor.y,
|
|
pin.x + size.cols + 1,
|
|
);
|
|
},
|
|
},
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Display a previously transmitted image.
|
|
fn delete(
|
|
io: std.Io,
|
|
alloc: Allocator,
|
|
terminal: *Terminal,
|
|
cmd: *const Command,
|
|
) Response {
|
|
const storage = &terminal.screens.active.kitty_images;
|
|
storage.delete(io, alloc, terminal, cmd.control.delete);
|
|
|
|
// Delete never responds on success
|
|
return .{};
|
|
}
|
|
|
|
fn loadAndAddImage(
|
|
io: std.Io,
|
|
alloc: Allocator,
|
|
terminal: *Terminal,
|
|
cmd: *const Command,
|
|
) !struct {
|
|
image: Image,
|
|
more: bool = false,
|
|
display: ?command.Display = null,
|
|
} {
|
|
const t = cmd.transmission().?;
|
|
const storage = &terminal.screens.active.kitty_images;
|
|
|
|
// Determine our image. This also handles chunking and early exit.
|
|
var loading: LoadingImage = if (storage.loading) |loading| loading: {
|
|
// Note: we do NOT want to call "cmd.toOwnedData" here because
|
|
// we're _copying_ the data. We want the command data to be freed.
|
|
try loading.addData(alloc, cmd.data);
|
|
|
|
// If we have more then we're done
|
|
if (t.more_chunks) return .{ .image = loading.image, .more = true };
|
|
|
|
// We have no more chunks. We're going to be completing the
|
|
// image so we want to destroy the pointer to the loading
|
|
// image and copy it out.
|
|
defer {
|
|
alloc.destroy(loading);
|
|
storage.loading = null;
|
|
}
|
|
|
|
break :loading loading.*;
|
|
} else try .init(io, alloc, cmd, storage.image_limits);
|
|
|
|
// We only want to deinit on error. If we're chunking, then we don't
|
|
// want to deinit at all. If we're not chunking, then we'll deinit
|
|
// after we've copied the image out.
|
|
errdefer loading.deinit(alloc);
|
|
|
|
// If the image has no ID, we assign one
|
|
if (loading.image.id == 0) {
|
|
loading.image.id = storage.next_image_id;
|
|
storage.next_image_id +%= 1;
|
|
|
|
// If the image also has no number then its auto-ID is "implicit".
|
|
// See the doc comment on the Image.implicit_id field for more detail.
|
|
if (loading.image.number == 0) loading.image.implicit_id = true;
|
|
}
|
|
|
|
// If this is chunked, this is the beginning of a new chunked transmission.
|
|
// (We checked for an in-progress chunk above.)
|
|
if (t.more_chunks) {
|
|
// We allocate the pointer on the heap because its rare and we
|
|
// don't want to always pay the memory cost to keep it around.
|
|
const loading_ptr = try alloc.create(LoadingImage);
|
|
errdefer alloc.destroy(loading_ptr);
|
|
loading_ptr.* = loading;
|
|
storage.loading = loading_ptr;
|
|
return .{ .image = loading.image, .more = true };
|
|
}
|
|
|
|
// Dump the image data before it is decompressed
|
|
// loading.debugDump() catch unreachable;
|
|
|
|
// Validate and store our image
|
|
var img = try loading.complete(alloc);
|
|
errdefer img.deinit(alloc);
|
|
try storage.addImage(io, alloc, img);
|
|
|
|
// Get our display settings
|
|
const display_ = loading.display;
|
|
|
|
// Ensure we deinit the loading state because we're done. The image
|
|
// won't be deinit because of "complete" above.
|
|
loading.deinit(alloc);
|
|
|
|
return .{ .image = img, .display = display_ };
|
|
}
|
|
|
|
const EncodeableError = Image.Error || Allocator.Error;
|
|
|
|
/// Encode an error code into a message for a response.
|
|
fn encodeError(r: *Response, err: EncodeableError) void {
|
|
switch (err) {
|
|
error.OutOfMemory => r.message = "ENOMEM: out of memory",
|
|
error.InvalidData => r.message = "EINVAL: invalid data",
|
|
error.DecompressionFailed => r.message = "EINVAL: decompression failed",
|
|
error.FilePathTooLong => r.message = "EINVAL: file path too long",
|
|
error.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir",
|
|
error.TemporaryFileNotNamedCorrectly => r.message = "EINVAL: temporary file not named correctly",
|
|
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
|
|
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
|
|
error.UnsupportedDepth => r.message = "EINVAL: unsupported pixel depth",
|
|
error.DimensionsRequired => r.message = "EINVAL: dimensions required",
|
|
error.DimensionsTooLarge => r.message = "EINVAL: dimensions too large",
|
|
}
|
|
}
|
|
|
|
test "kittygfx more chunks with q=1" {
|
|
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);
|
|
|
|
// Initial chunk has q=1
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=T,f=24,t=d,i=1,s=1,v=2,c=10,r=1,m=1,q=1;////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
|
|
// Subsequent chunk has no q but should respect initial
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"m=0;////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
}
|
|
|
|
test "kittygfx more chunks with q=0" {
|
|
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);
|
|
|
|
// Initial chunk has q=0
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=t,f=24,t=d,s=1,v=2,c=10,r=1,m=1,i=1,q=0;////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
|
|
// Subsequent chunk has no q so should respond OK
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"m=0;////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(resp.ok());
|
|
}
|
|
}
|
|
|
|
test "kittygfx more chunks with chunk increasing q" {
|
|
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);
|
|
|
|
// Initial chunk has q=0
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=t,f=24,t=d,s=1,v=2,c=10,r=1,m=1,i=1,q=0;////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
|
|
// Subsequent chunk sets q=1 so should not respond
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"m=0,q=1;////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
}
|
|
|
|
test "kittygfx default format is rgba" {
|
|
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,t=d,i=1,s=1,v=2,c=10,r=1;///////////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(resp.ok());
|
|
|
|
const storage = &t.screens.active.kitty_images;
|
|
const img = storage.imageById(1).?;
|
|
try testing.expectEqual(command.Transmission.Format.rgba, img.format);
|
|
}
|
|
|
|
test "kittygfx test valid u32 (expect invalid image ID)" {
|
|
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=p,i=4294967295",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(!resp.ok());
|
|
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
|
}
|
|
|
|
test "kittygfx test valid i32 (expect invalid image ID)" {
|
|
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=p,i=1,z=-2147483648",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(!resp.ok());
|
|
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
|
}
|
|
|
|
test "kittygfx no response with no image ID or number" {
|
|
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,t=d,s=1,v=2,c=10,r=1,i=0,I=0;////////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
}
|
|
|
|
test "kittygfx no response with no image ID or number load and display" {
|
|
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,t=d,s=1,v=2,c=10,r=1,i=0,I=0;////////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
}
|
|
|
|
test "kittygfx retransmit same id gets fresh image generation" {
|
|
const testing = std.testing;
|
|
const io = testing.io;
|
|
const alloc = testing.allocator;
|
|
|
|
var t = try Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
|
|
defer t.deinit(alloc);
|
|
const storage = &t.screens.active.kitty_images;
|
|
|
|
// Transmit a 1x2 RGB image with id=1.
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=t,t=d,f=24,i=1,s=1,v=2;////////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(resp.ok());
|
|
}
|
|
const gen1 = storage.imageById(1).?.generation;
|
|
try testing.expect(gen1 > 0);
|
|
try testing.expectEqual(gen1, storage.generation);
|
|
|
|
// Retransmit the same id with identical dimensions/length. The
|
|
// (width, height, format, len) tuple is identical, so only the
|
|
// generation can reveal that the contents were replaced.
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=t,t=d,f=24,i=1,s=1,v=2;AAAAAAAA",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(resp.ok());
|
|
}
|
|
const gen2 = storage.imageById(1).?.generation;
|
|
try testing.expect(gen2 > gen1);
|
|
try testing.expectEqual(gen2, storage.generation);
|
|
}
|
|
|
|
test "kittygfx delete then retransmit same id gets fresh generation" {
|
|
const testing = std.testing;
|
|
const io = testing.io;
|
|
const alloc = testing.allocator;
|
|
|
|
var t = try Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
|
|
defer t.deinit(alloc);
|
|
const storage = &t.screens.active.kitty_images;
|
|
|
|
// Transmit and display, then delete everything (including image
|
|
// data), then retransmit the same ID.
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=T,t=d,f=24,i=1,s=1,v=2,c=1,r=1;////////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(resp.ok());
|
|
}
|
|
const gen1 = storage.imageById(1).?.generation;
|
|
|
|
{
|
|
const cmd = try command.Parser.parseString(alloc, "a=d,d=A");
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd);
|
|
try testing.expect(resp == null);
|
|
}
|
|
try testing.expect(storage.imageById(1) == null);
|
|
const gen_delete = storage.generation;
|
|
try testing.expect(gen_delete > gen1);
|
|
|
|
{
|
|
const cmd = try command.Parser.parseString(
|
|
alloc,
|
|
"a=t,t=d,f=24,i=1,s=1,v=2;////////",
|
|
);
|
|
defer cmd.deinit(alloc);
|
|
const resp = execute(io, alloc, &t, &cmd).?;
|
|
try testing.expect(resp.ok());
|
|
}
|
|
const gen2 = storage.imageById(1).?.generation;
|
|
try testing.expect(gen2 > gen1);
|
|
try testing.expect(gen2 > gen_delete);
|
|
}
|