ghostty/src/input/key.zig
2023-09-29 20:00:50 -07:00

445 lines
11 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
/// 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,
/// "key" is the logical key that was pressed. For example, if
/// a Dvorak keyboard layout is being used on a US keyboard,
/// the "i" physical key will be reported as "c". The physical
/// key is the key that was physically pressed on the keyboard.
key: Key,
physical_key: Key = .invalid,
/// 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);
}
};
/// 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 { 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;
}
// 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),
);
}
};
/// 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 keys that can map to keybindings. These have no fixed enum
/// values because we map platform-specific keys to this set. Note that
/// this only needs to accommodate what maps to a key. If a key is not bound
/// to anything and the key can be mapped to a printable character, then that
/// unicode character is sent directly to the pty.
///
/// This is backed by a c_int so we can use this as-is for our embedding API.
///
/// IMPORTANT: Any changes here update include/ghostty.h
pub const Key = enum(c_int) {
invalid,
// a-z
a,
b,
c,
d,
e,
f,
g,
h,
i,
j,
k,
l,
m,
n,
o,
p,
q,
r,
s,
t,
u,
v,
w,
x,
y,
z,
// numbers
zero,
one,
two,
three,
four,
five,
six,
seven,
eight,
nine,
// puncuation
semicolon,
space,
apostrophe,
comma,
grave_accent, // `
period,
slash,
minus,
equal,
left_bracket, // [
right_bracket, // ]
backslash, // /
// control
up,
down,
right,
left,
home,
end,
insert,
delete,
caps_lock,
scroll_lock,
num_lock,
page_up,
page_down,
escape,
enter,
tab,
backspace,
print_screen,
pause,
// function keys
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,
// keypad
kp_0,
kp_1,
kp_2,
kp_3,
kp_4,
kp_5,
kp_6,
kp_7,
kp_8,
kp_9,
kp_decimal,
kp_divide,
kp_multiply,
kp_subtract,
kp_add,
kp_enter,
kp_equal,
// modifiers
left_shift,
left_control,
left_alt,
left_super,
right_shift,
right_control,
right_alt,
right_super,
// To support more keys (there are obviously more!) add them here
// and ensure the mapping is up to date in the Window key handler.
/// 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 awnt 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;
};
},
};
}
/// 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;
};
},
};
}
/// 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, "kp_");
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;
};
},
};
}
test "fromASCII should not return keypad keys" {
const testing = std.testing;
try testing.expect(Key.fromASCII('0').? == .zero);
try testing.expect(Key.fromASCII('*') == null);
}
test "keypad keys" {
const testing = std.testing;
try testing.expect(Key.kp_0.keypad());
try testing.expect(!Key.one.keypad());
}
const codepoint_map: []const struct { u21, Key } = &.{
.{ 'a', .a },
.{ 'b', .b },
.{ 'c', .c },
.{ 'd', .d },
.{ 'e', .e },
.{ 'f', .f },
.{ 'g', .g },
.{ 'h', .h },
.{ 'i', .i },
.{ 'j', .j },
.{ 'k', .k },
.{ 'l', .l },
.{ 'm', .m },
.{ 'n', .n },
.{ 'o', .o },
.{ 'p', .p },
.{ 'q', .q },
.{ 'r', .r },
.{ 's', .s },
.{ 't', .t },
.{ 'u', .u },
.{ 'v', .v },
.{ 'w', .w },
.{ 'x', .x },
.{ 'y', .y },
.{ 'z', .z },
.{ '0', .zero },
.{ '1', .one },
.{ '2', .two },
.{ '3', .three },
.{ '4', .four },
.{ '5', .five },
.{ '6', .six },
.{ '7', .seven },
.{ '8', .eight },
.{ '9', .nine },
.{ ';', .semicolon },
.{ ' ', .space },
.{ '\'', .apostrophe },
.{ ',', .comma },
.{ '`', .grave_accent },
.{ '.', .period },
.{ '/', .slash },
.{ '-', .minus },
.{ '=', .equal },
.{ '[', .left_bracket },
.{ ']', .right_bracket },
.{ '\\', .backslash },
// Keypad entries. We just assume keypad with the kp_ prefix
// so that has some special meaning. These must also always be last.
.{ '0', .kp_0 },
.{ '1', .kp_1 },
.{ '2', .kp_2 },
.{ '3', .kp_3 },
.{ '4', .kp_4 },
.{ '5', .kp_5 },
.{ '6', .kp_6 },
.{ '7', .kp_7 },
.{ '8', .kp_8 },
.{ '9', .kp_9 },
.{ '.', .kp_decimal },
.{ '/', .kp_divide },
.{ '*', .kp_multiply },
.{ '-', .kp_subtract },
.{ '+', .kp_add },
.{ '=', .kp_equal },
};
};