From 995959dce44b1d75cccb9fe5202902e8a67ff3c4 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 23 Feb 2025 12:57:24 -0600 Subject: [PATCH] kitty images: add delete by range operations Fixes #5937 Implement [deleting Kitty image ranges](https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images). --- src/terminal/kitty/graphics_command.zig | 93 ++++++++++++++++++ src/terminal/kitty/graphics_storage.zig | 125 ++++++++++++++++++++++++ 2 files changed, 218 insertions(+) diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index d199711d3..840949d74 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -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()); +} diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index ffd3aa580..474486e5b 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -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()); +}