summaryrefslogtreecommitdiff
path: root/macos/Sources/Features/Terminal
diff options
context:
space:
mode:
Diffstat (limited to 'macos/Sources/Features/Terminal')
-rw-r--r--macos/Sources/Features/Terminal/BaseTerminalController.swift38
-rw-r--r--macos/Sources/Features/Terminal/TerminalView.swift25
-rw-r--r--macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift3
-rw-r--r--macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift77
-rw-r--r--macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift4
-rw-r--r--macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift4
6 files changed, 137 insertions, 14 deletions
diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift
index f660ea3ad..b9f9c5a05 100644
--- a/macos/Sources/Features/Terminal/BaseTerminalController.swift
+++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift
@@ -48,6 +48,9 @@ class BaseTerminalController: NSWindowController,
/// This can be set to show/hide the command palette.
@Published var commandPaletteIsShowing: Bool = false
+
+ /// Set if the terminal view should show the update overlay.
+ @Published var updateOverlayIsVisible: Bool = false
/// Whether the terminal surface should focus when the mouse is over it.
var focusFollowsMouse: Bool {
@@ -818,7 +821,18 @@ class BaseTerminalController: NSWindowController,
}
}
- func fullscreenDidChange() {}
+ func fullscreenDidChange() {
+ guard let fullscreenStyle else { return }
+
+ // When we enter fullscreen, we want to show the update overlay so that it
+ // is easily visible. For native fullscreen this is visible by showing the
+ // menubar but we don't want to rely on that.
+ if fullscreenStyle.isFullscreen {
+ updateOverlayIsVisible = true
+ } else {
+ updateOverlayIsVisible = defaultUpdateOverlayVisibility()
+ }
+ }
// MARK: Clipboard Confirmation
@@ -900,6 +914,28 @@ class BaseTerminalController: NSWindowController,
fullscreenStyle = NativeFullscreen(window)
fullscreenStyle?.delegate = self
}
+
+ // Set our update overlay state
+ updateOverlayIsVisible = defaultUpdateOverlayVisibility()
+ }
+
+ func defaultUpdateOverlayVisibility() -> Bool {
+ guard let window else { return true }
+
+ // No titlebar we always show the update overlay because it can't support
+ // updates in the titlebar
+ guard window.styleMask.contains(.titled) else {
+ return true
+ }
+
+ // If it's a non terminal window we can't trust it has an update accessory,
+ // so we always want to show the overlay.
+ guard let window = window as? TerminalWindow else {
+ return true
+ }
+
+ // Show the overlay if the window isn't.
+ return !window.supportsUpdateAccessory
}
// MARK: NSWindowDelegate
diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift
index b5be0ae42..54cf9a02a 100644
--- a/macos/Sources/Features/Terminal/TerminalView.swift
+++ b/macos/Sources/Features/Terminal/TerminalView.swift
@@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject {
/// The command palette state.
var commandPaletteIsShowing: Bool { get set }
+
+ /// The update overlay should be visible.
+ var updateOverlayIsVisible: Bool { get }
}
/// The main terminal view. This terminal view supports splits.
@@ -109,6 +112,28 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
self.delegate?.performAction(action, on: surfaceView)
}
}
+
+ // Show update information above all else.
+ if viewModel.updateOverlayIsVisible {
+ UpdateOverlay()
+ }
+ }
+ }
+ }
+}
+
+fileprivate struct UpdateOverlay: View {
+ var body: some View {
+ if let appDelegate = NSApp.delegate as? AppDelegate {
+ VStack {
+ Spacer()
+
+ HStack {
+ Spacer()
+ UpdatePill(model: appDelegate.updateViewModel)
+ .padding(.bottom, 9)
+ .padding(.trailing, 9)
+ }
}
}
}
diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift
index dc7dd7633..dd8b258f3 100644
--- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift
@@ -1,6 +1,9 @@
import AppKit
class HiddenTitlebarTerminalWindow: TerminalWindow {
+ // No titlebar, we don't support accessories.
+ override var supportsUpdateAccessory: Bool { false }
+
override func awakeFromNib() {
super.awakeFromNib()
diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift
index 3ab6293dc..661c89121 100644
--- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift
@@ -5,6 +5,12 @@ import GhosttyKit
/// The base class for all standalone, "normal" terminal windows. This sets the basic
/// style and configuration of the window based on the app configuration.
class TerminalWindow: NSWindow {
+ /// Posted when a terminal window awakes from nib.
+ static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake")
+
+ /// Posted when a terminal window will close
+ static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose")
+
/// This is the key in UserDefaults to use for the default `level` value. This is
/// used by the manual float on top menu item feature.
static let defaultLevelKey: String = "TerminalDefaultLevel"
@@ -14,15 +20,25 @@ class TerminalWindow: NSWindow {
/// Reset split zoom button in titlebar
private let resetZoomAccessory = NSTitlebarAccessoryViewController()
+
+ /// Update notification UI in titlebar
+ private let updateAccessory = NSTitlebarAccessoryViewController()
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private(set) var derivedConfig: DerivedConfig = .init()
+
+ /// Whether this window supports the update accessory. If this is false, then views within this
+ /// window should determine how to show update notifications.
+ var supportsUpdateAccessory: Bool {
+ // Native window supports it.
+ true
+ }
/// Gets the terminal controller from the window controller.
var terminalController: TerminalController? {
windowController as? TerminalController
}
-
+
// MARK: NSWindow Overrides
override var toolbar: NSToolbar? {
@@ -35,6 +51,9 @@ class TerminalWindow: NSWindow {
}
override func awakeFromNib() {
+ // Notify that this terminal window has loaded
+ NotificationCenter.default.post(name: Self.terminalDidAwake, object: self)
+
// This is required so that window restoration properly creates our tabs
// again. I'm not sure why this is required. If you don't do this, then
// tabs restore as separate windows.
@@ -85,6 +104,17 @@ class TerminalWindow: NSWindow {
}))
addTitlebarAccessoryViewController(resetZoomAccessory)
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
+
+ // Create update notification accessory
+ if supportsUpdateAccessory {
+ updateAccessory.layoutAttribute = .right
+ updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView(
+ viewModel: viewModel,
+ model: appDelegate.updateViewModel
+ ))
+ addTitlebarAccessoryViewController(updateAccessory)
+ updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false
+ }
}
// Setup the accessory view for tabs that shows our keyboard shortcuts,
@@ -103,6 +133,11 @@ class TerminalWindow: NSWindow {
// still become key/main and receive events.
override var canBecomeKey: Bool { return true }
override var canBecomeMain: Bool { return true }
+
+ override func close() {
+ NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self)
+ super.close()
+ }
override func becomeKey() {
super.becomeKey()
@@ -198,6 +233,9 @@ class TerminalWindow: NSWindow {
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
removeTitlebarAccessoryViewController(at: idx)
}
+
+ // We don't need to do this with the update accessory. I don't know why but
+ // everything works fine.
}
private func tabBarDidDisappear() {
@@ -436,7 +474,7 @@ class TerminalWindow: NSWindow {
standardWindowButton(.miniaturizeButton)?.isHidden = true
standardWindowButton(.zoomButton)?.isHidden = true
}
-
+
// MARK: Config
struct DerivedConfig {
@@ -467,21 +505,20 @@ extension TerminalWindow {
class ViewModel: ObservableObject {
@Published var isSurfaceZoomed: Bool = false
@Published var hasToolbar: Bool = false
- }
-
- struct ResetZoomAccessoryView: View {
- @ObservedObject var viewModel: ViewModel
- let action: () -> Void
- // The padding from the top that the view appears. This was all just manually
- // measured based on the OS.
- var topPadding: CGFloat {
+ /// Calculates the top padding based on toolbar visibility and macOS version
+ fileprivate var accessoryTopPadding: CGFloat {
if #available(macOS 26.0, *) {
- return viewModel.hasToolbar ? 10 : 5
+ return hasToolbar ? 10 : 5
} else {
- return viewModel.hasToolbar ? 9 : 4
+ return hasToolbar ? 9 : 4
}
}
+ }
+
+ struct ResetZoomAccessoryView: View {
+ @ObservedObject var viewModel: ViewModel
+ let action: () -> Void
var body: some View {
if viewModel.isSurfaceZoomed {
@@ -497,10 +534,24 @@ extension TerminalWindow {
}
// With a toolbar, the window title is taller, so we need more padding
// to properly align.
- .padding(.top, topPadding)
+ .padding(.top, viewModel.accessoryTopPadding)
// We always need space at the end of the titlebar
.padding(.trailing, 10)
}
}
}
+
+ /// A pill-shaped button that displays update status and provides access to update actions.
+ struct UpdateAccessoryView: View {
+ @ObservedObject var viewModel: ViewModel
+ @ObservedObject var model: UpdateViewModel
+
+ var body: some View {
+ // We use the same top/trailing padding so that it hugs the same.
+ UpdatePill(model: model)
+ .padding(.top, viewModel.accessoryTopPadding)
+ .padding(.trailing, viewModel.accessoryTopPadding)
+ }
+ }
+
}
diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift
index 260fac4cc..855d29f52 100644
--- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift
@@ -8,6 +8,10 @@ import SwiftUI
class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate {
/// The view model for SwiftUI views
private var viewModel = ViewModel()
+
+ /// Titlebar tabs can't support the update accessory because of the way we layout
+ /// the native tabs back into the menu bar.
+ override var supportsUpdateAccessory: Bool { false }
deinit {
tabBarObserver = nil
diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift
index 8589877d8..0c087faeb 100644
--- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift
@@ -2,6 +2,10 @@ import Cocoa
/// Titlebar tabs for macOS 13 to 15.
class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
+ /// Titlebar tabs can't support the update accessory because of the way we layout
+ /// the native tabs back into the menu bar.
+ override var supportsUpdateAccessory: Bool { false }
+
/// This is used to determine if certain elements should be drawn light or dark and should
/// be updated whenever the window background color or surrounding elements changes.
fileprivate var isLightTheme: Bool = false