mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-15 12:02:39 +00:00
gtk: implement quick-terminal-screen for Wayland
Implement the quick-terminal-screen config option on Linux/Wayland so users can pin the quick terminal to a specific monitor instead of always following the mouse cursor. Use the kde_output_order_v1 protocol to identify the compositor's primary monitor by connector name (e.g. "DP-1"). When the protocol is unavailable, fall back to the first monitor in the GDK list. - Add resolveQuickTerminalMonitor() to map config values to a gdk.Monitor: .mouse returns null (compositor decides), .main and .macos-menu-bar match by connector name via the protocol - Call layer_shell.setMonitor() in both initQuickTerminal and syncQuickTerminal so config reloads take effect - Update enteredMonitor to size the window using the configured monitor rather than whichever monitor was entered - Update config documentation to reflect Linux support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -33,6 +34,18 @@ pub const App = struct {
|
||||
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
kde_output_order: ?*kde.OutputOrderV1 = 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: ?[63:0]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,
|
||||
@@ -83,9 +96,16 @@ 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
|
||||
// Set up listeners for protocols that send events on bind.
|
||||
// All listeners must be set before the roundtrip so that
|
||||
// events aren't lost.
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
}
|
||||
if (context.kde_output_order) |output_order| {
|
||||
output_order.setListener(*Context, outputOrderListener, context);
|
||||
}
|
||||
if (context.kde_decoration_manager != null or context.kde_output_order != null) {
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
@@ -127,9 +147,55 @@ 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);
|
||||
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).
|
||||
fn resolveQuickTerminalMonitor(
|
||||
context: *Context,
|
||||
apprt_window: *ApprtWindow,
|
||||
) ?*gdk.Monitor {
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return null;
|
||||
const display = apprt_window.as(gtk.Widget).getDisplay();
|
||||
|
||||
return switch (config.@"quick-terminal-screen") {
|
||||
.mouse => null,
|
||||
.main, .@"macos-menu-bar" => blk: {
|
||||
const monitors = display.getMonitors();
|
||||
const primary_name: ?[]const u8 = if (context.primary_output_name) |*buf|
|
||||
std.mem.sliceTo(buf, 0)
|
||||
else
|
||||
null;
|
||||
|
||||
var fallback: ?*gdk.Monitor = null;
|
||||
var i: u32 = 0;
|
||||
while (monitors.getObject(i)) |item| : (i += 1) {
|
||||
// getObject returns transfer-full; release immediately.
|
||||
// The display keeps its own ref so the pointer stays valid.
|
||||
item.unref();
|
||||
const monitor = gobject.ext.cast(gdk.Monitor, item) orelse continue;
|
||||
if (fallback == null) fallback = monitor;
|
||||
|
||||
if (primary_name) |name| {
|
||||
const connector = std.mem.sliceTo(
|
||||
monitor.getConnector() orelse continue,
|
||||
0,
|
||||
);
|
||||
if (std.mem.eql(u8, connector, name)) {
|
||||
break :blk monitor;
|
||||
}
|
||||
}
|
||||
}
|
||||
break :blk fallback;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
@@ -200,10 +266,20 @@ 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;
|
||||
if (@field(context, field.name)) |global| {
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
|
||||
// Reset cached primary-output state if the protocol
|
||||
// providing it disappears.
|
||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
||||
context.primary_output_name = null;
|
||||
context.primary_output_match_failed_logged = false;
|
||||
context.output_order_done = true;
|
||||
}
|
||||
break :remove;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -221,6 +297,30 @@ pub const App = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn outputOrderListener(
|
||||
_: *kde.OutputOrderV1,
|
||||
event: kde.OutputOrderV1.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.output => |v| {
|
||||
if (context.output_order_done) {
|
||||
context.output_order_done = false;
|
||||
const name = std.mem.sliceTo(v.output_name, 0);
|
||||
if (name.len <= 63) {
|
||||
var buf: [63:0]u8 = @splat(0);
|
||||
@memcpy(buf[0..name.len], name);
|
||||
context.primary_output_name = buf;
|
||||
log.debug("primary output: {s}", .{name});
|
||||
}
|
||||
}
|
||||
},
|
||||
.done => {
|
||||
context.output_order_done = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
@@ -417,6 +517,11 @@ 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);
|
||||
layer_shell.setMonitor(window, target_monitor);
|
||||
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.@"quick-terminal-keyboard-interactivity") {
|
||||
@@ -486,8 +591,17 @@ pub const Window = struct {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
// Use the configured monitor for sizing if not in mouse mode
|
||||
const size_monitor = switch (config.@"quick-terminal-screen") {
|
||||
.mouse => monitor,
|
||||
.main, .@"macos-menu-bar" => App.resolveQuickTerminalMonitor(
|
||||
apprt_window.winproto().wayland.app_context,
|
||||
apprt_window,
|
||||
) 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",
|
||||
|
||||
@@ -2680,7 +2680,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