mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #368 from mitchellh/macos-sync
macos: sync keybindings with Mac menu items
This commit is contained in:
@ -230,12 +230,11 @@ typedef enum {
|
||||
GHOSTTY_KEY_RIGHT_SUPER,
|
||||
} ghostty_input_key_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_BINDING_COPY_TO_CLIPBOARD,
|
||||
GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD,
|
||||
GHOSTTY_BINDING_NEW_TAB,
|
||||
GHOSTTY_BINDING_NEW_WINDOW,
|
||||
} ghostty_binding_action_e;
|
||||
typedef struct {
|
||||
ghostty_input_key_e key;
|
||||
ghostty_input_mods_e mods;
|
||||
bool physical;
|
||||
} ghostty_input_trigger_s;
|
||||
|
||||
// Fully defined types. This MUST be kept in sync with equivalent Zig
|
||||
// structs. To find the Zig struct, grep for this type name. The documentation
|
||||
@ -289,6 +288,7 @@ void ghostty_config_load_string(ghostty_config_t, const char *, uintptr_t);
|
||||
void ghostty_config_load_default_files(ghostty_config_t);
|
||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||
void ghostty_config_finalize(ghostty_config_t);
|
||||
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, const char *, uintptr_t);
|
||||
|
||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t);
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
@ -315,7 +315,7 @@ void ghostty_surface_ime_point(ghostty_surface_t, double *, double *);
|
||||
void ghostty_surface_request_close(ghostty_surface_t);
|
||||
void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e);
|
||||
void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e);
|
||||
void ghostty_surface_binding_action(ghostty_surface_t, ghostty_binding_action_e, void *);
|
||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
|
||||
|
||||
// APIs I'd like to get rid of eventually but are still needed for now.
|
||||
// Don't use these unless you know what you're doing.
|
||||
|
@ -11,6 +11,7 @@
|
||||
85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */; };
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||
85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */; };
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; };
|
||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
|
||||
A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */; };
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
||||
@ -37,6 +38,7 @@
|
||||
85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowController.swift; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindow.swift; sourceTree = "<group>"; };
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
|
||||
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowManager.swift; sourceTree = "<group>"; };
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
@ -132,6 +134,7 @@
|
||||
A55B7BB729B6F53A0055DE60 /* Package.swift */,
|
||||
A55B7BB529B6F47F0055DE60 /* AppState.swift */,
|
||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */,
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||
);
|
||||
@ -260,6 +263,7 @@
|
||||
files = (
|
||||
A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */,
|
||||
85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||
|
@ -3,7 +3,7 @@ import OSLog
|
||||
import GhosttyKit
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyAppStateDelegate {
|
||||
// The application logger. We should probably move this at some point to a dedicated
|
||||
// class/struct but for now it lives here! 🤷♂️
|
||||
static let logger = Logger(
|
||||
@ -14,6 +14,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
// confirmQuit published so other views can check whether quit needs to be confirmed.
|
||||
@Published var confirmQuit: Bool = false
|
||||
|
||||
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config.
|
||||
@IBOutlet private var menuQuit: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuNewWindow: NSMenuItem?
|
||||
@IBOutlet private var menuNewTab: NSMenuItem?
|
||||
@IBOutlet private var menuSplitHorizontal: NSMenuItem?
|
||||
@IBOutlet private var menuSplitVertical: NSMenuItem?
|
||||
@IBOutlet private var menuClose: NSMenuItem?
|
||||
@IBOutlet private var menuCloseWindow: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuCopy: NSMenuItem?
|
||||
@IBOutlet private var menuPaste: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuPreviousSplit: NSMenuItem?
|
||||
@IBOutlet private var menuNextSplit: NSMenuItem?
|
||||
@IBOutlet private var menuSelectSplitAbove: NSMenuItem?
|
||||
@IBOutlet private var menuSelectSplitBelow: NSMenuItem?
|
||||
@IBOutlet private var menuSelectSplitLeft: NSMenuItem?
|
||||
@IBOutlet private var menuSelectSplitRight: NSMenuItem?
|
||||
|
||||
/// The ghostty global state. Only one per process.
|
||||
private var ghostty: Ghostty.AppState = Ghostty.AppState()
|
||||
|
||||
@ -23,6 +43,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
ghostty.delegate = self
|
||||
windowManager = PrimaryWindowManager(ghostty: self.ghostty)
|
||||
}
|
||||
|
||||
@ -33,6 +54,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
"ApplePressAndHoldEnabled": false,
|
||||
])
|
||||
|
||||
// Sync our menu shortcuts with our Ghostty config
|
||||
syncMenuShortcuts()
|
||||
|
||||
// Let's launch our first window.
|
||||
// TODO: we should detect if we restored windows and if so not launch a new window.
|
||||
windowManager.addInitialWindow()
|
||||
@ -76,6 +100,64 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
return .terminateLater
|
||||
}
|
||||
|
||||
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
|
||||
private func syncMenuShortcuts() {
|
||||
guard ghostty.config != nil else { return }
|
||||
|
||||
syncMenuShortcut(action: "quit", menuItem: self.menuQuit)
|
||||
|
||||
syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitHorizontal)
|
||||
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitVertical)
|
||||
|
||||
syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
|
||||
syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
}
|
||||
|
||||
/// Syncs a single menu shortcut for the given action. The action string is the same
|
||||
/// action string used for the Ghostty configuration.
|
||||
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) {
|
||||
guard let cfg = ghostty.config else { return }
|
||||
guard let menu = menuItem else { return }
|
||||
|
||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
|
||||
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else {
|
||||
Self.logger.debug("no keyboard shorcut set for action=\(action)")
|
||||
return
|
||||
}
|
||||
|
||||
menu.keyEquivalent = equiv
|
||||
menu.keyEquivalentModifierMask = Ghostty.eventModifierFlags(mods: trigger.mods)
|
||||
}
|
||||
|
||||
private func focusedSurface() -> ghostty_surface_t? {
|
||||
guard let window = NSApp.keyWindow as? PrimaryWindow else { return nil }
|
||||
return window.focusedSurfaceWrapper.surface
|
||||
}
|
||||
|
||||
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||
guard let surface = focusedSurface() else { return }
|
||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||
}
|
||||
|
||||
//MARK: - GhosttyAppStateDelegate
|
||||
|
||||
func configDidReload(_ state: Ghostty.AppState) {
|
||||
syncMenuShortcuts()
|
||||
}
|
||||
|
||||
//MARK: - IB Actions
|
||||
|
||||
@IBAction func newWindow(_ sender: Any?) {
|
||||
windowManager.newWindow()
|
||||
}
|
||||
@ -98,11 +180,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
ghostty.requestClose(surface: surface)
|
||||
}
|
||||
|
||||
private func focusedSurface() -> ghostty_surface_t? {
|
||||
guard let window = NSApp.keyWindow as? PrimaryWindow else { return nil }
|
||||
return window.focusedSurfaceWrapper.surface
|
||||
}
|
||||
|
||||
@IBAction func splitHorizontally(_ sender: Any) {
|
||||
guard let surface = focusedSurface() else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT)
|
||||
@ -137,11 +214,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
splitMoveFocus(direction: .right)
|
||||
}
|
||||
|
||||
func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||
guard let surface = focusedSurface() else { return }
|
||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||
}
|
||||
|
||||
@IBAction func showHelp(_ sender: Any) {
|
||||
guard let url = URL(string: "https://github.com/mitchellh/ghostty") else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
|
@ -1,6 +1,11 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
protocol GhosttyAppStateDelegate: AnyObject {
|
||||
/// Called when the configuration did finish reloading.
|
||||
func configDidReload(_ state: Ghostty.AppState)
|
||||
}
|
||||
|
||||
extension Ghostty {
|
||||
enum AppReadiness {
|
||||
case loading, error, ready
|
||||
@ -12,6 +17,9 @@ extension Ghostty {
|
||||
/// The readiness value of the state.
|
||||
@Published var readiness: AppReadiness = .loading
|
||||
|
||||
/// Optional delegate
|
||||
weak var delegate: GhosttyAppStateDelegate?
|
||||
|
||||
/// The ghostty global configuration. This should only be changed when it is definitely
|
||||
/// safe to change. It is definite safe to change only when the embedded app runtime
|
||||
/// in Ghostty says so (usually, only in a reload configuration callback).
|
||||
@ -140,11 +148,17 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
func newTab(surface: ghostty_surface_t) {
|
||||
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_TAB, nil)
|
||||
let action = "new_tab"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
func newWindow(surface: ghostty_surface_t) {
|
||||
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_WINDOW, nil)
|
||||
let action = "new_window"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) {
|
||||
@ -236,6 +250,11 @@ extension Ghostty {
|
||||
let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
state.config = newConfig
|
||||
|
||||
// If we have a delegate, notify.
|
||||
if let delegate = state.delegate {
|
||||
delegate.configDidReload(state)
|
||||
}
|
||||
|
||||
return newConfig
|
||||
}
|
||||
|
||||
|
203
macos/Sources/Ghostty/Ghostty.Input.swift
Normal file
203
macos/Sources/Ghostty/Ghostty.Input.swift
Normal file
@ -0,0 +1,203 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// 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]
|
||||
}
|
||||
|
||||
/// 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] = [];
|
||||
if (mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0) { flags.append(.shift) }
|
||||
if (mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0) { flags.append(.control) }
|
||||
if (mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0) { flags.append(.option) }
|
||||
if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.append(.command) }
|
||||
return NSEvent.ModifierFlags(flags)
|
||||
}
|
||||
|
||||
/// A map from the Ghostty key enum to the keyEquivalent string for shortcuts.
|
||||
static let keyToEquivalent: [ghostty_input_key_e : String] = [
|
||||
// 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: "\u{F700}",
|
||||
GHOSTTY_KEY_DOWN: "\u{F701}",
|
||||
GHOSTTY_KEY_LEFT: "\u{F702}",
|
||||
GHOSTTY_KEY_RIGHT: "\u{F703}",
|
||||
GHOSTTY_KEY_HOME: "\u{F729}",
|
||||
GHOSTTY_KEY_END: "\u{F72B}",
|
||||
GHOSTTY_KEY_INSERT: "\u{F727}",
|
||||
GHOSTTY_KEY_DELETE: "\u{F728}",
|
||||
GHOSTTY_KEY_PAGE_UP: "\u{F72C}",
|
||||
GHOSTTY_KEY_PAGE_DOWN: "\u{F72D}",
|
||||
GHOSTTY_KEY_ESCAPE: "\u{1B}",
|
||||
GHOSTTY_KEY_ENTER: "\r",
|
||||
GHOSTTY_KEY_TAB: "\t",
|
||||
GHOSTTY_KEY_BACKSPACE: "\u{7F}",
|
||||
GHOSTTY_KEY_PRINT_SCREEN: "\u{F72E}",
|
||||
GHOSTTY_KEY_PAUSE: "\u{F72F}",
|
||||
|
||||
GHOSTTY_KEY_F1: "\u{F704}",
|
||||
GHOSTTY_KEY_F2: "\u{F705}",
|
||||
GHOSTTY_KEY_F3: "\u{F706}",
|
||||
GHOSTTY_KEY_F4: "\u{F707}",
|
||||
GHOSTTY_KEY_F5: "\u{F708}",
|
||||
GHOSTTY_KEY_F6: "\u{F709}",
|
||||
GHOSTTY_KEY_F7: "\u{F70A}",
|
||||
GHOSTTY_KEY_F8: "\u{F70B}",
|
||||
GHOSTTY_KEY_F9: "\u{F70C}",
|
||||
GHOSTTY_KEY_F10: "\u{F70D}",
|
||||
GHOSTTY_KEY_F11: "\u{F70E}",
|
||||
GHOSTTY_KEY_F12: "\u{F70F}",
|
||||
GHOSTTY_KEY_F13: "\u{F710}",
|
||||
GHOSTTY_KEY_F14: "\u{F711}",
|
||||
GHOSTTY_KEY_F15: "\u{F712}",
|
||||
GHOSTTY_KEY_F16: "\u{F713}",
|
||||
GHOSTTY_KEY_F17: "\u{F714}",
|
||||
GHOSTTY_KEY_F18: "\u{F715}",
|
||||
GHOSTTY_KEY_F19: "\u{F716}",
|
||||
GHOSTTY_KEY_F20: "\u{F717}",
|
||||
GHOSTTY_KEY_F21: "\u{F718}",
|
||||
GHOSTTY_KEY_F22: "\u{F719}",
|
||||
GHOSTTY_KEY_F23: "\u{F71A}",
|
||||
GHOSTTY_KEY_F24: "\u{F71B}",
|
||||
GHOSTTY_KEY_F25: "\u{F71C}",
|
||||
]
|
||||
|
||||
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,
|
||||
]
|
||||
}
|
||||
|
@ -375,17 +375,26 @@ extension Ghostty {
|
||||
|
||||
@IBAction func copy(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_COPY_TO_CLIPBOARD, nil)
|
||||
let action = "copy_to_clipboard"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func paste(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD, nil)
|
||||
let action = "paste_from_clipboard"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func pasteAsPlainText(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD, nil)
|
||||
let action = "paste_from_clipboard"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSTextInputClient
|
||||
@ -615,89 +624,6 @@ extension Ghostty {
|
||||
0x43: GHOSTTY_KEY_KP_MULTIPLY,
|
||||
0x4E: GHOSTTY_KEY_KP_SUBTRACT,
|
||||
];
|
||||
|
||||
static let ascii: [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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,25 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target"/>
|
||||
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
||||
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
|
||||
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
||||
<outlet property="menuNewTab" destination="uTG-Vz-hJU" id="eMg-R3-SeS"/>
|
||||
<outlet property="menuNewWindow" destination="Was-JA-tGl" id="lK7-3I-CPG"/>
|
||||
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
|
||||
<outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/>
|
||||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
|
||||
<outlet property="menuSelectSplitAbove" destination="0yU-hC-8xF" id="aPc-lS-own"/>
|
||||
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
|
||||
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
|
||||
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
|
||||
<outlet property="menuSplitHorizontal" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
|
||||
<outlet property="menuSplitVertical" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
@ -47,7 +65,8 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit Ghostty" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<menuItem title="Quit Ghostty" id="4sb-4s-VLi">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
@ -59,34 +78,40 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||
<items>
|
||||
<menuItem title="New Window" keyEquivalent="n" id="Was-JA-tGl">
|
||||
<menuItem title="New Window" id="Was-JA-tGl">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="newWindow:" target="bbz-4X-AYv" id="NnC-l5-DUY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="New Tab" keyEquivalent="t" id="uTG-Vz-hJU">
|
||||
<menuItem title="New Tab" id="uTG-Vz-hJU">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="newTab:" target="bbz-4X-AYv" id="cxO-CS-TJq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||
<menuItem title="Split Horizontally" keyEquivalent="d" id="VUR-Ld-nLx">
|
||||
<menuItem title="Split Horizontally" id="VUR-Ld-nLx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitHorizontally:" target="bbz-4X-AYv" id="QT1-Yt-gYJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Split Vertically" keyEquivalent="D" id="UDZ-4y-6xL">
|
||||
<menuItem title="Split Vertically" id="UDZ-4y-6xL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitVertically:" target="bbz-4X-AYv" id="ZZF-3f-OwW"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="sjq-M1-UGS"/>
|
||||
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||
<menuItem title="Close" id="DVo-aG-piG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="close:" target="bbz-4X-AYv" id="Szc-Fu-9yk"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Close Window" keyEquivalent="W" id="W5w-UZ-crk">
|
||||
<menuItem title="Close Window" id="W5w-UZ-crk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="closeWindow:" target="bbz-4X-AYv" id="j4w-Nd-9bO"/>
|
||||
</connections>
|
||||
@ -98,22 +123,18 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="iU4-OB-ccf">
|
||||
<items>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="Jqf-pv-Zcu">
|
||||
<menuItem title="Copy" id="Jqf-pv-Zcu">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="B4F-hg-R4T"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="i27-pK-umN">
|
||||
<menuItem title="Paste" id="i27-pK-umN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="ZKe-2B-mel"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="FFo-bM-GXj">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="Sfp-aT-ZgM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VYS-RG-uZD"/>
|
||||
</items>
|
||||
</menu>
|
||||
@ -141,12 +162,14 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="rlu-tP-x0P"/>
|
||||
<menuItem title="Select Previous Split" keyEquivalent="[" id="Lic-px-1wg">
|
||||
<menuItem title="Select Previous Split" id="Lic-px-1wg">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitMoveFocusPrevious:" target="bbz-4X-AYv" id="mOs-gG-dAC"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select Next Split" keyEquivalent="]" id="bD7-ei-wKU">
|
||||
<menuItem title="Select Next Split" id="bD7-ei-wKU">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitMoveFocusNext:" target="bbz-4X-AYv" id="rU6-Vw-DoW"/>
|
||||
</connections>
|
||||
@ -155,26 +178,26 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Select Split" id="8tg-60-ZSU">
|
||||
<items>
|
||||
<menuItem title="Select Split Above" keyEquivalent="" id="0yU-hC-8xF">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<menuItem title="Select Split Above" id="0yU-hC-8xF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitMoveFocusAbove:" target="bbz-4X-AYv" id="HDw-f2-RJY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select Split Below" keyEquivalent="" id="QDz-d9-CBr">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<menuItem title="Select Split Below" id="QDz-d9-CBr">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitMoveFocusBelow:" target="bbz-4X-AYv" id="fmW-hZ-uOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select Split Left" keyEquivalent="" id="cTK-oy-KuV">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<menuItem title="Select Split Left" id="cTK-oy-KuV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitMoveFocusLeft:" target="bbz-4X-AYv" id="N1i-a2-7N5"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select Split Right" keyEquivalent="" id="upj-mc-L7X">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<menuItem title="Select Split Right" id="upj-mc-L7X">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="splitMoveFocusRight:" target="bbz-4X-AYv" id="Pgi-df-84r"/>
|
||||
</connections>
|
||||
|
@ -882,22 +882,21 @@ pub const CAPI = struct {
|
||||
/// Invoke an action on the surface.
|
||||
export fn ghostty_surface_binding_action(
|
||||
ptr: *Surface,
|
||||
key: input.Binding.Key,
|
||||
unused: *anyopaque,
|
||||
) void {
|
||||
// For future arguments
|
||||
_ = unused;
|
||||
|
||||
const action: input.Binding.Action = switch (key) {
|
||||
.copy_to_clipboard => .{ .copy_to_clipboard = {} },
|
||||
.paste_from_clipboard => .{ .paste_from_clipboard = {} },
|
||||
.new_tab => .{ .new_tab = {} },
|
||||
.new_window => .{ .new_window = {} },
|
||||
action_ptr: [*]const u8,
|
||||
action_len: usize,
|
||||
) bool {
|
||||
const action_str = action_ptr[0..action_len];
|
||||
const action = input.Binding.Action.parse(action_str) catch |err| {
|
||||
log.err("error parsing binding action action={s} err={}", .{ action_str, err });
|
||||
return false;
|
||||
};
|
||||
|
||||
ptr.core_surface.performBindingAction(action) catch |err| {
|
||||
log.err("error performing binding action action={} err={}", .{ action, err });
|
||||
return false;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Sets the window background blur on macOS to the desired value.
|
||||
|
@ -1559,6 +1559,25 @@ pub const CAPI = struct {
|
||||
log.err("error finalizing config err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_config_trigger(
|
||||
self: *Config,
|
||||
str: [*]const u8,
|
||||
len: usize,
|
||||
) inputpkg.Binding.Trigger {
|
||||
return config_trigger_(self, str[0..len]) catch |err| err: {
|
||||
log.err("error finding trigger err={}", .{err});
|
||||
break :err .{};
|
||||
};
|
||||
}
|
||||
|
||||
fn config_trigger_(
|
||||
self: *Config,
|
||||
str: []const u8,
|
||||
) !inputpkg.Binding.Trigger {
|
||||
const action = try inputpkg.Binding.Action.parse(str);
|
||||
return self.keybind.set.getTrigger(action) orelse .{};
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
|
@ -82,72 +82,7 @@ pub fn parse(input: []const u8) !Binding {
|
||||
};
|
||||
|
||||
// Find a matching action
|
||||
const action: Action = action: {
|
||||
// Split our action by colon. A colon may not exist for some
|
||||
// actions so it is optional. The part preceding the colon is the
|
||||
// action name.
|
||||
const actionRaw = input[eqlIdx + 1 ..];
|
||||
const colonIdx = std.mem.indexOf(u8, actionRaw, ":");
|
||||
const action = actionRaw[0..(colonIdx orelse actionRaw.len)];
|
||||
|
||||
// An action name is always required
|
||||
if (action.len == 0) return Error.InvalidFormat;
|
||||
|
||||
const actionInfo = @typeInfo(Action).Union;
|
||||
inline for (actionInfo.fields) |field| {
|
||||
if (std.mem.eql(u8, action, field.name)) {
|
||||
// If the field type is void we expect no value
|
||||
switch (field.type) {
|
||||
void => {
|
||||
if (colonIdx != null) return Error.InvalidFormat;
|
||||
break :action @unionInit(Action, field.name, {});
|
||||
},
|
||||
|
||||
[]const u8 => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = actionRaw[idx + 1 ..];
|
||||
break :action @unionInit(Action, field.name, param);
|
||||
},
|
||||
|
||||
// Cursor keys can't be set currently
|
||||
Action.CursorKey => return Error.InvalidAction,
|
||||
|
||||
else => switch (@typeInfo(field.type)) {
|
||||
.Enum => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = actionRaw[idx + 1 ..];
|
||||
const value = std.meta.stringToEnum(
|
||||
field.type,
|
||||
param,
|
||||
) orelse return Error.InvalidFormat;
|
||||
|
||||
break :action @unionInit(Action, field.name, value);
|
||||
},
|
||||
|
||||
.Int => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = actionRaw[idx + 1 ..];
|
||||
const value = std.fmt.parseInt(field.type, param, 10) catch
|
||||
return Error.InvalidFormat;
|
||||
break :action @unionInit(Action, field.name, value);
|
||||
},
|
||||
|
||||
.Float => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = actionRaw[idx + 1 ..];
|
||||
const value = std.fmt.parseFloat(field.type, param) catch
|
||||
return Error.InvalidFormat;
|
||||
break :action @unionInit(Action, field.name, value);
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Error.InvalidFormat;
|
||||
};
|
||||
const action = try Action.parse(input[eqlIdx + 1 ..]);
|
||||
|
||||
return Binding{ .trigger = trigger, .action = action };
|
||||
}
|
||||
@ -266,6 +201,118 @@ pub const Action = union(enum) {
|
||||
bottom,
|
||||
right,
|
||||
};
|
||||
|
||||
/// Parse an action in the format of "key=value" where key is the
|
||||
/// action name and value is the action parameter. The parameter
|
||||
/// is optional depending on the action.
|
||||
pub fn parse(input: []const u8) !Action {
|
||||
// Split our action by colon. A colon may not exist for some
|
||||
// actions so it is optional. The part preceding the colon is the
|
||||
// action name.
|
||||
const colonIdx = std.mem.indexOf(u8, input, ":");
|
||||
const action = input[0..(colonIdx orelse input.len)];
|
||||
|
||||
// An action name is always required
|
||||
if (action.len == 0) return Error.InvalidFormat;
|
||||
|
||||
const actionInfo = @typeInfo(Action).Union;
|
||||
inline for (actionInfo.fields) |field| {
|
||||
if (std.mem.eql(u8, action, field.name)) {
|
||||
// If the field type is void we expect no value
|
||||
switch (field.type) {
|
||||
void => {
|
||||
if (colonIdx != null) return Error.InvalidFormat;
|
||||
return @unionInit(Action, field.name, {});
|
||||
},
|
||||
|
||||
[]const u8 => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = input[idx + 1 ..];
|
||||
return @unionInit(Action, field.name, param);
|
||||
},
|
||||
|
||||
// Cursor keys can't be set currently
|
||||
Action.CursorKey => return Error.InvalidAction,
|
||||
|
||||
else => switch (@typeInfo(field.type)) {
|
||||
.Enum => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = input[idx + 1 ..];
|
||||
const value = std.meta.stringToEnum(
|
||||
field.type,
|
||||
param,
|
||||
) orelse return Error.InvalidFormat;
|
||||
|
||||
return @unionInit(Action, field.name, value);
|
||||
},
|
||||
|
||||
.Int => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = input[idx + 1 ..];
|
||||
const value = std.fmt.parseInt(field.type, param, 10) catch
|
||||
return Error.InvalidFormat;
|
||||
return @unionInit(Action, field.name, value);
|
||||
},
|
||||
|
||||
.Float => {
|
||||
const idx = colonIdx orelse return Error.InvalidFormat;
|
||||
const param = input[idx + 1 ..];
|
||||
const value = std.fmt.parseFloat(field.type, param) catch
|
||||
return Error.InvalidFormat;
|
||||
return @unionInit(Action, field.name, value);
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Error.InvalidAction;
|
||||
}
|
||||
|
||||
/// Returns a hash code that can be used to uniquely identify this
|
||||
/// action.
|
||||
pub fn hash(self: Action) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
|
||||
// Always has the active tag.
|
||||
const Tag = @typeInfo(Action).Union.tag_type.?;
|
||||
std.hash.autoHash(&hasher, @as(Tag, self));
|
||||
|
||||
// Hash the value of the field.
|
||||
switch (self) {
|
||||
inline else => |field| {
|
||||
const FieldType = @TypeOf(field);
|
||||
switch (FieldType) {
|
||||
// Do nothing for void
|
||||
void => {},
|
||||
|
||||
// Floats are hashed by their bits. This is totally not
|
||||
// portable and there are edge cases such as NaNs and
|
||||
// signed zeros but these are not cases we expect for
|
||||
// our bindings.
|
||||
f32 => std.hash.autoHash(
|
||||
&hasher,
|
||||
@as(u32, @bitCast(field)),
|
||||
),
|
||||
f64 => std.hash.autoHash(
|
||||
&hasher,
|
||||
@as(u64, @bitCast(field)),
|
||||
),
|
||||
|
||||
// Everything else automatically handle.
|
||||
else => std.hash.autoHashStrat(
|
||||
&hasher,
|
||||
field,
|
||||
.DeepRecursive,
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return hasher.final();
|
||||
}
|
||||
};
|
||||
|
||||
// A key for the C API to execute an action. This must be kept in sync
|
||||
@ -278,7 +325,10 @@ pub const Key = enum(c_int) {
|
||||
};
|
||||
|
||||
/// Trigger is the associated key state that can trigger an action.
|
||||
pub const Trigger = struct {
|
||||
/// This is an extern struct because this is also used in the C API.
|
||||
///
|
||||
/// This must be kept in sync with include/ghostty.h ghostty_input_trigger_s
|
||||
pub const Trigger = extern struct {
|
||||
/// The key that has to be pressed for a binding to take action.
|
||||
key: key.Key = .invalid,
|
||||
|
||||
@ -308,15 +358,28 @@ pub const Set = struct {
|
||||
const HashMap = std.HashMapUnmanaged(
|
||||
Trigger,
|
||||
Action,
|
||||
Context,
|
||||
Context(Trigger),
|
||||
std.hash_map.default_max_load_percentage,
|
||||
);
|
||||
|
||||
const ReverseMap = std.HashMapUnmanaged(
|
||||
Action,
|
||||
Trigger,
|
||||
Context(Action),
|
||||
std.hash_map.default_max_load_percentage,
|
||||
);
|
||||
|
||||
/// The set of bindings.
|
||||
bindings: HashMap = .{},
|
||||
|
||||
/// The reverse mapping of action to binding. Note that multiple
|
||||
/// bindings can map to the same action and this map will only have
|
||||
/// the most recently added binding for an action.
|
||||
reverse: ReverseMap = .{},
|
||||
|
||||
pub fn deinit(self: *Set, alloc: Allocator) void {
|
||||
self.bindings.deinit(alloc);
|
||||
self.reverse.deinit(alloc);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -331,6 +394,9 @@ pub const Set = struct {
|
||||
// unbind should never go into the set, it should be handled prior
|
||||
assert(action != .unbind);
|
||||
try self.bindings.put(alloc, t, action);
|
||||
errdefer _ = self.bindings.remove(t);
|
||||
try self.reverse.put(alloc, action, t);
|
||||
errdefer _ = self.reverse.remove(action);
|
||||
}
|
||||
|
||||
/// Get a binding for a given trigger.
|
||||
@ -338,23 +404,45 @@ pub const Set = struct {
|
||||
return self.bindings.get(t);
|
||||
}
|
||||
|
||||
/// Get a trigger for the given action. An action can have multiple
|
||||
/// triggers so this will return the first one found.
|
||||
pub fn getTrigger(self: Set, a: Action) ?Trigger {
|
||||
return self.reverse.get(a);
|
||||
}
|
||||
|
||||
/// Remove a binding for a given trigger.
|
||||
pub fn remove(self: *Set, t: Trigger) void {
|
||||
const action = self.bindings.get(t) orelse return;
|
||||
_ = self.bindings.remove(t);
|
||||
|
||||
// Look for a matching action in bindings and use that.
|
||||
// Note: we'd LIKE to replace this with the most recent binding but
|
||||
// our hash map obviously has no concept of ordering so we have to
|
||||
// choose whatever. Maybe a switch to an array hash map here.
|
||||
const action_hash = action.hash();
|
||||
var it = self.bindings.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.value_ptr.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(action, entry.key_ptr.*);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hash map context for the set. This defines how the hash map
|
||||
/// gets the hash key and checks for equality.
|
||||
const Context = struct {
|
||||
pub fn hash(ctx: Context, k: Trigger) u64 {
|
||||
fn Context(comptime KeyType: type) type {
|
||||
return struct {
|
||||
pub fn hash(ctx: @This(), k: KeyType) u64 {
|
||||
_ = ctx;
|
||||
return k.hash();
|
||||
}
|
||||
|
||||
pub fn eql(ctx: Context, a: Trigger, b: Trigger) bool {
|
||||
pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool {
|
||||
return ctx.hash(a) == ctx.hash(b);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "parse: triggers" {
|
||||
@ -427,7 +515,7 @@ test "parse: action invalid" {
|
||||
const testing = std.testing;
|
||||
|
||||
// invalid action
|
||||
try testing.expectError(Error.InvalidFormat, parse("a=nopenopenope"));
|
||||
try testing.expectError(Error.InvalidAction, parse("a=nopenopenope"));
|
||||
}
|
||||
|
||||
test "parse: action no parameters" {
|
||||
@ -494,3 +582,31 @@ test "parse: action with float" {
|
||||
try testing.expectEqual(@as(f32, 0.5), binding.action.scroll_page_fractional);
|
||||
}
|
||||
}
|
||||
|
||||
test "set: maintains reverse mapping" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s: Set = .{};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.put(alloc, .{ .key = .a }, .{ .new_window = {} });
|
||||
{
|
||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||
try testing.expect(trigger.key == .a);
|
||||
}
|
||||
|
||||
// should be most recent
|
||||
try s.put(alloc, .{ .key = .b }, .{ .new_window = {} });
|
||||
{
|
||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||
try testing.expect(trigger.key == .b);
|
||||
}
|
||||
|
||||
// removal should replace
|
||||
s.remove(.{ .key = .b });
|
||||
{
|
||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||
try testing.expect(trigger.key == .a);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user