mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
245 lines
8.7 KiB
Zig
245 lines
8.7 KiB
Zig
//! Terminfo source format. This can be used to encode terminfo files.
|
|
//! This cannot parse terminfo source files yet because it isn't something
|
|
//! I need to do but this can be added later.
|
|
//!
|
|
//! Background: https://invisible-island.net/ncurses/man/terminfo.5.html
|
|
|
|
const Source = @This();
|
|
|
|
const std = @import("std");
|
|
|
|
/// The set of names for the terminal. These match the TERM environment variable
|
|
/// and are used to look up this terminal. Historically, the final name in the
|
|
/// list was the most common name for the terminal and contains spaces and
|
|
/// other characters. See terminfo(5) for details.
|
|
names: []const []const u8,
|
|
|
|
/// The set of capabilities in this terminfo file.
|
|
capabilities: []const Capability,
|
|
|
|
/// A capability in a terminfo file. This also includes any "use" capabilities
|
|
/// since they behave just like other capabilities as documented in terminfo(5).
|
|
pub const Capability = struct {
|
|
/// The name of capability. This is the "Cap-name" value in terminfo(5).
|
|
name: []const u8,
|
|
value: Value,
|
|
|
|
pub const Value = union(enum) {
|
|
/// Canceled value, i.e. suffixed with @
|
|
canceled: void,
|
|
|
|
/// Boolean values are always true if they exist so there is no value.
|
|
boolean: void,
|
|
|
|
/// Numeric values are always "unsigned decimal integers". The size
|
|
/// of the integer is unspecified in terminfo(5). I chose 32-bits
|
|
/// because it is a common integer size but this may be wrong.
|
|
numeric: u32,
|
|
|
|
string: []const u8,
|
|
};
|
|
};
|
|
|
|
/// Encode as a terminfo source file. The encoding is always done in a
|
|
/// human-readable format with whitespace. Fields are always written in the
|
|
/// order of the slices on this struct; this will not do any reordering.
|
|
pub fn encode(self: Source, writer: anytype) !void {
|
|
// Encode the names in the order specified
|
|
for (self.names, 0..) |name, i| {
|
|
if (i != 0) try writer.writeAll("|");
|
|
try writer.writeAll(name);
|
|
}
|
|
try writer.writeAll(",\n");
|
|
|
|
// Encode each of the capabilities in the order specified
|
|
for (self.capabilities) |cap| {
|
|
try writer.writeAll("\t");
|
|
try writer.writeAll(cap.name);
|
|
switch (cap.value) {
|
|
.canceled => try writer.writeAll("@"),
|
|
.boolean => {},
|
|
.numeric => |v| try writer.print("#{d}", .{v}),
|
|
.string => |v| try writer.print("={s}", .{v}),
|
|
}
|
|
try writer.writeAll(",\n");
|
|
}
|
|
}
|
|
|
|
/// Returns a ComptimeStringMap for all of the capabilities in this terminfo.
|
|
/// The value is the value that should be sent as a response to XTGETTCAP.
|
|
/// Important: the value is the FULL response included the escape sequences.
|
|
pub fn xtgettcapMap(comptime self: Source) type {
|
|
const KV = struct { []const u8, []const u8 };
|
|
|
|
// We have all of our capabilities plus To, TN, and RGB which aren't
|
|
// in the capabilities list but are query-able.
|
|
const len = self.capabilities.len + 3;
|
|
var kvs: [len]KV = .{.{ "", "" }} ** len;
|
|
|
|
// We first build all of our entries with raw K=V pairs.
|
|
kvs[0] = .{ "TN", self.names[0] };
|
|
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| 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 replacement = switch (result[idx + 1]) {
|
|
'?' => 0x7F, // DEL, special cased from ncurses
|
|
else => |c| c - 64,
|
|
};
|
|
result = comptimeReplace(
|
|
result,
|
|
result[idx .. idx + 2],
|
|
&.{replacement},
|
|
);
|
|
}
|
|
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.
|
|
for (&kvs) |*entry| {
|
|
// The key is just the raw hex-encoded string
|
|
entry[0] = hexencode(entry[0]);
|
|
|
|
// The value is more complex
|
|
var buf: [5 + entry[0].len + 1 + (entry[1].len * 2) + 2]u8 = undefined;
|
|
const out = if (std.mem.eql(u8, entry[1], "")) std.fmt.bufPrint(
|
|
&buf,
|
|
"\x1bP1+r{s}\x1b\\",
|
|
.{entry[0]}, // important: hex-encoded name
|
|
) catch unreachable else std.fmt.bufPrint(
|
|
&buf,
|
|
"\x1bP1+r{s}={s}\x1b\\",
|
|
.{ entry[0], hexencode(entry[1]) }, // important: hex-encoded name
|
|
) catch unreachable;
|
|
|
|
const final = buf;
|
|
entry[1] = final[0..out.len];
|
|
}
|
|
|
|
const kvs_final = kvs;
|
|
return std.ComptimeStringMap([]const u8, &kvs_final);
|
|
}
|
|
|
|
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;
|
|
|
|
const src: Source = .{
|
|
.names = &.{
|
|
"ghostty",
|
|
"xterm-ghostty",
|
|
"Ghostty",
|
|
},
|
|
|
|
.capabilities = &.{
|
|
.{ .name = "am", .value = .{ .boolean = {} } },
|
|
.{ .name = "colors", .value = .{ .numeric = 256 } },
|
|
.{ .name = "kx", .value = .{ .string = "^?" } },
|
|
.{ .name = "kbs", .value = .{ .string = "^H" } },
|
|
.{ .name = "kf1", .value = .{ .string = "\\EOP" } },
|
|
.{ .name = "Smulx", .value = .{ .string = "\\E[4:%p1%dm" } },
|
|
},
|
|
};
|
|
|
|
const map = comptime src.xtgettcapMap();
|
|
try testing.expectEqualStrings(
|
|
"\x1bP1+r616D\x1b\\",
|
|
map.get(hexencode("am")).?,
|
|
);
|
|
try testing.expectEqualStrings(
|
|
"\x1bP1+r6B78=7F\x1b\\",
|
|
map.get(hexencode("kx")).?,
|
|
);
|
|
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")).?,
|
|
);
|
|
}
|
|
|
|
test "encode" {
|
|
const src: Source = .{
|
|
.names = &.{
|
|
"ghostty",
|
|
"xterm-ghostty",
|
|
"Ghostty",
|
|
},
|
|
|
|
.capabilities = &.{
|
|
.{ .name = "am", .value = .{ .boolean = {} } },
|
|
.{ .name = "ccc", .value = .{ .canceled = {} } },
|
|
.{ .name = "colors", .value = .{ .numeric = 256 } },
|
|
.{ .name = "bel", .value = .{ .string = "^G" } },
|
|
},
|
|
};
|
|
|
|
// Encode
|
|
var buf: [1024]u8 = undefined;
|
|
var buf_stream = std.io.fixedBufferStream(&buf);
|
|
try src.encode(buf_stream.writer());
|
|
|
|
const expected =
|
|
\\ghostty|xterm-ghostty|Ghostty,
|
|
\\ am,
|
|
\\ ccc@,
|
|
\\ colors#256,
|
|
\\ bel=^G,
|
|
\\
|
|
;
|
|
try std.testing.expectEqualStrings(@as([]const u8, expected), buf_stream.getWritten());
|
|
}
|