mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
576 lines
19 KiB
Zig
576 lines
19 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 std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const builtin = @import("builtin");
|
|
const configpkg = @import("../../config.zig");
|
|
const input = @import("../../input.zig");
|
|
const internal_os = @import("../../os/main.zig");
|
|
const Config = configpkg.Config;
|
|
const CoreApp = @import("../../App.zig");
|
|
const CoreSurface = @import("../../Surface.zig");
|
|
const build_options = @import("build_options");
|
|
|
|
const Surface = @import("Surface.zig");
|
|
const Window = @import("Window.zig");
|
|
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
|
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
|
const c = @import("c.zig");
|
|
const inspector = @import("inspector.zig");
|
|
const key = @import("key.zig");
|
|
const testing = std.testing;
|
|
|
|
const log = std.log.scoped(.gtk);
|
|
|
|
pub const Options = struct {};
|
|
|
|
core_app: *CoreApp,
|
|
config: Config,
|
|
|
|
app: *c.GtkApplication,
|
|
ctx: *c.GMainContext,
|
|
|
|
/// The "none" cursor. We use one that is shared across the entire app.
|
|
cursor_none: ?*c.GdkCursor,
|
|
|
|
/// The shared application menu.
|
|
menu: ?*c.GMenu = null,
|
|
|
|
/// The configuration errors window, if it is currently open.
|
|
config_errors_window: ?*ConfigErrorsWindow = null,
|
|
|
|
/// The clipboard confirmation window, if it is currently open.
|
|
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
|
|
|
/// This is set to false when the main loop should exit.
|
|
running: bool = true,
|
|
|
|
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|
_ = opts;
|
|
|
|
// Load our configuration
|
|
var config = try Config.load(core_app.alloc);
|
|
errdefer config.deinit();
|
|
|
|
// If we had configuration errors, then log them.
|
|
if (!config._errors.empty()) {
|
|
for (config._errors.list.items) |err| {
|
|
log.warn("configuration error: {s}", .{err.message});
|
|
}
|
|
}
|
|
|
|
// The "none" cursor is used for hiding the cursor
|
|
const cursor_none = c.gdk_cursor_new_from_name("none", null);
|
|
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
|
|
|
|
const single_instance = switch (config.@"gtk-single-instance") {
|
|
.true => true,
|
|
.false => false,
|
|
.desktop => internal_os.launchedFromDesktop(),
|
|
};
|
|
|
|
// Setup the flags for our application.
|
|
const app_flags: c.GApplicationFlags = app_flags: {
|
|
var flags: c.GApplicationFlags = c.G_APPLICATION_DEFAULT_FLAGS;
|
|
if (!single_instance) flags |= c.G_APPLICATION_NON_UNIQUE;
|
|
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 (isValidAppId(class)) {
|
|
break :app_id class;
|
|
} else {
|
|
log.warn("invalid 'class' in config, ignoring", .{});
|
|
}
|
|
}
|
|
|
|
const default_id = "com.mitchellh.ghostty";
|
|
break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id;
|
|
};
|
|
|
|
// Create our GTK Application which encapsulates our process.
|
|
const app: *c.GtkApplication = app: {
|
|
const adwaita = build_options.libadwaita and config.@"gtk-adwaita";
|
|
|
|
log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
|
|
app_id,
|
|
single_instance,
|
|
adwaita,
|
|
});
|
|
|
|
// If not libadwaita, create a standard GTK application.
|
|
if (!adwaita) break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
|
|
app_id.ptr,
|
|
app_flags,
|
|
))) orelse return error.GtkInitFailed;
|
|
|
|
// Use libadwaita if requested. Using an AdwApplication lets us use
|
|
// Adwaita widgets and access things such as the color scheme.
|
|
const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
|
|
app_id.ptr,
|
|
app_flags,
|
|
))) orelse return error.GtkInitFailed;
|
|
|
|
const style_manager = c.adw_application_get_style_manager(adw_app);
|
|
c.adw_style_manager_set_color_scheme(
|
|
style_manager,
|
|
switch (config.@"window-theme") {
|
|
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
|
|
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
|
|
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
|
|
},
|
|
);
|
|
|
|
break :app @ptrCast(adw_app);
|
|
};
|
|
|
|
errdefer c.g_object_unref(app);
|
|
_ = c.g_signal_connect_data(
|
|
app,
|
|
"activate",
|
|
c.G_CALLBACK(>kActivate),
|
|
core_app,
|
|
null,
|
|
c.G_CONNECT_DEFAULT,
|
|
);
|
|
|
|
// We don't use g_application_run, we want to manually control the
|
|
// loop so we have to do the same things the run function does:
|
|
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
|
|
const ctx = c.g_main_context_default() orelse return error.GtkContextFailed;
|
|
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
|
|
errdefer c.g_main_context_release(ctx);
|
|
|
|
const gapp = @as(*c.GApplication, @ptrCast(app));
|
|
var err_: ?*c.GError = null;
|
|
if (c.g_application_register(
|
|
gapp,
|
|
null,
|
|
@ptrCast(&err_),
|
|
) == 0) {
|
|
if (err_) |err| {
|
|
log.warn("error registering application: {s}", .{err.message});
|
|
c.g_error_free(err);
|
|
}
|
|
return error.GtkApplicationRegisterFailed;
|
|
}
|
|
|
|
const display = c.gdk_display_get_default();
|
|
if (c.g_type_check_instance_is_a(@ptrCast(@alignCast(display)), c.gdk_x11_display_get_type()) != 0) {
|
|
// 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.
|
|
//
|
|
const prgname = if (config.@"x11-instance-name") |pn|
|
|
pn
|
|
else if (builtin.mode == .Debug)
|
|
"ghostty-debug"
|
|
else
|
|
"ghostty";
|
|
c.g_set_prgname(prgname);
|
|
c.gdk_x11_display_set_program_class(display, app_id);
|
|
}
|
|
|
|
// This just calls the "activate" signal but its part of the normal
|
|
// startup routine so we just call it:
|
|
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
|
c.g_application_activate(gapp);
|
|
|
|
return .{
|
|
.core_app = core_app,
|
|
.app = app,
|
|
.config = config,
|
|
.ctx = ctx,
|
|
.cursor_none = cursor_none,
|
|
// 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 = c.g_application_get_is_remote(gapp) == 0,
|
|
};
|
|
}
|
|
|
|
// 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 {
|
|
c.g_settings_sync();
|
|
while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
|
|
c.g_main_context_release(self.ctx);
|
|
c.g_object_unref(self.app);
|
|
|
|
if (self.cursor_none) |cursor| c.g_object_unref(cursor);
|
|
if (self.menu) |menu| c.g_object_unref(menu);
|
|
|
|
self.config.deinit();
|
|
}
|
|
|
|
/// Open the configuration in the system editor.
|
|
pub fn openConfig(self: *App) !void {
|
|
try configpkg.edit.open(self.core_app.alloc);
|
|
}
|
|
|
|
/// Reload the configuration. This should return the new configuration.
|
|
/// The old value can be freed immediately at this point assuming a
|
|
/// successful return.
|
|
///
|
|
/// The returned pointer value is only valid for a stable self pointer.
|
|
pub fn reloadConfig(self: *App) !?*const Config {
|
|
// Load our configuration
|
|
var config = try Config.load(self.core_app.alloc);
|
|
errdefer config.deinit();
|
|
|
|
// Update the existing config, be sure to clean up the old one.
|
|
self.config.deinit();
|
|
self.config = config;
|
|
self.syncConfigChanges() catch |err| {
|
|
log.warn("error handling configuration changes err={}", .{err});
|
|
};
|
|
|
|
return &self.config;
|
|
}
|
|
|
|
/// Call this anytime the configuration changes.
|
|
fn syncConfigChanges(self: *App) !void {
|
|
try self.updateConfigErrors();
|
|
try self.syncActionAccelerators();
|
|
}
|
|
|
|
/// This should be called whenever the configuration changes to update
|
|
/// the state of our config errors window. This will show the window if
|
|
/// there are new configuration errors and hide the window if the errors
|
|
/// are resolved.
|
|
fn updateConfigErrors(self: *App) !void {
|
|
if (!self.config._errors.empty()) {
|
|
if (self.config_errors_window == null) {
|
|
try ConfigErrorsWindow.create(self);
|
|
assert(self.config_errors_window != null);
|
|
}
|
|
}
|
|
|
|
if (self.config_errors_window) |window| {
|
|
window.update();
|
|
}
|
|
}
|
|
|
|
fn syncActionAccelerators(self: *App) !void {
|
|
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
|
|
try self.syncActionAccelerator("app.reload_config", .{ .reload_config = {} });
|
|
try self.syncActionAccelerator("app.toggle_inspector", .{ .inspector = .toggle });
|
|
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
|
|
try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
|
|
try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
|
|
}
|
|
|
|
fn syncActionAccelerator(
|
|
self: *App,
|
|
gtk_action: [:0]const u8,
|
|
action: input.Binding.Action,
|
|
) !void {
|
|
// Reset it initially
|
|
const zero = [_]?[*:0]const u8{null};
|
|
c.gtk_application_set_accels_for_action(@ptrCast(self.app), gtk_action.ptr, &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 = [_]?[*:0]const u8{ accel, null };
|
|
|
|
c.gtk_application_set_accels_for_action(
|
|
@ptrCast(self.app),
|
|
gtk_action.ptr,
|
|
&accels,
|
|
);
|
|
}
|
|
|
|
/// Called by CoreApp to wake up the event loop.
|
|
pub fn wakeup(self: App) void {
|
|
_ = self;
|
|
c.g_main_context_wakeup(null);
|
|
}
|
|
|
|
/// Run the event loop. This doesn't return until the app exits.
|
|
pub fn run(self: *App) !void {
|
|
if (!self.running) return;
|
|
|
|
// If we're not remote, then we also setup our actions and menus.
|
|
self.initActions();
|
|
self.initMenu();
|
|
|
|
// 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() catch |err| {
|
|
log.warn("error handling configuration changes err={}", .{err});
|
|
};
|
|
|
|
while (self.running) {
|
|
_ = c.g_main_context_iteration(self.ctx, 1);
|
|
|
|
// Tick the terminal app
|
|
const should_quit = try self.core_app.tick(self);
|
|
if (should_quit or self.core_app.surfaces.items.len == 0) self.quit();
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
pub 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_);
|
|
}
|
|
|
|
fn quit(self: *App) void {
|
|
// If we have no toplevel windows, then we're done.
|
|
const list = c.gtk_window_list_toplevels();
|
|
if (list == null) {
|
|
self.running = false;
|
|
return;
|
|
}
|
|
c.g_list_free(list);
|
|
|
|
// If the app says we don't need to confirm, then we can quit now.
|
|
if (!self.core_app.needsConfirmQuit()) {
|
|
self.quitNow();
|
|
return;
|
|
}
|
|
|
|
// If we have windows, then we want to confirm that we want to exit.
|
|
const alert = c.gtk_message_dialog_new(
|
|
null,
|
|
c.GTK_DIALOG_MODAL,
|
|
c.GTK_MESSAGE_QUESTION,
|
|
c.GTK_BUTTONS_YES_NO,
|
|
"Quit Ghostty?",
|
|
);
|
|
c.gtk_message_dialog_format_secondary_text(
|
|
@ptrCast(alert),
|
|
"All active terminal sessions will be terminated.",
|
|
);
|
|
|
|
// We want the "yes" to appear destructive.
|
|
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
|
@ptrCast(alert),
|
|
c.GTK_RESPONSE_YES,
|
|
);
|
|
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
|
|
|
|
// We want the "no" to be the default action
|
|
c.gtk_dialog_set_default_response(
|
|
@ptrCast(alert),
|
|
c.GTK_RESPONSE_NO,
|
|
);
|
|
|
|
_ = c.g_signal_connect_data(
|
|
alert,
|
|
"response",
|
|
c.G_CALLBACK(>kQuitConfirmation),
|
|
self,
|
|
null,
|
|
c.G_CONNECT_DEFAULT,
|
|
);
|
|
|
|
c.gtk_widget_show(alert);
|
|
}
|
|
|
|
/// This immediately destroys all windows, forcing the application to quit.
|
|
fn quitNow(self: *App) void {
|
|
_ = self;
|
|
const list = c.gtk_window_list_toplevels();
|
|
defer c.g_list_free(list);
|
|
c.g_list_foreach(list, struct {
|
|
fn callback(data: c.gpointer, _: c.gpointer) callconv(.C) void {
|
|
const ptr = data orelse return;
|
|
const widget: *c.GtkWidget = @ptrCast(@alignCast(ptr));
|
|
const window: *c.GtkWindow = @ptrCast(widget);
|
|
c.gtk_window_destroy(window);
|
|
}
|
|
}.callback, null);
|
|
}
|
|
|
|
fn gtkQuitConfirmation(
|
|
alert: *c.GtkMessageDialog,
|
|
response: c.gint,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
|
|
|
// Close the alert window
|
|
c.gtk_window_destroy(@ptrCast(alert));
|
|
|
|
// If we didn't confirm then we're done
|
|
if (response != c.GTK_RESPONSE_YES) return;
|
|
|
|
// Force close all open windows
|
|
self.quitNow();
|
|
}
|
|
|
|
/// 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(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
|
_ = app;
|
|
|
|
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
|
|
|
// Queue a new window
|
|
_ = core_app.mailbox.push(.{
|
|
.new_window = .{},
|
|
}, .{ .forever = {} });
|
|
}
|
|
|
|
fn gtkActionReloadConfig(
|
|
_: *c.GSimpleAction,
|
|
_: *c.GVariant,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
|
_ = self.core_app.mailbox.push(.{
|
|
.reload_config = {},
|
|
}, .{ .forever = {} });
|
|
}
|
|
|
|
fn gtkActionQuit(
|
|
_: *c.GSimpleAction,
|
|
_: *c.GVariant,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
|
self.core_app.setQuit() catch |err| {
|
|
log.warn("error setting quit err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
/// 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 {
|
|
const actions = .{
|
|
.{ "quit", >kActionQuit },
|
|
.{ "reload_config", >kActionReloadConfig },
|
|
};
|
|
|
|
inline for (actions) |entry| {
|
|
const action = c.g_simple_action_new(entry[0], null);
|
|
defer c.g_object_unref(action);
|
|
_ = c.g_signal_connect_data(
|
|
action,
|
|
"activate",
|
|
c.G_CALLBACK(entry[1]),
|
|
self,
|
|
null,
|
|
c.G_CONNECT_DEFAULT,
|
|
);
|
|
c.g_action_map_add_action(@ptrCast(self.app), @ptrCast(action));
|
|
}
|
|
}
|
|
|
|
/// This sets the self.menu property to the application menu that can be
|
|
/// shared by all application windows.
|
|
fn initMenu(self: *App) void {
|
|
const menu = c.g_menu_new();
|
|
errdefer c.g_object_unref(menu);
|
|
|
|
{
|
|
const section = c.g_menu_new();
|
|
defer c.g_object_unref(section);
|
|
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
c.g_menu_append(section, "New Window", "win.new_window");
|
|
c.g_menu_append(section, "New Tab", "win.new_tab");
|
|
c.g_menu_append(section, "Close Window", "win.close");
|
|
}
|
|
|
|
{
|
|
const section = c.g_menu_new();
|
|
defer c.g_object_unref(section);
|
|
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
|
c.g_menu_append(section, "Reload Configuration", "app.reload_config");
|
|
c.g_menu_append(section, "About Ghostty", "win.about");
|
|
}
|
|
|
|
// {
|
|
// const section = c.g_menu_new();
|
|
// defer c.g_object_unref(section);
|
|
// c.g_menu_append_submenu(menu, "File", @ptrCast(@alignCast(section)));
|
|
// }
|
|
|
|
self.menu = menu;
|
|
}
|
|
|
|
fn isValidAppId(app_id: [:0]const u8) bool {
|
|
if (app_id.len > 255 or app_id.len == 0) return false;
|
|
if (app_id[0] == '.') return false;
|
|
if (app_id[app_id.len - 1] == '.') return false;
|
|
|
|
var hasDot = false;
|
|
for (app_id) |char| {
|
|
switch (char) {
|
|
'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {},
|
|
'.' => hasDot = true,
|
|
else => return false,
|
|
}
|
|
}
|
|
if (!hasDot) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
test "isValidAppId" {
|
|
try testing.expect(isValidAppId("foo.bar"));
|
|
try testing.expect(isValidAppId("foo.bar.baz"));
|
|
try testing.expect(!isValidAppId("foo"));
|
|
try testing.expect(!isValidAppId("foo.bar?"));
|
|
try testing.expect(!isValidAppId("foo."));
|
|
try testing.expect(!isValidAppId(".foo"));
|
|
try testing.expect(!isValidAppId(""));
|
|
try testing.expect(!isValidAppId("foo" ** 86));
|
|
}
|