diff --git a/include/ghostty.h b/include/ghostty.h index 50ef2bcb0..7b4afa1d5 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -284,9 +284,22 @@ typedef struct { bool physical; } ghostty_input_trigger_s; +typedef enum { + GHOSTTY_BUILD_MODE_DEBUG, + GHOSTTY_BUILD_MODE_RELEASE_SAFE, + GHOSTTY_BUILD_MODE_RELEASE_FAST, + GHOSTTY_BUILD_MODE_RELEASE_SMALL, +} ghostty_build_mode_e; + // Fully defined types. This MUST be kept in sync with equivalent Zig // structs. To find the Zig struct, grep for this type name. The documentation // for all of these types is available in the Zig source. +typedef struct { + ghostty_build_mode_e build_mode; + const char *version; + uintptr_t version_len; +} ghostty_info_s; + typedef struct { const char *message; } ghostty_error_s; @@ -338,6 +351,7 @@ typedef struct { // Published API int ghostty_init(void); +ghostty_info_s ghostty_info(void); ghostty_config_t ghostty_config_new(); void ghostty_config_free(ghostty_config_t); diff --git a/macos/Sources/Features/Primary Window/PrimaryView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift index da110011b..b871585bd 100644 --- a/macos/Sources/Features/Primary Window/PrimaryView.swift +++ b/macos/Sources/Features/Primary Window/PrimaryView.swift @@ -94,38 +94,46 @@ struct PrimaryView: View { self.appDelegate.confirmQuit = $0 }) - Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: self.baseConfig) - .ghosttyApp(ghostty.app!) - .ghosttyConfig(ghostty.config!) - .background(WindowAccessor(window: $window)) - .onReceive(gotoTab) { onGotoTab(notification: $0) } - .onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) } - .focused($focused) - .onAppear { self.focused = true } - .onChange(of: focusedSurface) { newValue in - self.focusedSurfaceWrapper.surface = newValue?.surface + VStack(spacing: 0) { + // If we're running in debug mode we show a warning so that users + // know that performance will be degraded. + if (ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { + DebugBuildWarningView() } - .onChange(of: title) { newValue in - // We need to handle this manually because we are using AppKit lifecycle - // so navigationTitle no longer works. - guard let window = self.window else { return } - window.title = newValue - } - .confirmationDialog( - "Quit Ghostty?", - isPresented: confirmQuitting) { - Button("Close Ghostty") { - NSApplication.shared.reply(toApplicationShouldTerminate: true) - } - .keyboardShortcut(.defaultAction) - - Button("Cancel", role: .cancel) { - NSApplication.shared.reply(toApplicationShouldTerminate: false) - } - .keyboardShortcut(.cancelAction) - } message: { - Text("All terminal sessions will be terminated.") + + Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: self.baseConfig) + .ghosttyApp(ghostty.app!) + .ghosttyConfig(ghostty.config!) + .background(WindowAccessor(window: $window)) + .onReceive(gotoTab) { onGotoTab(notification: $0) } + .onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) } + .focused($focused) + .onAppear { self.focused = true } + .onChange(of: focusedSurface) { newValue in + self.focusedSurfaceWrapper.surface = newValue?.surface } + .onChange(of: title) { newValue in + // We need to handle this manually because we are using AppKit lifecycle + // so navigationTitle no longer works. + guard let window = self.window else { return } + window.title = newValue + } + .confirmationDialog( + "Quit Ghostty?", + isPresented: confirmQuitting) { + Button("Close Ghostty") { + NSApplication.shared.reply(toApplicationShouldTerminate: true) + } + .keyboardShortcut(.defaultAction) + + Button("Cancel", role: .cancel) { + NSApplication.shared.reply(toApplicationShouldTerminate: false) + } + .keyboardShortcut(.cancelAction) + } message: { + Text("All terminal sessions will be terminated.") + } + } } } @@ -196,3 +204,34 @@ struct PrimaryView: View { } } } + +struct DebugBuildWarningView: View { + @State private var isPopover = false + + var body: some View { + HStack { + Spacer() + + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.yellow) + + Text("You're running a debug build of Ghostty! Performance will be degraded.") + .padding(.all, 8) + .popover(isPresented: $isPopover, arrowEdge: .bottom) { + Text(""" + Debug builds of Ghostty are very slow and you may experience + performance problems. Debug builds are only recommended during + development. + """) + .padding(.all) + } + + Spacer() + } + .background(Color(.windowBackgroundColor)) + .frame(maxWidth: .infinity) + .onTapGesture { + isPopover = true + } + } +} diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index fda9bf9b5..253c705f8 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -11,6 +11,11 @@ extension Ghostty { case loading, error, ready } + struct Info { + var mode: ghostty_build_mode_e + var version: String + } + /// The AppState is the global state that is associated with the Swift app. This handles initially /// initializing Ghostty, loading the configuration, etc. class AppState: ObservableObject { @@ -46,6 +51,18 @@ extension Ghostty { return ghostty_app_needs_confirm_quit(app) } + /// Build information + var info: Info { + let raw = ghostty_info() + let version = NSString( + bytes: raw.version, + length: Int(raw.version_len), + encoding: NSUTF8StringEncoding + ) ?? "unknown" + + return Info(mode: raw.build_mode, version: String(version)) + } + /// Cached clipboard string for `read_clipboard` callback. private var cached_clipboard_string: String? = nil diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index fc6be8942..8c5adef41 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -392,14 +392,32 @@ const Window = struct { c.gtk_notebook_set_show_tabs(notebook, 0); c.gtk_notebook_set_show_border(notebook, 0); + // This is important so the notebook expands to fit available space. + // Otherwise, it will be zero/zero in the box below. + c.gtk_widget_set_vexpand(notebook_widget, 1); + c.gtk_widget_set_hexpand(notebook_widget, 1); + // Create our add button for new tabs const notebook_add_btn = c.gtk_button_new_from_icon_name("list-add-symbolic"); c.gtk_notebook_set_action_widget(notebook, notebook_add_btn, c.GTK_PACK_END); _ = c.g_signal_connect_data(notebook_add_btn, "clicked", c.G_CALLBACK(>kTabAddClick), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), self, null, G_CONNECT_DEFAULT); - // The notebook is our main child - c.gtk_window_set_child(gtk_window, notebook_widget); + // Create our box which will hold our widgets. + const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); + + // In debug we show a warning. This is a really common issue where + // people build from source in debug and performance is really bad. + if (builtin.mode == .Debug) { + const warning = c.gtk_label_new("⚠️ You're running a debug build of Ghostty! Performance will be degraded."); + c.gtk_widget_set_margin_top(warning, 10); + c.gtk_widget_set_margin_bottom(warning, 10); + c.gtk_box_append(@ptrCast(box), warning); + } + c.gtk_box_append(@ptrCast(box), notebook_widget); + + // The box is our main child + c.gtk_window_set_child(gtk_window, box); } pub fn deinit(self: *Window) void { diff --git a/src/main_c.zig b/src/main_c.zig index c68cdbf36..ec3441582 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -9,6 +9,7 @@ const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); +const build_config = @import("build_config.zig"); const main = @import("main.zig"); const apprt = @import("apprt.zig"); @@ -23,6 +24,20 @@ pub const std_options = main.std_options; pub usingnamespace @import("config.zig").CAPI; pub usingnamespace apprt.runtime.CAPI; +/// ghostty_info_s +const Info = extern struct { + mode: BuildMode, + version: [*]const u8, + version_len: usize, + + const BuildMode = enum(c_int) { + debug, + release_safe, + release_fast, + release_small, + }; +}; + /// Initialize ghostty global state. It is possible to have more than /// one global state but it has zero practical benefit. export fn ghostty_init() c_int { @@ -33,3 +48,16 @@ export fn ghostty_init() c_int { }; return 0; } + +export fn ghostty_info() Info { + return .{ + .mode = switch (builtin.mode) { + .Debug => .debug, + .ReleaseSafe => .release_safe, + .ReleaseFast => .release_fast, + .ReleaseSmall => .release_small, + }, + .version = build_config.version_string.ptr, + .version_len = build_config.version_string.len, + }; +}