kitty images: add delete by range operations

Fixes #5937

Implement [deleting Kitty image ranges](https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images).
This commit is contained in:
Jeffrey C. Ollie
2025-02-23 12:57:24 -06:00
parent eaeb6a620f
commit 995959dce4
2 changed files with 218 additions and 0 deletions

View File

@ -801,6 +801,13 @@ pub const Delete = union(enum) {
z: i32 = 0, // z
},
// r/R
range: struct {
delete: bool = false, // uppercase
first: u32 = 0, // x
last: u32 = 0, // y
},
// x/X
column: struct {
delete: bool = false, // uppercase
@ -885,6 +892,19 @@ pub const Delete = union(enum) {
break :blk result;
},
'r', 'R' => blk: {
const x = kv.get('x') orelse return error.InvalidFormat;
const y = kv.get('y') orelse return error.InvalidFormat;
if (x > y) return error.InvalidFormat;
break :blk .{
.range = .{
.delete = what == 'R',
.first = x,
.last = y,
},
};
},
'x', 'X' => blk: {
var result: Delete = .{ .column = .{ .delete = what == 'X' } };
if (kv.get('x')) |v| {
@ -1197,3 +1217,76 @@ test "response: encode with image ID and number" {
try r.encode(fbs.writer());
try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", fbs.getWritten());
}
test "delete range command 1" {
const testing = std.testing;
const alloc = testing.allocator;
var p = Parser.init(alloc);
defer p.deinit();
const input = "a=d,d=r,x=3,y=4";
for (input) |c| try p.feed(c);
const command = try p.complete();
defer command.deinit(alloc);
try testing.expect(command.control == .delete);
const v = command.control.delete;
try testing.expect(v == .range);
const range = v.range;
try testing.expect(!range.delete);
try testing.expectEqual(@as(u32, 3), range.first);
try testing.expectEqual(@as(u32, 4), range.last);
}
test "delete range command 2" {
const testing = std.testing;
const alloc = testing.allocator;
var p = Parser.init(alloc);
defer p.deinit();
const input = "a=d,d=R,x=5,y=11";
for (input) |c| try p.feed(c);
const command = try p.complete();
defer command.deinit(alloc);
try testing.expect(command.control == .delete);
const v = command.control.delete;
try testing.expect(v == .range);
const range = v.range;
try testing.expect(range.delete);
try testing.expectEqual(@as(u32, 5), range.first);
try testing.expectEqual(@as(u32, 11), range.last);
}
test "delete range command 3" {
const testing = std.testing;
const alloc = testing.allocator;
var p = Parser.init(alloc);
defer p.deinit();
const input = "a=d,d=R,x=5,y=4";
for (input) |c| try p.feed(c);
try testing.expectError(error.InvalidFormat, p.complete());
}
test "delete range command 4" {
const testing = std.testing;
const alloc = testing.allocator;
var p = Parser.init(alloc);
defer p.deinit();
const input = "a=d,d=R,x=5";
for (input) |c| try p.feed(c);
try testing.expectError(error.InvalidFormat, p.complete());
}
test "delete range command 5" {
const testing = std.testing;
const alloc = testing.allocator;
var p = Parser.init(alloc);
defer p.deinit();
const input = "a=d,d=R,y=5";
for (input) |c| try p.feed(c);
try testing.expectError(error.InvalidFormat, p.complete());
}

View File

@ -397,6 +397,31 @@ pub const ImageStorage = struct {
self.dirty = true;
},
.range => |v| range: {
if (v.first <= 0 or v.last <= 0) {
log.warn("delete range values must be greater than zero", .{});
break :range;
}
if (v.first > v.last) {
log.warn("delete range 'x' ({}) must be less than or equal to 'y' ({})", .{ v.first, v.last });
break :range;
}
var it = self.placements.iterator();
while (it.next()) |entry| {
if (entry.key_ptr.image_id >= v.first or entry.key_ptr.image_id <= v.last) {
const image_id = entry.key_ptr.image_id;
log.warn("delete range: {}", .{image_id});
entry.value_ptr.deinit(&t.screen);
self.placements.removeByPtr(entry.key_ptr);
if (v.delete) self.deleteIfUnused(alloc, image_id);
}
}
// Mark dirty to force redraw
self.dirty = true;
},
// We don't support animation frames yet so they are successfully
// deleted!
.animation_frames => {},
@ -1111,3 +1136,103 @@ test "storage: delete by row 1x1" {
.placement_id = .{ .tag = .external, .id = 3 },
}) != null);
}
test "storage: delete images by range 1" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(@as(usize, 2), s.placements.count());
s.dirty = false;
s.delete(alloc, &t, .{ .range = .{ .delete = false, .first = 1, .last = 2 } });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete images by range 2" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(@as(usize, 2), s.placements.count());
s.dirty = false;
s.delete(alloc, &t, .{ .range = .{ .delete = true, .first = 1, .last = 2 } });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.images.count());
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete images by range 3" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(@as(usize, 2), s.placements.count());
s.dirty = false;
s.delete(alloc, &t, .{ .range = .{ .delete = false, .first = 1, .last = 1 } });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete images by range 4" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(@as(usize, 2), s.placements.count());
s.dirty = false;
s.delete(alloc, &t, .{ .range = .{ .delete = true, .first = 1, .last = 1 } });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.images.count());
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}