Add Sparkle auto-update flow and titlebar update UI

This commit is contained in:
Lawrence Chen 2026-01-26 17:25:34 -08:00
parent a158c744ea
commit e3ee246930
19 changed files with 1813 additions and 80 deletions

View file

@ -0,0 +1,135 @@
import AppKit
import SwiftUI
final class WindowToolbarController: NSObject, NSToolbarDelegate {
private let commandItemIdentifier = NSToolbarItem.Identifier("cmux.focusedCommand")
private let updateItemIdentifier = NSToolbarItem.Identifier("cmux.updatePill")
private weak var tabManager: TabManager?
private weak var updateViewModel: UpdateViewModel?
private var commandLabels: [ObjectIdentifier: NSTextField] = [:]
private var observers: [NSObjectProtocol] = []
init(updateViewModel: UpdateViewModel) {
self.updateViewModel = updateViewModel
super.init()
}
deinit {
for observer in observers {
NotificationCenter.default.removeObserver(observer)
}
}
func start(tabManager: TabManager) {
self.tabManager = tabManager
attachToExistingWindows()
installObservers()
updateFocusedCommandText()
}
private func installObservers() {
let center = NotificationCenter.default
observers.append(center.addObserver(
forName: .ghosttyDidSetTitle,
object: nil,
queue: .main
) { [weak self] _ in
self?.updateFocusedCommandText()
})
observers.append(center.addObserver(
forName: .ghosttyDidFocusTab,
object: nil,
queue: .main
) { [weak self] _ in
self?.updateFocusedCommandText()
})
observers.append(center.addObserver(
forName: NSWindow.didBecomeMainNotification,
object: nil,
queue: .main
) { [weak self] notification in
guard let window = notification.object as? NSWindow else { return }
self?.attach(to: window)
})
}
private func attachToExistingWindows() {
for window in NSApp.windows {
attach(to: window)
}
}
private func attach(to window: NSWindow) {
guard window.toolbar == nil else { return }
let toolbar = NSToolbar(identifier: NSToolbar.Identifier("cmux.toolbar"))
toolbar.delegate = self
toolbar.displayMode = .iconOnly
toolbar.allowsUserCustomization = false
toolbar.autosavesConfiguration = false
toolbar.showsBaselineSeparator = false
window.toolbar = toolbar
window.toolbarStyle = .unified
window.titleVisibility = .visible
}
private func updateFocusedCommandText() {
guard let tabManager else { return }
let text: String
if let selectedId = tabManager.selectedTabId,
let tab = tabManager.tabs.first(where: { $0.id == selectedId }) {
let title = tab.title.trimmingCharacters(in: .whitespacesAndNewlines)
text = title.isEmpty ? "Cmd: —" : "Cmd: \(title)"
} else {
text = "Cmd: —"
}
for label in commandLabels.values {
label.stringValue = text
}
}
// MARK: - NSToolbarDelegate
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[commandItemIdentifier, .flexibleSpace, updateItemIdentifier]
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[commandItemIdentifier, .flexibleSpace, updateItemIdentifier]
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == commandItemIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
let label = NSTextField(labelWithString: "Cmd: —")
label.font = NSFont.systemFont(ofSize: 12, weight: .medium)
label.textColor = .secondaryLabelColor
label.lineBreakMode = .byTruncatingMiddle
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
item.view = label
commandLabels[ObjectIdentifier(toolbar)] = label
updateFocusedCommandText()
return item
}
if itemIdentifier == updateItemIdentifier, let updateViewModel {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
let view = NonDraggableHostingView(rootView: UpdatePill(
model: updateViewModel,
showWhenIdle: true,
onIdleTap: {
guard let delegate = NSApp.delegate as? AppDelegate else { return }
delegate.checkForUpdates(nil)
}
))
item.view = view
return item
}
return nil
}
}