terminal: support disabling kitty graphics protocol

pull/8840/head
Mitchell Hashimoto 2025-09-20 21:03:22 -07:00
parent 1b46884e72
commit 811f9f05d0
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
8 changed files with 157 additions and 81 deletions

View File

@ -1153,9 +1153,11 @@ const ReflowCursor = struct {
self.page_cell.style_id = id;
}
// Copy Kitty virtual placeholder status
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
self.page_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
// Copy Kitty virtual placeholder status
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
self.page_row.kitty_virtual_placeholder = true;
}
}
self.cursorForward();
@ -8917,6 +8919,8 @@ test "PageList resize reflow less cols to wrap a multi-codepoint grapheme with a
}
test "PageList resize reflow less cols copy kitty placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -8956,6 +8960,8 @@ test "PageList resize reflow less cols copy kitty placeholder" {
}
test "PageList resize reflow more cols clears kitty placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -8997,6 +9003,8 @@ test "PageList resize reflow more cols clears kitty placeholder" {
}
test "PageList resize reflow wrap moves kitty placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -66,7 +66,10 @@ protected_mode: ansi.ProtectedMode = .off,
kitty_keyboard: kitty.KeyFlagStack = .{},
/// Kitty graphics protocol state.
kitty_images: kitty.graphics.ImageStorage = .{},
kitty_images: if (build_options.kitty_graphics)
kitty.graphics.ImageStorage
else
struct {} = .{},
/// Dirty flags for the renderer.
dirty: Dirty = .{},
@ -208,7 +211,9 @@ pub fn init(
}
pub fn deinit(self: *Screen) void {
self.kitty_images.deinit(self.alloc, self);
if (comptime build_options.kitty_graphics) {
self.kitty_images.deinit(self.alloc, self);
}
self.cursor.deinit(self.alloc);
self.pages.deinit();
}
@ -269,9 +274,11 @@ pub fn reset(self: *Screen) void {
.page_cell = cursor_rac.cell,
};
// Reset kitty graphics storage
self.kitty_images.deinit(self.alloc, self);
self.kitty_images = .{ .dirty = true };
if (comptime build_options.kitty_graphics) {
// Reset kitty graphics storage
self.kitty_images.deinit(self.alloc, self);
self.kitty_images = .{ .dirty = true };
}
// Reset our basic state
self.saved_cursor = null;
@ -690,8 +697,10 @@ pub fn cursorDownScroll(self: *Screen) !void {
assert(self.cursor.y == self.pages.rows - 1);
defer self.assertIntegrity();
// Scrolling dirties the images because it updates their placements pins.
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.kitty_images.dirty = true;
}
// If we have no scrollback, then we shift all our rows instead.
if (self.no_scrollback) {
@ -1154,10 +1163,12 @@ pub const Scroll = union(enum) {
pub fn scroll(self: *Screen, behavior: Scroll) void {
defer self.assertIntegrity();
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
}
switch (behavior) {
.active => self.pages.scroll(.{ .active = {} }),
@ -1176,10 +1187,12 @@ pub fn scrollClear(self: *Screen) !void {
try self.pages.scrollClear();
self.cursorReload();
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
}
}
/// Returns true if the viewport is scrolled to the bottom of the screen.
@ -1299,14 +1312,16 @@ pub fn clearCells(
if (cells.len == self.pages.cols) row.styled = false;
}
if (row.kitty_virtual_placeholder and
cells.len == self.pages.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
if (comptime build_options.kitty_graphics) {
if (row.kitty_virtual_placeholder and
cells.len == self.pages.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
}
}
@memset(cells, self.blankCell());
@ -1570,8 +1585,10 @@ fn resizeInternal(
) !void {
defer self.assertIntegrity();
// No matter what we mark our image state as dirty
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// No matter what we mark our image state as dirty
self.kitty_images.dirty = true;
}
// Release the cursor style while resizing just
// in case the cursor ends up on a different page.

View File

@ -4,6 +4,7 @@
const Terminal = @This();
const std = @import("std");
const build_options = @import("terminal_options");
const builtin = @import("builtin");
const assert = std.debug.assert;
const testing = std.testing;
@ -679,8 +680,10 @@ fn printCell(
// If this is a Kitty unicode placeholder then we need to mark the
// row so that the renderer can lookup rows with these much faster.
if (c == kitty.graphics.unicode.placeholder) {
self.screen.cursor.page_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
if (c == kitty.graphics.unicode.placeholder) {
self.screen.cursor.page_row.kitty_virtual_placeholder = true;
}
}
// We check for an active hyperlink first because setHyperlink
@ -1143,8 +1146,10 @@ pub fn index(self: *Terminal) !void {
self.screen.cursor.x >= self.scrolling_region.left and
self.screen.cursor.x <= self.scrolling_region.right)
{
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
}
// If our scrolling region is at the top, we create scrollback.
if (self.scrolling_region.top == 0 and
@ -1472,8 +1477,10 @@ pub fn insertLines(self: *Terminal, count: usize) void {
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
}
// At the end we need to return the cursor to the row it started on.
const start_y = self.screen.cursor.y;
@ -1676,8 +1683,10 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
}
// At the end we need to return the cursor to the row it started on.
const start_y = self.screen.cursor.y;
@ -2136,12 +2145,14 @@ pub fn eraseDisplay(
// Unsets pending wrap state
self.screen.cursor.pending_wrap = false;
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
if (comptime build_options.kitty_graphics) {
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
}
},
.complete => {
@ -2195,12 +2206,14 @@ pub fn eraseDisplay(
// Unsets pending wrap state
self.screen.cursor.pending_wrap = false;
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
if (comptime build_options.kitty_graphics) {
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
}
// Cleared screen dirty bit
self.flags.dirty.clear = true;
@ -2574,10 +2587,12 @@ pub fn switchScreen(self: *Terminal, t: ScreenType) ?*Screen {
// Clear our selection
self.screen.clearSelection();
// Mark kitty images as dirty so they redraw. Without this set
// the images will remain where they were (the dirty bit on
// the screen only tracks the terminal grid, not the images).
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Mark kitty images as dirty so they redraw. Without this set
// the images will remain where they were (the dirty bit on
// the screen only tracks the terminal grid, not the images).
self.screen.kitty_images.dirty = true;
}
// Mark our terminal as dirty to redraw the grid.
self.flags.dirty.clear = true;
@ -3862,6 +3877,8 @@ test "Terminal: print invoke charset single" {
}
test "Terminal: print kitty unicode placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);

View File

@ -1,4 +1,5 @@
const std = @import("std");
const build_options = @import("terminal_options");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
@ -33,17 +34,22 @@ pub const Handler = struct {
.identify => {
switch (byte) {
// Kitty graphics protocol
'G' => self.state = .{ .kitty = kitty_gfx.CommandParser.init(alloc) },
'G' => self.state = if (comptime build_options.kitty_graphics)
.{ .kitty = kitty_gfx.CommandParser.init(alloc) }
else
.{ .ignore = {} },
// Unknown
else => self.state = .{ .ignore = {} },
}
},
.kitty => |*p| p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
self.state = .{ .ignore = {} };
},
.kitty => |*p| if (comptime build_options.kitty_graphics) {
p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
self.state = .{ .ignore = {} };
};
} else unreachable,
}
}
@ -57,6 +63,8 @@ pub const Handler = struct {
.inactive => unreachable,
.ignore, .identify => null,
.kitty => |*p| kitty: {
if (comptime !build_options.kitty_graphics) unreachable;
const command = p.complete() catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
break :kitty null;
@ -81,23 +89,35 @@ pub const State = union(enum) {
identify: void,
/// Kitty graphics protocol
kitty: kitty_gfx.CommandParser,
kitty: if (build_options.kitty_graphics)
kitty_gfx.CommandParser
else
void,
pub fn deinit(self: *State) void {
switch (self.*) {
.inactive, .ignore, .identify => {},
.kitty => |*v| v.deinit(),
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit()
else
unreachable,
}
}
};
/// Possible APC commands.
pub const Command = union(enum) {
kitty: kitty_gfx.Command,
kitty: if (build_options.kitty_graphics)
kitty_gfx.Command
else
void,
pub fn deinit(self: *Command, alloc: Allocator) void {
switch (self.*) {
.kitty => |*v| v.deinit(alloc),
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit(alloc)
else
unreachable,
}
}
};

View File

@ -34,6 +34,7 @@ pub fn addOptions(
opts.addOption(bool, "slow_runtime_safety", v.slow_runtime_safety);
// These are synthesized based on other options.
opts.addOption(bool, "kitty_graphics", v.oniguruma);
opts.addOption(bool, "tmux_control_mode", v.oniguruma);
m.addOptions("terminal_options", opts);

View File

@ -1,8 +1,10 @@
//! Types and functions related to Kitty protocols.
const build_options = @import("terminal_options");
const key = @import("kitty/key.zig");
pub const color = @import("kitty/color.zig");
pub const graphics = @import("kitty/graphics.zig");
pub const graphics = if (build_options.kitty_graphics) @import("kitty/graphics.zig") else struct {};
pub const KeyFlags = key.Flags;
pub const KeyFlagStack = key.FlagStack;

View File

@ -11,7 +11,7 @@ const mem = std.mem;
const assert = std.debug.assert;
const Allocator = mem.Allocator;
const RGB = @import("color.zig").RGB;
const kitty = @import("kitty.zig");
const kitty_color = @import("kitty/color.zig");
const osc_color = @import("osc/color.zig");
pub const color = osc_color;
@ -132,7 +132,7 @@ pub const Command = union(enum) {
/// Kitty color protocol, OSC 21
/// https://sw.kovidgoyal.net/kitty/color-stack/#id1
kitty_color_protocol: kitty.color.OSC,
kitty_color_protocol: kitty_color.OSC,
/// Show a desktop notification (OSC 9 or OSC 777)
show_desktop_notification: struct {
@ -796,7 +796,7 @@ pub const Parser = struct {
self.command = .{
.kitty_color_protocol = .{
.list = std.ArrayList(kitty.color.OSC.Request).init(alloc),
.list = std.ArrayList(kitty_color.OSC.Request).init(alloc),
},
};
@ -1490,7 +1490,7 @@ pub const Parser = struct {
return;
}
const key = kitty.color.Kind.parse(self.temp_state.key) orelse {
const key = kitty_color.Kind.parse(self.temp_state.key) orelse {
log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key});
return;
};
@ -1504,7 +1504,7 @@ pub const Parser = struct {
switch (self.command) {
.kitty_color_protocol => |*v| {
// Cap our allocation amount for our list.
if (v.list.items.len >= @as(usize, kitty.color.Kind.max) * 2) {
if (v.list.items.len >= @as(usize, kitty_color.Kind.max) * 2) {
self.state = .invalid;
log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{});
return;
@ -2600,7 +2600,7 @@ test "OSC: hyperlink end" {
test "OSC: kitty color protocol" {
const testing = std.testing;
const Kind = kitty.color.Kind;
const Kind = kitty_color.Kind;
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();

View File

@ -890,8 +890,10 @@ pub const Page = struct {
error.NeedsRehash => return error.StyleSetNeedsRehash,
} orelse src_cell.style_id;
}
if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
}
}
}
}
@ -980,8 +982,10 @@ pub const Page = struct {
dst.hyperlink = true;
dst_row.hyperlink = true;
}
if (src.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
if (src.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
}
}
}
}
@ -1002,7 +1006,9 @@ pub const Page = struct {
src_row.grapheme = false;
src_row.hyperlink = false;
src_row.styled = false;
src_row.kitty_virtual_placeholder = false;
if (comptime build_options.kitty_graphics) {
src_row.kitty_virtual_placeholder = false;
}
}
}
@ -1100,14 +1106,16 @@ pub const Page = struct {
if (cells.len == self.size.cols) row.styled = false;
}
if (row.kitty_virtual_placeholder and
cells.len == self.size.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
if (comptime build_options.kitty_graphics) {
if (row.kitty_virtual_placeholder and
cells.len == self.size.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
}
}
// Zero the cells as u64s since empirically this seems
@ -1929,6 +1937,9 @@ pub const Row = packed struct(u64) {
/// True if this row contains a virtual placeholder for the Kitty
/// graphics protocol. (U+10EEEE)
// Note: We keep this as memory-using even if the kitty graphics
// feature is disabled because we want to keep our padding and
// everything throughout the same.
kitty_virtual_placeholder: bool = false,
_padding: u23 = 0,