Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,8 @@ GHOSTTY_API bool ghostty_config_get(ghostty_config_t, void*, const char*, uintpt
GHOSTTY_API ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
const char*,
uintptr_t);
GHOSTTY_API bool ghostty_config_trigger_is_unbound(ghostty_config_t,
ghostty_input_trigger_s);
GHOSTTY_API uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
GHOSTTY_API ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
GHOSTTY_API ghostty_string_s ghostty_config_open_path(void);
Expand Down
13 changes: 10 additions & 3 deletions macos/Sources/App/macOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class AppDelegate: NSObject,
/// The custom app icon image that is currently in use.
@Published private(set) var appIcon: NSImage?

@MainActor private lazy var menuShortcutManager = Ghostty.MenuShortcutManager()
@MainActor private var menuShortcutManager = Ghostty.MenuShortcutManager(nil)

override init() {
#if DEBUG
Expand Down Expand Up @@ -210,6 +210,8 @@ class AppDelegate: NSObject,
toggleSecureInput(self)
}

menuShortcutManager = .init(NSApp.mainMenu)

// Initial config loading
ghosttyConfigDidChange(config: ghostty.config)

Expand Down Expand Up @@ -1144,8 +1146,13 @@ extension AppDelegate {
@MainActor private func syncMenuShortcuts(_ config: Ghostty.Config) {
guard ghostty.readiness == .ready else { return }

menuShortcutManager.reset()
menuShortcutManager.resetRegisteredGhosttyActions()

registerGhosttyActions(config)
menuShortcutManager.updateShortcut(in: NSApp.mainMenu, config: config)
}

@MainActor private func registerGhosttyActions(_ config: Ghostty.Config) {
syncMenuShortcut(config, action: "check_for_updates", menuItem: self.menuCheckForUpdates)
syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig)
syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig)
Expand Down Expand Up @@ -1212,7 +1219,7 @@ extension AppDelegate {
}

@MainActor private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) {
menuShortcutManager.syncMenuShortcut(config, action: action, menuItem: menuItem)
menuShortcutManager.register(action: action, menuItem: menuItem)
}

@MainActor func performGhosttyBindingMenuKeyEquivalent(with event: NSEvent) -> Bool {
Expand Down
10 changes: 10 additions & 0 deletions macos/Sources/Ghostty/Ghostty.Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ extension Ghostty {
let trigger = ghostty_config_trigger(cfg, action, UInt(action.lengthOfBytes(using: .utf8)))
return Ghostty.keyboardShortcut(for: trigger)
}

/// Check whether a keyboard shortcut is unbound in the config by `keybind=a=unbind`
func isKeyboardShortcutUnbound(_ keyboardShortcut: KeyboardShortcut) -> Bool {
guard
let cfg = self.config,
let trigger = Ghostty.ghosttyTrigger(keyboardShortcut)
else { return false }

return ghostty_config_trigger_is_unbound(cfg, trigger)
}
#endif

// MARK: - Configuration Values
Expand Down
31 changes: 31 additions & 0 deletions macos/Sources/Ghostty/Ghostty.Input.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ extension Ghostty {
modifiers: EventModifiers(nsFlags: Ghostty.eventModifierFlags(mods: trigger.mods)))
}

/// Map the KeyboardShortcut to `ghostty_input_trigger_s`
/// which is basically reversed from ``keyboardShortcut(for:)``
static func ghosttyTrigger(_ keyboardShortcut: KeyboardShortcut) -> ghostty_input_trigger_s? {

let flags = NSEvent.ModifierFlags(swiftUIFlags: keyboardShortcut.modifiers)
let mods = ghosttyMods(flags)
if let physicalKey = Self.equivalentToKey[keyboardShortcut.key.character] {
return ghostty_input_trigger_s(
tag: GHOSTTY_TRIGGER_PHYSICAL,
key: .init(physical: physicalKey),
mods: mods,
)
} else {
guard let unicodeValue = keyboardShortcut.key.character.unicodeScalars.first?.value else {
return nil
}
return ghostty_input_trigger_s(
tag: GHOSTTY_TRIGGER_UNICODE,
key: .init(unicode: unicodeValue),
mods: mods,
)
}
}

// MARK: Mods

/// Returns the event modifier flags set for the Ghostty mods enum.
Expand Down Expand Up @@ -81,9 +105,16 @@ extension Ghostty {
return ghostty_input_mods_e(mods)
}

/// Reverse map: KeyEquivalent character → ghostty_input_key_e, built lazily from `keyToEquivalent`.
private static let equivalentToKey: [Character: ghostty_input_key_e] = {
Dictionary(uniqueKeysWithValues: keyToEquivalent.map { ($0.value.character, $0.key) })
}()

/// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. Note that
/// not all ghostty key enum values are represented here because not all of them can be
/// mapped to a KeyEquivalent.
///
/// - Important: `KeyEquivalent` here should NOT be uppercased by any means
static let keyToEquivalent: [ghostty_input_key_e: KeyEquivalent] = [
// Function keys
GHOSTTY_KEY_ARROW_UP: .upArrow,
Expand Down
Loading
Loading