gtk: nuke the legacy apprt from orbit

We don't really have any large outstanding regressions on -ng to warrant
keeping this alive anymore. ¡Adiós!
This commit is contained in:
Leah Amelia Chen
2025-09-04 17:19:42 +02:00
parent c3e7857a2c
commit ac52af27d3
67 changed files with 21 additions and 13098 deletions

View File

@@ -22,7 +22,6 @@ jobs:
- build-macos-matrix
- build-windows
- test
- test-gtk
- test-gtk-ng
- test-sentry-linux
- test-macos
@@ -492,9 +491,6 @@ jobs:
- name: test
run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs -Demit-webdata
- name: Test GTK-NG Build
run: nix develop -c zig build -Dapp-runtime=gtk-ng -Demit-docs -Demit-webdata
@@ -502,55 +498,6 @@ jobs:
- name: Test System Build
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
test-gtk:
strategy:
fail-fast: false
matrix:
x11: ["true", "false"]
wayland: ["true", "false"]
name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Test
run: |
nix develop -c \
zig build \
-Dapp-runtime=gtk \
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }} \
test
- name: Build
run: |
nix develop -c \
zig build \
-Dapp-runtime=gtk \
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }}
test-gtk-ng:
strategy:
fail-fast: false

View File

@@ -118,7 +118,6 @@
/pkg/harfbuzz/ @ghostty-org/font
# GTK
/src/apprt/gtk/ @ghostty-org/gtk
/src/apprt/gtk-ng/ @ghostty-org/gtk
/src/os/cgroup.zig @ghostty-org/gtk
/src/os/flatpak.zig @ghostty-org/gtk

View File

@@ -16,7 +16,6 @@ const structs = @import("apprt/structs.zig");
pub const action = @import("apprt/action.zig");
pub const ipc = @import("apprt/ipc.zig");
pub const gtk = @import("apprt/gtk.zig");
pub const gtk_ng = @import("apprt/gtk-ng.zig");
pub const none = @import("apprt/none.zig");
pub const browser = @import("apprt/browser.zig");
@@ -43,7 +42,6 @@ pub const SurfaceSize = structs.SurfaceSize;
pub const runtime = switch (build_config.artifact) {
.exe => switch (build_config.app_runtime) {
.none => none,
.gtk => gtk,
.@"gtk-ng" => gtk_ng,
},
.lib => embedded,
@@ -64,11 +62,6 @@ pub const Runtime = enum {
/// approach to building the application.
@"gtk-ng",
/// GTK-backed. Rich windowed application. GTK is dynamically linked.
/// WARNING: Deprecated. This will be removed very soon. All bug fixes
/// and features should go into the gtk-ng backend.
gtk,
pub fn default(target: std.Target) Runtime {
return switch (target.os.tag) {
// The Linux and FreeBSD default is GTK because it is a full

View File

@@ -542,7 +542,7 @@ pub const InitialSize = extern struct {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
InitialSize,
.{ .name = "GhosttyApprtInitialSize" },
),

View File

@@ -1,12 +0,0 @@
//! Application runtime that uses GTK4.
pub const App = @import("gtk/App.zig");
pub const Surface = @import("gtk/Surface.zig");
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
test {
@import("std").testing.refAllDecls(@This());
_ = @import("gtk/inspector.zig");
_ = @import("gtk/key.zig");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +0,0 @@
/// Wrapper around GTK's builder APIs that perform some comptime checks.
const Builder = @This();
const std = @import("std");
const gtk = @import("gtk");
const gobject = @import("gobject");
resource_name: [:0]const u8,
builder: ?*gtk.Builder,
pub fn init(
/// The "name" of the resource.
comptime name: []const u8,
/// The major version of the minimum Adwaita version that is required to use
/// this resource.
comptime major: u16,
/// The minor version of the minimum Adwaita version that is required to use
/// this resource.
comptime minor: u16,
) Builder {
const resource_path = comptime resource_path: {
const gresource = @import("gresource.zig");
// Check to make sure that our file is listed as a
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
// could crash at runtime when we try and load a nonexistent
// GResource.
for (gresource.blueprint_files) |file| {
if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
// Use @embedFile to make sure that the `.blp` file exists
// at compile time. Zig _should_ discard the data so that
// it doesn't end up in the final executable. At runtime we
// will load the data from a GResource.
const blp_filename = std.fmt.comptimePrint(
"ui/{d}.{d}/{s}.blp",
.{
file.major,
file.minor,
file.name,
},
);
_ = @embedFile(blp_filename);
break :resource_path std.fmt.comptimePrint(
"/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
.{
file.major,
file.minor,
file.name,
},
);
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
};
return .{
.resource_name = resource_path,
.builder = null,
};
}
pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
class.setTemplateFromResource(self.resource_name);
}
pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T {
const builder = builder: {
if (self.builder) |builder| break :builder builder;
const builder = gtk.Builder.newFromResource(self.resource_name);
self.builder = builder;
break :builder builder;
};
return gobject.ext.cast(T, builder.getObject(name) orelse return null);
}
pub fn deinit(self: *const Builder) void {
if (self.builder) |builder| builder.unref();
}

View File

@@ -1,212 +0,0 @@
/// Clipboard Confirmation Window
const ClipboardConfirmation = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gtk = @import("gtk");
const adw = @import("adw");
const gobject = @import("gobject");
const gio = @import("gio");
const apprt = @import("../../apprt.zig");
const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const Builder = @import("Builder.zig");
const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk);
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
app: *App,
dialog: *DialogType,
data: [:0]u8,
core_surface: *CoreSurface,
pending_req: apprt.ClipboardRequest,
text_view: *gtk.TextView,
text_view_scroll: *gtk.ScrolledWindow,
reveal_button: *gtk.Button,
hide_button: *gtk.Button,
remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
pub fn create(
app: *App,
data: []const u8,
core_surface: *CoreSurface,
request: apprt.ClipboardRequest,
is_secure_input: bool,
) !void {
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
const alloc = app.core_app.alloc;
const self = try alloc.create(ClipboardConfirmation);
errdefer alloc.destroy(self);
try self.init(
app,
data,
core_surface,
request,
is_secure_input,
);
app.clipboard_confirmation_window = self;
}
/// Not public because this should be called by the GTK lifecycle.
fn destroy(self: *ClipboardConfirmation) void {
const alloc = self.app.core_app.alloc;
self.app.clipboard_confirmation_window = null;
alloc.free(self.data);
alloc.destroy(self);
}
fn init(
self: *ClipboardConfirmation,
app: *App,
data: []const u8,
core_surface: *CoreSurface,
request: apprt.ClipboardRequest,
is_secure_input: bool,
) !void {
var builder: Builder = switch (DialogType) {
adw.AlertDialog => switch (request) {
.osc_52_read => .init("ccw-osc-52-read", 1, 5),
.osc_52_write => .init("ccw-osc-52-write", 1, 5),
.paste => .init("ccw-paste", 1, 5),
},
adw.MessageDialog => switch (request) {
.osc_52_read => .init("ccw-osc-52-read", 1, 2),
.osc_52_write => .init("ccw-osc-52-write", 1, 2),
.paste => .init("ccw-paste", 1, 2),
},
else => unreachable,
};
defer builder.deinit();
const dialog = builder.getObject(DialogType, "clipboard_confirmation_window").?;
const text_view = builder.getObject(gtk.TextView, "text_view").?;
const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
const hide_button = builder.getObject(gtk.Button, "hide_button").?;
const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?;
const remember_choice = if (adw_version.supportsSwitchRow())
builder.getObject(adw.SwitchRow, "remember_choice")
else
null;
const copy = try app.core_app.alloc.dupeZ(u8, data);
errdefer app.core_app.alloc.free(copy);
self.* = .{
.app = app,
.dialog = dialog,
.data = copy,
.core_surface = core_surface,
.pending_req = request,
.text_view = text_view,
.text_view_scroll = text_view_scroll,
.reveal_button = reveal_button,
.hide_button = hide_button,
.remember_choice = remember_choice,
};
const buffer = gtk.TextBuffer.new(null);
errdefer buffer.unref();
buffer.insertAtCursor(copy.ptr, @intCast(copy.len));
text_view.setBuffer(buffer);
if (is_secure_input) {
text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
self.text_view.as(gtk.Widget).addCssClass("blurred");
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
_ = gtk.Button.signals.clicked.connect(
reveal_button,
*ClipboardConfirmation,
gtkRevealButtonClicked,
self,
.{},
);
_ = gtk.Button.signals.clicked.connect(
hide_button,
*ClipboardConfirmation,
gtkHideButtonClicked,
self,
.{},
);
}
_ = DialogType.signals.response.connect(
dialog,
*ClipboardConfirmation,
gtkResponse,
self,
.{},
);
switch (DialogType) {
adw.AlertDialog => {
const parent: ?*gtk.Widget = widget: {
const window = core_surface.rt_surface.container.window() orelse break :widget null;
break :widget window.window.as(gtk.Widget);
};
dialog.as(adw.Dialog).present(parent);
},
adw.MessageDialog => dialog.as(gtk.Window).present(),
else => unreachable,
}
}
fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
if (is_ok) {
self.core_surface.completeClipboardRequest(
self.pending_req,
self.data,
true,
) catch |err| {
log.err("Failed to requeue clipboard request: {}", .{err});
};
}
if (self.remember_choice) |remember| remember: {
if (!adw_version.supportsSwitchRow()) break :remember;
if (remember.getActive() == 0) break :remember;
switch (self.pending_req) {
.osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny,
.osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny,
.paste => {},
}
}
self.destroy();
}
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
const response = dialog.chooseFinish(result);
self.handleResponse(response);
}
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
self.handleResponse(response);
}
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
self.text_view.as(gtk.Widget).removeCssClass("blurred");
self.hide_button.as(gtk.Widget).setVisible(@intFromBool(true));
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
}
fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
self.text_view.as(gtk.Widget).addCssClass("blurred");
self.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
}

View File

@@ -1,151 +0,0 @@
const CloseDialog = @This();
const std = @import("std");
const gobject = @import("gobject");
const gio = @import("gio");
const adw = @import("adw");
const gtk = @import("gtk");
const i18n = @import("../../os/main.zig").i18n;
const App = @import("App.zig");
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const Surface = @import("Surface.zig");
const adwaita = @import("adw_version.zig");
const log = std.log.scoped(.close_dialog);
// We don't fall back to the GTK Message/AlertDialogs since
// we don't plan to support libadw < 1.2 as of time of writing
// TODO: Switch to just adw.AlertDialog when we drop Debian 12 support
const DialogType = if (adwaita.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
/// Open the dialog when the user requests to close a window/tab/split/etc.
/// but there's still one or more running processes inside the target that
/// cannot be closed automatically. We then ask the user whether they want
/// to terminate existing processes.
pub fn show(target: Target) !void {
// If we don't have a possible window to ask the user,
// in most situations (e.g. when a split isn't attached to a window)
// we should just close unconditionally.
const dialog_window = target.dialogWindow() orelse {
target.close();
return;
};
const dialog = switch (DialogType) {
adw.AlertDialog => adw.AlertDialog.new(target.title(), target.body()),
adw.MessageDialog => adw.MessageDialog.new(dialog_window, target.title(), target.body()),
else => unreachable,
};
// AlertDialog and MessageDialog have essentially the same API,
// so we can cheat a little here
dialog.addResponse("cancel", i18n._("Cancel"));
dialog.setCloseResponse("cancel");
dialog.addResponse("close", i18n._("Close"));
dialog.setResponseAppearance("close", .destructive);
// Need a stable pointer
const target_ptr = try target.allocator().create(Target);
target_ptr.* = target;
_ = DialogType.signals.response.connect(dialog, *Target, responseCallback, target_ptr, .{});
switch (DialogType) {
adw.AlertDialog => dialog.as(adw.Dialog).present(dialog_window.as(gtk.Widget)),
adw.MessageDialog => dialog.as(gtk.Window).present(),
else => unreachable,
}
}
fn responseCallback(
_: *DialogType,
response: [*:0]const u8,
target: *Target,
) callconv(.c) void {
const alloc = target.allocator();
defer alloc.destroy(target);
if (std.mem.orderZ(u8, response, "close") == .eq) target.close();
}
/// The target of a close dialog.
///
/// This is here so that we can consolidate all logic related to
/// prompting the user and closing windows/tabs/surfaces/etc.
/// together into one struct that is the sole source of truth.
pub const Target = union(enum) {
app: *App,
window: *Window,
tab: *Tab,
surface: *Surface,
pub fn title(self: Target) [*:0]const u8 {
return switch (self) {
.app => i18n._("Quit Ghostty?"),
.window => i18n._("Close Window?"),
.tab => i18n._("Close Tab?"),
.surface => i18n._("Close Split?"),
};
}
pub fn body(self: Target) [*:0]const u8 {
return switch (self) {
.app => i18n._("All terminal sessions will be terminated."),
.window => i18n._("All terminal sessions in this window will be terminated."),
.tab => i18n._("All terminal sessions in this tab will be terminated."),
.surface => i18n._("The currently running process in this split will be terminated."),
};
}
pub fn dialogWindow(self: Target) ?*gtk.Window {
return switch (self) {
.app => {
// Find the currently focused window. We don't store this
// anywhere inside the App structure for some reason, so
// we have to query every single open window and see which
// one is active (focused and receiving keyboard input)
const list = gtk.Window.listToplevels();
defer list.free();
const focused = list.findCustom(null, findActiveWindow);
return @ptrCast(@alignCast(focused.f_data));
},
.window => |v| v.window.as(gtk.Window),
.tab => |v| v.window.window.as(gtk.Window),
.surface => |v| {
const window_ = v.container.window() orelse return null;
return window_.window.as(gtk.Window);
},
};
}
fn allocator(self: Target) std.mem.Allocator {
return switch (self) {
.app => |v| v.core_app.alloc,
.window => |v| v.app.core_app.alloc,
.tab => |v| v.window.app.core_app.alloc,
.surface => |v| v.app.core_app.alloc,
};
}
fn close(self: Target) void {
switch (self) {
.app => |v| v.quitNow(),
.window => |v| v.window.as(gtk.Window).destroy(),
.tab => |v| v.remove(),
.surface => |v| v.container.remove(),
}
}
};
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
// Confusingly, `isActive` returns 1 when active,
// but we want to return 0 to indicate equality.
// Abusing integers to be enums and booleans is a terrible idea, C.
return if (window.isActive() != 0) 0 else -1;
}

View File

@@ -1,258 +0,0 @@
const CommandPalette = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
const gobject = @import("gobject");
const gtk = @import("gtk");
const configpkg = @import("../../config.zig");
const inputpkg = @import("../../input.zig");
const key = @import("key.zig");
const Builder = @import("Builder.zig");
const Window = @import("Window.zig");
const log = std.log.scoped(.command_palette);
window: *Window,
arena: std.heap.ArenaAllocator,
/// The dialog object containing the palette UI.
dialog: *adw.Dialog,
/// The search input text field.
search: *gtk.SearchEntry,
/// The view containing each result row.
view: *gtk.ListView,
/// The model that provides filtered data for the view to display.
model: *gtk.SingleSelection,
/// The list that serves as the data source of the model.
/// This is where all command data is ultimately stored.
source: *gio.ListStore,
pub fn init(self: *CommandPalette, window: *Window) !void {
// Register the custom command type *before* initializing the builder
// If we don't do this now, the builder will complain that it doesn't know
// about this type and fail to initialize
_ = Command.getGObjectType();
var builder = Builder.init("command-palette", 1, 5);
defer builder.deinit();
self.* = .{
.window = window,
.arena = .init(window.app.core_app.alloc),
.dialog = builder.getObject(adw.Dialog, "command-palette").?,
.search = builder.getObject(gtk.SearchEntry, "search").?,
.view = builder.getObject(gtk.ListView, "view").?,
.model = builder.getObject(gtk.SingleSelection, "model").?,
.source = builder.getObject(gio.ListStore, "source").?,
};
// Manually take a reference here so that the dialog
// remains in memory after closing
self.dialog.ref();
errdefer self.dialog.unref();
_ = gtk.SearchEntry.signals.stop_search.connect(
self.search,
*CommandPalette,
searchStopped,
self,
.{},
);
_ = gtk.SearchEntry.signals.activate.connect(
self.search,
*CommandPalette,
searchActivated,
self,
.{},
);
_ = gtk.ListView.signals.activate.connect(
self.view,
*CommandPalette,
rowActivated,
self,
.{},
);
try self.updateConfig(&self.window.app.config);
}
pub fn deinit(self: *CommandPalette) void {
self.arena.deinit();
self.dialog.unref();
}
pub fn toggle(self: *CommandPalette) void {
self.dialog.present(self.window.window.as(gtk.Widget));
// Focus on the search bar when opening the dialog
_ = self.search.as(gtk.Widget).grabFocus();
}
pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void {
// Clear existing binds and clear allocated data
self.source.removeAll();
_ = self.arena.reset(.retain_capacity);
for (config.@"command-palette-entry".value.items) |command| {
// Filter out actions that are not implemented
// or don't make sense for GTK
switch (command.action) {
.close_all_windows,
.toggle_secure_input,
.check_for_updates,
.redo,
.undo,
.reset_window_size,
.toggle_window_float_on_top,
=> continue,
else => {},
}
const cmd = try Command.new(
self.arena.allocator(),
command,
config.keybind.set,
);
const cmd_ref = cmd.as(gobject.Object);
self.source.append(cmd_ref);
cmd_ref.unref();
}
}
fn activated(self: *CommandPalette, pos: c_uint) void {
// Use self.model and not self.source here to use the list of *visible* results
const object = self.model.as(gio.ListModel).getObject(pos) orelse return;
const cmd = gobject.ext.cast(Command, object) orelse return;
// Close before running the action in order to avoid being replaced by another
// dialog (such as the change title dialog). If that occurs then the command
// palette dialog won't be counted as having closed properly and cannot
// receive focus when reopened.
_ = self.dialog.close();
const action = inputpkg.Binding.Action.parse(
std.mem.span(cmd.cmd_c.action_key),
) catch |err| {
log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err });
return;
};
self.window.performBindingAction(action);
}
fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
// ESC was pressed - close the palette
_ = self.dialog.close();
}
fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
// If Enter is pressed, activate the selected entry
self.activated(self.model.getSelected());
}
fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
self.activated(pos);
}
/// Object that wraps around a command.
///
/// As GTK list models only accept objects that are within the GObject hierarchy,
/// we have to construct a wrapper to be easily consumed by the list model.
const Command = extern struct {
parent: Parent,
cmd_c: inputpkg.Command.C,
pub const getGObjectType = gobject.ext.defineClass(Command, .{
.name = "GhosttyCommand",
.classInit = Class.init,
});
pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command {
const self = gobject.ext.newInstance(Command, .{});
var buf: [64]u8 = undefined;
const action = action: {
const trigger = keybinds.getTrigger(cmd.action) orelse break :action null;
const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null;
break :action try alloc.dupeZ(u8, accel);
};
self.cmd_c = .{
.title = cmd.title.ptr,
.description = cmd.description.ptr,
.action = if (action) |v| v.ptr else "",
.action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}),
};
return self;
}
fn as(self: *Command, comptime T: type) *T {
return gobject.ext.as(T, self);
}
pub const Parent = gobject.Object;
pub const Class = extern struct {
parent: Parent.Class,
pub const Instance = Command;
pub fn init(class: *Class) callconv(.c) void {
const info = @typeInfo(inputpkg.Command.C).@"struct";
// Expose all fields on the Command.C struct as properties
// that can be accessed by the GObject type system
// (and by extension, blueprints)
const properties = comptime props: {
var props: [info.fields.len]type = undefined;
for (info.fields, 0..) |field, i| {
const accessor = struct {
fn getter(cmd: *Command) ?[:0]const u8 {
return std.mem.span(@field(cmd.cmd_c, field.name));
}
};
// "Canonicalize" field names into the format GObject expects
const prop_name = prop_name: {
var buf: [field.name.len:0]u8 = undefined;
_ = std.mem.replace(u8, field.name, "_", "-", &buf);
break :prop_name buf;
};
props[i] = gobject.ext.defineProperty(
&prop_name,
Command,
?[:0]const u8,
.{
.default = null,
.accessor = gobject.ext.typedAccessor(
Command,
?[:0]const u8,
.{
.getter = &accessor.getter,
},
),
},
);
}
break :props props;
};
gobject.ext.registerProperties(class, &properties);
}
};
};

View File

@@ -1,102 +0,0 @@
/// Configuration errors window.
const ConfigErrorsDialog = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gobject = @import("gobject");
const gio = @import("gio");
const gtk = @import("gtk");
const adw = @import("adw");
const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
const App = @import("App.zig");
const Window = @import("Window.zig");
const Builder = @import("Builder.zig");
const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk);
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
builder: Builder,
dialog: *DialogType,
error_message: *gtk.TextBuffer,
pub fn maybePresent(app: *App, window: ?*Window) void {
if (app.config._diagnostics.empty()) return;
const config_errors_dialog = config_errors_dialog: {
if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog;
var builder: Builder = switch (DialogType) {
adw.AlertDialog => .init("config-errors-dialog", 1, 5),
adw.MessageDialog => .init("config-errors-dialog", 1, 2),
else => unreachable,
};
const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
_ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
app.config_errors_dialog = .{
.builder = builder,
.dialog = dialog,
.error_message = error_message,
};
break :config_errors_dialog app.config_errors_dialog.?;
};
{
var start = std.mem.zeroes(gtk.TextIter);
config_errors_dialog.error_message.getStartIter(&start);
var end = std.mem.zeroes(gtk.TextIter);
config_errors_dialog.error_message.getEndIter(&end);
config_errors_dialog.error_message.delete(&start, &end);
}
var msg_buf: [4095:0]u8 = undefined;
var fbs = std.io.fixedBufferStream(&msg_buf);
for (app.config._diagnostics.items()) |diag| {
fbs.reset();
diag.write(fbs.writer()) catch |err| {
log.warn(
"error writing diagnostic to buffer err={}",
.{err},
);
continue;
};
config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
config_errors_dialog.error_message.insertAtCursor("\n", 1);
}
switch (DialogType) {
adw.AlertDialog => {
const parent = if (window) |w| w.window.as(gtk.Widget) else null;
config_errors_dialog.dialog.as(adw.Dialog).present(parent);
},
adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(),
else => unreachable,
}
}
fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void {
if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit();
app.config_errors_dialog = null;
if (std.mem.orderZ(u8, response, "reload") == .eq) {
app.reloadConfig(.app, .{}) catch |err| {
log.warn("error reloading config error={}", .{err});
return;
};
}
}

