Leah Amelia Chen 2025-12-18 02:17:31 +07:00 committed by GitHub
commit 1f5b444157
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 330 additions and 12 deletions

View File

@ -286,18 +286,19 @@ pub const VTEvent = struct {
),
else => switch (Value) {
u8, u16 => try md.put(
key,
try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0),
),
[]const u8,
[:0]const u8,
=> try md.put(key, try alloc.dupeZ(u8, value)),
else => |T| {
@compileLog(T);
@compileError("unsupported type, see log");
else => switch (@typeInfo(Value)) {
.int => try md.put(
key,
try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0),
),
else => {
@compileLog(Value);
@compileError("unsupported type, see log");
},
},
},
}

View File

@ -5,6 +5,9 @@ const build_options = @import("terminal_options");
const key = @import("kitty/key.zig");
pub const color = @import("kitty/color.zig");
pub const graphics = if (build_options.kitty_graphics) @import("kitty/graphics.zig") else struct {};
pub const text_sizing = @import("kitty/text_sizing.zig");
pub const encoding = @import("kitty/encoding.zig");
pub const notification = @import("kitty/notification.zig");
pub const KeyFlags = key.Flags;
pub const KeyFlagStack = key.FlagStack;

View File

@ -0,0 +1,21 @@
//! Encodings used by various Kitty protocol extensions.
const std = @import("std");
/// Kitty defines "URL-safe UTF-8" as valid UTF-8 with the additional
/// requirement of not containing any C0 escape codes (0x00-0x1f)
pub fn isUrlSafeUtf8(s: []const u8) bool {
if (!std.unicode.utf8ValidateSlice(s)) {
@branchHint(.cold);
return false;
}
for (s) |c| switch (c) {
0x00...0x1f => {
@branchHint(.cold);
return false;
},
else => {},
};
return true;
}

View File

@ -0,0 +1,81 @@
//! Kitty's text sizing protocol (OSC 66)
//! Specification: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
const std = @import("std");
const build_options = @import("terminal_options");
const encoding = @import("encoding.zig");
const lib = @import("../../lib/main.zig");
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
const log = std.log.scoped(.kitty_text_sizing);
pub const max_payload_length = 4096;
pub const VAlign = lib.Enum(lib_target, &.{
"top",
"bottom",
"center",
});
pub const HAlign = lib.Enum(lib_target, &.{
"left",
"right",
"center",
});
pub const OSC = struct {
scale: u3 = 1, // 1 - 7
width: u3 = 0, // 0 - 7 (0 means default)
numerator: u4 = 0,
denominator: u4 = 0,
valign: VAlign = .top,
halign: HAlign = .left,
text: [:0]const u8,
/// We don't currently support encoding this to C in any way.
pub const C = void;
pub fn cval(_: OSC) C {
return {};
}
pub fn set(self: *OSC, key: u8, value: []const u8) !void {
const v = std.fmt.parseInt(
u4,
value,
10,
) catch return error.InvalidValue;
switch (key) {
's' => self.scale = std.math.cast(u3, v) orelse return error.InvalidValue,
'w' => self.width = std.math.cast(u3, v) orelse return error.InvalidValue,
'n' => self.numerator = v,
'd' => self.denominator = v,
'v' => self.valign = std.enums.fromInt(VAlign, v) orelse return error.InvalidValue,
'h' => self.halign = std.enums.fromInt(HAlign, v) orelse return error.InvalidValue,
else => return error.UnknownKey,
}
}
pub fn validate(self: OSC) bool {
if (self.text.len > max_payload_length) {
@branchHint(.cold);
log.warn("kitty text sizing payload exceeds maximum size", .{});
return false;
}
if (!encoding.isUrlSafeUtf8(self.text)) {
@branchHint(.cold);
log.warn("kitty text sizing payload is not URL-safe UTF-8", .{});
return false;
}
if (self.scale == 0) {
@branchHint(.cold);
log.warn("kitty text sizing cannot have 0 scale", .{});
return false;
}
return true;
}
};

View File

