Something went wrong. Try again.
A GPU terminal emulator for Wayland and Windows on libghostty-vt, drawing with Vulkan, running a program on a pty or a serial port.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Keys from the platform, in the terms libghostty-vt's encoder wants.//!//! The encoder needs the physical key — for the kitty keyboard protocol,//! and for keys that type nothing — as well as the text, so a platform's//! key numbers are mapped to `vt.input.Key` here. They are evdev codes,//! from `linux/input-event-codes.h`: Linux's own, and on Windows the//! scancode made one, which for most keys it already is.
const std = @import("std");const vt = @import("ghostty-vt");
const ev = @import("aerie").platform.event;
const Key = vt.input.Key;
/// The physical key an evdev code names.pub fn fromEvdev(code: u32) Key { return switch (code) { 1 => .escape, 2 => .digit_1, 3 => .digit_2, 4 => .digit_3, 5 => .digit_4, 6 => .digit_5, 7 => .digit_6, 8 => .digit_7, 9 => .digit_8, 10 => .digit_9, 11 => .digit_0, 12 => .minus, 13 => .equal, 14 => .backspace, 15 => .tab, 16 => .key_q, 17 => .key_w, 18 => .key_e, 19 => .key_r, 20 => .key_t, 21 => .key_y, 22 => .key_u, 23 => .key_i, 24 => .key_o, 25 => .key_p, 26 => .bracket_left, 27 => .bracket_right, 28 => .enter, 29 => .control_left, 30 => .key_a, 31 => .key_s, 32 => .key_d, 33 => .key_f, 34 => .key_g, 35 => .key_h, 36 => .key_j, 37 => .key_k, 38 => .key_l, 39 => .semicolon, 40 => .quote, 41 => .backquote, 42 => .shift_left, 43 => .backslash, 44 => .key_z, 45 => .key_x, 46 => .key_c, 47 => .key_v, 48 => .key_b, 49 => .key_n, 50 => .key_m, 51 => .comma, 52 => .period, 53 => .slash, 54 => .shift_right, 55 => .numpad_multiply, 56 => .alt_left, 57 => .space, 58 => .caps_lock, 59 => .f1, 60 => .f2, 61 => .f3, 62 => .f4, 63 => .f5, 64 => .f6, 65 => .f7, 66 => .f8, 67 => .f9, 68 => .f10, 69 => .num_lock, 70 => .scroll_lock, 71 => .numpad_7, 72 => .numpad_8, 73 => .numpad_9, 74 => .numpad_subtract, 75 => .numpad_4, 76 => .numpad_5, 77 => .numpad_6, 78 => .numpad_add, 79 => .numpad_1, 80 => .numpad_2, 81 => .numpad_3, 82 => .numpad_0, 83 => .numpad_decimal, 86 => .intl_backslash, 87 => .f11, 88 => .f12, 89 => .intl_ro, 92 => .convert, 93 => .kana_mode, 94 => .non_convert, 96 => .numpad_enter, 97 => .control_right, 98 => .numpad_divide, 99 => .print_screen, 100 => .alt_right, 102 => .home, 103 => .arrow_up, 104 => .page_up, 105 => .arrow_left, 106 => .arrow_right, 107 => .end, 108 => .arrow_down, 109 => .page_down, 110 => .insert, 111 => .delete, 113 => .audio_volume_mute, 114 => .audio_volume_down, 115 => .audio_volume_up, 116 => .power, 117 => .numpad_equal, 119 => .pause, 121 => .numpad_comma, 124 => .intl_yen, 125 => .meta_left, 126 => .meta_right, 127 => .context_menu, 138 => .help, 142 => .sleep, 143 => .wake_up, 158 => .browser_back, 159 => .browser_forward, 163 => .media_track_next, 164 => .media_play_pause, 165 => .media_track_previous, 166 => .media_stop, 172 => .browser_home, 173 => .browser_refresh, 183 => .f13, 184 => .f14, 185 => .f15, 186 => .f16, 187 => .f17, 188 => .f18, 189 => .f19, 190 => .f20, 191 => .f21, 192 => .f22, 193 => .f23, 194 => .f24, 217 => .browser_search, else => .unidentified, };}
/// The key a keysym names, for the keys that type nothing or type the same/// on every layout. Keysyms first, because a virtual keyboard — or a/// remapped one — puts keys at codes that mean nothing physical.pub fn fromKeysym(sym: u32) ?Key { return switch (sym) { 0x20 => .space, 0xff08 => .backspace, 0xff09, 0xfe20 => .tab, 0xff0d => .enter, 0xff13 => .pause, 0xff14 => .scroll_lock, 0xff1b => .escape, 0xff50 => .home, 0xff51 => .arrow_left, 0xff52 => .arrow_up, 0xff53 => .arrow_right, 0xff54 => .arrow_down, 0xff55 => .page_up, 0xff56 => .page_down, 0xff57 => .end, 0xff61 => .print_screen, 0xff63 => .insert, 0xff67 => .context_menu, 0xff7f => .num_lock, 0xff8d => .numpad_enter, 0xff95 => .numpad_home, 0xff96 => .numpad_left, 0xff97 => .numpad_up, 0xff98 => .numpad_right, 0xff99 => .numpad_down, 0xff9a => .numpad_page_up, 0xff9b => .numpad_page_down, 0xff9c => .numpad_end, 0xff9d => .numpad_begin, 0xff9e => .numpad_insert, 0xff9f => .numpad_delete, 0xffaa => .numpad_multiply, 0xffab => .numpad_add, 0xffac => .numpad_separator, 0xffad => .numpad_subtract, 0xffae => .numpad_decimal, 0xffaf => .numpad_divide, 0xffb0...0xffb9 => @fromBackingInt(@intCast(@backingInt(Key.numpad_0) + @as(c_int, @intCast(sym - 0xffb0)))), 0xffbd => .numpad_equal, 0xffbe...0xffd5 => @fromBackingInt(@intCast(@backingInt(Key.f1) + @as(c_int, @intCast(sym - 0xffbe)))), 0xffe1 => .shift_left, 0xffe2 => .shift_right, 0xffe3 => .control_left, 0xffe4 => .control_right, 0xffe5 => .caps_lock, 0xffe7, 0xffeb => .meta_left, 0xffe8, 0xffec => .meta_right, 0xffe9 => .alt_left, 0xffea => .alt_right, 0xffff => .delete, else => null, };}
/// The key for a character the key types unshifted: the letters, the/// digits and the punctuation of a US layout.fn fromUnshifted(cp: u21) ?Key { if (cp >= 0x80) return null; return Key.fromASCII(std.ascii.toLower(@intCast(cp)));}
pub fn mods(m: ev.Mods) vt.input.KeyMods { return .{ .shift = m.shift, .ctrl = m.ctrl, .alt = m.alt, .super = m.super, .caps_lock = m.caps_lock, .num_lock = m.num_lock, };}
/// The encoder's view of a platform key event. `text` is borrowed.pub fn event(k: ev.Key) vt.input.KeyEvent { // Control characters are what the encoder makes from the key and the // modifiers; handed one as text, it would send it as it is and lose the // modifiers. const text = if (k.text.len == 1 and (k.text[0] < 0x20 or k.text[0] == 0x7f)) "" else k.text; return .{ .action = switch (k.action) { .press => .press, .repeat => .repeat, .release => .release, }, .key = physical(k), .mods = mods(k.mods), .consumed_mods = mods(k.consumed), .composing = k.composing, .utf8 = text, .unshifted_codepoint = k.unshifted, };}
/// The key, from its keysym, from what it types, or from where it is./// The keysym a key is bound by, for a global hotkey: the inverse of/// `fromKeysym`, and the unshifted character of a letter, a digit or a/// punctuation key. Null for a key with none.pub fn toKeysym(key: Key) ?u32 { const name = @tagName(key); if (std.mem.startsWith(u8, name, "key_") and name.len == 5) return name[4]; if (std.mem.startsWith(u8, name, "digit_") and name.len == 7) return name[6]; if (fromUnshiftedChar(key)) |c| return c; var sym: u32 = 0xff00; while (sym <= 0xffff) : (sym += 1) if (fromKeysym(sym) == key) return sym; sym = 0xfe00; while (sym < 0xff00) : (sym += 1) if (fromKeysym(sym) == key) return sym; if (fromKeysym(0x20) == key) return 0x20; return null;}
/// The character a punctuation key types unshifted, on a US layout.fn fromUnshiftedChar(key: Key) ?u32 { return switch (key) { .backquote => '`', .minus => '-', .equal => '=', .bracket_left => '[', .bracket_right => ']', .backslash => '\\', .semicolon => ';', .quote => '\'', .comma => ',', .period => '.', .slash => '/', else => null, };}
pub fn physical(k: ev.Key) Key { if (fromKeysym(k.keysym)) |key| return key; if (fromUnshifted(k.unshifted)) |key| return key; return fromEvdev(k.code);}
/// What the key sends to the program, written to `w`, under the modes the/// terminal is in now. Called with the terminal's lock held.pub fn encode(t: *vt.Terminal, k: ev.Key, w: *std.Io.Writer) std.Io.Writer.Error!void { try vt.input.encodeKey(w, event(k), .fromTerminal(t));}
test "a keysym wins over a code that means nothing" { // A virtual keyboard's Return at evdev 1, which is Escape. const k: ev.Key = .{ .action = .press, .code = 1, .keysym = 0xff0d, .text = "\r", .mods = .{}, .consumed = .{}, .unshifted = '\r', .composing = false }; try std.testing.expectEqual(Key.enter, physical(k)); try std.testing.expectEqual(Key.f5, fromKeysym(0xffc2).?); try std.testing.expectEqual(Key.numpad_7, fromKeysym(0xffb7).?);}
test "a key's keysym, for a global hotkey" { try std.testing.expectEqual(@as(?u32, 0xffc2), toKeysym(.f5)); try std.testing.expectEqual(@as(?u32, 0xff55), toKeysym(.page_up)); try std.testing.expectEqual(@as(?u32, 0xff09), toKeysym(.tab)); try std.testing.expectEqual(@as(?u32, 0x20), toKeysym(.space)); try std.testing.expectEqual(@as(?u32, 'a'), toKeysym(.key_a)); try std.testing.expectEqual(@as(?u32, '7'), toKeysym(.digit_7)); try std.testing.expectEqual(@as(?u32, '`'), toKeysym(.backquote));}
test "letters and arrows" { try std.testing.expectEqual(Key.key_a, fromEvdev(30)); try std.testing.expectEqual(Key.arrow_up, fromEvdev(103)); try std.testing.expectEqual(Key.unidentified, fromEvdev(9999));}
test "a plain letter types itself, and ctrl+c is ETX" { var t: vt.Terminal = try .init(std.testing.io, std.testing.allocator, .{ .cols = 10, .rows = 2 }); defer t.deinit(std.testing.allocator); var buf: [32]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf); try encode(&t, .{ .action = .press, .code = 30, .keysym = 'a', .text = "a", .mods = .{}, .consumed = .{}, .unshifted = 'a', .composing = false }, &w); try std.testing.expectEqualStrings("a", w.buffered());
w = .fixed(&buf); try encode(&t, .{ .action = .press, .code = 46, .keysym = 'c', .text = "\x03", .mods = .{ .ctrl = true }, .consumed = .{}, .unshifted = 'c', .composing = false }, &w); try std.testing.expectEqualStrings("\x03", w.buffered());
w = .fixed(&buf); try encode(&t, .{ .action = .press, .code = 103, .keysym = 0xff52, .text = "", .mods = .{}, .consumed = .{}, .unshifted = 0, .composing = false }, &w); try std.testing.expectEqualStrings("\x1b[A", w.buffered());}