View File

@@ -1,422 +0,0 @@
const GlobalShortcuts = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const App = @import("App.zig");
const configpkg = @import("../../config.zig");
const Binding = @import("../../input.zig").Binding;
const key = @import("key.zig");
const log = std.log.scoped(.global_shortcuts);
const Token = [16]u8;
app: *App,
arena: std.heap.ArenaAllocator,
dbus: *gio.DBusConnection,
/// A mapping from a unique ID to an action.
/// Currently the unique ID is simply the serialized representation of the
/// trigger that was used for the action as triggers are unique in the keymap,
/// but this may change in the future.
map: std.StringArrayHashMapUnmanaged(Binding.Action) = .{},
/// The handle of the current global shortcuts portal session,
/// as a D-Bus object path.
handle: ?[:0]const u8 = null,
/// The D-Bus signal subscription for the response signal on requests.
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
response_subscription: c_uint = 0,
/// The D-Bus signal subscription for the keybind activate signal.
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
activate_subscription: c_uint = 0,
pub fn init(alloc: Allocator, gio_app: *gio.Application) ?GlobalShortcuts {
const dbus = gio_app.getDbusConnection() orelse return null;
return .{
// To be initialized later
.app = undefined,
.arena = .init(alloc),
.dbus = dbus,
};
}
pub fn deinit(self: *GlobalShortcuts) void {
self.close();
self.arena.deinit();
}
fn close(self: *GlobalShortcuts) void {
if (self.response_subscription != 0) {
self.dbus.signalUnsubscribe(self.response_subscription);
self.response_subscription = 0;
}
if (self.activate_subscription != 0) {
self.dbus.signalUnsubscribe(self.activate_subscription);
self.activate_subscription = 0;
}
if (self.handle) |handle| {
// Close existing session
self.dbus.call(
"org.freedesktop.portal.Desktop",
handle,
"org.freedesktop.portal.Session",
"Close",
null,
null,
.{},
-1,
null,
null,
null,
);
self.handle = null;
}
}
pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void {
// Ensure we have a valid reference to the app
// (it was left uninitialized in `init`)
self.app = app;
// Close any existing sessions
self.close();
// Update map
var trigger_buf: [256]u8 = undefined;
self.map.clearRetainingCapacity();
var it = self.app.config.keybind.set.bindings.iterator();
while (it.next()) |entry| {
const leaf = switch (entry.value_ptr.*) {
// Global shortcuts can't have leaders
.leader => continue,
.leaf => |leaf| leaf,
};
if (!leaf.flags.global) continue;
const trigger = try key.xdgShortcutFromTrigger(
&trigger_buf,
entry.key_ptr.*,
) orelse continue;
try self.map.put(
self.arena.allocator(),
try self.arena.allocator().dupeZ(u8, trigger),
leaf.action,
);
}
if (self.map.count() > 0) {
try self.request(.create_session);
}
}
fn shortcutActivated(
_: *gio.DBusConnection,
_: ?[*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
params: *glib.Variant,
ud: ?*anyopaque,
) callconv(.c) void {
const self: *GlobalShortcuts = @ptrCast(@alignCast(ud));
// 2nd value in the tuple is the activated shortcut ID
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated
var shortcut_id: [*:0]const u8 = undefined;
params.getChild(1, "&s", &shortcut_id);
log.debug("activated={s}", .{shortcut_id});
const action = self.map.get(std.mem.span(shortcut_id)) orelse return;
self.app.core_app.performAllAction(self.app, action) catch |err| {
log.err("failed to perform action={}", .{err});
};
}
const Method = enum {
create_session,
bind_shortcuts,
fn name(self: Method) [:0]const u8 {
return switch (self) {
.create_session => "CreateSession",
.bind_shortcuts => "BindShortcuts",
};
}
/// Construct the payload expected by the XDG portal call.
fn makePayload(
self: Method,
shortcuts: *GlobalShortcuts,
request_token: [:0]const u8,
) ?*glib.Variant {
switch (self) {
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
.create_session => {
var session_token: Token = undefined;
return glib.Variant.newParsed(
"({'handle_token': <%s>, 'session_handle_token': <%s>},)",
request_token.ptr,
generateToken(&session_token).ptr,
);
},
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
.bind_shortcuts => {
const handle = shortcuts.handle orelse return null;
const bind_type = glib.VariantType.new("a(sa{sv})");
defer glib.free(bind_type);
var binds: glib.VariantBuilder = undefined;
glib.VariantBuilder.init(&binds, bind_type);
var action_buf: [256]u8 = undefined;
var it = shortcuts.map.iterator();
while (it.next()) |entry| {
const trigger = entry.key_ptr.*.ptr;
const action = std.fmt.bufPrintZ(
&action_buf,
"{}",
.{entry.value_ptr.*},
) catch continue;
binds.addParsed(
"(%s, {'description': <%s>, 'preferred_trigger': <%s>})",
trigger,
action.ptr,
trigger,
);
}
return glib.Variant.newParsed(
"(%o, %*, '', {'handle_token': <%s>})",
handle.ptr,
binds.end(),
request_token.ptr,
);
},
}
}
fn onResponse(self: Method, shortcuts: *GlobalShortcuts, vardict: *glib.Variant) void {
switch (self) {
.create_session => {
var handle: ?[*:0]u8 = null;
if (vardict.lookup("session_handle", "&s", &handle) == 0) {
log.err(
"session handle not found in response={s}",
.{vardict.print(@intFromBool(true))},
);
return;
}
shortcuts.handle = shortcuts.arena.allocator().dupeZ(u8, std.mem.span(handle.?)) catch {
log.err("out of memory: failed to clone session handle", .{});
return;
};
log.debug("session_handle={?s}", .{handle});
// Subscribe to keybind activations
shortcuts.activate_subscription = shortcuts.dbus.signalSubscribe(
null,
"org.freedesktop.portal.GlobalShortcuts",
"Activated",
"/org/freedesktop/portal/desktop",
handle,
.{ .match_arg0_path = true },
shortcutActivated,
shortcuts,
null,
);
shortcuts.request(.bind_shortcuts) catch |err| {
log.err("failed to bind shortcuts={}", .{err});
return;
};
},
.bind_shortcuts => {},
}
}
};
/// Submit a request to the global shortcuts portal.
fn request(
self: *GlobalShortcuts,
comptime method: Method,
) !void {
// NOTE(pluiedev):
// XDG Portals are really, really poorly-designed pieces of hot garbage.
// How the protocol is _initially_ designed to work is as follows:
//
// 1. The client calls a method which returns the path of a Request object;
// 2. The client waits for the Response signal under said object path;
// 3. When the signal arrives, the actual return value and status code
// become available for the client for further processing.
//
// THIS DOES NOT WORK. Once the first two steps are complete, the client
// needs to immediately start listening for the third step, but an overeager
// server implementation could easily send the Response signal before the
// client is even ready, causing communications to break down over a simple
// race condition/two generals' problem that even _TCP_ had figured out
// decades ago. Worse yet, you get exactly _one_ chance to listen for the
// signal, or else your communication attempt so far has all been in vain.
//
// And they know this. Instead of fixing their freaking protocol, they just
// ask clients to manually construct the expected object path and subscribe
// to the request signal beforehand, making the whole response value of
// the original call COMPLETELY MEANINGLESS.
//
// Furthermore, this is _entirely undocumented_ aside from one tiny
// paragraph under the documentation for the Request interface, and
// anyone would be forgiven for missing it without reading the libportal
// source code.
//
// When in Rome, do as the Romans do, I guess...?
const callbacks = struct {
fn gotResponseHandle(
source: ?*gobject.Object,
res: *gio.AsyncResult,
_: ?*anyopaque,
) callconv(.c) void {
const dbus_ = gobject.ext.cast(gio.DBusConnection, source.?).?;
var err: ?*glib.Error = null;
defer if (err) |err_| err_.free();
const params_ = dbus_.callFinish(res, &err) orelse {
if (err) |err_| log.err("request failed={s} ({})", .{
err_.f_message orelse "(unknown)",
err_.f_code,
});
return;
};
defer params_.unref();
// TODO: XDG recommends updating the signal subscription if the actual
// returned request path is not the same as the expected request
// path, to retain compatibility with older versions of XDG portals.
// Although it suffers from the race condition outlined above,
// we should still implement this at some point.
}
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response
fn responded(
dbus: *gio.DBusConnection,
_: ?[*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
params_: *glib.Variant,
ud: ?*anyopaque,
) callconv(.c) void {
const self_: *GlobalShortcuts = @ptrCast(@alignCast(ud));
// Unsubscribe from the response signal
if (self_.response_subscription != 0) {
dbus.signalUnsubscribe(self_.response_subscription);
self_.response_subscription = 0;
}
var response: u32 = 0;
var vardict: ?*glib.Variant = null;
defer if (vardict) |v| v.unref();
params_.get("(u@a{sv})", &response, &vardict);
switch (response) {
0 => {
log.debug("request successful", .{});
method.onResponse(self_, vardict.?);
},
1 => log.debug("request was cancelled by user", .{}),
2 => log.warn("request ended unexpectedly", .{}),
else => log.err("unrecognized response code={}", .{response}),
}
}
};
var request_token_buf: Token = undefined;
const request_token = generateToken(&request_token_buf);
const payload = method.makePayload(self, request_token) orelse return;
const request_path = try self.getRequestPath(request_token);
self.response_subscription = self.dbus.signalSubscribe(
null,
"org.freedesktop.portal.Request",
"Response",
request_path,
null,
.{},
callbacks.responded,
self,
null,
);
self.dbus.call(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.GlobalShortcuts",
method.name(),
payload,
null,
.{},
-1,
null,
callbacks.gotResponseHandle,
null,
);
}
/// Generate a random token suitable for use in requests.
fn generateToken(buf: *Token) [:0]const u8 {
// u28 takes up 7 bytes in hex, 8 bytes for "ghostty_" and 1 byte for NUL
// 7 + 8 + 1 = 16
return std.fmt.bufPrintZ(
buf,
"ghostty_{x:0<7}",
.{std.crypto.random.int(u28)},
) catch unreachable;
}
/// Get the XDG portal request path for the current Ghostty instance.
///
/// If this sounds like nonsense, see `request` for an explanation as to
/// why we need to do this.
fn getRequestPath(self: *GlobalShortcuts, token: [:0]const u8) ![:0]const u8 {
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
// for the syntax XDG portals expect.
// `getUniqueName` should never return null here as we're using an ordinary
// message bus connection. If it doesn't, something is very wrong
const unique_name = std.mem.span(self.dbus.getUniqueName().?);
const object_path = try std.mem.joinZ(self.arena.allocator(), "/", &.{
"/org/freedesktop/portal/desktop/request",
unique_name[1..], // Remove leading `:`
token,
});
// Sanitize the unique name by replacing every `.` with `_`.
// In effect, this will turn a unique name like `:1.192` into `1_192`.
// Valid D-Bus object path components never contain `.`s anyway, so we're
// free to replace all instances of `.` here and avoid extra allocation.
std.mem.replaceScalar(u8, object_path, '.', '_');
return object_path;
}

View File

@@ -1,470 +0,0 @@
const ImguiWidget = @This();
const std = @import("std");
const assert = std.debug.assert;
const gdk = @import("gdk");
const gtk = @import("gtk");
const cimgui = @import("cimgui");
const gl = @import("opengl");
const key = @import("key.zig");
const input = @import("../../input.zig");
const log = std.log.scoped(.gtk_imgui_widget);
/// This is called every frame to populate the ImGui frame.
render_callback: ?*const fn (?*anyopaque) void = null,
render_userdata: ?*anyopaque = null,
/// Our OpenGL widget
gl_area: *gtk.GLArea,
im_context: *gtk.IMContext,
/// ImGui Context
ig_ctx: *cimgui.c.ImGuiContext,
/// Our previous instant used to calculate delta time for animations.
instant: ?std.time.Instant = null,
/// Initialize the widget. This must have a stable pointer for events.
pub fn init(self: *ImguiWidget) !void {
// Each widget gets its own imgui context so we can have multiple
// imgui views in the same application.
const ig_ctx = cimgui.c.igCreateContext(null) orelse return error.OutOfMemory;
errdefer cimgui.c.igDestroyContext(ig_ctx);
cimgui.c.igSetCurrentContext(ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
io.BackendPlatformName = "ghostty_gtk";
// Our OpenGL area for drawing
const gl_area = gtk.GLArea.new();
gl_area.setAutoRender(@intFromBool(true));
// The GL area has to be focusable so that it can receive events
gl_area.as(gtk.Widget).setFocusable(@intFromBool(true));
gl_area.as(gtk.Widget).setFocusOnClick(@intFromBool(true));
// Clicks
const gesture_click = gtk.GestureClick.new();
errdefer gesture_click.unref();
gesture_click.as(gtk.GestureSingle).setButton(0);
gl_area.as(gtk.Widget).addController(gesture_click.as(gtk.EventController));
// Mouse movement
const ec_motion = gtk.EventControllerMotion.new();
errdefer ec_motion.unref();
gl_area.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
// Scroll events
const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes);
errdefer ec_scroll.unref();
gl_area.as(gtk.Widget).addController(ec_scroll.as(gtk.EventController));
// Focus controller will tell us about focus enter/exit events
const ec_focus = gtk.EventControllerFocus.new();
errdefer ec_focus.unref();
gl_area.as(gtk.Widget).addController(ec_focus.as(gtk.EventController));
// Key event controller will tell us about raw keypress events.
const ec_key = gtk.EventControllerKey.new();
errdefer ec_key.unref();
gl_area.as(gtk.Widget).addController(ec_key.as(gtk.EventController));
errdefer gl_area.as(gtk.Widget).removeController(ec_key.as(gtk.EventController));
// The input method context that we use to translate key events into
// characters. This doesn't have an event key controller attached because
// we call it manually from our own key controller.
const im_context = gtk.IMMulticontext.new();
errdefer im_context.unref();
// Signals
_ = gtk.Widget.signals.realize.connect(
gl_area,
*ImguiWidget,
gtkRealize,
self,
.{},
);
_ = gtk.Widget.signals.unrealize.connect(
gl_area,
*ImguiWidget,
gtkUnrealize,
self,
.{},
);
_ = gtk.Widget.signals.destroy.connect(
gl_area,
*ImguiWidget,
gtkDestroy,
self,
.{},
);
_ = gtk.GLArea.signals.render.connect(
gl_area,
*ImguiWidget,
gtkRender,
self,
.{},
);
_ = gtk.GLArea.signals.resize.connect(
gl_area,
*ImguiWidget,
gtkResize,
self,
.{},
);
_ = gtk.EventControllerKey.signals.key_pressed.connect(
ec_key,
*ImguiWidget,
gtkKeyPressed,
self,
.{},
);
_ = gtk.EventControllerKey.signals.key_released.connect(
ec_key,
*ImguiWidget,
gtkKeyReleased,
self,
.{},
);
_ = gtk.EventControllerFocus.signals.enter.connect(
ec_focus,
*ImguiWidget,
gtkFocusEnter,
self,
.{},
);
_ = gtk.EventControllerFocus.signals.leave.connect(
ec_focus,
*ImguiWidget,
gtkFocusLeave,
self,
.{},
);
_ = gtk.GestureClick.signals.pressed.connect(
gesture_click,
*ImguiWidget,
gtkMouseDown,
self,
.{},
);
_ = gtk.GestureClick.signals.released.connect(
gesture_click,
*ImguiWidget,
gtkMouseUp,
self,
.{},
);
_ = gtk.EventControllerMotion.signals.motion.connect(
ec_motion,
*ImguiWidget,
gtkMouseMotion,
self,
.{},
);
_ = gtk.EventControllerScroll.signals.scroll.connect(
ec_scroll,
*ImguiWidget,
gtkMouseScroll,
self,
.{},
);
_ = gtk.IMContext.signals.commit.connect(
im_context,
*ImguiWidget,
gtkInputCommit,
self,
.{},
);
self.* = .{
.gl_area = gl_area,
.im_context = im_context.as(gtk.IMContext),
.ig_ctx = ig_ctx,
};
}
/// Deinitialize the widget. This should ONLY be called if the widget gl_area
/// was never added to a parent. Otherwise, cleanup automatically happens
/// when the widget is destroyed and this should NOT be called.
pub fn deinit(self: *ImguiWidget) void {
cimgui.c.igDestroyContext(self.ig_ctx);
}
/// This should be called anytime the underlying data for the UI changes
/// so that the UI can be refreshed.
pub fn queueRender(self: *const ImguiWidget) void {
self.gl_area.queueRender();
}
/// Initialize the frame. Expects that the context is already current.
fn newFrame(self: *ImguiWidget) !void {
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
// Determine our delta time
const now = try std.time.Instant.now();
io.DeltaTime = if (self.instant) |prev| delta: {
const since_ns = now.since(prev);
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
break :delta @max(0.00001, since_s);
} else (1 / 60);
self.instant = now;
}
fn translateMouseButton(button: c_uint) ?c_int {
return switch (button) {
1 => cimgui.c.ImGuiMouseButton_Left,
2 => cimgui.c.ImGuiMouseButton_Middle,
3 => cimgui.c.ImGuiMouseButton_Right,
else => null,
};
}
fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
log.debug("imgui widget destroy", .{});
self.deinit();
}
fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
log.debug("gl surface realized", .{});
// We need to make the context current so we can call GL functions.
area.makeCurrent();
if (area.getError()) |err| {
log.err("surface failed to realize: {s}", .{err.f_message orelse "(unknown)"});
return;
}
// realize means that our OpenGL context is ready, so we can now
// initialize the ImgUI OpenGL backend for our context.
cimgui.c.igSetCurrentContext(self.ig_ctx);
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
}
fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
_ = area;
log.debug("gl surface unrealized", .{});
cimgui.c.igSetCurrentContext(self.ig_ctx);
cimgui.ImGui_ImplOpenGL3_Shutdown();
}
fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor = area.as(gtk.Widget).getScaleFactor();
log.debug("gl resize width={} height={} scale={}", .{
width,
height,
scale_factor,
});
// Our display size is always unscaled. We'll do the scaling in the
// style instead. This creates crisper looking fonts.
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
// Setup a new style and scale it appropriately.
const style = cimgui.c.ImGuiStyle_ImGuiStyle();
defer cimgui.c.ImGuiStyle_destroy(style);
cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
const active_style = cimgui.c.igGetStyle();
active_style.* = style.*;
}
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
cimgui.c.igSetCurrentContext(self.ig_ctx);
// Setup our frame. We render twice because some ImGui behaviors
// take multiple renders to process. I don't know how to make this
// more efficient.
for (0..2) |_| {
cimgui.ImGui_ImplOpenGL3_NewFrame();
self.newFrame() catch |err| {
log.err("failed to setup frame: {}", .{err});
return 0;
};
cimgui.c.igNewFrame();
// Build our UI
if (self.render_callback) |cb| cb(self.render_userdata);
// Render
cimgui.c.igRender();
}
// OpenGL final render
gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
return 1;
}
fn gtkMouseMotion(
_: *gtk.EventControllerMotion,
x: f64,
y: f64,
self: *ImguiWidget,
) callconv(.c) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
cimgui.c.ImGuiIO_AddMousePosEvent(
io,
@floatCast(x * scale_factor),
@floatCast(y * scale_factor),
);
self.queueRender();
}
fn gtkMouseDown(
gesture: *gtk.GestureClick,
_: c_int,
_: f64,
_: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
if (translateMouseButton(gdk_button)) |button| {
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
}
}
fn gtkMouseUp(
gesture: *gtk.GestureClick,
_: c_int,
_: f64,
_: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
if (translateMouseButton(gdk_button)) |button| {
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
}
}
fn gtkMouseScroll(
_: *gtk.EventControllerScroll,
x: f64,
y: f64,
self: *ImguiWidget,
) callconv(.c) c_int {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddMouseWheelEvent(
io,
@floatCast(x),
@floatCast(-y),
);
return @intFromBool(true);
}
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
}
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
}
fn gtkInputCommit(
_: *gtk.IMMulticontext,
bytes: [*:0]u8,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
}
fn gtkKeyPressed(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
) callconv(.c) c_int {
return @intFromBool(self.keyEvent(
.press,
ec_key,
keyval,
keycode,
gtk_mods,
));
}
fn gtkKeyReleased(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
) callconv(.c) void {
_ = self.keyEvent(
.release,
ec_key,
keyval,
keycode,
gtk_mods,
);
}
fn keyEvent(
self: *ImguiWidget,
action: input.Action,
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
) bool {
_ = keycode;
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const mods = key.translateMods(gtk_mods);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
// If our keyval has a key, then we send that key event
if (key.keyFromKeyval(keyval)) |inputkey| {
if (inputkey.imguiKey()) |imgui_key| {
cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
}
}
// Try to process the event as text
if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
_ = self.im_context.filterKeypress(event);
}
return true;
}

View File

