core: implement backarrow key mode (DECBKM) - mode 67

This mode allows programs to modify the code that the `backspace`
key (backarrow key in DEC parlance) sends. If this mode is
`off`/`false`/`reset` (the default, the same as before this PR), we
send the byte `0x7f`. If this mode is `on`/`true`/`set` we send the
byte `0x08`.
This commit is contained in:
Jeffrey C. Ollie
2026-04-10 10:14:24 -05:00
parent 94cd3da8bc
commit aea70a5f7c
3 changed files with 82 additions and 3 deletions

View File

@@ -70,6 +70,7 @@ extern "C" {
#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_new(45, false)) /**< Reverse wrap */
#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_new(47, false)) /**< Alternate screen (legacy) */
#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_new(66, false)) /**< Application keypad */
#define GHOSTTY_MODE_BACKARROW_KEY_MODE (ghostty_mode_new(67, false)) /**< Backarrow key mode (DECBKM) */
#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_new(69, false)) /**< Left/right margin mode */
#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_new(1000, false)) /**< Normal mouse tracking */
#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_new(1002, false)) /**< Button-event mouse tracking */

View File

@@ -18,6 +18,12 @@ pub const Options = struct {
/// Terminal DEC mode 66
keypad_key_application: bool = false,
// DEC Backarrow Key Mode (DECBKM)
// See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170
// If `false` (the default), `backspace` emits 0x7f
// If `true`, `backspace` emits 0x08
backarrow_key_mode: bool = false,
/// Terminal DEC mode 1035
ignore_keypad_with_numlock: bool = false,
@@ -55,6 +61,7 @@ pub const Options = struct {
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
.cursor_key_application = t.modes.get(.cursor_keys),
.keypad_key_application = t.modes.get(.keypad_keys),
.backarrow_key_mode = t.modes.get(.backarrow_key_mode),
.ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock),
.modify_other_keys_state_2 = t.flags.modify_other_keys_2,
.kitty_flags = t.screens.active.kitty_keyboard.current(),
@@ -182,7 +189,9 @@ fn kitty(
switch (event.key) {
.enter => return try writer.writeByte('\r'),
.tab => return try writer.writeByte('\t'),
.backspace => return try writer.writeByte(0x7F),
.backspace => return try writer.writeByte(
if (opts.backarrow_key_mode) 0x08 else 0x7F,
),
else => {},
}
}
@@ -338,6 +347,7 @@ fn legacy(
opts.keypad_key_application,
opts.ignore_keypad_with_numlock,
opts.modify_other_keys_state_2,
opts.backarrow_key_mode,
)) |sequence| pc_style: {
// If we have UTF-8 text, then we never emit PC style function
// keys. Many function keys (escape, enter, backspace) have
@@ -601,6 +611,7 @@ fn pcStyleFunctionKey(
keypad_key_application_req: bool,
ignore_keypad_with_numlock: bool,
modify_other_keys: bool, // True if state 2
backarrow_key_mode: bool,
) ?[]const u8 {
// We only want binding-sensitive mods because lock keys
// and directional modifiers (left/right) don't matter for
@@ -653,6 +664,8 @@ fn pcStyleFunctionKey(
continue;
}
if (keyval == .backspace and backarrow_key_mode) return "\x08";
return entry.sequence;
}
@@ -1245,12 +1258,23 @@ test "kitty: enter, backspace, tab" {
try testing.expectEqualStrings("\r", writer.buffered());
}
{
// DECBKM reset
var writer: std.Io.Writer = .fixed(&buf);
try kitty(&writer, .{ .key = .backspace, .mods = .{}, .utf8 = "" }, .{
.kitty_flags = .{ .disambiguate = true },
.backarrow_key_mode = false,
});
try testing.expectEqualStrings("\x7f", writer.buffered());
}
{
// DECBKM set
var writer: std.Io.Writer = .fixed(&buf);
try kitty(&writer, .{ .key = .backspace, .mods = .{}, .utf8 = "" }, .{
.kitty_flags = .{ .disambiguate = true },
.backarrow_key_mode = true,
});
try testing.expectEqualStrings("\x08", writer.buffered());
}
{
var writer: std.Io.Writer = .fixed(&buf);
try kitty(&writer, .{ .key = .tab, .mods = .{}, .utf8 = "" }, .{
@@ -1888,6 +1912,24 @@ test "legacy: backspace with utf8 (dead key state)" {
try testing.expectEqualStrings("", writer.buffered());
}
test "kitty: backspace (DECBKM set) (report_all: true)" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try kitty(&writer, .{
.key = .backspace,
}, .{
.kitty_flags = .{
.disambiguate = true,
.report_events = true,
.report_alternates = true,
.report_all = true,
.report_associated = true,
},
.backarrow_key_mode = true,
});
try testing.expectEqualStrings("\x1b[127u", writer.buffered());
}
test "legacy: enter with utf8 (dead key state)" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
@@ -2039,6 +2081,26 @@ test "legacy: ctrl+shift+backspace" {
try testing.expectEqualStrings("\x08", writer.buffered());
}
test "legacy: backspace (DECBKM reset)" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try legacy(&writer, .{
.key = .backspace,
.mods = .{},
}, .{ .backarrow_key_mode = false });
try testing.expectEqualStrings("\x7f", writer.buffered());
}
test "legacy: backspace (DECBKM set)" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try legacy(&writer, .{
.key = .backspace,
.mods = .{},
}, .{ .backarrow_key_mode = true });
try testing.expectEqualStrings("\x08", writer.buffered());
}
test "legacy: ctrl+shift+char with modify other state 2" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
@@ -2355,17 +2417,28 @@ test "legacy: super and other mods on macOS with text" {
try testing.expectEqualStrings("", writer.buffered());
}
test "legacy: backspace with DEL utf8" {
test "legacy: backspace with DEL utf8 (DECBKM reset)" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try legacy(&writer, .{
.key = .backspace,
.utf8 = &.{0x7F},
.unshifted_codepoint = 0x08,
}, .{});
}, .{ .backarrow_key_mode = false });
try testing.expectEqualStrings("\x7F", writer.buffered());
}
test "legacy: backspace with DEL utf8 (DECBKM set)" {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try legacy(&writer, .{
.key = .backspace,
.utf8 = &.{0x7F},
.unshifted_codepoint = 0x08,
}, .{ .backarrow_key_mode = true });
try testing.expectEqualStrings("\x08", writer.buffered());
}
test "ctrlseq: normal ctrl c" {
const seq = ctrlSeq(.unidentified, "c", 'c', .{ .ctrl = true });
try testing.expectEqual(@as(u8, 0x03), seq.?);

View File

@@ -270,6 +270,11 @@ const entries: []const ModeEntry = &.{
.{ .name = "reverse_wrap", .value = 45 },
.{ .name = "alt_screen_legacy", .value = 47 },
.{ .name = "keypad_keys", .value = 66 },
// DEC Backarrow Key Mode (DECBKM)
// See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170
// If `false` (the default), `backspace` emits 0x7f
// If `true`, `backspace` emits 0x08
.{ .name = "backarrow_key_mode", .value = 67 },
.{ .name = "enable_left_and_right_margin", .value = 69 },
.{ .name = "mouse_event_normal", .value = 1000 },
.{ .name = "mouse_event_button", .value = 1002 },