const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const cimgui = @import("cimgui"); const config = @import("../config.zig"); /// A generic key input event. This is the information that is necessary /// regardless of apprt in order to generate the proper terminal /// control sequences for a given key press. /// /// Some apprts may not be able to provide all of this information, such /// as GLFW. In this case, the apprt should provide as much information /// as it can and it should be expected that the terminal behavior /// will not be totally correct. pub const KeyEvent = struct { /// The action: press, release, etc. action: Action = .press, /// The keycode of the physical key that was pressed. This is agnostic /// to the layout. Layout-dependent matching can only be done via the /// UTF-8 or unshifted codepoint. key: Key = .unidentified, /// Mods are the modifiers that are pressed. mods: Mods = .{}, /// The mods that were consumed in order to generate the text /// in utf8. This has the mods set that were consumed, so to /// get the set of mods that are effective you must negate /// mods with this. /// /// This field is meaningless if utf8 is empty. consumed_mods: Mods = .{}, /// Composing is true when this key event is part of a dead key /// composition sequence and we're in the middle of it. composing: bool = false, /// The utf8 sequence that was generated by this key event. /// This will be an empty string if there is no text generated. /// If composing is true and this is non-empty, this is preedit /// text. utf8: []const u8 = "", /// The codepoint for this key when it is unshifted. For example, /// shift+a is "A" in UTF-8 but unshifted would provide 'a'. unshifted_codepoint: u21 = 0, /// Returns the effective modifiers for this event. The effective /// modifiers are the mods that should be considered for keybindings. pub fn effectiveMods(self: KeyEvent) Mods { if (self.utf8.len == 0) return self.mods; return self.mods.unset(self.consumed_mods); } /// Returns a unique hash for this key event to be used for tracking /// uniquess specifically with bindings. This omits fields that are /// irrelevant for bindings. pub fn bindingHash(self: KeyEvent) u64 { var hasher = std.hash.Wyhash.init(0); // These are all the fields that are explicitly part of Trigger. std.hash.autoHash(&hasher, self.key); std.hash.autoHash(&hasher, self.unshifted_codepoint); std.hash.autoHash(&hasher, self.mods.binding()); // Notes on unmapped things and why: // // - action: we don't have action-specific bindings right now // AND we want to know if a key resulted in a binding regardless // of action because a press should also ignore a release and so on. // // We can add to this if there is other confusion. return hasher.final(); } }; /// A bitmask for all key modifiers. /// /// IMPORTANT: Any changes here update include/ghostty.h pub const Mods = packed struct(Mods.Backing) { pub const Backing = u16; shift: bool = false, ctrl: bool = false, alt: bool = false, super: bool = false, caps_lock: bool = false, num_lock: bool = false, sides: side = .{}, _padding: u6 = 0, /// Tracks the side that is active for any given modifier. Note /// that this doesn't confirm a modifier is pressed; you must check /// the bool for that in addition to this. /// /// Not all platforms support this, check apprt for more info. pub const side = packed struct(u4) { shift: Side = .left, ctrl: Side = .left, alt: Side = .left, super: Side = .left, }; pub const Side = enum(u1) { left, right }; /// Integer value of this struct. pub fn int(self: Mods) Backing { return @bitCast(self); } /// Returns true if no modifiers are set. pub fn empty(self: Mods) bool { return self.int() == 0; } /// Returns true if two mods are equal. pub fn equal(self: Mods, other: Mods) bool { return self.int() == other.int(); } /// Return mods that are only relevant for bindings. pub fn binding(self: Mods) Mods { return .{ .shift = self.shift, .ctrl = self.ctrl, .alt = self.alt, .super = self.super, }; } /// Perform `self &~ other` to remove the other mods from self. pub fn unset(self: Mods, other: Mods) Mods { return @bitCast(self.int() & ~other.int()); } /// Returns the mods without locks set. pub fn withoutLocks(self: Mods) Mods { var copy = self; copy.caps_lock = false; copy.num_lock = false; return copy; } /// Return the mods to use for key translation. This handles settings /// like macos-option-as-alt. The translation mods should be used for /// translation but never sent back in for the key callback. pub fn translation(self: Mods, option_as_alt: config.OptionAsAlt) Mods { var result = self; // macos-option-as-alt for darwin if (comptime builtin.target.os.tag.isDarwin()) alt: { // Alt has to be set only on the correct side switch (option_as_alt) { .false => break :alt, .true => {}, .left => if (self.sides.alt == .right) break :alt, .right => if (self.sides.alt == .left) break :alt, } // Unset alt result.alt = false; } return result; } /// Checks to see if super is on (MacOS) or ctrl. pub fn ctrlOrSuper(self: Mods) bool { if (comptime builtin.target.os.tag.isDarwin()) { return self.super; } return self.ctrl; } // For our own understanding test { const testing = std.testing; try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0)); try testing.expectEqual( @as(Backing, @bitCast(Mods{ .shift = true })), @as(Backing, 0b0000_0001), ); } test "translation macos-option-as-alt" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; const testing = std.testing; // Unset { const mods: Mods = .{}; const result = mods.translation(.true); try testing.expectEqual(result, mods); } // Set { const mods: Mods = .{ .alt = true }; const result = mods.translation(.true); try testing.expectEqual(Mods{}, result); } // Set but disabled { const mods: Mods = .{ .alt = true }; const result = mods.translation(.false); try testing.expectEqual(result, mods); } // Set wrong side { const mods: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; const result = mods.translation(.left); try testing.expectEqual(result, mods); } { const mods: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; const result = mods.translation(.right); try testing.expectEqual(result, mods); } // Set with other mods { const mods: Mods = .{ .alt = true, .shift = true }; const result = mods.translation(.true); try testing.expectEqual(Mods{ .shift = true }, result); } } }; /// The action associated with an input event. This is backed by a c_int /// so that we can use the enum as-is for our embedding API. /// /// IMPORTANT: Any changes here update include/ghostty.h pub const Action = enum(c_int) { release, press, repeat, }; /// The set of key codes that Ghostty is aware of. These represent /// physical keys on the keyboard. The logical key (or key string) /// is the string that is generated by the key event and that is up /// to the apprt to provide. /// /// Note that these are layout-independent. For example, the "a" /// key on a US keyboard is the same as the "ф" key on a Russian /// keyboard, but both will report the "a" enum value in the key /// event. These values are based on the W3C standard. See: /// https://www.w3.org/TR/uievents-code /// /// Layout-dependent strings are provided in the KeyEvent struct as /// UTF-8 and are produced by the associated apprt. Ghostty core has /// no mechanism to map input events to strings without the apprt. /// /// IMPORTANT: Any changes here update include/ghostty.h ghostty_input_key_e pub const Key = enum(c_int) { unidentified, // "Writing System Keys" § 3.1.1 backquote, backslash, bracket_left, bracket_right, comma, digit_0, digit_1, digit_2, digit_3, digit_4, digit_5, digit_6, digit_7, digit_8, digit_9, equal, intl_backslash, intl_ro, intl_yen, key_a, key_b, key_c, key_d, key_e, key_f, key_g, key_h, key_i, key_j, key_k, key_l, key_m, key_n, key_o, key_p, key_q, key_r, key_s, key_t, key_u, key_v, key_w, key_x, key_y, key_z, minus, period, quote, semicolon, slash, // "Functional Keys" § 3.1.2 alt_left, alt_right, backspace, caps_lock, context_menu, control_left, control_right, enter, meta_left, meta_right, shift_left, shift_right, space, tab, convert, kana_mode, non_convert, // "Control Pad Section" § 3.2 delete, end, help, home, insert, page_down, page_up, // "Arrow Pad Section" § 3.3 arrow_down, arrow_left, arrow_right, arrow_up, // "Numpad Section" § 3.4 num_lock, numpad_0, numpad_1, numpad_2, numpad_3, numpad_4, numpad_5, numpad_6, numpad_7, numpad_8, numpad_9, numpad_add, numpad_backspace, numpad_clear, numpad_clear_entry, numpad_comma, numpad_decimal, numpad_divide, numpad_enter, numpad_equal, numpad_memory_add, numpad_memory_clear, numpad_memory_recall, numpad_memory_store, numpad_memory_subtract, numpad_multiply, numpad_paren_left, numpad_paren_right, numpad_subtract, // > For numpads that provide keys not listed here, a code value string // > should be created by starting with "Numpad" and appending an // > appropriate description of the key. // // These numpad entries are distinguished by various encoding protocols // (legacy and Kitty) so we support them here in case the apprt can // produce them. numpad_separator, numpad_up, numpad_down, numpad_right, numpad_left, numpad_begin, numpad_home, numpad_end, numpad_insert, numpad_delete, numpad_page_up, numpad_page_down, // "Function Section" § 3.5 escape, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, f21, f22, f23, f24, f25, @"fn", fn_lock, print_screen, scroll_lock, pause, // "Media Keys" § 3.6 browser_back, browser_favorites, browser_forward, browser_home, browser_refresh, browser_search, browser_stop, eject, launch_app_1, launch_app_2, launch_mail, media_play_pause, media_select, media_stop, media_track_next, media_track_previous, power, sleep, audio_volume_down, audio_volume_mute, audio_volume_up, wake_up, /// Converts an ASCII character to a key, if possible. This returns /// null if the character is unknown. /// /// Note that this can't distinguish between physical keys, i.e. '0' /// may be from the number row or the keypad, but it always maps /// to '.zero'. /// /// This is what we want, we want people to create keybindings that /// are independent of the physical key. pub fn fromASCII(ch: u8) ?Key { return switch (ch) { inline else => |comptime_ch| { return comptime result: { @setEvalBranchQuota(100_000); for (codepoint_map) |entry| { // No ASCII characters should ever map to a keypad key if (entry[1].keypad()) continue; if (entry[0] == @as(u21, @intCast(comptime_ch))) { break :result entry[1]; } } break :result null; }; }, }; } /// Converts a W3C key code to a Ghostty key enum value. /// /// All required W3C key codes are supported, but there are a number of /// non-standard key codes that are not supported. In the case the value is /// invalid or unsupported, this function will return null. pub fn fromW3C(code: []const u8) ?Key { var result: [128]u8 = undefined; // If the code is bigger than our buffer it can't possibly match. if (code.len > result.len) return null; // First just check the whole thing lowercased, this is the simple case if (std.meta.stringToEnum( Key, std.ascii.lowerString(&result, code), )) |key| return key; // We need to convert FooBar to foo_bar var fbs = std.io.fixedBufferStream(&result); const w = fbs.writer(); for (code, 0..) |ch, i| switch (ch) { 'a'...'z' => w.writeByte(ch) catch return null, // Caps and numbers trigger underscores 'A'...'Z', '0'...'9' => { if (i > 0) w.writeByte('_') catch return null; w.writeByte(std.ascii.toLower(ch)) catch return null; }, // We don't know of any key codes that aren't alphanumeric. else => return null, }; return std.meta.stringToEnum(Key, fbs.getWritten()); } /// Converts a Ghostty key enum value to a W3C key code. pub fn w3c(self: Key) []const u8 { return switch (self) { inline else => |tag| comptime w3c: { @setEvalBranchQuota(50_000); const name = @tagName(tag); var buf: [128]u8 = undefined; var fbs = std.io.fixedBufferStream(&buf); const w = fbs.writer(); var i: usize = 0; while (i < name.len) { if (i == 0) { w.writeByte(std.ascii.toUpper(name[i])) catch unreachable; } else if (name[i] == '_') { i += 1; w.writeByte(std.ascii.toUpper(name[i])) catch unreachable; } else { w.writeByte(name[i]) catch unreachable; } i += 1; } const written = buf; const result = written[0..fbs.getWritten().len]; break :w3c result; }, }; } /// True if this key represents a printable character. pub fn printable(self: Key) bool { return switch (self) { inline else => |tag| { return comptime result: { @setEvalBranchQuota(10_000); for (codepoint_map) |entry| { if (entry[1] == tag) break :result true; } break :result false; }; }, }; } /// True if this key is a modifier. pub fn modifier(self: Key) bool { return switch (self) { .shift_left, .control_left, .alt_left, .meta_left, .shift_right, .control_right, .alt_right, .meta_right, => true, else => false, }; } /// Returns true if this is a keypad key. pub fn keypad(self: Key) bool { return switch (self) { inline else => |tag| { const name = @tagName(tag); const result = comptime std.mem.startsWith(u8, name, "numpad_"); return result; }, }; } // Returns the codepoint representing this key, or null if the key is not // printable pub fn codepoint(self: Key) ?u21 { return switch (self) { inline else => |tag| { return comptime result: { @setEvalBranchQuota(10_000); for (codepoint_map) |entry| { if (entry[1] == tag) break :result entry[0]; } break :result null; }; }, }; } /// Returns the cimgui key constant for this key. pub fn imguiKey(self: Key) ?c_uint { return switch (self) { .key_a => cimgui.c.ImGuiKey_A, .key_b => cimgui.c.ImGuiKey_B, .key_c => cimgui.c.ImGuiKey_C, .key_d => cimgui.c.ImGuiKey_D, .key_e => cimgui.c.ImGuiKey_E, .key_f => cimgui.c.ImGuiKey_F, .key_g => cimgui.c.ImGuiKey_G, .key_h => cimgui.c.ImGuiKey_H, .key_i => cimgui.c.ImGuiKey_I, .key_j => cimgui.c.ImGuiKey_J, .key_k => cimgui.c.ImGuiKey_K, .key_l => cimgui.c.ImGuiKey_L, .key_m => cimgui.c.ImGuiKey_M, .key_n => cimgui.c.ImGuiKey_N, .key_o => cimgui.c.ImGuiKey_O, .key_p => cimgui.c.ImGuiKey_P, .key_q => cimgui.c.ImGuiKey_Q, .key_r => cimgui.c.ImGuiKey_R, .key_s => cimgui.c.ImGuiKey_S, .key_t => cimgui.c.ImGuiKey_T, .key_u => cimgui.c.ImGuiKey_U, .key_v => cimgui.c.ImGuiKey_V, .key_w => cimgui.c.ImGuiKey_W, .key_x => cimgui.c.ImGuiKey_X, .key_y => cimgui.c.ImGuiKey_Y, .key_z => cimgui.c.ImGuiKey_Z, .digit_0 => cimgui.c.ImGuiKey_0, .digit_1 => cimgui.c.ImGuiKey_1, .digit_2 => cimgui.c.ImGuiKey_2, .digit_3 => cimgui.c.ImGuiKey_3, .digit_4 => cimgui.c.ImGuiKey_4, .digit_5 => cimgui.c.ImGuiKey_5, .digit_6 => cimgui.c.ImGuiKey_6, .digit_7 => cimgui.c.ImGuiKey_7, .digit_8 => cimgui.c.ImGuiKey_8, .digit_9 => cimgui.c.ImGuiKey_9, .semicolon => cimgui.c.ImGuiKey_Semicolon, .space => cimgui.c.ImGuiKey_Space, .quote => cimgui.c.ImGuiKey_Apostrophe, .comma => cimgui.c.ImGuiKey_Comma, .backquote => cimgui.c.ImGuiKey_GraveAccent, .period => cimgui.c.ImGuiKey_Period, .slash => cimgui.c.ImGuiKey_Slash, .minus => cimgui.c.ImGuiKey_Minus, .equal => cimgui.c.ImGuiKey_Equal, .bracket_left => cimgui.c.ImGuiKey_LeftBracket, .bracket_right => cimgui.c.ImGuiKey_RightBracket, .backslash => cimgui.c.ImGuiKey_Backslash, .arrow_up => cimgui.c.ImGuiKey_UpArrow, .arrow_down => cimgui.c.ImGuiKey_DownArrow, .arrow_left => cimgui.c.ImGuiKey_LeftArrow, .arrow_right => cimgui.c.ImGuiKey_RightArrow, .home => cimgui.c.ImGuiKey_Home, .end => cimgui.c.ImGuiKey_End, .insert => cimgui.c.ImGuiKey_Insert, .delete => cimgui.c.ImGuiKey_Delete, .caps_lock => cimgui.c.ImGuiKey_CapsLock, .scroll_lock => cimgui.c.ImGuiKey_ScrollLock, .num_lock => cimgui.c.ImGuiKey_NumLock, .page_up => cimgui.c.ImGuiKey_PageUp, .page_down => cimgui.c.ImGuiKey_PageDown, .escape => cimgui.c.ImGuiKey_Escape, .enter => cimgui.c.ImGuiKey_Enter, .tab => cimgui.c.ImGuiKey_Tab, .backspace => cimgui.c.ImGuiKey_Backspace, .print_screen => cimgui.c.ImGuiKey_PrintScreen, .pause => cimgui.c.ImGuiKey_Pause, .context_menu => cimgui.c.ImGuiKey_Menu, .f1 => cimgui.c.ImGuiKey_F1, .f2 => cimgui.c.ImGuiKey_F2, .f3 => cimgui.c.ImGuiKey_F3, .f4 => cimgui.c.ImGuiKey_F4, .f5 => cimgui.c.ImGuiKey_F5, .f6 => cimgui.c.ImGuiKey_F6, .f7 => cimgui.c.ImGuiKey_F7, .f8 => cimgui.c.ImGuiKey_F8, .f9 => cimgui.c.ImGuiKey_F9, .f10 => cimgui.c.ImGuiKey_F10, .f11 => cimgui.c.ImGuiKey_F11, .f12 => cimgui.c.ImGuiKey_F12, .numpad_0 => cimgui.c.ImGuiKey_Keypad0, .numpad_1 => cimgui.c.ImGuiKey_Keypad1, .numpad_2 => cimgui.c.ImGuiKey_Keypad2, .numpad_3 => cimgui.c.ImGuiKey_Keypad3, .numpad_4 => cimgui.c.ImGuiKey_Keypad4, .numpad_5 => cimgui.c.ImGuiKey_Keypad5, .numpad_6 => cimgui.c.ImGuiKey_Keypad6, .numpad_7 => cimgui.c.ImGuiKey_Keypad7, .numpad_8 => cimgui.c.ImGuiKey_Keypad8, .numpad_9 => cimgui.c.ImGuiKey_Keypad9, .numpad_decimal => cimgui.c.ImGuiKey_KeypadDecimal, .numpad_divide => cimgui.c.ImGuiKey_KeypadDivide, .numpad_multiply => cimgui.c.ImGuiKey_KeypadMultiply, .numpad_subtract => cimgui.c.ImGuiKey_KeypadSubtract, .numpad_add => cimgui.c.ImGuiKey_KeypadAdd, .numpad_enter => cimgui.c.ImGuiKey_KeypadEnter, .numpad_equal => cimgui.c.ImGuiKey_KeypadEqual, // We map KP_SEPARATOR to Comma because traditionally a numpad would // have a numeric separator key. Most modern numpads do not .numpad_left => cimgui.c.ImGuiKey_LeftArrow, .numpad_right => cimgui.c.ImGuiKey_RightArrow, .numpad_up => cimgui.c.ImGuiKey_UpArrow, .numpad_down => cimgui.c.ImGuiKey_DownArrow, .numpad_page_up => cimgui.c.ImGuiKey_PageUp, .numpad_page_down => cimgui.c.ImGuiKey_PageUp, .numpad_home => cimgui.c.ImGuiKey_Home, .numpad_end => cimgui.c.ImGuiKey_End, .numpad_insert => cimgui.c.ImGuiKey_Insert, .numpad_delete => cimgui.c.ImGuiKey_Delete, .numpad_begin => cimgui.c.ImGuiKey_NamedKey_BEGIN, .shift_left => cimgui.c.ImGuiKey_LeftShift, .control_left => cimgui.c.ImGuiKey_LeftCtrl, .alt_left => cimgui.c.ImGuiKey_LeftAlt, .meta_left => cimgui.c.ImGuiKey_LeftSuper, .shift_right => cimgui.c.ImGuiKey_RightShift, .control_right => cimgui.c.ImGuiKey_RightCtrl, .alt_right => cimgui.c.ImGuiKey_RightAlt, .meta_right => cimgui.c.ImGuiKey_RightSuper, // These keys aren't represented in cimgui .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20, .f21, .f22, .f23, .f24, .f25, .intl_backslash, .intl_ro, .intl_yen, .convert, .kana_mode, .non_convert, .numpad_separator, .numpad_backspace, .numpad_clear, .numpad_clear_entry, .numpad_comma, .numpad_memory_add, .numpad_memory_clear, .numpad_memory_recall, .numpad_memory_store, .numpad_memory_subtract, .numpad_paren_left, .numpad_paren_right, .@"fn", .fn_lock, .browser_back, .browser_favorites, .browser_forward, .browser_home, .browser_refresh, .browser_search, .browser_stop, .eject, .launch_app_1, .launch_app_2, .launch_mail, .media_play_pause, .media_select, .media_stop, .media_track_next, .media_track_previous, .power, .sleep, .audio_volume_down, .audio_volume_mute, .audio_volume_up, .wake_up, .help, => null, .unidentified, => null, }; } /// true if this key is one of the left or right versions of super (MacOS) /// or ctrl. pub fn ctrlOrSuper(self: Key) bool { if (comptime builtin.target.os.tag.isDarwin()) { return self == .meta_left or self == .meta_right; } return self == .control_left or self == .control_right; } /// true if this key is either left or right shift. pub fn leftOrRightShift(self: Key) bool { return self == .shift_left or self == .shift_right; } /// true if this key is either left or right alt. pub fn leftOrRightAlt(self: Key) bool { return self == .alt_left or self == .alt_right; } test "fromASCII should not return keypad keys" { const testing = std.testing; try testing.expect(Key.fromASCII('0').? == .digit_0); try testing.expect(Key.fromASCII('*') == null); } test "keypad keys" { const testing = std.testing; try testing.expect(Key.numpad_0.keypad()); try testing.expect(!Key.digit_1.keypad()); } test "w3c" { // All our keys should convert to and from the W3C format. // We don't support every key in the W3C spec, so we only // check the enum fields. const testing = std.testing; inline for (@typeInfo(Key).@"enum".fields) |field| { const key = @field(Key, field.name); const w3c_name = key.w3c(); try testing.expectEqual(key, Key.fromW3C(w3c_name).?); } } const codepoint_map: []const struct { u21, Key } = &.{ .{ 'a', .key_a }, .{ 'b', .key_b }, .{ 'c', .key_c }, .{ 'd', .key_d }, .{ 'e', .key_e }, .{ 'f', .key_f }, .{ 'g', .key_g }, .{ 'h', .key_h }, .{ 'i', .key_i }, .{ 'j', .key_j }, .{ 'k', .key_k }, .{ 'l', .key_l }, .{ 'm', .key_m }, .{ 'n', .key_n }, .{ 'o', .key_o }, .{ 'p', .key_p }, .{ 'q', .key_q }, .{ 'r', .key_r }, .{ 's', .key_s }, .{ 't', .key_t }, .{ 'u', .key_u }, .{ 'v', .key_v }, .{ 'w', .key_w }, .{ 'x', .key_x }, .{ 'y', .key_y }, .{ 'z', .key_z }, .{ '0', .digit_0 }, .{ '1', .digit_1 }, .{ '2', .digit_2 }, .{ '3', .digit_3 }, .{ '4', .digit_4 }, .{ '5', .digit_5 }, .{ '6', .digit_6 }, .{ '7', .digit_7 }, .{ '8', .digit_8 }, .{ '9', .digit_9 }, .{ ';', .semicolon }, .{ ' ', .space }, .{ '\'', .quote }, .{ ',', .comma }, .{ '`', .backquote }, .{ '.', .period }, .{ '/', .slash }, .{ '-', .minus }, .{ '=', .equal }, .{ '[', .bracket_left }, .{ ']', .bracket_right }, .{ '\\', .backslash }, // Control characters .{ '\t', .tab }, // Keypad entries. We just assume keypad with the numpad_ prefix // so that has some special meaning. These must also always be last, // so that our `fromASCII` function doesn't accidentally map them // over normal numerics and other keys. .{ '0', .numpad_0 }, .{ '1', .numpad_1 }, .{ '2', .numpad_2 }, .{ '3', .numpad_3 }, .{ '4', .numpad_4 }, .{ '5', .numpad_5 }, .{ '6', .numpad_6 }, .{ '7', .numpad_7 }, .{ '8', .numpad_8 }, .{ '9', .numpad_9 }, .{ '.', .numpad_decimal }, .{ '/', .numpad_divide }, .{ '*', .numpad_multiply }, .{ '-', .numpad_subtract }, .{ '+', .numpad_add }, .{ '=', .numpad_equal }, }; }; /// This sets either "ctrl" or "super" to true (but not both) /// on mods depending on if the build target is Mac or not. On /// Mac, we default to super (i.e. super+c for copy) and on /// non-Mac we default to ctrl (i.e. ctrl+c for copy). pub fn ctrlOrSuper(mods: Mods) Mods { var copy = mods; if (comptime builtin.target.os.tag.isDarwin()) { copy.super = true; } else { copy.ctrl = true; } return copy; } test "ctrlOrSuper" { const testing = std.testing; var m: Mods = ctrlOrSuper(.{}); try testing.expect(m.ctrlOrSuper()); }