cmux/Sources/KeyboardLayout.swift
Lawrence Chen 6347571b7c
Fix Dvorak Cmd+C colliding with notifications shortcut (#762)
* Fix layout-safe command shortcut matching

* Fix unshifted symbol shortcut coercion

* Fix layout handling for hardcoded Cmd shortcuts

* Support Cmd/Ctrl modifier hold shortcut hints

* Address PR shortcut review feedback

* Handle Caps Lock in command shortcut matching

* Address remaining PR shortcut review comments

* Handle Cmd+Ctrl+F control-character fallback

* Tighten shortcut hint hold-delay test

* Expose shortcut hint visibility in settings

* Restore Cmd+digit fallback on symbol-first layouts

* Stabilize shortcut regression coverage

* Align shortcut hint toggle with Cmd/Ctrl behavior

* Restore Claude workflow file

* Match Claude workflow file to main

* Keep shortcut hint hold behavior command-only

* Restore command shortcut fallback when layout data is unavailable

* Preserve command-aware shortcut translation
2026-03-05 18:32:42 -08:00

67 lines
2.4 KiB
Swift

import AppKit
import Carbon
class KeyboardLayout {
/// Return a string ID of the current keyboard input source.
static var id: String? {
if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) {
let sourceId = Unmanaged<CFString>.fromOpaque(sourceIdPointer).takeUnretainedValue()
return sourceId as String
}
return nil
}
/// Translate a physical keyCode to the character AppKit would use for shortcut matching,
/// preserving command-aware layouts such as "Dvorak - QWERTY Command".
static func character(
forKeyCode keyCode: UInt16,
modifierFlags: NSEvent.ModifierFlags = []
) -> String? {
guard let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
return nil
}
let layoutData = unsafeBitCast(layoutDataPointer, to: CFData.self)
guard let bytes = CFDataGetBytePtr(layoutData) else { return nil }
let keyboardLayout = UnsafeRawPointer(bytes).assumingMemoryBound(to: UCKeyboardLayout.self)
var deadKeyState: UInt32 = 0
var chars = [UniChar](repeating: 0, count: 4)
var length = 0
let status = UCKeyTranslate(
keyboardLayout,
keyCode,
UInt16(kUCKeyActionDisplay),
translationModifierKeyState(for: modifierFlags),
UInt32(LMGetKbdType()),
UInt32(kUCKeyTranslateNoDeadKeysBit),
&deadKeyState,
chars.count,
&length,
&chars
)
guard status == noErr, length > 0 else { return nil }
return String(utf16CodeUnits: chars, count: length).lowercased()
}
private static func translationModifierKeyState(for modifierFlags: NSEvent.ModifierFlags) -> UInt32 {
let normalized = modifierFlags
.intersection(.deviceIndependentFlagsMask)
.intersection([.shift, .command])
var carbonModifiers: Int = 0
if normalized.contains(.shift) {
carbonModifiers |= shiftKey
}
if normalized.contains(.command) {
carbonModifiers |= cmdKey
}
return UInt32((carbonModifiers >> 8) & 0xFF)
}
}