bd44b5aa08
Test / build-libghostty-vt (x86_64-windows-gnu) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-ios) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt-macos (x86_64-macos) (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-dist (push) Has been cancelled
Test / GTK x11=true wayland=true (push) Has been cancelled
Test / Build -Dsimd=false (push) Has been cancelled
Test / Build -Dsimd=true (push) Has been cancelled
Test / Build -Dsentry=false (push) Has been cancelled
Test / Build -Dsentry=true (push) Has been cancelled
Test / zig-fmt (push) Has been cancelled
Test / GitHub Actions Pins (push) Has been cancelled
Test / prettier (push) Has been cancelled
Test / swiftlint (push) Has been cancelled
Test / alejandra (push) Has been cancelled
Test / typos (push) Has been cancelled
Nix / Required Checks: Nix (push) Has been cancelled
Nix / check-zig-cache-hash (push) Has been cancelled
Test / skip (push) Has been cancelled
Test / Required Checks: Test (push) Has been cancelled
Test / build-bench (push) Has been cancelled
Test / list-examples (push) Has been cancelled
Test / Example ${{ matrix.dir }} (Windows) (push) Has been cancelled
Test / build-cmake (push) Has been cancelled
Test / test-lib-vt-pkgconfig (push) Has been cancelled
Test / build-flatpak (push) Has been cancelled
Test / build-snap (push) Has been cancelled
Test / build-libghostty-vt (aarch64-linux) (push) Has been cancelled
Test / build-libghostty-vt (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt (wasm32-freestanding) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux-musl) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-macos) (push) Has been cancelled
Test / build-libghostty-vt-android (aarch64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-android (arm-linux-androideabi) (push) Has been cancelled
Test / build-libghostty-vt-android (x86_64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-windows (push) Has been cancelled
Test / build-libghostty-windows-gnu (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-linux-libghostty (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-dist-lib-vt (push) Has been cancelled
Test / trigger-snap (push) Has been cancelled
Test / trigger-flatpak (push) Has been cancelled
Test / build-macos (push) Has been cancelled
Test / build-macos-freetype (push) Has been cancelled
Test / test (push) Has been cancelled
Test / test-lib-vt (push) Has been cancelled
Test / GTK x11=false wayland=false (push) Has been cancelled
Test / GTK x11=true wayland=false (push) Has been cancelled
Test / GTK x11=false wayland=true (push) Has been cancelled
Test / test-macos (push) Has been cancelled
Test / test-windows (push) Has been cancelled
Test / Build -Di18n=false (push) Has been cancelled
Test / Build -Di18n=true (push) Has been cancelled
Test / Build test/fuzz-libghostty (push) Has been cancelled
Test / shellcheck (push) Has been cancelled
Test / translations (push) Has been cancelled
Test / blueprint-compiler (push) Has been cancelled
Test / Test pkg/wuffs (push) Has been cancelled
Test / Test build on Debian 13 (push) Has been cancelled
Test / valgrind (push) Has been cancelled
Test / Example ${{ matrix.dir }} (push) Has been cancelled
163 lines
5.5 KiB
Swift
163 lines
5.5 KiB
Swift
import Cocoa
|
|
import CoreGraphics
|
|
import Carbon
|
|
import OSLog
|
|
import GhosttyKit
|
|
|
|
// Manages the event tap to monitor global events, currently only used for
|
|
// global keybindings.
|
|
class GlobalEventTap {
|
|
static let shared = GlobalEventTap()
|
|
|
|
fileprivate static let logger = Logger(
|
|
subsystem: Bundle.main.bundleIdentifier!,
|
|
category: String(describing: GlobalEventTap.self)
|
|
)
|
|
|
|
// The event tap used for global event listening. This is non-nil if it is
|
|
// created.
|
|
fileprivate var eventTap: CFMachPort?
|
|
|
|
// This is the timer used to retry enabling the global event tap if we
|
|
// don't have permissions.
|
|
private var enableTimer: Timer?
|
|
|
|
// Private init so it can't be constructed outside of our singleton
|
|
private init() {}
|
|
|
|
deinit {
|
|
disable()
|
|
}
|
|
|
|
// Enable the global event tap. This is safe to call if it is already enabled.
|
|
// If enabling fails due to permissions, this will start a timer to retry since
|
|
// accessibility permissions take affect immediately.
|
|
func enable() {
|
|
if eventTap != nil {
|
|
// Already enabled
|
|
return
|
|
}
|
|
|
|
// If we are already trying to enable, then stop the timer and restart it.
|
|
if let enableTimer {
|
|
enableTimer.invalidate()
|
|
}
|
|
|
|
// Try to enable the event tap immediately. If this succeeds then we're done!
|
|
if tryEnable() {
|
|
return
|
|
}
|
|
|
|
// Failed, probably due to permissions. The permissions dialog should've
|
|
// popped up. We retry on a timer since once the permissions are granted
|
|
// then they take affect immediately.
|
|
enableTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
|
_ = self.tryEnable()
|
|
}
|
|
}
|
|
|
|
// Disable the global event tap. This is safe to call if it is already disabled.
|
|
func disable() {
|
|
// Stop our enable timer if it is on
|
|
if let enableTimer {
|
|
enableTimer.invalidate()
|
|
self.enableTimer = nil
|
|
}
|
|
|
|
// Stop our event tap
|
|
if let eventTap {
|
|
Self.logger.debug("invalidating event tap mach port")
|
|
CFMachPortInvalidate(eventTap)
|
|
self.eventTap = nil
|
|
}
|
|
}
|
|
|
|
// Try to enable the global event type, returns false if it fails.
|
|
private func tryEnable() -> Bool {
|
|
// The events we care about
|
|
let eventMask = [
|
|
CGEventType.keyDown
|
|
].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue)})
|
|
|
|
// Try to create it
|
|
guard let eventTap = CGEvent.tapCreate(
|
|
tap: .cgSessionEventTap,
|
|
place: .headInsertEventTap,
|
|
options: .defaultTap,
|
|
eventsOfInterest: eventMask,
|
|
callback: cgEventFlagsChangedHandler(proxy:type:cgEvent:userInfo:),
|
|
userInfo: nil
|
|
) else {
|
|
// Return false if creation failed. This is usually because we don't have
|
|
// Accessibility permissions but can probably be other reasons I don't
|
|
// know about.
|
|
Self.logger.debug("creating global event tap failed, missing permissions?")
|
|
return false
|
|
}
|
|
|
|
// Store our event tap
|
|
self.eventTap = eventTap
|
|
|
|
// If we have an enable timer we always want to disable it
|
|
if let enableTimer {
|
|
enableTimer.invalidate()
|
|
self.enableTimer = nil
|
|
}
|
|
|
|
// Attach our event tap to the main run loop. Note if you don't do this then
|
|
// the event tap will block every
|
|
CFRunLoopAddSource(
|
|
CFRunLoopGetMain(),
|
|
CFMachPortCreateRunLoopSource(nil, eventTap, 0),
|
|
.commonModes
|
|
)
|
|
|
|
Self.logger.info("global event tap enabled for global keybinds")
|
|
return true
|
|
}
|
|
}
|
|
|
|
private func cgEventFlagsChangedHandler(
|
|
proxy: CGEventTapProxy,
|
|
type: CGEventType,
|
|
cgEvent: CGEvent,
|
|
userInfo: UnsafeMutableRawPointer?
|
|
) -> Unmanaged<CGEvent>? {
|
|
let result = Unmanaged.passUnretained(cgEvent)
|
|
|
|
// macOS disables the event tap if the callback is too slow or for other
|
|
// internal reasons. When that happens it sends this event type. We need
|
|
// to re-enable the tap or it stays dead forever.
|
|
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
|
|
GlobalEventTap.logger.warning("global event tap was disabled by the system, re-enabling")
|
|
if let machPort = GlobalEventTap.shared.eventTap {
|
|
CGEvent.tapEnable(tap: machPort, enable: true)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// We only care about keydown events
|
|
guard type == .keyDown else { return result }
|
|
|
|
// If our app is currently active then we don't process the key event.
|
|
// This is because we already have a local event handler in AppDelegate
|
|
// that processes all local events.
|
|
guard !NSApp.isActive else { return result }
|
|
|
|
// We need an app delegate to get the Ghostty app instance
|
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return result }
|
|
guard let ghostty = appDelegate.ghostty.app else { return result }
|
|
|
|
// We need an NSEvent for our logic below
|
|
guard let event: NSEvent = .init(cgEvent: cgEvent) else { return result }
|
|
|
|
// Build our event input and call ghostty
|
|
let key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)
|
|
if ghostty_app_key(ghostty, key_ev) {
|
|
GlobalEventTap.logger.info("global key event handled event=\(event, privacy: .public)")
|
|
return nil
|
|
}
|
|
|
|
return result
|
|
}
|