diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index d73d57911..922e427f6 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -25,12 +25,26 @@ class QuickTerminalController: BaseTerminalController { ) { self.position = position super.init(ghostty, baseConfig: base, surfaceTree: tree) + + // Setup our notifications for behaviors + let center = NotificationCenter.default + center.addObserver( + self, + selector: #selector(onToggleFullscreen), + name: Ghostty.Notification.ghosttyToggleFullscreen, + object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } + deinit { + // Remove all of our notificationcenter subscriptions + let center = NotificationCenter.default + center.removeObserver(self) + } + // MARK: NSWindowController override func windowDidLoad() { @@ -199,6 +213,11 @@ class QuickTerminalController: BaseTerminalController { // We always animate out to whatever screen the window is actually on. guard let screen = window.screen ?? NSScreen.main else { return } + // If we are in fullscreen, then we exit fullscreen. + if let fullscreenStyle, fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + } + // If we have a previously active application, restore focus to it. We // do this BEFORE the animation below because when the animation completes // macOS will bring forward another window. @@ -239,4 +258,19 @@ class QuickTerminalController: BaseTerminalController { alert.alertStyle = .warning alert.beginSheetModal(for: window) } + + @IBAction func toggleGhosttyFullScreen(_ sender: Any) { + guard let surface = focusedSurface?.surface else { return } + ghostty.toggleFullscreen(surface: surface) + } + + // MARK: Notifications + + @objc private func onToggleFullscreen(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard target == self.focusedSurface else { return } + + // We ignore the requested mode and always use non-native for the quick terminal + toggleFullscreen(mode: .nonNative) + } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 4417ce9cc..df61777b4 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -11,11 +11,15 @@ import GhosttyKit /// view the TerminalView SwiftUI view must be used and this class is the view model and /// delegate. /// +/// Special considerations to implement: +/// +/// - Fullscreen: you must manually listen for the right notification and implement the +/// callback that calls toggleFullscreen on this base class. +/// /// Notably, things this class does NOT implement (not exhaustive): /// /// - Tabbing, because there are many ways to get tabbed behavior in macOS and we /// don't want to be opinionated about it. -/// - Fullscreen /// - Window restoration or save state /// - Window visual styles (such as titlebar colors) /// @@ -25,7 +29,8 @@ class BaseTerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, TerminalViewModel, - ClipboardConfirmationViewDelegate + ClipboardConfirmationViewDelegate, + FullscreenDelegate { /// The app instance that this terminal view will represent. let ghostty: Ghostty.App @@ -46,6 +51,9 @@ class BaseTerminalController: NSWindowController, /// The clipboard confirmation window, if shown. private var clipboardConfirmation: ClipboardConfirmationController? = nil + /// Fullscreen state management. + private(set) var fullscreenStyle: FullscreenStyle? + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } @@ -123,6 +131,63 @@ class BaseTerminalController: NSWindowController, func zoomStateDidChange(to: Bool) {} + // 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: Clipboard Confirmation @objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 2d230561b..70df52b4b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -4,14 +4,9 @@ import SwiftUI import GhosttyKit /// A classic, tabbed terminal experience. -class TerminalController: BaseTerminalController, - FullscreenDelegate -{ +class TerminalController: BaseTerminalController { override var windowNibName: NSNib.Name? { "Terminal" } - /// Fullscreen state management. - 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 /// early if we don't care. @@ -200,63 +195,6 @@ 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() { @@ -584,7 +522,6 @@ class TerminalController: BaseTerminalController, targetWindow.makeKeyAndOrderFront(nil) } - @objc private func onToggleFullscreen(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard target == self.focusedSurface else { return } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 5df3ae8e4..da87ac230 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -376,9 +376,12 @@ pub const Action = union(enum) { /// /// - It is a singleton; only one instance can exist at a time. /// - It does not support tabs. - /// - It does not support fullscreen. /// - It will not be restored when the application is restarted /// (for systems that support window restoration). + /// - It supports fullscreen, but fullscreen will always be a non-native + /// fullscreen (macos-non-native-fullscreen = true). This only applies + /// to the quick terminal window. This is a requirement due to how + /// the quick terminal is rendered. /// /// See the various configurations for the quick terminal in the /// configuration file to customize its behavior.