mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #2389 from ghostty-org/fn-globe
macOS: Allow binding the function/globe modifier
This commit is contained in:
@ -88,12 +88,13 @@ typedef enum {
|
|||||||
GHOSTTY_MODS_CTRL = 1 << 1,
|
GHOSTTY_MODS_CTRL = 1 << 1,
|
||||||
GHOSTTY_MODS_ALT = 1 << 2,
|
GHOSTTY_MODS_ALT = 1 << 2,
|
||||||
GHOSTTY_MODS_SUPER = 1 << 3,
|
GHOSTTY_MODS_SUPER = 1 << 3,
|
||||||
GHOSTTY_MODS_CAPS = 1 << 4,
|
GHOSTTY_MODS_FN = 1 << 4,
|
||||||
GHOSTTY_MODS_NUM = 1 << 5,
|
GHOSTTY_MODS_CAPS = 1 << 5,
|
||||||
GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6,
|
GHOSTTY_MODS_NUM = 1 << 6,
|
||||||
GHOSTTY_MODS_CTRL_RIGHT = 1 << 7,
|
GHOSTTY_MODS_SHIFT_RIGHT = 1 << 7,
|
||||||
GHOSTTY_MODS_ALT_RIGHT = 1 << 8,
|
GHOSTTY_MODS_CTRL_RIGHT = 1 << 8,
|
||||||
GHOSTTY_MODS_SUPER_RIGHT = 1 << 9,
|
GHOSTTY_MODS_ALT_RIGHT = 1 << 9,
|
||||||
|
GHOSTTY_MODS_SUPER_RIGHT = 1 << 10,
|
||||||
} ghostty_input_mods_e;
|
} ghostty_input_mods_e;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
@ -149,6 +149,12 @@ class AppDelegate: NSObject,
|
|||||||
// This registers the Ghostty => Services menu to exist.
|
// This registers the Ghostty => Services menu to exist.
|
||||||
NSApp.servicesMenu = menuServices
|
NSApp.servicesMenu = menuServices
|
||||||
|
|
||||||
|
// Setup a local event monitor for app-level keyboard shortcuts. See
|
||||||
|
// localEventHandler for more info why.
|
||||||
|
_ = NSEvent.addLocalMonitorForEvents(
|
||||||
|
matching: [.keyDown],
|
||||||
|
handler: localEventHandler)
|
||||||
|
|
||||||
// Configure user notifications
|
// Configure user notifications
|
||||||
let actions = [
|
let actions = [
|
||||||
UNNotificationAction(identifier: Ghostty.userNotificationActionShow, title: "Show")
|
UNNotificationAction(identifier: Ghostty.userNotificationActionShow, title: "Show")
|
||||||
@ -348,6 +354,11 @@ class AppDelegate: NSObject,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (equiv.modifiers.contains(.function)) {
|
||||||
|
// NSMenuItem key equivalent cannot contain function modifiers.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
menu.keyEquivalent = equiv.key
|
menu.keyEquivalent = equiv.key
|
||||||
menu.keyEquivalentModifierMask = equiv.modifiers
|
menu.keyEquivalentModifierMask = equiv.modifiers
|
||||||
}
|
}
|
||||||
@ -356,6 +367,53 @@ class AppDelegate: NSObject,
|
|||||||
return terminalManager.focusedSurface?.surface
|
return terminalManager.focusedSurface?.surface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Notifications and Events
|
||||||
|
|
||||||
|
/// This handles events from the NSEvent.addLocalEventMonitor. We use this so we can get
|
||||||
|
/// events without any terminal windows open.
|
||||||
|
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||||
|
return switch event.type {
|
||||||
|
case .keyDown:
|
||||||
|
localEventKeyDown(event)
|
||||||
|
|
||||||
|
default:
|
||||||
|
event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func localEventKeyDown(_ event: NSEvent) -> NSEvent? {
|
||||||
|
// If we have a main window then we don't process any of the keys
|
||||||
|
// because we let it capture and propagate.
|
||||||
|
guard NSApp.mainWindow == nil else { return event }
|
||||||
|
|
||||||
|
// If this event would be handled by our menu then we do nothing.
|
||||||
|
if let mainMenu = NSApp.mainMenu,
|
||||||
|
mainMenu.performKeyEquivalent(with: event) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach this point then we try to process the key event
|
||||||
|
// through the Ghostty key mechanism.
|
||||||
|
|
||||||
|
// Ghostty must be loaded
|
||||||
|
guard let ghostty = self.ghostty.app else { return event }
|
||||||
|
|
||||||
|
// Build our event input and call ghostty
|
||||||
|
var key_ev = ghostty_input_key_s()
|
||||||
|
key_ev.action = GHOSTTY_ACTION_PRESS
|
||||||
|
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
key_ev.keycode = UInt32(event.keyCode)
|
||||||
|
key_ev.text = nil
|
||||||
|
key_ev.composing = false
|
||||||
|
if (ghostty_app_key(ghostty, key_ev)) {
|
||||||
|
// The key was used so we want to stop it from going to our Mac app
|
||||||
|
Ghostty.logger.debug("local key event handled event=\(event)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - Restorable State
|
//MARK: - Restorable State
|
||||||
|
|
||||||
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
|
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
|
||||||
|
@ -87,7 +87,7 @@ extension Ghostty {
|
|||||||
// Subscribe to notifications for keyboard layout change so that we can update Ghostty.
|
// Subscribe to notifications for keyboard layout change so that we can update Ghostty.
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(self.keyboardSelectionDidChange(notification:)),
|
selector: #selector(keyboardSelectionDidChange(notification:)),
|
||||||
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
|
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
|
||||||
object: nil)
|
object: nil)
|
||||||
#endif
|
#endif
|
||||||
|
@ -14,6 +14,7 @@ extension Ghostty {
|
|||||||
if (mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0) { flags.insert(.control) }
|
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_ALT.rawValue != 0) { flags.insert(.option) }
|
||||||
if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) }
|
if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) }
|
||||||
|
if (mods.rawValue & GHOSTTY_MODS_FN.rawValue != 0) { flags.insert(.function) }
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ extension Ghostty {
|
|||||||
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
||||||
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
||||||
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
||||||
|
if (flags.contains(.function)) { mods |= GHOSTTY_MODS_FN.rawValue }
|
||||||
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
||||||
|
|
||||||
// Handle sided input. We can't tell that both are pressed in the
|
// Handle sided input. We can't tell that both are pressed in the
|
||||||
|
@ -726,6 +726,7 @@ extension Ghostty {
|
|||||||
case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue
|
case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue
|
||||||
case 0x3A, 0x3D: mod = GHOSTTY_MODS_ALT.rawValue
|
case 0x3A, 0x3D: mod = GHOSTTY_MODS_ALT.rawValue
|
||||||
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
|
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
|
||||||
|
case 0x3F: mod = GHOSTTY_MODS_FN.rawValue
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,9 +294,6 @@ pub fn keyEvent(
|
|||||||
.leaf => |leaf| leaf,
|
.leaf => |leaf| leaf,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We only care about global keybinds
|
|
||||||
if (!leaf.flags.global) return false;
|
|
||||||
|
|
||||||
// Perform the action
|
// Perform the action
|
||||||
self.performAllAction(rt_app, leaf.action) catch |err| {
|
self.performAllAction(rt_app, leaf.action) catch |err| {
|
||||||
log.warn("error performing global keybind action action={s} err={}", .{
|
log.warn("error performing global keybind action action={s} err={}", .{
|
||||||
|
@ -133,6 +133,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
|||||||
const ctrl_style: vaxis.Style = .{ .fg = .{ .index = 2 } };
|
const ctrl_style: vaxis.Style = .{ .fg = .{ .index = 2 } };
|
||||||
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
|
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
|
||||||
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
|
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
|
||||||
|
const fn_style: vaxis.Style = .{ .fg = .{ .index = 5 } };
|
||||||
|
|
||||||
var longest_col: usize = 0;
|
var longest_col: usize = 0;
|
||||||
|
|
||||||
@ -142,6 +143,10 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
|||||||
|
|
||||||
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
|
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
|
||||||
const trigger = bind.trigger;
|
const trigger = bind.trigger;
|
||||||
|
if (trigger.mods.function) {
|
||||||
|
result = try win.printSegment(.{ .text = "fn ", .style = fn_style }, .{ .col_offset = result.col });
|
||||||
|
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||||
|
}
|
||||||
if (trigger.mods.super) {
|
if (trigger.mods.super) {
|
||||||
result = try win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
result = try win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
||||||
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||||
|
@ -670,9 +670,22 @@ class: ?[:0]const u8 = null,
|
|||||||
/// translated by any system keyboard layouts. Example: "ctrl+physical:a"
|
/// translated by any system keyboard layouts. Example: "ctrl+physical:a"
|
||||||
///
|
///
|
||||||
/// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`,
|
/// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`,
|
||||||
/// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier
|
/// `option`), `super` (alias: `cmd`, `command`), and `function` (alias: `fn`,
|
||||||
/// or the alias. When debugging keybinds, the non-aliased modifier will always
|
/// `globe`). You may use the modifier or the alias. When debugging keybinds,
|
||||||
/// be used in output.
|
/// the non-aliased modifier will always be used in output.
|
||||||
|
///
|
||||||
|
/// Some notes about the `function` modifier:
|
||||||
|
///
|
||||||
|
/// * It is only available on macOS.
|
||||||
|
/// * It is used by many system shortcuts and Ghostty is not able to
|
||||||
|
/// override these shortcuts. If a system shortcut is triggered, the
|
||||||
|
/// system shortcut will take precedence.
|
||||||
|
/// * If you have multiple keyboard layouts active and don't press the
|
||||||
|
/// combination fast enough, macOS will switch to the next keyboard
|
||||||
|
/// layout.
|
||||||
|
/// * Menu items on macOS cannot be bound to the `function` modifier,
|
||||||
|
/// so this modifier will work with Ghostty but will not be visible
|
||||||
|
/// in the menu.
|
||||||
///
|
///
|
||||||
/// You may also specify multiple triggers separated by `>` to require a
|
/// You may also specify multiple triggers separated by `>` to require a
|
||||||
/// sequence of triggers to activate the action. For example,
|
/// sequence of triggers to activate the action. For example,
|
||||||
|
@ -896,9 +896,13 @@ pub const Trigger = struct {
|
|||||||
|
|
||||||
// Alias modifiers
|
// Alias modifiers
|
||||||
const alias_mods = .{
|
const alias_mods = .{
|
||||||
.{ "cmd", "super" }, .{ "command", "super" },
|
.{ "cmd", "super" },
|
||||||
.{ "opt", "alt" }, .{ "option", "alt" },
|
.{ "command", "super" },
|
||||||
|
.{ "opt", "alt" },
|
||||||
|
.{ "option", "alt" },
|
||||||
.{ "control", "ctrl" },
|
.{ "control", "ctrl" },
|
||||||
|
.{ "fn", "function" },
|
||||||
|
.{ "globe", "function" },
|
||||||
};
|
};
|
||||||
inline for (alias_mods) |pair| {
|
inline for (alias_mods) |pair| {
|
||||||
if (std.mem.eql(u8, part, pair[0])) {
|
if (std.mem.eql(u8, part, pair[0])) {
|
||||||
|
@ -89,10 +89,11 @@ pub const Mods = packed struct(Mods.Backing) {
|
|||||||
ctrl: bool = false,
|
ctrl: bool = false,
|
||||||
alt: bool = false,
|
alt: bool = false,
|
||||||
super: bool = false,
|
super: bool = false,
|
||||||
|
function: bool = false,
|
||||||
caps_lock: bool = false,
|
caps_lock: bool = false,
|
||||||
num_lock: bool = false,
|
num_lock: bool = false,
|
||||||
sides: side = .{},
|
sides: side = .{},
|
||||||
_padding: u6 = 0,
|
_padding: u5 = 0,
|
||||||
|
|
||||||
/// Tracks the side that is active for any given modifier. Note
|
/// Tracks the side that is active for any given modifier. Note
|
||||||
/// that this doesn't confirm a modifier is pressed; you must check
|
/// that this doesn't confirm a modifier is pressed; you must check
|
||||||
|
Reference in New Issue
Block a user