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/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/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 52bb556e3..6ab5a4cc8 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(), @@ -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.?); 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.*, } } 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 },