diff --git a/macos/Sources/Features/Primary Window/PrimaryWindow.swift b/macos/Sources/Features/Primary Window/PrimaryWindow.swift index b959e494b..51194ad91 100644 --- a/macos/Sources/Features/Primary Window/PrimaryWindow.swift +++ b/macos/Sources/Features/Primary Window/PrimaryWindow.swift @@ -19,7 +19,7 @@ class PrimaryWindow: NSWindow { static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate, baseConfig: ghostty_surface_config_s? = nil) -> PrimaryWindow { let window = PrimaryWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), - styleMask: [.titled, .closable, .miniaturizable, .resizable], + styleMask: getStyleMask(renderDecoration: ghostty.windowDecorations), backing: .buffered, defer: false) window.center() @@ -45,6 +45,15 @@ class PrimaryWindow: NSWindow { return window } + static func getStyleMask(renderDecoration: Bool) -> NSWindow.StyleMask { + var mask: NSWindow.StyleMask = [.resizable, .closable, .miniaturizable] + if renderDecoration { + mask.insert(.titled) + } + + return mask + } + override var canBecomeKey: Bool { return true } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 253c705f8..e0dab0b75 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -10,21 +10,21 @@ extension Ghostty { enum AppReadiness { 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 { /// The readiness value of the state. @Published var readiness: AppReadiness = .loading - + /// Optional delegate weak var delegate: GhosttyAppStateDelegate? - + /// The ghostty global configuration. This should only be changed when it is definitely /// safe to change. It is definite safe to change only when the embedded app runtime /// in Ghostty says so (usually, only in a reload configuration callback). @@ -35,7 +35,7 @@ extension Ghostty { ghostty_config_free(old) } } - + /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... @Published var app: ghostty_app_t? = nil { @@ -44,13 +44,13 @@ extension Ghostty { ghostty_app_free(old) } } - + /// True if we need to confirm before quitting. var needsConfirmQuit: Bool { guard let app = app else { return false } return ghostty_app_needs_confirm_quit(app) } - + /// Build information var info: Info { let raw = ghostty_info() @@ -59,10 +59,19 @@ extension Ghostty { length: Int(raw.version_len), encoding: NSUTF8StringEncoding ) ?? "unknown" - + return Info(mode: raw.build_mode, version: String(version)) } - + + /// True if we want to render window decorations + var windowDecorations: Bool { + guard let config = self.config else { return true } + var v = false; + let key = "window-decoration" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v; + } + /// Cached clipboard string for `read_clipboard` callback. private var cached_clipboard_string: String? = nil @@ -73,14 +82,14 @@ extension Ghostty { readiness = .error return } - + // Initialize the global configuration. guard let cfg = Self.loadConfig() else { readiness = .error return } self.config = cfg; - + // Create our "runtime" config. The "runtime" is the configuration that ghostty // uses to interface with the application runtime environment. var runtime_cfg = ghostty_runtime_config_s( @@ -110,7 +119,7 @@ extension Ghostty { return } self.app = app - + // Subscribe to notifications for keyboard layout change so that we can update Ghostty. NotificationCenter.default.addObserver( self, @@ -120,19 +129,19 @@ extension Ghostty { self.readiness = .ready } - + deinit { // This will force the didSet callbacks to run which free. self.app = nil self.config = nil - + // Remove our observer NotificationCenter.default.removeObserver( self, name: NSTextInputContext.keyboardSelectionDidChangeNotification, object: nil) } - + /// Initializes a new configuration and loads all the values. static func loadConfig() -> ghostty_config_t? { // Initialize the global configuration. @@ -140,19 +149,19 @@ extension Ghostty { AppDelegate.logger.critical("ghostty_config_new failed") return nil } - + // Load our configuration files from the home directory. ghostty_config_load_default_files(cfg); ghostty_config_load_cli_args(cfg); ghostty_config_load_recursive_files(cfg); - + // TODO: we'd probably do some config loading here... for now we'd // have to do this synchronously. When we support config updating we can do // this async and update later. - + // Finalize will make our defaults available. ghostty_config_finalize(cfg) - + // Log any configuration errors. These will be automatically shown in a // pop-up window too. let errCount = ghostty_config_errors_count(cfg) @@ -166,14 +175,14 @@ extension Ghostty { AppDelegate.logger.warning("config error: \(message)") } } - + return cfg } - + /// Returns the configuration errors (if any). func configErrors() -> [String] { guard let cfg = self.config else { return [] } - + var errors: [String] = []; let errCount = ghostty_config_errors_count(cfg) for i in 0.. UnsafePointer? { // We only support the standard clipboard if (location != GHOSTTY_CLIPBOARD_STANDARD) { return nil } - - guard let appState = self.appState(fromSurface: userdata) else { return nil } + + guard let surface = self.surfaceUserdata(from: userdata) else { return nil } + guard let appState = self.appState(fromView: surface) else { return nil } guard let str = NSPasteboard.general.string(forType: .string) else { return nil } - + // Ghostty requires we cache the string because the pointer we return has to remain // stable until the next call to readClipboard. appState.cached_clipboard_string = str return (str as NSString).utf8String } - + static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e) { // We only support the standard clipboard if (location != GHOSTTY_CLIPBOARD_STANDARD) { return } - + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } let pb = NSPasteboard.general pb.declareTypes([.string], owner: nil) pb.setString(valueStr, forType: .string) } - + static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { guard let newConfig = Self.loadConfig() else { AppDelegate.logger.warning("failed to reload configuration") return nil } - + // Assign the new config. This will automatically free the old config. // It is safe to free the old config from within this function call. let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() state.config = newConfig - + // If we have a delegate, notify. if let delegate = state.delegate { delegate.configDidReload(state) } - + return newConfig } - + static func wakeup(_ userdata: UnsafeMutableRawPointer?) { let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - + // Wakeup can be called from any thread so we schedule the app tick // from the main thread. There is probably some improvements we can make // to coalesce multiple ticks but I don't think it matters from a performance // standpoint since we don't do this much. DispatchQueue.main.async { state.appTick() } } - + static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() guard let titleStr = String(cString: title!, encoding: .utf8) else { return } @@ -351,12 +361,12 @@ extension Ghostty { surfaceView.title = titleStr } } - + static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) { let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() surfaceView.setCursorShape(shape) } - + static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) { let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() surfaceView.setCursorVisibility(visible) @@ -372,10 +382,21 @@ extension Ghostty { ] ) } - + static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } + guard let appState = self.appState(fromView: surface) else { return } + guard appState.windowDecorations else { + let alert = NSAlert() + alert.messageText = "Tabs are disabled" + alert.informativeText = "Enable window decorations to use tabs" + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + _ = alert.runModal() + return + } + NotificationCenter.default.post( name: Notification.ghosttyNewTab, object: surface, @@ -384,10 +405,10 @@ extension Ghostty { ] ) } - + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } - + NotificationCenter.default.post( name: Notification.ghosttyNewWindow, object: surface, @@ -396,16 +417,15 @@ extension Ghostty { ] ) } - + /// Returns the GhosttyState from the given userdata value. - static private func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> AppState? { - let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - guard let surface = surfaceView.surface else { return nil } + static private func appState(fromView view: SurfaceView) -> AppState? { + guard let surface = view.surface else { return nil } guard let app = ghostty_surface_app(surface) else { return nil } guard let app_ud = ghostty_app_userdata(app) else { return nil } return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() } - + /// Returns the surface view from the userdata. static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView? { return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() @@ -428,7 +448,7 @@ extension EnvironmentValues { get { self[GhosttyAppKey.self] } set { self[GhosttyAppKey.self] = newValue } } - + var ghosttyConfig: ghostty_config_t? { get { self[GhosttyConfigKey.self] } set { self[GhosttyConfigKey.self] = newValue } @@ -439,7 +459,7 @@ extension View { func ghosttyApp(_ app: ghostty_app_t?) -> some View { environment(\.ghosttyApp, app) } - + func ghosttyConfig(_ config: ghostty_config_t?) -> some View { environment(\.ghosttyConfig, config) } diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullScreenHandler.swift index a85864b5b..2fe650587 100644 --- a/macos/Sources/Helpers/FullScreenHandler.swift +++ b/macos/Sources/Helpers/FullScreenHandler.swift @@ -1,16 +1,16 @@ import SwiftUI import GhosttyKit -class FullScreenHandler { - var previousTabGroup: NSWindowTabGroup? +class FullScreenHandler { var previousTabGroup: NSWindowTabGroup? var previousTabGroupIndex: Int? var previousContentFrame: NSRect? - var isInFullscreen: Bool = false + var previousStyleMask: NSWindow.StyleMask? = nil // We keep track of whether we entered non-native fullscreen in case // a user goes to fullscreen, changes the config to disable non-native fullscreen // and then wants to toggle it off var isInNonNativeFullscreen: Bool = false + var isInFullscreen: Bool = false func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE @@ -89,6 +89,7 @@ class FullScreenHandler { // This is important: it gives us the full screen, including the // notch area on MacBooks. + self.previousStyleMask = window.styleMask window.styleMask.remove(.titled) // Set frame to screen size, accounting for the menu bar if needed @@ -126,8 +127,8 @@ class FullScreenHandler { func leaveFullscreen(window: NSWindow) { guard let previousFrame = previousContentFrame else { return } - // Restore title bar - window.styleMask.insert(.titled) + // Restore the style mask + window.styleMask = self.previousStyleMask! // Restore previous presentation options NSApp.presentationOptions = [] diff --git a/src/config/Config.zig b/src/config/Config.zig index 3faf0101a..629392436 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -242,7 +242,6 @@ keybind: Keybinds = .{}, /// If false, windows won't have native decorations, i.e. titlebar and /// borders. -/// Currently only supported with GTK. @"window-decoration": bool = true, /// Whether to allow programs running in the terminal to read/write to