mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: implement key sequence UI
This commit is contained in:
@ -516,7 +516,8 @@ extension Ghostty {
|
|||||||
toggleVisibility(app, target: target)
|
toggleVisibility(app, target: target)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||||
fallthrough
|
keySequence(app, target: target, v: action.action.key_sequence)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||||
fallthrough
|
fallthrough
|
||||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
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
|
// MARK: User Notifications
|
||||||
|
|
||||||
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
||||||
|
@ -87,25 +87,6 @@ extension Ghostty {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// MARK: - Keybindings
|
// 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
|
/// 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
|
/// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty
|
||||||
/// configuration would be "quit" action.
|
/// configuration would be "quit" action.
|
||||||
@ -115,33 +96,7 @@ extension Ghostty {
|
|||||||
guard let cfg = self.config else { return nil }
|
guard let cfg = self.config else { return nil }
|
||||||
|
|
||||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
|
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
|
||||||
let equiv: String
|
return Ghostty.keyEquivalent(for: trigger)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -2,11 +2,68 @@ import Cocoa
|
|||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
extension Ghostty {
|
extension Ghostty {
|
||||||
|
// MARK: Key Equivalents
|
||||||
|
|
||||||
/// Returns the "keyEquivalent" string for a given input key. This doesn't always have a corresponding key.
|
/// 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? {
|
static func keyEquivalent(key: ghostty_input_key_e) -> String? {
|
||||||
return Self.keyToEquivalent[key]
|
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.
|
/// Returns the event modifier flags set for the Ghostty mods enum.
|
||||||
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
|
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
|
||||||
var flags = NSEvent.ModifierFlags(rawValue: 0);
|
var flags = NSEvent.ModifierFlags(rawValue: 0);
|
||||||
|
@ -262,7 +262,12 @@ extension Ghostty.Notification {
|
|||||||
|
|
||||||
/// Notification that renderer health changed
|
/// Notification that renderer health changed
|
||||||
static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth")
|
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.
|
// Make the input enum hashable.
|
||||||
extension ghostty_input_key_e : Hashable {}
|
extension ghostty_input_key_e : @retroactive Hashable {}
|
||||||
|
@ -184,6 +184,34 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.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..<surfaceView.keySequence.count, id: \.description) { index in
|
||||||
|
let key = surfaceView.keySequence[index]
|
||||||
|
Text(verbatim: key.description)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.padding(3)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 5)
|
||||||
|
.fill(Color(NSColor.selectedTextBackgroundColor))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(.background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// If we have a URL from hovering a link, we show that.
|
// If we have a URL from hovering a link, we show that.
|
||||||
if let url = surfaceView.hoverUrl {
|
if let url = surfaceView.hoverUrl {
|
||||||
let padding: CGFloat = 3
|
let padding: CGFloat = 3
|
||||||
|
@ -30,6 +30,9 @@ extension Ghostty {
|
|||||||
// The hovered URL string
|
// The hovered URL string
|
||||||
@Published var hoverUrl: String? = nil
|
@Published var hoverUrl: String? = nil
|
||||||
|
|
||||||
|
// The currently active key sequence. The sequence is not active if this is empty.
|
||||||
|
@Published var keySequence: [Ghostty.KeyEquivalent] = []
|
||||||
|
|
||||||
// The time this surface last became focused. This is a ContinuousClock.Instant
|
// The time this surface last became focused. This is a ContinuousClock.Instant
|
||||||
// on supported platforms.
|
// on supported platforms.
|
||||||
@Published var focusInstant: Any? = nil
|
@Published var focusInstant: Any? = nil
|
||||||
@ -132,6 +135,16 @@ extension Ghostty {
|
|||||||
selector: #selector(onUpdateRendererHealth),
|
selector: #selector(onUpdateRendererHealth),
|
||||||
name: Ghostty.Notification.didUpdateRendererHealth,
|
name: Ghostty.Notification.didUpdateRendererHealth,
|
||||||
object: self)
|
object: self)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyDidContinueKeySequence),
|
||||||
|
name: Ghostty.Notification.didContinueKeySequence,
|
||||||
|
object: self)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyDidEndKeySequence),
|
||||||
|
name: Ghostty.Notification.didEndKeySequence,
|
||||||
|
object: self)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(windowDidChangeScreen),
|
selector: #selector(windowDidChangeScreen),
|
||||||
@ -316,6 +329,16 @@ extension Ghostty {
|
|||||||
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func ghosttyDidContinueKeySequence(notification: SwiftUI.Notification) {
|
||||||
|
guard let keyAny = notification.userInfo?[Ghostty.Notification.KeySequenceKey] else { return }
|
||||||
|
guard let key = keyAny as? Ghostty.KeyEquivalent else { return }
|
||||||
|
keySequence.append(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func ghosttyDidEndKeySequence(notification: SwiftUI.Notification) {
|
||||||
|
keySequence = []
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||||
|
Reference in New Issue
Block a user