mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00

This replaces the use of our custom `Ghostty.KeyEquivalent` with the SwiftUI `KeyboardShortcut` type. This is a more standard way to represent keyboard shortcuts and lets us more tightly integrate with SwiftUI/AppKit when necessary over our custom type. Note that not all Ghostty triggers can be represented as KeyboardShortcut values because macOS itself does not support binding keys such as function keys (e.g. F1-F12) to KeyboardShortcuts. This isn't an issue since all input also passes through a lower level libghostty API which can handle all key events, we just can't show these keyboard shortcuts on things like the menu bar. This was already true before this commit.
358 lines
12 KiB
Swift
358 lines
12 KiB
Swift
import Cocoa
|
|
import SwiftUI
|
|
import GhosttyKit
|
|
|
|
extension Ghostty {
|
|
// MARK: Keyboard Shortcuts
|
|
|
|
/// Returns the SwiftUI KeyEquivalent for a given key. Note that not all keys known by
|
|
/// Ghostty have a macOS equivalent since macOS doesn't allow all keys as equivalents.
|
|
static func keyEquivalent(key: ghostty_input_key_e) -> KeyEquivalent? {
|
|
return Self.keyToEquivalent[key]
|
|
}
|
|
|
|
/// Return the keyboard shortcut for a trigger.
|
|
///
|
|
/// Returns nil if the trigger doesn't have an equivalent KeyboardShortcut. This is possible
|
|
/// because Ghostty input triggers are a superset of what can be represented by a macOS
|
|
/// KeyboardShortcut. For example, macOS doesn't have any way to represent function keys
|
|
/// (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? {
|
|
let key: KeyEquivalent
|
|
switch (trigger.tag) {
|
|
case GHOSTTY_TRIGGER_TRANSLATED:
|
|
if let v = Ghostty.keyEquivalent(key: trigger.key.translated) {
|
|
key = v
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
case GHOSTTY_TRIGGER_PHYSICAL:
|
|
if let v = Ghostty.keyEquivalent(key: trigger.key.physical) {
|
|
key = v
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
case GHOSTTY_TRIGGER_UNICODE:
|
|
guard let scalar = UnicodeScalar(trigger.key.unicode) else { return nil }
|
|
key = KeyEquivalent(Character(scalar))
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
return KeyboardShortcut(
|
|
key,
|
|
modifiers: EventModifiers(nsFlags: 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);
|
|
if (mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0) { flags.insert(.shift) }
|
|
if (mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0) { flags.insert(.control) }
|
|
if (mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0) { flags.insert(.option) }
|
|
if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) }
|
|
return flags
|
|
}
|
|
|
|
/// Translate event modifier flags to a ghostty mods enum.
|
|
static func ghosttyMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
|
|
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
|
|
|
|
if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
|
|
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
|
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
|
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
|
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
|
|
|
// Handle sided input. We can't tell that both are pressed in the
|
|
// Ghostty structure but thats okay -- we don't use that information.
|
|
let rawFlags = flags.rawValue
|
|
if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue }
|
|
if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue }
|
|
if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue }
|
|
if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue }
|
|
|
|
return ghostty_input_mods_e(mods)
|
|
}
|
|
|
|
/// 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.
|
|
static let keyToEquivalent: [ghostty_input_key_e : KeyEquivalent] = [
|
|
// 0-9
|
|
GHOSTTY_KEY_ZERO: "0",
|
|
GHOSTTY_KEY_ONE: "1",
|
|
GHOSTTY_KEY_TWO: "2",
|
|
GHOSTTY_KEY_THREE: "3",
|
|
GHOSTTY_KEY_FOUR: "4",
|
|
GHOSTTY_KEY_FIVE: "5",
|
|
GHOSTTY_KEY_SIX: "6",
|
|
GHOSTTY_KEY_SEVEN: "7",
|
|
GHOSTTY_KEY_EIGHT: "8",
|
|
GHOSTTY_KEY_NINE: "9",
|
|
|
|
// a-z
|
|
GHOSTTY_KEY_A: "a",
|
|
GHOSTTY_KEY_B: "b",
|
|
GHOSTTY_KEY_C: "c",
|
|
GHOSTTY_KEY_D: "d",
|
|
GHOSTTY_KEY_E: "e",
|
|
GHOSTTY_KEY_F: "f",
|
|
GHOSTTY_KEY_G: "g",
|
|
GHOSTTY_KEY_H: "h",
|
|
GHOSTTY_KEY_I: "i",
|
|
GHOSTTY_KEY_J: "j",
|
|
GHOSTTY_KEY_K: "k",
|
|
GHOSTTY_KEY_L: "l",
|
|
GHOSTTY_KEY_M: "m",
|
|
GHOSTTY_KEY_N: "n",
|
|
GHOSTTY_KEY_O: "o",
|
|
GHOSTTY_KEY_P: "p",
|
|
GHOSTTY_KEY_Q: "q",
|
|
GHOSTTY_KEY_R: "r",
|
|
GHOSTTY_KEY_S: "s",
|
|
GHOSTTY_KEY_T: "t",
|
|
GHOSTTY_KEY_U: "u",
|
|
GHOSTTY_KEY_V: "v",
|
|
GHOSTTY_KEY_W: "w",
|
|
GHOSTTY_KEY_X: "x",
|
|
GHOSTTY_KEY_Y: "y",
|
|
GHOSTTY_KEY_Z: "z",
|
|
|
|
// Symbols
|
|
GHOSTTY_KEY_APOSTROPHE: "'",
|
|
GHOSTTY_KEY_BACKSLASH: "\\",
|
|
GHOSTTY_KEY_COMMA: ",",
|
|
GHOSTTY_KEY_EQUAL: "=",
|
|
GHOSTTY_KEY_GRAVE_ACCENT: "`",
|
|
GHOSTTY_KEY_LEFT_BRACKET: "[",
|
|
GHOSTTY_KEY_MINUS: "-",
|
|
GHOSTTY_KEY_PERIOD: ".",
|
|
GHOSTTY_KEY_RIGHT_BRACKET: "]",
|
|
GHOSTTY_KEY_SEMICOLON: ";",
|
|
GHOSTTY_KEY_SLASH: "/",
|
|
|
|
// Function keys
|
|
GHOSTTY_KEY_UP: .upArrow,
|
|
GHOSTTY_KEY_DOWN: .downArrow,
|
|
GHOSTTY_KEY_LEFT: .leftArrow,
|
|
GHOSTTY_KEY_RIGHT: .rightArrow,
|
|
GHOSTTY_KEY_HOME: .home,
|
|
GHOSTTY_KEY_END: .end,
|
|
GHOSTTY_KEY_DELETE: .delete,
|
|
GHOSTTY_KEY_PAGE_UP: .pageUp,
|
|
GHOSTTY_KEY_PAGE_DOWN: .pageDown,
|
|
GHOSTTY_KEY_ESCAPE: .escape,
|
|
GHOSTTY_KEY_ENTER: .return,
|
|
GHOSTTY_KEY_TAB: .tab,
|
|
GHOSTTY_KEY_BACKSPACE: .delete,
|
|
]
|
|
|
|
static let asciiToKey: [UInt8 : ghostty_input_key_e] = [
|
|
// 0-9
|
|
0x30: GHOSTTY_KEY_ZERO,
|
|
0x31: GHOSTTY_KEY_ONE,
|
|
0x32: GHOSTTY_KEY_TWO,
|
|
0x33: GHOSTTY_KEY_THREE,
|
|
0x34: GHOSTTY_KEY_FOUR,
|
|
0x35: GHOSTTY_KEY_FIVE,
|
|
0x36: GHOSTTY_KEY_SIX,
|
|
0x37: GHOSTTY_KEY_SEVEN,
|
|
0x38: GHOSTTY_KEY_EIGHT,
|
|
0x39: GHOSTTY_KEY_NINE,
|
|
|
|
// A-Z
|
|
0x41: GHOSTTY_KEY_A,
|
|
0x42: GHOSTTY_KEY_B,
|
|
0x43: GHOSTTY_KEY_C,
|
|
0x44: GHOSTTY_KEY_D,
|
|
0x45: GHOSTTY_KEY_E,
|
|
0x46: GHOSTTY_KEY_F,
|
|
0x47: GHOSTTY_KEY_G,
|
|
0x48: GHOSTTY_KEY_H,
|
|
0x49: GHOSTTY_KEY_I,
|
|
0x4A: GHOSTTY_KEY_J,
|
|
0x4B: GHOSTTY_KEY_K,
|
|
0x4C: GHOSTTY_KEY_L,
|
|
0x4D: GHOSTTY_KEY_M,
|
|
0x4E: GHOSTTY_KEY_N,
|
|
0x4F: GHOSTTY_KEY_O,
|
|
0x50: GHOSTTY_KEY_P,
|
|
0x51: GHOSTTY_KEY_Q,
|
|
0x52: GHOSTTY_KEY_R,
|
|
0x53: GHOSTTY_KEY_S,
|
|
0x54: GHOSTTY_KEY_T,
|
|
0x55: GHOSTTY_KEY_U,
|
|
0x56: GHOSTTY_KEY_V,
|
|
0x57: GHOSTTY_KEY_W,
|
|
0x58: GHOSTTY_KEY_X,
|
|
0x59: GHOSTTY_KEY_Y,
|
|
0x5A: GHOSTTY_KEY_Z,
|
|
|
|
// a-z
|
|
0x61: GHOSTTY_KEY_A,
|
|
0x62: GHOSTTY_KEY_B,
|
|
0x63: GHOSTTY_KEY_C,
|
|
0x64: GHOSTTY_KEY_D,
|
|
0x65: GHOSTTY_KEY_E,
|
|
0x66: GHOSTTY_KEY_F,
|
|
0x67: GHOSTTY_KEY_G,
|
|
0x68: GHOSTTY_KEY_H,
|
|
0x69: GHOSTTY_KEY_I,
|
|
0x6A: GHOSTTY_KEY_J,
|
|
0x6B: GHOSTTY_KEY_K,
|
|
0x6C: GHOSTTY_KEY_L,
|
|
0x6D: GHOSTTY_KEY_M,
|
|
0x6E: GHOSTTY_KEY_N,
|
|
0x6F: GHOSTTY_KEY_O,
|
|
0x70: GHOSTTY_KEY_P,
|
|
0x71: GHOSTTY_KEY_Q,
|
|
0x72: GHOSTTY_KEY_R,
|
|
0x73: GHOSTTY_KEY_S,
|
|
0x74: GHOSTTY_KEY_T,
|
|
0x75: GHOSTTY_KEY_U,
|
|
0x76: GHOSTTY_KEY_V,
|
|
0x77: GHOSTTY_KEY_W,
|
|
0x78: GHOSTTY_KEY_X,
|
|
0x79: GHOSTTY_KEY_Y,
|
|
0x7A: GHOSTTY_KEY_Z,
|
|
|
|
// Symbols
|
|
0x27: GHOSTTY_KEY_APOSTROPHE,
|
|
0x5C: GHOSTTY_KEY_BACKSLASH,
|
|
0x2C: GHOSTTY_KEY_COMMA,
|
|
0x3D: GHOSTTY_KEY_EQUAL,
|
|
0x60: GHOSTTY_KEY_GRAVE_ACCENT,
|
|
0x5B: GHOSTTY_KEY_LEFT_BRACKET,
|
|
0x2D: GHOSTTY_KEY_MINUS,
|
|
0x2E: GHOSTTY_KEY_PERIOD,
|
|
0x5D: GHOSTTY_KEY_RIGHT_BRACKET,
|
|
0x3B: GHOSTTY_KEY_SEMICOLON,
|
|
0x2F: GHOSTTY_KEY_SLASH,
|
|
]
|
|
|
|
// Mapping of event keyCode to ghostty input key values. This is cribbed from
|
|
// glfw mostly since we started as a glfw-based app way back in the day!
|
|
static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [
|
|
0x1D: GHOSTTY_KEY_ZERO,
|
|
0x12: GHOSTTY_KEY_ONE,
|
|
0x13: GHOSTTY_KEY_TWO,
|
|
0x14: GHOSTTY_KEY_THREE,
|
|
0x15: GHOSTTY_KEY_FOUR,
|
|
0x17: GHOSTTY_KEY_FIVE,
|
|
0x16: GHOSTTY_KEY_SIX,
|
|
0x1A: GHOSTTY_KEY_SEVEN,
|
|
0x1C: GHOSTTY_KEY_EIGHT,
|
|
0x19: GHOSTTY_KEY_NINE,
|
|
0x00: GHOSTTY_KEY_A,
|
|
0x0B: GHOSTTY_KEY_B,
|
|
0x08: GHOSTTY_KEY_C,
|
|
0x02: GHOSTTY_KEY_D,
|
|
0x0E: GHOSTTY_KEY_E,
|
|
0x03: GHOSTTY_KEY_F,
|
|
0x05: GHOSTTY_KEY_G,
|
|
0x04: GHOSTTY_KEY_H,
|
|
0x22: GHOSTTY_KEY_I,
|
|
0x26: GHOSTTY_KEY_J,
|
|
0x28: GHOSTTY_KEY_K,
|
|
0x25: GHOSTTY_KEY_L,
|
|
0x2E: GHOSTTY_KEY_M,
|
|
0x2D: GHOSTTY_KEY_N,
|
|
0x1F: GHOSTTY_KEY_O,
|
|
0x23: GHOSTTY_KEY_P,
|
|
0x0C: GHOSTTY_KEY_Q,
|
|
0x0F: GHOSTTY_KEY_R,
|
|
0x01: GHOSTTY_KEY_S,
|
|
0x11: GHOSTTY_KEY_T,
|
|
0x20: GHOSTTY_KEY_U,
|
|
0x09: GHOSTTY_KEY_V,
|
|
0x0D: GHOSTTY_KEY_W,
|
|
0x07: GHOSTTY_KEY_X,
|
|
0x10: GHOSTTY_KEY_Y,
|
|
0x06: GHOSTTY_KEY_Z,
|
|
|
|
0x27: GHOSTTY_KEY_APOSTROPHE,
|
|
0x2A: GHOSTTY_KEY_BACKSLASH,
|
|
0x2B: GHOSTTY_KEY_COMMA,
|
|
0x18: GHOSTTY_KEY_EQUAL,
|
|
0x32: GHOSTTY_KEY_GRAVE_ACCENT,
|
|
0x21: GHOSTTY_KEY_LEFT_BRACKET,
|
|
0x1B: GHOSTTY_KEY_MINUS,
|
|
0x2F: GHOSTTY_KEY_PERIOD,
|
|
0x1E: GHOSTTY_KEY_RIGHT_BRACKET,
|
|
0x29: GHOSTTY_KEY_SEMICOLON,
|
|
0x2C: GHOSTTY_KEY_SLASH,
|
|
|
|
0x33: GHOSTTY_KEY_BACKSPACE,
|
|
0x39: GHOSTTY_KEY_CAPS_LOCK,
|
|
0x75: GHOSTTY_KEY_DELETE,
|
|
0x7D: GHOSTTY_KEY_DOWN,
|
|
0x77: GHOSTTY_KEY_END,
|
|
0x24: GHOSTTY_KEY_ENTER,
|
|
0x35: GHOSTTY_KEY_ESCAPE,
|
|
0x7A: GHOSTTY_KEY_F1,
|
|
0x78: GHOSTTY_KEY_F2,
|
|
0x63: GHOSTTY_KEY_F3,
|
|
0x76: GHOSTTY_KEY_F4,
|
|
0x60: GHOSTTY_KEY_F5,
|
|
0x61: GHOSTTY_KEY_F6,
|
|
0x62: GHOSTTY_KEY_F7,
|
|
0x64: GHOSTTY_KEY_F8,
|
|
0x65: GHOSTTY_KEY_F9,
|
|
0x6D: GHOSTTY_KEY_F10,
|
|
0x67: GHOSTTY_KEY_F11,
|
|
0x6F: GHOSTTY_KEY_F12,
|
|
0x69: GHOSTTY_KEY_PRINT_SCREEN,
|
|
0x6B: GHOSTTY_KEY_F14,
|
|
0x71: GHOSTTY_KEY_F15,
|
|
0x6A: GHOSTTY_KEY_F16,
|
|
0x40: GHOSTTY_KEY_F17,
|
|
0x4F: GHOSTTY_KEY_F18,
|
|
0x50: GHOSTTY_KEY_F19,
|
|
0x5A: GHOSTTY_KEY_F20,
|
|
0x73: GHOSTTY_KEY_HOME,
|
|
0x72: GHOSTTY_KEY_INSERT,
|
|
0x7B: GHOSTTY_KEY_LEFT,
|
|
0x3A: GHOSTTY_KEY_LEFT_ALT,
|
|
0x3B: GHOSTTY_KEY_LEFT_CONTROL,
|
|
0x38: GHOSTTY_KEY_LEFT_SHIFT,
|
|
0x37: GHOSTTY_KEY_LEFT_SUPER,
|
|
0x47: GHOSTTY_KEY_NUM_LOCK,
|
|
0x79: GHOSTTY_KEY_PAGE_DOWN,
|
|
0x74: GHOSTTY_KEY_PAGE_UP,
|
|
0x7C: GHOSTTY_KEY_RIGHT,
|
|
0x3D: GHOSTTY_KEY_RIGHT_ALT,
|
|
0x3E: GHOSTTY_KEY_RIGHT_CONTROL,
|
|
0x3C: GHOSTTY_KEY_RIGHT_SHIFT,
|
|
0x36: GHOSTTY_KEY_RIGHT_SUPER,
|
|
0x31: GHOSTTY_KEY_SPACE,
|
|
0x30: GHOSTTY_KEY_TAB,
|
|
0x7E: GHOSTTY_KEY_UP,
|
|
|
|
0x52: GHOSTTY_KEY_KP_0,
|
|
0x53: GHOSTTY_KEY_KP_1,
|
|
0x54: GHOSTTY_KEY_KP_2,
|
|
0x55: GHOSTTY_KEY_KP_3,
|
|
0x56: GHOSTTY_KEY_KP_4,
|
|
0x57: GHOSTTY_KEY_KP_5,
|
|
0x58: GHOSTTY_KEY_KP_6,
|
|
0x59: GHOSTTY_KEY_KP_7,
|
|
0x5B: GHOSTTY_KEY_KP_8,
|
|
0x5C: GHOSTTY_KEY_KP_9,
|
|
0x45: GHOSTTY_KEY_KP_ADD,
|
|
0x41: GHOSTTY_KEY_KP_DECIMAL,
|
|
0x4B: GHOSTTY_KEY_KP_DIVIDE,
|
|
0x4C: GHOSTTY_KEY_KP_ENTER,
|
|
0x51: GHOSTTY_KEY_KP_EQUAL,
|
|
0x43: GHOSTTY_KEY_KP_MULTIPLY,
|
|
0x4E: GHOSTTY_KEY_KP_SUBTRACT,
|
|
];
|
|
}
|