mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
gtk: implement quick-terminal-screen for Linux/Wayland (#11117)
This commit is contained in:
@@ -3,6 +3,7 @@ const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("gtk4-layer-shell.h");
|
||||
});
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
pub const ShellLayer = enum(c_uint) {
|
||||
@@ -61,6 +62,10 @@ pub fn setKeyboardMode(window: *gtk.Window, mode: KeyboardMode) void {
|
||||
c.gtk_layer_set_keyboard_mode(@ptrCast(window), @intFromEnum(mode));
|
||||
}
|
||||
|
||||
pub fn setMonitor(window: *gtk.Window, monitor: ?*gdk.Monitor) void {
|
||||
c.gtk_layer_set_monitor(@ptrCast(window), @ptrCast(monitor));
|
||||
}
|
||||
|
||||
pub fn setNamespace(window: *gtk.Window, name: [:0]const u8) void {
|
||||
c.gtk_layer_set_namespace(@ptrCast(window), name.ptr);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const kde = wayland.client.kde;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
@@ -25,14 +26,30 @@ pub const App = struct {
|
||||
context: *Context,
|
||||
|
||||
const Context = struct {
|
||||
alloc: Allocator,
|
||||
|
||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
||||
kde_decoration_manager_global_name: ?u32 = null,
|
||||
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
kde_output_order: ?*kde.OutputOrderV1 = null,
|
||||
kde_output_order_global_name: ?u32 = null,
|
||||
|
||||
/// Connector name of the primary output (e.g., "DP-1") as reported
|
||||
/// by kde_output_order_v1. The first output in each priority list
|
||||
/// is the primary.
|
||||
primary_output_name: ?[:0]const u8 = null,
|
||||
|
||||
/// Tracks the output order event cycle. Set to true after a `done`
|
||||
/// event so the next `output` event is captured as the new primary.
|
||||
/// Initialized to true so the first event after binding is captured.
|
||||
output_order_done: bool = true,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
@@ -74,8 +91,11 @@ pub const App = struct {
|
||||
// a stable pointer, but it's too scary that we'd need one in the future
|
||||
// and not have it and corrupt memory or something so let's just do it.
|
||||
const context = try alloc.create(Context);
|
||||
errdefer alloc.destroy(context);
|
||||
context.* = .{};
|
||||
errdefer {
|
||||
if (context.primary_output_name) |name| alloc.free(name);
|
||||
alloc.destroy(context);
|
||||
}
|
||||
context.* = .{ .alloc = alloc };
|
||||
|
||||
// Get our display registry so we can get all the available interfaces
|
||||
// and bind to what we need.
|
||||
@@ -83,9 +103,10 @@ pub const App = struct {
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
// Do another round-trip to get the default decoration mode
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
// Do another roundtrip to process events emitted by globals we bound
|
||||
// during registry discovery (e.g. default decoration mode, output
|
||||
// order). Listeners are installed at bind time in registryListener.
|
||||
if (context.kde_decoration_manager != null or context.kde_output_order != null) {
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
@@ -96,6 +117,7 @@ pub const App = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
if (self.context.primary_output_name) |name| alloc.free(name);
|
||||
alloc.destroy(self.context);
|
||||
}
|
||||
|
||||
@@ -127,16 +149,63 @@ pub const App = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
|
||||
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
layer_shell.initForWindow(window);
|
||||
|
||||
// Set target monitor based on config (null lets compositor decide)
|
||||
const monitor = resolveQuickTerminalMonitor(self.context, apprt_window);
|
||||
defer if (monitor) |v| v.unref();
|
||||
layer_shell.setMonitor(window, monitor);
|
||||
}
|
||||
|
||||
/// Resolve the quick-terminal-screen config to a specific monitor.
|
||||
/// Returns null to let the compositor decide (used for .mouse mode).
|
||||
/// Caller owns the returned ref and must unref it.
|
||||
fn resolveQuickTerminalMonitor(
|
||||
context: *Context,
|
||||
apprt_window: *ApprtWindow,
|
||||
) ?*gdk.Monitor {
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return null;
|
||||
|
||||
switch (config.@"quick-terminal-screen") {
|
||||
.mouse => return null,
|
||||
.main, .@"macos-menu-bar" => {},
|
||||
}
|
||||
|
||||
const display = apprt_window.as(gtk.Widget).getDisplay();
|
||||
const monitors = display.getMonitors();
|
||||
|
||||
// Try to find the monitor matching the primary output name.
|
||||
if (context.primary_output_name) |stored_name| {
|
||||
var i: u32 = 0;
|
||||
while (monitors.getObject(i)) |item| : (i += 1) {
|
||||
const monitor = gobject.ext.cast(gdk.Monitor, item) orelse {
|
||||
item.unref();
|
||||
continue;
|
||||
};
|
||||
if (monitor.getConnector()) |connector_z| {
|
||||
if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) {
|
||||
return monitor;
|
||||
}
|
||||
}
|
||||
monitor.unref();
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the first monitor in the list.
|
||||
const first = monitors.getObject(0) orelse return null;
|
||||
return gobject.ext.cast(gdk.Monitor, first) orelse {
|
||||
first.unref();
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
.pointer => |v| if (v.size == .one) v.child else return null,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
@@ -147,6 +216,25 @@ pub const App = struct {
|
||||
return T;
|
||||
}
|
||||
|
||||
/// Returns the Context field that stores the registry global name for
|
||||
/// protocols that support replacement, or null for simple protocols.
|
||||
fn getGlobalNameField(comptime field_name: []const u8) ?[]const u8 {
|
||||
if (std.mem.eql(u8, field_name, "kde_decoration_manager")) {
|
||||
return "kde_decoration_manager_global_name";
|
||||
}
|
||||
if (std.mem.eql(u8, field_name, "kde_output_order")) {
|
||||
return "kde_output_order_global_name";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Reset cached state derived from kde_output_order_v1.
|
||||
fn resetOutputOrderState(context: *Context) void {
|
||||
if (context.primary_output_name) |name| context.alloc.free(name);
|
||||
context.primary_output_name = null;
|
||||
context.output_order_done = true;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
@@ -171,15 +259,10 @@ pub const App = struct {
|
||||
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
T.interface.name,
|
||||
) == .eq) {
|
||||
if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) {
|
||||
log.debug("matched {}", .{T});
|
||||
|
||||
@field(context, field.name) = registry.bind(
|
||||
const global = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
@@ -190,6 +273,32 @@ pub const App = struct {
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
// Destroy old binding if this global was re-advertised.
|
||||
// Bind first so a failed bind preserves the old binding.
|
||||
if (@field(context, field.name)) |old| {
|
||||
old.destroy();
|
||||
|
||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
||||
resetOutputOrderState(context);
|
||||
}
|
||||
}
|
||||
|
||||
@field(context, field.name) = global;
|
||||
if (comptime getGlobalNameField(field.name)) |name_field| {
|
||||
@field(context, name_field) = v.name;
|
||||
}
|
||||
|
||||
// Install listeners immediately at bind time. This
|
||||
// keeps listener setup and object lifetime in one
|
||||
// place and also supports globals that appear later.
|
||||
if (comptime std.mem.eql(u8, field.name, "kde_decoration_manager")) {
|
||||
global.setListener(*Context, decoManagerListener, context);
|
||||
}
|
||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
||||
global.setListener(*Context, outputOrderListener, context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -200,10 +309,29 @@ pub const App = struct {
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
|
||||
const global_name_field = comptime getGlobalNameField(field.name);
|
||||
if (global_name_field) |name_field| {
|
||||
if (@field(context, name_field)) |stored_name| {
|
||||
if (stored_name == v.name) {
|
||||
if (@field(context, field.name)) |global| global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
@field(context, name_field) = null;
|
||||
|
||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
||||
resetOutputOrderState(context);
|
||||
}
|
||||
break :remove;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (@field(context, field.name)) |global| {
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
break :remove;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -221,6 +349,44 @@ pub const App = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn outputOrderListener(
|
||||
_: *kde.OutputOrderV1,
|
||||
event: kde.OutputOrderV1.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.output => |v| {
|
||||
// Only the first output event after a `done` is the new primary.
|
||||
if (!context.output_order_done) return;
|
||||
context.output_order_done = false;
|
||||
|
||||
const name = std.mem.sliceTo(v.output_name, 0);
|
||||
if (context.primary_output_name) |old| context.alloc.free(old);
|
||||
|
||||
if (name.len == 0) {
|
||||
context.primary_output_name = null;
|
||||
log.warn("ignoring empty primary output name from kde_output_order_v1", .{});
|
||||
} else {
|
||||
context.primary_output_name = context.alloc.dupeZ(u8, name) catch |err| {
|
||||
context.primary_output_name = null;
|
||||
log.warn("failed to allocate primary output name: {}", .{err});
|
||||
return;
|
||||
};
|
||||
log.debug("primary output: {s}", .{name});
|
||||
}
|
||||
},
|
||||
.done => {
|
||||
if (context.output_order_done) {
|
||||
// No output arrived since the previous done. Treat this as
|
||||
// an empty update and drop any stale cached primary.
|
||||
resetOutputOrderState(context);
|
||||
return;
|
||||
}
|
||||
context.output_order_done = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
@@ -417,6 +583,12 @@ pub const Window = struct {
|
||||
});
|
||||
layer_shell.setNamespace(window, config.@"gtk-quick-terminal-namespace");
|
||||
|
||||
// Re-resolve the target monitor on every sync so that config reloads
|
||||
// and primary-output changes take effect without recreating the window.
|
||||
const target_monitor = App.resolveQuickTerminalMonitor(self.app_context, self.apprt_window);
|
||||
defer if (target_monitor) |v| v.unref();
|
||||
layer_shell.setMonitor(window, target_monitor);
|
||||
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.@"quick-terminal-keyboard-interactivity") {
|
||||
@@ -486,8 +658,17 @@ pub const Window = struct {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
const resolved_monitor = App.resolveQuickTerminalMonitor(
|
||||
apprt_window.winproto().wayland.app_context,
|
||||
apprt_window,
|
||||
);
|
||||
defer if (resolved_monitor) |v| v.unref();
|
||||
|
||||
// Use the configured monitor for sizing if not in mouse mode.
|
||||
const size_monitor = resolved_monitor orelse monitor;
|
||||
|
||||
var monitor_size: gdk.Rectangle = undefined;
|
||||
monitor.getGeometry(&monitor_size);
|
||||
size_monitor.getGeometry(&monitor_size);
|
||||
|
||||
const dims = config.@"quick-terminal-size".calculate(
|
||||
config.@"quick-terminal-position",
|
||||
|
||||
@@ -636,12 +636,16 @@ fn addGtkNg(
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/slide.xml"),
|
||||
);
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/kde-output-order-v1.xml"),
|
||||
);
|
||||
scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml");
|
||||
|
||||
scanner.generate("wl_compositor", 1);
|
||||
scanner.generate("org_kde_kwin_blur_manager", 1);
|
||||
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
|
||||
scanner.generate("org_kde_kwin_slide_manager", 1);
|
||||
scanner.generate("kde_output_order_v1", 1);
|
||||
scanner.generate("xdg_activation_v1", 1);
|
||||
|
||||
step.root_module.addImport("wayland", b.createModule(.{
|
||||
@@ -657,10 +661,10 @@ fn addGtkNg(
|
||||
.optimize = optimize,
|
||||
})) |gtk4_layer_shell| {
|
||||
const layer_shell_module = gtk4_layer_shell.module("gtk4-layer-shell");
|
||||
if (gobject_) |gobject| layer_shell_module.addImport(
|
||||
"gtk",
|
||||
gobject.module("gtk4"),
|
||||
);
|
||||
if (gobject_) |gobject| {
|
||||
layer_shell_module.addImport("gtk", gobject.module("gtk4"));
|
||||
layer_shell_module.addImport("gdk", gobject.module("gdk4"));
|
||||
}
|
||||
step.root_module.addImport(
|
||||
"gtk4-layer-shell",
|
||||
layer_shell_module,
|
||||
|
||||
@@ -2687,7 +2687,13 @@ keybind: Keybinds = .{},
|
||||
/// The default value is `main` because this is the recommended screen
|
||||
/// by the operating system.
|
||||
///
|
||||
/// Only implemented on macOS.
|
||||
/// On macOS, `macos-menu-bar` uses the screen containing the menu bar.
|
||||
/// On Linux/Wayland, `macos-menu-bar` is treated as equivalent to `main`.
|
||||
///
|
||||
/// Note: On Linux, there is no universal concept of a "primary" monitor.
|
||||
/// Ghostty uses the compositor-reported primary output when available and
|
||||
/// falls back to the first monitor reported by GDK if no primary output can
|
||||
/// be resolved.
|
||||
@"quick-terminal-screen": QuickTerminalScreen = .main,
|
||||
|
||||
/// Duration (in seconds) of the quick terminal enter and exit animation.
|
||||
|
||||
Reference in New Issue
Block a user