@@ -1,165 +0,0 @@
//! Structure for managing GUI progress bar for a surface.
const ProgressBar = @This();
const std = @import("std");
const glib = @import("glib");
const gtk = @import("gtk");
const Surface = @import("./Surface.zig");
const terminal = @import("../../terminal/main.zig");
const log = std.log.scoped(.gtk_progress_bar);
/// The surface that we belong to.
surface: *Surface,
/// Widget for showing progress bar.
progress_bar: ?*gtk.ProgressBar = null,
/// Timer used to remove the progress bar if we have not received an update from
/// the TUI in a while.
progress_bar_timer: ?c_uint = null,
pub fn init(surface: *Surface) ProgressBar {
return .{
.surface = surface,
};
}
pub fn deinit(self: *ProgressBar) void {
self.stopProgressBarTimer();
}
/// Show (or update if it already exists) a GUI progress bar.
pub fn handleProgressReport(self: *ProgressBar, value: terminal.osc.Command.ProgressReport) error{}!bool {
// Remove the progress bar.
if (value.state == .remove) {
self.stopProgressBarTimer();
self.removeProgressBar();
return true;
}
const progress_bar = self.addProgressBar();
self.startProgressBarTimer();
switch (value.state) {
// already handled above
.remove => unreachable,
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
// Remove the `error` CSS class so that the progress bar shows as normal.
.set => {
progress_bar.as(gtk.Widget).removeCssClass("error");
if (value.progress) |progress| {
progress_bar.setFraction(computeFraction(progress));
} else {
progress_bar.pulse();
}
},
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
// Set the `error` CSS class so that the progress bar shows as an error color.
.@"error" => {
progress_bar.as(gtk.Widget).addCssClass("error");
if (value.progress) |progress| {
progress_bar.setFraction(computeFraction(progress));
} else {
progress_bar.pulse();
}
},
// The state of progress is unknown, so pulse the progress bar to
// indicate that things are still happening.
.indeterminate => {
progress_bar.pulse();
},
// If a progress value was provided, set the progress bar to that value.
// Don't pulse the progress bar as that would indicate that things were
// happening. Otherwise this is mainly used to keep the progress bar on
// screen instead of timing out.
.pause => {
if (value.progress) |progress| {
progress_bar.setFraction(computeFraction(progress));
}
},
}
return true;
}
/// Compute a fraction [0.0, 1.0] from the supplied progress, which is clamped
/// to [0, 100].
fn computeFraction(progress: u8) f64 {
return @as(f64, @floatFromInt(std.math.clamp(progress, 0, 100))) / 100.0;
}
test "computeFraction" {
try std.testing.expectEqual(1.0, computeFraction(100));
try std.testing.expectEqual(1.0, computeFraction(255));
try std.testing.expectEqual(0.0, computeFraction(0));
try std.testing.expectEqual(0.5, computeFraction(50));
}
/// Add a progress bar to our overlay.
fn addProgressBar(self: *ProgressBar) *gtk.ProgressBar {
if (self.progress_bar) |progress_bar| return progress_bar;
const progress_bar = gtk.ProgressBar.new();
self.progress_bar = progress_bar;
const progress_bar_widget = progress_bar.as(gtk.Widget);
progress_bar_widget.setHalign(.fill);
progress_bar_widget.setValign(.start);
progress_bar_widget.addCssClass("osd");
self.surface.overlay.addOverlay(progress_bar_widget);
return progress_bar;
}
/// Remove the progress bar from our overlay.
fn removeProgressBar(self: *ProgressBar) void {
if (self.progress_bar) |progress_bar| {
const progress_bar_widget = progress_bar.as(gtk.Widget);
self.surface.overlay.removeOverlay(progress_bar_widget);
self.progress_bar = null;
}
}
/// Start a timer that will remove the progress bar if the TUI forgets to remove
/// it.
fn startProgressBarTimer(self: *ProgressBar) void {
const progress_bar_timeout_seconds = 15;
// Remove an old timer that hasn't fired yet.
self.stopProgressBarTimer();
self.progress_bar_timer = glib.timeoutAdd(
progress_bar_timeout_seconds * std.time.ms_per_s,
handleProgressBarTimeout,
self,
);
}
/// Stop any existing timer for removing the progress bar.
fn stopProgressBarTimer(self: *ProgressBar) void {
if (self.progress_bar_timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove progress bar timer", .{});
}
self.progress_bar_timer = null;
}
}
/// The progress bar hasn't been updated by the TUI recently, remove it.
fn handleProgressBarTimeout(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ProgressBar = @ptrCast(@alignCast(ud.?));
self.progress_bar_timer = null;
self.removeProgressBar();
return @intFromBool(glib.SOURCE_REMOVE);
}

View File

@@ -1,206 +0,0 @@
const ResizeOverlay = @This();
const std = @import("std");
const glib = @import("glib");
const gtk = @import("gtk");
const configpkg = @import("../../config.zig");
const Surface = @import("Surface.zig");
const log = std.log.scoped(.gtk);
/// local copy of configuration data
const DerivedConfig = struct {
resize_overlay: configpkg.Config.ResizeOverlay,
resize_overlay_position: configpkg.Config.ResizeOverlayPosition,
resize_overlay_duration: configpkg.Config.Duration,
pub fn init(config: *const configpkg.Config) DerivedConfig {
return .{
.resize_overlay = config.@"resize-overlay",
.resize_overlay_position = config.@"resize-overlay-position",
.resize_overlay_duration = config.@"resize-overlay-duration",
};
}
};
/// the surface that we are attached to
surface: *Surface,
/// a copy of the configuration that we need to operate
config: DerivedConfig,
/// If non-null this is the widget on the overlay that shows the size of the
/// surface when it is resized.
label: ?*gtk.Label = null,
/// If non-null this is a timer for dismissing the resize overlay.
timer: ?c_uint = null,
/// If non-null this is a timer for dismissing the resize overlay.
idler: ?c_uint = null,
/// If true, the next resize event will be the first one.
first: bool = true,
/// Initialize the ResizeOverlay. This doesn't do anything more than save a
/// pointer to the surface that we are a part of as all of the widget creation
/// is done later.
pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
self.* = .{
.surface = surface,
.config = .init(config),
};
}
pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
self.config = .init(config);
}
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
/// may not have fired yet.
pub fn deinit(self: *ResizeOverlay) void {
if (self.idler) |idler| {
if (glib.Source.remove(idler) == 0) {
log.warn("unable to remove resize overlay idler", .{});
}
self.idler = null;
}
if (self.timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove resize overlay timer", .{});
}
self.timer = null;
}
}
/// If we're configured to do so, update the text in the resize overlay widget
/// and make it visible. Schedule a timer to hide the widget after the delay
/// expires.
///
/// If we're not configured to show the overlay, do nothing.
pub fn maybeShow(self: *ResizeOverlay) void {
switch (self.config.resize_overlay) {
.never => return,
.always => {},
.@"after-first" => if (self.first) {
self.first = false;
return;
},
}
self.first = false;
// When updating a widget, wait until GTK is "idle", i.e. not in the middle
// of doing any other updates. Since we are called in the middle of resizing
// GTK is doing a lot of work rearranging all of the widgets. Not doing this
// results in a lot of warnings from GTK and _horrible_ flickering of the
// resize overlay.
if (self.idler != null) return;
self.idler = glib.idleAdd(gtkUpdate, self);
}
/// Actually update the overlay widget. This should only be called from a GTK
/// idle handler.
fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
// No matter what our idler is complete with this callback
self.idler = null;
const grid_size = self.surface.core_surface.size.grid();
var buf: [32]u8 = undefined;
const text = std.fmt.bufPrintZ(
&buf,
"{d} x {d}",
.{
grid_size.columns,
grid_size.rows,
},
) catch |err| {
log.err("unable to format text: {}", .{err});
return 0;
};
if (self.label) |label| {
// The resize overlay widget already exists, just update it.
label.setText(text.ptr);
setPosition(label, &self.config);
show(label);
} else {
// Create the resize overlay widget.
const label = gtk.Label.new(text.ptr);
label.setJustify(gtk.Justification.center);
label.setSelectable(0);
setPosition(label, &self.config);
const widget = label.as(gtk.Widget);
widget.addCssClass("view");
widget.addCssClass("size-overlay");
widget.setFocusable(0);
widget.setCanTarget(0);
const overlay: *gtk.Overlay = @ptrCast(@alignCast(self.surface.overlay));
overlay.addOverlay(widget);
self.label = label;
}
if (self.timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove size overlay timer", .{});
}
}
self.timer = glib.timeoutAdd(
self.surface.app.config.@"resize-overlay-duration".asMilliseconds(),
gtkTimerExpired,
self,
);
return 0;
}
// This should only be called from a GTK idle handler or timer.
fn show(label: *gtk.Label) void {
const widget = label.as(gtk.Widget);
widget.removeCssClass("hidden");
}
// This should only be called from a GTK idle handler or timer.
fn hide(label: *gtk.Label) void {
const widget = label.as(gtk.Widget);
widget.addCssClass("hidden");
}
/// Update the position of the resize overlay widget. It might seem excessive to
/// do this often, but it should make hot config reloading of the position work.
/// This should only be called from a GTK idle handler.
fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
const widget = label.as(gtk.Widget);
widget.setHalign(
switch (config.resize_overlay_position) {
.center, .@"top-center", .@"bottom-center" => gtk.Align.center,
.@"top-left", .@"bottom-left" => gtk.Align.start,
.@"top-right", .@"bottom-right" => gtk.Align.end,
},
);
widget.setValign(
switch (config.resize_overlay_position) {
.center => gtk.Align.center,
.@"top-left", .@"top-center", .@"top-right" => gtk.Align.start,
.@"bottom-left", .@"bottom-center", .@"bottom-right" => gtk.Align.end,
},
);
}
/// If this fires, it means that the delay period has expired and the resize
/// overlay widget should be hidden.
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
self.timer = null;
if (self.label) |label| hide(label);
return 0;
}

View File

@@ -1,441 +0,0 @@
/// Split represents a surface split where two surfaces are shown side-by-side
/// within the same window either vertically or horizontally.
const Split = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gobject = @import("gobject");
const gtk = @import("gtk");
const apprt = @import("../../apprt.zig");
const font = @import("../../font/main.zig");
const CoreSurface = @import("../../Surface.zig");
const Surface = @import("Surface.zig");
const Tab = @import("Tab.zig");
const log = std.log.scoped(.gtk);
/// The split orientation.
pub const Orientation = enum {
horizontal,
vertical,
pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
return switch (direction) {
.right, .left => .horizontal,
.down, .up => .vertical,
};
}
pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
return switch (direction) {
.up, .down => .vertical,
.left, .right => .horizontal,
};
}
};
/// Our actual GtkPaned widget
paned: *gtk.Paned,
/// The container for this split panel.
container: Surface.Container,
/// The orientation of this split panel.
orientation: Orientation,
/// The elements of this split panel.
top_left: Surface.Container.Elem,
bottom_right: Surface.Container.Elem,
/// Create a new split panel with the given sibling surface in the given
/// direction. The direction is where the new surface will be initialized.
///
/// The sibling surface can be in a split already or it can be within a
/// tab. This properly handles updating the surface container so that
/// it represents the new split.
pub fn create(
alloc: Allocator,
sibling: *Surface,
direction: apprt.action.SplitDirection,
) !*Split {
var split = try alloc.create(Split);
errdefer alloc.destroy(split);
try split.init(sibling, direction);
return split;
}
pub fn init(
self: *Split,
sibling: *Surface,
direction: apprt.action.SplitDirection,
) !void {
// If our sibling is too small to be split in half then we don't
// allow the split to happen. This avoids a situation where the
// split becomes too small.
//
// This is kind of a hack. Ideally we'd use gtk_widget_set_size_request
// properly along the path to ensure minimum sizes. I don't know if
// GTK even respects that all but any way GTK does this for us seems
// better than this.
{
// This is the min size of the sibling split. This means the
// smallest split is half of this.
const multiplier = 4;
const size = &sibling.core_surface.size;
const small = switch (direction) {
.right, .left => size.screen.width < size.cell.width * multiplier,
.down, .up => size.screen.height < size.cell.height * multiplier,
};
if (small) return error.SplitTooSmall;
}
// Create the new child surface for the other direction.
const alloc = sibling.app.core_app.alloc;
var surface = try Surface.create(alloc, sibling.app, .{
.parent = &sibling.core_surface,
});
errdefer surface.destroy(alloc);
sibling.dimSurface();
sibling.setSplitZoom(false);
// Create the actual GTKPaned, attach the proper children.
const orientation: gtk.Orientation = switch (direction) {
.right, .left => .horizontal,
.down, .up => .vertical,
};
const paned = gtk.Paned.new(orientation);
errdefer paned.unref();
// Keep a long-lived reference, which we unref in destroy.
paned.ref();
// Update all of our containers to point to the right place.
// The split has to point to where the sibling pointed to because
// we're inheriting its parent. The sibling points to its location
// in the split, and the surface points to the other location.
const container = sibling.container;
const tl: *Surface, const br: *Surface = switch (direction) {
.right, .down => right_down: {
sibling.container = .{ .split_tl = &self.top_left };
surface.container = .{ .split_br = &self.bottom_right };
break :right_down .{ sibling, surface };
},
.left, .up => left_up: {
sibling.container = .{ .split_br = &self.bottom_right };
surface.container = .{ .split_tl = &self.top_left };
break :left_up .{ surface, sibling };
},
};
self.* = .{
.paned = paned,
.container = container,
.top_left = .{ .surface = tl },
.bottom_right = .{ .surface = br },
.orientation = .fromDirection(direction),
};
// Replace the previous containers element with our split. This allows a
// non-split to become a split, a split to become a nested split, etc.
container.replace(.{ .split = self });
// Update our children so that our GL area is properly added to the paned.
self.updateChildren();
// The new surface should always grab focus
surface.grabFocus();
}
pub fn destroy(self: *Split, alloc: Allocator) void {
self.top_left.deinit(alloc);
self.bottom_right.deinit(alloc);
// Clean up our GTK reference. This will trigger all the destroy callbacks
// that are necessary for the surfaces to clean up.
self.paned.unref();
alloc.destroy(self);
}
/// Remove the top left child.
pub fn removeTopLeft(self: *Split) void {
self.removeChild(self.top_left, self.bottom_right);
}
/// Remove the top left child.
pub fn removeBottomRight(self: *Split) void {
self.removeChild(self.bottom_right, self.top_left);
}
fn removeChild(
self: *Split,
remove: Surface.Container.Elem,
keep: Surface.Container.Elem,
) void {
const window = self.container.window() orelse return;
const alloc = window.app.core_app.alloc;
// Remove our children since we are going to no longer be a split anyways.
// This prevents widgets with multiple parents.
self.removeChildren();
// Our container must become whatever our top left is
self.container.replace(keep);
// Grab focus of the left-over side
keep.grabFocus();
// When a child is removed we are no longer a split, so destroy ourself
remove.deinit(alloc);
alloc.destroy(self);
}
/// Move the divider in the given direction by the given amount.
pub fn moveDivider(
self: *Split,
direction: apprt.action.ResizeSplit.Direction,
amount: u16,
) void {
const min_pos = 10;
const pos = self.paned.getPosition();
const new = switch (direction) {
.up, .left => @max(pos - amount, min_pos),
.down, .right => new_pos: {
const max_pos: u16 = @as(u16, @intFromFloat(self.maxPosition())) - min_pos;
break :new_pos @min(pos + amount, max_pos);
},
};
self.paned.setPosition(new);
}
/// Equalize the splits in this split panel. Each split is equalized based on
/// its weight, i.e. the number of Surfaces it contains.
///
/// It works recursively by equalizing the children of each split.
///
/// It returns this split's weight.
pub fn equalize(self: *Split) f64 {
// Calculate weights of top_left/bottom_right
const top_left_weight = self.top_left.equalize();
const bottom_right_weight = self.bottom_right.equalize();
const weight = top_left_weight + bottom_right_weight;
// Ratio of top_left weight to overall weight, which gives the split ratio
const ratio = top_left_weight / weight;
// Convert split ratio into new position for divider
self.paned.setPosition(@intFromFloat(self.maxPosition() * ratio));
return weight;
}
// maxPosition returns the maximum position of the GtkPaned, which is the
// "max-position" attribute.
fn maxPosition(self: *Split) f64 {
var value: gobject.Value = std.mem.zeroes(gobject.Value);
defer value.unset();
_ = value.init(gobject.ext.types.int);
self.paned.as(gobject.Object).getProperty(
"max-position",
&value,
);
return @floatFromInt(value.getInt());
}
// This replaces the element at the given pointer with a new element.
// The ptr must be either top_left or bottom_right (asserted in debug).
// The memory of the old element must be freed or otherwise handled by
// the caller.
pub fn replace(
self: *Split,
ptr: *Surface.Container.Elem,
new: Surface.Container.Elem,
) void {
// We can write our element directly. There's nothing special.
assert(&self.top_left == ptr or &self.bottom_right == ptr);
ptr.* = new;
// Update our paned children. This will reset the divider
// position but we want to keep it in place so save and restore it.
const pos = self.paned.getPosition();
defer self.paned.setPosition(pos);
self.updateChildren();
}
// grabFocus grabs the focus of the top-left element.
pub fn grabFocus(self: *Split) void {
self.top_left.grabFocus();
}
/// Update the paned children to represent the current state.
/// This should be called anytime the top/left or bottom/right
/// element is changed.
pub fn updateChildren(self: *const Split) void {
// We have to set both to null. If we overwrite the pane with
// the same value, then GTK bugs out (the GL area unrealizes
// and never rerealizes).
self.removeChildren();
// Set our current children
self.paned.setStartChild(self.top_left.widget());
self.paned.setEndChild(self.bottom_right.widget());
}
/// A mapping of direction to the element (if any) in that direction.
pub const DirectionMap = std.EnumMap(
apprt.action.GotoSplit,
?*Surface,
);
pub const Side = enum { top_left, bottom_right };
/// Returns the map that can be used to determine elements in various
/// directions (primarily for gotoSplit).
pub fn directionMap(self: *const Split, from: Side) DirectionMap {
var result = DirectionMap.initFull(null);
if (self.directionPrevious(from)) |prev| {
result.put(.previous, prev.surface);
if (!prev.wrapped) {
result.put(.up, prev.surface);
}
}
if (self.directionNext(from)) |next| {
result.put(.next, next.surface);
if (!next.wrapped) {
result.put(.down, next.surface);
}
}
if (self.directionLeft(from)) |left| {
result.put(.left, left);
}
if (self.directionRight(from)) |right| {
result.put(.right, right);
}
return result;
}
fn directionLeft(self: *const Split, from: Side) ?*Surface {
switch (from) {
.bottom_right => {
switch (self.orientation) {
.horizontal => return self.top_left.deepestSurface(.bottom_right),
.vertical => return directionLeft(
self.container.split() orelse return null,
.bottom_right,
),
}
},
.top_left => return directionLeft(
self.container.split() orelse return null,
.bottom_right,
),
}
}
fn directionRight(self: *const Split, from: Side) ?*Surface {
switch (from) {
.top_left => {
switch (self.orientation) {
.horizontal => return self.bottom_right.deepestSurface(.top_left),
.vertical => return directionRight(
self.container.split() orelse return null,
.top_left,
),
}
},
.bottom_right => return directionRight(
self.container.split() orelse return null,
.top_left,
),
}
}
fn directionPrevious(self: *const Split, from: Side) ?struct {
surface: *Surface,
wrapped: bool,
} {
switch (from) {
// From the bottom right, our previous is the deepest surface
// in the top-left of our own split.
.bottom_right => return .{
.surface = self.top_left.deepestSurface(.bottom_right) orelse return null,
.wrapped = false,
},
// From the top left its more complicated. It is the de
.top_left => {
// If we have no parent split then there can be no unwrapped prev.
// We can still have a wrapped previous.
const parent = self.container.split() orelse return .{
.surface = self.bottom_right.deepestSurface(.bottom_right) orelse return null,
.wrapped = true,
};
// The previous value is the previous of the side that we are.
const side = self.container.splitSide() orelse return null;
return switch (side) {
.top_left => parent.directionPrevious(.top_left),
.bottom_right => parent.directionPrevious(.bottom_right),
};
},
}
}
fn directionNext(self: *const Split, from: Side) ?struct {
surface: *Surface,
wrapped: bool,
} {
switch (from) {
// From the top left, our next is the earliest surface in the
// top-left direction of the bottom-right side of our split. Fun!
.top_left => return .{
.surface = self.bottom_right.deepestSurface(.top_left) orelse return null,
.wrapped = false,
},
// From the bottom right is more compliated. It is the deepest
// (last) surface in the
.bottom_right => {
// If we have no parent split then there can be no next.
const parent = self.container.split() orelse return .{
.surface = self.top_left.deepestSurface(.top_left) orelse return null,
.wrapped = true,
};
// The previous value is the previous of the side that we are.
const side = self.container.splitSide() orelse return null;
return switch (side) {
.top_left => parent.directionNext(.top_left),
.bottom_right => parent.directionNext(.bottom_right),
};
},
}
}
pub fn detachTopLeft(self: *const Split) void {
self.paned.setStartChild(null);
}
pub fn detachBottomRight(self: *const Split) void {
self.paned.setEndChild(null);
}
fn removeChildren(self: *const Split) void {
self.detachTopLeft();
self.detachBottomRight();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,171 +0,0 @@
//! The state associated with a single tab in the window.
//!
//! A tab can contain one or more terminals due to splits.
const Tab = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gobject = @import("gobject");
const gtk = @import("gtk");
const font = @import("../../font/main.zig");
const input = @import("../../input.zig");
const CoreSurface = @import("../../Surface.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
const CloseDialog = @import("CloseDialog.zig");
const log = std.log.scoped(.gtk);
pub const GHOSTTY_TAB = "ghostty_tab";
/// The window that owns this tab.
window: *Window,
/// The tab label. The tab label is the text that appears on the tab.
label_text: *gtk.Label,
/// We'll put our children into this box instead of packing them
/// directly, so that we can send the box into `c.g_signal_connect_data`
/// for the close button
box: *gtk.Box,
/// The element of this tab so that we can handle splits and so on.
elem: Surface.Container.Elem,
// We'll update this every time a Surface gains focus, so that we have it
// when we switch to another Tab. Then when we switch back to this tab, we
// can easily re-focus that terminal.
focus_child: ?*Surface,
pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab {
var tab = try alloc.create(Tab);
errdefer alloc.destroy(tab);
try tab.init(window, parent_);
return tab;
}
/// Initialize the tab, create a surface, and add it to the window. "self" needs
/// to be a stable pointer, since it is used for GTK events.
pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
self.* = .{
.window = window,
.label_text = undefined,
.box = undefined,
.elem = undefined,
.focus_child = null,
};
// Create a Box in which we'll later keep either Surface or Split. Using a
// box makes it easier to maintain the tab contents because we never need to
// change the root widget of the notebook page (tab).
const box = gtk.Box.new(.vertical, 0);
errdefer box.unref();
const box_widget = box.as(gtk.Widget);
box_widget.setHexpand(1);
box_widget.setVexpand(1);
self.box = box;
// Create the initial surface since all tabs start as a single non-split
var surface = try Surface.create(window.app.core_app.alloc, window.app, .{
.parent = parent_,
});
errdefer surface.unref();
surface.container = .{ .tab_ = self };
self.elem = .{ .surface = surface };
// Add Surface to the Tab
self.box.append(surface.primaryWidget());
// Set the userdata of the box to point to this tab.
self.box.as(gobject.Object).setData(GHOSTTY_TAB, self);
window.notebook.addTab(self, "Ghostty");
// Attach all events
_ = gtk.Widget.signals.destroy.connect(
self.box,
*Tab,
gtkDestroy,
self,
.{},
);
// We need to grab focus after Surface and Tab is added to the window. When
// creating a Tab we want to always focus on the widget.
surface.grabFocus();
}
/// Deinits tab by deiniting child elem.
pub fn deinit(self: *Tab, alloc: Allocator) void {
self.elem.deinit(alloc);
}
/// Deinit and deallocate the tab.
pub fn destroy(self: *Tab, alloc: Allocator) void {
self.deinit(alloc);
alloc.destroy(self);
}
// TODO: move this
/// Replace the surface element that this tab is showing.
pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
// Remove our previous widget
self.box.remove(self.elem.widget());
// Add our new one
self.box.append(elem.widget());
self.elem = elem;
}
pub fn setTitleText(self: *Tab, title: [:0]const u8) void {
self.window.notebook.setTabTitle(self, title);
}
pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
self.window.notebook.setTabTooltip(self, tooltip);
}
/// Remove this tab from the window.
pub fn remove(self: *Tab) void {
self.window.closeTab(self);
}
/// Helper function to check if any surface in the split hierarchy needs close confirmation
fn needsConfirm(elem: Surface.Container.Elem) bool {
return switch (elem) {
.surface => |s| s.core_surface.needsConfirmQuit(),
.split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
};
}
/// Close the tab, asking for confirmation if any surface requests it.
pub fn closeWithConfirmation(tab: *Tab) void {
switch (tab.elem) {
.surface => |s| s.closeWithConfirmation(
s.core_surface.needsConfirmQuit(),
.{ .tab = tab },
),
.split => |s| {
if (!needsConfirm(s.top_left) and !needsConfirm(s.bottom_right)) {
tab.remove();
return;
}
CloseDialog.show(.{ .tab = tab }) catch |err| {
log.err("failed to open close dialog={}", .{err});
};
},
}
}
fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void {
log.debug("tab box destroy", .{});
const alloc = self.window.app.core_app.alloc;
// When our box is destroyed, we want to destroy our tab, too.
self.destroy(alloc);
}

