input: starting to work on KeyEncoder, got ctrl sequences

This commit is contained in:
Mitchell Hashimoto
2023-08-16 08:52:53 -07:00
parent 62081a51b0
commit f57fd99d3e
3 changed files with 186 additions and 2 deletions

View File

@ -6,6 +6,7 @@ pub usingnamespace @import("input/key.zig");
pub const function_keys = @import("input/function_keys.zig"); pub const function_keys = @import("input/function_keys.zig");
pub const keycodes = @import("input/keycodes.zig"); pub const keycodes = @import("input/keycodes.zig");
pub const Binding = @import("input/Binding.zig"); pub const Binding = @import("input/Binding.zig");
pub const KeyEncoder = @import("input/KeyEncoder.zig");
pub const SplitDirection = Binding.Action.SplitDirection; pub const SplitDirection = Binding.Action.SplitDirection;
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;

131
src/input/KeyEncoder.zig Normal file
View File

@ -0,0 +1,131 @@
/// KeyEncoder is responsible for processing keyboard input and generating
/// the proper VT sequence for any events.
///
/// A new KeyEncoder should be created for each individual key press.
/// These encoders are not meant to be reused.
const KeyEncoder = @This();
const std = @import("std");
const testing = std.testing;
const key = @import("key.zig");
key: key.Key,
binding_mods: key.Mods,
/// Initialize
fn init(event: key.Event) KeyEncoder {
const effective_mods = event.effectiveMods();
const binding_mods = effective_mods.binding();
return .{
.key = event.key,
.binding_mods = binding_mods,
};
}
/// Returns the C0 byte for the key event if it should be used.
/// This converts a key event into the expected terminal behavior
/// such as Ctrl+C turning into 0x03, amongst many other translations.
///
/// This will return null if the key event should not be converted
/// into a C0 byte. There are many cases for this and you should read
/// the source code to understand them.
fn ctrlSeq(self: *const KeyEncoder) ?u8 {
// Remove alt from our modifiers because it does not impact whether
// we are generating a ctrl sequence.
const unalt_mods = unalt_mods: {
var unalt_mods = self.binding_mods;
unalt_mods.alt = false;
break :unalt_mods unalt_mods;
};
// If we have any other modifier key set, then we do not generate
// a C0 sequence.
const ctrl_only = comptime (key.Mods{ .ctrl = true }).int();
if (unalt_mods.int() != ctrl_only) return null;
// The normal approach to get this value is to make the ascii byte
// with 0x1F. However, not all apprt key translation will properly
// generate the correct value so we just hardcode this based on
// logical key.
return switch (self.key) {
.space => 0,
.slash => 0x1F,
.zero => 0x30,
.one => 0x31,
.two => 0x00,
.three => 0x1B,
.four => 0x1C,
.five => 0x1D,
.six => 0x1E,
.seven => 0x1F,
.eight => 0x7F,
.nine => 0x39,
.backslash => 0x1C,
.left_bracket => 0x1B,
.right_bracket => 0x1D,
.a => 0x01,
.b => 0x02,
.c => 0x03,
.d => 0x04,
.e => 0x05,
.f => 0x06,
.g => 0x07,
.h => 0x08,
.i => 0x09,
.j => 0x0A,
.k => 0x0B,
.l => 0x0C,
.m => 0x0D,
.n => 0x0E,
.o => 0x0F,
.p => 0x10,
.q => 0x11,
.r => 0x12,
.s => 0x13,
.t => 0x14,
.u => 0x15,
.v => 0x16,
.w => 0x17,
.x => 0x18,
.y => 0x19,
.z => 0x1A,
else => null,
};
}
test "ctrlseq: normal ctrl c" {
const enc = init(.{ .key = .c, .mods = .{ .ctrl = true } });
const seq = enc.ctrlSeq();
try testing.expectEqual(@as(u8, 0x03), seq.?);
}
test "ctrlseq: alt should be allowed" {
const enc = init(.{ .key = .c, .mods = .{ .alt = true, .ctrl = true } });
const seq = enc.ctrlSeq();
try testing.expectEqual(@as(u8, 0x03), seq.?);
}
test "ctrlseq: no ctrl does nothing" {
const enc = init(.{ .key = .c, .mods = .{} });
try testing.expect(enc.ctrlSeq() == null);
}
test "ctrlseq: shift does not generate ctrl seq" {
{
const enc = init(.{
.key = .c,
.mods = .{ .shift = true },
});
try testing.expect(enc.ctrlSeq() == null);
}
{
const enc = init(.{
.key = .c,
.mods = .{ .shift = true, .ctrl = true },
});
try testing.expect(enc.ctrlSeq() == null);
}
}

View File

@ -1,8 +1,55 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
/// A bitmask for all key modifiers. This is taken directly from the /// A generic key input event. This is the information that is necessary
/// GLFW representation, but we use this generically. /// 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 Event = 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 = "",
/// Returns the effective modifiers for this event. The effective
/// modifiers are the mods that should be considered for keybindings.
pub fn effectiveMods(self: Event) 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 /// IMPORTANT: Any changes here update include/ghostty.h
pub const Mods = packed struct(Mods.Backing) { pub const Mods = packed struct(Mods.Backing) {
@ -56,6 +103,11 @@ pub const Mods = packed struct(Mods.Backing) {
}; };
} }
/// 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. /// Returns the mods without locks set.
pub fn withoutLocks(self: Mods) Mods { pub fn withoutLocks(self: Mods) Mods {
var copy = self; var copy = self;