mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-24 05:40:15 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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, &.{
|
||||
|
||||
@@ -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.?);
|
||||
|
||||
@@ -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.*,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user