diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b60eb11f5..57070dc47 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; + A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; }; @@ -139,6 +140,7 @@ A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; + A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -233,6 +235,7 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, @@ -582,6 +585,7 @@ A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, + A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */, A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 77d2d0033..ea35790fd 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -57,6 +57,14 @@ class BaseTerminalController: NSWindowController, /// Event monitor (see individual events for why) private var eventMonitor: Any? = nil + /// The previous frame information from the window + private var savedFrame: SavedFrame? = nil + + struct SavedFrame { + let window: NSRect + let screen: NSRect + } + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } @@ -80,6 +88,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(onConfirmClipboardRequest), name: Ghostty.Notification.confirmClipboard, object: nil) + center.addObserver( + self, + selector: #selector(didChangeScreenParametersNotification), + name: NSApplication.didChangeScreenParametersNotification, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -89,6 +102,8 @@ class BaseTerminalController: NSWindowController, } deinit { + NotificationCenter.default.removeObserver(self) + if let eventMonitor { NSEvent.removeMonitor(eventMonitor) } @@ -121,6 +136,57 @@ class BaseTerminalController: NSWindowController, } } + // Call this whenever the frame changes + private func windowFrameDidChange() { + // We need to update our saved frame information in case of monitor + // changes (see didChangeScreenParameters notification). + savedFrame = nil + guard let window, let screen = window.screen else { return } + savedFrame = .init(window: window.frame, screen: screen.visibleFrame) + } + + // MARK: Notifications + + @objc private func didChangeScreenParametersNotification(_ notification: Notification) { + // If we have a window that is visible and it is outside the bounds of the + // screen then we clamp it back to within the screen. + guard let window else { return } + guard window.isVisible else { return } + guard let screen = window.screen else { return } + + let visibleFrame = screen.visibleFrame + var newFrame = window.frame + + // Clamp width/height + if newFrame.size.width > visibleFrame.size.width { + newFrame.size.width = visibleFrame.size.width + } + if newFrame.size.height > visibleFrame.size.height { + newFrame.size.height = visibleFrame.size.height + } + + // Ensure the window is on-screen. We only do this if the previous frame + // was also on screen. If a user explicitly wanted their window off screen + // then we let it stay that way. + x: if newFrame.origin.x < visibleFrame.origin.x { + if let savedFrame, savedFrame.window.origin.x < savedFrame.screen.origin.x { + break x; + } + + newFrame.origin.x = visibleFrame.origin.x + } + y: if newFrame.origin.y < visibleFrame.origin.y { + if let savedFrame, savedFrame.window.origin.y < savedFrame.screen.origin.y { + break y; + } + + newFrame.origin.y = visibleFrame.origin.y + } + + // Apply the new window frame + window.setFrame(newFrame, display: true) + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -371,6 +437,14 @@ class BaseTerminalController: NSWindowController, } } + func windowDidResize(_ notification: Notification) { + windowFrameDidChange() + } + + func windowDidMove(_ notification: Notification) { + windowFrameDidChange() + } + // MARK: First Responder @IBAction func close(_ sender: Any) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 70df52b4b..f43454edd 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -358,7 +358,8 @@ class TerminalController: BaseTerminalController { self.fixTabBar() } - func windowDidMove(_ notification: Notification) { + override func windowDidMove(_ notification: Notification) { + super.windowDidMove(notification) self.fixTabBar() } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 69c9f992b..29639c39e 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -56,7 +56,13 @@ extension Ghostty { // same filesystem concept. #if os(macOS) ghostty_config_load_default_files(cfg); - ghostty_config_load_cli_args(cfg); + + // We only load CLI args when not running in Xcode because in Xcode we + // pass some special parameters to control the debugger. + if !isRunningInXcode() { + ghostty_config_load_cli_args(cfg); + } + ghostty_config_load_recursive_files(cfg); #endif diff --git a/macos/Sources/Helpers/Xcode.swift b/macos/Sources/Helpers/Xcode.swift new file mode 100644 index 000000000..281bad18b --- /dev/null +++ b/macos/Sources/Helpers/Xcode.swift @@ -0,0 +1,10 @@ +import Foundation + +/// True if we appear to be running in Xcode. +func isRunningInXcode() -> Bool { + if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { + return true + } + + return false +}