diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 4079669da..cde2496c7 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -4,23 +4,23 @@ CFBundleDocumentTypes - - CFBundleTypeExtensions - - command - tool - sh - zsh - csh - pl - - CFBundleTypeIconFile - AppIcon.icns - CFBundleTypeName - Terminal scripts - CFBundleTypeRole - Editor - + + CFBundleTypeExtensions + + command + tool + sh + zsh + csh + pl + + CFBundleTypeIconFile + AppIcon.icns + CFBundleTypeName + Terminal scripts + CFBundleTypeRole + Editor + CFBundleTypeName Folders @@ -42,87 +42,57 @@ + GhosttyCommit + LSEnvironment GHOSTTY_MAC_APP 1 - NSServices - - - NSMenuItem - - default - New Ghostty Tab Here - - NSMessage - openTab - NSRequiredContext - - NSTextContent - FilePath - - NSSendTypes - - NSFilenamesPboardType - public.plain-text - - - - NSMenuItem - - default - New Ghostty Window Here - - NSMessage - openWindow - NSRequiredContext - - NSTextContent - FilePath - - NSSendTypes - - NSFilenamesPboardType - public.plain-text - - - NSHighResolutionCapable - NSAppleEventsUsageDescription - A program in Ghostty wants to use AppleScript. - NSCalendarsUsageDescription - A program in Ghostty wants to use your calendar. - NSCameraUsageDescription - A program in Ghostty wants to use the camera. - NSContactsUsageDescription - A program in Ghostty wants to use your contacts. - NSLocalNetworkUsageDescription - A program in Ghostty wants to access the local network. - NSLocationTemporaryUsageDescriptionDictionary - A program in Ghostty wants to use your location temporarily. - NSLocationUsageDescription - A program in Ghostty wants to use your location information. - NSMicrophoneUsageDescription - A program in Ghostty wants to use your microphone. - NSMotionUsageDescription - A program in Ghostty wants to access motion data. - NSPhotoLibraryUsageDescription - A program in Ghostty wants to use your photo library. - NSRemindersUsageDescription - A program in Ghostty wants to access your reminders. - NSSpeechRecognitionUsageDescription - A program in Ghostty wants to use speech recognition. - NSSystemAdministrationUsageDescription - A program in Ghostty requires elevated privileges. + NSServices + + + NSMenuItem + + default + New Ghostty Tab Here + + NSMessage + openTab + NSRequiredContext + + NSTextContent + FilePath + + NSSendTypes + + NSFilenamesPboardType + public.plain-text + + + + NSMenuItem + + default + New Ghostty Window Here + + NSMessage + openWindow + NSRequiredContext + + NSTextContent + FilePath + + NSSendTypes + + NSFilenamesPboardType + public.plain-text + + + SUPublicEDKey wsNcGf5hirwtdXMVnYoxRIX/SqZQLMOsYlD3q3imeok= - CFBundleVersion - - CFBundleShortVersionString - - GhosttyCommit - diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index e3ad5adf3..502eb3e6a 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; }; 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; - 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; @@ -22,6 +21,9 @@ A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; }; A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; }; A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */; }; + A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */; }; + A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */; }; + A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */; }; A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; }; A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; }; A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */; }; @@ -94,7 +96,6 @@ 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = ""; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; - 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = ""; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; @@ -105,6 +106,9 @@ A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = ""; }; A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = ""; }; A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalScreen.swift; sourceTree = ""; }; + A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fullscreen.swift; sourceTree = ""; }; + A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FullscreenMode+Extension.swift"; sourceTree = ""; }; + A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+Extension.swift"; sourceTree = ""; }; A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = ""; }; A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = ""; }; A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = ""; }; @@ -233,11 +237,12 @@ A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, A5D0AF3C2B37804400D21823 /* CodableBridge.swift */, - 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */, + A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, + A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, @@ -314,6 +319,7 @@ A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */, A55685DF29A03A9F004303CE /* AppError.swift */, + A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */, ); path = Ghostty; sourceTree = ""; @@ -573,8 +579,10 @@ A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */, A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */, AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */, + A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, + A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, @@ -597,8 +605,8 @@ A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */, - 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, + A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */, AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */, C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, @@ -704,8 +712,21 @@ INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript."; + INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar."; + INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera."; + INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts."; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network."; + INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily."; + INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information."; INFOPLIST_KEY_NSMainNibFile = MainMenu; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone."; + INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library."; + INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition."; + INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -858,8 +879,21 @@ INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript."; + INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar."; + INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera."; + INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts."; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network."; + INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily."; + INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information."; INFOPLIST_KEY_NSMainNibFile = MainMenu; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone."; + INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library."; + INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition."; + INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -867,7 +901,7 @@ MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; - PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty.debug; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "Sources/App/macOS/ghostty-bridging-header.h"; @@ -898,8 +932,21 @@ INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript."; + INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar."; + INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera."; + INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts."; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network."; + INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily."; + INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information."; INFOPLIST_KEY_NSMainNibFile = MainMenu; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone."; + INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library."; + INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition."; + INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bb8b5665d..2d230561b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -4,12 +4,13 @@ import SwiftUI import GhosttyKit /// A classic, tabbed terminal experience. -class TerminalController: BaseTerminalController +class TerminalController: BaseTerminalController, + FullscreenDelegate { override var windowNibName: NSNib.Name? { "Terminal" } /// Fullscreen state management. - let fullscreenHandler = FullScreenHandler() + private(set) var fullscreenStyle: FullscreenStyle? /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail @@ -199,6 +200,63 @@ class TerminalController: BaseTerminalController } } + // MARK: Fullscreen + + /// Toggle fullscreen for the given mode. + func toggleFullscreen(mode: FullscreenMode) { + // We need a window to fullscreen + guard let window = self.window else { return } + + // If we have a previous fullscreen style initialized, we want to check if + // our mode changed. If it changed and we're in fullscreen, we exit so we can + // toggle it next time. If it changed and we're not in fullscreen we can just + // switch the handler. + var newStyle = mode.style(for: window) + newStyle?.delegate = self + old: if let oldStyle = self.fullscreenStyle { + // If we're not fullscreen, we can nil it out so we get the new style + if !oldStyle.isFullscreen { + self.fullscreenStyle = newStyle + break old + } + + assert(oldStyle.isFullscreen) + + // We consider our mode changed if the types change (obvious) but + // also if its nil (not obvious) because nil means that the style has + // likely changed but we don't support it. + if newStyle == nil || type(of: newStyle) != type(of: oldStyle) { + // Our mode changed. Exit fullscreen (since we're toggling anyways) + // and then unset the style so that we replace it next time. + oldStyle.exit() + self.fullscreenStyle = nil + + // We're done + return + } + + // Style is the same. + } else { + // We have no previous style + self.fullscreenStyle = newStyle + } + guard let fullscreenStyle else { return } + + if fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + } else { + fullscreenStyle.enter() + } + } + + func fullscreenDidChange() { + // For some reason focus can get lost when we change fullscreen. Regardless of + // mode above we just move it back. + if let focusedSurface { + Ghostty.moveFocus(to: focusedSurface) + } + } + //MARK: - NSWindowController override func windowWillLoad() { @@ -531,17 +589,16 @@ class TerminalController: BaseTerminalController guard let target = notification.object as? Ghostty.SurfaceView else { return } guard target == self.focusedSurface else { return } - // We need a window to fullscreen - guard let window = self.window else { return } - - // Check whether we use non-native fullscreen - guard let fullscreenModeAny = notification.userInfo?[Ghostty.Notification.FullscreenModeKey] else { return } - guard let fullscreenMode = fullscreenModeAny as? ghostty_action_fullscreen_e else { return } - self.fullscreenHandler.toggleFullscreen(window: window, mode: fullscreenMode) - - // For some reason focus always gets lost when we toggle fullscreen, so we set it back. - if let focusedSurface { - Ghostty.moveFocus(to: focusedSurface) + // Get the fullscreen mode we want to toggle + let fullscreenMode: FullscreenMode + if let any = notification.userInfo?[Ghostty.Notification.FullscreenModeKey], + let mode = any as? FullscreenMode { + fullscreenMode = mode + } else { + Ghostty.logger.warning("no fullscreen mode specified or invalid mode, doing nothing") + return } + + toggleFullscreen(mode: fullscreenMode) } } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 3930012df..f71e198ee 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -65,17 +65,25 @@ class TerminalManager { let c = createWindow(withBaseConfig: base) let window = c.window! - // We want to go fullscreen if we're configured for new windows to go fullscreen - var toggleFullScreen = ghostty.config.windowFullscreen - - // If the previous focused window prior to creating this window is fullscreen, - // then this window also becomes fullscreen. - if let parent = focusedSurface?.window, parent.styleMask.contains(.fullScreen) { - toggleFullScreen = true - } - - if (toggleFullScreen && !window.styleMask.contains(.fullScreen)) { + // If the previous focused window was native fullscreen, the new window also + // becomes native fullscreen. + if let parent = focusedSurface?.window, + parent.styleMask.contains(.fullScreen) { window.toggleFullScreen(nil) + } else if ghostty.config.windowFullscreen { + switch (ghostty.config.windowFullscreenMode) { + case .native: + // Native has to be done immediately so that our stylemask contains + // fullscreen for the logic later in this method. + c.toggleFullscreen(mode: .native) + + case .nonNative, .nonNativeVisibleMenu: + // If we're non-native then we have to do it on a later loop + // so that the content view is setup. + DispatchQueue.main.async { + c.toggleFullscreen(mode: self.ghostty.config.windowFullscreenMode) + } + } } // If our app isn't active, we make it active. All new_window actions @@ -114,7 +122,8 @@ class TerminalManager { // If our parent is in non-native fullscreen, then new tabs do not work. // See: https://github.com/mitchellh/ghostty/issues/392 if let controller = parent.windowController as? TerminalController, - controller.fullscreenHandler.isInNonNativeFullscreen { + let fullscreenStyle = controller.fullscreenStyle, + fullscreenStyle.isFullscreen && !fullscreenStyle.supportsTabs { let alert = NSAlert() alert.messageText = "Cannot Create New Tab" alert.informativeText = "New tabs are unsupported while in non-native fullscreen. Exit fullscreen and try again." diff --git a/macos/Sources/Ghostty/FullscreenMode+Extension.swift b/macos/Sources/Ghostty/FullscreenMode+Extension.swift new file mode 100644 index 000000000..fffd8e84b --- /dev/null +++ b/macos/Sources/Ghostty/FullscreenMode+Extension.swift @@ -0,0 +1,20 @@ +import GhosttyKit + +extension FullscreenMode { + /// Initialize from a Ghostty fullscreen action. + static func from(ghostty: ghostty_action_fullscreen_e) -> Self? { + return switch ghostty { + case GHOSTTY_FULLSCREEN_NATIVE: + .native + + case GHOSTTY_FULLSCREEN_NON_NATIVE: + .nonNative + + case GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU: + .nonNativeVisibleMenu + + default: + nil + } + } +} diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 05c01a75e..70e4ca94c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -598,7 +598,7 @@ extension Ghostty { private static func toggleFullscreen( _ app: ghostty_app_t, target: ghostty_target_s, - mode: ghostty_action_fullscreen_e) { + mode raw: ghostty_action_fullscreen_e) { switch (target.tag) { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle fullscreen does nothing with an app target") @@ -607,6 +607,10 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let mode = FullscreenMode.from(ghostty: raw) else { + Ghostty.logger.warning("unknow fullscreen mode raw=\(raw.rawValue)") + return + } NotificationCenter.default.post( name: Notification.ghosttyToggleFullscreen, object: surfaceView, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 76f85d2a3..40a0e0fde 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -219,6 +219,28 @@ extension Ghostty { return v } + #if canImport(AppKit) + var windowFullscreenMode: FullscreenMode { + let defaultValue: FullscreenMode = .native + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "macos-non-native-fullscreen" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return switch str { + case "false": + .native + case "true": + .nonNative + case "visible-menu": + .nonNativeVisibleMenu + default: + defaultValue + } + } + #endif + var windowTitleFontFamily: String? { guard let config = self.config else { return nil } var v: UnsafePointer? = nil diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullScreenHandler.swift deleted file mode 100644 index d12809d71..000000000 --- a/macos/Sources/Helpers/FullScreenHandler.swift +++ /dev/null @@ -1,239 +0,0 @@ -import SwiftUI -import GhosttyKit - -class FullScreenHandler { - var previousTabGroup: NSWindowTabGroup? - var previousTabGroupIndex: Int? - var previousContentFrame: NSRect? - 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, mode: ghostty_action_fullscreen_e) { - let useNonNativeFullscreen = switch (mode) { - case GHOSTTY_FULLSCREEN_NATIVE: - false - - case GHOSTTY_FULLSCREEN_NON_NATIVE, GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU: - true - - default: - false - } - - if isInFullscreen { - if useNonNativeFullscreen || isInNonNativeFullscreen { - leaveFullscreen(window: window) - isInNonNativeFullscreen = false - } else { - // Restore titlebar separator style. See below for explanation. - window.titlebarSeparatorStyle = .automatic - window.toggleFullScreen(nil) - } - isInFullscreen = false - } else { - if useNonNativeFullscreen { - let hideMenu = mode != GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU - enterFullscreen(window: window, hideMenu: hideMenu) - isInNonNativeFullscreen = true - } else { - // The titlebar separator shows up erroneously in fullscreen if the tab bar - // is made to appear and then disappear by opening and then closing a tab. - // We get rid of the separator while in fullscreen to prevent this. - window.titlebarSeparatorStyle = .none - window.toggleFullScreen(nil) - } - isInFullscreen = true - } - } - - func enterFullscreen(window: NSWindow, hideMenu: Bool) { - guard let screen = window.screen else { return } - guard let contentView = window.contentView else { return } - - previousTabGroup = window.tabGroup - previousTabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) - - // Save previous contentViewFrame and screen - previousContentFrame = window.convertToScreen(contentView.frame) - - // Change presentation style to hide menu bar and dock if needed - // It's important to do this in two calls, because setting them in a single call guarantees - // that the menu bar will also be hidden on any additional displays (why? nobody knows!) - // When these options are set separately, the menu bar hiding problem will only occur in - // specific scenarios. More investigation is needed to pin these scenarios down precisely, - // but it seems to have something to do with which app had focus last. - // Furthermore, it's much easier to figure out which screen the dock is on if the menubar - // has not yet been hidden, so the order matters here! - if (shouldHideDock(screen: screen)) { - self.hideDock() - - // Ensure that we always hide the dock bar for this window, but not for non fullscreen ones - NotificationCenter.default.addObserver( - self, - selector: #selector(FullScreenHandler.hideDock), - name: NSWindow.didBecomeMainNotification, - object: window) - NotificationCenter.default.addObserver( - self, - selector: #selector(FullScreenHandler.unHideDock), - name: NSWindow.didResignMainNotification, - object: window) - } - if (hideMenu) { - self.hideMenu() - - // Ensure that we always hide the menu bar for this window, but not for non fullscreen ones - // This is not the best way to do this, not least because it causes the menu to stay visible - // for a brief moment before being hidden in some cases (e.g. when switching spaces). - // If we end up adding a NSWindowDelegate to PrimaryWindow, then we may be better off - // handling this there. - NotificationCenter.default.addObserver( - self, - selector: #selector(FullScreenHandler.hideMenu), - name: NSWindow.didBecomeMainNotification, - object: window) - NotificationCenter.default.addObserver( - self, - selector: #selector(FullScreenHandler.onDidResignMain), - name: NSWindow.didResignMainNotification, - object: window) - } - - // 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 - let frame = calculateFullscreenFrame(screen: screen, subtractMenu: !hideMenu) - window.setFrame(frame, display: true) - - // Focus window - window.makeKeyAndOrderFront(nil) - } - - @objc func hideMenu() { - NSApp.presentationOptions.insert(.autoHideMenuBar) - } - - @objc func onDidResignMain(_ notification: Notification) { - guard let resigningWindow = notification.object as? NSWindow else { return } - guard let mainWindow = NSApplication.shared.mainWindow else { return } - - // We're only unhiding the menu bar, if the focus shifted within our application. - // In that case, `mainWindow` is the window of our application the focus shifted - // to. - if !resigningWindow.isEqual(mainWindow) { - NSApp.presentationOptions.remove(.autoHideMenuBar) - } - } - - @objc func hideDock() { - NSApp.presentationOptions.insert(.autoHideDock) - } - - @objc func unHideDock() { - NSApp.presentationOptions.remove(.autoHideDock) - } - - func calculateFullscreenFrame(screen: NSScreen, subtractMenu: Bool)->NSRect { - if (subtractMenu) { - if let menuHeight = NSApp.mainMenu?.menuBarHeight { - var padding: CGFloat = 0 - - // Detect the notch. If there is a safe area on top it includes the - // menu height as a safe area so we also subtract that from it. - if (screen.safeAreaInsets.top > 0) { - padding = screen.safeAreaInsets.top - menuHeight; - } - - return NSMakeRect( - screen.frame.minX, - screen.frame.minY, - screen.frame.width, - screen.frame.height - (menuHeight + padding) - ) - } - } - return screen.frame - } - - func leaveFullscreen(window: NSWindow) { - guard let previousFrame = previousContentFrame else { return } - - // Restore the style mask - window.styleMask = self.previousStyleMask! - - // Restore previous presentation options - NSApp.presentationOptions = [] - - // Stop handling any window focus notifications - // that we use to manage menu bar visibility - NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window) - NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window) - - // Restore frame - window.setFrame(window.frameRect(forContentRect: previousFrame), display: true) - - // Have titlebar tabs set itself up again, since removing the titlebar when fullscreen breaks its constraints. - if let window = window as? TerminalWindow, window.titlebarTabs { - window.titlebarTabs = true - } - - // If the window was previously in a tab group that isn't empty now, we re-add it - if let group = previousTabGroup, let tabIndex = previousTabGroupIndex, !group.windows.isEmpty { - var tabWindow: NSWindow? - var order: NSWindow.OrderingMode = .below - - // Index of the window before `window` - let tabIndexBefore = tabIndex-1 - if tabIndexBefore < 0 { - // If we were the first tab, we add the window *before* (.below) the first one. - tabWindow = group.windows.first - } else if tabIndexBefore < group.windows.count { - // If we weren't the first tab in the group, we add our window after - // the tab that was before it. - tabWindow = group.windows[tabIndexBefore] - order = .above - } else { - // If index is after group, add it after last window - tabWindow = group.windows.last - } - - // Add the window - tabWindow?.addTabbedWindow(window, ordered: order) - } - - // Focus window - window.makeKeyAndOrderFront(nil) - } - - // We only want to hide the dock if it's not already going to be hidden automatically, and if - // it's on the same display as the ghostty window that we want to make fullscreen. - func shouldHideDock(screen: NSScreen) -> Bool { - if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { - if (dockAutohide) { return false } - } - - // There is no public API to directly ask about dock visibility, so we have to figure it out - // by comparing the sizes of visibleFrame (the currently usable area of the screen) and - // frame (the full screen size). We also need to account for the menubar, any inset caused - // by the notch on macbooks, and a little extra padding to compensate for the boundary area - // which triggers showing the dock. - let frame = screen.frame - let visibleFrame = screen.visibleFrame - let menuHeight = NSApp.mainMenu?.menuBarHeight ?? 0 - var notchInset = 0.0 - if #available(macOS 12, *) { - notchInset = screen.safeAreaInsets.top - } - let boundaryAreaPadding = 5.0 - - return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding) || visibleFrame.width < frame.width - } -} diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift new file mode 100644 index 000000000..65de2f627 --- /dev/null +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -0,0 +1,362 @@ +import Cocoa +import GhosttyKit + +/// The fullscreen modes we support define how the fullscreen behaves. +enum FullscreenMode { + case native + case nonNative + case nonNativeVisibleMenu + + /// Initializes the fullscreen style implementation for the mode. This will not toggle any + /// fullscreen properties. This may fail if the window isn't configured properly for a given + /// mode. + func style(for window: NSWindow) -> FullscreenStyle? { + switch self { + case .native: + return NativeFullscreen(window) + + case .nonNative: + return NonNativeFullscreen(window) + + case .nonNativeVisibleMenu: + return NonNativeFullscreenVisibleMenu(window) + } + } +} + +/// Protocol that must be implemented by all fullscreen styles. +protocol FullscreenStyle { + var delegate: FullscreenDelegate? { get set } + var isFullscreen: Bool { get } + var supportsTabs: Bool { get } + init?(_ window: NSWindow) + func enter() + func exit() +} + +/// Delegate that can be implemented for fullscreen implementations. +protocol FullscreenDelegate: AnyObject { + /// Called whenever the fullscreen state changed. You can call isFullscreen to see + /// the current state. + func fullscreenDidChange() +} + +extension FullscreenDelegate { + func fullscreenDidChange() {} +} + +/// macOS native fullscreen. This is the typical behavior you get by pressing the green fullscreen +/// button on regular titlebars. +class NativeFullscreen: FullscreenStyle { + private let window: NSWindow + + weak var delegate: FullscreenDelegate? + var isFullscreen: Bool { window.styleMask.contains(.fullScreen) } + var supportsTabs: Bool { true } + + required init?(_ window: NSWindow) { + // TODO: There are many requirements for native fullscreen we should + // check here such as the stylemask. + + self.window = window + } + + func enter() { + guard !isFullscreen else { return } + + // The titlebar separator shows up erroneously in fullscreen if the tab bar + // is made to appear and then disappear by opening and then closing a tab. + // We get rid of the separator while in fullscreen to prevent this. + window.titlebarSeparatorStyle = .none + + // Enter fullscreen + window.toggleFullScreen(self) + + // Notify the delegate + delegate?.fullscreenDidChange() + } + + func exit() { + guard isFullscreen else { return } + + // Restore titlebar separator style. See enter for explanation. + window.titlebarSeparatorStyle = .automatic + + window.toggleFullScreen(nil) + + // Notify the delegate + delegate?.fullscreenDidChange() + } +} + +class NonNativeFullscreen: FullscreenStyle { + weak var delegate: FullscreenDelegate? + + // Non-native fullscreen never supports tabs because tabs require + // the "titled" style and we don't have it for non-native fullscreen. + var supportsTabs: Bool { false } + + // isFullscreen is dependent on if we have saved state currently. We + // could one day try to do fancier stuff like inspecting the window + // state but there isn't currently a need for it. + var isFullscreen: Bool { savedState != nil } + + // The default properties. Subclasses can override this to change + // behavior. This shouldn't be written to (only computed) because + // it must be immutable. + var properties: Properties { Properties() } + + struct Properties { + var hideMenu: Bool = true + } + + private let window: NSWindow + private var savedState: SavedState? + + required init?(_ window: NSWindow) { + self.window = window + } + + func enter() { + // If we are in fullscreen we don't do it again. + guard !isFullscreen else { return } + + // If we are in native fullscreen, exit native fullscreen. This is counter + // intuitive but if we entered native fullscreen (through the green max button + // or an external event) and we press the fullscreen keybind, we probably + // want to EXIT fullscreen. + if window.styleMask.contains(.fullScreen) { + window.toggleFullScreen(nil) + return + } + + // This is the screen that we're going to go fullscreen on. We use the + // screen the window is currently on. + guard let screen = window.screen else { return } + + // Save the state that we need to exit again + guard let savedState = SavedState(window) else { return } + self.savedState = savedState + + // We hide the dock if the window is on a screen with the dock. + if (savedState.dock) { + hideDock() + } + + // Hide the menu if requested + if (properties.hideMenu) { + hideMenu() + } + + // When this window becomes or resigns main we need to run some logic. + NotificationCenter.default.addObserver( + self, + selector: #selector(windowDidBecomeMain), + name: NSWindow.didBecomeMainNotification, + object: window) + NotificationCenter.default.addObserver( + self, + selector: #selector(windowDidResignMain), + name: NSWindow.didResignMainNotification, + object: window) + + // When we change screens we need to redo everything. + NotificationCenter.default.addObserver( + self, + selector: #selector(windowDidChangeScreen), + name: NSWindow.didChangeScreenNotification, + object: window) + + // Being untitled let's our content take up the full frame. + window.styleMask.remove(.titled) + + // Focus window + window.makeKeyAndOrderFront(nil) + + // Set frame to screen size, accounting for any elements such as the menu bar. + // We do this async so that all the style edits above (title removal, dock + // hide, menu hide, etc.) take effect. This fixes: + // https://github.com/ghostty-org/ghostty/issues/1996 + DispatchQueue.main.async { + self.window.setFrame(self.fullscreenFrame(screen), display: true) + self.delegate?.fullscreenDidChange() + } + } + + func exit() { + guard isFullscreen else { return } + guard let savedState else { return } + + // Remove all our notifications + NotificationCenter.default.removeObserver(self) + + // Unhide our elements + if savedState.dock { + unhideDock() + } + unhideMenu() + + // Restore our saved state + window.styleMask = savedState.styleMask + window.setFrame(window.frameRect(forContentRect: savedState.contentFrame), display: true) + + // This is a hack that I want to remove from this but for now, we need to + // fix up the titlebar tabs here before we do everything below. + if let window = window as? TerminalWindow, + window.titlebarTabs { + window.titlebarTabs = true + } + + // If the window was previously in a tab group that isn't empty now, + // we re-add it. We have to do this because our process of doing non-native + // fullscreen removes the window from the tab group. + if let tabGroup = savedState.tabGroup, + let tabIndex = savedState.tabGroupIndex, + !tabGroup.windows.isEmpty { + if tabIndex == 0 { + // We were previously the first tab. Add it before ("below") + // the first window in the tab group currently. + tabGroup.windows.first!.addTabbedWindow(window, ordered: .below) + } else if tabIndex <= tabGroup.windows.count { + // We were somewhere in the middle + tabGroup.windows[tabIndex - 1].addTabbedWindow(window, ordered: .above) + } else { + // We were at the end + tabGroup.windows.last!.addTabbedWindow(window, ordered: .below) + } + } + + // Unset our saved state, we're restored! + self.savedState = nil + + // Focus window + window.makeKeyAndOrderFront(nil) + + // Notify the delegate + self.delegate?.fullscreenDidChange() + } + + private func fullscreenFrame(_ screen: NSScreen) -> NSRect { + // It would make more sense to use "visibleFrame" but visibleFrame + // will omit space by our dock and isn't updated until an event + // loop tick which we don't have time for. So we use frame and + // calculate this ourselves. + var frame = screen.frame + + if (!properties.hideMenu) { + // We need to subtract the menu height since we're still showing it. + frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 + + // NOTE on macOS bugs: macOS used to have a bug where menuBarHeight + // didn't account for the notch. I reported this as a radar and it + // was fixed at some point. I don't know when that was so I can't + // put an #available check, but it was in a bug fix release so I think + // if a bug is reported to Ghostty we can just advise the user to + // update. + } + + return frame + } + + // MARK: Window Events + + @objc func windowDidChangeScreen(_ notification: Notification) { + guard isFullscreen else { return } + guard let savedState else { return } + + // This should always be true due to how we register but just be sure + guard let object = notification.object as? NSWindow, + object == window else { return } + + // Our screens must have changed + guard savedState.screen != window.screen else { return } + + // When we change screens, we simply exit fullscreen. Changing + // screens shouldn't naturally be possible, it can only happen + // through external window managers. There's a lot of accounting + // to do to get the screen change right so instead of breaking + // we just exit out. The user can re-enter fullscreen thereafter. + exit() + } + + @objc func windowDidBecomeMain(_ notification: Notification) { + guard let savedState else { return } + + // This should always be true due to how we register but just be sure + guard let object = notification.object as? NSWindow, + object == window else { return } + + // This is crazy but at least on macOS 15.0, you must hide the dock + // FIRST then hide the menu. If you do the opposite, it does not + // work. + + if savedState.dock { + hideDock() + } + + if (properties.hideMenu) { + hideMenu() + } + } + + @objc func windowDidResignMain(_ notification: Notification) { + guard let savedState else { return } + + // This should always be true due to how we register but just be sure + guard let object = notification.object as? NSWindow, + object == window else { return } + + if (properties.hideMenu) { + unhideMenu() + } + + if savedState.dock { + unhideDock() + } + } + + // MARK: Dock + + private func hideDock() { + NSApp.presentationOptions.insert(.autoHideDock) + } + + private func unhideDock() { + NSApp.presentationOptions.remove(.autoHideDock) + } + + // MARK: Menu + + func hideMenu() { + NSApp.presentationOptions.insert(.autoHideMenuBar) + } + + func unhideMenu() { + NSApp.presentationOptions.remove(.autoHideMenuBar) + } + + /// The state that must be saved for non-native fullscreen to exit fullscreen. + class SavedState { + weak var screen: NSScreen? + let tabGroup: NSWindowTabGroup? + let tabGroupIndex: Int? + let contentFrame: NSRect + let styleMask: NSWindow.StyleMask + let dock: Bool + + init?(_ window: NSWindow) { + guard let contentView = window.contentView else { return nil } + + self.screen = window.screen + self.tabGroup = window.tabGroup + self.tabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) + self.contentFrame = window.convertToScreen(contentView.frame) + self.styleMask = window.styleMask + self.dock = window.screen?.hasDock ?? false + } + } +} + +class NonNativeFullscreenVisibleMenu: NonNativeFullscreen { + override var properties: Properties { Properties(hideMenu: false) } +} diff --git a/macos/Sources/Helpers/NSScreen+Extension.swift b/macos/Sources/Helpers/NSScreen+Extension.swift new file mode 100644 index 000000000..f5a08b524 --- /dev/null +++ b/macos/Sources/Helpers/NSScreen+Extension.swift @@ -0,0 +1,36 @@ +import Cocoa + +extension NSScreen { + // Returns true if the given screen has a visible dock. This isn't + // point-in-time visible, this is true if the dock is always visible + // AND present on this screen. + var hasDock: Bool { + // If the dock autohides then we don't have a dock ever. + if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { + if (dockAutohide) { return false } + } + + // There is no public API to directly ask about dock visibility, so we have to figure it out + // by comparing the sizes of visibleFrame (the currently usable area of the screen) and + // frame (the full screen size). We also need to account for the menubar, any inset caused + // by the notch on macbooks, and a little extra padding to compensate for the boundary area + // which triggers showing the dock. + + // If our visible width is less than the frame we assume its the dock. + if (visibleFrame.width < frame.width) { + return true + } + + // We need to see if our visible frame height is less than the full + // screen height minus the menu and notch and such. + let menuHeight = NSApp.mainMenu?.menuBarHeight ?? 0 + let notchInset: CGFloat = if #available(macOS 12, *) { + safeAreaInsets.top + } else { + 0 + } + let boundaryAreaPadding = 5.0 + + return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding) + } +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 0f5e9b81b..35156dc18 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1375,8 +1375,15 @@ keybind: Keybinds = .{}, /// using a new space. It's faster than the native fullscreen mode since it /// doesn't use animations. /// -/// Warning: tabs do not work with a non-native fullscreen window. This -/// can be fixed but is looking for contributors to help. See issue #392. +/// Important: tabs DO NOT WORK in this mode. Non-native fullscreen removes +/// the titlebar and macOS native tabs require the titlebar. If you use tabs, +/// you should not use this mode. +/// +/// If you fullscreen a window with tabs, the currently focused tab will +/// become fullscreen while the others will remain in a separate window in +/// the background. You can switch to that window using normal window-switching +/// keybindings such as command+tilde. When you exit fullscreen, the window +/// will return to the tabbed state it was in before. /// /// Allowable values are: /// @@ -1384,6 +1391,9 @@ keybind: Keybinds = .{}, /// * `true` - Use non-native macOS fullscreen, hide the menu bar /// * `false` - Use native macOS fullscreen /// +/// Changing this option at runtime works, but will only apply to the next +/// time the window is made fullscreen. If a window is already fullscreen, +/// it will retain the previous setting until fullscreen is exited. @"macos-non-native-fullscreen": NonNativeFullscreen = .false, /// The style of the macOS titlebar. Available values are: "native",