View File

@@ -1,284 +0,0 @@
/// An abstraction over the Adwaita tab view to manage all the terminal tabs in
/// a window.
const TabView = @This();
const std = @import("std");
const gtk = @import("gtk");
const adw = @import("adw");
const gobject = @import("gobject");
const glib = @import("glib");
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk);
/// our window
window: *Window,
/// the tab view
tab_view: *adw.TabView,
/// Set to true so that the adw close-page handler knows we're forcing
/// and to allow a close to happen with no confirm. This is a bit of a hack
/// because we currently use GTK alerts to confirm tab close and they
/// don't carry with them the ADW state that we are confirming or not.
/// Long term we should move to ADW alerts so we can know if we are
/// confirming or not.
forcing_close: bool = false,
pub fn init(self: *TabView, window: *Window) void {
self.* = .{
.window = window,
.tab_view = adw.TabView.new(),
};
self.tab_view.as(gtk.Widget).addCssClass("notebook");
if (adw_version.atLeast(1, 2, 0)) {
// Adwaita enables all of the shortcuts by default.
// We want to manage keybindings ourselves.
self.tab_view.removeShortcuts(.{
.alt_digits = true,
.alt_zero = true,
.control_end = true,
.control_home = true,
.control_page_down = true,
.control_page_up = true,
.control_shift_end = true,
.control_shift_home = true,
.control_shift_page_down = true,
.control_shift_page_up = true,
.control_shift_tab = true,
.control_tab = true,
});
}
_ = adw.TabView.signals.page_attached.connect(
self.tab_view,
*TabView,
adwPageAttached,
self,
.{},
);
_ = adw.TabView.signals.close_page.connect(
self.tab_view,
*TabView,
adwClosePage,
self,
.{},
);
_ = adw.TabView.signals.create_window.connect(
self.tab_view,
*TabView,
adwTabViewCreateWindow,
self,
.{},
);
_ = gobject.Object.signals.notify.connect(
self.tab_view,
*TabView,
adwSelectPage,
self,
.{
.detail = "selected-page",
},
);
}
pub fn asWidget(self: *TabView) *gtk.Widget {
return self.tab_view.as(gtk.Widget);
}
pub fn nPages(self: *TabView) c_int {
return self.tab_view.getNPages();
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
fn currentPage(self: *TabView) ?c_int {
const page = self.tab_view.getSelectedPage() orelse return null;
return self.tab_view.getPagePosition(page);
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *TabView) ?*Tab {
const page = self.tab_view.getSelectedPage() orelse return null;
const child = page.getChild().as(gobject.Object);
return @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return null));
}
pub fn gotoNthTab(self: *TabView, position: c_int) bool {
const page_to_select = self.tab_view.getNthPage(position);
self.tab_view.setSelectedPage(page_to_select);
return true;
}
pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
return self.tab_view.getPage(tab.box.as(gtk.Widget));
}
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
}
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
// The next index is the previous or we wrap around.
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
const max = self.nPages();
break :next_idx max -| 1;
};
// Do nothing if we have one tab
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn gotoNextTab(self: *TabView, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
const max = self.nPages() -| 1;
const next_idx = if (page_idx < max) page_idx + 1 else 0;
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
const page_idx = self.getTabPosition(tab) orelse return;
const max = self.nPages() -| 1;
var new_position: c_int = page_idx + position;
if (new_position < 0) {
new_position = max + new_position + 1;
} else if (new_position > max) {
new_position = new_position - max - 1;
}
if (new_position == page_idx) return;
self.reorderPage(tab, new_position);
}
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
_ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
}
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const page = self.getTabPage(tab) orelse return;
page.setTitle(title.ptr);
}
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
const page = self.getTabPage(tab) orelse return;
page.setTooltip(tooltip.ptr);
}
fn newTabInsertPosition(self: *TabView, tab: *Tab) c_int {
const numPages = self.nPages();
return switch (tab.window.app.config.@"window-new-tab-position") {
.current => if (self.currentPage()) |page| page + 1 else numPages,
.end => numPages,
};
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const position = self.newTabInsertPosition(tab);
const page = self.tab_view.insert(tab.box.as(gtk.Widget), position);
self.setTabTitle(tab, title);
self.tab_view.setSelectedPage(page);
}
pub fn closeTab(self: *TabView, tab: *Tab) void {
// closeTab always expects to close unconditionally so we mark this
// as true so that the close_page call below doesn't request
// confirmation.
self.forcing_close = true;
const n = self.nPages();
defer {
// self becomes invalid if we close the last page because we close
// the whole window
if (n > 1) self.forcing_close = false;
}
if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
// If we have no more tabs we close the window
if (self.nPages() == 0) {
// libadw versions < 1.5.1 leak the final page view
// which causes our surface to not properly cleanup. We
// unref to force the cleanup. This will trigger a critical
// warning from GTK, but I don't know any other workaround.
if (!adw_version.atLeast(1, 5, 1)) {
tab.box.unref();
}
self.window.close();
}
}
pub fn createWindow(window: *Window) !*Window {
const new_window = try Window.create(window.app.core_app.alloc, window.app);
new_window.present();
return new_window;
}
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void {
const child = page.getChild().as(gobject.Object);
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
tab.window = self.window;
self.window.focusCurrentTab();
}
fn adwClosePage(
_: *adw.TabView,
page: *adw.TabPage,
self: *TabView,
) callconv(.c) c_int {
const child = page.getChild().as(gobject.Object);
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
if (!self.forcing_close) {
// We cannot trigger a close directly in here as the page will stay
// alive until this handler returns, breaking the assumption where
// no pages means they are all destroyed.
//
// Schedule the close request to happen in the next event cycle.
_ = glib.idleAddOnce(glibIdleOnceCloseTab, tab);
}
return 1;
}
fn adwTabViewCreateWindow(
_: *adw.TabView,
self: *TabView,
) callconv(.c) ?*adw.TabView {
const window = createWindow(self.window) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
return window.notebook.tab_view;
}
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void {
const page = self.tab_view.getSelectedPage() orelse return;
// If the tab was previously marked as needing attention
// (e.g. due to a bell character), we now unmark that
page.setNeedsAttention(@intFromBool(false));
const title = page.getTitle();
self.window.setTitle(std.mem.span(title));
}
fn glibIdleOnceCloseTab(data: ?*anyopaque) callconv(.c) void {
const tab: *Tab = @ptrCast(@alignCast(data orelse return));
tab.closeWithConfirmation();
}

View File

@@ -1,115 +0,0 @@
//! Represents the URL hover widgets that show the hovered URL.
//!
//! To explain a bit how this all works since its split across a few places:
//! We create a left/right pair of labels. The left label is shown by default,
//! and the right label is hidden. When the mouse enters the left label, we
//! show the right label. When the mouse leaves the left label, we hide the
//! right label.
//!
//! The hover and styling is done with a combination of GTK event controllers
//! and CSS in style.css.
const URLWidget = @This();
const gtk = @import("gtk");
/// The label that appears on the bottom left.
left: *gtk.Label,
/// The label that appears on the bottom right.
right: *gtk.Label,
pub fn init(
/// The overlay that we will attach our labels to.
overlay: *gtk.Overlay,
/// The URL to display.
str: [:0]const u8,
) URLWidget {
// Create the left
const left = left: {
const left = gtk.Label.new(str.ptr);
left.setEllipsize(.middle);
const widget = left.as(gtk.Widget);
widget.addCssClass("view");
widget.addCssClass("url-overlay");
widget.addCssClass("left");
widget.setHalign(.start);
widget.setValign(.end);
break :left left;
};
// Create the right
const right = right: {
const right = gtk.Label.new(str.ptr);
right.setEllipsize(.middle);
const widget = right.as(gtk.Widget);
widget.addCssClass("hidden");
widget.addCssClass("view");
widget.addCssClass("url-overlay");
widget.addCssClass("right");
widget.setHalign(.end);
widget.setValign(.end);
break :right right;
};
// Setup our mouse hover event controller for the left label.
const ec_motion = gtk.EventControllerMotion.new();
errdefer ec_motion.unref();
left.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
_ = gtk.EventControllerMotion.signals.enter.connect(
ec_motion,
*gtk.Label,
gtkLeftEnter,
right,
.{},
);
_ = gtk.EventControllerMotion.signals.leave.connect(
ec_motion,
*gtk.Label,
gtkLeftLeave,
right,
.{},
);
// Show it
overlay.addOverlay(left.as(gtk.Widget));
overlay.addOverlay(right.as(gtk.Widget));
return .{
.left = left,
.right = right,
};
}
/// Remove our labels from the overlay.
pub fn deinit(self: *URLWidget, overlay: *gtk.Overlay) void {
overlay.removeOverlay(self.left.as(gtk.Widget));
overlay.removeOverlay(self.right.as(gtk.Widget));
}
/// Change the URL that is displayed.
pub fn setText(self: *const URLWidget, str: [:0]const u8) void {
self.left.setText(str.ptr);
self.right.setText(str.ptr);
}
/// Callback for when the mouse enters the left label. That means that we should
/// show the right label. CSS will handle hiding the left label.
fn gtkLeftEnter(
_: *gtk.EventControllerMotion,
_: f64,
_: f64,
right: *gtk.Label,
) callconv(.c) void {
right.as(gtk.Widget).removeCssClass("hidden");
}
/// Callback for when the mouse leaves the left label. That means that we should
/// hide the right label. CSS will handle showing the left label.
fn gtkLeftLeave(
_: *gtk.EventControllerMotion,
right: *gtk.Label,
) callconv(.c) void {
right.as(gtk.Widget).addCssClass("hidden");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,122 +0,0 @@
const std = @import("std");
// Until the gobject bindings are built at the same time we are building
// Ghostty, we need to import `adwaita.h` directly to ensure that the version
// macros match the version of `libadwaita` that we are building/linking
// against.
const c = @cImport({
@cInclude("adwaita.h");
});
const adw = @import("adw");
const log = std.log.scoped(.gtk);
pub const comptime_version: std.SemanticVersion = .{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
.patch = c.ADW_MICRO_VERSION,
};
pub fn getRuntimeVersion() std.SemanticVersion {
return .{
.major = adw.getMajorVersion(),
.minor = adw.getMinorVersion(),
.patch = adw.getMicroVersion(),
};
}
pub fn logVersion() void {
log.info("libadwaita version build={} runtime={}", .{
comptime_version,
getRuntimeVersion(),
});
}
/// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to not build with
/// libadwaita.
///
/// This can be run in both a comptime and runtime context. If it is run in a
/// comptime context, it will only check the version in the headers. If it is
/// run in a runtime context, it will check the actual version of the library we
/// are linked against. So generally you probably want to do both checks!
///
/// This is inlined so that the comptime checks will disable the runtime checks
/// if the comptime checks fail.
pub inline fn atLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
// If our header has lower versions than the given version, we can return
// false immediately. This prevents us from compiling against unknown
// symbols and makes runtime checks very slightly faster.
if (comptime comptime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) == .lt) return false;
// If we're in comptime then we can't check the runtime version.
if (@inComptime()) return true;
return runtimeAtLeast(major, minor, micro);
}
/// Verifies that the libadwaita version at runtime is at least the given version.
///
/// This function should be used in cases where the only the runtime behavior
/// is affected by the version check. For checks which would affect code
/// generation, use `atLeast`.
pub inline fn runtimeAtLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
// because the function gets the actual runtime version.
const runtime_version = getRuntimeVersion();
return runtime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) != .lt;
}
test "versionAtLeast" {
const testing = std.testing;
const funs = &.{ atLeast, runtimeAtLeast };
inline for (funs) |fun| {
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
}
}
// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
pub inline fn supportsDialogs() bool {
return atLeast(1, 5, 0);
}
pub inline fn supportsTabOverview() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsSwitchRow() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsToolbarView() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsBanner() bool {
return atLeast(1, 3, 0);
}

View File

@@ -1,160 +0,0 @@
const std = @import("std");
pub const c = @cImport({
@cInclude("adwaita.h");
});
const adwaita_version = std.SemanticVersion{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
.patch = c.ADW_MICRO_VERSION,
};
const required_blueprint_version = std.SemanticVersion{
.major = 0,
.minor = 16,
.patch = 0,
};
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
_ = it.next();
const required_adwaita_version = std.SemanticVersion{
.major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10),
.minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10),
.patch = 0,
};
const output = it.next() orelse return error.NoOutput;
const input = it.next() orelse return error.NoInput;
if (adwaita_version.order(required_adwaita_version) == .lt) {
std.debug.print(
\\`libadwaita` is too old.
\\
\\Ghostty requires a version {} or newer of `libadwaita` to
\\compile this blueprint. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
, .{required_adwaita_version});
std.posix.exit(1);
}
{
var stdout: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(alloc);
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stderr.deinit(alloc);
var blueprint_compiler = std.process.Child.init(
&.{
"blueprint-compiler",
"--version",
},
alloc,
);
blueprint_compiler.stdout_behavior = .Pipe;
blueprint_compiler.stderr_behavior = .Pipe;
try blueprint_compiler.spawn();
try blueprint_compiler.collectOutput(
alloc,
&stdout,
&stderr,
std.math.maxInt(u16),
);
const term = blueprint_compiler.wait() catch |err| switch (err) {
error.FileNotFound => {
std.debug.print(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires version {} or newer of
\\`blueprint-compiler` as a build-time dependency starting
\\from version 1.2. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
\\
, .{required_blueprint_version});
std.posix.exit(1);
},
else => return err,
};
switch (term) {
.Exited => |rc| {
if (rc != 0) std.process.exit(1);
},
else => std.process.exit(1),
}
const version = try std.SemanticVersion.parse(std.mem.trim(u8, stdout.items, &std.ascii.whitespace));
if (version.order(required_blueprint_version) == .lt) {
std.debug.print(
\\`blueprint-compiler` is the wrong version.
\\
\\Ghostty requires version {} or newer of
\\`blueprint-compiler` as a build-time dependency starting
\\from version 1.2. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
\\
, .{required_blueprint_version});
std.posix.exit(1);
}
}
{
var stdout: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(alloc);
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stderr.deinit(alloc);
var blueprint_compiler = std.process.Child.init(
&.{
"blueprint-compiler",
"compile",
"--output",
output,
input,
},
alloc,
);
blueprint_compiler.stdout_behavior = .Pipe;
blueprint_compiler.stderr_behavior = .Pipe;
try blueprint_compiler.spawn();
try blueprint_compiler.collectOutput(
alloc,
&stdout,
&stderr,
std.math.maxInt(u16),
);
const term = blueprint_compiler.wait() catch |err| switch (err) {
error.FileNotFound => {
std.debug.print(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires version {} or newer of
\\`blueprint-compiler` as a build-time dependency starting
\\from version 1.2. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
\\
, .{required_blueprint_version});
std.posix.exit(1);
},
else => return err,
};
switch (term) {
.Exited => |rc| {
if (rc != 0) {
std.debug.print("{s}", .{stderr.items});
std.process.exit(1);
}
},
else => {
std.debug.print("{s}", .{stderr.items});
std.process.exit(1);
},
}
}
}

View File

@@ -1,205 +0,0 @@
/// Contains all the logic for putting the Ghostty process and
/// each individual surface into its own cgroup.
const std = @import("std");
const assert = std.debug.assert;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const Allocator = std.mem.Allocator;
const App = @import("App.zig");
const internal_os = @import("../../os/main.zig");
const log = std.log.scoped(.gtk_systemd_cgroup);
/// Initialize the cgroup for the app. This will create our
/// transient scope, initialize the cgroups we use for the app,
/// configure them, and return the cgroup path for the app.
pub fn init(app: *App) ![]const u8 {
const pid = std.os.linux.getpid();
const alloc = app.core_app.alloc;
// Get our initial cgroup. We need this so we can compare
// and detect when we've switched to our transient group.
const original = try internal_os.cgroup.current(
alloc,
pid,
) orelse "";
defer alloc.free(original);
// Create our transient scope. If this succeeds then the unit
// was created, but we may not have moved into it yet, so we need
// to do a dumb busy loop to wait for the move to complete.
try createScope(app, pid);
const transient = transient: while (true) {
const current = try internal_os.cgroup.current(
alloc,
pid,
) orelse "";
if (!std.mem.eql(u8, original, current)) break :transient current;
alloc.free(current);
std.time.sleep(25 * std.time.ns_per_ms);
};
errdefer alloc.free(transient);
log.info("transient scope created cgroup={s}", .{transient});
// Create the app cgroup and put ourselves in it. This is
// required because controllers can't be configured while a
// process is in a cgroup.
try internal_os.cgroup.create(transient, "app", pid);
// Create a cgroup that will contain all our surfaces. We will
// enable the controllers and configure resource limits for surfaces
// only on this cgroup so that it doesn't affect our main app.
try internal_os.cgroup.create(transient, "surfaces", null);
const surfaces = try std.fmt.allocPrint(alloc, "{s}/surfaces", .{transient});
defer alloc.free(surfaces);
// Enable all of our cgroup controllers. If these fail then
// we just log. We can't reasonably undo what we've done above
// so we log the warning and still return the transient group.
// I don't know a scenario where this fails yet.
try enableControllers(alloc, transient);
try enableControllers(alloc, surfaces);
// Configure the "high" memory limit. This limit is used instead
// of "max" because it's a soft limit that can be exceeded and
// can be monitored by things like systemd-oomd to kill if needed,
// versus an instant hard kill.
if (app.config.@"linux-cgroup-memory-limit") |limit| {
try internal_os.cgroup.configureLimit(surfaces, .{
.memory_high = limit,
});
}
// Configure the "max" pids limit. This is a hard limit and cannot be
// exceeded.
if (app.config.@"linux-cgroup-processes-limit") |limit| {
try internal_os.cgroup.configureLimit(surfaces, .{
.pids_max = limit,
});
}
return transient;
}
/// Enable all the cgroup controllers for the given cgroup.
fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
const raw = try internal_os.cgroup.controllers(alloc, cgroup);
defer alloc.free(raw);
// Build our string builder for enabling all controllers
var builder = std.ArrayList(u8).init(alloc);
defer builder.deinit();
// Controllers are space-separated
var it = std.mem.splitScalar(u8, raw, ' ');
while (it.next()) |controller| {
try builder.append('+');
try builder.appendSlice(controller);
if (it.rest().len > 0) try builder.append(' ');
}
// Enable them all
try internal_os.cgroup.configureControllers(
cgroup,
builder.items,
);
}
/// Create a transient systemd scope unit for the current process.
///
/// On success this will return the name of the transient scope
/// cgroup prefix, allocated with the given allocator.
fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
const gio_app = app.app.as(gio.Application);
const connection = gio_app.getDbusConnection() orelse
return error.DbusConnectionRequired;
const pid: u32 = @intCast(pid_);
// The unit name needs to be unique. We use the pid for this.
var name_buf: [256]u8 = undefined;
const name = std.fmt.bufPrintZ(
&name_buf,
"app-ghostty-transient-{}.scope",
.{pid},
) catch unreachable;
const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))");
defer glib.free(builder_type);
// Initialize our builder to build up our parameters
var builder: glib.VariantBuilder = undefined;
builder.init(builder_type);
builder.add("s", name.ptr);
builder.add("s", "fail");
{
// Properties
const properties_type = glib.VariantType.new("a(sv)");
defer glib.free(properties_type);
builder.open(properties_type);
defer builder.close();
// https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
const pressure_value = glib.Variant.newString("kill");
builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value);
// Delegate
const delegate_value = glib.Variant.newBoolean(1);
builder.add("(sv)", "Delegate", delegate_value);
// Pid to move into the unit
const pids_value_type = glib.VariantType.new("u");
defer glib.free(pids_value_type);
const pids_value = glib.Variant.newFixedArray(pids_value_type, &pid, 1, @sizeOf(u32));
builder.add("(sv)", "PIDs", pids_value);
}
{
// Aux
const aux_type = glib.VariantType.new("a(sa(sv))");
defer glib.free(aux_type);
builder.open(aux_type);
defer builder.close();
}
var err: ?*glib.Error = null;
defer if (err) |e| e.free();
const reply_type = glib.VariantType.new("(o)");
defer glib.free(reply_type);
const value = builder.end();
const reply = connection.callSync(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"StartTransientUnit",
value,
reply_type,
.{},
-1,
null,
&err,
) orelse {
if (err) |e| log.err(
"creating transient cgroup scope failed code={} err={s}",
.{
e.f_code,
if (e.f_message) |msg| msg else "(no message)",
},
);
return error.DbusCallFailed;
};
defer reply.unref();
}

