diff --git a/include/ghostty.h b/include/ghostty.h index 2cb915ddd..14d8d2b02 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -239,7 +239,7 @@ typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e); typedef void (*ghostty_runtime_close_surface_cb)(void *, bool); typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e); typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t); -typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *); +typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, bool); typedef struct { void *userdata; diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 2b29f105b..b4df4d048 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; }; + 85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */; }; + 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; + 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */; }; + A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; }; + A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */ = {isa = PBXBuildFile; fileRef = A545D1A12A5772CE006E0AE4 /* shell-integration */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; @@ -17,16 +23,21 @@ A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; - A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; }; A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; }; A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; - A5FECBD729D1FC3900022361 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* ContentView.swift */; }; + A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* PrimaryView.swift */; }; A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = ""; }; + 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowController.swift; sourceTree = ""; }; + 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; + 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindow.swift; sourceTree = ""; }; + A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowManager.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; A545D1A12A5772CE006E0AE4 /* shell-integration */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "shell-integration"; path = "../zig-out/share/shell-integration"; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; @@ -38,14 +49,13 @@ A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; - A5B30534299BEAAA0047F10C /* GhosttyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyApp.swift; sourceTree = ""; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = ""; }; A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; - A5FECBD629D1FC3900022361 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = ""; }; A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -61,19 +71,54 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + A53426362A7DC53000EBB7A2 /* Features */ = { + isa = PBXGroup; + children = ( + A53426372A7DC53A00EBB7A2 /* Primary Window */, + A534263E2A7DCC5800EBB7A2 /* Settings */, + ); + path = Features; + sourceTree = ""; + }; + A53426372A7DC53A00EBB7A2 /* Primary Window */ = { + isa = PBXGroup; + children = ( + A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */, + 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */, + 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */, + A5FECBD629D1FC3900022361 /* PrimaryView.swift */, + A535B9D9299C569B0017E2E4 /* ErrorView.swift */, + ); + path = "Primary Window"; + sourceTree = ""; + }; + A534263D2A7DCBB000EBB7A2 /* Helpers */ = { + isa = PBXGroup; + children = ( + A5CEAFFE29C2410700646FDA /* Backport.swift */, + 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */, + A5FECBD829D2010400022361 /* WindowAccessor.swift */, + A5CEAFDA29B8005900646FDA /* SplitView */, + ); + path = Helpers; + sourceTree = ""; + }; + A534263E2A7DCC5800EBB7A2 /* Settings */ = { + isa = PBXGroup; + children = ( + A59444F629A2ED5200725BBA /* SettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; A54CD6ED299BEB14008C95BB /* Sources */ = { isa = PBXGroup; children = ( - A5D495A0299BEC2200DD1313 /* Preview Content */, - A5CEAFDA29B8005900646FDA /* SplitView */, + A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */, + 857F63802A5E64F200CA4815 /* MainMenu.xib */, + A53426362A7DC53000EBB7A2 /* Features */, + A534263D2A7DCBB000EBB7A2 /* Helpers */, A55B7BB429B6F4410055DE60 /* Ghostty */, - A5B30534299BEAAA0047F10C /* GhosttyApp.swift */, - A535B9D9299C569B0017E2E4 /* ErrorView.swift */, - A55685DF29A03A9F004303CE /* AppError.swift */, - A59444F629A2ED5200725BBA /* SettingsView.swift */, - A5CEAFFE29C2410700646FDA /* Backport.swift */, - A5FECBD629D1FC3900022361 /* ContentView.swift */, - A5FECBD829D2010400022361 /* WindowAccessor.swift */, ); path = Sources; sourceTree = ""; @@ -85,6 +130,7 @@ A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */, + A55685DF29A03A9F004303CE /* AppError.swift */, ); path = Ghostty; sourceTree = ""; @@ -128,13 +174,6 @@ path = SplitView; sourceTree = ""; }; - A5D495A0299BEC2200DD1313 /* Preview Content */ = { - isa = PBXGroup; - children = ( - ); - path = "Preview Content"; - sourceTree = ""; - }; A5D495A3299BECBA00DD1313 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -204,6 +243,7 @@ A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, + 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -214,9 +254,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */, + 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */, + A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, - A5FECBD729D1FC3900022361 /* ContentView.swift in Sources */, + A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */, A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, @@ -224,8 +267,9 @@ A55685E029A03A9F004303CE /* AppError.swift in Sources */, A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, - A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */, + 85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, + 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -357,7 +401,6 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -391,7 +434,6 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme new file mode 100644 index 000000000..9fc836f02 --- /dev/null +++ b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift new file mode 100644 index 000000000..836ac7467 --- /dev/null +++ b/macos/Sources/AppDelegate.swift @@ -0,0 +1,143 @@ +import AppKit +import OSLog +import GhosttyKit + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { + // The application logger. We should probably move this at some point to a dedicated + // class/struct but for now it lives here! 🤷‍♂️ + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: AppDelegate.self) + ) + + // confirmQuit published so other views can check whether quit needs to be confirmed. + @Published var confirmQuit: Bool = false + + /// The ghostty global state. Only one per process. + private var ghostty: Ghostty.AppState = Ghostty.AppState() + + /// Manages windows and tabs, ensuring they're allocated/deallocated correctly + private var windowManager: PrimaryWindowManager! + + override init() { + super.init() + + windowManager = PrimaryWindowManager(ghostty: self.ghostty) + } + + func applicationDidFinishLaunching(_ notification: Notification) { + // System settings overrides + UserDefaults.standard.register(defaults: [ + // Disable this so that repeated key events make it through to our terminal views. + "ApplePressAndHoldEnabled": false, + ]) + + // Let's launch our first window. + // TODO: we should detect if we restored windows and if so not launch a new window. + windowManager.addInitialWindow() + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + let windows = NSApplication.shared.windows + if (windows.isEmpty) { return .terminateNow } + + // This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't + // quite work with SwiftUI because windows are retained on close. So instead we check + // if there are any that are visible. I'm guessing this breaks under certain scenarios. + if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow } + + // If the user is shutting down, restarting, or logging out, we don't confirm quit. + if let event = NSAppleEventManager.shared().currentAppleEvent { + if let why = event.attributeDescriptor(forKeyword: AEKeyword("why?")!) { + switch (why.typeCodeValue) { + case kAEShutDown: + fallthrough + + case kAERestart: + fallthrough + + case kAEReallyLogOut: + return .terminateNow + + default: + break + } + } + } + + // We have some visible window, and all our windows will watch the confirmQuit. + confirmQuit = true + return .terminateLater + } + + @IBAction func newWindow(_ sender: Any?) { + windowManager.addNewWindow() + } + + @IBAction func newTab(_ sender: Any?) { + if let existingWindow = windowManager.mainWindow { + windowManager.addNewTab(to: existingWindow) + } else { + windowManager.addNewWindow() + } + } + + @IBAction func closeWindow(_ sender: Any) { + guard let currentWindow = NSApp.keyWindow else { return } + currentWindow.close() + } + + @IBAction func close(_ sender: Any) { + guard let surface = focusedSurface() else { + self.closeWindow(self) + return + } + + ghostty.requestClose(surface: surface) + } + + private func focusedSurface() -> ghostty_surface_t? { + guard let window = NSApp.keyWindow as? PrimaryWindow else { return nil } + return window.focusedSurfaceWrapper.surface + } + + @IBAction func splitHorizontally(_ sender: Any) { + guard let surface = focusedSurface() else { return } + ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT) + } + + @IBAction func splitVertically(_ sender: Any) { + guard let surface = focusedSurface() else { return } + ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN) + } + + @IBAction func splitMoveFocusPrevious(_ sender: Any) { + splitMoveFocus(direction: .previous) + } + + @IBAction func splitMoveFocusNext(_ sender: Any) { + splitMoveFocus(direction: .next) + } + + @IBAction func splitMoveFocusAbove(_ sender: Any) { + splitMoveFocus(direction: .top) + } + + @IBAction func splitMoveFocusBelow(_ sender: Any) { + splitMoveFocus(direction: .bottom) + } + + @IBAction func splitMoveFocusLeft(_ sender: Any) { + splitMoveFocus(direction: .left) + } + + @IBAction func splitMoveFocusRight(_ sender: Any) { + splitMoveFocus(direction: .right) + } + + func splitMoveFocus(direction: Ghostty.SplitFocusDirection) { + guard let surface = focusedSurface() else { return } + ghostty.splitMoveFocus(surface: surface, direction: direction) + } +} diff --git a/macos/Sources/ErrorView.swift b/macos/Sources/Features/Primary Window/ErrorView.swift similarity index 100% rename from macos/Sources/ErrorView.swift rename to macos/Sources/Features/Primary Window/ErrorView.swift diff --git a/macos/Sources/ContentView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift similarity index 69% rename from macos/Sources/ContentView.swift rename to macos/Sources/Features/Primary Window/PrimaryView.swift index e3161f599..a7843f9ba 100644 --- a/macos/Sources/ContentView.swift +++ b/macos/Sources/Features/Primary Window/PrimaryView.swift @@ -1,16 +1,29 @@ import SwiftUI import GhosttyKit -struct ContentView: View { +struct PrimaryView: View { let ghostty: Ghostty.AppState // We need access to our app delegate to know if we're quitting or not. - @EnvironmentObject private var appDelegate: AppDelegate + // Make sure to use `@ObservedObject` so we can keep track of `appDelegate.confirmQuit`. + @ObservedObject var appDelegate: AppDelegate + + // We need this to report back up the app controller which surface in this view is focused. + let focusedSurfaceWrapper: FocusedSurfaceWrapper // We need access to our window to know if we're the key window to determine // if we show the quit confirmation or not. @State private var window: NSWindow? + // This handles non-native fullscreen + @State private var fullScreen = FullScreenHandler() + + // This seems like a crutch after switchign from SwiftUI to AppKit lifecycle. + @FocusState private var focused: Bool + + @FocusedValue(\.ghosttySurfaceView) private var focusedSurface + @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle + var body: some View { switch ghostty.readiness { case .loading: @@ -35,12 +48,24 @@ struct ContentView: View { }, set: { self.appDelegate.confirmQuit = $0 }) - + Ghostty.TerminalSplit(onClose: Self.closeWindow) .ghosttyApp(ghostty.app!) .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: surfaceTitle) { 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 } + guard let title = newValue else { return } + window.title = title + } .confirmationDialog( "Quit Ghostty?", isPresented: confirmQuitting) { @@ -63,7 +88,7 @@ struct ContentView: View { guard let currentWindow = NSApp.keyWindow else { return } currentWindow.close() } - + private func onGotoTab(notification: SwiftUI.Notification) { // Notification center indiscriminately sends to every subscriber (makes sense) // but we only want to process this once. In order to process it once lets only @@ -93,7 +118,13 @@ struct ContentView: View { // currently focused window. guard let window = self.window else { return } guard window.isKeyWindow else { return } - - window.toggleFullScreen(nil) + + // Check whether we use non-native fullscreen + guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return } + guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? Bool else { return } + + self.fullScreen.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen) + // After toggling fullscreen we need to focus the terminal again. + self.focused = true } } diff --git a/macos/Sources/Features/Primary Window/PrimaryWindow.swift b/macos/Sources/Features/Primary Window/PrimaryWindow.swift new file mode 100644 index 000000000..85ca49601 --- /dev/null +++ b/macos/Sources/Features/Primary Window/PrimaryWindow.swift @@ -0,0 +1,48 @@ +import Cocoa +import SwiftUI +import GhosttyKit + +// FocusedSurfaceWrapper is here so that we can pass a reference down +// the view hierarchy and keep track of which surface is focused. +class FocusedSurfaceWrapper { + var surface: ghostty_surface_t? +} + +// PrimaryWindow is the primary window you'd associate with a terminal: the window +// that contains one or more terminals (splits, and such). +// +// We need to subclass NSWindow so that we can override some methods for features +// such as non-native fullscreen. +class PrimaryWindow: NSWindow { + var focusedSurfaceWrapper: FocusedSurfaceWrapper = FocusedSurfaceWrapper() + + static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate) -> PrimaryWindow { + let window = PrimaryWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, + defer: false) + window.center() + + window.contentView = NSHostingView(rootView: PrimaryView( + ghostty: ghostty, + appDelegate: appDelegate, + focusedSurfaceWrapper: window.focusedSurfaceWrapper)) + + // We do want to cascade when new windows are created + window.windowController?.shouldCascadeWindows = true + + // A default title. This should be overwritten quickly by the Ghostty core. + window.title = "Ghostty 👻" + + return window + } + + override var canBecomeKey: Bool { + return true + } + + override var canBecomeMain: Bool { + return true + } +} diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowController.swift b/macos/Sources/Features/Primary Window/PrimaryWindowController.swift new file mode 100644 index 000000000..180fee960 --- /dev/null +++ b/macos/Sources/Features/Primary Window/PrimaryWindowController.swift @@ -0,0 +1,19 @@ +import Cocoa + +class PrimaryWindowController: NSWindowController { + // Keep track of the last point that our window was launched at so that new + // windows "cascade" over each other and don't just launch directly on top + // of each other. + static var lastCascadePoint = NSPoint(x: 0, y: 0) + + // This is used to programmatically control tabs. + weak var windowManager: PrimaryWindowManager? + + // This is required for the "+" button to show up in the tab bar to add a + // new tab. + override func newWindowForTab(_ sender: Any?) { + guard let window = self.window else { preconditionFailure("Expected window to be loaded") } + guard let manager = self.windowManager else { return } + manager.addNewTab(to: window) + } +} diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift new file mode 100644 index 000000000..16b241070 --- /dev/null +++ b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift @@ -0,0 +1,98 @@ +import Cocoa +import Combine + +// PrimaryWindowManager manages the windows and tabs in the primary window +// of the application. It keeps references to windows and cleans them up when +// they're cloned. +// +// If we ever have multiple tabbed window types we can make this generic but +// right now only our primary window is ever duplicated or tabbed so we're not +// doing that. +// +// It is based on the patterns presented in this blog post: +// https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/ +class PrimaryWindowManager { + struct ManagedWindow { + let windowController: NSWindowController + let window: NSWindow + let closePublisher: AnyCancellable + } + + // Keep track of the last point that our window was launched at so that new + // windows "cascade" over each other and don't just launch directly on top + // of each other. + static var lastCascadePoint = NSPoint(x: 0, y: 0) + + /// Returns the main window of the managed window stack. + /// Falls back the first element if no window is main. Note that this would + /// likely be an internal inconsistency we gracefully handle here. + var mainWindow: NSWindow? { + let mainManagedWindow = managedWindows + .first { $0.window.isMainWindow } + + // In case we run into the inconsistency, let it crash in debug mode so we + // can fix our window management setup to prevent this from happening. + assert(mainManagedWindow != nil || managedWindows.isEmpty) + + return (mainManagedWindow ?? managedWindows.first) + .map { $0.window } + } + + private var ghostty: Ghostty.AppState + private var managedWindows: [ManagedWindow] = [] + + init(ghostty: Ghostty.AppState) { + self.ghostty = ghostty + } + + /// Add the initial window for the application. This should only be called once from the AppDelegate. + func addInitialWindow() { + guard let controller = createWindowController() else { return } + controller.showWindow(self) + let result = addManagedWindow(windowController: controller) + if result == nil { + preconditionFailure("Failed to create initial window") + } + } + + func addNewWindow() { + guard let controller = createWindowController() else { return } + guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } + newWindow.makeKeyAndOrderFront(nil) + } + + func addNewTab(to window: NSWindow) { + guard let controller = createWindowController() else { return } + guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } + window.addTabbedWindow(newWindow, ordered: .above) + newWindow.makeKeyAndOrderFront(nil) + } + + private func createWindowController() -> PrimaryWindowController? { + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } + let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate) + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + let controller = PrimaryWindowController(window: window) + controller.windowManager = self + return controller + } + + private func addManagedWindow(windowController: PrimaryWindowController) -> ManagedWindow? { + guard let window = windowController.window else { return nil } + + let pubClose = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: window) + .sink { notification in + guard let window = notification.object as? NSWindow else { return } + self.removeWindow(window: window) + } + + let managed = ManagedWindow(windowController: windowController, window: window, closePublisher: pubClose) + managedWindows.append(managed) + + return managed + } + + private func removeWindow(window: NSWindow) { + self.managedWindows.removeAll(where: { $0.window === window }) + } +} diff --git a/macos/Sources/SettingsView.swift b/macos/Sources/Features/Settings/SettingsView.swift similarity index 100% rename from macos/Sources/SettingsView.swift rename to macos/Sources/Features/Settings/SettingsView.swift diff --git a/macos/Sources/AppError.swift b/macos/Sources/Ghostty/AppError.swift similarity index 100% rename from macos/Sources/AppError.swift rename to macos/Sources/Ghostty/AppError.swift diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 0d25c8c37..4f5a1358d 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -38,7 +38,7 @@ extension Ghostty { init() { // Initialize ghostty global state. This happens once per process. guard ghostty_init() == GHOSTTY_SUCCESS else { - GhosttyApp.logger.critical("ghostty_init failed") + AppDelegate.logger.critical("ghostty_init failed") readiness = .error return } @@ -63,12 +63,12 @@ extension Ghostty { close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, - toggle_fullscreen_cb: { userdata in AppState.toggleFullscreen(userdata) } + toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, useNonNativeFullscreen: nonNativeFullscreen) } ) // Create the ghostty app. guard let app = ghostty_app_new(&runtime_cfg, cfg) else { - GhosttyApp.logger.critical("ghostty_app_new failed") + AppDelegate.logger.critical("ghostty_app_new failed") readiness = .error return } @@ -87,7 +87,7 @@ extension Ghostty { static func reloadConfig() -> ghostty_config_t? { // Initialize the global configuration. guard let cfg = ghostty_config_new() else { - GhosttyApp.logger.critical("ghostty_config_new failed") + AppDelegate.logger.critical("ghostty_config_new failed") return nil } @@ -189,7 +189,7 @@ extension Ghostty { static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { guard let newConfig = AppState.reloadConfig() else { - GhosttyApp.logger.warning("failed to reload configuration") + AppDelegate.logger.warning("failed to reload configuration") return nil } @@ -219,12 +219,15 @@ extension Ghostty { } } - static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?) { + static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, useNonNativeFullscreen: Bool) { + // togo: use non-native fullscreen guard let surface = self.surfaceUserdata(from: userdata) else { return } NotificationCenter.default.post( name: Notification.ghosttyToggleFullscreen, object: surface, - userInfo: [:] + userInfo: [ + Notification.NonNativeFullscreenKey: useNonNativeFullscreen, + ] ) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 6045d6dbb..d4b3a9fa9 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -81,6 +81,7 @@ extension Ghostty.Notification { /// Toggle fullscreen of current window static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") + static let NonNativeFullscreenKey = ghosttyToggleFullscreen.rawValue } // Make the input enum hashable. diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift deleted file mode 100644 index 85845dd02..000000000 --- a/macos/Sources/GhosttyApp.swift +++ /dev/null @@ -1,197 +0,0 @@ -import OSLog -import SwiftUI -import GhosttyKit - -@main -struct GhosttyApp: App { - static let logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: String(describing: GhosttyApp.self) - ) - - /// The ghostty global state. Only one per process. - @StateObject private var ghostty = Ghostty.AppState() - @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate - - /// The current focused Ghostty surface in this app - @FocusedValue(\.ghosttySurfaceView) private var focusedSurface - - var body: some Scene { - WindowGroup { - ContentView(ghostty: ghostty) - } - - .backport.defaultSize(width: 800, height: 600) - - .commands { - CommandGroup(after: .newItem) { - Button("New Tab", action: Self.newTab).keyboardShortcut("t", modifiers: [.command]) - Divider() - Button("Split Horizontally", action: splitHorizontally).keyboardShortcut("d", modifiers: [.command]) - Button("Split Vertically", action: splitVertically).keyboardShortcut("d", modifiers: [.command, .shift]) - Divider() - Button("Close", action: close).keyboardShortcut("w", modifiers: [.command]) - Button("Close Window", action: Self.closeWindow).keyboardShortcut("w", modifiers: [.command, .shift]) - } - - CommandGroup(before: .windowArrangement) { - Divider() - Button("Select Previous Split") { splitMoveFocus(direction: .previous) } - .keyboardShortcut("[", modifiers: .command) - Button("Select Next Split") { splitMoveFocus(direction: .next) } - .keyboardShortcut("]", modifiers: .command) - Menu("Select Split") { - Button("Select Split Above") { splitMoveFocus(direction: .top) } - .keyboardShortcut(.upArrow, modifiers: [.command, .option]) - Button("Select Split Below") { splitMoveFocus(direction: .bottom) } - .keyboardShortcut(.downArrow, modifiers: [.command, .option]) - Button("Select Split Left") { splitMoveFocus(direction: .left) } - .keyboardShortcut(.leftArrow, modifiers: [.command, .option]) - Button("Select Split Right") { splitMoveFocus(direction: .right)} - .keyboardShortcut(.rightArrow, modifiers: [.command, .option]) - } - - Divider() - } - } - - Settings { - SettingsView() - } - } - - // Create a new tab in the currently active window - static func newTab() { - guard let currentWindow = NSApp.keyWindow else { return } - guard let windowController = currentWindow.windowController else { return } - windowController.newWindowForTab(nil) - if let newWindow = NSApp.keyWindow, currentWindow != newWindow { - currentWindow.addTabbedWindow(newWindow, ordered: .above) - } - } - - static func closeWindow() { - guard let currentWindow = NSApp.keyWindow else { return } - currentWindow.close() - } - - func close() { - guard let surfaceView = focusedSurface else { - Self.closeWindow() - return - } - - guard let surface = surfaceView.surface else { return } - ghostty.requestClose(surface: surface) - } - - func splitHorizontally() { - guard let surfaceView = focusedSurface else { return } - guard let surface = surfaceView.surface else { return } - ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT) - } - - func splitVertically() { - guard let surfaceView = focusedSurface else { return } - guard let surface = surfaceView.surface else { return } - ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN) - } - - func splitMoveFocus(direction: Ghostty.SplitFocusDirection) { - guard let surfaceView = focusedSurface else { return } - guard let surface = surfaceView.surface else { return } - ghostty.splitMoveFocus(surface: surface, direction: direction) - } -} - -class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { - @Published var confirmQuit: Bool = false - - // See CursedMenuManager for more information. - private var menuManager: CursedMenuManager? - - func applicationDidFinishLaunching(_ notification: Notification) { - UserDefaults.standard.register(defaults: [ - // Disable this so that repeated key events make it through to our terminal views. - "ApplePressAndHoldEnabled": false, - ]) - - // Create our menu manager to create some custom menu items that - // we can't create from SwiftUI. - menuManager = CursedMenuManager() - } - - func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - let windows = NSApplication.shared.windows - if (windows.isEmpty) { return .terminateNow } - - // This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't - // quite work with SwiftUI because windows are retained on close. So instead we check - // if there are any that are visible. I'm guessing this breaks under certain scenarios. - if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow } - - // If the user is shutting down, restarting, or logging out, we don't confirm quit. - if let event = NSAppleEventManager.shared().currentAppleEvent { - if let why = event.attributeDescriptor(forKeyword: AEKeyword("why?")!) { - switch (why.typeCodeValue) { - case kAEShutDown: - fallthrough - - case kAERestart: - fallthrough - - case kAEReallyLogOut: - return .terminateNow - - default: - break - } - } - } - - // We have some visible window, and all our windows will watch the confirmQuit. - confirmQuit = true - return .terminateLater - } -} - -/// SwiftUI as of macOS 13.x provides no way to manage the default menu items that are created -/// as part of a WindowGroup. This class is prefixed with "Cursed" because this is a truly cursed -/// solution to the problem and I think its quite brittle. As soon as SwiftUI supports a better option -/// we should conditionally compile for that when supported. -/// -/// The way this works is by setting up KVO on various menu objects and reacting to it. For example, -/// when SwiftUI tries to add a "Close" menu, we intercept it and delete it. Nice try! -private class CursedMenuManager { - var mainToken: NSKeyValueObservation? - var fileToken: NSKeyValueObservation? - - init() { - // If the whole menu changed we want to setup our new KVO - self.mainToken = NSApp.observe(\.mainMenu, options: .new) { app, change in - self.onNewMenu() - } - - // Initial setup - onNewMenu() - } - - private func onNewMenu() { - guard let menu = NSApp.mainMenu else { return } - guard let file = menu.item(withTitle: "File") else { return } - guard let submenu = file.submenu else { return } - fileToken = submenu.observe(\.items) { (_, _) in - let remove = ["Close", "Close All"] - - // We look for the items in reverse since we're removing only the - // ones SwiftUI inserts which are at the end. We make replacements - // which we DON'T want deleted. - let items = submenu.items.reversed() - remove.forEach { title in - if let item = items.first(where: { $0.title.caseInsensitiveCompare(title) == .orderedSame }) { - submenu.removeItem(item) - } - } - } - } -} diff --git a/macos/Sources/Backport.swift b/macos/Sources/Helpers/Backport.swift similarity index 100% rename from macos/Sources/Backport.swift rename to macos/Sources/Helpers/Backport.swift diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullScreenHandler.swift new file mode 100644 index 000000000..b182d4c9c --- /dev/null +++ b/macos/Sources/Helpers/FullScreenHandler.swift @@ -0,0 +1,103 @@ +import SwiftUI + +class FullScreenHandler { + var previousTabGroup: NSWindowTabGroup? + var previousTabGroupIndex: Int? + var previousContentFrame: NSRect? + var previousStyleMask: NSWindow.StyleMask? + var isInFullscreen: Bool = false + + // 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 + + func toggleFullscreen(window: NSWindow, nonNativeFullscreen: Bool) { + if isInFullscreen { + if nonNativeFullscreen || isInNonNativeFullscreen { + leaveFullscreen(window: window) + isInNonNativeFullscreen = false + } else { + window.toggleFullScreen(nil) + } + isInFullscreen = false + } else { + if nonNativeFullscreen { + enterFullscreen(window: window) + isInNonNativeFullscreen = true + } else { + window.toggleFullScreen(nil) + } + isInFullscreen = true + } + } + + func enterFullscreen(window: NSWindow) { + 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 style mask + previousStyleMask = window.styleMask + // Save previous contentViewFrame and screen + previousContentFrame = window.convertToScreen(contentView.frame) + + // Change presentation style to hide menu bar and dock + NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock] + // Turn it into borderless window + window.styleMask.insert(.borderless) + // This is important: it gives us the full screen, including the + // notch area on MacBooks. + window.styleMask.remove(.titled) + + // Set frame to screen size + window.setFrame(screen.frame, display: true) + + // Focus window + window.makeKeyAndOrderFront(nil) + } + + + func leaveFullscreen(window: NSWindow) { + guard let previousFrame = previousContentFrame else { return } + guard let previousStyleMask = previousStyleMask else { return } + + // Restore previous style + window.styleMask = previousStyleMask + + // Restore previous presentation options + NSApp.presentationOptions = [] + + // Restore frame + window.setFrame(window.frameRect(forContentRect: previousFrame), display: 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) + } +} diff --git a/macos/Sources/SplitView/SplitView.Divider.swift b/macos/Sources/Helpers/SplitView/SplitView.Divider.swift similarity index 100% rename from macos/Sources/SplitView/SplitView.Divider.swift rename to macos/Sources/Helpers/SplitView/SplitView.Divider.swift diff --git a/macos/Sources/SplitView/SplitView.swift b/macos/Sources/Helpers/SplitView/SplitView.swift similarity index 100% rename from macos/Sources/SplitView/SplitView.swift rename to macos/Sources/Helpers/SplitView/SplitView.swift diff --git a/macos/Sources/WindowAccessor.swift b/macos/Sources/Helpers/WindowAccessor.swift similarity index 100% rename from macos/Sources/WindowAccessor.swift rename to macos/Sources/Helpers/WindowAccessor.swift diff --git a/macos/Sources/MainMenu.xib b/macos/Sources/MainMenu.xib new file mode 100644 index 000000000..36c752c41 --- /dev/null +++ b/macos/Sources/MainMenu.xib @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Preview Content/.gitkeep b/macos/Sources/Preview Content/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/Surface.zig b/src/Surface.zig index fdf562923..df2612a55 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -146,6 +146,7 @@ const DerivedConfig = struct { clipboard_trim_trailing_spaces: bool, confirm_close_surface: bool, mouse_interval: u64, + macos_non_native_fullscreen: bool, pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig { var arena = ArenaAllocator.init(alloc_gpa); @@ -160,6 +161,7 @@ const DerivedConfig = struct { .clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", .confirm_close_surface = config.@"confirm-close-surface", .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms + .macos_non_native_fullscreen = config.@"macos-non-native-fullscreen", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -1213,7 +1215,7 @@ pub fn keyCallback( .toggle_fullscreen => { if (@hasDecl(apprt.Surface, "toggleFullscreen")) { - self.rt_surface.toggleFullscreen(); + self.rt_surface.toggleFullscreen(self.config.macos_non_native_fullscreen); } else log.warn("runtime doesn't implement toggleFullscreen", .{}); }, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 1b5c3c4b8..f39f21c6b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -66,7 +66,7 @@ pub const App = struct { goto_tab: ?*const fn (SurfaceUD, usize) callconv(.C) void = null, /// Toggle fullscreen for current window. - toggle_fullscreen: ?*const fn (SurfaceUD) callconv(.C) void = null, + toggle_fullscreen: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, }; core_app: *CoreApp, @@ -374,13 +374,13 @@ pub const Surface = struct { func(self.opts.userdata, n); } - pub fn toggleFullscreen(self: *Surface) void { + pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: bool) void { const func = self.app.opts.toggle_fullscreen orelse { log.info("runtime embedder does not toggle_fullscreen", .{}); return; }; - func(self.opts.userdata); + func(self.opts.userdata, nonNativeFullscreen); } /// The cursor position from the host directly is in screen coordinates but diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index a5fabc577..328141414 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -483,7 +483,7 @@ const Window = struct { } /// Toggle fullscreen for this window. - fn toggleFullscreen(self: *Window) void { + fn toggleFullscreen(self: *Window, _: bool) void { const is_fullscreen = c.gtk_window_is_fullscreen(self.window); if (is_fullscreen == 0) { c.gtk_window_fullscreen(self.window); diff --git a/src/config.zig b/src/config.zig index 86ea3a7ef..7d2a7fc24 100644 --- a/src/config.zig +++ b/src/config.zig @@ -221,6 +221,12 @@ pub const Config = struct { /// The default value is "detect". @"shell-integration": ShellIntegration = .detect, + /// If true, fullscreen mode on macOS will not use the native fullscreen, + /// but make the window fullscreen without animations and using a new space. + /// That's faster than the native fullscreen mode since it doesn't use + /// animations. + @"macos-non-native-fullscreen": bool = false, + /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null,