This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243//! 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 StaticStringMap 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) std.StaticStringMap([]const u8) { 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 = @splat(.{ "", "" });
// 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.StaticStringMap([]const u8).initComptime(&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,\n" ++ "\tam,\n" ++ "\tccc@,\n" ++ "\tcolors#256,\n" ++ "\tbel=^G,\n"; try std.testing.expectEqualStrings(@as([]const u8, expected), buf_stream.getWritten());}