View File

@@ -1,29 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const internal_os = @import("../../os/main.zig");
const glib = @import("glib");
pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
if (comptime build_config.flatpak) {
// Only consult Flatpak runtime data for host case.
if (internal_os.isFlatpak()) {
var result: internal_os.ResourcesDir = .{
.app_path = try alloc.dupe(u8, "/app/share/ghostty"),
};
errdefer alloc.free(result.app_path.?);
const keyfile = glib.KeyFile.new();
defer keyfile.unref();
if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
defer glib.free(app_dir.ptr);
result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
return result;
}
}
return try internal_os.resourcesDir(alloc);
}

View File

@@ -1,168 +0,0 @@
const std = @import("std");
const css_files = [_][]const u8{
"style.css",
"style-dark.css",
"style-hc.css",
"style-hc-dark.css",
};
const icons = [_]struct {
alias: []const u8,
source: []const u8,
}{
.{
.alias = "16x16",
.source = "16",
},
.{
.alias = "16x16@2",
.source = "32",
},
.{
.alias = "32x32",
.source = "32",
},
.{
.alias = "32x32@2",
.source = "64",
},
.{
.alias = "128x128",
.source = "128",
},
.{
.alias = "128x128@2",
.source = "256",
},
.{
.alias = "256x256",
.source = "256",
},
.{
.alias = "256x256@2",
.source = "512",
},
.{
.alias = "512x512",
.source = "512",
},
.{
.alias = "1024x1024",
.source = "1024",
},
};
pub const VersionedBlueprint = struct {
major: u16,
minor: u16,
name: []const u8,
};
pub const blueprint_files = [_]VersionedBlueprint{
.{ .major = 1, .minor = 5, .name = "prompt-title-dialog" },
.{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" },
.{ .major = 1, .minor = 5, .name = "command-palette" },
.{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
.{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
.{ .major = 1, .minor = 5, .name = "ccw-paste" },
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
.{ .major = 1, .minor = 2, .name = "ccw-paste" },
};
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
defer {
for (extra_ui_files.items) |item| alloc.free(item);
extra_ui_files.deinit(alloc);
}
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
while (it.next()) |argument| {
if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
try extra_ui_files.append(alloc, try alloc.dupe(u8, argument));
}
}
const writer = std.io.getStdOut().writer();
try writer.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<gresources>
\\ <gresource prefix="/com/mitchellh/ghostty">
\\
);
for (css_files) |css_file| {
try writer.print(
" <file compressed=\"true\" alias=\"{s}\">src/apprt/gtk/{s}</file>\n",
.{ css_file, css_file },
);
}
try writer.writeAll(
\\ </gresource>
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
\\
);
for (icons) |icon| {
try writer.print(
" <file alias=\"{s}/apps/com.mitchellh.ghostty.png\">images/gnome/{s}.png</file>\n",
.{ icon.alias, icon.source },
);
}
try writer.writeAll(
\\ </gresource>
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
\\
);
for (extra_ui_files.items) |ui_file| {
for (blueprint_files) |file| {
const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.name });
defer alloc.free(expected);
if (!std.mem.endsWith(u8, ui_file, expected)) continue;
try writer.print(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{d}.{d}/{s}.ui\">{s}</file>\n",
.{ file.major, file.minor, file.name, ui_file },
);
break;
} else return error.BlueprintNotFound;
}
try writer.writeAll(
\\ </gresource>
\\</gresources>
\\
);
}
pub const dependencies = deps: {
const total = css_files.len + icons.len + blueprint_files.len;
var deps: [total][]const u8 = undefined;
var index: usize = 0;
for (css_files) |css_file| {
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
index += 1;
}
for (icons) |icon| {
deps[index] = std.fmt.comptimePrint("images/gnome/{s}.png", .{icon.source});
index += 1;
}
for (blueprint_files) |blueprint_file| {
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{
blueprint_file.major,
blueprint_file.minor,
blueprint_file.name,
});
index += 1;
}
break :deps deps;
};

View File

@@ -1,140 +0,0 @@
const std = @import("std");
// Until the gobject bindings are built at the same time we are building
// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version
// macros match the version of `gtk4` that we are building/linking against.
const c = @cImport({
@cInclude("gtk/gtk.h");
});
const gtk = @import("gtk");
const log = std.log.scoped(.gtk);
pub const comptime_version: std.SemanticVersion = .{
.major = c.GTK_MAJOR_VERSION,
.minor = c.GTK_MINOR_VERSION,
.patch = c.GTK_MICRO_VERSION,
};
pub fn getRuntimeVersion() std.SemanticVersion {
return .{
.major = gtk.getMajorVersion(),
.minor = gtk.getMinorVersion(),
.patch = gtk.getMicroVersion(),
};
}
pub fn logVersion() void {
log.info("GTK version build={} runtime={}", .{
comptime_version,
getRuntimeVersion(),
});
}
/// Verifies that the GTK version is at least the given version.
///
/// This can be run in both a comptime and runtime context. If it is run in a
/// comptime context, it will only check the version in the headers. If it is
/// run in a runtime context, it will check the actual version of the library we
/// are linked against.
///
/// This function should be used in cases where the version check would affect
/// code generation, such as using symbols that are only available beyond a
/// certain version. For checks which only depend on GTK's runtime behavior,
/// use `runtimeAtLeast`.
///
/// This is inlined so that the comptime checks will disable the runtime checks
/// if the comptime checks fail.
pub inline fn atLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
// If our header has lower versions than the given version,
// we can return false immediately. This prevents us from
// compiling against unknown symbols and makes runtime checks
// very slightly faster.
if (comptime comptime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) == .lt) return false;
// If we're in comptime then we can't check the runtime version.
if (@inComptime()) return true;
return runtimeAtLeast(major, minor, micro);
}
/// Verifies that the GTK version at runtime is at least the given version.
///
/// This function should be used in cases where the only the runtime behavior
/// is affected by the version check. For checks which would affect code
/// generation, use `atLeast`.
pub inline fn runtimeAtLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
// because the function gets the actual runtime version.
const runtime_version = getRuntimeVersion();
return runtime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) != .lt;
}
pub inline fn runtimeUntil(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
const runtime_version = getRuntimeVersion();
return runtime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) == .lt;
}
test "atLeast" {
const testing = std.testing;
const funs = &.{ atLeast, runtimeAtLeast };
inline for (funs) |fun| {
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
}
}
test "runtimeUntil" {
const testing = std.testing;
// This is an array in case we add a comptime variant.
const funs = &.{runtimeUntil};
inline for (funs) |fun| {
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
}
}

View File

@@ -1,54 +0,0 @@
const HeaderBar = @This();
const std = @import("std");
const adw = @import("adw");
const gtk = @import("gtk");
const Window = @import("Window.zig");
/// the Adwaita headerbar widget
headerbar: *adw.HeaderBar,
/// the Window that we belong to
window: *Window,
/// the Adwaita window title widget
title: *adw.WindowTitle,
pub fn init(self: *HeaderBar, window: *Window) void {
self.* = .{
.headerbar = adw.HeaderBar.new(),
.window = window,
.title = adw.WindowTitle.new(
window.window.as(gtk.Window).getTitle() orelse "Ghostty",
"",
),
};
self.headerbar.setTitleWidget(self.title.as(gtk.Widget));
}
pub fn setVisible(self: *const HeaderBar, visible: bool) void {
self.headerbar.as(gtk.Widget).setVisible(@intFromBool(visible));
}
pub fn asWidget(self: *const HeaderBar) *gtk.Widget {
return self.headerbar.as(gtk.Widget);
}
pub fn packEnd(self: *const HeaderBar, widget: *gtk.Widget) void {
self.headerbar.packEnd(widget);
}
pub fn packStart(self: *const HeaderBar, widget: *gtk.Widget) void {
self.headerbar.packStart(widget);
}
pub fn setTitle(self: *const HeaderBar, title: [:0]const u8) void {
self.window.window.as(gtk.Window).setTitle(title);
self.title.setTitle(title);
}
pub fn setSubtitle(self: *const HeaderBar, subtitle: [:0]const u8) void {
self.title.setSubtitle(subtitle);
}

View File

@@ -1,184 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gtk = @import("gtk");
const build_config = @import("../../build_config.zig");
const i18n = @import("../../os/main.zig").i18n;
const App = @import("App.zig");
const Surface = @import("Surface.zig");
const TerminalWindow = @import("Window.zig");
const ImguiWidget = @import("ImguiWidget.zig");
const CoreInspector = @import("../../inspector/main.zig").Inspector;
const log = std.log.scoped(.inspector);
/// Inspector is the primary stateful object that represents a terminal
/// inspector. An inspector is 1:1 with a Surface and is owned by a Surface.
/// Closing a surface must close its inspector.
pub const Inspector = struct {
/// The surface that owns this inspector.
surface: *Surface,
/// The current state of where this inspector is rendered. The Inspector
/// is the state of the inspector but this is the state of the GUI.
location: LocationState,
/// This is true if we want to destroy this inspector as soon as the
/// location is closed. For example: set this to true, request the
/// window be closed, let GTK do its cleanup, then note this to destroy
/// the inner state.
destroy_on_close: bool = true,
/// Location where the inspector will be launched.
pub const Location = union(LocationKey) {
hidden: void,
window: void,
};
/// The internal state for each possible location.
const LocationState = union(LocationKey) {
hidden: void,
window: Window,
};
const LocationKey = enum {
/// No GUI, but load the inspector state.
hidden,
/// A dedicated window for the inspector.
window,
};
/// Create an inspector for the given surface in the given location.
pub fn create(surface: *Surface, location: Location) !*Inspector {
const alloc = surface.app.core_app.alloc;
var ptr = try alloc.create(Inspector);
errdefer alloc.destroy(ptr);
try ptr.init(surface, location);
return ptr;
}
/// Destroy all memory associated with this inspector. You generally
/// should NOT call this publicly and should call `close` instead to
/// use the GTK lifecycle.
pub fn destroy(self: *Inspector) void {
assert(self.location == .hidden);
const alloc = self.allocator();
self.surface.inspector = null;
self.deinit();
alloc.destroy(self);
}
fn init(self: *Inspector, surface: *Surface, location: Location) !void {
self.* = .{
.surface = surface,
.location = undefined,
};
// Activate the inspector. If it doesn't work we ignore the error
// because we can just show an error in the inspector window.
self.surface.core_surface.activateInspector() catch |err| {
log.err("failed to activate inspector err={}", .{err});
};
switch (location) {
.hidden => self.location = .{ .hidden = {} },
.window => try self.initWindow(),
}
}
fn deinit(self: *Inspector) void {
self.surface.core_surface.deactivateInspector();
}
/// Request the inspector is closed.
pub fn close(self: *Inspector) void {
switch (self.location) {
.hidden => self.locationDidClose(),
.window => |v| v.close(),
}
}
fn locationDidClose(self: *Inspector) void {
self.location = .{ .hidden = {} };
if (self.destroy_on_close) self.destroy();
}
pub fn queueRender(self: *const Inspector) void {
switch (self.location) {
.hidden => {},
.window => |v| v.imgui_widget.queueRender(),
}
}
fn allocator(self: *const Inspector) Allocator {
return self.surface.app.core_app.alloc;
}
fn initWindow(self: *Inspector) !void {
self.location = .{ .window = undefined };
try self.location.window.init(self);
}
};
/// A dedicated window to hold an inspector instance.
const Window = struct {
inspector: *Inspector,
window: *gtk.ApplicationWindow,
imgui_widget: ImguiWidget,
pub fn init(self: *Window, inspector: *Inspector) !void {
// Initialize to undefined
self.* = .{
.inspector = inspector,
.window = undefined,
.imgui_widget = undefined,
};
// Create the window
self.window = .new(inspector.surface.app.app.as(gtk.Application));
errdefer self.window.as(gtk.Window).destroy();
self.window.as(gtk.Window).setTitle(i18n._("Ghostty: Terminal Inspector"));
self.window.as(gtk.Window).setDefaultSize(1000, 600);
self.window.as(gtk.Window).setIconName(build_config.bundle_id);
self.window.as(gtk.Widget).addCssClass("window");
self.window.as(gtk.Widget).addCssClass("inspector-window");
// Initialize our imgui widget
try self.imgui_widget.init();
errdefer self.imgui_widget.deinit();
self.imgui_widget.render_callback = &imguiRender;
self.imgui_widget.render_userdata = self;
CoreInspector.setup();
// Signals
_ = gtk.Widget.signals.destroy.connect(self.window, *Window, gtkDestroy, self, .{});
// Show the window
self.window.as(gtk.Window).setChild(self.imgui_widget.gl_area.as(gtk.Widget));
self.window.as(gtk.Window).present();
}
pub fn deinit(self: *Window) void {
self.inspector.locationDidClose();
}
pub fn close(self: *const Window) void {
self.window.as(gtk.Window).destroy();
}
fn imguiRender(ud: ?*anyopaque) void {
const self: *Window = @ptrCast(@alignCast(ud orelse return));
const surface = &self.inspector.surface.core_surface;
const inspector = surface.inspector orelse return;
inspector.render();
}
/// "destroy" signal for the window
fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.c) void {
log.debug("window destroy", .{});
self.deinit();
}
};

View File

@@ -1 +0,0 @@
pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow;

View File

