mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-13 13:26:09 +00:00
macos: Fallback to standard driver when no unobtrusive targets exist
This commit is contained in:
@@ -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"
|
||||
@@ -45,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.
|
||||
@@ -125,6 +134,11 @@ class TerminalWindow: NSWindow {
|
||||
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()
|
||||
resetZoomTabButton.contentTintColor = .controlAccentColor
|
||||
|
@@ -10,21 +10,56 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||
self.viewModel = viewModel
|
||||
self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil)
|
||||
super.init()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleTerminalWindowWillClose),
|
||||
name: TerminalWindow.terminalWillCloseNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func handleTerminalWindowWillClose() {
|
||||
// If we lost the ability to show unobtrusive states, cancel whatever
|
||||
// update state we're in. This will allow the manual `check for updates`
|
||||
// call to initialize the standard driver.
|
||||
//
|
||||
// We have to do this after a short delay so that the window can fully
|
||||
// close.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in
|
||||
guard let self else { return }
|
||||
guard !hasUnobtrusiveTarget else { return }
|
||||
viewModel.state.cancel()
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
|
||||
func show(_ request: SPUUpdatePermissionRequest,
|
||||
reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) {
|
||||
viewModel.state = .permissionRequest(.init(request: request, reply: reply))
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.show(request, reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {
|
||||
viewModel.state = .checking(.init(cancel: cancellation))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUserInitiatedUpdateCheck(cancellation: cancellation)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateFound(with appcastItem: SUAppcastItem,
|
||||
state: SPUUserUpdateState,
|
||||
reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||
viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply))
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdateFound(with: appcastItem, state: state, reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
||||
@@ -39,8 +74,13 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||
func showUpdateNotFoundWithError(_ error: any Error,
|
||||
acknowledgement: @escaping () -> Void) {
|
||||
viewModel.state = .notFound
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement)
|
||||
} else {
|
||||
acknowledgement()
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdaterError(_ error: any Error,
|
||||
acknowledgement: @escaping () -> Void) {
|
||||
@@ -56,6 +96,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||
dismiss: { [weak viewModel] in
|
||||
viewModel?.state = .idle
|
||||
}))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdaterError(error, acknowledgement: acknowledgement)
|
||||
} else {
|
||||
acknowledgement()
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
||||
@@ -63,6 +109,10 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||
cancel: cancellation,
|
||||
expectedLength: nil,
|
||||
progress: 0))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadInitiated(cancellation: cancellation)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
||||
@@ -74,6 +124,10 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||
cancel: downloading.cancel,
|
||||
expectedLength: expectedContentLength,
|
||||
progress: 0))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
||||
@@ -85,36 +139,67 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||
cancel: downloading.cancel,
|
||||
expectedLength: downloading.expectedLength,
|
||||
progress: downloading.progress + length))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadDidReceiveData(ofLength: length)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidStartExtractingUpdate() {
|
||||
viewModel.state = .extracting(.init(progress: 0))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadDidStartExtractingUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func showExtractionReceivedProgress(_ progress: Double) {
|
||||
viewModel.state = .extracting(.init(progress: progress))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showExtractionReceivedProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||
viewModel.state = .readyToInstall(.init(reply: reply))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showReady(toInstallAndRelaunch: reply)
|
||||
}
|
||||
}
|
||||
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
||||
viewModel.state = .installing
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||
// We don't do anything here.
|
||||
standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement)
|
||||
viewModel.state = .idle
|
||||
}
|
||||
|
||||
func showUpdateInFocus() {
|
||||
// We don't currently implement this because our update state is
|
||||
// shown in a terminal window. We may want to implement this at some
|
||||
// point to handle the case that no windows are open, though.
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdateInFocus()
|
||||
}
|
||||
}
|
||||
|
||||
func dismissUpdateInstallation() {
|
||||
viewModel.state = .idle
|
||||
standard.dismissUpdateInstallation()
|
||||
}
|
||||
|
||||
// MARK: No-Window Fallback
|
||||
|
||||
/// True if there is a target that can render our unobtrusive update checker.
|
||||
var hasUnobtrusiveTarget: Bool {
|
||||
NSApp.windows.contains { window in
|
||||
window is TerminalWindow &&
|
||||
window.isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -134,6 +134,23 @@ enum UpdateState: Equatable {
|
||||
return false
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
switch self {
|
||||
case .checking(let checking):
|
||||
checking.cancel()
|
||||
case .updateAvailable(let available):
|
||||
available.reply(.dismiss)
|
||||
case .downloading(let downloading):
|
||||
downloading.cancel()
|
||||
case .readyToInstall(let ready):
|
||||
ready.reply(.dismiss)
|
||||
case .error(let err):
|
||||
err.dismiss()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: UpdateState, rhs: UpdateState) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.idle, .idle):
|
||||
|
Reference in New Issue
Block a user