terminal/kitty: add loading limits to kitty graphics protocol

Add a Limits type to LoadingImage that controls which transmission
mediums (file, temporary_file, shared_memory) are allowed when
loading images. This defaults to "direct" (most restrictive) on
ImageStorage and is set to "all" by Termio, allowing apprt
embedders like libghostty to restrict medium types for resource or
security reasons.

The limits are stored on ImageStorage, plumbed through
Screen.Options for screen initialization and inheritance, and
enforced in graphics_exec during both query and transmit. Two new
Terminal methods (setKittyGraphicsSizeLimit, setKittyGraphicsLoadingLimits)
centralize updating all screens, replacing the manual iteration
previously done in Termio.
This commit is contained in:
Mitchell Hashimoto
2026-04-05 07:11:45 -07:00
parent 6a99c248d0
commit 64dcb91c1f
6 changed files with 60 additions and 26 deletions

View File

@@ -257,6 +257,12 @@ pub const Options = struct {
/// screen. Kitty image storage is per-screen.
kitty_image_storage_limit: usize = 320 * 1000 * 1000, // 320MB
/// The limits for what medium types are allowed for Kitty image loading.
kitty_image_loading_limits: if (build_options.kitty_graphics)
kitty.graphics.LoadingImage.Limits
else
void = if (build_options.kitty_graphics) .direct else {},
/// A simple, default terminal. If you rely on specific dimensions or
/// scrollback (or lack of) then do not use this directly. This is just
/// for callers that need some defaults.
@@ -313,6 +319,7 @@ pub fn init(
&result,
opts.kitty_image_storage_limit,
) catch unreachable;
result.kitty_images.image_limits = opts.kitty_image_loading_limits;
}
return result;

View File

@@ -2693,6 +2693,34 @@ pub fn kittyGraphics(
return kitty.graphics.execute(alloc, self, cmd);
}
/// Set the storage size limit for Kitty graphics across all screens.
pub fn setKittyGraphicsSizeLimit(
self: *Terminal,
alloc: Allocator,
limit: usize,
) !void {
if (comptime !build_options.kitty_graphics) return;
var it = self.screens.all.iterator();
while (it.next()) |entry| {
const screen: *Screen = entry.value.*;
try screen.kitty_images.setLimit(alloc, screen, limit);
}
}
/// Set the allowed medium types for Kitty graphics image loading
/// across all screens.
pub fn setKittyGraphicsLoadingLimits(
self: *Terminal,
limits: kitty.graphics.LoadingImage.Limits,
) void {
if (comptime !build_options.kitty_graphics) return;
var it = self.screens.all.iterator();
while (it.next()) |entry| {
const screen: *Screen = entry.value.*;
screen.kitty_images.image_limits = limits;
}
}
/// Set a style attribute.
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
try self.screens.active.setAttribute(attr);
@@ -2941,12 +2969,15 @@ pub fn switchScreen(self: *Terminal, key: ScreenSet.Key) !?*Screen {
.alternate => 0,
},
// Inherit our Kitty image storage limit from the primary
// Inherit our Kitty image settings from the primary
// screen if we have to initialize.
.kitty_image_storage_limit = if (comptime build_options.kitty_graphics)
primary.kitty_images.total_limit
else
0,
.kitty_image_loading_limits = if (comptime build_options.kitty_graphics)
primary.kitty_images.image_limits
else {},
},
);
};

View File

@@ -25,6 +25,7 @@ pub const unicode = @import("graphics_unicode.zig");
pub const Command = command.Command;
pub const CommandParser = command.Parser;
pub const Image = image.Image;
pub const LoadingImage = image.LoadingImage;
pub const ImageStorage = storage.ImageStorage;
pub const RenderPlacement = render.Placement;
pub const Response = command.Response;

View File

@@ -44,7 +44,7 @@ pub fn execute(
var quiet = cmd.quiet;
const resp_: ?Response = switch (cmd.control) {
.query => query(alloc, cmd),
.query => query(alloc, terminal, cmd),
.display => display(alloc, terminal, cmd),
.delete => delete(alloc, terminal, cmd),
@@ -94,7 +94,11 @@ pub fn execute(
/// 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: *const Command) Response {
fn query(
alloc: Allocator,
terminal: *const Terminal,
cmd: *const Command,
) Response {
const t = cmd.control.query;
// Query requires image ID. We can't actually send a response without
@@ -112,7 +116,8 @@ fn query(alloc: Allocator, cmd: *const Command) Response {
};
// Attempt to load the image. If we cannot, then set an appropriate error.
var loading = LoadingImage.init(alloc, cmd, .all) catch |err| {
const storage = &terminal.screens.active.kitty_images;
var loading = LoadingImage.init(alloc, cmd, storage.image_limits) catch |err| {
encodeError(&result, err);
return result;
};
@@ -322,7 +327,7 @@ fn loadAndAddImage(
}
break :loading loading.*;
} else try .init(alloc, cmd, .all);
} else try .init(alloc, cmd, storage.image_limits);
// We only want to deinit on error. If we're chunking, then we don't
// want to deinit at all. If we're not chunking, then we'll deinit

View File

@@ -51,6 +51,9 @@ pub const ImageStorage = struct {
/// Non-null if there is an in-progress loading image.
loading: ?*LoadingImage = null,
/// The limits of what medium types are allowed for image loading.
image_limits: LoadingImage.Limits = .direct,
/// 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.
@@ -89,8 +92,9 @@ pub const ImageStorage = struct {
) !void {
// Special case disabling by quickly deleting all
if (limit == 0) {
const image_limits = self.image_limits;
self.deinit(alloc, s);
self.* = .{};
self.* = .{ .image_limits = image_limits };
}
// If we re lowering our limit, check if we need to evict.

View File

@@ -259,16 +259,9 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
});
errdefer term.deinit(alloc);
// Set the image size limits
var it = term.screens.all.iterator();
while (it.next()) |entry| {
const screen: *terminalpkg.Screen = entry.value.*;
try screen.kitty_images.setLimit(
alloc,
screen,
opts.config.image_storage_limit,
);
}
// Set the Kitty image settings
try term.setKittyGraphicsSizeLimit(alloc, opts.config.image_storage_limit);
term.setKittyGraphicsLoadingLimits(.all);
// Set our default cursor style
term.screens.active.cursor.cursor_style = opts.config.cursor_style;
@@ -463,16 +456,9 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
break :cursor color.toTerminalRGB() orelse break :cursor null;
};
// Set the image size limits
var it = self.terminal.screens.all.iterator();
while (it.next()) |entry| {
const screen: *terminalpkg.Screen = entry.value.*;
try screen.kitty_images.setLimit(
self.alloc,
screen,
config.image_storage_limit,
);
}
// Set the image limits
try self.terminal.setKittyGraphicsSizeLimit(self.alloc, config.image_storage_limit);
self.terminal.setKittyGraphicsLoadingLimits(.all);
}
/// Resize the terminal.