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

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`.

<img width="659" height="715" alt="Screenshot From 2026-04-09 11-00-25"
src="https://github.com/user-attachments/assets/4f3e14ac-757d-4bb2-9fc5-b17019ad35d5"
/>
This commit is contained in:
Mitchell Hashimoto
2026-04-13 06:43:30 -07:00
committed by GitHub
6 changed files with 155 additions and 15 deletions

View File

@@ -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);

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

@@ -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, &.{

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(),
@@ -338,6 +345,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 +609,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 +662,10 @@ fn pcStyleFunctionKey(
continue;
}
if (backarrow_key_mode)
if (entry.sequence_decbkm) |sequence|
return sequence;
return entry.sequence;
}
@@ -1245,9 +1258,20 @@ 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 (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("\x7f", writer.buffered());
}
@@ -1888,6 +1912,43 @@ 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, .{
.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 +2100,50 @@ 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 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);
try legacy(&writer, .{
.key = .backspace,
.mods = .{},
}, .{ .backarrow_key_mode = true });
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);
@@ -2355,17 +2460,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

@@ -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.*,
}
}

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 },