@@ -1,172 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const apprt = @import("../../../apprt.zig");
// Use a D-Bus method call to open a new window on GTK.
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
//
// `ghostty +new-window` is equivalent to the following command (on a release build):
//
// ```
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] []
// ```
//
// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build):
//
// ```
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
// ```
pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
const stderr = std.io.getStdErr().writer();
// Get the appropriate bus name and object path for contacting the
// Ghostty instance we're interested in.
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
.class => |class| result: {
// Force the usage of the class specified on the CLI to determine the
// bus name and object path.
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
std.mem.replaceScalar(u8, object_path, '.', '/');
std.mem.replaceScalar(u8, object_path, '-', '_');
break :result .{ class, object_path };
},
.detect => switch (builtin.mode) {
.Debug, .ReleaseSafe => .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" },
.ReleaseFast, .ReleaseSmall => .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" },
},
};
defer {
switch (target) {
.class => alloc.free(object_path),
.detect => {},
}
}
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
return error.IPCFailed;
}
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
return error.IPCFailed;
}
const dbus = dbus: {
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const dbus_ = gio.busGetSync(.session, null, &err_);
if (err_) |err| {
try stderr.print(
"Unable to establish connection to D-Bus session bus: {s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
break :dbus dbus_ orelse {
try stderr.print("gio.busGetSync returned null\n", .{});
return error.IPCFailed;
};
};
defer dbus.unref();
// use a builder to create the D-Bus method call payload
const payload = payload: {
const builder_type = glib.VariantType.new("(sava{sv})");
defer glib.free(builder_type);
// Initialize our builder to build up our parameters
var builder: glib.VariantBuilder = undefined;
builder.init(builder_type);
errdefer builder.clear();
// action
if (value.arguments == null) {
builder.add("s", "new-window");
} else {
builder.add("s", "new-window-command");
}
// parameters
{
const av = glib.VariantType.new("av");
defer av.free();
var parameters: glib.VariantBuilder = undefined;
parameters.init(av);
errdefer parameters.clear();
if (value.arguments) |arguments| {
// If `-e` was specified on the command line, the first
// parameter is an array of strings that contain the arguments
// that came after `-e`, which will be interpreted as a command
// to run.
{
const as = glib.VariantType.new("as");
defer as.free();
var command: glib.VariantBuilder = undefined;
command.init(as);
errdefer command.clear();
for (arguments) |argument| {
command.add("s", argument.ptr);
}
parameters.add("v", command.end());
}
}
builder.addValue(parameters.end());
}
{
const platform_data = glib.VariantType.new("a{sv}");
defer platform_data.free();
builder.open(platform_data);
defer builder.close();
// we have no platform data
}
break :payload builder.end();
};
{
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const result_ = dbus.callSync(
bus_name,
object_path,
"org.gtk.Actions",
"Activate",
payload,
null, // We don't care about the return type, we don't do anything with it.
.{}, // no flags
-1, // default timeout
null, // not cancellable
&err_,
);
defer if (result_) |result| result.unref();
if (err_) |err| {
try stderr.print(
"D-Bus method call returned an error err={s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
}
return true;
}

View File

@@ -1,405 +0,0 @@
const std = @import("std");
const build_options = @import("build_options");
const gdk = @import("gdk");
const glib = @import("glib");
const gtk = @import("gtk");
const input = @import("../../input.zig");
const winproto = @import("winproto.zig");
/// Returns a GTK accelerator string from a trigger.
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
var buf_stream = std.io.fixedBufferStream(buf);
const writer = buf_stream.writer();
// Modifiers
if (trigger.mods.shift) try writer.writeAll("<Shift>");
if (trigger.mods.ctrl) try writer.writeAll("<Ctrl>");
if (trigger.mods.alt) try writer.writeAll("<Alt>");
if (trigger.mods.super) try writer.writeAll("<Super>");
// Write our key
if (!try writeTriggerKey(writer, trigger)) return null;
// We need to make the string null terminated.
try writer.writeByte(0);
const slice = buf_stream.getWritten();
return slice[0 .. slice.len - 1 :0];
}
/// Returns a XDG-compliant shortcuts string from a trigger.
/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/
pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
var buf_stream = std.io.fixedBufferStream(buf);
const writer = buf_stream.writer();
// Modifiers
if (trigger.mods.shift) try writer.writeAll("SHIFT+");
if (trigger.mods.ctrl) try writer.writeAll("CTRL+");
if (trigger.mods.alt) try writer.writeAll("ALT+");
if (trigger.mods.super) try writer.writeAll("LOGO+");
// Write our key
// NOTE: While the spec specifies that only libxkbcommon keysyms are
// expected, using GTK's keysyms should still work as they are identical
// to *X11's* keysyms (which I assume is a subset of libxkbcommon's).
// I haven't been able to any evidence to back up that assumption but
// this works for now
if (!try writeTriggerKey(writer, trigger)) return null;
// We need to make the string null terminated.
try writer.writeByte(0);
const slice = buf_stream.getWritten();
return slice[0 .. slice.len - 1 :0];
}
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool {
switch (trigger.key) {
.physical => |k| {
const keyval = keyvalFromKey(k) orelse return false;
try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false));
},
.unicode => |cp| {
if (gdk.keyvalName(cp)) |name| {
try writer.writeAll(std.mem.span(name));
} else {
try writer.print("{u}", .{cp});
}
},
}
return true;
}
pub fn translateMods(state: gdk.ModifierType) input.Mods {
return .{
.shift = state.shift_mask,
.ctrl = state.control_mask,
.alt = state.alt_mask,
.super = state.super_mask,
// Lock is dependent on the X settings but we just assume caps lock.
.caps_lock = state.lock_mask,
};
}
// Get the unshifted unicode value of the keyval. This is used
// by the Kitty keyboard protocol.
pub fn keyvalUnicodeUnshifted(
widget: *gtk.Widget,
event: *gdk.KeyEvent,
keycode: u32,
) u21 {
const display = widget.getDisplay();
// We need to get the currently active keyboard layout so we know
// what group to look at.
const layout = event.getLayout();
// Get all the possible keyboard mappings for this keycode. A keycode is the
// physical key pressed.
var keys: [*]gdk.KeymapKey = undefined;
var keyvals: [*]c_uint = undefined;
var n_entries: c_int = 0;
if (display.mapKeycode(keycode, &keys, &keyvals, &n_entries) == 0) return 0;
defer glib.free(keys);
defer glib.free(keyvals);
// debugging:
// std.log.debug("layout={}", .{layout});
// for (0..@intCast(n_entries)) |i| {
// std.log.debug("keymap key={} codepoint={x}", .{
// keys[i],
// gdk.keyvalToUnicode(keyvals[i]),
// });
// }
for (0..@intCast(n_entries)) |i| {
if (keys[i].f_group == layout and
keys[i].f_level == 0)
{
return std.math.cast(
u21,
gdk.keyvalToUnicode(keyvals[i]),
) orelse 0;
}
}
return 0;
}
/// Returns the mods to use a key event from a GTK event.
/// This requires a lot of context because the GdkEvent
/// doesn't contain enough on its own.
pub fn eventMods(
event: *gdk.Event,
physical_key: input.Key,
gtk_mods: gdk.ModifierType,
action: input.Action,
app_winproto: *winproto.App,
) input.Mods {
const device = event.getDevice();
var mods = app_winproto.eventMods(device, gtk_mods);
mods.num_lock = if (device) |d| d.getNumLockState() != 0 else false;
// We use the physical key to determine sided modifiers. As
// far as I can tell there's no other way to reliably determine
// this.
//
// We also set the main modifier to true if either side is true,
// since on both X11/Wayland, GTK doesn't set the main modifier
// if only the modifier key is pressed, but our core logic
// relies on it.
switch (physical_key) {
.shift_left => {
mods.shift = action != .release;
mods.sides.shift = .left;
},
.shift_right => {
mods.shift = action != .release;
mods.sides.shift = .right;
},
.control_left => {
mods.ctrl = action != .release;
mods.sides.ctrl = .left;
},
.control_right => {
mods.ctrl = action != .release;
mods.sides.ctrl = .right;
},
.alt_left => {
mods.alt = action != .release;
mods.sides.alt = .left;
},
.alt_right => {
mods.alt = action != .release;
mods.sides.alt = .right;
},
.meta_left => {
mods.super = action != .release;
mods.sides.super = .left;
},
.meta_right => {
mods.super = action != .release;
mods.sides.super = .right;
},
else => {},
}
return mods;
}
/// Returns an input key from a keyval or null if we don't have a mapping.
pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
for (keymap) |entry| {
if (entry[0] == keyval) return entry[1];
}
return null;
}
/// Returns a keyval from an input key or null if we don't have a mapping.
pub fn keyvalFromKey(key: input.Key) ?c_uint {
switch (key) {
inline else => |key_comptime| {
return comptime value: {
@setEvalBranchQuota(50_000);
for (keymap) |entry| {
if (entry[1] == key_comptime) break :value entry[0];
}
break :value null;
};
},
}
}
test "accelFromTrigger" {
const testing = std.testing;
var buf: [256]u8 = undefined;
try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
.mods = .{ .super = true },
.key = .{ .unicode = 'q' },
})).?);
try testing.expectEqualStrings("<Shift><Ctrl><Alt><Super>backslash", (try accelFromTrigger(&buf, .{
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
.key = .{ .unicode = 92 },
})).?);
}
test "xdgShortcutFromTrigger" {
const testing = std.testing;
var buf: [256]u8 = undefined;
try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{
.mods = .{ .super = true },
.key = .{ .unicode = 'q' },
})).?);
try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
.key = .{ .unicode = 92 },
})).?);
}
/// A raw entry in the keymap. Our keymap contains mappings between
/// GDK keys and our own key enum.
const RawEntry = struct { c_uint, input.Key };
const keymap: []const RawEntry = &.{
.{ gdk.KEY_a, .key_a },
.{ gdk.KEY_b, .key_b },
.{ gdk.KEY_c, .key_c },
.{ gdk.KEY_d, .key_d },
.{ gdk.KEY_e, .key_e },
.{ gdk.KEY_f, .key_f },
.{ gdk.KEY_g, .key_g },
.{ gdk.KEY_h, .key_h },
.{ gdk.KEY_i, .key_i },
.{ gdk.KEY_j, .key_j },
.{ gdk.KEY_k, .key_k },
.{ gdk.KEY_l, .key_l },
.{ gdk.KEY_m, .key_m },
.{ gdk.KEY_n, .key_n },
.{ gdk.KEY_o, .key_o },
.{ gdk.KEY_p, .key_p },
.{ gdk.KEY_q, .key_q },
.{ gdk.KEY_r, .key_r },
.{ gdk.KEY_s, .key_s },
.{ gdk.KEY_t, .key_t },
.{ gdk.KEY_u, .key_u },
.{ gdk.KEY_v, .key_v },
.{ gdk.KEY_w, .key_w },
.{ gdk.KEY_x, .key_x },
.{ gdk.KEY_y, .key_y },
.{ gdk.KEY_z, .key_z },
.{ gdk.KEY_0, .digit_0 },
.{ gdk.KEY_1, .digit_1 },
.{ gdk.KEY_2, .digit_2 },
.{ gdk.KEY_3, .digit_3 },
.{ gdk.KEY_4, .digit_4 },
.{ gdk.KEY_5, .digit_5 },
.{ gdk.KEY_6, .digit_6 },
.{ gdk.KEY_7, .digit_7 },
.{ gdk.KEY_8, .digit_8 },
.{ gdk.KEY_9, .digit_9 },
.{ gdk.KEY_semicolon, .semicolon },
.{ gdk.KEY_space, .space },
.{ gdk.KEY_apostrophe, .quote },
.{ gdk.KEY_comma, .comma },
.{ gdk.KEY_grave, .backquote },
.{ gdk.KEY_period, .period },
.{ gdk.KEY_slash, .slash },
.{ gdk.KEY_minus, .minus },
.{ gdk.KEY_equal, .equal },
.{ gdk.KEY_bracketleft, .bracket_left },
.{ gdk.KEY_bracketright, .bracket_right },
.{ gdk.KEY_backslash, .backslash },
.{ gdk.KEY_Up, .arrow_up },
.{ gdk.KEY_Down, .arrow_down },
.{ gdk.KEY_Right, .arrow_right },
.{ gdk.KEY_Left, .arrow_left },
.{ gdk.KEY_Home, .home },
.{ gdk.KEY_End, .end },
.{ gdk.KEY_Insert, .insert },
.{ gdk.KEY_Delete, .delete },
.{ gdk.KEY_Caps_Lock, .caps_lock },
.{ gdk.KEY_Scroll_Lock, .scroll_lock },
.{ gdk.KEY_Num_Lock, .num_lock },
.{ gdk.KEY_Page_Up, .page_up },
.{ gdk.KEY_Page_Down, .page_down },
.{ gdk.KEY_Escape, .escape },
.{ gdk.KEY_Return, .enter },
.{ gdk.KEY_Tab, .tab },
.{ gdk.KEY_BackSpace, .backspace },
.{ gdk.KEY_Print, .print_screen },
.{ gdk.KEY_Pause, .pause },
.{ gdk.KEY_F1, .f1 },
.{ gdk.KEY_F2, .f2 },
.{ gdk.KEY_F3, .f3 },
.{ gdk.KEY_F4, .f4 },
.{ gdk.KEY_F5, .f5 },
.{ gdk.KEY_F6, .f6 },
.{ gdk.KEY_F7, .f7 },
.{ gdk.KEY_F8, .f8 },
.{ gdk.KEY_F9, .f9 },
.{ gdk.KEY_F10, .f10 },
.{ gdk.KEY_F11, .f11 },
.{ gdk.KEY_F12, .f12 },
.{ gdk.KEY_F13, .f13 },
.{ gdk.KEY_F14, .f14 },
.{ gdk.KEY_F15, .f15 },
.{ gdk.KEY_F16, .f16 },
.{ gdk.KEY_F17, .f17 },
.{ gdk.KEY_F18, .f18 },
.{ gdk.KEY_F19, .f19 },
.{ gdk.KEY_F20, .f20 },
.{ gdk.KEY_F21, .f21 },
.{ gdk.KEY_F22, .f22 },
.{ gdk.KEY_F23, .f23 },
.{ gdk.KEY_F24, .f24 },
.{ gdk.KEY_F25, .f25 },
.{ gdk.KEY_KP_0, .numpad_0 },
.{ gdk.KEY_KP_1, .numpad_1 },
.{ gdk.KEY_KP_2, .numpad_2 },
.{ gdk.KEY_KP_3, .numpad_3 },
.{ gdk.KEY_KP_4, .numpad_4 },
.{ gdk.KEY_KP_5, .numpad_5 },
.{ gdk.KEY_KP_6, .numpad_6 },
.{ gdk.KEY_KP_7, .numpad_7 },
.{ gdk.KEY_KP_8, .numpad_8 },
.{ gdk.KEY_KP_9, .numpad_9 },
.{ gdk.KEY_KP_Decimal, .numpad_decimal },
.{ gdk.KEY_KP_Divide, .numpad_divide },
.{ gdk.KEY_KP_Multiply, .numpad_multiply },
.{ gdk.KEY_KP_Subtract, .numpad_subtract },
.{ gdk.KEY_KP_Add, .numpad_add },
.{ gdk.KEY_KP_Enter, .numpad_enter },
.{ gdk.KEY_KP_Equal, .numpad_equal },
.{ gdk.KEY_KP_Separator, .numpad_separator },
.{ gdk.KEY_KP_Left, .numpad_left },
.{ gdk.KEY_KP_Right, .numpad_right },
.{ gdk.KEY_KP_Up, .numpad_up },
.{ gdk.KEY_KP_Down, .numpad_down },
.{ gdk.KEY_KP_Page_Up, .numpad_page_up },
.{ gdk.KEY_KP_Page_Down, .numpad_page_down },
.{ gdk.KEY_KP_Home, .numpad_home },
.{ gdk.KEY_KP_End, .numpad_end },
.{ gdk.KEY_KP_Insert, .numpad_insert },
.{ gdk.KEY_KP_Delete, .numpad_delete },
.{ gdk.KEY_KP_Begin, .numpad_begin },
.{ gdk.KEY_Copy, .copy },
.{ gdk.KEY_Cut, .cut },
.{ gdk.KEY_Paste, .paste },
.{ gdk.KEY_Shift_L, .shift_left },
.{ gdk.KEY_Control_L, .control_left },
.{ gdk.KEY_Alt_L, .alt_left },
.{ gdk.KEY_Super_L, .meta_left },
.{ gdk.KEY_Shift_R, .shift_right },
.{ gdk.KEY_Control_R, .control_right },
.{ gdk.KEY_Alt_R, .alt_right },
.{ gdk.KEY_Super_R, .meta_right },
// TODO: media keys
};

View File

@@ -1,139 +0,0 @@
const std = @import("std");
const gtk = @import("gtk");
const gdk = @import("gdk");
const gio = @import("gio");
const gobject = @import("gobject");
const apprt = @import("../../apprt.zig");
const App = @import("App.zig");
const Window = @import("Window.zig");
const Surface = @import("Surface.zig");
const Builder = @import("Builder.zig");
/// Abstract GTK menus to take advantage of machinery for buildtime/comptime
/// error checking.
pub fn Menu(
/// GTK apprt type that the menu is "for". Window and Surface are supported
/// right now.
comptime T: type,
/// Name of the menu. Along with the apprt type, this is used to look up the
/// builder ui definitions of the menu.
comptime menu_name: []const u8,
/// Should the popup have a pointer pointing to the location that it's
/// attached to.
comptime arrow: bool,
) type {
return struct {
const Self = @This();
/// parent apprt object
parent: *T,
/// our widget
menu_widget: *gtk.PopoverMenu,
/// initialize the menu
pub fn init(self: *Self, parent: *T) void {
const object_type = switch (T) {
Window => "window",
Surface => "surface",
else => unreachable,
};
var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0);
defer builder.deinit();
const menu_model = builder.getObject(gio.MenuModel, "menu").?;
const menu_widget = gtk.PopoverMenu.newFromModelFull(menu_model, .{ .nested = true });
// If this menu has an arrow, don't modify the horizontal alignment
// or you get visual anomalies. See PR #6087. Otherwise set the
// horizontal alignment to `start` so that the top left corner of
// the menu aligns with the point that the menu is popped up at.
if (!arrow) menu_widget.as(gtk.Widget).setHalign(.start);
menu_widget.as(gtk.Popover).setHasArrow(@intFromBool(arrow));
_ = gtk.Popover.signals.closed.connect(
menu_widget,
*Self,
gtkRefocusTerm,
self,
.{},
);
self.* = .{
.parent = parent,
.menu_widget = menu_widget,
};
}
pub fn setParent(self: *const Self, widget: *gtk.Widget) void {
self.menu_widget.as(gtk.Widget).setParent(widget);
}
pub fn asWidget(self: *const Self) *gtk.Widget {
return self.menu_widget.as(gtk.Widget);
}
pub fn isVisible(self: *const Self) bool {
return self.menu_widget.as(gtk.Widget).getVisible() != 0;
}
/// Refresh the menu. Right now that means enabling/disabling the "Copy"
/// menu item based on whether there is an active selection or not, but
/// that may change in the future.
pub fn refresh(self: *const Self) void {
const window: *gtk.Window, const has_selection: bool = switch (T) {
Window => window: {
const has_selection = if (self.parent.actionSurface()) |core_surface|
core_surface.hasSelection()
else
false;
break :window .{ self.parent.window.as(gtk.Window), has_selection };
},
Surface => surface: {
const window = self.parent.container.window() orelse return;
const has_selection = self.parent.core_surface.hasSelection();
break :surface .{ window.window.as(gtk.Window), has_selection };
},
else => unreachable,
};
const action_map: *gio.ActionMap = gobject.ext.cast(gio.ActionMap, window) orelse return;
const action: *gio.SimpleAction = gobject.ext.cast(
gio.SimpleAction,
action_map.lookupAction("copy") orelse return,
) orelse return;
action.setEnabled(@intFromBool(has_selection));
}
/// Pop up the menu at the given coordinates
pub fn popupAt(self: *const Self, x: c_int, y: c_int) void {
const rect: gdk.Rectangle = .{
.f_x = x,
.f_y = y,
.f_width = 1,
.f_height = 1,
};
const popover = self.menu_widget.as(gtk.Popover);
popover.setPointingTo(&rect);
self.refresh();
popover.popup();
}
/// Refocus tab that lost focus because of the popover menu
fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.c) void {
const window: *Window = switch (T) {
Window => self.parent,
Surface => self.parent.container.window() orelse return,
else => unreachable,
};
window.focusCurrentTab();
}
};
}

View File

@@ -1,8 +0,0 @@
.transparent {
background-color: transparent;
}
.terminal-window .notebook paned > separator {
background-color: rgba(36, 36, 36, 1);
background-clip: content-box;
}

View File

@@ -1,3 +0,0 @@
.transparent {
background-color: transparent;
}

View File

@@ -1,3 +0,0 @@
.transparent {
background-color: transparent;
}

View File

@@ -1,116 +0,0 @@
label.url-overlay {
padding: 4px 8px 4px 8px;
outline-style: solid;
outline-color: #555555;
outline-width: 1px;
}
label.url-overlay:hover {
opacity: 0;
}
label.url-overlay.left {
border-radius: 0px 6px 0px 0px;
}
label.url-overlay.right {
border-radius: 6px 0px 0px 0px;
}
label.url-overlay.hidden {
opacity: 0;
}
label.size-overlay {
padding: 4px 8px 4px 8px;
border-radius: 6px 6px 6px 6px;
outline-style: solid;
outline-width: 1px;
outline-color: #555555;
}
label.size-overlay.hidden {
opacity: 0;
}
window.ssd.no-border-radius {
/* Without clearing the border radius, at least on Mutter with
* gtk-titlebar=true and gtk-adwaita=false, there is some window artifacting
* that this will mitigate.
*/
border-radius: 0 0;
}
.transparent {
background-color: transparent;
}
.terminal-window .notebook paned > separator {
background-color: rgba(250, 250, 250, 1);
background-clip: content-box;
/* This works around the oversized drag area for the right side of GtkPaned.
*
* Upstream Gtk issue:
* https://gitlab.gnome.org/GNOME/gtk/-/issues/4484#note_2362002
*
* Ghostty issue:
* https://github.com/ghostty-org/ghostty/issues/3020
*
* Without this, it's not possible to select the first character on the
* right-hand side of a split.
*/
margin: 0;
padding: 0;
}
.clipboard-overlay {
border-radius: 10px;
}
.clipboard-content-view {
filter: blur(0px);
transition: filter 0.3s ease;
border-radius: 10px;
}
.clipboard-content-view.blurred {
filter: blur(5px);
}
.command-palette-search {
font-size: 1.25rem;
padding: 4px;
-gtk-icon-size: 20px;
}
.command-palette-search > image:first-child {
margin-left: 8px;
margin-right: 4px;
}
.command-palette-search > image:last-child {
margin-left: 4px;
margin-right: 8px;
}
banner.child_exited_normally revealer widget {
background-color: rgba(38, 162, 105, 0.5);
/* after GTK 4.16 is a requirement, switch to the following:
/* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */
}
banner.child_exited_abnormally revealer widget {
background-color: rgba(192, 28, 40, 0.5);
/* after GTK 4.16 is a requirement, switch to the following:
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
}
/*
* Change the color of an error progressbar
*/
progressbar.error trough progress {
background-color: rgb(192, 28, 40);
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: var(--error-bg-color); */
}

View File

@@ -1,25 +0,0 @@
using Gtk 4.0;
menu menu {
section {
item {
label: _("Split Up");
action: "win.split-up";
}
item {
label: _("Split Down");
action: "win.split-down";
}
item {
label: _("Split Left");
action: "win.split-left";
}
item {
label: _("Split Right");
action: "win.split-right";
}
}
}

View File

@@ -1,102 +0,0 @@
using Gtk 4.0;
menu menu {
section {
item {
label: _("Copy");
action: "win.copy";
}
item {
label: _("Paste");
action: "win.paste";
}
}
section {
item {
label: _("Clear");
action: "win.clear";
}
item {
label: _("Reset");
action: "win.reset";
}
}
section {
submenu {
label: _("Split");
item {
label: _("Change Title…");
action: "win.prompt-title";
}
item {
label: _("Split Up");
action: "win.split-up";
}
item {
label: _("Split Down");
action: "win.split-down";
}
item {
label: _("Split Left");
action: "win.split-left";
}
item {
label: _("Split Right");
action: "win.split-right";
}
}
submenu {
label: _("Tab");
item {
label: _("New Tab");
action: "win.new-tab";
}
item {
label: _("Close Tab");
action: "win.close-tab";
}
}
submenu {
label: _("Window");
item {
label: _("New Window");
action: "win.new-window";
}
item {
label: _("Close Window");
action: "win.close";
}
}
}
section {
submenu {
label: _("Config");
item {
label: _("Open Configuration");
action: "app.open-config";
}
item {
label: _("Reload Configuration");
action: "app.reload-config";
}
}
}
}

View File

@@ -1,116 +0,0 @@
using Gtk 4.0;
menu menu {
section {
item {
label: _("Copy");
action: "win.copy";
}
item {
label: _("Paste");
action: "win.paste";
}
}
section {
item {
label: _("New Window");
action: "win.new-window";
}
item {
label: _("Close Window");
action: "win.close";
}
}
section {
item {
label: _("New Tab");
action: "win.new-tab";
}
item {
label: _("Close Tab");
action: "win.close-tab";
}
}
section {
submenu {
label: _("Split");
item {
label: _("Change Title…");
action: "win.prompt-title";
}
item {
label: _("Split Up");
action: "win.split-up";
}
item {
label: _("Split Down");
action: "win.split-down";
}
item {
label: _("Split Left");
action: "win.split-left";
}
item {
label: _("Split Right");
action: "win.split-right";
}
}
}
section {
item {
label: _("Clear");
action: "win.clear";
}
item {
label: _("Reset");
action: "win.reset";
}
}
section {
item {
label: _("Command Palette");
action: "win.toggle-command-palette";
}
item {
label: _("Terminal Inspector");
action: "win.toggle-inspector";
}
item {
label: _("Open Configuration");
action: "app.open-config";
}
item {
label: _("Reload Configuration");
action: "app.reload-config";
}
}
section {
item {
label: _("About Ghostty");
action: "win.about";
}
item {
label: _("Quit");
action: "app.quit";
}
}
}

View File

@@ -1,71 +0,0 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.MessageDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: Overlay {
styles [
"osd",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@@ -1,71 +0,0 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.MessageDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: Overlay {
styles [
"osd",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@@ -1,71 +0,0 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.MessageDialog clipboard_confirmation_window {
heading: _("Warning: Potentially Unsafe Paste");
body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
responses [
cancel: _("Cancel") suggested,
ok: _("Paste") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: Overlay {
styles [
"osd",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@@ -1,28 +0,0 @@
using Gtk 4.0;
using Adw 1;
Adw.MessageDialog config_errors_dialog {
heading: _("Configuration Errors");
body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.");
responses [
ignore: _("Ignore"),
reload: _("Reload Configuration") suggested,
]
extra-child: ScrolledWindow {
min-content-width: 500;
min-content-height: 100;
TextView {
editable: false;
cursor-visible: false;
top-margin: 8;
bottom-margin: 8;
left-margin: 8;
right-margin: 8;
buffer: TextBuffer error_message {};
}
};
}

View File

@@ -1,85 +0,0 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.AlertDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: ListBox {
selection-mode: none;
styles [
"boxed-list-separate",
]
Overlay {
styles [
"osd",
"clipboard-overlay",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 200;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
}
Adw.SwitchRow remember_choice {
title: _("Remember choice for this split");
subtitle: _("Reload configuration to show this prompt again");
}
};
}

View File

@@ -1,81 +0,0 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.AlertDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: ListBox {
selection-mode: none;
styles [
"boxed-list-separate",
]
Overlay {
styles [
"osd",
"clipboard-overlay",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 200;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
}
}
Adw.SwitchRow remember_choice {
title: _("Remember choice for this split");
subtitle: _("Reload configuration to show this prompt again");
}
};
}

View File

@@ -1,71 +0,0 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.AlertDialog clipboard_confirmation_window {
heading: _("Warning: Potentially Unsafe Paste");
body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
responses [
cancel: _("Cancel") suggested,
ok: _("Paste") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: Overlay {
styles [
"osd",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@@ -1,106 +0,0 @@
using Gtk 4.0;
using Gio 2.0;
using Adw 1;
Adw.Dialog command-palette {
content-width: 700;
Adw.ToolbarView {
top-bar-style: flat;
[top]
Adw.HeaderBar {
[title]
SearchEntry search {
hexpand: true;
placeholder-text: _("Execute a command…");
styles [
"command-palette-search",
]
}
}
ScrolledWindow {
min-content-height: 300;
ListView view {
show-separators: true;
single-click-activate: true;
model: SingleSelection model {
model: FilterListModel {
incremental: true;
filter: AnyFilter {
StringFilter {
expression: expr item as <$GhosttyCommand>.title;
search: bind search.text;
}
StringFilter {
expression: expr item as <$GhosttyCommand>.action-key;
search: bind search.text;
}
};
model: Gio.ListStore source {
item-type: typeof<$GhosttyCommand>;
};
};
};
styles [
"rich-list",
]
factory: BuilderListItemFactory {
template ListItem {
child: Box {
orientation: horizontal;
spacing: 10;
tooltip-text: bind template.item as <$GhosttyCommand>.description;
Box {
orientation: vertical;
hexpand: true;
Label {
ellipsize: end;
halign: start;
wrap: false;
single-line-mode: true;
styles [
"title",
]
label: bind template.item as <$GhosttyCommand>.title;
}
Label {
ellipsize: end;
halign: start;
wrap: false;
single-line-mode: true;
styles [
"subtitle",
"monospace",
]
label: bind template.item as <$GhosttyCommand>.action-key;
}
}
ShortcutLabel {
accelerator: bind template.item as <$GhosttyCommand>.action;
valign: center;
}
};
}
};
}
}
}
}

View File

@@ -1,28 +0,0 @@
using Gtk 4.0;
using Adw 1;
Adw.AlertDialog config_errors_dialog {
heading: _("Configuration Errors");
body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.");
responses [
ignore: _("Ignore"),
reload: _("Reload Configuration") suggested,
]
extra-child: ScrolledWindow {
min-content-width: 500;
min-content-height: 100;
TextView {
editable: false;
cursor-visible: false;
top-margin: 8;
bottom-margin: 8;
left-margin: 8;
right-margin: 8;
buffer: TextBuffer error_message {};
}
};
}

View File

@@ -1,16 +0,0 @@
using Gtk 4.0;
using Adw 1;
Adw.AlertDialog prompt_title_dialog {
heading: _("Change Terminal Title");
body: _("Leave blank to restore the default title.");
responses [
cancel: _("Cancel") suggested,
ok: _("OK") destructive,
]
focus-widget: title_entry;
extra-child: Entry title_entry {};
}

View File

@@ -1,15 +0,0 @@
# GTK UI files
This directory is for storing GTK blueprints. GTK blueprints are compiled into
GTK resource builder `.ui` files by `blueprint-compiler` at build time and then
converted into an embeddable resource by `glib-compile-resources`.
Blueprint files should be stored in directories that represent the minimum
Adwaita version needed to use that resource. Blueprint files should also be
formatted using `blueprint-compiler format` as well to ensure consistency
(formatting will be checked in CI).
`blueprint-compiler` version 0.16.0 or newer is required to compile Blueprint
files. If your system does not have `blueprint-compiler` or does not have a
new enough version you can use the generated source tarballs, which contain
precompiled versions of the blueprints.

View File

@@ -1,155 +0,0 @@
const std = @import("std");
const build_options = @import("build_options");
const Allocator = std.mem.Allocator;
const gdk = @import("gdk");
const Config = @import("../../config.zig").Config;
const input = @import("../../input.zig");
const key = @import("key.zig");
const ApprtWindow = @import("Window.zig");
pub const noop = @import("winproto/noop.zig");
pub const x11 = @import("winproto/x11.zig");
pub const wayland = @import("winproto/wayland.zig");
pub const Protocol = enum {
none,
wayland,
x11,
};
/// App-state for the underlying windowing protocol. There should be one
/// instance of this struct per application.
pub const App = union(Protocol) {
none: noop.App,
wayland: if (build_options.wayland) wayland.App else noop.App,
x11: if (build_options.x11) x11.App else noop.App,
pub fn init(
alloc: Allocator,
gdk_display: *gdk.Display,
app_id: [:0]const u8,
config: *const Config,
) !App {
inline for (@typeInfo(App).@"union".fields) |field| {
if (try field.type.init(
alloc,
gdk_display,
app_id,
config,
)) |v| {
return @unionInit(App, field.name, v);
}
}
return .{ .none = .{} };
}
pub fn deinit(self: *App, alloc: Allocator) void {
switch (self.*) {
inline else => |*v| v.deinit(alloc),
}
}
pub fn eventMods(
self: *App,
device: ?*gdk.Device,
gtk_mods: gdk.ModifierType,
) input.Mods {
return switch (self.*) {
inline else => |*v| v.eventMods(device, gtk_mods),
} orelse key.translateMods(gtk_mods);
}
pub fn supportsQuickTerminal(self: App) bool {
return switch (self) {
inline else => |v| v.supportsQuickTerminal(),
};
}
/// Set up necessary support for the quick terminal that must occur
/// *before* the window-level winproto object is created.
///
/// Only has an effect on the Wayland backend, where the gtk4-layer-shell
/// library is initialized.
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
switch (self.*) {
inline else => |*v| try v.initQuickTerminal(apprt_window),
}
}
};
/// Per-Window state for the underlying windowing protocol.
///
/// In Wayland, the terminology used is "Surface" and for it, this is
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
/// heavily to mean something completely different, so we use "Window" here
/// to better match what it generally maps to in the Ghostty codebase.
pub const Window = union(Protocol) {
none: noop.Window,
wayland: if (build_options.wayland) wayland.Window else noop.Window,
x11: if (build_options.x11) x11.Window else noop.Window,
pub fn init(
alloc: Allocator,
app: *App,
apprt_window: *ApprtWindow,
) !Window {
return switch (app.*) {
inline else => |*v, tag| {
inline for (@typeInfo(Window).@"union".fields) |field| {
if (comptime std.mem.eql(
u8,
field.name,
@tagName(tag),
)) return @unionInit(
Window,
field.name,
try field.type.init(
alloc,
v,
apprt_window,
),
);
}
},
};
}
pub fn deinit(self: *Window, alloc: Allocator) void {
switch (self.*) {
inline else => |*v| v.deinit(alloc),
}
}
pub fn resizeEvent(self: *Window) !void {
switch (self.*) {
inline else => |*v| try v.resizeEvent(),
}
}
pub fn syncAppearance(self: *Window) !void {
switch (self.*) {
inline else => |*v| try v.syncAppearance(),
}
}
pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self) {
inline else => |v| v.clientSideDecorationEnabled(),
};
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
switch (self.*) {
inline else => |*v| try v.addSubprocessEnv(env),
}
}
pub fn setUrgent(self: *Window, urgent: bool) !void {
switch (self.*) {
inline else => |*v| try v.setUrgent(urgent),
}
}
};

View File

@@ -1,75 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const gdk = @import("gdk");
const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig");
const ApprtWindow = @import("../Window.zig");
const log = std.log.scoped(.winproto_noop);
pub const App = struct {
pub fn init(
_: Allocator,
_: *gdk.Display,
_: [:0]const u8,
_: *const Config,
) !?App {
return null;
}
pub fn deinit(self: *App, alloc: Allocator) void {
_ = self;
_ = alloc;
}
pub fn eventMods(
_: *App,
_: ?*gdk.Device,
_: gdk.ModifierType,
) ?input.Mods {
return null;
}
pub fn supportsQuickTerminal(_: App) bool {
return false;
}
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
};
pub const Window = struct {
pub fn init(
_: Allocator,
_: *App,
_: *ApprtWindow,
) !Window {
return .{};
}
pub fn deinit(self: Window, alloc: Allocator) void {
_ = self;
_ = alloc;
}
pub fn updateConfigEvent(
_: *Window,
_: *const ApprtWindow.DerivedConfig,
) !void {}
pub fn resizeEvent(_: *Window) !void {}
pub fn syncAppearance(_: *Window) !void {}
/// This returns true if CSD is enabled for this window. This
/// should be the actual present state of the window, not the
/// desired state.
pub fn clientSideDecorationEnabled(self: Window) bool {
_ = self;
return true;
}
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
pub fn setUrgent(_: *Window, _: bool) !void {}
};

View File

@@ -1,511 +0,0 @@
//! Wayland protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_options = @import("build_options");
const gdk = @import("gdk");
const gdk_wayland = @import("gdk_wayland");
const gobject = @import("gobject");
const gtk = @import("gtk");
const layer_shell = @import("gtk4-layer-shell");
const wayland = @import("wayland");
const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig");
const ApprtWindow = @import("../Window.zig");
const wl = wayland.client.wl;
const org = wayland.client.org;
const xdg = wayland.client.xdg;
const log = std.log.scoped(.winproto_wayland);
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
pub const App = struct {
display: *wl.Display,
context: *Context,
const Context = struct {
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_slide_manager: ?*org.KdeKwinSlideManager = null,
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
xdg_activation: ?*xdg.ActivationV1 = null,
/// Whether the xdg_wm_dialog_v1 protocol is present.
///
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
/// creates a quick terminal, and we need to ensure this fails
/// gracefully if this situation occurs.
///
/// FIXME: This is a temporary workaround - we should remove this when
/// all of our supported distros drop support for affected old
/// gtk4-layer-shell versions.
///
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
xdg_wm_dialog_present: bool = false,
};
pub fn init(
alloc: Allocator,
gdk_display: *gdk.Display,
app_id: [:0]const u8,
config: *const Config,
) !?App {
_ = config;
_ = app_id;
const gdk_wayland_display = gobject.ext.cast(
gdk_wayland.WaylandDisplay,
gdk_display,
) orelse return null;
const display: *wl.Display = @ptrCast(@alignCast(
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
));
// Create our context for our callbacks so we have a stable pointer.
// Note: at the time of writing this comment, we don't really need
// 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.* = .{};
// Get our display registry so we can get all the available interfaces
// and bind to what we need.
const registry = try display.getRegistry();
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);
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
}
return .{
.display = display,
.context = context,
};
}
pub fn deinit(self: *App, alloc: Allocator) void {
alloc.destroy(self.context);
}
pub fn eventMods(
_: *App,
_: ?*gdk.Device,
_: gdk.ModifierType,
) ?input.Mods {
return null;
}
pub fn supportsQuickTerminal(self: App) bool {
if (!layer_shell.isSupported()) {
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
return false;
}
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
.major = 1,
.minor = 0,
.patch = 4,
}) == .lt) {
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
return false;
}
return true;
}
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
const window = apprt_window.window.as(gtk.Window);
layer_shell.initForWindow(window);
layer_shell.setLayer(window, .top);
layer_shell.setNamespace(window, "ghostty-quick-terminal");
}
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,
else => return null,
},
else => return null,
};
// Only process Wayland interfaces
if (!@hasDecl(T, "interface")) return null;
return T;
}
fn registryListener(
registry: *wl.Registry,
event: wl.Registry.Event,
context: *Context,
) void {
const ctx_fields = @typeInfo(Context).@"struct".fields;
switch (event) {
.global => |v| {
log.debug("found global {s}", .{v.interface});
// We don't actually do anything with this other than checking
// for its existence, so we process this separately.
if (std.mem.orderZ(
u8,
v.interface,
"xdg_wm_dialog_v1",
) == .eq) {
context.xdg_wm_dialog_present = true;
return;
}
inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue;
if (std.mem.orderZ(
u8,
v.interface,
T.interface.name,
) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind(
v.name,
T,
T.generated_version,
) catch |err| {
log.warn(
"error binding interface {s} error={}",
.{ v.interface, err },
);
return;
};
}
}
},
// This should be a rare occurrence, but in case a global
// is suddenly no longer available, we destroy and unset it
// as the protocol mandates.
.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;
}
}
},
}
}
fn decoManagerListener(
_: *org.KdeKwinServerDecorationManager,
event: org.KdeKwinServerDecorationManager.Event,
context: *Context,
) void {
switch (event) {
.default_mode => |mode| {
context.default_deco_mode = @enumFromInt(mode.mode);
},
}
}
};
/// Per-window (wl_surface) state for the Wayland protocol.
pub const Window = struct {
apprt_window: *ApprtWindow,
/// The Wayland surface for this window.
surface: *wl.Surface,
/// The context from the app where we can load our Wayland interfaces.
app_context: *App.Context,
/// A token that, when present, indicates that the window is blurred.
blur_token: ?*org.KdeKwinBlur = null,
/// Object that controls the decoration mode (client/server/auto)
/// of the window.
decoration: ?*org.KdeKwinServerDecoration = null,
/// Object that controls the slide-in/slide-out animations of the
/// quick terminal. Always null for windows other than the quick terminal.
slide: ?*org.KdeKwinSlide = null,
/// Object that, when present, denotes that the window is currently
/// requesting attention from the user.
activation_token: ?*xdg.ActivationTokenV1 = null,
pub fn init(
alloc: Allocator,
app: *App,
apprt_window: *ApprtWindow,
) !Window {
_ = alloc;
const gtk_native = apprt_window.window.as(gtk.Native);
const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface;
// This should never fail, because if we're being called at this point
// then we've already asserted that our app state is Wayland.
const gdk_wl_surface = gobject.ext.cast(
gdk_wayland.WaylandSurface,
gdk_surface,
) orelse return error.NoWaylandSurface;
const wl_surface: *wl.Surface = @ptrCast(@alignCast(
gdk_wl_surface.getWlSurface() orelse return error.NoWaylandSurface,
));
// Get our decoration object so we can control the
// CSD vs SSD status of this surface.
const deco: ?*org.KdeKwinServerDecoration = deco: {
const mgr = app.context.kde_decoration_manager orelse
break :deco null;
const deco: *org.KdeKwinServerDecoration = mgr.create(
wl_surface,
) catch |err| {
log.warn("could not create decoration object={}", .{err});
break :deco null;
};
break :deco deco;
};
if (apprt_window.isQuickTerminal()) {
_ = gdk.Surface.signals.enter_monitor.connect(
gdk_surface,
*ApprtWindow,
enteredMonitor,
apprt_window,
.{},
);
}
return .{
.apprt_window = apprt_window,
.surface = wl_surface,
.app_context = app.context,
.decoration = deco,
};
}
pub fn deinit(self: Window, alloc: Allocator) void {
_ = alloc;
if (self.blur_token) |blur| blur.release();
if (self.decoration) |deco| deco.release();
if (self.slide) |slide| slide.release();
}
pub fn resizeEvent(_: *Window) !void {}
pub fn syncAppearance(self: *Window) !void {
self.syncBlur() catch |err| {
log.err("failed to sync blur={}", .{err});
};
self.syncDecoration() catch |err| {
log.err("failed to sync blur={}", .{err});
};
if (self.apprt_window.isQuickTerminal()) {
self.syncQuickTerminal() catch |err| {
log.warn("failed to sync quick terminal appearance={}", .{err});
};
}
}
pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self.getDecorationMode()) {
.Client => true,
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
// However, if we do not support SSDs (e.g. GNOME) then we should enable
// CSDs even if the user prefers SSDs.
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
.None => false,
else => unreachable,
};
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
_ = self;
_ = env;
}
pub fn setUrgent(self: *Window, urgent: bool) !void {
const activation = self.app_context.xdg_activation orelse return;
// If there already is a token, destroy and unset it
if (self.activation_token) |token| token.destroy();
self.activation_token = if (urgent) token: {
const token = try activation.getActivationToken();
token.setSurface(self.surface);
token.setListener(*Window, onActivationTokenEvent, self);
token.commit();
break :token token;
} else null;
}
/// Update the blur state of the window.
fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return;
const blur = self.apprt_window.config.background_blur;
if (self.blur_token) |tok| {
// Only release token when transitioning from blurred -> not blurred
if (!blur.enabled()) {
manager.unset(self.surface);
tok.release();
self.blur_token = null;
}
} else {
// Only acquire token when transitioning from not blurred -> blurred
if (blur.enabled()) {
const tok = try manager.create(self.surface);
tok.commit();
self.blur_token = tok;
}
}
}
fn syncDecoration(self: *Window) !void {
const deco = self.decoration orelse return;
// The protocol requests uint instead of enum so we have
// to convert it.
deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
}
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
return switch (self.apprt_window.config.window_decoration) {
.auto => self.app_context.default_deco_mode orelse .Client,
.client => .Client,
.server => .Server,
.none => .None,
};
}
fn syncQuickTerminal(self: *Window) !void {
const window = self.apprt_window.window.as(gtk.Window);
const config = &self.apprt_window.config;
layer_shell.setKeyboardMode(
window,
switch (config.quick_terminal_keyboard_interactivity) {
.none => .none,
.@"on-demand" => on_demand: {
if (layer_shell.getProtocolVersion() < 4) {
log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{});
break :on_demand .exclusive;
}
break :on_demand .on_demand;
},
.exclusive => .exclusive,
},
);
const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) {
.left => .left,
.right => .right,
.top => .top,
.bottom => .bottom,
.center => null,
};
for (std.meta.tags(layer_shell.ShellEdge)) |edge| {
if (anchored_edge) |anchored| {
if (edge == anchored) {
layer_shell.setMargin(window, edge, 0);
layer_shell.setAnchor(window, edge, true);
continue;
}
}
// Arbitrary margin - could be made customizable?
layer_shell.setMargin(window, edge, 20);
layer_shell.setAnchor(window, edge, false);
}
if (self.slide) |slide| slide.release();
self.slide = if (anchored_edge) |anchored| slide: {
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
const slide = mgr.create(self.surface) catch |err| {
log.warn("could not create slide object={}", .{err});
break :slide null;
};
const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
.top => .top,
.bottom => .bottom,
.left => .left,
.right => .right,
};
slide.setLocation(@intCast(@intFromEnum(slide_location)));
slide.commit();
break :slide slide;
} else null;
}
/// Update the size of the quick terminal based on monitor dimensions.
fn enteredMonitor(
_: *gdk.Surface,
monitor: *gdk.Monitor,
apprt_window: *ApprtWindow,
) callconv(.c) void {
const window = apprt_window.window.as(gtk.Window);
const config = &apprt_window.config;
var monitor_size: gdk.Rectangle = undefined;
monitor.getGeometry(&monitor_size);
const dims = config.quick_terminal_size.calculate(
config.quick_terminal_position,
.{
.width = @intCast(monitor_size.f_width),
.height = @intCast(monitor_size.f_height),
},
);
window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
}
fn onActivationTokenEvent(
token: *xdg.ActivationTokenV1,
event: xdg.ActivationTokenV1.Event,
self: *Window,
) void {
const activation = self.app_context.xdg_activation orelse return;
const current_token = self.activation_token orelse return;
if (token.getId() != current_token.getId()) {
log.warn("received event for unknown activation token; ignoring", .{});
return;
}
switch (event) {
.done => |done| {
activation.activate(done.token, self.surface);
token.destroy();
self.activation_token = null;
},
}
}
};

View File

@@ -1,507 +0,0 @@
//! X11 window protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gdk = @import("gdk");
const gdk_x11 = @import("gdk_x11");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const xlib = @import("xlib");
pub const c = @cImport({
@cInclude("X11/Xlib.h");
@cInclude("X11/Xatom.h");
@cInclude("X11/XKBlib.h");
});
const input = @import("../../../input.zig");
const Config = @import("../../../config.zig").Config;
const ApprtWindow = @import("../Window.zig");
const log = std.log.scoped(.gtk_x11);
pub const App = struct {
display: *xlib.Display,
base_event_code: c_int,
atoms: Atoms,
pub fn init(
_: Allocator,
gdk_display: *gdk.Display,
app_id: [:0]const u8,
config: *const Config,
) !?App {
// If the display isn't X11, then we don't need to do anything.
const gdk_x11_display = gobject.ext.cast(
gdk_x11.X11Display,
gdk_display,
) orelse return null;
const xlib_display = gdk_x11_display.getXdisplay();
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
pn
else if (builtin.mode == .Debug)
"ghostty-debug"
else
"ghostty";
// Set the X11 window class property (WM_CLASS) if are are on an X11
// display.
//
// Note that we also set the program name here using g_set_prgname.
// This is how the instance name field for WM_CLASS is derived when
// calling gdk_x11_display_set_program_class; there does not seem to be
// a way to set it directly. It does not look like this is being set by
// our other app initialization routines currently, but since we're
// currently deriving its value from x11-instance-name effectively, I
// feel like gating it behind an X11 check is better intent.
//
// This makes the property show up like so when using xprop:
//
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
//
// Append "-debug" on both when using the debug build.
glib.setPrgname(x11_program_name);
gdk_x11.X11Display.setProgramClass(gdk_display, app_id);
// XKB
log.debug("Xkb.init: initializing Xkb", .{});
log.debug("Xkb.init: running XkbQueryExtension", .{});
var opcode: c_int = 0;
var base_event_code: c_int = 0;
var base_error_code: c_int = 0;
var major = c.XkbMajorVersion;
var minor = c.XkbMinorVersion;
if (c.XkbQueryExtension(
@ptrCast(@alignCast(xlib_display)),
&opcode,
&base_event_code,
&base_error_code,
&major,
&minor,
) == 0) {
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
return error.XkbInitializationError;
}
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
if (c.XkbSelectEventDetails(
@ptrCast(@alignCast(xlib_display)),
c.XkbUseCoreKbd,
c.XkbStateNotify,
c.XkbModifierStateMask,
c.XkbModifierStateMask,
) == 0) {
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
return error.XkbInitializationError;
}
return .{
.display = xlib_display,
.base_event_code = base_event_code,
.atoms = .init(gdk_x11_display),
};
}
pub fn deinit(self: *App, alloc: Allocator) void {
_ = self;
_ = alloc;
}
/// Checks for an immediate pending XKB state update event, and returns the
/// keyboard state based on if it finds any. This is necessary as the
/// standard GTK X11 API (and X11 in general) does not include the current
/// key pressed in any modifier state snapshot for that event (e.g. if the
/// pressed key is a modifier, that is not necessarily reflected in the
/// modifiers).
///
/// Returns null if there is no event. In this case, the caller should fall
/// back to the standard GDK modifier state (this likely means the key
/// event did not result in a modifier change).
pub fn eventMods(
self: App,
device: ?*gdk.Device,
gtk_mods: gdk.ModifierType,
) ?input.Mods {
_ = device;
_ = gtk_mods;
// Shoutout to Mozilla for figuring out a clean way to do this, this is
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
if (c.XEventsQueued(
@ptrCast(@alignCast(self.display)),
c.QueuedAfterReading,
) == 0) return null;
var nextEvent: c.XEvent = undefined;
_ = c.XPeekEvent(@ptrCast(@alignCast(self.display)), &nextEvent);
if (nextEvent.type != self.base_event_code) return null;
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
// Check the state according to XKB masks.
const lookup_mods = xkb_state_notify_event.lookup_mods;
var mods: input.Mods = .{};
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
return mods;
}
pub fn supportsQuickTerminal(_: App) bool {
log.warn("quick terminal is not yet supported on X11", .{});
return false;
}
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
};
pub const Window = struct {
app: *App,
config: *const ApprtWindow.DerivedConfig,
gtk_window: *adw.ApplicationWindow,
x11_surface: *gdk_x11.X11Surface,
blur_region: Region = .{},
pub fn init(
alloc: Allocator,
app: *App,
apprt_window: *ApprtWindow,
) !Window {
_ = alloc;
const surface = apprt_window.window.as(
gtk.Native,
).getSurface() orelse return error.NotX11Surface;
const x11_surface = gobject.ext.cast(
gdk_x11.X11Surface,
surface,
) orelse return error.NotX11Surface;
return .{
.app = app,
.config = &apprt_window.config,
.gtk_window = apprt_window.window,
.x11_surface = x11_surface,
};
}
pub fn deinit(self: Window, alloc: Allocator) void {
_ = self;
_ = alloc;
}
pub fn resizeEvent(self: *Window) !void {
// The blur region must update with window resizes
try self.syncBlur();
}
pub fn syncAppearance(self: *Window) !void {
// The user could have toggled between CSDs and SSDs,
// therefore we need to recalculate the blur region offset.
self.blur_region = blur: {
// NOTE(pluiedev): CSDs are a f--king mistake.
// Please, GNOME, stop this nonsense of making a window ~30% bigger
// internally than how they really are just for your shadows and
// rounded corners and all that fluff. Please. I beg of you.
var x: f64 = 0;
var y: f64 = 0;
self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y);
// Transform surface coordinates to device coordinates.
const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor());
x *= scale;
y *= scale;
break :blur .{
.x = @intFromFloat(x),
.y = @intFromFloat(y),
};
};
self.syncBlur() catch |err| {
log.err("failed to synchronize blur={}", .{err});
};
self.syncDecorations() catch |err| {
log.err("failed to synchronize decorations={}", .{err});
};
}
pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self.config.window_decoration) {
.auto, .client => true,
.server, .none => false,
};
}
fn syncBlur(self: *Window) !void {
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
// which means that the blur region will grow slightly outside of the
// window borders. Unfortunately, actually calculating the rounded
// region can be quite complex without having access to existing APIs
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
// and I think it's not really noticeable enough to justify the effort.
// (Wayland also has this visual artifact anyway...)
const gtk_widget = self.gtk_window.as(gtk.Widget);
// Transform surface coordinates to device coordinates.
const scale = self.gtk_window.as(gtk.Widget).getScaleFactor();
self.blur_region.width = gtk_widget.getWidth() * scale;
self.blur_region.height = gtk_widget.getHeight() * scale;
const blur = self.config.background_blur;
log.debug("set blur={}, window xid={}, region={}", .{
blur,
self.x11_surface.getXid(),
self.blur_region,
});
if (blur.enabled()) {
try self.changeProperty(
Region,
self.app.atoms.kde_blur,
c.XA_CARDINAL,
._32,
.{ .mode = .replace },
&self.blur_region,
);
} else {
try self.deleteProperty(self.app.atoms.kde_blur);
}
}
fn syncDecorations(self: *Window) !void {
var hints: MotifWMHints = .{};
self.getWindowProperty(
MotifWMHints,
self.app.atoms.motif_wm_hints,
self.app.atoms.motif_wm_hints,
._32,
.{},
&hints,
) catch |err| switch (err) {
// motif_wm_hints is already initialized, so this is fine
error.PropertyNotFound => {},
error.RequestFailed,
error.PropertyTypeMismatch,
error.PropertyFormatMismatch,
=> return err,
};
hints.flags.decorations = true;
hints.decorations.all = switch (self.config.window_decoration) {
.server => true,
.auto, .client, .none => false,
};
try self.changeProperty(
MotifWMHints,
self.app.atoms.motif_wm_hints,
self.app.atoms.motif_wm_hints,
._32,
.{ .mode = .replace },
&hints,
);
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
var buf: [64]u8 = undefined;
const window_id = try std.fmt.bufPrint(
&buf,
"{}",
.{self.x11_surface.getXid()},
);
try env.put("WINDOWID", window_id);
}
pub fn setUrgent(self: *Window, urgent: bool) !void {
self.x11_surface.setUrgencyHint(@intFromBool(urgent));
}
fn getWindowProperty(
self: *Window,
comptime T: type,
name: c.Atom,
typ: c.Atom,
comptime format: PropertyFormat,
options: struct {
offset: c_long = 0,
length: c_long = std.math.maxInt(c_long),
delete: bool = false,
},
result: *T,
) GetWindowPropertyError!void {
// FIXME: Maybe we should switch to libxcb one day.
// Sounds like a much better idea than whatever this is
var actual_type_return: c.Atom = undefined;
var actual_format_return: c_int = undefined;
var nitems_return: c_ulong = undefined;
var bytes_after_return: c_ulong = undefined;
var prop_return: ?format.bufferType() = null;
const code = c.XGetWindowProperty(
@ptrCast(@alignCast(self.app.display)),
self.x11_surface.getXid(),
name,
options.offset,
options.length,
@intFromBool(options.delete),
typ,
&actual_type_return,
&actual_format_return,
&nitems_return,
&bytes_after_return,
@ptrCast(&prop_return),
);
if (code != c.Success) return error.RequestFailed;
if (actual_type_return == c.None) return error.PropertyNotFound;
if (typ != actual_type_return) return error.PropertyTypeMismatch;
if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
const data_ptr: *T = @ptrCast(prop_return);
result.* = data_ptr.*;
_ = c.XFree(prop_return);
}
fn changeProperty(
self: *Window,
comptime T: type,
name: c.Atom,
typ: c.Atom,
comptime format: PropertyFormat,
options: struct {
mode: PropertyChangeMode,
},
value: *T,
) X11Error!void {
const data: format.bufferType() = @ptrCast(value);
const status = c.XChangeProperty(
@ptrCast(@alignCast(self.app.display)),
self.x11_surface.getXid(),
name,
typ,
@intFromEnum(format),
@intFromEnum(options.mode),
data,
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
);
// For some godforsaken reason Xlib alternates between
// error values (0 = success) and booleans (1 = success), and they look exactly
// the same in the signature (just `int`, since Xlib is written in C89)...
if (status == 0) return error.RequestFailed;
}
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
const status = c.XDeleteProperty(
@ptrCast(@alignCast(self.app.display)),
self.x11_surface.getXid(),
name,
);
if (status == 0) return error.RequestFailed;
}
};
const X11Error = error{
RequestFailed,
};
const GetWindowPropertyError = X11Error || error{
PropertyNotFound,
PropertyTypeMismatch,
PropertyFormatMismatch,
};
const Atoms = struct {
kde_blur: c.Atom,
motif_wm_hints: c.Atom,
fn init(display: *gdk_x11.X11Display) Atoms {
return .{
.kde_blur = gdk_x11.x11GetXatomByNameForDisplay(
display,
"_KDE_NET_WM_BLUR_BEHIND_REGION",
),
.motif_wm_hints = gdk_x11.x11GetXatomByNameForDisplay(
display,
"_MOTIF_WM_HINTS",
),
};
}
};
const PropertyChangeMode = enum(c_int) {
replace = c.PropModeReplace,
prepend = c.PropModePrepend,
append = c.PropModeAppend,
};
const PropertyFormat = enum(c_int) {
_8 = 8,
_16 = 16,
_32 = 32,
fn elemType(comptime self: PropertyFormat) type {
return switch (self) {
._8 => c_char,
._16 => c_int,
._32 => c_long,
};
}
fn bufferType(comptime self: PropertyFormat) type {
// The buffer type has to be a multi-pointer to bytes
// *aligned to the element type* (very important,
// otherwise you'll read garbage!)
//
// I know this is really ugly. X11 is ugly. I consider it apropos.
return [*]align(@alignOf(self.elemType())) u8;
}
};
const Region = extern struct {
x: c_long = 0,
y: c_long = 0,
width: c_long = 0,
height: c_long = 0,
};
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
const MotifWMHints = extern struct {
flags: packed struct(c_ulong) {
_pad: u1 = 0,
decorations: bool = false,
// We don't really care about the other flags
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
} = .{},
functions: c_ulong = 0,
decorations: packed struct(c_ulong) {
all: bool = false,
// We don't really care about the other flags
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
} = .{},
input_mode: c_long = 0,
status: c_ulong = 0,
};

View File

@@ -39,13 +39,13 @@ pub const Clipboard = enum(Backing) {
// Our backing isn't is as small as we can in Zig, but a full
// C int if we're binding to C APIs.
const Backing = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => c_int,
.@"gtk-ng" => c_int,
else => u2,
};
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
.@"gtk-ng" => @import("gobject").ext.defineEnum(
Clipboard,
.{ .name = "GhosttyApprtClipboard" },
),
@@ -74,7 +74,7 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
ClipboardRequest,
.{ .name = "GhosttyClipboardRequest" },
),

View File

@@ -111,7 +111,6 @@ pub const Message = union(enum) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk,
.@"gtk-ng",
=> @import("gobject").ext.defineBoxed(
ChildExited,

View File

@@ -20,11 +20,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
// Get the resources we're going to inject into the source tarball.
const alloc = b.allocator;
var resources: std.ArrayListUnmanaged(Resource) = .empty;
{
const gtk = SharedDeps.gtkDistResources(b);
try resources.append(alloc, gtk.resources_c);
try resources.append(alloc, gtk.resources_h);
}
{
const gtk = SharedDeps.gtkNgDistResources(b);
try resources.append(alloc, gtk.resources_c);

View File

@@ -3,7 +3,7 @@ const GhosttyI18n = @This();
const std = @import("std");
const builtin = @import("builtin");
const Config = @import("Config.zig");
const gresource = @import("../apprt/gtk/gresource.zig");
const gresource = @import("../apprt/gtk-ng/build/gresource.zig");
const internal_os = @import("../os/main.zig");
const domain = "com.mitchellh.ghostty";
@@ -78,9 +78,9 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
// Not cacheable due to the gresource files
xgettext.has_side_effects = true;
inline for (gresource.blueprint_files) |blp| {
inline for (gresource.blueprints) |blp| {
const path = std.fmt.comptimePrint(
"src/apprt/gtk/ui/{[major]}.{[minor]}/{[name]s}.blp",
"src/apprt/gtk-ng/ui/{[major]}.{[minor]}/{[name]s}.blp",
blp,
);
// The arguments to xgettext must be the relative path in the build root
@@ -105,7 +105,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
}
var gtk_dir = try b.build_root.handle.openDir(
"src/apprt/gtk",
"src/apprt/gtk-ng",
.{ .iterate = true },
);
defer gtk_dir.close();
@@ -138,7 +138,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
);
for (gtk_files.items) |item| {
const path = b.pathJoin(&.{ "src/apprt/gtk", item });
const path = b.pathJoin(&.{ "src/apprt/gtk-ng", item });
// The arguments to xgettext must be the relative path in the build root
// or the resulting files will contain the absolute path. This will
// cause a lot of churn because not everyone has the Ghostty code

View File

@@ -550,7 +550,6 @@ pub fn add(
switch (self.config.app_runtime) {
.none => {},
.gtk => try self.addGTK(step),
.@"gtk-ng" => try self.addGtkNg(step),
}
}
@@ -789,234 +788,6 @@ pub fn gtkNgDistResources(
};
}
/// Setup the dependencies for the GTK apprt build. The GTK apprt
/// is particularly involved compared to others so we pull this out
/// into a dedicated function.
fn addGTK(
self: *const SharedDeps,
step: *std.Build.Step.Compile,
) !void {
const b = step.step.owner;
const target = step.root_module.resolved_target.?;
const optimize = step.root_module.optimize.?;
const gobject_ = b.lazyDependency("gobject", .{
.target = target,
.optimize = optimize,
});
if (gobject_) |gobject| {
const gobject_imports = .{
.{ "adw", "adw1" },
.{ "gdk", "gdk4" },
.{ "gio", "gio2" },
.{ "glib", "glib2" },
.{ "gobject", "gobject2" },
.{ "gtk", "gtk4" },
.{ "xlib", "xlib2" },
};
inline for (gobject_imports) |import| {
const name, const module = import;
step.root_module.addImport(name, gobject.module(module));
}
}
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
if (self.config.x11) {
step.linkSystemLibrary2("X11", dynamic_link_opts);
if (gobject_) |gobject| {
step.root_module.addImport(
"gdk_x11",
gobject.module("gdkx114"),
);
}
}
if (self.config.wayland) wayland: {
// These need to be all be called to note that we need them.
const wayland_dep_ = b.lazyDependency("wayland", .{});
const wayland_protocols_dep_ = b.lazyDependency(
"wayland_protocols",
.{},
);
const plasma_wayland_protocols_dep_ = b.lazyDependency(
"plasma_wayland_protocols",
.{},
);
// Unwrap or return, there are no more dependencies below.
const wayland_dep = wayland_dep_ orelse break :wayland;
const wayland_protocols_dep = wayland_protocols_dep_ orelse break :wayland;
const plasma_wayland_protocols_dep = plasma_wayland_protocols_dep_ orelse break :wayland;
// Note that zig_wayland cannot be lazy because lazy dependencies
// can't be imported since they don't exist and imports are
// resolved at compile time of the build.
const zig_wayland_dep = b.dependency("zig_wayland", .{});
const Scanner = @import("zig_wayland").Scanner;
const scanner = Scanner.create(zig_wayland_dep.builder, .{
.wayland_xml = wayland_dep.path("protocol/wayland.xml"),
.wayland_protocols = wayland_protocols_dep.path(""),
});
scanner.addCustomProtocol(
plasma_wayland_protocols_dep.path("src/protocols/blur.xml"),
);
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
scanner.addCustomProtocol(
plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"),
);
scanner.addCustomProtocol(
plasma_wayland_protocols_dep.path("src/protocols/slide.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("xdg_activation_v1", 1);
step.root_module.addImport("wayland", b.createModule(.{
.root_source_file = scanner.result,
}));
if (gobject_) |gobject| step.root_module.addImport(
"gdk_wayland",
gobject.module("gdkwayland4"),
);
if (b.lazyDependency("gtk4_layer_shell", .{
.target = target,
.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"),
);
step.root_module.addImport(
"gtk4-layer-shell",
layer_shell_module,
);
// IMPORTANT: gtk4-layer-shell must be linked BEFORE
// wayland-client, as it relies on shimming libwayland's APIs.
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
} else {
// gtk4-layer-shell *must* be dynamically linked,
// so we don't add it as a static library
const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
b.installArtifact(shared_lib);
step.linkLibrary(shared_lib);
}
}
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
}
{
// Get our gresource c/h files and add them to our build.
const dist = gtkDistResources(b);
step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
step.addIncludePath(dist.resources_h.path(b).dirname());
}
}
/// Creates the resources that can be prebuilt for our dist build.
pub fn gtkDistResources(
b: *std.Build,
) struct {
resources_c: DistResource,
resources_h: DistResource,
} {
const gresource = @import("../apprt/gtk/gresource.zig");
const gresource_xml = gresource_xml: {
const xml_exe = b.addExecutable(.{
.name = "generate_gresource_xml",
.root_source_file = b.path("src/apprt/gtk/gresource.zig"),
.target = b.graph.host,
});
const xml_run = b.addRunArtifact(xml_exe);
const blueprint_exe = b.addExecutable(.{
.name = "gtk_blueprint_compiler",
.root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"),
.target = b.graph.host,
});
blueprint_exe.linkLibC();
blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts);
blueprint_exe.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
for (gresource.blueprint_files) |blueprint_file| {
const blueprint_run = b.addRunArtifact(blueprint_exe);
blueprint_run.addArgs(&.{
b.fmt("{d}", .{blueprint_file.major}),
b.fmt("{d}", .{blueprint_file.minor}),
});
const ui_file = blueprint_run.addOutputFileArg(b.fmt(
"{d}.{d}/{s}.ui",
.{
blueprint_file.major,
blueprint_file.minor,
blueprint_file.name,
},
));
blueprint_run.addFileArg(b.path(b.fmt(
"src/apprt/gtk/ui/{d}.{d}/{s}.blp",
.{
blueprint_file.major,
blueprint_file.minor,
blueprint_file.name,
},
)));
xml_run.addFileArg(ui_file);
}
break :gresource_xml xml_run.captureStdOut();
};
const generate_c = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-source",
"--target",
});
const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
generate_c.addFileArg(gresource_xml);
for (gresource.dependencies) |file| {
generate_c.addFileInput(b.path(file));
}
const generate_h = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-header",
"--target",
});
const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
generate_h.addFileArg(gresource_xml);
for (gresource.dependencies) |file| {
generate_h.addFileInput(b.path(file));
}
return .{
.resources_c = .{
.dist = "src/apprt/gtk/ghostty_resources.c",
.generated = resources_c,
},
.resources_h = .{
.dist = "src/apprt/gtk/ghostty_resources.h",
.generated = resources_h,
},
};
}
// For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library
// before falling back to static.

View File

@@ -7,8 +7,8 @@ const internal_os = @import("../os/main.zig");
const xev = @import("../global.zig").xev;
const renderer = @import("../renderer.zig");
const gtk_version = @import("../apprt/gtk/gtk_version.zig");
const adw_version = @import("../apprt/gtk/adw_version.zig");
const gtk_version = @import("../apprt/gtk-ng/gtk_version.zig");
const adw_version = @import("../apprt/gtk-ng/adw_version.zig");
pub const Options = struct {};
@@ -38,7 +38,7 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print(" - font engine : {}\n", .{build_config.font_backend});
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
if (comptime build_config.app_runtime == .gtk) {
if (comptime build_config.app_runtime == .@"gtk-ng") {
if (comptime builtin.os.tag == .linux) {
const kernel_info = internal_os.getKernelInfo(alloc);
defer if (kernel_info) |k| alloc.free(k);

View File

@@ -7149,7 +7149,7 @@ pub const GtkTitlebarStyle = enum(c_int) {
tabs,
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
.@"gtk-ng" => @import("gobject").ext.defineEnum(
GtkTitlebarStyle,
.{ .name = "GhosttyGtkTitlebarStyle" },
),
@@ -7717,7 +7717,7 @@ pub const WindowDecoration = enum(c_int) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
.@"gtk-ng" => @import("gobject").ext.defineEnum(
WindowDecoration,
.{ .name = "GhosttyConfigWindowDecoration" },
),

View File

@@ -1266,7 +1266,7 @@ pub fn SplitTree(comptime V: type) type {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
Self,
.{
// To get the type name we get the non-qualified type name

View File

@@ -59,7 +59,7 @@ pub const DesiredSize = struct {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
DesiredSize,
.{ .name = "GhosttyFontDesiredSize" },
),

View File

@@ -744,7 +744,7 @@ pub const Action = union(enum) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
Action,
.{ .name = "GhosttyBindingAction" },
),

View File

@@ -165,7 +165,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
else => @compileError("unsupported app runtime for OpenGL"),
// GTK uses global OpenGL context so we load from null.
apprt.gtk,
apprt.gtk_ng,
=> try prepareContext(null),
@@ -201,7 +200,7 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk, apprt.gtk_ng => {
apprt.gtk_ng => {
// GTK doesn't support threaded OpenGL operations as far as I can
// tell, so we use the renderer thread to setup all the state
// but then do the actual draws and texture syncs and all that
@@ -223,7 +222,7 @@ pub fn threadExit(self: *const OpenGL) void {
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk, apprt.gtk_ng => {
apprt.gtk_ng => {
// We don't need to do any unloading for GTK because we may
// be sharing the global bindings with other windows.
},
@@ -238,7 +237,7 @@ pub fn displayRealized(self: *const OpenGL) void {
_ = self;
switch (apprt.runtime) {
apprt.gtk, apprt.gtk_ng => prepareContext(null) catch |err| {
apprt.gtk_ng => prepareContext(null) catch |err| {
log.warn(
"Error preparing GL context in displayRealized, err={}",
.{err},

View File

@@ -49,7 +49,7 @@ pub const MouseShape = enum(c_int) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
.@"gtk-ng" => @import("gobject").ext.defineEnum(
MouseShape,
.{ .name = "GhosttyMouseShape" },
),