input: encode ctrl keys with modifyOtherKeys 2

#7425

Control-modified characters use xterm's MOK2 encoding.

I incorrectly believed previously that MOK2 ctrl chars were still encoded 
as C0 bytes. This is wrong. I'm going to do a more in depth audit if
possible with every possible key combination against xterm to see where
we diverge but this fixes this for now without regressing any tests.

Background: https://invisible-island.net/xterm/modified-keys.html
This commit is contained in:
Mitchell Hashimoto
2026-09-04 08:41:06 -07:00
parent c81f0b2687
commit 4406cea3e9

View File

@@ -379,42 +379,10 @@ fn legacy(
return try writer.writeAll(sequence);
}
// If we match a control sequence, we output that directly. For
// ctrlSeq we have to use all mods because we want it to only
// match ctrl+<char>.
if (ctrlSeq(
event.key,
event.utf8,
event.unshifted_codepoint,
all_mods,
)) |char| {
// C0 sequences support alt-as-esc prefixing.
if (binding_mods.alt) {
try writer.writeByte(0x1B);
try writer.writeByte(char);
return;
}
try writer.writeByte(char);
return;
}
// If we have no UTF8 text then the only possibility is the
// alt-prefix handling of unshifted codepoints... so we process that.
const utf8 = event.utf8;
if (utf8.len == 0) {
if (try legacyAltPrefix(
event,
binding_mods,
all_mods,
opts,
)) |byte| try writer.print("\x1B{c}", .{byte});
return;
}
// In modify other keys state 2, we send the CSI 27 sequence
// for any char with a modifier. Ctrl sequences like Ctrl+a
// are already handled above.
// for any char with a modifier. This must happen before converting
// Ctrl+<char> to a C0 byte because mode 2 encodes those keys, too.
const utf8 = event.utf8;
if (opts.modify_other_keys_state_2) modify_other: {
const view = std.unicode.Utf8View.init(utf8) catch {
// Assume invalid UTF-8 means we no UTF-8.
@@ -476,6 +444,38 @@ fn legacy(
}
}
// If we match a control sequence, we output that directly. For
// ctrlSeq we have to use all mods because we want it to only
// match ctrl+<char>.
if (ctrlSeq(
event.key,
event.utf8,
event.unshifted_codepoint,
all_mods,
)) |char| {
// C0 sequences support alt-as-esc prefixing.
if (binding_mods.alt) {
try writer.writeByte(0x1B);
try writer.writeByte(char);
return;
}
try writer.writeByte(char);
return;
}
// If we have no UTF8 text then the only possibility is the
// alt-prefix handling of unshifted codepoints... so we process that.
if (utf8.len == 0) {
if (try legacyAltPrefix(
event,
binding_mods,
all_mods,
opts,
)) |byte| try writer.print("\x1B{c}", .{byte});
return;
}
// Let's see if we should apply fixterms to this codepoint.
// At this stage of key processing, we only need to apply fixterms
// to unicode codepoints if we have ctrl set.
@@ -2219,6 +2219,30 @@ test "legacy: ctrl+shift+char with modify other state 2" {
try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered());
}
test "legacy: ctrl+char with modify other state 2" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try legacy(&writer, .{
.key = .key_p,
.mods = .{ .ctrl = true },
.utf8 = "p",
}, .{
.modify_other_keys_state_2 = true,
});
try testing.expectEqualStrings("\x1b[27;5;112~", writer.buffered());
}
test "legacy: ctrl+char without modify other state 2" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try legacy(&writer, .{
.key = .key_p,
.mods = .{ .ctrl = true },
.utf8 = "p",
}, .{});
try testing.expectEqualStrings("\x10", writer.buffered());
}
test "legacy: ctrl+shift+char with modify other state 2 and consumed mods" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);