From 8994a8c6276e01d18a37ad4b99f8babdf7260706 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Oct 2024 21:54:22 -0700 Subject: [PATCH] macos: implement key sequence UI --- macos/Sources/Ghostty/Ghostty.App.swift | 35 +++++++++++- macos/Sources/Ghostty/Ghostty.Config.swift | 47 +-------------- macos/Sources/Ghostty/Ghostty.Input.swift | 57 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 7 ++- macos/Sources/Ghostty/SurfaceView.swift | 28 +++++++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 23 ++++++++ 6 files changed, 149 insertions(+), 48 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 82a08f666..a2ed8903a 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -516,7 +516,8 @@ extension Ghostty { toggleVisibility(app, target: target) case GHOSTTY_ACTION_KEY_SEQUENCE: - fallthrough + keySequence(app, target: target, v: action.action.key_sequence) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -1073,6 +1074,38 @@ extension Ghostty { } } + private static func keySequence( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_key_sequence_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("key sequence does nothing with an app target") + return + + 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: keyEquivalent(for: v.trigger) as Any + ] + ) + } else { + NotificationCenter.default.post( + name: Notification.didEndKeySequence, + object: surfaceView + ) + } + + default: + assertionFailure() + } + } + // MARK: User Notifications /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 99e08bf9d..95f8ad734 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -87,25 +87,6 @@ extension Ghostty { #if os(macOS) // MARK: - Keybindings - /// A convenience struct that has the key + modifiers for some keybinding. - struct KeyEquivalent: CustomStringConvertible { - let key: String - let modifiers: NSEvent.ModifierFlags - - var description: String { - var key = self.key - - // Note: the order below matters; it matches the ordering modifiers - // shown for macOS menu shortcut labels. - if modifiers.contains(.command) { key = "⌘\(key)" } - if modifiers.contains(.shift) { key = "⇧\(key)" } - if modifiers.contains(.option) { key = "⌥\(key)" } - if modifiers.contains(.control) { key = "⌃\(key)" } - - return key - } - } - /// Return the key equivalent for the given action. The action is the name of the action /// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty /// configuration would be "quit" action. @@ -115,33 +96,7 @@ extension Ghostty { guard let cfg = self.config else { return nil } let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) - let equiv: String - switch (trigger.tag) { - case GHOSTTY_TRIGGER_TRANSLATED: - if let v = Ghostty.keyEquivalent(key: trigger.key.translated) { - equiv = v - } else { - return nil - } - - case GHOSTTY_TRIGGER_PHYSICAL: - if let v = Ghostty.keyEquivalent(key: trigger.key.physical) { - equiv = v - } else { - return nil - } - - case GHOSTTY_TRIGGER_UNICODE: - equiv = String(trigger.key.unicode) - - default: - return nil - } - - return KeyEquivalent( - key: equiv, - modifiers: Ghostty.eventModifierFlags(mods: trigger.mods) - ) + return Ghostty.keyEquivalent(for: trigger) } #endif diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index d7fd96f12..43bf8d096 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -2,11 +2,68 @@ import Cocoa import GhosttyKit extension Ghostty { + // MARK: Key Equivalents + /// Returns the "keyEquivalent" string for a given input key. This doesn't always have a corresponding key. static func keyEquivalent(key: ghostty_input_key_e) -> String? { return Self.keyToEquivalent[key] } + /// A convenience struct that has the key + modifiers for some keybinding. + struct KeyEquivalent: CustomStringConvertible { + let key: String + let modifiers: NSEvent.ModifierFlags + + var description: String { + var key = self.key + + // Note: the order below matters; it matches the ordering modifiers + // shown for macOS menu shortcut labels. + if modifiers.contains(.command) { key = "⌘\(key)" } + if modifiers.contains(.shift) { key = "⇧\(key)" } + if modifiers.contains(.option) { key = "⌥\(key)" } + if modifiers.contains(.control) { key = "⌃\(key)" } + + return key + } + } + + /// Return the key equivalent for the given trigger. + /// + /// Returns nil if the trigger can't be processed. This should only happen for unknown trigger types + /// or keys. + static func keyEquivalent(for trigger: ghostty_input_trigger_s) -> KeyEquivalent? { + let equiv: String + switch (trigger.tag) { + case GHOSTTY_TRIGGER_TRANSLATED: + if let v = Ghostty.keyEquivalent(key: trigger.key.translated) { + equiv = v + } else { + return nil + } + + case GHOSTTY_TRIGGER_PHYSICAL: + if let v = Ghostty.keyEquivalent(key: trigger.key.physical) { + equiv = v + } else { + return nil + } + + case GHOSTTY_TRIGGER_UNICODE: + equiv = String(trigger.key.unicode) + + default: + return nil + } + + return KeyEquivalent( + key: equiv, + modifiers: Ghostty.eventModifierFlags(mods: trigger.mods) + ) + } + + // MARK: Mods + /// Returns the event modifier flags set for the Ghostty mods enum. static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { var flags = NSEvent.ModifierFlags(rawValue: 0); diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 1c5695319..63a41596a 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -262,7 +262,12 @@ extension Ghostty.Notification { /// Notification that renderer health changed static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth") + + /// Notifications related to key sequences + static let didContinueKeySequence = Notification.Name("com.mitchellh.ghostty.didContinueKeySequence") + static let didEndKeySequence = Notification.Name("com.mitchellh.ghostty.didEndKeySequence") + static let KeySequenceKey = didContinueKeySequence.rawValue + ".key" } // Make the input enum hashable. -extension ghostty_input_key_e : Hashable {} +extension ghostty_input_key_e : @retroactive Hashable {} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 5eb277ba1..b7643aada 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -184,6 +184,34 @@ extension Ghostty { } .ghosttySurfaceView(surfaceView) +#if canImport(AppKit) + // If we are in the middle of a key sequence, then we show a visual element. We only + // support this on macOS currently although in theory we can support mobile with keyboards! + if !surfaceView.keySequence.isEmpty { + let padding: CGFloat = 5 + VStack { + Spacer() + + HStack { + Text(verbatim: "Pending Key Sequence:") + ForEach(0..