mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
input: KeyEncoder legacy encoding handles old keyCallback logic
This commit is contained in:
@ -9,19 +9,102 @@ const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
const key = @import("key.zig");
|
||||
const function_keys = @import("function_keys.zig");
|
||||
|
||||
key: key.Key,
|
||||
binding_mods: key.Mods,
|
||||
event: key.Event,
|
||||
|
||||
/// Initialize
|
||||
fn init(event: key.Event) KeyEncoder {
|
||||
const effective_mods = event.effectiveMods();
|
||||
/// The state of various modes of a terminal that impact encoding.
|
||||
cursor_key_application: bool = false,
|
||||
keypad_key_application: bool = false,
|
||||
modify_other_keys_state_2: bool = false,
|
||||
|
||||
/// Perform legacy encoding of the key event. "Legacy" in this case
|
||||
/// is referring to the behavior of traditional terminals, plus
|
||||
/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec.
|
||||
/// These together combine the legacy protocol because they're all
|
||||
/// meant to be extensions that do not change any existing behavior
|
||||
/// and therefore safe to combine.
|
||||
fn legacy(
|
||||
self: *const KeyEncoder,
|
||||
buf: []u8,
|
||||
) ![]const u8 {
|
||||
const effective_mods = self.event.effectiveMods();
|
||||
const binding_mods = effective_mods.binding();
|
||||
|
||||
return .{
|
||||
.key = event.key,
|
||||
.binding_mods = binding_mods,
|
||||
};
|
||||
// Legacy encoding only does press/repeat
|
||||
if (self.event.action != .press and
|
||||
self.event.action != .repeat) return "";
|
||||
|
||||
// If we match a PC style function key then that is our result.
|
||||
if (pcStyleFunctionKey(
|
||||
self.event.key,
|
||||
binding_mods,
|
||||
self.cursor_key_application,
|
||||
self.keypad_key_application,
|
||||
self.modify_other_keys_state_2,
|
||||
)) |sequence| return sequence;
|
||||
|
||||
// If we match a control sequence, we output that directly.
|
||||
if (ctrlSeq(self.event.key, binding_mods)) |char| {
|
||||
// C0 sequences support alt-as-esc prefixing.
|
||||
if (binding_mods.alt) {
|
||||
if (buf.len < 2) return error.OutOfMemory;
|
||||
buf[0] = 0x1B;
|
||||
buf[1] = char;
|
||||
return buf[0..2];
|
||||
}
|
||||
|
||||
if (buf.len < 1) return error.OutOfMemory;
|
||||
buf[0] = char;
|
||||
return buf[0..1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Determines whether the key should be encoded in the xterm
|
||||
/// "PC-style Function Key" syntax (roughly). This is a hardcoded
|
||||
/// table of keys and modifiers that result in a specific sequence.
|
||||
fn pcStyleFunctionKey(
|
||||
keyval: key.Key,
|
||||
mods: key.Mods,
|
||||
cursor_key_application: bool,
|
||||
keypad_key_application: bool,
|
||||
modify_other_keys: bool, // True if state 2
|
||||
) ?[]const u8 {
|
||||
const mods_int = mods.int();
|
||||
for (function_keys.keys.get(keyval)) |entry| {
|
||||
switch (entry.cursor) {
|
||||
.any => {},
|
||||
.normal => if (cursor_key_application) continue,
|
||||
.application => if (!cursor_key_application) continue,
|
||||
}
|
||||
|
||||
switch (entry.keypad) {
|
||||
.any => {},
|
||||
.normal => if (keypad_key_application) continue,
|
||||
.application => if (!keypad_key_application) continue,
|
||||
}
|
||||
|
||||
switch (entry.modify_other_keys) {
|
||||
.any => {},
|
||||
.set => if (modify_other_keys) continue,
|
||||
.set_other => if (!modify_other_keys) continue,
|
||||
}
|
||||
|
||||
const entry_mods_int = entry.mods.int();
|
||||
if (entry_mods_int == 0) {
|
||||
if (mods_int != 0 and !entry.mods_empty_is_any) continue;
|
||||
// mods are either empty, or empty means any so we allow it.
|
||||
} else if (entry_mods_int != mods_int) {
|
||||
// any set mods require an exact match
|
||||
continue;
|
||||
}
|
||||
|
||||
return entry.sequence;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the C0 byte for the key event if it should be used.
|
||||
@ -31,11 +114,11 @@ fn init(event: key.Event) KeyEncoder {
|
||||
/// 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 {
|
||||
fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?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;
|
||||
var unalt_mods = mods;
|
||||
unalt_mods.alt = false;
|
||||
break :unalt_mods unalt_mods;
|
||||
};
|
||||
@ -49,7 +132,7 @@ fn ctrlSeq(self: *const KeyEncoder) ?u8 {
|
||||
// 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) {
|
||||
return switch (keyval) {
|
||||
.space => 0,
|
||||
.slash => 0x1F,
|
||||
.zero => 0x30,
|
||||
@ -95,37 +178,73 @@ fn ctrlSeq(self: *const KeyEncoder) ?u8 {
|
||||
};
|
||||
}
|
||||
|
||||
test "legacy: ctrl+alt+c" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var enc: KeyEncoder = .{
|
||||
.event = .{
|
||||
.key = .c,
|
||||
.mods = .{ .ctrl = true, .alt = true },
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try enc.legacy(&buf);
|
||||
try testing.expectEqualStrings("\x1b\x03", actual);
|
||||
}
|
||||
|
||||
test "legacy: ctrl+c" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var enc: KeyEncoder = .{
|
||||
.event = .{
|
||||
.key = .c,
|
||||
.mods = .{ .ctrl = true },
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try enc.legacy(&buf);
|
||||
try testing.expectEqualStrings("\x03", actual);
|
||||
}
|
||||
|
||||
test "legacy: ctrl+space" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var enc: KeyEncoder = .{
|
||||
.event = .{
|
||||
.key = .space,
|
||||
.mods = .{ .ctrl = true },
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try enc.legacy(&buf);
|
||||
try testing.expectEqualStrings("\x00", actual);
|
||||
}
|
||||
|
||||
test "legacy: ctrl+shift+backspace" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var enc: KeyEncoder = .{
|
||||
.event = .{
|
||||
.key = .backspace,
|
||||
.mods = .{ .ctrl = true, .shift = true },
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try enc.legacy(&buf);
|
||||
try testing.expectEqualStrings("\x08", actual);
|
||||
}
|
||||
|
||||
test "ctrlseq: normal ctrl c" {
|
||||
const enc = init(.{ .key = .c, .mods = .{ .ctrl = true } });
|
||||
const seq = enc.ctrlSeq();
|
||||
const seq = ctrlSeq(.c, .{ .ctrl = true });
|
||||
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();
|
||||
const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true });
|
||||
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);
|
||||
try testing.expect(ctrlSeq(.c, .{}) == 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);
|
||||
}
|
||||
try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null);
|
||||
try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null);
|
||||
}
|
||||
|
Reference in New Issue
Block a user