/// 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)); }