From 76e994e3af9ca151f4d93eabf83ede3435466368 Mon Sep 17 00:00:00 2001 From: Tim Kersten Date: Tue, 25 Aug 2026 11:19:53 +0200 Subject: [PATCH] fix(macos): resolve paste/copy key codes for non-QWERTY layouts macos-fast-paste.swift hardcoded 0x09 for 'v' and 0x08 for 'c', which are only correct on QWERTY. On Dvorak, Colemak and similar layouts the posted Cmd+V / Cmd+C landed on the wrong key, so paste and selection capture silently did the wrong thing. Look the key code up in the current keyboard layout via UCKeyTranslate instead. Falls back to the QWERTY key code for the same mode if the lookup fails, so a failed lookup never turns a copy into a paste. --- resources/macos-fast-paste.swift | 42 +++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/resources/macos-fast-paste.swift b/resources/macos-fast-paste.swift index faeba79d2e..040e66ce0e 100644 --- a/resources/macos-fast-paste.swift +++ b/resources/macos-fast-paste.swift @@ -1,14 +1,54 @@ import Cocoa +import Carbon if !AXIsProcessTrusted() { exit(2) } +// Look up which key code produces a given character in the current keyboard +// layout. Hardcoded key codes (0x09 for 'v', 0x08 for 'c') are only correct on +// QWERTY, so Cmd+V / Cmd+C land on the wrong key on Dvorak, Colemak, etc. +func keyCodeForCharacter(_ target: UniChar) -> CGKeyCode? { + guard let sourceRef = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(), + let layoutDataRef = TISGetInputSourceProperty(sourceRef, kTISPropertyUnicodeKeyLayoutData) else { + return nil + } + let layoutData = unsafeBitCast(layoutDataRef, to: CFData.self) + let keyLayout = unsafeBitCast(CFDataGetBytePtr(layoutData), to: UnsafePointer.self) + + for keyCode: UInt16 in 0..<128 { + var deadKeyState: UInt32 = 0 + var chars = [UniChar](repeating: 0, count: 4) + var length = 0 + + let status = UCKeyTranslate( + keyLayout, + keyCode, + UInt16(kUCKeyActionDown), + 0, + UInt32(LMGetKbdType()), + UInt32(kUCKeyTranslateNoDeadKeysBit), + &deadKeyState, + 4, + &length, + &chars + ) + + if status == noErr && length > 0 && chars[0] == target { + return CGKeyCode(keyCode) + } + } + return nil +} + // Selection capture sends ⌘C and reports which app received it, so the caller // can tell a copied selection from a target that changed underneath it. With no // arguments this stays what the paste path expects: ⌘V, no output. let copyMode = CommandLine.arguments.contains("--copy") -let virtualKey: CGKeyCode = copyMode ? 0x08 : 0x09 // kVK_ANSI_C : kVK_ANSI_V +// 0x0063 = 'c', 0x0076 = 'v'. Fall back to the QWERTY key code for the same +// mode if the layout lookup fails, never to the other mode's key. +let virtualKey: CGKeyCode = keyCodeForCharacter(copyMode ? 0x0063 : 0x0076) + ?? (copyMode ? 0x08 : 0x09) // kVK_ANSI_C : kVK_ANSI_V // Resolved before the keystroke is posted: this is the app that will receive it. let target = copyMode ? NSWorkspace.shared.frontmostApplication : nil