const std = @import("std"); const mem = std.mem; const fmt = std.fmt; const io = std.io; const Self = @This(); pub fn init(allocator: mem.Allocator) !Self { const result = try std.ChildProcess.run(.{ .allocator = allocator, .argv = &[_][]const u8{ "infocmp", "-x" }, }); defer allocator.free(result.stdout); defer allocator.free(result.stderr); return parse(allocator, result.stdout); } pub fn parse(allocator: mem.Allocator, entry: []const u8) !Self { var self = Self{ .allocator = allocator, .entry = try allocator.dupe(u8, entry), .names = undefined, .bools = std.StringHashMap(bool).init(allocator), .ints = std.StringHashMap(u32).init(allocator), .strings = std.StringHashMap([]const u8).init(allocator), }; var num_fields: usize = 0; var line_it = mem.splitScalar(u8, self.entry, '\n'); while (line_it.next()) |line| { if (mem.startsWith(u8, line, "#")) continue; var field_it = FieldIterator{ .buffer = line, .index = 0 }; while (field_it.next()) |field| : (num_fields += 1) { if (num_fields == 0) { self.names = field; continue; } // std.debug.print("'{s}' (len {d}): ", .{ arg, arg.len }); if (mem.indexOfScalar(u8, field, '=')) |idx| { // string try self.strings.put(field[0..idx], try unescapeString(allocator, field[idx + 1 ..])); // std.debug.print("string {s} {s}\n", .{ field[0..idx], field[idx + 1 ..] }); } else if (mem.indexOfScalar(u8, field, '#')) |idx| { // int try self.ints.put(field[0..idx], try fmt.parseInt(u32, field[idx + 1 ..], 0)); // std.debug.print("int {s} {s}\n", .{ field[0..idx], field[idx + 1 ..] }); } else { // bool // std.debug.print("bool {s}\n", .{field}); try self.bools.put(field, true); } } } // var it = self.bools.keyIterator(); // while (it.next()) |k| std.debug.print("{s}\n", .{k.*}); return self; } fn unescapeString(allocator: mem.Allocator, input: []const u8) ![]u8 { var output = try allocator.alloc(u8, input.len); errdefer allocator.free(output); var i: usize = 0; var slide: usize = 0; var size = input.len; while (slide < input.len) { if (startsWithNoEscape(input, slide, "\\E") or startsWithNoEscape(input, slide, "\\e")) { output[i] = '\x1b'; i += 1; slide += 2; size -= 1; } else if (input[slide] == '^' and (slide == 0 or input[slide - 1] != '\\') and std.ascii.isUpper(input[slide + 1])) { output[i] = input[slide + 1] - 64; // convert to control character i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\n")) { output[i] = '\n'; i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\l")) { output[i] = '\x0a'; i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\r")) { output[i] = '\r'; i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\t")) { output[i] = '\t'; i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\b")) { output[i] = '\x08'; i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\f")) { output[i] = '\x0c'; i += 1; slide += 2; size -= 1; } else if (startsWithNoEscape(input, slide, "\\s")) { output[i] = ' '; i += 1; slide += 2; size -= 1; } else { output[i] = input[slide]; i += 1; slide += 1; } } // TODO: now do the reverse and make a startsWithEscape // const in = try allocator.dupe(u8, output); // defer allocator.free(in); // i = 0; // slide = 0; // while (slide < in.len) { // if (in[slide] == '^' and (slide == 0 or in[slide - 1] != '\\') and std.ascii.isUpper(in[slide + 1])) { // output[i] = in[slide + 1] - 64; // convert to control character // i += 1; // slide += 2; // size -= 1; // } else { // output[i] = in[slide]; // i += 1; // slide += 1; // } // } return try allocator.realloc(output, size); } fn startsWithNoEscape(haystack: []const u8, index: usize, needle: []const u8) bool { return mem.startsWith(u8, haystack[index..], needle) and (index == 0 or haystack[index - 1] != '\\'); } pub fn deinit(self: *Self) void { self.bools.deinit(); self.ints.deinit(); var sit = self.strings.valueIterator(); while (sit.next()) |s| self.allocator.free(s.*); self.strings.deinit(); self.allocator.free(self.entry); } /// Iterates over terminfo fields, ignoring backslash-escaped commas /// NOTE: Does not follow full iterator pattern! const FieldIterator = struct { buffer: []const u8, index: ?usize, pub fn next(self: *FieldIterator) ?[]const u8 { const start = self.index orelse return null; const end = if (getEnd(self.buffer, start)) |delim_start| blk: { self.index = delim_start + 1; break :blk delim_start; } else blk: { self.index = null; break :blk self.buffer.len; }; const trimmed = mem.trim(u8, self.buffer[start..end], &std.ascii.whitespace); if (trimmed.len == 0) return null; return trimmed; } fn getEnd(buf: []const u8, start: usize) ?usize { var comma = mem.indexOfScalarPos(u8, buf, start, ',') orelse return null; while (buf[comma - 1] == '\\') comma = mem.indexOfScalarPos(u8, buf, comma + 1, ',') orelse return null; return comma; } }; allocator: mem.Allocator, entry: []u8, names: []const u8, bools: std.StringHashMap(bool), ints: std.StringHashMap(u32), strings: std.StringHashMap([]const u8), /// Writes the formatted sequence to a given writer. pub fn writeString(self: *Self, string: String, writer: anytype) !void { const output = self.strings.get(string.toCapName()); if (output) |o| try writer.writeAll(o); } pub const Bool = enum { auto_left_margin, auto_right_margin, back_color_erase, can_change, ceol_standout_glitch, col_addr_glitch, cpi_changes_res, cr_cancels_micro_mode, dest_tabs_magic_smso, eat_newline_glitch, erase_overstrike, generic_type, hard_copy, hard_cursor, has_meta_key, has_print_wheel, has_status_line, hue_lightness_saturation, insert_null_glitch, lpi_changes_res, memory_above, memory_below, move_insert_mode, move_standout_mode, needs_xon_xoff, no_esc_ctlc, no_pad_char, non_dest_scroll_region, non_rev_rmcup, over_strike, prtr_silent, row_addr_glitch, semi_auto_right_margin, status_line_esc_ok, tilde_glitch, transparent_underline, xon_xoff, pub fn toCapName(self: Bool) []const u8 { return switch (self) { .auto_left_margin => "bw", .auto_right_margin => "am", .back_color_erase => "bce", .can_change => "ccc", .ceol_standout_glitch => "xhp", .col_addr_glitch => "xhpa", .cpi_changes_res => "cpix", .cr_cancels_micro_mode => "crxm", .dest_tabs_magic_smso => "xt", .eat_newline_glitch => "xenl", .erase_overstrike => "eo", .generic_type => "gn", .hard_copy => "hc", .hard_cursor => "chts", .has_meta_key => "km", .has_print_wheel => "daisy", .has_status_line => "hs", .hue_lightness_saturation => "hls", .insert_null_glitch => "in", .lpi_changes_res => "lpix", .memory_above => "da", .memory_below => "db", .move_insert_mode => "mir", .move_standout_mode => "msgr", .needs_xon_xoff => "nxon", .no_esc_ctlc => "xsb", .no_pad_char => "npc", .non_dest_scroll_region => "ndscr", .non_rev_rmcup => "nrrmc", .over_strike => "os", .prtr_silent => "mc5i", .row_addr_glitch => "xvpa", .semi_auto_right_margin => "sam", .status_line_esc_ok => "eslok", .tilde_glitch => "hz", .transparent_underline => "ul", .xon_xoff => "xon", }; } }; pub const Int = enum { columns, init_tabs, label_height, label_width, lines, lines_of_memory, magic_cookie_glitch, max_attributes, max_colors, max_pairs, maximum_windows, no_color_video, num_labels, padding_baud_rate, virtual_terminal, width_status_line, }; pub const String = enum { acs_chars, back_tab, bell, carriage_return, change_char_pitch, change_line_pitch, change_res_horz, change_res_vert, change_scroll_region, char_padding, clear_all_tabs, clear_margins, clear_screen, clr_bol, clr_eol, clr_eos, column_address, command_character, create_window, cursor_address, cursor_down, cursor_home, cursor_invisible, cursor_left, cursor_mem_address, cursor_normal, cursor_right, cursor_to_ll, cursor_up, cursor_visible, define_char, delete_character, delete_line, dial_phone, dis_status_line, display_clock, down_half_line, ena_acs, enter_alt_charset_mode, enter_am_mode, enter_blink_mode, enter_bold_mode, enter_ca_mode, enter_delete_mode, enter_dim_mode, enter_doublewide_mode, enter_draft_quality, enter_insert_mode, enter_italics_mode, enter_leftward_mode, enter_micro_mode, enter_near_letter_quality, enter_normal_quality, enter_protected_mode, enter_reverse_mode, enter_secure_mode, enter_shadow_mode, enter_standout_mode, enter_subscript_mode, enter_superscript_mode, enter_underline_mode, enter_upward_mode, enter_xon_mode, erase_chars, exit_alt_charset_mode, exit_am_mode, exit_attribute_mode, exit_ca_mode, exit_delete_mode, exit_doublewide_mode, exit_insert_mode, exit_italics_mode, exit_leftward_mode, exit_micro_mode, exit_shadow_mode, exit_standout_mode, exit_subscript_mode, exit_superscript_mode, exit_underline_mode, exit_upward_mode, exit_xon_mode, fixed_pause, flash_hook, flash_screen, form_feed, from_status_line, goto_window, hangup, init_1string, init_2string, init_3string, init_file, init_prog, initialize_color, initialize_pair, insert_character, insert_line, insert_padding, // TODO: keys micro_column_address, micro_down, micro_left, micro_right, micro_row_address, micro_up, newline, order_of_pins, orig_colors, orig_pair, pad_char, parm_dch, parm_delete_line, parm_down_cursor, parm_down_micro, parm_ich, parm_index, parm_insert_line, parm_left_cursor, parm_left_micro, parm_right_cursor, parm_right_micro, parm_rindex, parm_up_cursor, parm_up_micro, pkey_key, pkey_local, pkey_xmit, plab_norm, print_screen, prtr_non, prtr_off, prtr_on, pulse, quick_dial, remove_clock, repeat_char, req_for_input, reset_1string, reset_2string, reset_3string, reset_file, restore_cursor, row_address, save_cursor, scroll_forward, scroll_reverse, select_char_set, set_attributes, set_background, set_bottom_margin, set_bottom_margin_parm, set_clock, set_color_pair, set_foreground, set_left_margin, set_left_margin_parm, set_right_margin, set_right_margin_parm, set_tab, set_top_margin, set_top_margin_parm, set_window, start_bit_image, start_char_set_def, stop_bit_image, stop_char_set_def, subscript_characters, superscript_characters, tab, these_cause_cr, to_status_line, tone, underline_char, up_half_line, user0, user1, user2, user3, user4, user5, user6, user7, user8, user9, wait_tone, xoff_character, xon_character, zero_motion, pub fn toCapName(self: String) []const u8 { return switch (self) { .acs_chars => "acsc", .back_tab => "cbt", .bell => "bel", .carriage_return => "cr", .change_char_pitch => "cpi", .change_line_pitch => "lpi", .change_res_horz => "chr", .change_res_vert => "cvr", .change_scroll_region => "csr", .char_padding => "rmp", .clear_all_tabs => "tbc", .clear_margins => "mgc", .clear_screen => "clear", .clr_bol => "el1", .clr_eol => "el", .clr_eos => "ed", .column_address => "hpa", .command_character => "cmdch", .create_window => "cwin", .cursor_address => "cup", .cursor_down => "cud1", .cursor_home => "home", .cursor_invisible => "civis", .cursor_left => "cub1", .cursor_mem_address => "mrcup", .cursor_normal => "cnorm", .cursor_right => "cuf1", .cursor_to_ll => "ll", .cursor_up => "cuu1", .cursor_visible => "cvvis", .define_char => "defc", .delete_character => "dch1", .delete_line => "dl1", .dial_phone => "dial", .dis_status_line => "dsl", .display_clock => "dclk", .down_half_line => "hd", .ena_acs => "enacs", .enter_alt_charset_mode => "smacs", .enter_am_mode => "smam", .enter_blink_mode => "blink", .enter_bold_mode => "bold", .enter_ca_mode => "smcup", .enter_delete_mode => "smdc", .enter_dim_mode => "dim", .enter_doublewide_mode => "swidm", .enter_draft_quality => "sdrfq", .enter_insert_mode => "smir", .enter_italics_mode => "sitm", .enter_leftward_mode => "slm", .enter_micro_mode => "smicm", .enter_near_letter_quality => "snlq", .enter_normal_quality => "snrmq", .enter_protected_mode => "prot", .enter_reverse_mode => "rev", .enter_secure_mode => "invis", .enter_shadow_mode => "sshm", .enter_standout_mode => "smso", .enter_subscript_mode => "ssub,", .enter_superscript_mode => "ssupm", .enter_underline_mode => "smul", .enter_upward_mode => "sum", .enter_xon_mode => "smxon", .erase_chars => "ech", .exit_alt_charset_mode => "rmacs", .exit_am_mode => "rmam", .exit_attribute_mode => "sgr0", .exit_ca_mode => "rmcup", .exit_delete_mode => "rmdc", .exit_doublewide_mode => "rwidm", .exit_insert_mode => "rmir", .exit_italics_mode => "ritm", .exit_leftward_mode => "rlm", .exit_micro_mode => "rmicm", .exit_shadow_mode => "rshm", .exit_standout_mode => "rmso", .exit_subscript_mode => "rsubm", .exit_superscript_mode => "rsupm", .exit_underline_mode => "rmul", .exit_upward_mode => "rum", .exit_xon_mode => "rmxon", .fixed_pause => "pause", .flash_hook => "hook", .flash_screen => "flash", .form_feed => "ff", .from_status_line => "fsl", .goto_window => "wingo", .hangup => "hup", .init_1string => "is1", .init_2string => "is2", .init_3string => "is3", .init_file => "if", .init_prog => "iprog", .initialize_color => "initc", .initialize_pair => "initp", .insert_character => "ich1", .insert_line => "il1", .insert_padding => "ip", // keys .micro_column_address => "mhpa", .micro_down => "mcud1", .micro_left => "mcub1", .micro_right => "mcuf1", .micro_row_address => "mvpa", .micro_up => "mcuu1", .newline => "nel", .order_of_pins => "porder", .orig_colors => "oc", .orig_pair => "op", .pad_char => "pad", .parm_dch => "dch", .parm_delete_line => "dl", .parm_down_cursor => "cud", .parm_down_micro => "mcud", .parm_ich => "ich", .parm_index => "indn", .parm_insert_line => "il", .parm_left_cursor => "cub", .parm_left_micro => "mcub", .parm_right_cursor => "cuf", .parm_right_micro => "mcuf", .parm_rindex => "rin", .parm_up_cursor => "cuu", .parm_up_micro => "mcuu", .pkey_key => "pfkey", .pkey_local => "pfloc", .pkey_xmit => "pfx", .plab_norm => "pln", .print_screen => "mc0", .prtr_non => "mc5p", .prtr_off => "mc4", .prtr_on => "mc5", .pulse => "pulse", .quick_dial => "qdial", .remove_clock => "rmclk", .repeat_char => "rep", .req_for_input => "rfi", .reset_1string => "rs1", .reset_2string => "rs2", .reset_3string => "rs3", .reset_file => "rf", .restore_cursor => "rc", .row_address => "vpa", .save_cursor => "sc", .scroll_forward => "ind", .scroll_reverse => "ri", .select_char_set => "scs", .set_attributes => "sgr", .set_background => "setb", .set_bottom_margin => "smgb", .set_bottom_margin_parm => "smgbp", .set_clock => "sclk", .set_color_pair => "scp", .set_foreground => "setf", .set_left_margin => "smgl", .set_left_margin_parm => "smglp", .set_right_margin => "smgr", .set_right_margin_parm => "smgrp", .set_tab => "hts", .set_top_margin => "smgt", .set_top_margin_parm => "smgtp", .set_window => "wind", .start_bit_image => "sbim", .start_char_set_def => "scsd", .stop_bit_image => "rbim", .stop_char_set_def => "rcsd", .subscript_characters => "subcs", .superscript_characters => "supcs", .tab => "ht", .these_cause_cr => "docr", .to_status_line => "tsl", .tone => "tone", .underline_char => "uc", .up_half_line => "hu", .user0 => "u0", .user1 => "u1", .user2 => "u2", .user3 => "u3", .user4 => "u4", .user5 => "u5", .user6 => "u6", .user7 => "u7", .user8 => "u8", .user9 => "u9", .wait_tone => "wait", .xoff_character => "xoffc", .xon_character => "xonc", .zero_motion => "zerom", // else => "", }; } }; test "parse terminfo" { var terminfo = try parse(std.testing.allocator, \\# Reconstructed via infocmp from file: /nix/store/c1d0bgq6whz4khqxncmqikpdsxmr1szw-kitty-0.32.2/lib/kitty/terminfo/x/xterm-kitty \\xterm-kitty|KovIdTTY, \\ am, ccc, hs, km, mc5i, mir, msgr, npc, xenl, \\ colors#0x100, cols#80, it#8, lines#24, pairs#0x7fff, \\ acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, \\ bel=^G, bold=\E[1m, cbt=\E[Z, civis=\E[?25l, \\ clear=\E[H\E[2J, cnorm=\E[?12h\E[?25h, cr=\r, \\ csr=\E[%i%p1%d;%p2%dr, cub=\E[%p1%dD, cub1=^H, \\ cud=\E[%p1%dB, cud1=\n, cuf=\E[%p1%dC, cuf1=\E[C, \\ cup=\E[%i%p1%d;%p2%dH, cuu=\E[%p1%dA, cuu1=\E[A, \\ cvvis=\E[?12;25h, dch=\E[%p1%dP, dch1=\E[P, dim=\E[2m, \\ dl=\E[%p1%dM, dl1=\E[M, dsl=\E]2;\E\\, ech=\E[%p1%dX, \\ ed=\E[J, el=\E[K, el1=\E[1K, flash=\E[?5h$<100/>\E[?5l, \\ fsl=^G, home=\E[H, hpa=\E[%i%p1%dG, ht=^I, hts=\EH, \\ ich=\E[%p1%d@, il=\E[%p1%dL, il1=\E[L, ind=\n, \\ indn=\E[%p1%dS, \\ initc=\E]4;%p1%d;rgb:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, \\ kBEG=\E[1;2E, kDC=\E[3;2~, kEND=\E[1;2F, kHOM=\E[1;2H, \\ kIC=\E[2;2~, kLFT=\E[1;2D, kNXT=\E[6;2~, kPRV=\E[5;2~, \\ kRIT=\E[1;2C, ka1=, ka3=, kbeg=\EOE, kbs=^?, kc1=, kc3=, \\ kcbt=\E[Z, kcub1=\EOD, kcud1=\EOB, kcuf1=\EOC, kcuu1=\EOA, \\ kdch1=\E[3~, kend=\EOF, kf1=\EOP, kf10=\E[21~, kf11=\E[23~, \\ kf12=\E[24~, kf13=\E[1;2P, kf14=\E[1;2Q, kf15=\E[13;2~, \\ kf16=\E[1;2S, kf17=\E[15;2~, kf18=\E[17;2~, \\ kf19=\E[18;2~, kf2=\EOQ, kf20=\E[19;2~, kf21=\E[20;2~, \\ kf22=\E[21;2~, kf23=\E[23;2~, kf24=\E[24;2~, \\ kf25=\E[1;5P, kf26=\E[1;5Q, kf27=\E[13;5~, kf28=\E[1;5S, \\ kf29=\E[15;5~, kf3=\EOR, kf30=\E[17;5~, kf31=\E[18;5~, \\ kf32=\E[19;5~, kf33=\E[20;5~, kf34=\E[21;5~, \\ kf35=\E[23;5~, kf36=\E[24;5~, kf37=\E[1;6P, kf38=\E[1;6Q, \\ kf39=\E[13;6~, kf4=\EOS, kf40=\E[1;6S, kf41=\E[15;6~, \\ kf42=\E[17;6~, kf43=\E[18;6~, kf44=\E[19;6~, \\ kf45=\E[20;6~, kf46=\E[21;6~, kf47=\E[23;6~, \\ kf48=\E[24;6~, kf49=\E[1;3P, kf5=\E[15~, kf50=\E[1;3Q, \\ kf51=\E[13;3~, kf52=\E[1;3S, kf53=\E[15;3~, \\ kf54=\E[17;3~, kf55=\E[18;3~, kf56=\E[19;3~, \\ kf57=\E[20;3~, kf58=\E[21;3~, kf59=\E[23;3~, kf6=\E[17~, \\ kf60=\E[24;3~, kf61=\E[1;4P, kf62=\E[1;4Q, kf63=\E[13;4~, \\ kf7=\E[18~, kf8=\E[19~, kf9=\E[20~, khlp=, khome=\EOH, \\ kich1=\E[2~, kind=\E[1;2B, kmous=\E[M, knp=\E[6~, \\ kpp=\E[5~, kri=\E[1;2A, kund=, oc=\E]104\007, op=\E[39;49m, \\ rc=\E8, rep=%p1%c\E[%p2%{1}%-%db, rev=\E[7m, ri=\EM, \\ rin=\E[%p1%dT, ritm=\E[23m, rmacs=\E(B, rmam=\E[?7l, \\ rmcup=\E[?1049l, rmir=\E[4l, rmkx=\E[?1l, rmso=\E[27m, \\ rmul=\E[24m, rs1=\E]\E\\\Ec, sc=\E7, \\ setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, \\ setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, \\ sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, \\ sgr0=\E(B\E[m, sitm=\E[3m, smacs=\E(0, smam=\E[?7h, \\ smcup=\E[?1049h, smir=\E[4h, smkx=\E[?1h, smso=\E[7m, \\ smul=\E[4m, tbc=\E[3g, tsl=\E]2;, u6=\E[%i%d;%dR, u7=\E[6n, \\ u8=\E[?%[;0123456789]c, u9=\E[c, vpa=\E[%i%p1%dd, ); defer terminfo.deinit(); }