mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2398 from ghostty-org/app-focus
Make core app-focus aware, used to determine global keybind behavior
This commit is contained in:
@ -615,6 +615,7 @@ ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
bool ghostty_app_tick(ghostty_app_t);
|
||||
void* ghostty_app_userdata(ghostty_app_t);
|
||||
void ghostty_app_set_focus(ghostty_app_t, bool);
|
||||
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
|
||||
void ghostty_app_keyboard_changed(ghostty_app_t);
|
||||
void ghostty_app_open_config(ghostty_app_t);
|
||||
|
@ -83,14 +83,27 @@ extension Ghostty {
|
||||
}
|
||||
self.app = app
|
||||
|
||||
#if os(macOS)
|
||||
// Subscribe to notifications for keyboard layout change so that we can update Ghostty.
|
||||
NotificationCenter.default.addObserver(
|
||||
#if os(macOS)
|
||||
// Set our initial focus state
|
||||
ghostty_app_set_focus(app, NSApp.isActive)
|
||||
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardSelectionDidChange(notification:)),
|
||||
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
|
||||
object: nil)
|
||||
#endif
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidBecomeActive(notification:)),
|
||||
name: NSApplication.didBecomeActiveNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidResignActive(notification:)),
|
||||
name: NSApplication.didResignActiveNotification,
|
||||
object: nil)
|
||||
#endif
|
||||
|
||||
self.readiness = .ready
|
||||
}
|
||||
@ -98,14 +111,10 @@ extension Ghostty {
|
||||
deinit {
|
||||
// This will force the didSet callbacks to run which free.
|
||||
self.app = nil
|
||||
|
||||
#if os(macOS)
|
||||
// Remove our observer
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
|
||||
object: nil)
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: App Operations
|
||||
@ -266,6 +275,19 @@ extension Ghostty {
|
||||
ghostty_app_keyboard_changed(app)
|
||||
}
|
||||
|
||||
// Called when the app becomes active.
|
||||
@objc private func applicationDidBecomeActive(notification: NSNotification) {
|
||||
guard let app = self.app else { return }
|
||||
ghostty_app_set_focus(app, true)
|
||||
}
|
||||
|
||||
// Called when the app becomes inactive.
|
||||
@objc private func applicationDidResignActive(notification: NSNotification) {
|
||||
guard let app = self.app else { return }
|
||||
ghostty_app_set_focus(app, false)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Ghostty Callbacks (macOS)
|
||||
|
||||
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {
|
||||
|
66
src/App.zig
66
src/App.zig
@ -30,6 +30,21 @@ alloc: Allocator,
|
||||
/// The list of surfaces that are currently active.
|
||||
surfaces: SurfaceList,
|
||||
|
||||
/// This is true if the app that Ghostty is in is focused. This may
|
||||
/// mean that no surfaces (terminals) are focused but the app is still
|
||||
/// focused, i.e. may an about window. On macOS, this concept is known
|
||||
/// as the "active" app while focused windows are known as the
|
||||
/// "main" window.
|
||||
///
|
||||
/// This is used to determine if keyboard shortcuts that are non-global
|
||||
/// should be processed. If the app is not focused, then we don't want
|
||||
/// to process keyboard shortcuts that are not global.
|
||||
///
|
||||
/// This defaults to true since we assume that the app is focused when
|
||||
/// Ghostty is initialized but a well behaved apprt should call
|
||||
/// focusEvent to set this to the correct value right away.
|
||||
focused: bool = true,
|
||||
|
||||
/// The last focused surface. This surface may not be valid;
|
||||
/// you must always call hasSurface to validate it.
|
||||
focused_surface: ?*Surface = null,
|
||||
@ -54,6 +69,9 @@ last_notification_digest: u64 = 0,
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
///
|
||||
/// After calling this function, well behaved apprts should then call
|
||||
/// `focusEvent` to set the initial focus state of the app.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
) !*App {
|
||||
@ -265,9 +283,25 @@ pub fn setQuit(self: *App) !void {
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// Handle an app-level focus event. This should be called whenever
|
||||
/// the focus state of the entire app containing Ghostty changes.
|
||||
/// This is separate from surface focus events. See the `focused`
|
||||
/// field for more information.
|
||||
pub fn focusEvent(self: *App, focused: bool) void {
|
||||
// Prevent redundant focus events
|
||||
if (self.focused == focused) return;
|
||||
|
||||
log.debug("focus event focused={}", .{focused});
|
||||
self.focused = focused;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// If the app currently has focus then all key events are processed.
|
||||
/// If the app does not have focus then only global key events are
|
||||
/// processed.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
@ -294,13 +328,33 @@ pub fn keyEvent(
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
|
||||
// We only care about global keybinds
|
||||
if (!leaf.flags.global) return false;
|
||||
// If we aren't focused, then we only process global keybinds.
|
||||
if (!self.focused and !leaf.flags.global) return false;
|
||||
|
||||
// Perform the action
|
||||
self.performAllAction(rt_app, leaf.action) catch |err| {
|
||||
log.warn("error performing global keybind action action={s} err={}", .{
|
||||
@tagName(leaf.action),
|
||||
// Global keybinds are done using performAll so that they
|
||||
// can target all surfaces too.
|
||||
if (leaf.flags.global) {
|
||||
self.performAllAction(rt_app, leaf.action) catch |err| {
|
||||
log.warn("error performing global keybind action action={s} err={}", .{
|
||||
@tagName(leaf.action),
|
||||
err,
|
||||
});
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must be focused to process non-global keybinds
|
||||
assert(self.focused);
|
||||
assert(!leaf.flags.global);
|
||||
|
||||
// If we are focused, then we process keybinds only if they are
|
||||
// app-scoped. Otherwise, we do nothing. Surface-scoped should
|
||||
// be processed by Surface.keyEvent.
|
||||
const app_action = leaf.action.scoped(.app) orelse return false;
|
||||
self.performAction(rt_app, app_action) catch |err| {
|
||||
log.warn("error performing app keybind action action={s} err={}", .{
|
||||
@tagName(app_action),
|
||||
err,
|
||||
});
|
||||
};
|
||||
|
@ -132,6 +132,11 @@ pub const App = struct {
|
||||
surface: *Surface,
|
||||
};
|
||||
|
||||
/// See CoreApp.focusEvent
|
||||
pub fn focusEvent(self: *App, focused: bool) void {
|
||||
self.core_app.focusEvent(focused);
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
@ -1296,6 +1301,14 @@ pub const CAPI = struct {
|
||||
core_app.destroy();
|
||||
}
|
||||
|
||||
/// Update the focused state of the app.
|
||||
export fn ghostty_app_set_focus(
|
||||
app: *App,
|
||||
focused: bool,
|
||||
) void {
|
||||
app.focusEvent(focused);
|
||||
}
|
||||
|
||||
/// Notify the app of a global keypress capture. This will return
|
||||
/// true if the key was captured by the app, in which case the caller
|
||||
/// should not process the key.
|
||||
|
@ -97,7 +97,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
c.gtk_get_minor_version(),
|
||||
c.gtk_get_micro_version(),
|
||||
});
|
||||
|
||||
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api");
|
||||
@ -235,6 +235,24 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
|
||||
// Other signals
|
||||
_ = c.g_signal_connect_data(
|
||||
app,
|
||||
"window-added",
|
||||
c.G_CALLBACK(>kWindowAdded),
|
||||
core_app,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
_ = c.g_signal_connect_data(
|
||||
app,
|
||||
"window-removed",
|
||||
c.G_CALLBACK(>kWindowRemoved),
|
||||
core_app,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
|
||||
// We don't use g_application_run, we want to manually control the
|
||||
// loop so we have to do the same things the run function does:
|
||||
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
|
||||
@ -1065,6 +1083,72 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
fn gtkWindowAdded(
|
||||
_: *c.GtkApplication,
|
||||
window: *c.GtkWindow,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
||||
|
||||
// Request the is-active property change so we can detect
|
||||
// when our app loses focus.
|
||||
_ = c.g_signal_connect_data(
|
||||
window,
|
||||
"notify::is-active",
|
||||
c.G_CALLBACK(>kWindowIsActive),
|
||||
core_app,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
}
|
||||
|
||||
fn gtkWindowRemoved(
|
||||
_: *c.GtkApplication,
|
||||
_: *c.GtkWindow,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
||||
|
||||
// Recheck if we are focused
|
||||
gtkWindowIsActive(null, undefined, core_app);
|
||||
}
|
||||
|
||||
fn gtkWindowIsActive(
|
||||
window: ?*c.GtkWindow,
|
||||
_: *c.GParamSpec,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
||||
|
||||
// If our window is active, then we can tell the app
|
||||
// that we are focused.
|
||||
if (window) |w| {
|
||||
if (c.gtk_window_is_active(w) == 1) {
|
||||
core_app.focusEvent(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the window becomes inactive, we need to check if any
|
||||
// other windows are active. If not, then we are no longer
|
||||
// focused.
|
||||
if (c.gtk_window_list_toplevels()) |list| {
|
||||
defer c.g_list_free(list);
|
||||
var current: ?*c.GList = list;
|
||||
while (current) |elem| : (current = elem.next) {
|
||||
// If the window is active then we are still focused.
|
||||
// This is another window since we did our check above.
|
||||
// That window should trigger its own is-active
|
||||
// callback so we don't need to call it here.
|
||||
const w: *c.GtkWindow = @alignCast(@ptrCast(elem.data));
|
||||
if (c.gtk_window_is_active(w) == 1) return;
|
||||
}
|
||||
}
|
||||
|
||||
// We are not focused
|
||||
core_app.focusEvent(false);
|
||||
}
|
||||
|
||||
/// Call a D-Bus method to determine the current color scheme. If there
|
||||
/// is any error at any point we'll log the error and return "light"
|
||||
pub fn getColorScheme(self: *App) apprt.ColorScheme {
|
||||
|
Reference in New Issue
Block a user