diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig index 97f31d5b6..0f4373df9 100644 --- a/src/terminfo/Source.zig +++ b/src/terminfo/Source.zig @@ -81,17 +81,39 @@ pub fn xtgettcapMap(comptime self: Source) type { kvs[1] = .{ "Co", "256" }; kvs[2] = .{ "RGB", "8" }; for (self.capabilities, 3..) |cap, i| { - kvs[i] = .{ cap.name, switch (cap.value) { - .canceled => @compileError("canceled not handled yet"), - .boolean => "", - .string => |v| v, - .numeric => |v| numeric: { - var buf: [10]u8 = undefined; - const num_len = std.fmt.formatIntBuf(&buf, v, 10, .upper, .{}); - const final = buf; - break :numeric final[0..num_len]; + kvs[i] = .{ + cap.name, switch (cap.value) { + .canceled => @compileError("canceled not handled yet"), + .boolean => "", + .string => |v| string: { + @setEvalBranchQuota(100_000); + // If a string contains parameters, then we do not escape + // anything within the string. I BELIEVE the history here is + // xterm initially only supported specific capabilities and none + // had parameters so it returned the tcap encoded form. Later, + // Kitty added support for more capabilities some of which + // have parameters. But Kitty returned them in terminfo source + // format. So we need to handle both cases. + if (std.mem.indexOfScalar(u8, v, '%') != null) break :string v; + // No-parameters. Encode and return. + // First replace \E with the escape char (0x1B) + var result = comptimeReplace(v, "\\E", "\x1b"); + // Replace '^' with the control char version of that char. + while (std.mem.indexOfScalar(u8, result, '^')) |idx| { + if (idx > 0) @compileError("handle control-char in middle of string"); + const c = result[idx + 1]; + result = comptimeReplace(result, result[idx .. idx + 2], &.{c - 64}); + } + break :string result; + }, + .numeric => |v| numeric: { + var buf: [10]u8 = undefined; + const num_len = std.fmt.formatIntBuf(&buf, v, 10, .upper, .{}); + const final = buf; + break :numeric final[0..num_len]; + }, }, - } }; + }; } // Now go through and convert them all to hex-encoded strings. @@ -123,6 +145,22 @@ fn hexencode(comptime input: []const u8) []const u8 { return comptime &(std.fmt.bytesToHex(input, .upper)); } +/// std.mem.replace but comptime-only so we can return the string +/// allocated in comptime memory. +fn comptimeReplace( + input: []const u8, + needle: []const u8, + replacement: []const u8, +) []const u8 { + comptime { + const len = std.mem.replacementSize(u8, input, needle, replacement); + var buf: [len]u8 = undefined; + _ = std.mem.replace(u8, input, needle, replacement, &buf); + const final = buf; + return &final; + } +} + test "xtgettcap map" { const testing = std.testing; @@ -136,6 +174,8 @@ test "xtgettcap map" { .capabilities = &.{ .{ .name = "am", .value = .{ .boolean = {} } }, .{ .name = "colors", .value = .{ .numeric = 256 } }, + .{ .name = "kbs", .value = .{ .string = "^H" } }, + .{ .name = "kf1", .value = .{ .string = "\\EOP" } }, .{ .name = "Smulx", .value = .{ .string = "\\E[4:%p1%dm" } }, }, }; @@ -145,6 +185,14 @@ test "xtgettcap map" { "\x1bP1+r616D\x1b\\", map.get(hexencode("am")).?, ); + try testing.expectEqualStrings( + "\x1bP1+r6B6273=08\x1b\\", + map.get(hexencode("kbs")).?, + ); + try testing.expectEqualStrings( + "\x1bP1+r6B6631=1B4F50\x1b\\", + map.get(hexencode("kf1")).?, + ); try testing.expectEqualStrings( "\x1bP1+r536D756C78=5C455B343A25703125646D\x1b\\", map.get(hexencode("Smulx")).?, diff --git a/src/terminfo/ghostty.zig b/src/terminfo/ghostty.zig index f96154c9b..84059aae0 100644 --- a/src/terminfo/ghostty.zig +++ b/src/terminfo/ghostty.zig @@ -304,7 +304,7 @@ pub const ghostty: Source = .{ .{ .name = "kUP5", .value = .{ .string = "\\E[1;5A" } }, .{ .name = "kUP6", .value = .{ .string = "\\E[1;6A" } }, .{ .name = "kUP7", .value = .{ .string = "\\E[1;7A" } }, - .{ .name = "kbs", .value = .{ .string = "^?" } }, + .{ .name = "kbs", .value = .{ .string = "^H" } }, .{ .name = "kcbt", .value = .{ .string = "\\E[Z" } }, .{ .name = "kcub1", .value = .{ .string = "\\EOD" } }, .{ .name = "kcud1", .value = .{ .string = "\\EOB" } },