diff --git a/src/terminal2/PageList.zig b/src/terminal2/PageList.zig index 4b78b1514..02bcd82da 100644 --- a/src/terminal2/PageList.zig +++ b/src/terminal2/PageList.zig @@ -1405,6 +1405,10 @@ pub fn untrackPin(self: *PageList, p: *Pin) void { } } +pub fn countTrackedPins(self: *const PageList) usize { + return self.tracked_pins.count(); +} + /// Returns the viewport for the given pin, prefering to pin to /// "active" if the pin is within the active area. fn pinIsActive(self: *const PageList, p: Pin) bool { diff --git a/src/terminal2/kitty.zig b/src/terminal2/kitty.zig index 6b86a3280..e2341a3dc 100644 --- a/src/terminal2/kitty.zig +++ b/src/terminal2/kitty.zig @@ -6,4 +6,7 @@ pub usingnamespace @import("../terminal/kitty/key.zig"); test { @import("std").testing.refAllDecls(@This()); + + _ = @import("kitty/graphics.zig"); + _ = @import("kitty/key.zig"); } diff --git a/src/terminal2/kitty/graphics.zig b/src/terminal2/kitty/graphics.zig new file mode 100644 index 000000000..cfc45adbc --- /dev/null +++ b/src/terminal2/kitty/graphics.zig @@ -0,0 +1,22 @@ +//! Kitty graphics protocol support. +//! +//! Documentation: +//! https://sw.kovidgoyal.net/kitty/graphics-protocol +//! +//! Unimplemented features that are still todo: +//! - shared memory transmit +//! - virtual placement w/ unicode +//! - animation +//! +//! Performance: +//! The performance of this particular subsystem of Ghostty is not great. +//! We can avoid a lot more allocations, we can replace some C code (which +//! implicitly allocates) with native Zig, we can improve the data structures +//! to avoid repeated lookups, etc. I tried to avoid pessimization but my +//! aim to ship a v1 of this implementation came at some cost. I learned a lot +//! though and I think we can go back through and fix this up. + +pub usingnamespace @import("graphics_command.zig"); +pub usingnamespace @import("graphics_exec.zig"); +pub usingnamespace @import("graphics_image.zig"); +pub usingnamespace @import("graphics_storage.zig"); diff --git a/src/terminal2/kitty/graphics_command.zig b/src/terminal2/kitty/graphics_command.zig new file mode 100644 index 000000000..ca7a4d674 --- /dev/null +++ b/src/terminal2/kitty/graphics_command.zig @@ -0,0 +1,984 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +/// The key-value pairs for the control information for a command. The +/// keys are always single characters and the values are either single +/// characters or 32-bit unsigned integers. +/// +/// For the value of this: if the value is a single printable ASCII character +/// it is the ASCII code. Otherwise, it is parsed as a 32-bit unsigned integer. +const KV = std.AutoHashMapUnmanaged(u8, u32); + +/// Command parser parses the Kitty graphics protocol escape sequence. +pub const CommandParser = struct { + /// The memory used by the parser is stored in an arena because it is + /// all freed at the end of the command. + arena: ArenaAllocator, + + /// This is the list of KV pairs that we're building up. + kv: KV = .{}, + + /// This is used as a buffer to store the key/value of a KV pair. + /// The value of a KV pair is at most a 32-bit integer which at most + /// is 10 characters (4294967295). + kv_temp: [10]u8 = undefined, + kv_temp_len: u4 = 0, + kv_current: u8 = 0, // Current kv key + + /// This is the list of bytes that contains both KV data and final + /// data. You shouldn't access this directly. + data: std.ArrayList(u8), + + /// Internal state for parsing. + state: State = .control_key, + + const State = enum { + /// Parsing k/v pairs. The "ignore" variants are in that state + /// but ignore any data because we know they're invalid. + control_key, + control_key_ignore, + control_value, + control_value_ignore, + + /// We're parsing the data blob. + data, + }; + + /// Initialize the parser. The allocator given will be used for both + /// temporary data and long-lived values such as the final image blob. + pub fn init(alloc: Allocator) CommandParser { + var arena = ArenaAllocator.init(alloc); + errdefer arena.deinit(); + return .{ + .arena = arena, + .data = std.ArrayList(u8).init(alloc), + }; + } + + pub fn deinit(self: *CommandParser) void { + // We don't free the hash map because its in the arena + self.arena.deinit(); + self.data.deinit(); + } + + /// Feed a single byte to the parser. + /// + /// The first byte to start parsing should be the byte immediately following + /// the "G" in the APC sequence, i.e. "\x1b_G123" the first byte should + /// be "1". + pub fn feed(self: *CommandParser, c: u8) !void { + switch (self.state) { + .control_key => switch (c) { + // '=' means the key is complete and we're moving to the value. + '=' => if (self.kv_temp_len != 1) { + // All control keys are a single character right now so + // if we're not a single character just ignore follow-up + // data. + self.state = .control_value_ignore; + self.kv_temp_len = 0; + } else { + self.kv_current = self.kv_temp[0]; + self.kv_temp_len = 0; + self.state = .control_value; + }, + + else => try self.accumulateValue(c, .control_key_ignore), + }, + + .control_key_ignore => switch (c) { + '=' => self.state = .control_value_ignore, + else => {}, + }, + + .control_value => switch (c) { + ',' => try self.finishValue(.control_key), // move to next key + ';' => try self.finishValue(.data), // move to data + else => try self.accumulateValue(c, .control_value_ignore), + }, + + .control_value_ignore => switch (c) { + ',' => self.state = .control_key_ignore, + ';' => self.state = .data, + else => {}, + }, + + .data => try self.data.append(c), + } + + // We always add to our data list because this is our stable + // array of bytes that we'll reference everywhere else. + } + + /// Complete the parsing. This must be called after all the + /// bytes have been fed to the parser. + /// + /// The allocator given will be used for the long-lived data + /// of the final command. + pub fn complete(self: *CommandParser) !Command { + switch (self.state) { + // We can't ever end in the control key state and be valid. + // This means the command looked something like "a=1,b" + .control_key, .control_key_ignore => return error.InvalidFormat, + + // Some commands (i.e. placements) end without extra data so + // we end in the value state. i.e. "a=1,b=2" + .control_value => try self.finishValue(.data), + .control_value_ignore => {}, + + // Most commands end in data, i.e. "a=1,b=2;1234" + .data => {}, + } + + // Determine our action, which is always a single character. + const action: u8 = action: { + const value = self.kv.get('a') orelse break :action 't'; + const c = std.math.cast(u8, value) orelse return error.InvalidFormat; + break :action c; + }; + const control: Command.Control = switch (action) { + 'q' => .{ .query = try Transmission.parse(self.kv) }, + 't' => .{ .transmit = try Transmission.parse(self.kv) }, + 'T' => .{ .transmit_and_display = .{ + .transmission = try Transmission.parse(self.kv), + .display = try Display.parse(self.kv), + } }, + 'p' => .{ .display = try Display.parse(self.kv) }, + 'd' => .{ .delete = try Delete.parse(self.kv) }, + 'f' => .{ .transmit_animation_frame = try AnimationFrameLoading.parse(self.kv) }, + 'a' => .{ .control_animation = try AnimationControl.parse(self.kv) }, + 'c' => .{ .compose_animation = try AnimationFrameComposition.parse(self.kv) }, + else => return error.InvalidFormat, + }; + + // Determine our quiet value + const quiet: Command.Quiet = if (self.kv.get('q')) |v| quiet: { + break :quiet switch (v) { + 0 => .no, + 1 => .ok, + 2 => .failures, + else => return error.InvalidFormat, + }; + } else .no; + + return .{ + .control = control, + .quiet = quiet, + .data = if (self.data.items.len == 0) "" else data: { + break :data try self.data.toOwnedSlice(); + }, + }; + } + + fn accumulateValue(self: *CommandParser, c: u8, overflow_state: State) !void { + const idx = self.kv_temp_len; + self.kv_temp_len += 1; + if (self.kv_temp_len > self.kv_temp.len) { + self.state = overflow_state; + self.kv_temp_len = 0; + return; + } + self.kv_temp[idx] = c; + } + + fn finishValue(self: *CommandParser, next_state: State) !void { + const alloc = self.arena.allocator(); + + // We can move states right away, we don't use it. + self.state = next_state; + + // Check for ASCII chars first + if (self.kv_temp_len == 1) { + const c = self.kv_temp[0]; + if (c < '0' or c > '9') { + try self.kv.put(alloc, self.kv_current, @intCast(c)); + self.kv_temp_len = 0; + return; + } + } + + // Only "z" is currently signed. This is a bit of a kloodge; if more + // fields become signed we can rethink this but for now we parse + // "z" as i32 then bitcast it to u32 then bitcast it back later. + if (self.kv_current == 'z') { + const v = try std.fmt.parseInt(i32, self.kv_temp[0..self.kv_temp_len], 10); + try self.kv.put(alloc, self.kv_current, @bitCast(v)); + } else { + const v = try std.fmt.parseInt(u32, self.kv_temp[0..self.kv_temp_len], 10); + try self.kv.put(alloc, self.kv_current, v); + } + + // Clear our temp buffer + self.kv_temp_len = 0; + } +}; + +/// Represents a possible response to a command. +pub const Response = struct { + id: u32 = 0, + image_number: u32 = 0, + placement_id: u32 = 0, + message: []const u8 = "OK", + + pub fn encode(self: Response, writer: anytype) !void { + // We only encode a result if we have either an id or an image number. + if (self.id == 0 and self.image_number == 0) return; + + try writer.writeAll("\x1b_G"); + if (self.id > 0) { + try writer.print("i={}", .{self.id}); + } + if (self.image_number > 0) { + if (self.id > 0) try writer.writeByte(','); + try writer.print("I={}", .{self.image_number}); + } + if (self.placement_id > 0) { + try writer.print(",p={}", .{self.placement_id}); + } + try writer.writeByte(';'); + try writer.writeAll(self.message); + try writer.writeAll("\x1b\\"); + } + + /// Returns true if this response is not an error. + pub fn ok(self: Response) bool { + return std.mem.eql(u8, self.message, "OK"); + } +}; + +pub const Command = struct { + control: Control, + quiet: Quiet = .no, + data: []const u8 = "", + + pub const Action = enum { + query, // q + transmit, // t + transmit_and_display, // T + display, // p + delete, // d + transmit_animation_frame, // f + control_animation, // a + compose_animation, // c + }; + + pub const Quiet = enum { + no, // 0 + ok, // 1 + failures, // 2 + }; + + pub const Control = union(Action) { + query: Transmission, + transmit: Transmission, + transmit_and_display: struct { + transmission: Transmission, + display: Display, + }, + display: Display, + delete: Delete, + transmit_animation_frame: AnimationFrameLoading, + control_animation: AnimationControl, + compose_animation: AnimationFrameComposition, + }; + + /// Take ownership over the data in this command. If the returned value + /// has a length of zero, then the data was empty and need not be freed. + pub fn toOwnedData(self: *Command) []const u8 { + const result = self.data; + self.data = ""; + return result; + } + + /// Returns the transmission data if it has any. + pub fn transmission(self: Command) ?Transmission { + return switch (self.control) { + .query => |t| t, + .transmit => |t| t, + .transmit_and_display => |t| t.transmission, + else => null, + }; + } + + /// Returns the display data if it has any. + pub fn display(self: Command) ?Display { + return switch (self.control) { + .display => |d| d, + .transmit_and_display => |t| t.display, + else => null, + }; + } + + pub fn deinit(self: Command, alloc: Allocator) void { + if (self.data.len > 0) alloc.free(self.data); + } +}; + +pub const Transmission = struct { + format: Format = .rgb, // f + medium: Medium = .direct, // t + width: u32 = 0, // s + height: u32 = 0, // v + size: u32 = 0, // S + offset: u32 = 0, // O + image_id: u32 = 0, // i + image_number: u32 = 0, // I + placement_id: u32 = 0, // p + compression: Compression = .none, // o + more_chunks: bool = false, // m + + pub const Format = enum { + rgb, // 24 + rgba, // 32 + png, // 100 + + // The following are not supported directly via the protocol + // but they are formats that a png may decode to that we + // support. + grey_alpha, + }; + + pub const Medium = enum { + direct, // d + file, // f + temporary_file, // t + shared_memory, // s + }; + + pub const Compression = enum { + none, + zlib_deflate, // z + }; + + fn parse(kv: KV) !Transmission { + var result: Transmission = .{}; + if (kv.get('f')) |v| { + result.format = switch (v) { + 24 => .rgb, + 32 => .rgba, + 100 => .png, + else => return error.InvalidFormat, + }; + } + + if (kv.get('t')) |v| { + const c = std.math.cast(u8, v) orelse return error.InvalidFormat; + result.medium = switch (c) { + 'd' => .direct, + 'f' => .file, + 't' => .temporary_file, + 's' => .shared_memory, + else => return error.InvalidFormat, + }; + } + + if (kv.get('s')) |v| { + result.width = v; + } + + if (kv.get('v')) |v| { + result.height = v; + } + + if (kv.get('S')) |v| { + result.size = v; + } + + if (kv.get('O')) |v| { + result.offset = v; + } + + if (kv.get('i')) |v| { + result.image_id = v; + } + + if (kv.get('I')) |v| { + result.image_number = v; + } + + if (kv.get('p')) |v| { + result.placement_id = v; + } + + if (kv.get('o')) |v| { + const c = std.math.cast(u8, v) orelse return error.InvalidFormat; + result.compression = switch (c) { + 'z' => .zlib_deflate, + else => return error.InvalidFormat, + }; + } + + if (kv.get('m')) |v| { + result.more_chunks = v > 0; + } + + return result; + } +}; + +pub const Display = struct { + image_id: u32 = 0, // i + image_number: u32 = 0, // I + placement_id: u32 = 0, // p + x: u32 = 0, // x + y: u32 = 0, // y + width: u32 = 0, // w + height: u32 = 0, // h + x_offset: u32 = 0, // X + y_offset: u32 = 0, // Y + columns: u32 = 0, // c + rows: u32 = 0, // r + cursor_movement: CursorMovement = .after, // C + virtual_placement: bool = false, // U + z: i32 = 0, // z + + pub const CursorMovement = enum { + after, // 0 + none, // 1 + }; + + fn parse(kv: KV) !Display { + var result: Display = .{}; + + if (kv.get('i')) |v| { + result.image_id = v; + } + + if (kv.get('I')) |v| { + result.image_number = v; + } + + if (kv.get('p')) |v| { + result.placement_id = v; + } + + if (kv.get('x')) |v| { + result.x = v; + } + + if (kv.get('y')) |v| { + result.y = v; + } + + if (kv.get('w')) |v| { + result.width = v; + } + + if (kv.get('h')) |v| { + result.height = v; + } + + if (kv.get('X')) |v| { + result.x_offset = v; + } + + if (kv.get('Y')) |v| { + result.y_offset = v; + } + + if (kv.get('c')) |v| { + result.columns = v; + } + + if (kv.get('r')) |v| { + result.rows = v; + } + + if (kv.get('C')) |v| { + result.cursor_movement = switch (v) { + 0 => .after, + 1 => .none, + else => return error.InvalidFormat, + }; + } + + if (kv.get('U')) |v| { + result.virtual_placement = switch (v) { + 0 => false, + 1 => true, + else => return error.InvalidFormat, + }; + } + + if (kv.get('z')) |v| { + // We can bitcast here because of how we parse it earlier. + result.z = @bitCast(v); + } + + return result; + } +}; + +pub const AnimationFrameLoading = struct { + x: u32 = 0, // x + y: u32 = 0, // y + create_frame: u32 = 0, // c + edit_frame: u32 = 0, // r + gap_ms: u32 = 0, // z + composition_mode: CompositionMode = .alpha_blend, // X + background: Background = .{}, // Y + + pub const Background = packed struct(u32) { + r: u8 = 0, + g: u8 = 0, + b: u8 = 0, + a: u8 = 0, + }; + + fn parse(kv: KV) !AnimationFrameLoading { + var result: AnimationFrameLoading = .{}; + + if (kv.get('x')) |v| { + result.x = v; + } + + if (kv.get('y')) |v| { + result.y = v; + } + + if (kv.get('c')) |v| { + result.create_frame = v; + } + + if (kv.get('r')) |v| { + result.edit_frame = v; + } + + if (kv.get('z')) |v| { + result.gap_ms = v; + } + + if (kv.get('X')) |v| { + result.composition_mode = switch (v) { + 0 => .alpha_blend, + 1 => .overwrite, + else => return error.InvalidFormat, + }; + } + + if (kv.get('Y')) |v| { + result.background = @bitCast(v); + } + + return result; + } +}; + +pub const AnimationFrameComposition = struct { + frame: u32 = 0, // c + edit_frame: u32 = 0, // r + x: u32 = 0, // x + y: u32 = 0, // y + width: u32 = 0, // w + height: u32 = 0, // h + left_edge: u32 = 0, // X + top_edge: u32 = 0, // Y + composition_mode: CompositionMode = .alpha_blend, // C + + fn parse(kv: KV) !AnimationFrameComposition { + var result: AnimationFrameComposition = .{}; + + if (kv.get('c')) |v| { + result.frame = v; + } + + if (kv.get('r')) |v| { + result.edit_frame = v; + } + + if (kv.get('x')) |v| { + result.x = v; + } + + if (kv.get('y')) |v| { + result.y = v; + } + + if (kv.get('w')) |v| { + result.width = v; + } + + if (kv.get('h')) |v| { + result.height = v; + } + + if (kv.get('X')) |v| { + result.left_edge = v; + } + + if (kv.get('Y')) |v| { + result.top_edge = v; + } + + if (kv.get('C')) |v| { + result.composition_mode = switch (v) { + 0 => .alpha_blend, + 1 => .overwrite, + else => return error.InvalidFormat, + }; + } + + return result; + } +}; + +pub const AnimationControl = struct { + action: AnimationAction = .invalid, // s + frame: u32 = 0, // r + gap_ms: u32 = 0, // z + current_frame: u32 = 0, // c + loops: u32 = 0, // v + + pub const AnimationAction = enum { + invalid, // 0 + stop, // 1 + run_wait, // 2 + run, // 3 + }; + + fn parse(kv: KV) !AnimationControl { + var result: AnimationControl = .{}; + + if (kv.get('s')) |v| { + result.action = switch (v) { + 0 => .invalid, + 1 => .stop, + 2 => .run_wait, + 3 => .run, + else => return error.InvalidFormat, + }; + } + + if (kv.get('r')) |v| { + result.frame = v; + } + + if (kv.get('z')) |v| { + result.gap_ms = v; + } + + if (kv.get('c')) |v| { + result.current_frame = v; + } + + if (kv.get('v')) |v| { + result.loops = v; + } + + return result; + } +}; + +pub const Delete = union(enum) { + // a/A + all: bool, + + // i/I + id: struct { + delete: bool = false, // uppercase + image_id: u32 = 0, // i + placement_id: u32 = 0, // p + }, + + // n/N + newest: struct { + delete: bool = false, // uppercase + image_number: u32 = 0, // I + placement_id: u32 = 0, // p + }, + + // c/C, + intersect_cursor: bool, + + // f/F + animation_frames: bool, + + // p/P + intersect_cell: struct { + delete: bool = false, // uppercase + x: u32 = 0, // x + y: u32 = 0, // y + }, + + // q/Q + intersect_cell_z: struct { + delete: bool = false, // uppercase + x: u32 = 0, // x + y: u32 = 0, // y + z: i32 = 0, // z + }, + + // x/X + column: struct { + delete: bool = false, // uppercase + x: u32 = 0, // x + }, + + // y/Y + row: struct { + delete: bool = false, // uppercase + y: u32 = 0, // y + }, + + // z/Z + z: struct { + delete: bool = false, // uppercase + z: i32 = 0, // z + }, + + fn parse(kv: KV) !Delete { + const what: u8 = what: { + const value = kv.get('d') orelse break :what 'a'; + const c = std.math.cast(u8, value) orelse return error.InvalidFormat; + break :what c; + }; + + return switch (what) { + 'a', 'A' => .{ .all = what == 'A' }, + + 'i', 'I' => blk: { + var result: Delete = .{ .id = .{ .delete = what == 'I' } }; + if (kv.get('i')) |v| { + result.id.image_id = v; + } + if (kv.get('p')) |v| { + result.id.placement_id = v; + } + + break :blk result; + }, + + 'n', 'N' => blk: { + var result: Delete = .{ .newest = .{ .delete = what == 'N' } }; + if (kv.get('I')) |v| { + result.newest.image_number = v; + } + if (kv.get('p')) |v| { + result.newest.placement_id = v; + } + + break :blk result; + }, + + 'c', 'C' => .{ .intersect_cursor = what == 'C' }, + + 'f', 'F' => .{ .animation_frames = what == 'F' }, + + 'p', 'P' => blk: { + var result: Delete = .{ .intersect_cell = .{ .delete = what == 'P' } }; + if (kv.get('x')) |v| { + result.intersect_cell.x = v; + } + if (kv.get('y')) |v| { + result.intersect_cell.y = v; + } + + break :blk result; + }, + + 'q', 'Q' => blk: { + var result: Delete = .{ .intersect_cell_z = .{ .delete = what == 'Q' } }; + if (kv.get('x')) |v| { + result.intersect_cell_z.x = v; + } + if (kv.get('y')) |v| { + result.intersect_cell_z.y = v; + } + if (kv.get('z')) |v| { + // We can bitcast here because of how we parse it earlier. + result.intersect_cell_z.z = @bitCast(v); + } + + break :blk result; + }, + + 'x', 'X' => blk: { + var result: Delete = .{ .column = .{ .delete = what == 'X' } }; + if (kv.get('x')) |v| { + result.column.x = v; + } + + break :blk result; + }, + + 'y', 'Y' => blk: { + var result: Delete = .{ .row = .{ .delete = what == 'Y' } }; + if (kv.get('y')) |v| { + result.row.y = v; + } + + break :blk result; + }, + + 'z', 'Z' => blk: { + var result: Delete = .{ .z = .{ .delete = what == 'Z' } }; + if (kv.get('z')) |v| { + // We can bitcast here because of how we parse it earlier. + result.z.z = @bitCast(v); + } + + break :blk result; + }, + + else => return error.InvalidFormat, + }; + } +}; + +pub const CompositionMode = enum { + alpha_blend, // 0 + overwrite, // 1 +}; + +test "transmission command" { + const testing = std.testing; + const alloc = testing.allocator; + var p = CommandParser.init(alloc); + defer p.deinit(); + + const input = "f=24,s=10,v=20"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .transmit); + const v = command.control.transmit; + try testing.expectEqual(Transmission.Format.rgb, v.format); + try testing.expectEqual(@as(u32, 10), v.width); + try testing.expectEqual(@as(u32, 20), v.height); +} + +test "query command" { + const testing = std.testing; + const alloc = testing.allocator; + var p = CommandParser.init(alloc); + defer p.deinit(); + + const input = "i=31,s=1,v=1,a=q,t=d,f=24;AAAA"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .query); + const v = command.control.query; + try testing.expectEqual(Transmission.Medium.direct, v.medium); + try testing.expectEqual(@as(u32, 1), v.width); + try testing.expectEqual(@as(u32, 1), v.height); + try testing.expectEqual(@as(u32, 31), v.image_id); + try testing.expectEqualStrings("AAAA", command.data); +} + +test "display command" { + const testing = std.testing; + const alloc = testing.allocator; + var p = CommandParser.init(alloc); + defer p.deinit(); + + const input = "a=p,U=1,i=31,c=80,r=120"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .display); + const v = command.control.display; + try testing.expectEqual(@as(u32, 80), v.columns); + try testing.expectEqual(@as(u32, 120), v.rows); + try testing.expectEqual(@as(u32, 31), v.image_id); +} + +test "delete command" { + const testing = std.testing; + const alloc = testing.allocator; + var p = CommandParser.init(alloc); + defer p.deinit(); + + const input = "a=d,d=p,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 == .intersect_cell); + const dv = v.intersect_cell; + try testing.expect(!dv.delete); + try testing.expectEqual(@as(u32, 3), dv.x); + try testing.expectEqual(@as(u32, 4), dv.y); +} + +test "ignore unknown keys (long)" { + const testing = std.testing; + const alloc = testing.allocator; + var p = CommandParser.init(alloc); + defer p.deinit(); + + const input = "f=24,s=10,v=20,hello=world"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .transmit); + const v = command.control.transmit; + try testing.expectEqual(Transmission.Format.rgb, v.format); + try testing.expectEqual(@as(u32, 10), v.width); + try testing.expectEqual(@as(u32, 20), v.height); +} + +test "ignore very long values" { + const testing = std.testing; + const alloc = testing.allocator; + var p = CommandParser.init(alloc); + defer p.deinit(); + + const input = "f=24,s=10,v=2000000000000000000000000000000000000000"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .transmit); + const v = command.control.transmit; + try testing.expectEqual(Transmission.Format.rgb, v.format); + try testing.expectEqual(@as(u32, 10), v.width); + try testing.expectEqual(@as(u32, 0), v.height); +} + +test "response: encode nothing without ID or image number" { + const testing = std.testing; + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + + var r: Response = .{}; + try r.encode(fbs.writer()); + try testing.expectEqualStrings("", fbs.getWritten()); +} + +test "response: encode with only image id" { + const testing = std.testing; + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + + var r: Response = .{ .id = 4 }; + try r.encode(fbs.writer()); + try testing.expectEqualStrings("\x1b_Gi=4;OK\x1b\\", fbs.getWritten()); +} + +test "response: encode with only image number" { + const testing = std.testing; + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + + var r: Response = .{ .image_number = 4 }; + try r.encode(fbs.writer()); + try testing.expectEqualStrings("\x1b_GI=4;OK\x1b\\", fbs.getWritten()); +} + +test "response: encode with image ID and number" { + const testing = std.testing; + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + + var r: Response = .{ .id = 12, .image_number = 4 }; + try r.encode(fbs.writer()); + try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", fbs.getWritten()); +} diff --git a/src/terminal2/kitty/graphics_exec.zig b/src/terminal2/kitty/graphics_exec.zig new file mode 100644 index 000000000..b4047c1d5 --- /dev/null +++ b/src/terminal2/kitty/graphics_exec.zig @@ -0,0 +1,344 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const renderer = @import("../../renderer.zig"); +const point = @import("../point.zig"); +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( + alloc: Allocator, + terminal: *Terminal, + cmd: *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.screen.kitty_images.enabled()) { + log.debug("kitty graphics requested but disabled", .{}); + return null; + } + + log.debug("executing kitty graphics command: quiet={} control={}", .{ + cmd.quiet, + cmd.control, + }); + + const resp_: ?Response = switch (cmd.control) { + .query => query(alloc, cmd), + .transmit, .transmit_and_display => transmit(alloc, terminal, cmd), + .display => display(alloc, terminal, cmd), + .delete => delete(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 (cmd.quiet) { + .no => 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(alloc: Allocator, cmd: *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. + var loading = LoadingImage.init(alloc, cmd) 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( + alloc: Allocator, + terminal: *Terminal, + cmd: *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(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; + return display(alloc, terminal, &.{ + .control = .{ .display = d_copy }, + .quiet = cmd.quiet, + }); + } + + // If there are more chunks expected we do not respond. + if (load.more) return .{}; + + // After the image is added, set the ID in case it changed + result.id = load.image.id; + + // If the original request had an image number, then we respond. + // Otherwise, we don't respond. + if (load.image.number == 0) return .{}; + + return result; +} + +/// Display a previously transmitted image. +fn display( + 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.screen.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 = "EINVAL: image not found"; + return result; + }; + + // Make sure our response has the image id in case we looked up by number + result.id = img.id; + + // Determine the screen point for the placement. + const placement_point = (point.Viewport{ + .x = terminal.screen.cursor.x, + .y = terminal.screen.cursor.y, + }).toScreen(&terminal.screen); + + // Add the placement + const p: ImageStorage.Placement = .{ + .point = placement_point, + .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( + alloc, + img.id, + result.placement_id, + p, + ) catch |err| { + encodeError(&result, err); + return result; + }; + + // Cursor needs to move after placement + switch (d.cursor_movement) { + .none => {}, + .after => { + const rect = p.rect(img, terminal); + + // We can do better by doing this with pure internal screen state + // but this handles scroll regions. + const height = rect.bottom_right.y - rect.top_left.y; + for (0..height) |_| terminal.index() catch |err| { + log.warn("failed to move cursor: {}", .{err}); + break; + }; + + terminal.setCursorPos( + terminal.screen.cursor.y, + rect.bottom_right.x + 1, + ); + }, + } + + // Display does not result in a response on success + return .{}; +} + +/// Display a previously transmitted image. +fn delete( + alloc: Allocator, + terminal: *Terminal, + cmd: *Command, +) Response { + const storage = &terminal.screen.kitty_images; + storage.delete(alloc, terminal, cmd.control.delete); + + // Delete never responds on success + return .{}; +} + +fn loadAndAddImage( + alloc: Allocator, + terminal: *Terminal, + cmd: *Command, +) !struct { + image: Image, + more: bool = false, + display: ?command.Display = null, +} { + const t = cmd.transmission().?; + const storage = &terminal.screen.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 LoadingImage.init(alloc, cmd); + + // 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 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(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.InternalError => r.message = "EINVAL: internal error", + 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.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", + } +} diff --git a/src/terminal2/kitty/graphics_image.zig b/src/terminal2/kitty/graphics_image.zig new file mode 100644 index 000000000..d84ea91d6 --- /dev/null +++ b/src/terminal2/kitty/graphics_image.zig @@ -0,0 +1,776 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const command = @import("graphics_command.zig"); +const point = @import("../point.zig"); +const internal_os = @import("../../os/main.zig"); +const stb = @import("../../stb/main.zig"); + +const log = std.log.scoped(.kitty_gfx); + +/// Maximum width or height of an image. Taken directly from Kitty. +const max_dimension = 10000; + +/// Maximum size in bytes, taken from Kitty. +const max_size = 400 * 1024 * 1024; // 400MB + +/// An image that is still being loaded. The image should be initialized +/// using init on the first chunk and then addData for each subsequent +/// chunk. Once all chunks have been added, complete should be called +/// to finalize the image. +pub const LoadingImage = struct { + /// The in-progress image. The first chunk must have all the metadata + /// so this comes from that initially. + image: Image, + + /// The data that is being built up. + data: std.ArrayListUnmanaged(u8) = .{}, + + /// This is non-null when a transmit and display command is given + /// so that we display the image after it is fully loaded. + display: ?command.Display = null, + + /// Initialize a chunked immage from the first image transmission. + /// If this is a multi-chunk image, this should only be the FIRST + /// chunk. + pub fn init(alloc: Allocator, cmd: *command.Command) !LoadingImage { + // Build our initial image from the properties sent via the control. + // These can be overwritten by the data loading process. For example, + // PNG loading sets the width/height from the data. + const t = cmd.transmission().?; + var result: LoadingImage = .{ + .image = .{ + .id = t.image_id, + .number = t.image_number, + .width = t.width, + .height = t.height, + .compression = t.compression, + .format = t.format, + }, + + .display = cmd.display(), + }; + + // Special case for the direct medium, we just add it directly + // which will handle copying the data, base64 decoding, etc. + if (t.medium == .direct) { + try result.addData(alloc, cmd.data); + return result; + } + + // For every other medium, we'll need to at least base64 decode + // the data to make it useful so let's do that. Also, all the data + // has to be path data so we can put it in a stack-allocated buffer. + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const Base64Decoder = std.base64.standard.Decoder; + const size = Base64Decoder.calcSizeForSlice(cmd.data) catch |err| { + log.warn("failed to calculate base64 size for file path: {}", .{err}); + return error.InvalidData; + }; + if (size > buf.len) return error.FilePathTooLong; + Base64Decoder.decode(&buf, cmd.data) catch |err| { + log.warn("failed to decode base64 data: {}", .{err}); + return error.InvalidData; + }; + + if (comptime builtin.os.tag != .windows) { + if (std.mem.indexOfScalar(u8, buf[0..size], 0) != null) { + // std.os.realpath *asserts* that the path does not have + // internal nulls instead of erroring. + log.warn("failed to get absolute path: BadPathName", .{}); + return error.InvalidData; + } + } + + var abs_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = std.os.realpath(buf[0..size], &abs_buf) catch |err| { + log.warn("failed to get absolute path: {}", .{err}); + return error.InvalidData; + }; + + // Depending on the medium, load the data from the path. + switch (t.medium) { + .direct => unreachable, // handled above + .file => try result.readFile(.file, alloc, t, path), + .temporary_file => try result.readFile(.temporary_file, alloc, t, path), + .shared_memory => try result.readSharedMemory(alloc, t, path), + } + + return result; + } + + /// Reads the data from a shared memory segment. + fn readSharedMemory( + self: *LoadingImage, + alloc: Allocator, + t: command.Transmission, + path: []const u8, + ) !void { + // We require libc for this for shm_open + if (comptime !builtin.link_libc) return error.UnsupportedMedium; + + // Todo: support shared memory + _ = self; + _ = alloc; + _ = t; + _ = path; + return error.UnsupportedMedium; + } + + /// Reads the data from a temporary file and returns it. This allocates + /// and does not free any of the data, so the caller must free it. + /// + /// This will also delete the temporary file if it is in a safe location. + fn readFile( + self: *LoadingImage, + comptime medium: command.Transmission.Medium, + alloc: Allocator, + t: command.Transmission, + path: []const u8, + ) !void { + switch (medium) { + .file, .temporary_file => {}, + else => @compileError("readFile only supports file and temporary_file"), + } + + // Verify file seems "safe". This is logic copied directly from Kitty, + // mostly. This is really rough but it will catch obvious bad actors. + if (std.mem.startsWith(u8, path, "/proc/") or + std.mem.startsWith(u8, path, "/sys/") or + (std.mem.startsWith(u8, path, "/dev/") and + !std.mem.startsWith(u8, path, "/dev/shm/"))) + { + return error.InvalidData; + } + + // Temporary file logic + if (medium == .temporary_file) { + if (!isPathInTempDir(path)) return error.TemporaryFileNotInTempDir; + } + defer if (medium == .temporary_file) { + std.os.unlink(path) catch |err| { + log.warn("failed to delete temporary file: {}", .{err}); + }; + }; + + var file = std.fs.cwd().openFile(path, .{}) catch |err| { + log.warn("failed to open temporary file: {}", .{err}); + return error.InvalidData; + }; + defer file.close(); + + // File must be a regular file + if (file.stat()) |stat| { + if (stat.kind != .file) { + log.warn("file is not a regular file kind={}", .{stat.kind}); + return error.InvalidData; + } + } else |err| { + log.warn("failed to stat file: {}", .{err}); + return error.InvalidData; + } + + if (t.offset > 0) { + file.seekTo(@intCast(t.offset)) catch |err| { + log.warn("failed to seek to offset {}: {}", .{ t.offset, err }); + return error.InvalidData; + }; + } + + var buf_reader = std.io.bufferedReader(file.reader()); + const reader = buf_reader.reader(); + + // Read the file + var managed = std.ArrayList(u8).init(alloc); + errdefer managed.deinit(); + const size: usize = if (t.size > 0) @min(t.size, max_size) else max_size; + reader.readAllArrayList(&managed, size) catch |err| { + log.warn("failed to read temporary file: {}", .{err}); + return error.InvalidData; + }; + + // Set our data + assert(self.data.items.len == 0); + self.data = .{ .items = managed.items, .capacity = managed.capacity }; + } + + /// Returns true if path appears to be in a temporary directory. + /// Copies logic from Kitty. + fn isPathInTempDir(path: []const u8) bool { + if (std.mem.startsWith(u8, path, "/tmp")) return true; + if (std.mem.startsWith(u8, path, "/dev/shm")) return true; + if (internal_os.allocTmpDir(std.heap.page_allocator)) |dir| { + defer internal_os.freeTmpDir(std.heap.page_allocator, dir); + if (std.mem.startsWith(u8, path, dir)) return true; + + // The temporary dir is sometimes a symlink. On macOS for + // example /tmp is /private/var/... + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + if (std.os.realpath(dir, &buf)) |real_dir| { + if (std.mem.startsWith(u8, path, real_dir)) return true; + } else |_| {} + } + + return false; + } + + pub fn deinit(self: *LoadingImage, alloc: Allocator) void { + self.image.deinit(alloc); + self.data.deinit(alloc); + } + + pub fn destroy(self: *LoadingImage, alloc: Allocator) void { + self.deinit(alloc); + alloc.destroy(self); + } + + /// Adds a chunk of base64-encoded data to the image. Use this if the + /// image is coming in chunks (the "m" parameter in the protocol). + pub fn addData(self: *LoadingImage, alloc: Allocator, data: []const u8) !void { + // If no data, skip + if (data.len == 0) return; + + // Grow our array list by size capacity if it needs it + const Base64Decoder = std.base64.standard.Decoder; + const size = Base64Decoder.calcSizeForSlice(data) catch |err| { + log.warn("failed to calculate size for base64 data: {}", .{err}); + return error.InvalidData; + }; + + // If our data would get too big, return an error + if (self.data.items.len + size > max_size) { + log.warn("image data too large max_size={}", .{max_size}); + return error.InvalidData; + } + + try self.data.ensureUnusedCapacity(alloc, size); + + // We decode directly into the arraylist + const start_i = self.data.items.len; + self.data.items.len = start_i + size; + const buf = self.data.items[start_i..]; + Base64Decoder.decode(buf, data) catch |err| switch (err) { + // We have to ignore invalid padding because lots of encoders + // add the wrong padding. Since we validate image data later + // (PNG decode or simple dimensions check), we can ignore this. + error.InvalidPadding => {}, + + else => { + log.warn("failed to decode base64 data: {}", .{err}); + return error.InvalidData; + }, + }; + } + + /// Complete the chunked image, returning a completed image. + pub fn complete(self: *LoadingImage, alloc: Allocator) !Image { + const img = &self.image; + + // Decompress the data if it is compressed. + try self.decompress(alloc); + + // Decode the png if we have to + if (img.format == .png) try self.decodePng(alloc); + + // Validate our dimensions. + if (img.width == 0 or img.height == 0) return error.DimensionsRequired; + if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge; + + // Data length must be what we expect + const bpp: u32 = switch (img.format) { + .grey_alpha => 2, + .rgb => 3, + .rgba => 4, + .png => unreachable, // png should be decoded by here + }; + const expected_len = img.width * img.height * bpp; + const actual_len = self.data.items.len; + if (actual_len != expected_len) { + std.log.warn( + "unexpected length image id={} width={} height={} bpp={} expected_len={} actual_len={}", + .{ img.id, img.width, img.height, bpp, expected_len, actual_len }, + ); + return error.InvalidData; + } + + // Set our time + self.image.transmit_time = std.time.Instant.now() catch |err| { + log.warn("failed to get time: {}", .{err}); + return error.InternalError; + }; + + // Everything looks good, copy the image data over. + var result = self.image; + result.data = try self.data.toOwnedSlice(alloc); + errdefer result.deinit(alloc); + self.image = .{}; + return result; + } + + /// Debug function to write the data to a file. This is useful for + /// capturing some test data for unit tests. + pub fn debugDump(self: LoadingImage) !void { + if (comptime builtin.mode != .Debug) @compileError("debugDump in non-debug"); + + var buf: [1024]u8 = undefined; + const filename = try std.fmt.bufPrint( + &buf, + "image-{s}-{s}-{d}x{d}-{}.data", + .{ + @tagName(self.image.format), + @tagName(self.image.compression), + self.image.width, + self.image.height, + self.image.id, + }, + ); + const cwd = std.fs.cwd(); + const f = try cwd.createFile(filename, .{}); + defer f.close(); + + const writer = f.writer(); + try writer.writeAll(self.data.items); + } + + /// Decompress the data in-place. + fn decompress(self: *LoadingImage, alloc: Allocator) !void { + return switch (self.image.compression) { + .none => {}, + .zlib_deflate => self.decompressZlib(alloc), + }; + } + + fn decompressZlib(self: *LoadingImage, alloc: Allocator) !void { + // Open our zlib stream + var fbs = std.io.fixedBufferStream(self.data.items); + var stream = std.compress.zlib.decompressor(fbs.reader()); + + // Write it to an array list + var list = std.ArrayList(u8).init(alloc); + errdefer list.deinit(); + stream.reader().readAllArrayList(&list, max_size) catch |err| { + log.warn("failed to read decompressed data: {}", .{err}); + return error.DecompressionFailed; + }; + + // Empty our current data list, take ownership over managed array list + self.data.deinit(alloc); + self.data = .{ .items = list.items, .capacity = list.capacity }; + + // Make sure we note that our image is no longer compressed + self.image.compression = .none; + } + + /// Decode the data as PNG. This will also updated the image dimensions. + fn decodePng(self: *LoadingImage, alloc: Allocator) !void { + assert(self.image.format == .png); + + // Decode PNG + var width: c_int = 0; + var height: c_int = 0; + var bpp: c_int = 0; + const data = stb.stbi_load_from_memory( + self.data.items.ptr, + @intCast(self.data.items.len), + &width, + &height, + &bpp, + 0, + ) orelse return error.InvalidData; + defer stb.stbi_image_free(data); + const len: usize = @intCast(width * height * bpp); + if (len > max_size) { + log.warn("png image too large size={} max_size={}", .{ len, max_size }); + return error.InvalidData; + } + + // Validate our bpp + if (bpp < 2 or bpp > 4) { + log.warn("png with unsupported bpp={}", .{bpp}); + return error.UnsupportedDepth; + } + + // Replace our data + self.data.deinit(alloc); + self.data = .{}; + try self.data.ensureUnusedCapacity(alloc, len); + try self.data.appendSlice(alloc, data[0..len]); + + // Store updated image dimensions + self.image.width = @intCast(width); + self.image.height = @intCast(height); + self.image.format = switch (bpp) { + 2 => .grey_alpha, + 3 => .rgb, + 4 => .rgba, + else => unreachable, // validated above + }; + } +}; + +/// Image represents a single fully loaded image. +pub const Image = struct { + id: u32 = 0, + number: u32 = 0, + width: u32 = 0, + height: u32 = 0, + format: command.Transmission.Format = .rgb, + compression: command.Transmission.Compression = .none, + data: []const u8 = "", + transmit_time: std.time.Instant = undefined, + + pub const Error = error{ + InternalError, + InvalidData, + DecompressionFailed, + DimensionsRequired, + DimensionsTooLarge, + FilePathTooLong, + TemporaryFileNotInTempDir, + UnsupportedFormat, + UnsupportedMedium, + UnsupportedDepth, + }; + + pub fn deinit(self: *Image, alloc: Allocator) void { + if (self.data.len > 0) alloc.free(self.data); + } + + /// Mostly for logging + pub fn withoutData(self: *const Image) Image { + var copy = self.*; + copy.data = ""; + return copy; + } +}; + +/// The rect taken up by some image placement, in grid cells. This will +/// be rounded up to the nearest grid cell since we can't place images +/// in partial grid cells. +pub const Rect = struct { + top_left: point.ScreenPoint = .{}, + bottom_right: point.ScreenPoint = .{}, + + /// True if the rect contains a given screen point. + pub fn contains(self: Rect, p: point.ScreenPoint) bool { + return p.y >= self.top_left.y and + p.y <= self.bottom_right.y and + p.x >= self.top_left.x and + p.x <= self.bottom_right.x; + } +}; + +/// Easy base64 encoding function. +fn testB64(alloc: Allocator, data: []const u8) ![]const u8 { + const B64Encoder = std.base64.standard.Encoder; + const b64 = try alloc.alloc(u8, B64Encoder.calcSize(data.len)); + errdefer alloc.free(b64); + return B64Encoder.encode(b64, data); +} + +/// Easy base64 decoding function. +fn testB64Decode(alloc: Allocator, data: []const u8) ![]const u8 { + const B64Decoder = std.base64.standard.Decoder; + const result = try alloc.alloc(u8, try B64Decoder.calcSizeForSlice(data)); + errdefer alloc.free(result); + try B64Decoder.decode(result, data); + return result; +} + +// This specifically tests we ALLOW invalid RGB data because Kitty +// documents that this should work. +test "image load with invalid RGB data" { + const testing = std.testing; + const alloc = testing.allocator; + + // _Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\ + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .width = 1, + .height = 1, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, "AAAA"), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); +} + +test "image load with image too wide" { + const testing = std.testing; + const alloc = testing.allocator; + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .width = max_dimension + 1, + .height = 1, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, "AAAA"), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc)); +} + +test "image load with image too tall" { + const testing = std.testing; + const alloc = testing.allocator; + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .height = max_dimension + 1, + .width = 1, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, "AAAA"), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc)); +} + +test "image load: rgb, zlib compressed, direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .direct, + .compression = .zlib_deflate, + .height = 96, + .width = 128, + .image_id = 31, + } }, + .data = try alloc.dupe( + u8, + @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"), + ), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + + // should be decompressed + try testing.expect(img.compression == .none); +} + +test "image load: rgb, not compressed, direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .direct, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try alloc.dupe( + u8, + @embedFile("testdata/image-rgb-none-20x15-2147483647.data"), + ), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + + // should be decompressed + try testing.expect(img.compression == .none); +} + +test "image load: rgb, zlib compressed, direct, chunked" { + const testing = std.testing; + const alloc = testing.allocator; + + const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"); + + // Setup our initial chunk + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .direct, + .compression = .zlib_deflate, + .height = 96, + .width = 128, + .image_id = 31, + .more_chunks = true, + } }, + .data = try alloc.dupe(u8, data[0..1024]), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + + // Read our remaining chunks + var fbs = std.io.fixedBufferStream(data[1024..]); + var buf: [1024]u8 = undefined; + while (fbs.reader().readAll(&buf)) |size| { + try loading.addData(alloc, buf[0..size]); + if (size < buf.len) break; + } else |err| return err; + + // Complete + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); +} + +test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk" { + const testing = std.testing; + const alloc = testing.allocator; + + const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"); + + // Setup our initial chunk + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .direct, + .compression = .zlib_deflate, + .height = 96, + .width = 128, + .image_id = 31, + .more_chunks = true, + } }, + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + + // Read our remaining chunks + var fbs = std.io.fixedBufferStream(data); + var buf: [1024]u8 = undefined; + while (fbs.reader().readAll(&buf)) |size| { + try loading.addData(alloc, buf[0..size]); + if (size < buf.len) break; + } else |err| return err; + + // Complete + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); +} + +test "image load: rgb, not compressed, temporary file" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try internal_os.TempDir.init(); + defer tmp_dir.deinit(); + const data = try testB64Decode( + alloc, + @embedFile("testdata/image-rgb-none-20x15-2147483647.data"), + ); + defer alloc.free(data); + try tmp_dir.dir.writeFile("image.data", data); + + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try tmp_dir.dir.realpath("image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .temporary_file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try testB64(alloc, path), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); + + // Temporary file should be gone + try testing.expectError(error.FileNotFound, tmp_dir.dir.access(path, .{})); +} + +test "image load: rgb, not compressed, regular file" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try internal_os.TempDir.init(); + defer tmp_dir.deinit(); + const data = try testB64Decode( + alloc, + @embedFile("testdata/image-rgb-none-20x15-2147483647.data"), + ); + defer alloc.free(data); + try tmp_dir.dir.writeFile("image.data", data); + + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try tmp_dir.dir.realpath("image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try testB64(alloc, path), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); + try tmp_dir.dir.access(path, .{}); +} + +test "image load: png, not compressed, regular file" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try internal_os.TempDir.init(); + defer tmp_dir.deinit(); + const data = @embedFile("testdata/image-png-none-50x76-2147483647-raw.data"); + try tmp_dir.dir.writeFile("image.data", data); + + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try tmp_dir.dir.realpath("image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .png, + .medium = .file, + .compression = .none, + .width = 0, + .height = 0, + .image_id = 31, + } }, + .data = try testB64(alloc, path), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); + try testing.expect(img.format == .rgb); + try tmp_dir.dir.access(path, .{}); +} diff --git a/src/terminal2/kitty/graphics_storage.zig b/src/terminal2/kitty/graphics_storage.zig new file mode 100644 index 000000000..230c1edc3 --- /dev/null +++ b/src/terminal2/kitty/graphics_storage.zig @@ -0,0 +1,919 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const terminal = @import("../main.zig"); +const point = @import("../point.zig"); +const command = @import("graphics_command.zig"); +const PageList = @import("../PageList.zig"); +const Screen = @import("../Screen.zig"); +const LoadingImage = @import("graphics_image.zig").LoadingImage; +const Image = @import("graphics_image.zig").Image; +const Rect = @import("graphics_image.zig").Rect; +const Command = command.Command; + +const log = std.log.scoped(.kitty_gfx); + +/// An image storage is associated with a terminal screen (i.e. main +/// screen, alt screen) and contains all the transmitted images and +/// placements. +pub const ImageStorage = struct { + const ImageMap = std.AutoHashMapUnmanaged(u32, Image); + const PlacementMap = std.AutoHashMapUnmanaged(PlacementKey, Placement); + + /// Dirty is set to true if placements or images change. This is + /// purely informational for the renderer and doesn't affect the + /// correctness of the program. The renderer must set this to false + /// if it cares about this value. + dirty: bool = false, + + /// This is the next automatically assigned image ID. We start mid-way + /// through the u32 range to avoid collisions with buggy programs. + next_image_id: u32 = 2147483647, + + /// This is the next automatically assigned placement ID. This is never + /// user-facing so we can start at 0. This is 32-bits because we use + /// the same space for external placement IDs. We can start at zero + /// because any number is valid. + next_internal_placement_id: u32 = 0, + + /// The set of images that are currently known. + images: ImageMap = .{}, + + /// The set of placements for loaded images. + placements: PlacementMap = .{}, + + /// Non-null if there is an in-progress loading image. + loading: ?*LoadingImage = null, + + /// The total bytes of image data that have been loaded and the limit. + /// If the limit is reached, the oldest images will be evicted to make + /// space. Unused images take priority. + total_bytes: usize = 0, + total_limit: usize = 320 * 1000 * 1000, // 320MB + + pub fn deinit( + self: *ImageStorage, + alloc: Allocator, + t: *terminal.Terminal, + ) void { + if (self.loading) |loading| loading.destroy(alloc); + + var it = self.images.iterator(); + while (it.next()) |kv| kv.value_ptr.deinit(alloc); + self.images.deinit(alloc); + + self.clearPlacements(t); + self.placements.deinit(alloc); + } + + /// Kitty image protocol is enabled if we have a non-zero limit. + pub fn enabled(self: *const ImageStorage) bool { + return self.total_limit != 0; + } + + /// Sets the limit in bytes for the total amount of image data that + /// can be loaded. If this limit is lower, this will do an eviction + /// if necessary. If the value is zero, then Kitty image protocol will + /// be disabled. + pub fn setLimit(self: *ImageStorage, alloc: Allocator, limit: usize) !void { + // Special case disabling by quickly deleting all + if (limit == 0) { + self.deinit(alloc); + self.* = .{}; + } + + // If we re lowering our limit, check if we need to evict. + if (limit < self.total_bytes) { + const req_bytes = self.total_bytes - limit; + log.info("evicting images to lower limit, evicting={}", .{req_bytes}); + if (!try self.evictImage(alloc, req_bytes)) { + log.warn("failed to evict enough images for required bytes", .{}); + } + } + + self.total_limit = limit; + } + + /// Add an already-loaded image to the storage. This will automatically + /// free any existing image with the same ID. + pub fn addImage(self: *ImageStorage, alloc: Allocator, img: Image) Allocator.Error!void { + // If the image itself is over the limit, then error immediately + if (img.data.len > self.total_limit) return error.OutOfMemory; + + // If this would put us over the limit, then evict. + const total_bytes = self.total_bytes + img.data.len; + if (total_bytes > self.total_limit) { + const req_bytes = total_bytes - self.total_limit; + log.info("evicting images to make space for {} bytes", .{req_bytes}); + if (!try self.evictImage(alloc, req_bytes)) { + log.warn("failed to evict enough images for required bytes", .{}); + return error.OutOfMemory; + } + } + + // Do the gop op first so if it fails we don't get a partial state + const gop = try self.images.getOrPut(alloc, img.id); + + log.debug("addImage image={}", .{img: { + var copy = img; + copy.data = ""; + break :img copy; + }}); + + // Write our new image + if (gop.found_existing) { + self.total_bytes -= gop.value_ptr.data.len; + gop.value_ptr.deinit(alloc); + } + + gop.value_ptr.* = img; + self.total_bytes += img.data.len; + + self.dirty = true; + } + + /// Add a placement for a given image. The caller must verify in advance + /// the image exists to prevent memory corruption. + pub fn addPlacement( + self: *ImageStorage, + alloc: Allocator, + image_id: u32, + placement_id: u32, + p: Placement, + ) !void { + assert(self.images.get(image_id) != null); + log.debug("placement image_id={} placement_id={} placement={}\n", .{ + image_id, + placement_id, + p, + }); + + // The important piece here is that the placement ID needs to + // be marked internal if it is zero. This allows multiple placements + // to be added for the same image. If it is non-zero, then it is + // an external placement ID and we can only have one placement + // per (image id, placement id) pair. + const key: PlacementKey = .{ + .image_id = image_id, + .placement_id = if (placement_id == 0) .{ + .tag = .internal, + .id = id: { + defer self.next_internal_placement_id +%= 1; + break :id self.next_internal_placement_id; + }, + } else .{ + .tag = .external, + .id = placement_id, + }, + }; + + const gop = try self.placements.getOrPut(alloc, key); + gop.value_ptr.* = p; + + self.dirty = true; + } + + fn clearPlacements(self: *ImageStorage, t: *terminal.Terminal) void { + var it = self.placements.iterator(); + while (it.next()) |entry| entry.value_ptr.deinit(t); + self.placements.clearRetainingCapacity(); + } + + /// Get an image by its ID. If the image doesn't exist, null is returned. + pub fn imageById(self: *const ImageStorage, image_id: u32) ?Image { + return self.images.get(image_id); + } + + /// Get an image by its number. If the image doesn't exist, return null. + pub fn imageByNumber(self: *const ImageStorage, image_number: u32) ?Image { + var newest: ?Image = null; + + var it = self.images.iterator(); + while (it.next()) |kv| { + if (kv.value_ptr.number == image_number) { + if (newest == null or + kv.value_ptr.transmit_time.order(newest.?.transmit_time) == .gt) + { + newest = kv.value_ptr.*; + } + } + } + + return newest; + } + + /// Delete placements, images. + pub fn delete( + self: *ImageStorage, + alloc: Allocator, + t: *terminal.Terminal, + cmd: command.Delete, + ) void { + switch (cmd) { + .all => |delete_images| if (delete_images) { + // We just reset our entire state. + self.deinit(alloc, t); + self.* = .{ + .dirty = true, + .total_limit = self.total_limit, + }; + } else { + // Delete all our placements + self.clearPlacements(t); + self.placements.deinit(alloc); + self.placements = .{}; + self.dirty = true; + }, + + .id => |v| self.deleteById( + alloc, + t, + v.image_id, + v.placement_id, + v.delete, + ), + + .newest => |v| newest: { + if (true) @panic("TODO"); + const img = self.imageByNumber(v.image_number) orelse break :newest; + self.deleteById(alloc, img.id, v.placement_id, v.delete); + }, + + .intersect_cursor => |delete_images| { + if (true) @panic("TODO"); + const target = (point.Viewport{ + .x = t.screen.cursor.x, + .y = t.screen.cursor.y, + }).toScreen(&t.screen); + self.deleteIntersecting(alloc, t, target, delete_images, {}, null); + }, + + .intersect_cell => |v| { + if (true) @panic("TODO"); + const target = (point.Viewport{ .x = v.x, .y = v.y }).toScreen(&t.screen); + self.deleteIntersecting(alloc, t, target, v.delete, {}, null); + }, + + .intersect_cell_z => |v| { + if (true) @panic("TODO"); + const target = (point.Viewport{ .x = v.x, .y = v.y }).toScreen(&t.screen); + self.deleteIntersecting(alloc, t, target, v.delete, v.z, struct { + fn filter(ctx: i32, p: Placement) bool { + return p.z == ctx; + } + }.filter); + }, + + .column => |v| { + if (true) @panic("TODO"); + var it = self.placements.iterator(); + while (it.next()) |entry| { + const img = self.imageById(entry.key_ptr.image_id) orelse continue; + const rect = entry.value_ptr.rect(img, t); + if (rect.top_left.x <= v.x and rect.bottom_right.x >= v.x) { + self.placements.removeByPtr(entry.key_ptr); + if (v.delete) self.deleteIfUnused(alloc, img.id); + } + } + + // Mark dirty to force redraw + self.dirty = true; + }, + + .row => |v| { + if (true) @panic("TODO"); + // Get the screenpoint y + const y = (point.Viewport{ .x = 0, .y = v.y }).toScreen(&t.screen).y; + + var it = self.placements.iterator(); + while (it.next()) |entry| { + const img = self.imageById(entry.key_ptr.image_id) orelse continue; + const rect = entry.value_ptr.rect(img, t); + if (rect.top_left.y <= y and rect.bottom_right.y >= y) { + self.placements.removeByPtr(entry.key_ptr); + if (v.delete) self.deleteIfUnused(alloc, img.id); + } + } + + // Mark dirty to force redraw + self.dirty = true; + }, + + .z => |v| { + if (true) @panic("TODO"); + var it = self.placements.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.z == v.z) { + const image_id = entry.key_ptr.image_id; + 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 => {}, + } + } + + fn deleteById( + self: *ImageStorage, + alloc: Allocator, + t: *terminal.Terminal, + image_id: u32, + placement_id: u32, + delete_unused: bool, + ) void { + // If no placement, we delete all placements with the ID + if (placement_id == 0) { + var it = self.placements.iterator(); + while (it.next()) |entry| { + if (entry.key_ptr.image_id == image_id) { + entry.value_ptr.deinit(t); + self.placements.removeByPtr(entry.key_ptr); + } + } + } else { + if (self.placements.getEntry(.{ + .image_id = image_id, + .placement_id = .{ .tag = .external, .id = placement_id }, + })) |entry| { + entry.value_ptr.deinit(t); + self.placements.removeByPtr(entry.key_ptr); + } + } + + // If this is specified, then we also delete the image + // if it is no longer in use. + if (delete_unused) self.deleteIfUnused(alloc, image_id); + + // Mark dirty to force redraw + self.dirty = true; + } + + /// Delete an image if it is unused. + fn deleteIfUnused(self: *ImageStorage, alloc: Allocator, image_id: u32) void { + var it = self.placements.iterator(); + while (it.next()) |kv| { + if (kv.key_ptr.image_id == image_id) { + return; + } + } + + // If we get here, we can delete the image. + if (self.images.getEntry(image_id)) |entry| { + self.total_bytes -= entry.value_ptr.data.len; + entry.value_ptr.deinit(alloc); + self.images.removeByPtr(entry.key_ptr); + } + } + + /// Deletes all placements intersecting a screen point. + fn deleteIntersecting( + self: *ImageStorage, + alloc: Allocator, + t: *const terminal.Terminal, + p: point.ScreenPoint, + delete_unused: bool, + filter_ctx: anytype, + comptime filter: ?fn (@TypeOf(filter_ctx), Placement) bool, + ) void { + var it = self.placements.iterator(); + while (it.next()) |entry| { + const img = self.imageById(entry.key_ptr.image_id) orelse continue; + const rect = entry.value_ptr.rect(img, t); + if (rect.contains(p)) { + if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue; + self.placements.removeByPtr(entry.key_ptr); + if (delete_unused) self.deleteIfUnused(alloc, img.id); + } + } + + // Mark dirty to force redraw + self.dirty = true; + } + + /// Evict image to make space. This will evict the oldest image, + /// prioritizing unused images first, as recommended by the published + /// Kitty spec. + /// + /// This will evict as many images as necessary to make space for + /// req bytes. + fn evictImage(self: *ImageStorage, alloc: Allocator, req: usize) !bool { + assert(req <= self.total_limit); + + // Ironically we allocate to evict. We should probably redesign the + // data structures to avoid this but for now allocating a little + // bit is fine compared to the megabytes we're looking to save. + const Candidate = struct { + id: u32, + time: std.time.Instant, + used: bool, + }; + + var candidates = std.ArrayList(Candidate).init(alloc); + defer candidates.deinit(); + + var it = self.images.iterator(); + while (it.next()) |kv| { + const img = kv.value_ptr; + + // This is a huge waste. See comment above about redesigning + // our data structures to avoid this. Eviction should be very + // rare though and we never have that many images/placements + // so hopefully this will last a long time. + const used = used: { + var p_it = self.placements.iterator(); + while (p_it.next()) |p_kv| { + if (p_kv.key_ptr.image_id == img.id) { + break :used true; + } + } + + break :used false; + }; + + try candidates.append(.{ + .id = img.id, + .time = img.transmit_time, + .used = used, + }); + } + + // Sort + std.mem.sortUnstable( + Candidate, + candidates.items, + {}, + struct { + fn lessThan( + ctx: void, + lhs: Candidate, + rhs: Candidate, + ) bool { + _ = ctx; + + // If they're usage matches, then its based on time. + if (lhs.used == rhs.used) return switch (lhs.time.order(rhs.time)) { + .lt => true, + .gt => false, + .eq => lhs.id < rhs.id, + }; + + // If not used, then its a better candidate + return !lhs.used; + } + }.lessThan, + ); + + // They're in order of best to evict. + var evicted: usize = 0; + for (candidates.items) |c| { + // Delete all the placements for this image and the image. + var p_it = self.placements.iterator(); + while (p_it.next()) |entry| { + if (entry.key_ptr.image_id == c.id) { + self.placements.removeByPtr(entry.key_ptr); + } + } + + if (self.images.getEntry(c.id)) |entry| { + log.info("evicting image id={} bytes={}", .{ c.id, entry.value_ptr.data.len }); + + evicted += entry.value_ptr.data.len; + self.total_bytes -= entry.value_ptr.data.len; + + entry.value_ptr.deinit(alloc); + self.images.removeByPtr(entry.key_ptr); + + if (evicted > req) return true; + } + } + + return false; + } + + /// Every placement is uniquely identified by the image ID and the + /// placement ID. If an image ID isn't specified it is assumed to be 0. + /// Likewise, if a placement ID isn't specified it is assumed to be 0. + pub const PlacementKey = struct { + image_id: u32, + placement_id: packed struct { + tag: enum(u1) { internal, external }, + id: u32, + }, + }; + + pub const Placement = struct { + /// The tracked pin for this placement. + pin: *PageList.Pin, + + /// Offset of the x/y from the top-left of the cell. + x_offset: u32 = 0, + y_offset: u32 = 0, + + /// Source rectangle for the image to pull from + source_x: u32 = 0, + source_y: u32 = 0, + source_width: u32 = 0, + source_height: u32 = 0, + + /// The columns/rows this image occupies. + columns: u32 = 0, + rows: u32 = 0, + + /// The z-index for this placement. + z: i32 = 0, + + pub fn deinit( + self: *const Placement, + t: *terminal.Terminal, + ) void { + t.screen.pages.untrackPin(self.pin); + } + + /// Returns a selection of the entire rectangle this placement + /// occupies within the screen. + pub fn rect( + self: Placement, + image: Image, + t: *const terminal.Terminal, + ) Rect { + // If we have columns/rows specified we can simplify this whole thing. + if (self.columns > 0 and self.rows > 0) { + return .{ + .top_left = self.point, + .bottom_right = .{ + .x = @min(self.point.x + self.columns, t.cols - 1), + .y = self.point.y + self.rows, + }, + }; + } + + // Calculate our cell size. + const terminal_width_f64: f64 = @floatFromInt(t.width_px); + const terminal_height_f64: f64 = @floatFromInt(t.height_px); + const grid_columns_f64: f64 = @floatFromInt(t.cols); + const grid_rows_f64: f64 = @floatFromInt(t.rows); + const cell_width_f64 = terminal_width_f64 / grid_columns_f64; + const cell_height_f64 = terminal_height_f64 / grid_rows_f64; + + // Our image width + const width_px = if (self.source_width > 0) self.source_width else image.width; + const height_px = if (self.source_height > 0) self.source_height else image.height; + + // Calculate our image size in grid cells + const width_f64: f64 = @floatFromInt(width_px); + const height_f64: f64 = @floatFromInt(height_px); + const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64)); + const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64)); + + return .{ + .top_left = self.point, + .bottom_right = .{ + .x = @min(self.point.x + width_cells, t.cols - 1), + .y = self.point.y + height_cells, + }, + }; + } + }; +}; + +// Our pin for the placement +fn trackPin( + t: *terminal.Terminal, + pt: point.Point.Coordinate, +) !*PageList.Pin { + return try t.screen.pages.trackPin(t.screen.pages.pin(.{ + .active = pt, + }).?); +} + +test "storage: add placement with zero placement id" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 100, 100); + defer t.deinit(alloc); + t.width_px = 100; + t.height_px = 100; + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); + try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); + try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + + try testing.expectEqual(@as(usize, 2), s.placements.count()); + try testing.expectEqual(@as(usize, 2), s.images.count()); + + // verify the placement is what we expect + try testing.expect(s.placements.get(.{ + .image_id = 1, + .placement_id = .{ .tag = .internal, .id = 0 }, + }) != null); + try testing.expect(s.placements.get(.{ + .image_id = 1, + .placement_id = .{ .tag = .internal, .id = 1 }, + }) != null); +} + +test "storage: delete all placements and images" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 3, 3); + defer t.deinit(alloc); + const tracked = t.screen.pages.countTrackedPins(); + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + try s.addImage(alloc, .{ .id = 1 }); + try s.addImage(alloc, .{ .id = 2 }); + try s.addImage(alloc, .{ .id = 3 }); + try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + + s.dirty = false; + s.delete(alloc, &t, .{ .all = true }); + try testing.expect(s.dirty); + try testing.expectEqual(@as(usize, 0), s.images.count()); + try testing.expectEqual(@as(usize, 0), s.placements.count()); + try testing.expectEqual(tracked, t.screen.pages.countTrackedPins()); +} + +test "storage: delete all placements and images preserves limit" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 3, 3); + defer t.deinit(alloc); + const tracked = t.screen.pages.countTrackedPins(); + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + s.total_limit = 5000; + try s.addImage(alloc, .{ .id = 1 }); + try s.addImage(alloc, .{ .id = 2 }); + try s.addImage(alloc, .{ .id = 3 }); + try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + + s.dirty = false; + s.delete(alloc, &t, .{ .all = true }); + try testing.expect(s.dirty); + try testing.expectEqual(@as(usize, 0), s.images.count()); + try testing.expectEqual(@as(usize, 0), s.placements.count()); + try testing.expectEqual(@as(usize, 5000), s.total_limit); + try testing.expectEqual(tracked, t.screen.pages.countTrackedPins()); +} + +test "storage: delete all placements" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 3, 3); + defer t.deinit(alloc); + const tracked = t.screen.pages.countTrackedPins(); + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + try s.addImage(alloc, .{ .id = 1 }); + try s.addImage(alloc, .{ .id = 2 }); + try s.addImage(alloc, .{ .id = 3 }); + try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + + s.dirty = false; + s.delete(alloc, &t, .{ .all = false }); + try testing.expect(s.dirty); + try testing.expectEqual(@as(usize, 0), s.placements.count()); + try testing.expectEqual(@as(usize, 3), s.images.count()); + try testing.expectEqual(tracked, t.screen.pages.countTrackedPins()); +} + +test "storage: delete all placements by image id" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 3, 3); + defer t.deinit(alloc); + const tracked = t.screen.pages.countTrackedPins(); + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + try s.addImage(alloc, .{ .id = 1 }); + try s.addImage(alloc, .{ .id = 2 }); + try s.addImage(alloc, .{ .id = 3 }); + try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + + s.dirty = false; + s.delete(alloc, &t, .{ .id = .{ .image_id = 2 } }); + try testing.expect(s.dirty); + try testing.expectEqual(@as(usize, 1), s.placements.count()); + try testing.expectEqual(@as(usize, 3), s.images.count()); + try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins()); +} + +test "storage: delete all placements by image id and unused images" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 3, 3); + defer t.deinit(alloc); + const tracked = t.screen.pages.countTrackedPins(); + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + try s.addImage(alloc, .{ .id = 1 }); + try s.addImage(alloc, .{ .id = 2 }); + try s.addImage(alloc, .{ .id = 3 }); + try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + + s.dirty = false; + s.delete(alloc, &t, .{ .id = .{ .delete = true, .image_id = 2 } }); + try testing.expect(s.dirty); + try testing.expectEqual(@as(usize, 1), s.placements.count()); + try testing.expectEqual(@as(usize, 2), s.images.count()); + try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins()); +} + +test "storage: delete placement by specific id" { + const testing = std.testing; + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, 3, 3); + defer t.deinit(alloc); + const tracked = t.screen.pages.countTrackedPins(); + + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t); + try s.addImage(alloc, .{ .id = 1 }); + try s.addImage(alloc, .{ .id = 2 }); + try s.addImage(alloc, .{ .id = 3 }); + try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + + s.dirty = false; + s.delete(alloc, &t, .{ .id = .{ + .delete = true, + .image_id = 1, + .placement_id = 2, + } }); + try testing.expect(s.dirty); + try testing.expectEqual(@as(usize, 2), s.placements.count()); + try testing.expectEqual(@as(usize, 3), s.images.count()); + try testing.expectEqual(tracked + 2, t.screen.pages.countTrackedPins()); +} + +// test "storage: delete intersecting cursor" { +// const testing = std.testing; +// const alloc = testing.allocator; +// var t = try terminal.Terminal.init(alloc, 100, 100); +// defer t.deinit(alloc); +// t.width_px = 100; +// t.height_px = 100; +// +// var s: ImageStorage = .{}; +// defer s.deinit(alloc); +// try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); +// try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); +// try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); +// try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); +// +// t.screen.cursor.x = 12; +// t.screen.cursor.y = 12; +// +// s.dirty = false; +// s.delete(alloc, &t, .{ .intersect_cursor = false }); +// try testing.expect(s.dirty); +// try testing.expectEqual(@as(usize, 1), s.placements.count()); +// try testing.expectEqual(@as(usize, 2), s.images.count()); +// +// // verify the placement is what we expect +// try testing.expect(s.placements.get(.{ +// .image_id = 1, +// .placement_id = .{ .tag = .external, .id = 2 }, +// }) != null); +// } +// +// test "storage: delete intersecting cursor plus unused" { +// const testing = std.testing; +// const alloc = testing.allocator; +// var t = try terminal.Terminal.init(alloc, 100, 100); +// defer t.deinit(alloc); +// t.width_px = 100; +// t.height_px = 100; +// +// var s: ImageStorage = .{}; +// defer s.deinit(alloc); +// try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); +// try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); +// try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); +// try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); +// +// t.screen.cursor.x = 12; +// t.screen.cursor.y = 12; +// +// s.dirty = false; +// s.delete(alloc, &t, .{ .intersect_cursor = true }); +// try testing.expect(s.dirty); +// try testing.expectEqual(@as(usize, 1), s.placements.count()); +// try testing.expectEqual(@as(usize, 2), s.images.count()); +// +// // verify the placement is what we expect +// try testing.expect(s.placements.get(.{ +// .image_id = 1, +// .placement_id = .{ .tag = .external, .id = 2 }, +// }) != null); +// } +// +// test "storage: delete intersecting cursor hits multiple" { +// const testing = std.testing; +// const alloc = testing.allocator; +// var t = try terminal.Terminal.init(alloc, 100, 100); +// defer t.deinit(alloc); +// t.width_px = 100; +// t.height_px = 100; +// +// var s: ImageStorage = .{}; +// defer s.deinit(alloc); +// try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); +// try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); +// try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); +// try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); +// +// t.screen.cursor.x = 26; +// t.screen.cursor.y = 26; +// +// s.dirty = false; +// s.delete(alloc, &t, .{ .intersect_cursor = true }); +// try testing.expect(s.dirty); +// try testing.expectEqual(@as(usize, 0), s.placements.count()); +// try testing.expectEqual(@as(usize, 1), s.images.count()); +// } +// +// test "storage: delete by column" { +// const testing = std.testing; +// const alloc = testing.allocator; +// var t = try terminal.Terminal.init(alloc, 100, 100); +// defer t.deinit(alloc); +// t.width_px = 100; +// t.height_px = 100; +// +// var s: ImageStorage = .{}; +// defer s.deinit(alloc); +// try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); +// try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); +// try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); +// try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); +// +// s.dirty = false; +// s.delete(alloc, &t, .{ .column = .{ +// .delete = false, +// .x = 60, +// } }); +// try testing.expect(s.dirty); +// try testing.expectEqual(@as(usize, 1), s.placements.count()); +// try testing.expectEqual(@as(usize, 2), s.images.count()); +// +// // verify the placement is what we expect +// try testing.expect(s.placements.get(.{ +// .image_id = 1, +// .placement_id = .{ .tag = .external, .id = 1 }, +// }) != null); +// } +// +// test "storage: delete by row" { +// const testing = std.testing; +// const alloc = testing.allocator; +// var t = try terminal.Terminal.init(alloc, 100, 100); +// defer t.deinit(alloc); +// t.width_px = 100; +// t.height_px = 100; +// +// var s: ImageStorage = .{}; +// defer s.deinit(alloc); +// try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); +// try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); +// try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } }); +// try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } }); +// +// s.dirty = false; +// s.delete(alloc, &t, .{ .row = .{ +// .delete = false, +// .y = 60, +// } }); +// try testing.expect(s.dirty); +// try testing.expectEqual(@as(usize, 1), s.placements.count()); +// try testing.expectEqual(@as(usize, 2), s.images.count()); +// +// // verify the placement is what we expect +// try testing.expect(s.placements.get(.{ +// .image_id = 1, +// .placement_id = .{ .tag = .external, .id = 1 }, +// }) != null); +// } diff --git a/src/terminal2/kitty/key.zig b/src/terminal2/kitty/key.zig new file mode 100644 index 000000000..938bf65b5 --- /dev/null +++ b/src/terminal2/kitty/key.zig @@ -0,0 +1,151 @@ +//! Kitty keyboard protocol support. + +const std = @import("std"); + +/// Stack for the key flags. This implements the push/pop behavior +/// of the CSI > u and CSI < u sequences. We implement the stack as +/// fixed size to avoid heap allocation. +pub const KeyFlagStack = struct { + const len = 8; + + flags: [len]KeyFlags = .{.{}} ** len, + idx: u3 = 0, + + /// Return the current stack value + pub fn current(self: KeyFlagStack) KeyFlags { + return self.flags[self.idx]; + } + + /// Perform the "set" operation as described in the spec for + /// the CSI = u sequence. + pub fn set( + self: *KeyFlagStack, + mode: KeySetMode, + v: KeyFlags, + ) void { + switch (mode) { + .set => self.flags[self.idx] = v, + .@"or" => self.flags[self.idx] = @bitCast( + self.flags[self.idx].int() | v.int(), + ), + .not => self.flags[self.idx] = @bitCast( + self.flags[self.idx].int() & ~v.int(), + ), + } + } + + /// Push a new set of flags onto the stack. If the stack is full + /// then the oldest entry is evicted. + pub fn push(self: *KeyFlagStack, flags: KeyFlags) void { + // Overflow and wrap around if we're full, which evicts + // the oldest entry. + self.idx +%= 1; + self.flags[self.idx] = flags; + } + + /// Pop `n` entries from the stack. This will just wrap around + /// if `n` is greater than the amount in the stack. + pub fn pop(self: *KeyFlagStack, n: usize) void { + // If n is more than our length then we just reset the stack. + // This also avoids a DoS vector where a malicious client + // could send a huge number of pop commands to waste cpu. + if (n >= self.flags.len) { + self.idx = 0; + self.flags = .{.{}} ** len; + return; + } + + for (0..n) |_| { + self.flags[self.idx] = .{}; + self.idx -%= 1; + } + } + + // Make sure we the overflow works as expected + test { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.idx = stack.flags.len - 1; + stack.idx +%= 1; + try testing.expect(stack.idx == 0); + + stack.idx = 0; + stack.idx -%= 1; + try testing.expect(stack.idx == stack.flags.len - 1); + } +}; + +/// The possible flags for the Kitty keyboard protocol. +pub const KeyFlags = packed struct(u5) { + disambiguate: bool = false, + report_events: bool = false, + report_alternates: bool = false, + report_all: bool = false, + report_associated: bool = false, + + pub fn int(self: KeyFlags) u5 { + return @bitCast(self); + } + + // Its easy to get packed struct ordering wrong so this test checks. + test { + const testing = std.testing; + + try testing.expectEqual( + @as(u5, 0b1), + (KeyFlags{ .disambiguate = true }).int(), + ); + try testing.expectEqual( + @as(u5, 0b10), + (KeyFlags{ .report_events = true }).int(), + ); + } +}; + +/// The possible modes for setting the key flags. +pub const KeySetMode = enum { set, @"or", not }; + +test "KeyFlagStack: push pop" { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.push(.{ .disambiguate = true }); + try testing.expectEqual( + KeyFlags{ .disambiguate = true }, + stack.current(), + ); + + stack.pop(1); + try testing.expectEqual(KeyFlags{}, stack.current()); +} + +test "KeyFlagStack: pop big number" { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.pop(100); + try testing.expectEqual(KeyFlags{}, stack.current()); +} + +test "KeyFlagStack: set" { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.set(.set, .{ .disambiguate = true }); + try testing.expectEqual( + KeyFlags{ .disambiguate = true }, + stack.current(), + ); + + stack.set(.@"or", .{ .report_events = true }); + try testing.expectEqual( + KeyFlags{ + .disambiguate = true, + .report_events = true, + }, + stack.current(), + ); + + stack.set(.not, .{ .report_events = true }); + try testing.expectEqual( + KeyFlags{ .disambiguate = true }, + stack.current(), + ); +} diff --git a/src/terminal2/kitty/testdata/image-png-none-50x76-2147483647-raw.data b/src/terminal2/kitty/testdata/image-png-none-50x76-2147483647-raw.data new file mode 100644 index 000000000..032cb07c7 Binary files /dev/null and b/src/terminal2/kitty/testdata/image-png-none-50x76-2147483647-raw.data differ diff --git a/src/terminal2/kitty/testdata/image-rgb-none-20x15-2147483647.data b/src/terminal2/kitty/testdata/image-rgb-none-20x15-2147483647.data new file mode 100644 index 000000000..f65d40ce8 --- /dev/null +++ b/src/terminal2/kitty/testdata/image-rgb-none-20x15-2147483647.data @@ -0,0 +1 @@ +DRoeCxgcCxcjEh4qDBgkCxcjChYiCxcjCRclBRMhBxIXHysvTVNRbHJwcXB2Li0zCBYXEyEiCxkaDBobChcbCBUZDxsnBBAcEBwoChYiCxcjDBgkDhwqBxUjDBccm6aqy9HP1NrYzs3UsK+2IjAxCBYXCBYXBxUWFBoaDxUVICYqIyktERcZDxUXDxUVEhgYDhUTCxIQGh8XusC4zM7FvL61q6elmZWTTVtcDBobDRscCxkaKS8vaW9vxMnOur/EiY+RaW5wICYmW2FhfYOBQEZEnqSc4ebeqauilZaOsa2rm5eVcH5/GigpChgZCBYX0NHP3d7c3tzbx8XExsTEvry8wL241dLN0tDF0tDF29nM4d/StbKpzMrAUk5DZmJXeYSGKTU3ER0fDRkb1tfVysvJ0tDPsa+tr6ytop+gmZaRqaahuritw8G2urirqKaZiYZ9paKZZmJXamZbOkZIDhocBxMVBBASxMDBtrKzqqanoZ2ejYeLeHF2eXFvhn58npePta6ml5CKgXp0W1hPaWZdZWdSYmRPFiYADR0AFCQAEyMAt7O0lJCRf3t8eHR1Zl9kY1xhYVpYbGRieXJqeHFpdW1oc2tmcG1kX1xTbW9ajY96jp55kaF8kKB7kaF8sK6rcnFtX11cXFpZW1pWWFdTXVpTXltUaGJgY11bY11da2Vla25dam1ccHtTnqmBorVtp7pypLdvobRsh4aCaGdjWFZVXFpZYWBcZ2ZiaGVeZGFaY11bYlxaV1FRZ2FhdHdmbG9egItjo66GpLdvq752rL93rsF5kpKIZ2ddWFxTW19WbnZdipJ6cnhaaW9RaGhgV1ZPY2Jga2poanFQd35dk6Vpn7B0oLFvorNxm6xqmKlnv760enpwVlpRW19Wc3til5+Hl55/k5p7iIiAcnJqd3Z0bm1rcHdWh45tipxgladrkaJglKVjkaJgkqNh09DJiYZ/YmZdY2deeYZYjJlrj51ijpxhztHClJaIdHNvdHNvanNHi5RpmaxnjKBbmqhrmadqkJ5hi5lcxsO8jImCaGtiYmZdg5Bikp9xjJpfjpxh1djJqq2eamllZ2Zid4BVmKF2kqZhh5tWlaNmlaNmjpxfjJpdw729rqiodnZ0cHBuiplij55nj6FVjJ5SzdC9t7qncW1sXlpZh45iqbCEmKllmapmmqlqnq1unaxtoK9w \ No newline at end of file diff --git a/src/terminal2/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647.data b/src/terminal2/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647.data new file mode 100644 index 000000000..c1d6d87b3 --- /dev/null +++ b/src/terminal2/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647.data @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/terminal2/main.zig b/src/terminal2/main.zig index 22e66466f..14933d3de 100644 --- a/src/terminal2/main.zig +++ b/src/terminal2/main.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); +pub const kitty = @import("kitty.zig"); pub const page = @import("page.zig"); pub const point = @import("point.zig"); pub const PageList = @import("PageList.zig");