mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 00:21:46 +00:00
macos: enforce keyboard layout actor isolation
Text Input Sources APIs are not thread-safe, but shortcut translation could be called outside a declared main-actor context. Mark keyboard layout and shortcut conversion as main-actor isolated, update their tests, and dispatch key-sequence UI notifications to the main queue before translating their shortcuts.
This commit is contained in:
@@ -1985,19 +1985,21 @@ extension Ghostty {
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
if v.active {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didContinueKeySequence,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.KeySequenceKey: keyboardShortcut(for: v.trigger) as Any
|
||||
]
|
||||
)
|
||||
} else {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didEndKeySequence,
|
||||
object: surfaceView
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
if v.active {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didContinueKeySequence,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.KeySequenceKey: keyboardShortcut(for: v.trigger) as Any
|
||||
]
|
||||
)
|
||||
} else {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didEndKeySequence,
|
||||
object: surfaceView
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -111,7 +111,7 @@ extension Ghostty {
|
||||
/// configuration would be "quit" action.
|
||||
///
|
||||
/// Returns nil if there is no key equivalent for the given action.
|
||||
func keyboardShortcut(for action: String) -> KeyboardShortcut? {
|
||||
@MainActor func keyboardShortcut(for action: String) -> KeyboardShortcut? {
|
||||
guard let trigger = keybindTrigger(for: action) else { return nil }
|
||||
return Ghostty.keyboardShortcut(for: trigger)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ extension Ghostty {
|
||||
/// (F1, F2, ...) with a KeyboardShortcut. This doesn't represent a practical issue because input
|
||||
/// handling for Ghostty is handled at a lower level (usually). This function should generally only
|
||||
/// be used for things like NSMenu that only support keyboard shortcuts anyways.
|
||||
static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? {
|
||||
@MainActor static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? {
|
||||
let modifierFlags = Self.eventModifierFlags(mods: trigger.mods)
|
||||
let key: KeyEquivalent
|
||||
switch trigger.tag {
|
||||
|
||||
@@ -16,7 +16,7 @@ class KeyboardLayout {
|
||||
/// Translate a physical keycode for use as a menu key equivalent.
|
||||
///
|
||||
/// Must be called on the main thread because Text Input Sources APIs are not thread-safe.
|
||||
static func character(
|
||||
@MainActor static func character(
|
||||
for keyCode: UInt16,
|
||||
modifiers: NSEvent.ModifierFlags
|
||||
) -> Character? {
|
||||
|
||||
@@ -223,8 +223,8 @@ struct ConfigTests {
|
||||
|
||||
// MARK: - Keybind
|
||||
|
||||
@Test
|
||||
func uppercasedLetterShouldBeNormalized() async throws {
|
||||
@MainActor @Test
|
||||
func uppercasedLetterShouldBeNormalized() throws {
|
||||
let config = try TemporaryConfig("""
|
||||
keybind=cmd+L=goto_split:left
|
||||
""")
|
||||
@@ -238,8 +238,8 @@ struct ConfigTests {
|
||||
#expect(shortcut2 == .init("ä", modifiers: [.command]))
|
||||
}
|
||||
|
||||
@Test
|
||||
func emptyConfigShouldBeHaveDefaultShortcut() async throws {
|
||||
@MainActor @Test
|
||||
func emptyConfigShouldBeHaveDefaultShortcut() throws {
|
||||
let config = try TemporaryConfig("")
|
||||
let newWindow = try #require(config.keyboardShortcut(for: "new_window"))
|
||||
#expect(newWindow == .init("n", modifiers: [.command]))
|
||||
|
||||
@@ -26,14 +26,14 @@ struct MenuShortcutManagerTests {
|
||||
#expect(item.keyEquivalentModifierMask == .command)
|
||||
}
|
||||
|
||||
@Test func physicalBackquoteUsesCurrentKeyboardLayout() async throws {
|
||||
@MainActor @Test func physicalBackquoteUsesCurrentKeyboardLayout() throws {
|
||||
let config = try TemporaryConfig("keybind=super+backquote=toggle_quick_terminal")
|
||||
let expected = try #require(KeyboardLayout.character(for: 0x32, modifiers: .command))
|
||||
let item = NSMenuItem(title: "Quick Terminal", action: nil, keyEquivalent: "")
|
||||
let manager = await Ghostty.MenuShortcutManager()
|
||||
let manager = Ghostty.MenuShortcutManager()
|
||||
|
||||
await manager.reset()
|
||||
await manager.syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: item)
|
||||
manager.reset()
|
||||
manager.syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: item)
|
||||
|
||||
#expect(item.keyEquivalent == String(expected))
|
||||
#expect(item.keyEquivalentModifierMask == .command)
|
||||
|
||||
Reference in New Issue
Block a user