From f4309055fbb8cfd74bf0559a054e5eb7ddc361d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 17 Aug 2026 06:38:21 -0700 Subject: [PATCH] macos: don't put 0x7F as text in key event Fixes #13869 We already checked `< 0x20` but missed `0x7F` which causes similar problems. --- .../Surface View/SurfaceView_AppKit.swift | 11 ++++++----- .../Helpers/Extensions/String+Extension.swift | 6 ++++++ src/input/key_encode.zig | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 9912278e6..0dbdeefef 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1484,11 +1484,12 @@ extension Ghostty { var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing - // For text, we only encode UTF8 if we don't have a single control - // character. Control characters are encoded by Ghostty itself. - // Without this, `ctrl+enter` does the wrong thing. - if let text, text.count > 0, - let codepoint = text.utf8.first, codepoint >= 0x20 { + // Control characters are encoded by Ghostty itself so that the + // physical key and its modifiers remain available to protocols + // such as the Kitty keyboard protocol. + if let text, + !text.isEmpty, + !text.startsWithASCIIControlCharacter { return text.withCString { ptr in key_ev.text = ptr return ghostty_surface_key(surface, key_ev) diff --git a/macos/Sources/Helpers/Extensions/String+Extension.swift b/macos/Sources/Helpers/Extensions/String+Extension.swift index beb5d87e7..491c18b2f 100644 --- a/macos/Sources/Helpers/Extensions/String+Extension.swift +++ b/macos/Sources/Helpers/Extensions/String+Extension.swift @@ -1,4 +1,10 @@ extension String { + /// True when the first scalar is an ASCII control character (C0 or DEL). + var startsWithASCIIControlCharacter: Bool { + guard let scalar = unicodeScalars.first else { return false } + return scalar.value < 0x20 || scalar.value == 0x7F + } + func truncate(length: Int, trailing: String = "…") -> String { let maxLength = length - trailing.count guard maxLength > 0, !self.isEmpty, self.count > length else { diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index fd388452d..18e4a1efe 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -1359,6 +1359,22 @@ test "kitty: shift+backspace emits CSI u" { try testing.expectEqualStrings("\x1b[127;2u", writer.buffered()); } +test "kitty: alt+backspace emits CSI u" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .backspace, + .mods = .{ .alt = true }, + // macOS may mark Option as consumed while translating the key. With + // no attached control text, all modifiers must remain effective. + .consumed_mods = .{ .alt = true }, + .utf8 = "", + }, .{ + .kitty_flags = .{ .disambiguate = true }, + }); + try testing.expectEqualStrings("\x1b[127;3u", writer.buffered()); +} + test "kitty: shift+enter emits CSI u" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf);