ghostty/src/apprt/gtk/App.zig

1887 lines
59 KiB
Zig

/// App is the entrypoint for the application. This is called after all
/// of the runtime-agnostic initialization is complete and we're ready
/// to start.
///
/// There is only ever one App instance per process. This is because most
/// application frameworks also have this restriction so it simplifies
/// the assumptions.
///
/// In GTK, the App contains the primary GApplication and GMainContext
/// (event loop) along with any global app state.
const App = @This();
const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../../build_config.zig");
const xev = @import("../../global.zig").xev;
const build_options = @import("build_options");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const input = @import("../../input.zig");
const internal_os = @import("../../os/main.zig");
const systemd = @import("../../os/systemd.zig");
const terminal = @import("../../terminal/main.zig");
const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig");
const ipc = @import("ipc.zig");
const cgroup = @import("cgroup.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
const ConfigErrorsDialog = @import("ConfigErrorsDialog.zig");
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const CloseDialog = @import("CloseDialog.zig");
const GlobalShortcuts = @import("GlobalShortcuts.zig");
const Split = @import("Split.zig");
const OpenURI = @import("portal.zig").OpenURI;
const inspector = @import("inspector.zig");
const key = @import("key.zig");
const winprotopkg = @import("winproto.zig");
const gtk_version = @import("gtk_version.zig");
const adw_version = @import("adw_version.zig");
pub const c = @cImport({
// generated header files
@cInclude("ghostty_resources.h");
});
const log = std.log.scoped(.gtk);
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
/// message so that we can call `drawFrame` ourselves from the app thread,
/// because GTK's `GLArea` does not support drawing from a different thread.
pub const must_draw_from_app_thread = true;
pub const Options = struct {};
core_app: *CoreApp,
config: Config,
app: *adw.Application,
ctx: *glib.MainContext,
/// State and logic for the underlying windowing protocol.
winproto: winprotopkg.App,
/// True if the app was launched with single instance mode.
single_instance: bool,
/// The "none" cursor. We use one that is shared across the entire app.
cursor_none: ?*gdk.Cursor,
/// The clipboard confirmation window, if it is currently open.
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
/// The config errors dialog, if it is currently open.
config_errors_dialog: ?ConfigErrorsDialog = null,
/// The window containing the quick terminal.
/// Null when never initialized.
quick_terminal: ?*Window = null,
/// This is set to false when the main loop should exit.
running: bool = true,
/// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled
/// and initialization was successful.
transient_cgroup_base: ?[]const u8 = null,
/// CSS Provider for any styles based on ghostty configuration values
css_provider: *gtk.CssProvider,
/// Providers for loading custom stylesheets defined by user
custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .{},
global_shortcuts: ?GlobalShortcuts,
open_uri: OpenURI = undefined,
/// The timer used to quit the application after the last window is closed.
quit_timer: union(enum) {
off: void,
active: c_uint,
expired: void,
} = .{ .off = {} },
pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
_ = opts;
// Log our GTK version
gtk_version.logVersion();
// log the adwaita version
adw_version.logVersion();
// Set gettext global domain to be our app so that our unqualified
// translations map to our translations.
try internal_os.i18n.initGlobalDomain();
// Load our configuration
var config = try Config.load(core_app.alloc);
errdefer config.deinit();
// If we had configuration errors, then log them.
if (!config._diagnostics.empty()) {
var buf = std.ArrayList(u8).init(core_app.alloc);
defer buf.deinit();
for (config._diagnostics.items()) |diag| {
try diag.write(buf.writer());
log.warn("configuration error: {s}", .{buf.items});
buf.clearRetainingCapacity();
}
// If we have any CLI errors, exit.
if (config._diagnostics.containsLocation(.cli)) {
log.warn("CLI errors detected, exiting", .{});
std.posix.exit(1);
}
}
// Setup our event loop backend
if (config.@"async-backend" != .auto) {
const result: bool = switch (config.@"async-backend") {
.auto => unreachable,
.epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
.io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
};
if (result) {
log.info(
"libxev manual backend={s}",
.{@tagName(xev.backend)},
);
} else {
log.warn(
"libxev manual backend failed, using default={s}",
.{@tagName(xev.backend)},
);
}
}
var gdk_debug: struct {
/// output OpenGL debug information
opengl: bool = false,
/// disable GLES, Ghostty can't use GLES
@"gl-disable-gles": bool = false,
// GTK's new renderer can cause blurry font when using fractional scaling.
@"gl-no-fractional": bool = false,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
@"vulkan-disable": bool = false,
} = .{
.opengl = config.@"gtk-opengl-debug",
};
var gdk_disable: struct {
@"gles-api": bool = false,
/// current gtk implementation for color management is not good enough.
/// see: https://bugs.kde.org/show_bug.cgi?id=495647
/// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
@"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
} = .{};
environment: {
if (gtk_version.runtimeAtLeast(4, 18, 0)) {
gdk_disable.@"color-mgmt" = false;
}
if (gtk_version.runtimeAtLeast(4, 16, 0)) {
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
// For the remainder of "why" see the 4.14 comment below.
gdk_disable.@"gles-api" = true;
gdk_disable.vulkan = true;
break :environment;
}
if (gtk_version.runtimeAtLeast(4, 14, 0)) {
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
// Older versions of GTK do not support these values so it is safe
// to always set this. Forwards versions are uncertain so we'll have
// to reassess...
//
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
gdk_debug.@"gl-disable-gles" = true;
gdk_debug.@"vulkan-disable" = true;
if (gtk_version.runtimeUntil(4, 17, 5)) {
// Removed at GTK v4.17.5
gdk_debug.@"gl-no-fractional" = true;
}
break :environment;
}
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
// is an environment that isn't tested well and we don't have a
// good understanding of what we may need to do.
gdk_debug.@"vulkan-disable" = true;
}
{
var buf: [128]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| {
if (@field(gdk_debug, field.name)) {
if (!first) try writer.writeAll(",");
try writer.writeAll(field.name);
first = false;
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
}
{
var buf: [128]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| {
if (@field(gdk_disable, field.name)) {
if (!first) try writer.writeAll(",");
try writer.writeAll(field.name);
first = false;
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
}
adw.init();
const display: *gdk.Display = gdk.Display.getDefault() orelse {
// I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here.
log.warn("gdk display is null, exiting", .{});
std.posix.exit(1);
};
// The "none" cursor is used for hiding the cursor
const cursor_none = gdk.Cursor.newFromName("none", null);
errdefer if (cursor_none) |cursor| cursor.unref();
const single_instance = switch (config.@"gtk-single-instance") {
.true => true,
.false => false,
.desktop => switch (config.@"launched-from".?) {
.desktop, .systemd, .dbus => true,
.cli => false,
},
};
// Setup the flags for our application.
const app_flags: gio.ApplicationFlags = app_flags: {
var flags: gio.ApplicationFlags = .flags_default_flags;
if (!single_instance) flags.non_unique = true;
break :app_flags flags;
};
// Our app ID determines uniqueness and maps to our desktop file.
// We append "-debug" to the ID if we're in debug mode so that we
// can develop Ghostty in Ghostty.
const app_id: [:0]const u8 = app_id: {
if (config.class) |class| {
if (gio.Application.idIsValid(class) != 0) {
break :app_id class;
} else {
log.warn("invalid 'class' in config, ignoring", .{});
}
}
const default_id = comptime build_config.bundle_id;
break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id;
};
// Create our GTK Application which encapsulates our process.
log.debug("creating GTK application id={s} single-instance={}", .{
app_id,
single_instance,
});
// Using an AdwApplication lets us use Adwaita widgets and access things
// such as the color scheme.
const adw_app = adw.Application.new(
app_id.ptr,
app_flags,
);
errdefer adw_app.unref();
const style_manager = adw_app.getStyleManager();
style_manager.setColorScheme(
switch (config.@"window-theme") {
.auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5)
.prefer_light
else
.prefer_dark;
},
.system => .prefer_light,
.dark => .force_dark,
.light => .force_light,
},
);
const gio_app = adw_app.as(gio.Application);
// force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources
gio_app.setResourceBasePath("/com/mitchellh/ghostty");
gio.resourcesRegister(@ptrCast(@alignCast(c.ghostty_get_resource() orelse {
log.err("unable to load resources", .{});
return error.GtkNoResources;
})));
// The `activate` signal is used when Ghostty is first launched and when a
// secondary Ghostty is launched and requests a new window.
_ = gio.Application.signals.activate.connect(
adw_app,
*CoreApp,
gtkActivate,
core_app,
.{},
);
// Other signals
_ = gtk.Application.signals.window_added.connect(
adw_app,
*CoreApp,
gtkWindowAdded,
core_app,
.{},
);
_ = gtk.Application.signals.window_removed.connect(
adw_app,
*CoreApp,
gtkWindowRemoved,
core_app,
.{},
);
// Setup a listener for SIGUSR2 to reload the configuration.
_ = glib.unixSignalAdd(
std.posix.SIG.USR2,
sigusr2,
self,
);
// We don't use g_application_run, we want to manually control the
// loop so we have to do the same things the run function does:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
const ctx = glib.MainContext.default();
if (glib.MainContext.acquire(ctx) == 0) return error.GtkContextAcquireFailed;
errdefer glib.MainContext.release(ctx);
var err_: ?*glib.Error = null;
if (gio_app.register(
null,
&err_,
) == 0) {
if (err_) |err| {
log.warn("error registering application: {s}", .{err.f_message orelse "(unknown)"});
err.free();
}
return error.GtkApplicationRegisterFailed;
}
// Setup our windowing protocol logic
var winproto_app = try winprotopkg.App.init(
core_app.alloc,
display,
app_id,
&config,
);
errdefer winproto_app.deinit(core_app.alloc);
log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
// This just calls the `activate` signal but its part of the normal startup
// routine so we just call it, but only if the config allows it (this allows
// for launching Ghostty in the "background" without immediately opening
// a window). An initial window will not be immediately created if we were
// launched by D-Bus activation or systemd. D-Bus activation will send it's
// own `activate` or `new-window` signal later.
//
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
if (config.@"initial-window") switch (config.@"launched-from".?) {
.desktop, .cli => gio_app.activate(),
.dbus, .systemd => {},
};
// Internally, GTK ensures that only one instance of this provider exists in the provider list
// for the display.
const css_provider = gtk.CssProvider.new();
gtk.StyleContext.addProviderForDisplay(
display,
css_provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
);
self.* = .{
.core_app = core_app,
.app = adw_app,
.config = config,
.ctx = ctx,
.cursor_none = cursor_none,
.winproto = winproto_app,
.single_instance = single_instance,
// If we are NOT the primary instance, then we never want to run.
// This means that another instance of the GTK app is running and
// our "activate" call above will open a window.
.running = gio_app.getIsRemote() == 0,
.css_provider = css_provider,
.global_shortcuts = .init(core_app.alloc, gio_app),
};
try self.open_uri.init(self);
}
// Terminate the application. The application will not be restarted after
// this so all global state can be cleaned up.
pub fn terminate(self: *App) void {
gio.Settings.sync();
while (glib.MainContext.iteration(self.ctx, 0) != 0) {}
glib.MainContext.release(self.ctx);
self.app.unref();
if (self.cursor_none) |cursor| cursor.unref();
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
for (self.custom_css_providers.items) |provider| {
provider.unref();
}
self.custom_css_providers.deinit(self.core_app.alloc);
self.winproto.deinit(self.core_app.alloc);
if (self.global_shortcuts) |*shortcuts| shortcuts.deinit();
self.config.deinit();
self.open_uri.deinit();
}
/// Perform a given action. Returns `true` if the action was able to be
/// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) !bool {
switch (action) {
.quit => self.quit(),
.new_window => _ = try self.newWindow(switch (target) {
.app => null,
.surface => |v| v,
}),
.close_window => return try self.closeWindow(target),
.toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value),
.new_tab => try self.newTab(target),
.close_tab => return try self.closeTab(target),
.goto_tab => return self.gotoTab(target, value),
.move_tab => self.moveTab(target, value),
.new_split => try self.newSplit(target, value),
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
.goto_split => return self.gotoSplit(target, value),
.open_config => return self.openConfig(),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value),
.show_gtk_inspector => self.showGTKInspector(),
.desktop_notification => self.showDesktopNotification(target, value),
.set_title => try self.setTitle(target, value),
.pwd => try self.setPwd(target, value),
.present_terminal => self.presentTerminal(target),
.initial_size => try self.setInitialSize(target, value),
.size_limit => try self.setSizeLimit(target, value),
.mouse_visibility => self.setMouseVisibility(target, value),
.mouse_shape => try self.setMouseShape(target, value),
.mouse_over_link => self.setMouseOverLink(target, value),
.toggle_tab_overview => self.toggleTabOverview(target),
.toggle_split_zoom => self.toggleSplitZoom(target),
.toggle_window_decorations => self.toggleWindowDecorations(target),
.quit_timer => self.quitTimer(value),
.prompt_title => try self.promptTitle(target),
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
.secure_input => self.setSecureInput(target, value),
.ring_bell => try self.ringBell(target),
.toggle_command_palette => try self.toggleCommandPalette(target),
.open_url => self.openUrl(value),
.show_child_exited => return try self.showChildExited(target, value),
// Unimplemented
.close_all_windows,
.float_window,
.toggle_visibility,
.cell_size,
.key_sequence,
.render_inspector,
.renderer_health,
.color_change,
.reset_window_size,
.check_for_updates,
.undo,
.redo,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
}
// We can assume it was handled because all unknown/unimplemented actions
// are caught above.
return true;
}
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
/// able to be performed, `false` otherwise.
///
/// Note that this is a static function. Since this is called from a CLI app (or
/// some other process that is not Ghostty) there is no full-featured apprt App
/// to use.
pub fn performIpc(
alloc: Allocator,
target: apprt.ipc.Target,
comptime action: apprt.ipc.Action.Key,
value: apprt.ipc.Action.Value(action),
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
switch (action) {
.new_window => return try ipc.openNewWindow(alloc, target, value),
}
}
fn newTab(_: *App, target: apprt.Target) !void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"new_tab invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
try window.newTab(v);
},
}
}
fn closeTab(_: *App, target: apprt.Target) !bool {
switch (target) {
.app => return false,
.surface => |v| {
const tab = v.rt_surface.container.tab() orelse {
log.info(
"close_tab invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return false;
};
tab.closeWithConfirmation();
return true;
},
}
}
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) bool {
switch (target) {
.app => return false,
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"gotoTab invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return false;
};
return switch (tab) {
.previous => window.gotoPreviousTab(v.rt_surface),
.next => window.gotoNextTab(v.rt_surface),
.last => window.gotoLastTab(),
else => window.gotoTab(@intCast(@intFromEnum(tab))),
};
},
}
}
fn moveTab(_: *App, target: apprt.Target, move_tab: apprt.action.MoveTab) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"moveTab invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.moveTab(v.rt_surface, @intCast(move_tab.amount));
},
}
}
fn newSplit(
self: *App,
target: apprt.Target,
direction: apprt.action.SplitDirection,
) !void {
switch (target) {
.app => {},
.surface => |v| {
const alloc = self.core_app.alloc;
_ = try Split.create(alloc, v.rt_surface, direction);
},
}
}
fn equalizeSplits(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| {
const tab = v.rt_surface.container.tab() orelse return;
const top_split = switch (tab.elem) {
.split => |s| s,
else => return,
};
_ = top_split.equalize();
},
}
}
fn gotoSplit(
_: *const App,
target: apprt.Target,
direction: apprt.action.GotoSplit,
) bool {
switch (target) {
.app => return false,
.surface => |v| {
const s = v.rt_surface.container.split() orelse return false;
const map = s.directionMap(switch (v.rt_surface.container) {
.split_tl => .top_left,
.split_br => .bottom_right,
.none, .tab_ => unreachable,
});
const surface_ = map.get(direction) orelse return false;
if (surface_) |surface| {
surface.grabFocus();
return true;
}
return false;
},
}
}
fn resizeSplit(
_: *const App,
target: apprt.Target,
resize: apprt.action.ResizeSplit,
) void {
switch (target) {
.app => {},
.surface => |v| {
const s = v.rt_surface.container.firstSplitWithOrientation(
Split.Orientation.fromResizeDirection(resize.direction),
) orelse return;
s.moveDivider(resize.direction, resize.amount);
},
}
}
fn presentTerminal(
_: *const App,
target: apprt.Target,
) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.present(),
}
}
fn controlInspector(
_: *const App,
target: apprt.Target,
mode: apprt.action.Inspector,
) void {
const surface: *Surface = switch (target) {
.app => return,
.surface => |v| v.rt_surface,
};
surface.controlInspector(mode);
}
fn showGTKInspector(
_: *const App,
) void {
gtk.Window.setInteractiveDebugging(@intFromBool(true));
}
fn toggleMaximize(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"toggleMaximize invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.toggleMaximize();
},
}
}
fn toggleFullscreen(
_: *App,
target: apprt.Target,
_: apprt.action.Fullscreen,
) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"toggleFullscreen invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.toggleFullscreen();
},
}
}
fn toggleTabOverview(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"toggleTabOverview invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.toggleTabOverview();
},
}
}
fn toggleSplitZoom(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |surface| surface.rt_surface.toggleSplitZoom(),
}
}
fn toggleWindowDecorations(
_: *App,
target: apprt.Target,
) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"toggleWindowDecorations invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.toggleWindowDecorations();
},
}
}
fn toggleQuickTerminal(self: *App) !bool {
if (self.quick_terminal) |qt| {
qt.toggleVisibility();
return true;
}
if (!self.winproto.supportsQuickTerminal()) return false;
const qt = Window.create(self.core_app.alloc, self) catch |err| {
log.err("failed to initialize quick terminal={}", .{err});
return true;
};
self.quick_terminal = qt;
// The setup has to happen *before* the window-specific winproto is
// initialized, so we need to initialize it through the app winproto
try self.winproto.initQuickTerminal(qt);
// Finalize creating the quick terminal
try qt.newTab(null);
qt.present();
return true;
}
fn ringBell(_: *App, target: apprt.Target) !void {
switch (target) {
.app => {},
.surface => |surface| try surface.rt_surface.ringBell(),
}
}
fn toggleCommandPalette(_: *App, target: apprt.Target) !void {
switch (target) {
.app => {},
.surface => |surface| {
const window = surface.rt_surface.container.window() orelse {
log.info(
"toggleCommandPalette invalid for container={s}",
.{@tagName(surface.rt_surface.container)},
);
return;
};
window.toggleCommandPalette();
},
}
}
fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) error{}!bool {
switch (target) {
.app => return false,
.surface => |surface| return try surface.rt_surface.showChildExited(value),
}
}
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
switch (mode) {
.start => self.startQuitTimer(),
.stop => self.stopQuitTimer(),
}
}
fn promptTitle(_: *App, target: apprt.Target) !void {
switch (target) {
.app => {},
.surface => |v| {
try v.rt_surface.promptTitle();
},
}
}
fn setTitle(
_: *App,
target: apprt.Target,
title: apprt.action.SetTitle,
) !void {
switch (target) {
.app => {},
.surface => |v| try v.rt_surface.setTitle(title.title, .terminal),
}
}
fn setPwd(
_: *App,
target: apprt.Target,
pwd: apprt.action.Pwd,
) !void {
switch (target) {
.app => {},
.surface => |v| try v.rt_surface.setPwd(pwd.pwd),
}
}
fn setMouseVisibility(
_: *App,
target: apprt.Target,
visibility: apprt.action.MouseVisibility,
) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.setMouseVisibility(switch (visibility) {
.visible => true,
.hidden => false,
}),
}
}
fn setMouseShape(
_: *App,
target: apprt.Target,
shape: terminal.MouseShape,
) !void {
switch (target) {
.app => {},
.surface => |v| try v.rt_surface.setMouseShape(shape),
}
}
fn setMouseOverLink(
_: *App,
target: apprt.Target,
value: apprt.action.MouseOverLink,
) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.mouseOverLink(if (value.url.len > 0)
value.url
else
null),
}
}
fn setInitialSize(
_: *App,
target: apprt.Target,
value: apprt.action.InitialSize,
) !void {
switch (target) {
.app => {},
.surface => |v| try v.rt_surface.setInitialWindowSize(
value.width,
value.height,
),
}
}
fn setSizeLimit(
_: *App,
target: apprt.Target,
value: apprt.action.SizeLimit,
) !void {
switch (target) {
.app => {},
.surface => |v| try v.rt_surface.setSizeLimits(.{
.width = value.min_width,
.height = value.min_height,
}, if (value.max_width > 0) .{
.width = value.max_width,
.height = value.max_height,
} else null),
}
}
fn showDesktopNotification(
self: *App,
target: apprt.Target,
n: apprt.action.DesktopNotification,
) void {
// Set a default title if we don't already have one
const t = switch (n.title.len) {
0 => "Ghostty",
else => n.title,
};
const notification = gio.Notification.new(t);
defer notification.unref();
notification.setBody(n.body);
const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
defer icon.unref();
notification.setIcon(icon.as(gio.Icon));
const pointer = glib.Variant.newUint64(switch (target) {
.app => 0,
.surface => |v| @intFromPtr(v),
});
notification.setDefaultActionAndTargetValue("app.present-surface", pointer);
const gio_app = self.app.as(gio.Application);
// We set the notification ID to the body content. If the content is the
// same, this notification may replace a previous notification
gio_app.sendNotification(n.body, notification);
}
fn configChange(
self: *App,
target: apprt.Target,
new_config: *const Config,
) void {
switch (target) {
.surface => |surface| surface: {
surface.rt_surface.updateConfig(new_config) catch |err| {
log.err("unable to update surface config: {}", .{err});
};
const window = surface.rt_surface.container.window() orelse break :surface;
window.updateConfig(new_config) catch |err| {
log.warn("error updating config for window err={}", .{err});
};
},
.app => {
// We clone (to take ownership) and update our configuration.
if (new_config.clone(self.core_app.alloc)) |config_clone| {
self.config.deinit();
self.config = config_clone;
} else |err| {
log.warn("error cloning configuration err={}", .{err});
}
// App changes needs to show a toast that our configuration
// has reloaded.
const window = window: {
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| {
window.onConfigReloaded();
break :window window;
}
}
break :window null;
};
self.syncConfigChanges(window) catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
},
}
}
pub fn reloadConfig(
self: *App,
target: apprt.action.Target,
opts: apprt.action.ReloadConfig,
) !void {
// Tell systemd that reloading has started.
systemd.notify.reloading();
// When we exit this function tell systemd that reloading has finished.
defer systemd.notify.ready();
if (opts.soft) {
switch (target) {
.app => try self.core_app.updateConfig(self, &self.config),
.surface => |core_surface| try core_surface.updateConfig(
&self.config,
),
}
return;
}
// Load our configuration
var config = try Config.load(self.core_app.alloc);
errdefer config.deinit();
// Call into our app to update
switch (target) {
.app => try self.core_app.updateConfig(self, &config),
.surface => |core_surface| try core_surface.updateConfig(&config),
}
// Update the existing config, be sure to clean up the old one.
self.config.deinit();
self.config = config;
}
/// Call this anytime the configuration changes.
fn syncConfigChanges(self: *App, window: ?*Window) !void {
ConfigErrorsDialog.maybePresent(self, window);
try self.syncActionAccelerators();
if (self.global_shortcuts) |*shortcuts| {
shortcuts.refreshSession(self) catch |err| {
log.warn("failed to refresh global shortcuts={}", .{err});
};
}
// Load our runtime and custom CSS. If this fails then our window is just stuck
// with the old CSS but we don't want to fail the entire sync operation.
self.loadRuntimeCss() catch |err| switch (err) {
error.OutOfMemory => log.warn(
"out of memory loading runtime CSS, no runtime CSS applied",
.{},
),
};
self.loadCustomCss() catch |err| {
log.warn("Failed to load custom CSS, no custom CSS applied, err={}", .{err});
};
}
fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
try self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector);
try self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette);
try self.syncActionAccelerator("win.close", .{ .close_window = {} });
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
try self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
try self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} });
}
fn syncActionAccelerator(
self: *App,
gtk_action: [:0]const u8,
action: input.Binding.Action,
) !void {
const gtk_app = self.app.as(gtk.Application);
// Reset it initially
const zero = [_:null]?[*:0]const u8{};
gtk_app.setAccelsForAction(gtk_action, &zero);
const trigger = self.config.keybind.set.getTrigger(action) orelse return;
var buf: [256]u8 = undefined;
const accel = try key.accelFromTrigger(&buf, trigger) orelse return;
const accels = [_:null]?[*:0]const u8{accel};
gtk_app.setAccelsForAction(gtk_action, &accels);
}
fn loadRuntimeCss(
self: *const App,
) Allocator.Error!void {
const alloc = self.core_app.alloc;
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(alloc);
const writer = buf.writer(alloc);
const config: *const Config = &self.config;
const window_theme = config.@"window-theme";
const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background;
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
try writer.print(
\\widget.unfocused-split {{
\\ opacity: {d:.2};
\\ background-color: rgb({d},{d},{d});
\\}}
, .{
1.0 - config.@"unfocused-split-opacity",
unfocused_fill.r,
unfocused_fill.g,
unfocused_fill.b,
});
if (config.@"split-divider-color") |color| {
try writer.print(
\\.terminal-window .notebook separator {{
\\ color: rgb({[r]d},{[g]d},{[b]d});
\\ background: rgb({[r]d},{[g]d},{[b]d});
\\}}
, .{
.r = color.r,
.g = color.g,
.b = color.b,
});
}
if (config.@"window-title-font-family") |font_family| {
try writer.print(
\\.window headerbar {{
\\ font-family: "{[font_family]s}";
\\}}
, .{ .font_family = font_family });
}
if (gtk_version.runtimeAtLeast(4, 16, 0)) {
switch (window_theme) {
.ghostty => try writer.print(
\\:root {{
\\ --ghostty-fg: rgb({d},{d},{d});
\\ --ghostty-bg: rgb({d},{d},{d});
\\ --headerbar-fg-color: var(--ghostty-fg);
\\ --headerbar-bg-color: var(--ghostty-bg);
\\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
\\ --overview-fg-color: var(--ghostty-fg);
\\ --overview-bg-color: var(--ghostty-bg);
\\ --popover-fg-color: var(--ghostty-fg);
\\ --popover-bg-color: var(--ghostty-bg);
\\ --window-fg-color: var(--ghostty-fg);
\\ --window-bg-color: var(--ghostty-bg);
\\}}
\\windowhandle {{
\\ background-color: var(--headerbar-bg-color);
\\ color: var(--headerbar-fg-color);
\\}}
\\windowhandle:backdrop {{
\\ background-color: var(--headerbar-backdrop-color);
\\}}
, .{
headerbar_foreground.r,
headerbar_foreground.g,
headerbar_foreground.b,
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
}),
else => {},
}
} else {
try writer.print(
\\window.window-theme-ghostty .top-bar,
\\window.window-theme-ghostty .bottom-bar,
\\window.window-theme-ghostty box > tabbar {{
\\ background-color: rgb({d},{d},{d});
\\ color: rgb({d},{d},{d});
\\}}
, .{
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
headerbar_foreground.r,
headerbar_foreground.g,
headerbar_foreground.b,
});
}
const data = try alloc.dupeZ(u8, buf.items);
defer alloc.free(data);
// Clears any previously loaded CSS from this provider
loadCssProviderFromData(self.css_provider, data);
}
fn loadCustomCss(self: *App) !void {
const alloc = self.core_app.alloc;
const display = gdk.Display.getDefault() orelse {
log.warn("unable to get display", .{});
return;
};
// unload the previously loaded style providers
for (self.custom_css_providers.items) |provider| {
gtk.StyleContext.removeProviderForDisplay(
display,
provider.as(gtk.StyleProvider),
);
provider.unref();
}
self.custom_css_providers.clearRetainingCapacity();
for (self.config.@"gtk-custom-css".value.items) |p| {
const path, const optional = switch (p) {
.optional => |path| .{ path, true },
.required => |path| .{ path, false },
};
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
if (err != error.FileNotFound or !optional) {
log.err(
"error opening gtk-custom-css file {s}: {}",
.{ path, err },
);
}
continue;
};
defer file.close();
log.info("loading gtk-custom-css path={s}", .{path});
const contents = try file.reader().readAllAlloc(
self.core_app.alloc,
5 * 1024 * 1024, // 5MB,
);
defer alloc.free(contents);
const data = try alloc.dupeZ(u8, contents);
defer alloc.free(data);
const provider = gtk.CssProvider.new();
loadCssProviderFromData(provider, data);
gtk.StyleContext.addProviderForDisplay(
display,
provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_USER,
);
try self.custom_css_providers.append(self.core_app.alloc, provider);
}
}
fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void {
if (gtk_version.atLeast(4, 12, 0)) {
const g_bytes = glib.Bytes.new(data.ptr, data.len);
defer g_bytes.unref();
provider.loadFromBytes(g_bytes);
} else {
provider.loadFromData(data, @intCast(data.len));
}
}
/// Called by CoreApp to wake up the event loop.
pub fn wakeup(_: App) void {
glib.MainContext.wakeup(null);
}
/// Run the event loop. This doesn't return until the app exits.
pub fn run(self: *App) !void {
// Running will be false when we're not the primary instance and should
// exit (GTK single instance mode). If we're not running, we're done
// right away.
if (!self.running) return;
// If we are running, then we proceed to setup our app.
// Setup our cgroup configurations for our surfaces.
if (switch (self.config.@"linux-cgroup") {
.never => false,
.always => true,
.@"single-instance" => self.single_instance,
}) cgroup: {
const path = cgroup.init(self) catch |err| {
// If we can't initialize cgroups then that's okay. We
// want to continue to run so we just won't isolate surfaces.
// NOTE(mitchellh): do we want a config to force it?
log.warn(
"failed to initialize cgroups, terminals will not be isolated err={}",
.{err},
);
// If we have hard fail enabled then we exit now.
if (self.config.@"linux-cgroup-hard-fail") {
log.err("linux-cgroup-hard-fail enabled, exiting", .{});
return error.CgroupInitFailed;
}
break :cgroup;
};
log.info("cgroup isolation enabled base={s}", .{path});
self.transient_cgroup_base = path;
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
// Setup color scheme notifications
const style_manager: *adw.StyleManager = self.app.getStyleManager();
_ = gobject.Object.signals.notify.connect(
style_manager,
*App,
adwNotifyDark,
self,
.{
.detail = "dark",
},
);
// Make an initial request to set up the color scheme
const light = style_manager.getDark() == 0;
self.colorSchemeEvent(if (light) .light else .dark);
// Setup our actions
self.initActions();
// On startup, we want to check for configuration errors right away
// so we can show our error window. We also need to setup other initial
// state.
self.syncConfigChanges(null) catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
// Tell systemd that we are ready.
systemd.notify.ready();
while (self.running) {
_ = glib.MainContext.iteration(self.ctx, 1);
// Tick the terminal app and see if we should quit.
try self.core_app.tick(self);
// Check if we must quit based on the current state.
const must_quit = q: {
// If we are configured to always stay running, don't quit.
if (!self.config.@"quit-after-last-window-closed") break :q false;
// If the quit timer has expired, quit.
if (self.quit_timer == .expired) break :q true;
// There's no quit timer running, or it hasn't expired, don't quit.
break :q false;
};
if (must_quit) self.quit();
}
}
// This timeout function is started when no surfaces are open. It can be
// cancelled if a new surface is opened before the timer expires.
pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
const self: *App = @ptrCast(@alignCast(ud));
self.quit_timer = .{ .expired = {} };
return 0;
}
/// This will get called when there are no more open surfaces.
fn startQuitTimer(self: *App) void {
// Cancel any previous timer.
self.stopQuitTimer();
// This is a no-op unless we are configured to quit after last window is closed.
if (!self.config.@"quit-after-last-window-closed") return;
if (self.config.@"quit-after-last-window-closed-delay") |v| {
// If a delay is configured, set a timeout function to quit after the delay.
self.quit_timer = .{
.active = glib.timeoutAdd(
v.asMilliseconds(),
gtkQuitTimerExpired,
self,
),
};
} else {
// If no delay is configured, treat it as expired.
self.quit_timer = .{ .expired = {} };
}
}
/// This will get called when a new surface gets opened.
fn stopQuitTimer(self: *App) void {
switch (self.quit_timer) {
.off => {},
.expired => self.quit_timer = .{ .off = {} },
.active => |source| {
if (glib.Source.remove(source) == 0) {
log.warn("unable to remove quit timer source={d}", .{source});
}
self.quit_timer = .{ .off = {} };
},
}
}
/// Close the given surface.
pub fn redrawSurface(self: *App, surface: *Surface) void {
_ = self;
surface.redraw();
}
/// Redraw the inspector for the given surface.
pub fn redrawInspector(self: *App, surface: *Surface) void {
_ = self;
surface.queueInspectorRender();
}
/// Called by CoreApp to create a new window with a new surface.
fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
const alloc = self.core_app.alloc;
// Allocate a fixed pointer for our window. We try to minimize
// allocations but windows and other GUI requirements are so minimal
// compared to the steady-state terminal operation so we use heap
// allocation for this.
//
// The allocation is owned by the GtkWindow created. It will be
// freed when the window is closed.
var window = try Window.create(alloc, self);
// Add our initial tab
try window.newTab(parent_);
// Show the new window
window.present();
}
fn setSecureInput(_: *App, target: apprt.Target, value: apprt.action.SecureInput) void {
switch (target) {
.app => {},
.surface => |surface| {
surface.rt_surface.setSecureInput(value);
},
}
}
fn closeWindow(_: *App, target: apprt.action.Target) !bool {
switch (target) {
.app => return false,
.surface => |v| {
const window = v.rt_surface.container.window() orelse return false;
window.closeWithConfirmation();
return true;
},
}
}
fn quit(self: *App) void {
// If we're already not running, do nothing.
if (!self.running) return;
// If the app says we don't need to confirm, then we can quit now.
if (!self.core_app.needsConfirmQuit()) {
self.quitNow();
return;
}
CloseDialog.show(.{ .app = self }) catch |err| {
log.err("failed to open close dialog={}", .{err});
};
}
/// This immediately destroys all windows, forcing the application to quit.
pub fn quitNow(self: *App) void {
const list = gtk.Window.listToplevels();
defer list.free();
list.foreach(struct {
fn callback(data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
const ptr = data orelse return;
const window: *gtk.Window = @ptrCast(@alignCast(ptr));
window.destroy();
}
}.callback, null);
self.running = false;
}
// SIGUSR2 signal handler via g_unix_signal_add
fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int {
const self: *App = @ptrCast(@alignCast(ud orelse
return @intFromBool(glib.SOURCE_CONTINUE)));
log.info("received SIGUSR2, reloading configuration", .{});
self.reloadConfig(.app, .{ .soft = false }) catch |err| {
log.err(
"error reloading configuration for SIGUSR2: {}",
.{err},
);
};
return @intFromBool(glib.SOURCE_CONTINUE);
}
/// This is called by the `activate` signal. This is sent on program startup and
/// also when a secondary instance launches and requests a new window.
fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {
// Queue a new window
_ = core_app.mailbox.push(.{
.new_window = .{},
}, .{ .forever = {} });
}
fn gtkWindowAdded(
_: *adw.Application,
window: *gtk.Window,
core_app: *CoreApp,
) callconv(.c) void {
// Request the is-active property change so we can detect
// when our app loses focus.
_ = gobject.Object.signals.notify.connect(
window,
*CoreApp,
gtkWindowIsActive,
core_app,
.{
.detail = "is-active",
},
);
}
fn gtkWindowRemoved(
_: *adw.Application,
_: *gtk.Window,
core_app: *CoreApp,
) callconv(.c) void {
// Recheck if we are focused
gtkWindowIsActive(null, undefined, core_app);
}
fn gtkWindowIsActive(
window: ?*gtk.Window,
_: *gobject.ParamSpec,
core_app: *CoreApp,
) callconv(.c) void {
// If our window is active, then we can tell the app
// that we are focused.
if (window) |w| {
if (w.isActive() != 0) {
core_app.focusEvent(true);
return;
}
}
// If the window becomes inactive, we need to check if any
// other windows are active. If not, then we are no longer
// focused.
{
const list = gtk.Window.listToplevels();
defer list.free();
var current: ?*glib.List = list;
while (current) |elem| : (current = elem.f_next) {
// If the window is active then we are still focused.
// This is another window since we did our check above.
// That window should trigger its own is-active
// callback so we don't need to call it here.
const w: *gtk.Window = @alignCast(@ptrCast(elem.f_data));
if (w.isActive() == 1) return;
}
}
// We are not focused
core_app.focusEvent(false);
}
fn adwNotifyDark(
style_manager: *adw.StyleManager,
_: *gobject.ParamSpec,
self: *App,
) callconv(.c) void {
const color_scheme: apprt.ColorScheme = if (style_manager.getDark() == 0)
.light
else
.dark;
self.colorSchemeEvent(color_scheme);
}
fn colorSchemeEvent(
self: *App,
scheme: apprt.ColorScheme,
) void {
self.core_app.colorSchemeEvent(self, scheme) catch |err| {
log.err("error updating app color scheme err={}", .{err});
};
for (self.core_app.surfaces.items) |surface| {
surface.core_surface.colorSchemeCallback(scheme) catch |err| {
log.err("unable to tell surface about color scheme change err={}", .{err});
};
}
}
fn gtkActionOpenConfig(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
_ = self.core_app.mailbox.push(.{
.open_config = {},
}, .{ .forever = {} });
}
fn gtkActionReloadConfig(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
self.reloadConfig(.app, .{}) catch |err| {
log.err("error reloading configuration: {}", .{err});
};
}
fn gtkActionQuit(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
self.core_app.performAction(self, .quit) catch |err| {
log.err("error quitting err={}", .{err});
};
}
/// Action sent by the window manager asking us to present a specific surface to
/// the user. Usually because the user clicked on a desktop notification.
fn gtkActionPresentSurface(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
const parameter = parameter_ orelse return;
const t = glib.ext.VariantType.newFor(u64);
defer glib.VariantType.free(t);
// Make sure that we've receiived a u64 from the system.
if (glib.Variant.isOfType(parameter, t) == 0) {
return;
}
// Convert that u64 to pointer to a core surface. A value of zero
// means that there was no target surface for the notification so
// we don't focus any surface.
const ptr_int = parameter.getUint64();
if (ptr_int == 0) return;
const surface: *CoreSurface = @ptrFromInt(ptr_int);
// Send a message through the core app mailbox rather than presenting the
// surface directly so that it can validate that the surface pointer is
// valid. We could get an invalid pointer if a desktop notification outlives
// a Ghostty instance and a new one starts up, or there are multiple Ghostty
// instances running.
_ = self.core_app.mailbox.push(
.{
.surface_message = .{
.surface = surface,
.message = .{ .present_surface = {} },
},
},
.{ .forever = {} },
);
}
fn gtkActionShowGTKInspector(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
self.core_app.performAction(self, .show_gtk_inspector) catch |err| {
log.err("error showing GTK inspector err={}", .{err});
};
}
fn gtkActionNewWindow(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
log.debug("received new window action", .{});
parameter: {
// were we given a parameter?
const parameter = parameter_ orelse break :parameter;
const as = glib.VariantType.new("as");
defer as.free();
// ensure that the supplied parameter is an array of strings
if (glib.Variant.isOfType(parameter, as) == 0) {
log.warn("parameter is of type {s}", .{parameter.getTypeString()});
break :parameter;
}
const s = glib.VariantType.new("s");
defer s.free();
var it: glib.VariantIter = undefined;
_ = it.init(parameter);
while (it.nextValue()) |value| {
defer value.unref();
// just to be sure
if (value.isOfType(s) == 0) continue;
var len: usize = undefined;
const buf = value.getString(&len);
const str = buf[0..len];
log.debug("new-window command argument: {s}", .{str});
}
}
_ = self.core_app.mailbox.push(.{
.new_window = .{},
}, .{ .forever = {} });
}
/// This is called to setup the action map that this application supports.
/// This should be called only once on startup.
fn initActions(self: *App) void {
// The set of actions. Each action has (in order):
// [0] The action name
// [1] The callback function
// [2] The GVariantType of the parameter
//
// For action names:
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
const t = glib.ext.VariantType.newFor(u64);
defer t.free();
const as = glib.VariantType.new("as");
defer as.free();
const actions = .{
.{ "quit", gtkActionQuit, null },
.{ "open-config", gtkActionOpenConfig, null },
.{ "reload-config", gtkActionReloadConfig, null },
.{ "present-surface", gtkActionPresentSurface, t },
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
.{ "new-window", gtkActionNewWindow, null },
.{ "new-window-command", gtkActionNewWindow, as },
};
inline for (actions) |entry| {
const action = gio.SimpleAction.new(entry[0], entry[2]);
defer action.unref();
_ = gio.SimpleAction.signals.activate.connect(
action,
*App,
entry[1],
self,
.{},
);
const action_map = self.app.as(gio.ActionMap);
action_map.addAction(action.as(gio.Action));
}
}
fn openConfig(self: *App) !bool {
// Get the config file path
const alloc = self.core_app.alloc;
const path = configpkg.edit.openPath(alloc) catch |err| {
log.warn("error getting config file path: {}", .{err});
return false;
};
defer alloc.free(path);
// Open it using openURL. "path" isn't actually a URL but
// at the time of writing that works just fine for GTK.
self.openUrl(.{ .kind = .text, .url = path });
return true;
}
fn openUrl(
self: *App,
value: apprt.action.OpenUrl,
) void {
if (std.mem.startsWith(u8, value.url, "/")) {
self.openUrlFallback(value.kind, value.url);
return;
}
if (std.mem.startsWith(u8, value.url, "file://")) {
self.openUrlFallback(value.kind, value.url);
return;
}
self.open_uri.start(value) catch |err| {
log.err("unable to open uri err={}", .{err});
self.openUrlFallback(value.kind, value.url);
return;
};
}
pub fn openUrlFallback(self: *App, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
self.core_app.alloc,
kind,
url,
) catch |err| log.warn("unable to open url: {}", .{err});
}