From 4b164b4a291d40b8315a230f7a71807a1c040b7f Mon Sep 17 00:00:00 2001 From: Evgeny Ruban Date: Thu, 9 Jul 2026 15:35:34 +0400 Subject: [PATCH] fix-non-latin-layout - --- .../Ghostty/GhosttySurfaceView.swift | 30 ++++++++++- supacodeTests/GhosttySurfaceViewTests.swift | 52 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift index 1dc22cd29..b2ffe731f 100644 --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift @@ -179,6 +179,25 @@ final class GhosttySurfaceView: NSView, Identifiable { .fileURL, .URL, ] + private static let layoutIndependentControlCharactersByKeyCode: [UInt16: String] = { + let candidates = "abcdefghijklmnopqrstuvwxyz0123456789[]\\;',./-=`" + var map: [UInt16: String] = [:] + for scalar in candidates.unicodeScalars { + guard let keyCode = AppShortcutOverride.keyCode(for: Character(scalar)) else { continue } + map[keyCode] = String(scalar) + } + map[UInt16(kVK_Space)] = " " + return map + }() + + static func layoutIndependentControlCharacter( + keyCode: UInt16, + modifiers: NSEvent.ModifierFlags + ) -> String? { + let shortcutModifiers = modifiers.intersection([.shift, .control, .option, .command]) + guard shortcutModifiers == [.control] else { return nil } + return layoutIndependentControlCharactersByKeyCode[keyCode] + } static func normalizedWorkingDirectoryPath(_ path: String) -> String { var normalized = path @@ -1514,7 +1533,10 @@ final class GhosttySurfaceView: NSView, Identifiable { translationMods: resolvedMods, composing: composing ) - let finalText = text ?? ghosttyCharacters(resolvedEvent) + let finalText = + Self.layoutIndependentControlCharacter(keyCode: event.keyCode, modifiers: event.modifierFlags) + ?? text + ?? ghosttyCharacters(resolvedEvent) if let finalText, !finalText.isEmpty, let codepoint = finalText.utf8.first, codepoint >= 0x20 { @@ -1607,7 +1629,11 @@ final class GhosttySurfaceView: NSView, Identifiable { keyEvent.consumed_mods = ghosttyMods(translationMods.subtracting([.control, .command])) keyEvent.unshifted_codepoint = 0 if event.type == .keyDown || event.type == .keyUp { - if let chars = event.characters(byApplyingModifiers: []), + if let chars = Self.layoutIndependentControlCharacter(keyCode: event.keyCode, modifiers: originalMods), + let codepoint = chars.unicodeScalars.first + { + keyEvent.unshifted_codepoint = codepoint.value + } else if let chars = event.characters(byApplyingModifiers: []), let codepoint = chars.unicodeScalars.first { keyEvent.unshifted_codepoint = codepoint.value diff --git a/supacodeTests/GhosttySurfaceViewTests.swift b/supacodeTests/GhosttySurfaceViewTests.swift index aa88af245..70c338fb0 100644 --- a/supacodeTests/GhosttySurfaceViewTests.swift +++ b/supacodeTests/GhosttySurfaceViewTests.swift @@ -1,4 +1,5 @@ import AppKit +import Carbon import Foundation import GhosttyKit import SupacodeSettingsShared @@ -80,6 +81,57 @@ struct GhosttySurfaceViewTests { #expect(suppression.isExpired(at: 11.1)) } + @Test func layoutIndependentControlCharacterUsesPhysicalUSQwertyKey() { + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_ANSI_C), + modifiers: [.control] + ) == "c" + ) + } + + @Test func layoutIndependentControlCharacterIncludesTerminalPunctuationAndSpace() { + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_ANSI_Slash), + modifiers: [.control] + ) == "/" + ) + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_Space), + modifiers: [.control] + ) == " " + ) + } + + @Test func layoutIndependentControlCharacterRequiresPlainControl() { + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_ANSI_C), + modifiers: [] + ) == nil + ) + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_ANSI_C), + modifiers: [.control, .shift] + ) == nil + ) + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_ANSI_C), + modifiers: [.control, .command] + ) == nil + ) + #expect( + GhosttySurfaceView.layoutIndependentControlCharacter( + keyCode: UInt16(kVK_ANSI_C), + modifiers: [.control, .option] + ) == nil + ) + } + private static func keyEvent( chars: String, ignoringModifiers: String,