From aea70a5f7c48c40177077992fe6318fc643f86ca Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 10 Apr 2026 10:14:24 -0500 Subject: [PATCH 1/4] 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`. --- include/ghostty/vt/modes.h | 1 + src/input/key_encode.zig | 79 ++++++++++++++++++++++++++++++++++++-- src/terminal/modes.zig | 5 +++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/include/ghostty/vt/modes.h b/include/ghostty/vt/modes.h index db95a1a7d..8e1fd9117 100644 --- a/include/ghostty/vt/modes.h +++ b/include/ghostty/vt/modes.h @@ -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 */ diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index 52bb556e3..b939c7fed 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -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.?); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 0d8cde5b7..c92f97b08 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -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 }, From 203895e3f76af0e3f9a8e16778a706d08f6718f1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 11 Apr 2026 21:27:18 -0500 Subject: [PATCH 2/4] decbkm: address review comments * Don't alter Kitty keyboard protocol responses. Kitty does not support DECBKM so KKP doesn't take DECBKM into consideration. * Make better use of the function key lookup to control what sequence is returned when backspace is pressed using the legacy encoding. --- src/input/function_keys.zig | 7 +++-- src/input/key_encode.zig | 56 +++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/input/function_keys.zig b/src/input/function_keys.zig index efe86d9e3..66ab4bc4d 100644 --- a/src/input/function_keys.zig +++ b/src/input/function_keys.zig @@ -43,6 +43,9 @@ pub const Entry = struct { /// The sequence to send to the pty if this entry matches. sequence: []const u8, + + /// Sequence to send to the PTY if DECBKM is set. + sequence_decbkm: ?[]const u8 = null, }; /// The list of modifier combinations for modify other key sequences. @@ -161,8 +164,8 @@ pub const keys = keys: { .{ .mods = .{ .alt = true, .super = true, .ctrl = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;15;127~" }, .{ .mods = .{ .alt = true, .super = true, .ctrl = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;16;127~" }, - .{ .mods = .{ .ctrl = true }, .sequence = "\x08" }, - .{ .sequence = "\x7f" }, + .{ .mods = .{ .ctrl = true }, .sequence = "\x08", .sequence_decbkm = "\x7f" }, + .{ .sequence = "\x7f", .sequence_decbkm = "\x08" }, }); result.set(.tab, &.{ diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index b939c7fed..dc585d39d 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -189,9 +189,7 @@ fn kitty( switch (event.key) { .enter => return try writer.writeByte('\r'), .tab => return try writer.writeByte('\t'), - .backspace => return try writer.writeByte( - if (opts.backarrow_key_mode) 0x08 else 0x7F, - ), + .backspace => return try writer.writeByte(0x7F), else => {}, } } @@ -664,7 +662,10 @@ fn pcStyleFunctionKey( continue; } - if (keyval == .backspace and backarrow_key_mode) return "\x08"; + decbkm: { + if (!backarrow_key_mode) break :decbkm; + return entry.sequence_decbkm orelse break :decbkm; + } return entry.sequence; } @@ -1267,13 +1268,13 @@ test "kitty: enter, backspace, tab" { try testing.expectEqualStrings("\x7f", writer.buffered()); } { - // DECBKM set + // DECBKM set (Kitty does not support DECBKM so there should be no change) 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()); + try testing.expectEqualStrings("\x7f", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); @@ -1912,7 +1913,26 @@ test "legacy: backspace with utf8 (dead key state)" { try testing.expectEqualStrings("", writer.buffered()); } +test "kitty: backspace (DECBKM reset) (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 = false, + }); + try testing.expectEqualStrings("\x1b[127u", writer.buffered()); +} + test "kitty: backspace (DECBKM set) (report_all: true)" { + // Kitty does not support DECBKM so there should be no difference. var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ @@ -2091,6 +2111,18 @@ test "legacy: backspace (DECBKM reset)" { try testing.expectEqualStrings("\x7f", writer.buffered()); } +test "legacy: backspace (DECBKM reset, with ctrl)" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .mods = .{ + .ctrl = true, + }, + }, .{ .backarrow_key_mode = false }); + try testing.expectEqualStrings("\x08", writer.buffered()); +} + test "legacy: backspace (DECBKM set)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); @@ -2101,6 +2133,18 @@ test "legacy: backspace (DECBKM set)" { try testing.expectEqualStrings("\x08", writer.buffered()); } +test "legacy: backspace (DECBKM set, with ctrl)" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .mods = .{ + .ctrl = true, + }, + }, .{ .backarrow_key_mode = true }); + try testing.expectEqualStrings("\x7f", writer.buffered()); +} + test "legacy: ctrl+shift+char with modify other state 2" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); From de4992c2b273ea701008c2fe4a7b39e9276f6818 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 12 Apr 2026 00:46:43 -0500 Subject: [PATCH 3/4] decbkm: use if statements instead of named blocks --- src/input/key_encode.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index dc585d39d..6ab5a4cc8 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -662,10 +662,9 @@ fn pcStyleFunctionKey( continue; } - decbkm: { - if (!backarrow_key_mode) break :decbkm; - return entry.sequence_decbkm orelse break :decbkm; - } + if (backarrow_key_mode) + if (entry.sequence_decbkm) |sequence| + return sequence; return entry.sequence; } From 3a9ae7a0f28791e3ab451954c63d72142ad3ccf4 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 12 Apr 2026 16:04:16 -0500 Subject: [PATCH 4/4] decbkm: expose DECBKM to libghostty-vt --- include/ghostty/vt/key/encoder.h | 30 +++++++++++++++++++----------- src/terminal/c/key_encode.zig | 7 +++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/include/ghostty/vt/key/encoder.h b/include/ghostty/vt/key/encoder.h index dc9e27e7e..3aeec6597 100644 --- a/include/ghostty/vt/key/encoder.h +++ b/include/ghostty/vt/key/encoder.h @@ -87,24 +87,32 @@ typedef enum GHOSTTY_ENUM_TYPED { typedef enum GHOSTTY_ENUM_TYPED { /** Terminal DEC mode 1: cursor key application mode (value: bool) */ GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, - + /** Terminal DEC mode 66: keypad key application mode (value: bool) */ GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, - + /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, - + /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, - + /** xterm modifyOtherKeys mode 2 (value: bool) */ GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, - + /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, - + /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, + + /** Backarrow key mode (value: bool) + * See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170 + * If `false` (the default), `backspace` emits 0x7f + * If `true`, `backspace` emits 0x08 + */ + GHOSTTY_KEY_ENCODER_OPT_BACKARROW_KEY_MODE = 7, + GHOSTTY_KEY_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyKeyEncoderOption; @@ -205,17 +213,17 @@ GHOSTTY_API void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder enco * size_t required = 0; * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); * assert(result == GHOSTTY_OUT_OF_SPACE); - * + * * // Allocate buffer of required size * char *buf = malloc(required); - * + * * // Encode with properly sized buffer * size_t written = 0; * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); * assert(result == GHOSTTY_SUCCESS); - * + * * // Use the encoded sequence... - * + * * free(buf); * @endcode * @@ -226,7 +234,7 @@ GHOSTTY_API void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder enco * char buf[128]; * size_t written = 0; * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); - * + * * if (result == GHOSTTY_SUCCESS) { * // Write the encoded sequence to the terminal * write(pty_fd, buf, written); diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index 15fa74dd8..f5d459f01 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -52,6 +52,11 @@ pub const Option = enum(c_int) { modify_other_keys_state_2 = 4, kitty_flags = 5, macos_option_as_alt = 6, + /// 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 = 7, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -61,6 +66,7 @@ pub const Option = enum(c_int) { .ignore_keypad_with_numlock, .alt_esc_prefix, .modify_other_keys_state_2, + .backarrow_key_mode, => bool, .kitty_flags => u8, .macos_option_as_alt => OptionAsAlt, @@ -114,6 +120,7 @@ fn setoptTyped( } opts.macos_option_as_alt = value.*; }, + .backarrow_key_mode => opts.backarrow_key_mode = value.*, } }