mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-14 05:46:17 +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
|
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||||
/// style and configuration of the window based on the app configuration.
|
/// style and configuration of the window based on the app configuration.
|
||||||
class TerminalWindow: NSWindow {
|
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
|
/// 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.
|
/// used by the manual float on top menu item feature.
|
||||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
||||||
@@ -45,6 +51,9 @@ class TerminalWindow: NSWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
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
|
// 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
|
// again. I'm not sure why this is required. If you don't do this, then
|
||||||
// tabs restore as separate windows.
|
// tabs restore as separate windows.
|
||||||
@@ -124,6 +133,11 @@ class TerminalWindow: NSWindow {
|
|||||||
// still become key/main and receive events.
|
// still become key/main and receive events.
|
||||||
override var canBecomeKey: Bool { return true }
|
override var canBecomeKey: Bool { return true }
|
||||||
override var canBecomeMain: 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() {
|
override func becomeKey() {
|
||||||
super.becomeKey()
|
super.becomeKey()
|
||||||
|
@@ -10,21 +10,56 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
|||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil)
|
self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil)
|
||||||
super.init()
|
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,
|
func show(_ request: SPUUpdatePermissionRequest,
|
||||||
reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) {
|
reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) {
|
||||||
viewModel.state = .permissionRequest(.init(request: request, reply: reply))
|
viewModel.state = .permissionRequest(.init(request: request, reply: reply))
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.show(request, reply: reply)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {
|
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {
|
||||||
viewModel.state = .checking(.init(cancel: cancellation))
|
viewModel.state = .checking(.init(cancel: cancellation))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showUserInitiatedUpdateCheck(cancellation: cancellation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUpdateFound(with appcastItem: SUAppcastItem,
|
func showUpdateFound(with appcastItem: SUAppcastItem,
|
||||||
state: SPUUserUpdateState,
|
state: SPUUserUpdateState,
|
||||||
reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||||
viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply))
|
viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply))
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showUpdateFound(with: appcastItem, state: state, reply: reply)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
||||||
@@ -39,7 +74,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
|||||||
func showUpdateNotFoundWithError(_ error: any Error,
|
func showUpdateNotFoundWithError(_ error: any Error,
|
||||||
acknowledgement: @escaping () -> Void) {
|
acknowledgement: @escaping () -> Void) {
|
||||||
viewModel.state = .notFound
|
viewModel.state = .notFound
|
||||||
acknowledgement()
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement)
|
||||||
|
} else {
|
||||||
|
acknowledgement()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUpdaterError(_ error: any Error,
|
func showUpdaterError(_ error: any Error,
|
||||||
@@ -56,6 +96,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
|||||||
dismiss: { [weak viewModel] in
|
dismiss: { [weak viewModel] in
|
||||||
viewModel?.state = .idle
|
viewModel?.state = .idle
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showUpdaterError(error, acknowledgement: acknowledgement)
|
||||||
|
} else {
|
||||||
|
acknowledgement()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
||||||
@@ -63,6 +109,10 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
|||||||
cancel: cancellation,
|
cancel: cancellation,
|
||||||
expectedLength: nil,
|
expectedLength: nil,
|
||||||
progress: 0))
|
progress: 0))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showDownloadInitiated(cancellation: cancellation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
||||||
@@ -74,6 +124,10 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
|||||||
cancel: downloading.cancel,
|
cancel: downloading.cancel,
|
||||||
expectedLength: expectedContentLength,
|
expectedLength: expectedContentLength,
|
||||||
progress: 0))
|
progress: 0))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
||||||
@@ -85,36 +139,67 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
|||||||
cancel: downloading.cancel,
|
cancel: downloading.cancel,
|
||||||
expectedLength: downloading.expectedLength,
|
expectedLength: downloading.expectedLength,
|
||||||
progress: downloading.progress + length))
|
progress: downloading.progress + length))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showDownloadDidReceiveData(ofLength: length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showDownloadDidStartExtractingUpdate() {
|
func showDownloadDidStartExtractingUpdate() {
|
||||||
viewModel.state = .extracting(.init(progress: 0))
|
viewModel.state = .extracting(.init(progress: 0))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showDownloadDidStartExtractingUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showExtractionReceivedProgress(_ progress: Double) {
|
func showExtractionReceivedProgress(_ progress: Double) {
|
||||||
viewModel.state = .extracting(.init(progress: progress))
|
viewModel.state = .extracting(.init(progress: progress))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showExtractionReceivedProgress(progress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||||
viewModel.state = .readyToInstall(.init(reply: reply))
|
viewModel.state = .readyToInstall(.init(reply: reply))
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showReady(toInstallAndRelaunch: reply)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
||||||
viewModel.state = .installing
|
viewModel.state = .installing
|
||||||
|
|
||||||
|
if !hasUnobtrusiveTarget {
|
||||||
|
standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||||
// We don't do anything here.
|
standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement)
|
||||||
viewModel.state = .idle
|
viewModel.state = .idle
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUpdateInFocus() {
|
func showUpdateInFocus() {
|
||||||
// We don't currently implement this because our update state is
|
if !hasUnobtrusiveTarget {
|
||||||
// shown in a terminal window. We may want to implement this at some
|
standard.showUpdateInFocus()
|
||||||
// point to handle the case that no windows are open, though.
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dismissUpdateInstallation() {
|
func dismissUpdateInstallation() {
|
||||||
viewModel.state = .idle
|
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
|
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 {
|
static func == (lhs: UpdateState, rhs: UpdateState) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.idle, .idle):
|
case (.idle, .idle):
|
||||||
|
Reference in New Issue
Block a user