Files
ghostty/src/input/key.zig
2025-05-11 07:15:46 -07:00

947 lines
28 KiB
Zig

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());
}