mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: Add "Use Option as Meta Key" menu item
Fixes #2520. Makes state per surface, to match Terminal.app Adds menu item to xib and wires it up. Clicking the menu item sends an action into libghostty, which toggles state on the surface and sends a notification back. The menu item state then updates in response to that notification. The menu item also updates when the current surface changes, either if the current split changes, or when tabs change, or when the quick terminal is opened or closed.
This commit is contained in:
@ -399,6 +399,14 @@ typedef enum {
|
||||
GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
|
||||
} ghostty_action_fullscreen_e;
|
||||
|
||||
// apprt.action.OptionAsAlt
|
||||
typedef enum {
|
||||
GHOSTTY_OPTION_AS_ALT_OFF,
|
||||
GHOSTTY_OPTION_AS_ALT_ON,
|
||||
GHOSTTY_OPTION_AS_ALT_LEFT,
|
||||
GHOSTTY_OPTION_AS_ALT_RIGHT,
|
||||
} ghostty_action_option_as_alt_e;
|
||||
|
||||
// apprt.action.SecureInput
|
||||
typedef enum {
|
||||
GHOSTTY_SECURE_INPUT_ON,
|
||||
@ -579,6 +587,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
GHOSTTY_ACTION_RELOAD_CONFIG,
|
||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||
GHOSTTY_ACTION_TOGGLE_MACOS_USE_OPT_AS_ALT,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
@ -722,6 +731,7 @@ void ghostty_surface_split_resize(ghostty_surface_t,
|
||||
ghostty_action_resize_split_direction_e,
|
||||
uint16_t);
|
||||
void ghostty_surface_split_equalize(ghostty_surface_t);
|
||||
ghostty_action_option_as_alt_e ghostty_surface_uses_opt_as_alt(ghostty_surface_t);
|
||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
|
||||
void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
|
||||
const char*,
|
||||
|
@ -36,6 +36,7 @@ class AppDelegate: NSObject,
|
||||
@IBOutlet private var menuCopy: NSMenuItem?
|
||||
@IBOutlet private var menuPaste: NSMenuItem?
|
||||
@IBOutlet private var menuSelectAll: NSMenuItem?
|
||||
@IBOutlet private var menuUseOptionAsMetaKey: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuToggleVisibility: NSMenuItem?
|
||||
@IBOutlet private var menuToggleFullScreen: NSMenuItem?
|
||||
@ -328,6 +329,18 @@ class AppDelegate: NSObject,
|
||||
return dockMenu
|
||||
}
|
||||
|
||||
func setToggleUseOptAsAltMenuState(value: ghostty_action_option_as_alt_e) {
|
||||
let menu_state = switch value {
|
||||
case GHOSTTY_OPTION_AS_ALT_OFF:
|
||||
NSControl.StateValue.off
|
||||
case GHOSTTY_OPTION_AS_ALT_ON:
|
||||
NSControl.StateValue.on
|
||||
default:
|
||||
NSControl.StateValue.mixed
|
||||
};
|
||||
menuUseOptionAsMetaKey?.state = menu_state;
|
||||
}
|
||||
|
||||
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
|
||||
private func syncMenuShortcuts(_ config: Ghostty.Config) {
|
||||
guard ghostty.readiness == .ready else { return }
|
||||
@ -364,6 +377,7 @@ class AppDelegate: NSObject,
|
||||
syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
syncMenuShortcut(config, action: "toggle_macos_option_as_alt", menuItem: self.menuUseOptionAsMetaKey)
|
||||
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||
syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@ -48,6 +48,7 @@
|
||||
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
|
||||
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
|
||||
<outlet property="menuToggleVisibility" destination="DOX-wA-ilh" id="iBj-Bc-2bq"/>
|
||||
<outlet property="menuUseOptionAsMetaKey" destination="7fW-nh-YqK" id="b7F-MJ-i0U"/>
|
||||
<outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
@ -192,6 +193,15 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VYS-RG-uZD"/>
|
||||
<menuItem title="Use Option as Meta Key" state="on" id="7fW-nh-YqK">
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
CQ
|
||||
</string>
|
||||
<connections>
|
||||
<action selector="toggleUseOptionAsMetaKey:" target="-1" id="9CI-7C-odU"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="D4E-a6-qFl"/>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
|
@ -255,6 +255,10 @@ class BaseTerminalController: NSWindowController,
|
||||
|
||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||
focusedSurface = to
|
||||
|
||||
if let appDelegate = NSApplication.shared.delegate as? AppDelegate, let focusedSurface {
|
||||
appDelegate.setToggleUseOptAsAltMenuState(value:focusedSurface.usesOptAsAlt!)
|
||||
}
|
||||
}
|
||||
|
||||
func titleDidChange(to: String) {
|
||||
@ -473,6 +477,10 @@ class BaseTerminalController: NSWindowController,
|
||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||
// so things like cursors blink, pty events are sent, etc.
|
||||
self.syncFocusToSurfaceTree()
|
||||
|
||||
if let appDelegate = NSApplication.shared.delegate as? AppDelegate, let focusedSurface {
|
||||
appDelegate.setToggleUseOptAsAltMenuState(value:focusedSurface.usesOptAsAlt!)
|
||||
}
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
@ -601,6 +609,11 @@ class BaseTerminalController: NSWindowController,
|
||||
ghostty.resetTerminal(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func toggleUseOptionAsMetaKey(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleUseOptionAsMetaKey(surface: surface)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
|
@ -255,6 +255,13 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func toggleUseOptionAsMetaKey(surface: ghostty_surface_t) {
|
||||
let action = "toggle_macos_option_as_alt"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
func toggleTerminalInspector(surface: ghostty_surface_t) {
|
||||
let action = "inspector:toggle"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
@ -523,6 +530,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_RENDERER_HEALTH:
|
||||
rendererHealth(app, target: target, v: action.action.renderer_health)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_MACOS_USE_OPT_AS_ALT:
|
||||
toggleMacOSUseOptAsAlt(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL:
|
||||
toggleQuickTerminal(app, target: target)
|
||||
|
||||
@ -892,6 +902,26 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleMacOSUseOptAsAlt(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("macos toggle use opt as alt 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 }
|
||||
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
||||
appDelegate.setToggleUseOptAsAltMenuState(value:surfaceView.usesOptAsAlt!)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleSecureInput(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
@ -88,6 +88,11 @@ extension Ghostty {
|
||||
return ghostty_surface_inspector(surface)
|
||||
}
|
||||
|
||||
var usesOptAsAlt: ghostty_action_option_as_alt_e? {
|
||||
guard let surface = self.surface else { return nil }
|
||||
return ghostty_surface_uses_opt_as_alt(surface)
|
||||
}
|
||||
|
||||
// True if the inspector should be visible
|
||||
@Published var inspectorVisible: Bool = false {
|
||||
didSet {
|
||||
|
@ -3725,6 +3725,15 @@ fn showMouse(self: *Surface) void {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn usesOptionAsAlt(self: *Surface) apprt.action.OptionAsAlt {
|
||||
return switch (self.config.macos_option_as_alt) {
|
||||
.false => .off,
|
||||
.true => .on,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
};
|
||||
}
|
||||
|
||||
/// Perform a binding action. A binding is a keybinding. This function
|
||||
/// must be called from the GUI thread.
|
||||
///
|
||||
@ -4056,6 +4065,20 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_macos_option_as_alt => {
|
||||
self.config.macos_option_as_alt = switch (self.config.macos_option_as_alt) {
|
||||
.false => .true,
|
||||
.true => .false,
|
||||
.left => .right,
|
||||
.right => .left,
|
||||
};
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_macos_option_as_alt,
|
||||
{},
|
||||
);
|
||||
},
|
||||
|
||||
.toggle_fullscreen => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_fullscreen,
|
||||
|
@ -217,6 +217,9 @@ pub const Action = union(Key) {
|
||||
/// for changes.
|
||||
config_change: ConfigChange,
|
||||
|
||||
/// macOS only: Toggle if option always acts as alt.
|
||||
toggle_macos_option_as_alt,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
new_window,
|
||||
@ -254,6 +257,7 @@ pub const Action = union(Key) {
|
||||
color_change,
|
||||
reload_config,
|
||||
config_change,
|
||||
toggle_macos_option_as_alt,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
@ -375,6 +379,14 @@ pub const Fullscreen = enum(c_int) {
|
||||
macos_non_native_visible_menu,
|
||||
};
|
||||
|
||||
/// ghostty.h API wrapper for config.OptionAsAlt.
|
||||
pub const OptionAsAlt = enum(c_int) {
|
||||
off,
|
||||
on,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
pub const SecureInput = enum(c_int) {
|
||||
on,
|
||||
off,
|
||||
|
@ -162,7 +162,11 @@ pub const App = struct {
|
||||
const translate_mods = translate_mods: {
|
||||
var translate_mods = mods;
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
const strip = switch (self.config.@"macos-option-as-alt") {
|
||||
const option_as_alt = switch (target) {
|
||||
.app => self.config.@"macos-option-as-alt",
|
||||
.surface => |surface| surface.core_surface.config.macos_option_as_alt,
|
||||
};
|
||||
const strip = switch (option_as_alt) {
|
||||
.false => false,
|
||||
.true => mods.alt,
|
||||
.left => mods.sides.alt == .left,
|
||||
@ -1725,6 +1729,10 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_surface_uses_opt_as_alt(ptr: *Surface) c_int {
|
||||
return @intCast(@intFromEnum(ptr.core_surface.usesOptionAsAlt()));
|
||||
}
|
||||
|
||||
/// Invoke an action on the surface.
|
||||
export fn ghostty_surface_binding_action(
|
||||
ptr: *Surface,
|
||||
|
@ -2207,6 +2207,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
.{ .key = .{ .translated = .a }, .mods = .{ .super = true } },
|
||||
.{ .select_all = {} },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .o }, .mods = .{ .super = true, .alt = true } },
|
||||
.{ .toggle_macos_option_as_alt = {} },
|
||||
);
|
||||
|
||||
// Viewport scrolling
|
||||
try result.keybind.set.put(
|
||||
|
@ -358,6 +358,19 @@ pub const Action = union(enum) {
|
||||
/// Toggle fullscreen mode of window.
|
||||
toggle_fullscreen: void,
|
||||
|
||||
/// Toggle if option acts as alt.
|
||||
///
|
||||
/// Since this is a binary toggle and macos-option-as-alt is not a binary configuration,
|
||||
/// the behavior of the action is as follows:
|
||||
///
|
||||
/// 1. false is mapped to true
|
||||
/// 2. true is mapped to false
|
||||
/// 3. left is mapped to right
|
||||
/// 4. right is mapped to left
|
||||
///
|
||||
// The UI shows the last two as indeterminate states.
|
||||
toggle_macos_option_as_alt: void,
|
||||
|
||||
/// Toggle window decorations on and off. This only works on Linux.
|
||||
toggle_window_decorations: void,
|
||||
|
||||
@ -665,6 +678,7 @@ pub const Action = union(enum) {
|
||||
.last_tab,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.toggle_macos_option_as_alt,
|
||||
.toggle_tab_overview,
|
||||
.new_split,
|
||||
.goto_split,
|
||||
|
Reference in New Issue
Block a user