mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-28 18:11:39 +00:00
macOS: Input Improvements (#4591)
Sorry for the vague title. This PR addresses multiple issues: 1. Fixes #4540 2. #4522 is fixed for macOS only 3. Fixes #4590 4. Fixes an untracked issue where `command+key` events will not send release events for Kitty keyboard protocol, something I only noticed while working on this. There are multiple components to this PR. ## Part 1: `App/Surface.keyEventIsBinding` This new API (also available in libghostty as `ghostty_surface_key_is_binding`) returns a boolean true if the given key event would match a binding trigger if it was the next key event sent. It does not process the binding now. This can be used by event handlers that intercept key events to determine if it should send the event to Ghostty. This helps resolve #4590 for us but is also part of all resolved issues. ## Part 2: macOS `performKeyEquivalent` changes macOS calls `performKeyEquivalent` for any key combination that may trigger a key equivalent. if this returns `true` then it is handled and macOS ceases processing the event. We were already using this to intercept things like `Ctrl+/` which triggers a context menu in macOS Sequoia. But we now expand this to intercept all events to check for bindings. This lets us fix #4590. Additionally, it's been changed to special case `cmd+period`. I'm sure more need to be added. ## Part 3: NSEvent local listener for command keyUp events macOS simply doesn't send `keyUp` events for key events with command pressed. The only way to work around this is to register an `NSEvent` local listener. We now do this. This fixes the untracked issue noted above.
This commit is contained in:
19
src/App.zig
19
src/App.zig
@@ -313,6 +313,25 @@ pub fn focusEvent(self: *App, focused: bool) void {
|
||||
self.focused = focused;
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
_ = self;
|
||||
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return rt_app.config.keybind.set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Handle a key event at the app-scope. If this key event is used,
|
||||
/// this will return true and the caller shouldn't continue processing
|
||||
/// the event. If the event is not used, this will return false.
|
||||
|
||||
@@ -1637,6 +1637,31 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
///
|
||||
/// Note that this function does not check if the binding itself
|
||||
/// is performable, only if the key event would trigger a binding.
|
||||
/// If a performable binding is found and the event is not performable,
|
||||
/// then Ghosty will act as though the binding does not exist.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// Our keybinding set is either our current nested set (for
|
||||
// sequences) or the root set.
|
||||
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Called for any key events. This handles keybindings, encoding and
|
||||
/// sending to the terminal, etc.
|
||||
pub fn keyCallback(
|
||||
|
||||
@@ -147,12 +147,12 @@ pub const App = struct {
|
||||
self.core_app.focusEvent(focused);
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
/// Convert a C key event into a Zig key event.
|
||||
fn coreKeyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
) !?input.KeyEvent {
|
||||
const action = event.action;
|
||||
const keycode = event.keycode;
|
||||
const mods = event.mods;
|
||||
@@ -243,7 +243,7 @@ pub const App = struct {
|
||||
result.text,
|
||||
) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -251,7 +251,7 @@ pub const App = struct {
|
||||
.app => {},
|
||||
.surface => |surface| surface.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ pub const App = struct {
|
||||
} else .invalid;
|
||||
|
||||
// Build our final key event
|
||||
const input_event: input.KeyEvent = .{
|
||||
return .{
|
||||
.action = action,
|
||||
.key = key,
|
||||
.physical_key = physical_key,
|
||||
@@ -345,24 +345,39 @@ pub const App = struct {
|
||||
.utf8 = result.text,
|
||||
.unshifted_codepoint = unshifted_codepoint,
|
||||
};
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
// Convert our C key event into a Zig one.
|
||||
const input_event: input.KeyEvent = (try self.coreKeyEvent(
|
||||
target,
|
||||
event,
|
||||
)) orelse return false;
|
||||
|
||||
// Invoke the core Ghostty logic to handle this input.
|
||||
const effect: CoreSurface.InputEffect = switch (target) {
|
||||
.app => if (self.core_app.keyEvent(
|
||||
self,
|
||||
input_event,
|
||||
))
|
||||
.consumed
|
||||
else
|
||||
.ignored,
|
||||
)) .consumed else .ignored,
|
||||
|
||||
.surface => |surface| try surface.core_surface.keyCallback(input_event),
|
||||
.surface => |surface| try surface.core_surface.keyCallback(
|
||||
input_event,
|
||||
),
|
||||
};
|
||||
|
||||
return switch (effect) {
|
||||
.closed => true,
|
||||
.ignored => false,
|
||||
.consumed => consumed: {
|
||||
const is_down = input_event.action == .press or
|
||||
input_event.action == .repeat;
|
||||
|
||||
if (is_down) {
|
||||
// If we consume the key then we want to reset the dead
|
||||
// key state.
|
||||
@@ -1371,6 +1386,28 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_app_key_is_binding(
|
||||
app: *App,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = app.coreKeyEvent(
|
||||
.app,
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return app.core_app.keyEventIsBinding(app, core_event);
|
||||
}
|
||||
|
||||
/// Notify the app that the keyboard was changed. This causes the
|
||||
/// keyboard layout to be reloaded from the OS.
|
||||
export fn ghostty_app_keyboard_changed(v: *App) void {
|
||||
@@ -1591,16 +1628,38 @@ pub const CAPI = struct {
|
||||
export fn ghostty_surface_key(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) void {
|
||||
_ = surface.app.keyEvent(
|
||||
) bool {
|
||||
return surface.app.keyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_surface_key_is_binding(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = surface.app.coreKeyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return surface.core_surface.keyEventIsBinding(core_event);
|
||||
}
|
||||
|
||||
/// Send raw text to the terminal. This is treated like a paste
|
||||
/// so this isn't useful for sending escape sequences. For that,
|
||||
/// individual key input should be used.
|
||||
|
||||
Reference in New Issue
Block a user