mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
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:
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
Reference in New Issue
Block a user