@ -13,7 +13,8 @@ const assert = @import("../quirks.zig").inlineAssert;
const Allocator = mem.Allocator;
const LibEnum = @import("../lib/enum.zig").Enum;
const RGB = @import("color.zig").RGB;
const kitty_color = @import("kitty/color.zig");
const kitty = @import("kitty.zig");
const kitty_color = kitty.color;
const osc_color = @import("osc/color.zig");
const string_encoding = @import("../os/string_encoding.zig");
pub const color = osc_color;
@ -193,6 +194,9 @@ pub const Command = union(Key) {
/// ConEmu GUI macro (OSC 9;6)
conemu_guimacro: [:0]const u8,
/// Kitty text sizing protocol (OSC 66)
kitty_text_sizing: kitty.text_sizing.OSC,
pub const Key = LibEnum(
if (build_options.c_abi) .c else .zig,
// NOTE: Order matters, see LibEnum documentation.
@ -218,6 +222,7 @@ pub const Command = union(Key) {
"conemu_progress_report",
"conemu_wait_input",
"conemu_guimacro",
"kitty_text_sizing",
},
);
@ -344,11 +349,15 @@ pub const Parser = struct {
// Maximum length of a single OSC command. This is the full OSC command
// sequence length (excluding ESC ]). This is arbitrary, I couldn't find
// any definitive resource on how long this should be.
// any definitive resource on how long this should be, except for OSC 66
// and 99 (Kitty's text sizing and notification protocols respectively)
// imposing a maximum _payload_ length of 4096 bytes, so let's be generous
// and allow 8192 bytes.
//
// NOTE: This does mean certain OSC sequences such as OSC 8 (hyperlinks)
// won't work if their parameters are larger than fit in the buffer.
const MAX_BUF = 2048;
// won't work if their parameters too large to fit in the buffer.
// Other OSCs that use allocable strings may exceed this size limit.
const MAX_BUF = 8192;
pub const State = enum {
empty,
@ -377,6 +386,8 @@ pub const Parser = struct {
@"4",
@"5",
@"52",
@"6",
@"66",
@"7",
@"77",
@"777",
@ -451,6 +462,10 @@ pub const Parser = struct {
conemu_progress_prevalue,
conemu_progress_value,
conemu_guimacro,
// Kitty text size protocol
kitty_text_sizing_key,
kitty_text_sizing_value,
};
pub fn init(alloc: ?Allocator) Parser {
@ -559,6 +574,7 @@ pub const Parser = struct {
'2' => self.state = .@"2",
'4' => self.state = .@"4",
'5' => self.state = .@"5",
'6' => self.state = .@"6",
'7' => self.state = .@"7",
'8' => self.state = .@"8",
'9' => self.state = .@"9",
@ -976,6 +992,38 @@ pub const Parser = struct {
else => self.state = .invalid,
},
.@"6" => switch (c) {
'6' => self.state = .@"66",
else => self.state = .invalid,
},
.@"66" => switch (c) {
';' => {
self.command = .{ .kitty_text_sizing = .{
.text = "",
} };
self.temp_state = .{ .key = "" };
self.state = .kitty_text_sizing_key;
self.buf_start = self.buf_idx;
},
else => self.state = .invalid,
},
.kitty_text_sizing_key => switch (c) {
';' => self.endKittyTextSizingOption(true),
'=' => {
self.state = .kitty_text_sizing_value;
self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] };
self.buf_start = self.buf_idx;
},
else => {},
},
.kitty_text_sizing_value => switch (c) {
':' => self.endKittyTextSizingOption(false),
';' => self.endKittyTextSizingOption(true),
else => {},
},
.@"7" => switch (c) {
';' => {
self.command = .{ .report_pwd = .{ .value = "" } };
@ -1685,6 +1733,54 @@ pub const Parser = struct {
};
}
fn endKittyTextSizingOption(self: *Parser, final: bool) void {
if (self.command != .kitty_text_sizing) {
@branchHint(.cold);
log.warn("tried to end text sizing option with an invalid command: {}", .{self.command});
return;
}
if (self.temp_state.key.len > 0) {
// All keys so far are single characters
if (self.temp_state.key.len > 1) {
@branchHint(.cold);
log.warn("invalid kitty text sizing option key", .{});
self.state = .invalid;
return;
}
if (self.buf_idx == self.buf_start) {
@branchHint(.cold);
log.warn("kitty text sizing option does not have a value", .{});
self.state = .invalid;
return;
}
const key = self.temp_state.key[0];
const value = self.buf[self.buf_start .. self.buf_idx - 1];
self.command.kitty_text_sizing.set(key, value) catch |err| {
@branchHint(.cold);
switch (err) {
error.InvalidValue => log.warn("invalid kitty text sizing option value for {c}: {s}", .{ key, value }),
error.UnknownKey => log.warn("unknown kitty text sizing option key: {c}", .{key}),
}
self.state = .invalid;
return;
};
}
if (final) {
self.temp_state = .{ .str = &self.command.kitty_text_sizing.text };
self.state = .string;
self.buf_start = self.buf_idx;
self.complete = true;
} else {
self.state = .kitty_text_sizing_key;
self.buf_start = self.buf_idx;
}
}
fn endAllocableString(self: *Parser) void {
const alloc = self.alloc.?;
const list = self.buf_dynamic.?;
@ -1755,12 +1851,16 @@ pub const Parser = struct {
.conemu_progress_value,
=> {},
.kitty_text_sizing_key => self.endKittyTextSizingOption(true),
.kitty_text_sizing_value => self.endKittyTextSizingOption(true),
else => {},
}
switch (self.command) {
.kitty_color_protocol => |*c| c.terminator = .init(terminator_ch),
.color_operation => |*c| c.terminator = .init(terminator_ch),
.kitty_text_sizing => |c| if (!c.validate()) return null,
else => {},
}
@ -3337,3 +3437,114 @@ test "OSC: OSC 777 show desktop notification with title" {
try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title");
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
}
test "OSC 66: empty parameters" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;;bobr";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_text_sizing);
try testing.expectEqual(1, cmd.kitty_text_sizing.scale);
try testing.expectEqualStrings("bobr", cmd.kitty_text_sizing.text);
}
test "OSC 66: single parameter" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;s=2;kurwa";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_text_sizing);
try testing.expectEqual(2, cmd.kitty_text_sizing.scale);
try testing.expectEqualStrings("kurwa", cmd.kitty_text_sizing.text);
}
test "OSC 66: multiple parameters" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;s=2:w=7:n=13:d=15:v=1:h=2;long";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_text_sizing);
try testing.expectEqual(2, cmd.kitty_text_sizing.scale);
try testing.expectEqual(7, cmd.kitty_text_sizing.width);
try testing.expectEqual(13, cmd.kitty_text_sizing.numerator);
try testing.expectEqual(15, cmd.kitty_text_sizing.denominator);
try testing.expectEqual(.bottom, cmd.kitty_text_sizing.valign);
try testing.expectEqual(.center, cmd.kitty_text_sizing.halign);
try testing.expectEqualStrings("long", cmd.kitty_text_sizing.text);
}
test "OSC 66: scale is zero" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;s=0;nope";
for (input) |ch| p.next(ch);
try testing.expect(p.end('\x1b') == null);
}
test "OSC 66: parameters are too large" {
const testing = std.testing;
var p: Parser = .init(null);
for ("66;w=8;") |ch| p.next(ch);
try testing.expect(p.end('\x1b') == null);
p.reset();
for ("66;v=3;") |ch| p.next(ch);
try testing.expect(p.end('\x1b') == null);
p.reset();
for ("66;n=16;") |ch| p.next(ch);
try testing.expect(p.end('\x1b') == null);
p.reset();
}
test "OSC 66: UTF-8" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;;👻魑魅魍魉ゴースッティ";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_text_sizing);
try testing.expectEqualStrings("👻魑魅魍魉ゴースッティ", cmd.kitty_text_sizing.text);
}
test "OSC 66: unsafe UTF-8" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;;\n";
for (input) |ch| p.next(ch);
try testing.expect(p.end('\x1b') == null);
}
test "OSC 66: overlong UTF-8" {
const testing = std.testing;
var p: Parser = .init(null);
const input = "66;;" ++ "bobr" ** 1025;
for (input) |ch| p.next(ch);
try testing.expect(p.end('\x1b') == null);
}

View File

@ -2107,6 +2107,7 @@ pub fn Stream(comptime Handler: type) type {
.conemu_change_tab_title,
.conemu_wait_input,
.conemu_guimacro,
.kitty_text_sizing,
=> {
log.warn("unimplemented OSC callback: {}", .{cmd});
},