Merge pull request #1380 from mitchellh/ctrlseqs

input: handle more ctrl+<key> sequences, namely ctrl+_
This commit is contained in:
Mitchell Hashimoto
2024-01-25 15:29:41 -08:00
committed by GitHub
2 changed files with 137 additions and 67 deletions

View File

@ -918,6 +918,20 @@ pub const Surface = struct {
=> .invalid, => .invalid,
}; };
// This is a hack for GLFW. We require our apprts to send both
// the UTF8 encoding AND the keypress at the same time. Its critical
// for things like ctrl sequences to work. However, GLFW doesn't
// provide this information all at once. So we just infer based on
// the key press. This isn't portable but GLFW is only for testing.
const utf8 = switch (key) {
inline else => |k| utf8: {
if (mods.shift) break :utf8 "";
const cp = k.codepoint() orelse break :utf8 "";
const byte = std.math.cast(u8, cp) orelse break :utf8 "";
break :utf8 &.{byte};
},
};
const key_event: input.KeyEvent = .{ const key_event: input.KeyEvent = .{
.action = action, .action = action,
.key = key, .key = key,
@ -925,7 +939,8 @@ pub const Surface = struct {
.mods = mods, .mods = mods,
.consumed_mods = .{}, .consumed_mods = .{},
.composing = false, .composing = false,
.utf8 = "", .utf8 = utf8,
.unshifted_codepoint = if (utf8.len > 0) @intCast(utf8[0]) else 0,
}; };
const effect = core_win.keyCallback(key_event) catch |err| { const effect = core_win.keyCallback(key_event) catch |err| {

View File

@ -262,7 +262,7 @@ fn legacy(
// If we match a control sequence, we output that directly. For // If we match a control sequence, we output that directly. For
// ctrlSeq we have to use all mods because we want it to only // ctrlSeq we have to use all mods because we want it to only
// match ctrl+<char>. // match ctrl+<char>.
if (ctrlSeq(self.event.key, all_mods)) |char| { if (ctrlSeq(self.event.utf8, all_mods)) |char| {
// C0 sequences support alt-as-esc prefixing. // C0 sequences support alt-as-esc prefixing.
if (binding_mods.alt) { if (binding_mods.alt) {
if (buf.len < 2) return error.OutOfMemory; if (buf.len < 2) return error.OutOfMemory;
@ -341,7 +341,19 @@ fn legacy(
// effective mods. The fixterms spec states the shifted chars // effective mods. The fixterms spec states the shifted chars
// should be sent uppercase but Kitty changes that behavior // should be sent uppercase but Kitty changes that behavior
// so we'll send all the mods. // so we'll send all the mods.
const csi_u_mods = CsiUMods.fromInput(self.event.mods); const csi_u_mods = mods: {
var mods = CsiUMods.fromInput(self.event.mods);
// If our unshifted codepoint is identical to the shifted
// then we consider shift. Otherwise, we do not because the
// shift key was used to obtain the character. This is specified
// by fixterms.
if (self.event.unshifted_codepoint != @as(u21, @intCast(utf8[0]))) {
mods.shift = false;
}
break :mods mods;
};
const result = try std.fmt.bufPrint( const result = try std.fmt.bufPrint(
buf, buf,
"\x1B[{};{}u", "\x1B[{};{}u",
@ -486,70 +498,92 @@ fn pcStyleFunctionKey(
/// This will return null if the key event should not be converted /// 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 /// into a C0 byte. There are many cases for this and you should read
/// the source code to understand them. /// the source code to understand them.
fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { fn ctrlSeq(utf8: []const u8, mods: key.Mods) ?u8 {
// If ctrl is not pressed then we never do anything.
if (!mods.ctrl) return null;
// If we don't have exactly one byte in our utf8 sequence, then
// we don't do anything, since all our ctrl keys are based on ASCII.
if (utf8.len != 1) return null;
const char = utf8[0];
const unset_mods = unset_mods: {
var unset_mods = mods;
// Remove alt from our modifiers because it does not impact whether // Remove alt from our modifiers because it does not impact whether
// we are generating a ctrl sequence. // we are generating a ctrl sequence and we handle the ESC-prefix
const unalt_mods = unalt_mods: { // logic separately.
var unalt_mods = mods; unset_mods.alt = false;
unalt_mods.alt = false;
break :unalt_mods unalt_mods.binding(); // Remove shift if we have something outside of the US letter range
if (unset_mods.shift and (char < 'A' or char > 'Z')) shift: {
// Special case for fixterms awkward case as specified.
if (char == '@') break :shift;
unset_mods.shift = false;
}
break :unset_mods unset_mods.binding();
}; };
// If we have any other modifier key set, then we do not generate // After unsetting, we only continue if we have ONLY control set.
// a C0 sequence.
const ctrl_only = comptime (key.Mods{ .ctrl = true }).int(); const ctrl_only = comptime (key.Mods{ .ctrl = true }).int();
if (unalt_mods.int() != ctrl_only) return null; if (unset_mods.int() != ctrl_only) return null;
// The normal approach to get this value is to make the ascii byte // From Kitty's key encoding logic. I tried to discern the exact
// with 0x1F. However, not all apprt key translation will properly // behavior across different terminals but it's not clear, so I'm
// generate the correct value so we just hardcode this based on // just going to repeat what Kitty does.
// logical key. return switch (char) {
return switch (keyval) { ' ' => 0,
.space => 0, '/' => 31,
.slash => 0x1F, '0' => 48,
.zero => 0x30, '1' => 49,
.one => 0x31, '2' => 0,
.two => 0x00, '3' => 27,
.three => 0x1B, '4' => 28,
.four => 0x1C, '5' => 29,
.five => 0x1D, '6' => 30,
.six => 0x1E, '7' => 31,
.seven => 0x1F, '8' => 127,
.eight => 0x7F, '9' => 57,
.nine => 0x39, '?' => 127,
.backslash => 0x1C, '@' => 0,
.right_bracket => 0x1D, '\\' => 28,
.a => 0x01, ']' => 29,
.b => 0x02, '^' => 30,
.c => 0x03, '_' => 31,
.d => 0x04, 'a' => 1,
.e => 0x05, 'b' => 2,
.f => 0x06, 'c' => 3,
.g => 0x07, 'd' => 4,
.h => 0x08, 'e' => 5,
.j => 0x0A, 'f' => 6,
.k => 0x0B, 'g' => 7,
.l => 0x0C, 'h' => 8,
.n => 0x0E, 'j' => 10,
.o => 0x0F, 'k' => 11,
.p => 0x10, 'l' => 12,
.q => 0x11, 'n' => 14,
.r => 0x12, 'o' => 15,
.s => 0x13, 'p' => 16,
.t => 0x14, 'q' => 17,
.u => 0x15, 'r' => 18,
.v => 0x16, 's' => 19,
.w => 0x17, 't' => 20,
.x => 0x18, 'u' => 21,
.y => 0x19, 'v' => 22,
.z => 0x1A, 'w' => 23,
'x' => 24,
'y' => 25,
'z' => 26,
'~' => 30,
// These are purposely NOT handled here because of the fixterms // These are purposely NOT handled here because of the fixterms
// specification: https://www.leonerd.org.uk/hacks/fixterms/ // specification: https://www.leonerd.org.uk/hacks/fixterms/
// These are processed as CSI u. // These are processed as CSI u.
// .i => 0x09, // 'i' => 0x09,
// .m => 0x0D, // 'm' => 0x0D,
// .left_bracket => 0x1B, // '[' => 0x1B,
else => null, else => null,
}; };
@ -1476,12 +1510,27 @@ test "legacy: enter with utf8 (dead key state)" {
try testing.expectEqualStrings("A", actual); try testing.expectEqualStrings("A", actual);
} }
test "legacy: ctrl+shift+minus (underscore on US)" {
var buf: [128]u8 = undefined;
var enc: KeyEncoder = .{
.event = .{
.key = .minus,
.mods = .{ .ctrl = true, .shift = true },
.utf8 = "_",
},
};
const actual = try enc.legacy(&buf);
try testing.expectEqualStrings("\x1F", actual);
}
test "legacy: ctrl+alt+c" { test "legacy: ctrl+alt+c" {
var buf: [128]u8 = undefined; var buf: [128]u8 = undefined;
var enc: KeyEncoder = .{ var enc: KeyEncoder = .{
.event = .{ .event = .{
.key = .c, .key = .c,
.mods = .{ .ctrl = true, .alt = true }, .mods = .{ .ctrl = true, .alt = true },
.utf8 = "c",
}, },
}; };
@ -1580,6 +1629,7 @@ test "legacy: ctrl+c" {
.event = .{ .event = .{
.key = .c, .key = .c,
.mods = .{ .ctrl = true }, .mods = .{ .ctrl = true },
.utf8 = "c",
}, },
}; };
@ -1593,6 +1643,7 @@ test "legacy: ctrl+space" {
.event = .{ .event = .{
.key = .space, .key = .space,
.mods = .{ .ctrl = true }, .mods = .{ .ctrl = true },
.utf8 = " ",
}, },
}; };
@ -1658,15 +1709,14 @@ test "legacy: fixterm awkward letters" {
try testing.expectEqualStrings("\x1b[91;5u", actual); try testing.expectEqualStrings("\x1b[91;5u", actual);
} }
{ {
// This doesn't exactly match the fixterm spec but matches the
// behavior of Kitty.
var enc: KeyEncoder = .{ .event = .{ var enc: KeyEncoder = .{ .event = .{
.key = .two, .key = .two,
.mods = .{ .ctrl = true, .shift = true }, .mods = .{ .ctrl = true, .shift = true },
.utf8 = "@", .utf8 = "@",
.unshifted_codepoint = '2',
} }; } };
const actual = try enc.legacy(&buf); const actual = try enc.legacy(&buf);
try testing.expectEqualStrings("\x1b[64;6u", actual); try testing.expectEqualStrings("\x1b[64;5u", actual);
} }
} }
@ -1841,20 +1891,25 @@ test "legacy: right_shift+tab" {
} }
test "ctrlseq: normal ctrl c" { test "ctrlseq: normal ctrl c" {
const seq = ctrlSeq(.c, .{ .ctrl = true }); const seq = ctrlSeq("c", .{ .ctrl = true });
try testing.expectEqual(@as(u8, 0x03), seq.?); try testing.expectEqual(@as(u8, 0x03), seq.?);
} }
test "ctrlseq: alt should be allowed" { test "ctrlseq: alt should be allowed" {
const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true }); const seq = ctrlSeq("c", .{ .alt = true, .ctrl = true });
try testing.expectEqual(@as(u8, 0x03), seq.?); try testing.expectEqual(@as(u8, 0x03), seq.?);
} }
test "ctrlseq: no ctrl does nothing" { test "ctrlseq: no ctrl does nothing" {
try testing.expect(ctrlSeq(.c, .{}) == null); try testing.expect(ctrlSeq("c", .{}) == null);
} }
test "ctrlseq: shift does not generate ctrl seq" { test "ctrlseq: shift does not generate ctrl seq" {
try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null); try testing.expect(ctrlSeq("C", .{ .shift = true }) == null);
try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null); try testing.expect(ctrlSeq("C", .{ .shift = true, .ctrl = true }) == null);
}
test "ctrlseq: shifted non-character" {
const seq = ctrlSeq("_", .{ .ctrl = true, .shift = true });
try testing.expectEqual(@as(u8, 0x1F), seq.?);
} }