Handle Sparkle updater cache and bump version
This commit is contained in:
parent
96941c6dbc
commit
e743498fe1
6 changed files with 198 additions and 73 deletions
|
|
@ -57,6 +57,7 @@ ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcode
|
|||
## Ghostty submodule workflow
|
||||
|
||||
Ghostty changes must be committed in the `ghostty` submodule and pushed to the `manaflow-ai/ghostty` fork.
|
||||
Keep `docs/ghostty-fork.md` up to date with any fork changes and conflict notes.
|
||||
|
||||
```bash
|
||||
cd ghostty
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -526,7 +526,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
MARKETING_VERSION = 1.5.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -555,7 +555,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -571,7 +571,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
MARKETING_VERSION = 1.5.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -624,10 +624,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
MARKETING_VERSION = 1.5.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -641,10 +641,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
MARKETING_VERSION = 1.5.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ struct NotificationsPage: View {
|
|||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||
@EnvironmentObject var tabManager: TabManager
|
||||
@Binding var selection: SidebarSelection
|
||||
@FocusState private var focusedNotificationId: UUID?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -26,7 +27,8 @@ struct NotificationsPage: View {
|
|||
},
|
||||
onClear: {
|
||||
notificationStore.remove(id: notification.id)
|
||||
}
|
||||
},
|
||||
focusedNotificationId: $focusedNotificationId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +38,20 @@ struct NotificationsPage: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
.onAppear(perform: setInitialFocus)
|
||||
.onChange(of: notificationStore.notifications.first?.id) { _ in
|
||||
setInitialFocus()
|
||||
}
|
||||
}
|
||||
|
||||
private func setInitialFocus() {
|
||||
guard let firstId = notificationStore.notifications.first?.id else {
|
||||
focusedNotificationId = nil
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
focusedNotificationId = firstId
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
|
|
@ -91,44 +107,56 @@ private struct NotificationRow: View {
|
|||
let tabTitle: String?
|
||||
let onOpen: () -> Void
|
||||
let onClear: () -> Void
|
||||
let focusedNotificationId: FocusState<UUID?>.Binding
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Circle()
|
||||
.fill(notification.isRead ? Color.clear : Color.accentColor)
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay(
|
||||
Button(action: onOpen) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Circle()
|
||||
.stroke(Color.accentColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 6)
|
||||
.fill(notification.isRead ? Color.clear : Color.accentColor)
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.accentColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 6)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(notification.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(notification.createdAt, style: .time)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(notification.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(notification.createdAt, style: .time)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if !notification.body.isEmpty {
|
||||
Text(notification.body)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
if !notification.body.isEmpty {
|
||||
Text(notification.body)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
|
||||
if let tabTitle {
|
||||
Text(tabTitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
if let tabTitle {
|
||||
Text(tabTitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
.buttonStyle(.plain)
|
||||
.focusable()
|
||||
.focused(focusedNotificationId, equals: notification.id)
|
||||
.modifier(DefaultActionModifier(isActive: focusedNotificationId.wrappedValue == notification.id))
|
||||
|
||||
Button(action: onClear) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
|
|
@ -141,7 +169,17 @@ private struct NotificationRow: View {
|
|||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color(nsColor: .controlBackgroundColor))
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onOpen)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DefaultActionModifier: ViewModifier {
|
||||
let isActive: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if isActive {
|
||||
content.keyboardShortcut(.defaultAction)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class UpdateController {
|
|||
|
||||
/// Start the updater. If startup fails, the error is shown via the custom UI.
|
||||
func startUpdater() {
|
||||
ensureSparkleInstallationCache()
|
||||
do {
|
||||
try updater.start()
|
||||
} catch {
|
||||
|
|
@ -74,6 +75,7 @@ class UpdateController {
|
|||
/// Check for updates (used by the menu item).
|
||||
@objc func checkForUpdates() {
|
||||
UpdateLogStore.shared.append("checkForUpdates invoked (state=\(viewModel.state.isIdle ? "idle" : "busy"))")
|
||||
ensureSparkleInstallationCache()
|
||||
if viewModel.state == .idle {
|
||||
updater.checkForUpdates()
|
||||
return
|
||||
|
|
@ -164,4 +166,39 @@ class UpdateController {
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func ensureSparkleInstallationCache() {
|
||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
|
||||
guard let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
let baseURL = cachesURL
|
||||
.appendingPathComponent(bundleIdentifier)
|
||||
.appendingPathComponent("org.sparkle-project.Sparkle")
|
||||
let installURL = baseURL.appendingPathComponent("Installation")
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
if FileManager.default.fileExists(atPath: installURL.path, isDirectory: &isDirectory) {
|
||||
if !isDirectory.boolValue {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: installURL)
|
||||
} catch {
|
||||
UpdateLogStore.shared.append("Failed removing Sparkle installation cache file: \(error)")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: installURL,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: [.posixPermissions: 0o700]
|
||||
)
|
||||
UpdateLogStore.shared.append("Ensured Sparkle installation cache at \(installURL.path)")
|
||||
} catch {
|
||||
UpdateLogStore.shared.append("Failed creating Sparkle installation cache: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -428,7 +428,12 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
popover.animates = true
|
||||
popover.delegate = self
|
||||
popover.contentViewController = NSHostingController(
|
||||
rootView: NotificationsPopoverView(notificationStore: notificationStore)
|
||||
rootView: NotificationsPopoverView(
|
||||
notificationStore: notificationStore,
|
||||
onDismiss: { [weak popover] in
|
||||
popover?.performClose(nil)
|
||||
}
|
||||
)
|
||||
)
|
||||
return popover
|
||||
}
|
||||
|
|
@ -436,6 +441,8 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
|
||||
private struct NotificationsPopoverView: View {
|
||||
@ObservedObject var notificationStore: TerminalNotificationStore
|
||||
let onDismiss: () -> Void
|
||||
@FocusState private var focusedNotificationId: UUID?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -475,7 +482,8 @@ private struct NotificationsPopoverView: View {
|
|||
notification: notification,
|
||||
tabTitle: tabTitle(for: notification.tabId),
|
||||
onOpen: { open(notification) },
|
||||
onClear: { notificationStore.remove(id: notification.id) }
|
||||
onClear: { notificationStore.remove(id: notification.id) },
|
||||
focusedNotificationId: $focusedNotificationId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -485,6 +493,20 @@ private struct NotificationsPopoverView: View {
|
|||
}
|
||||
}
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
.onAppear(perform: setInitialFocus)
|
||||
.onChange(of: notificationStore.notifications.first?.id) { _ in
|
||||
setInitialFocus()
|
||||
}
|
||||
}
|
||||
|
||||
private func setInitialFocus() {
|
||||
guard let firstId = notificationStore.notifications.first?.id else {
|
||||
focusedNotificationId = nil
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
focusedNotificationId = firstId
|
||||
}
|
||||
}
|
||||
|
||||
private func tabTitle(for tabId: UUID) -> String? {
|
||||
|
|
@ -494,6 +516,7 @@ private struct NotificationsPopoverView: View {
|
|||
private func open(_ notification: TerminalNotification) {
|
||||
AppDelegate.shared?.tabManager?.focusTabFromNotification(notification.tabId, surfaceId: notification.surfaceId)
|
||||
markReadIfFocused(notification)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
private func markReadIfFocused(_ notification: TerminalNotification) {
|
||||
|
|
@ -513,44 +536,56 @@ private struct NotificationPopoverRow: View {
|
|||
let tabTitle: String?
|
||||
let onOpen: () -> Void
|
||||
let onClear: () -> Void
|
||||
let focusedNotificationId: FocusState<UUID?>.Binding
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Circle()
|
||||
.fill(notification.isRead ? Color.clear : Color.accentColor)
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay(
|
||||
Button(action: onOpen) {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Circle()
|
||||
.stroke(Color.accentColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 6)
|
||||
.fill(notification.isRead ? Color.clear : Color.accentColor)
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.accentColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 6)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(notification.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(notification.createdAt, style: .time)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(notification.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(notification.createdAt, style: .time)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if !notification.body.isEmpty {
|
||||
Text(notification.body)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
if !notification.body.isEmpty {
|
||||
Text(notification.body)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
|
||||
if let tabTitle {
|
||||
Text(tabTitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
if let tabTitle {
|
||||
Text(tabTitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
.buttonStyle(.plain)
|
||||
.focusable()
|
||||
.focused(focusedNotificationId, equals: notification.id)
|
||||
.modifier(DefaultActionModifier(isActive: focusedNotificationId.wrappedValue == notification.id))
|
||||
|
||||
Button(action: onClear) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
|
|
@ -563,8 +598,18 @@ private struct NotificationPopoverRow: View {
|
|||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(nsColor: .controlBackgroundColor))
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onOpen)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DefaultActionModifier: ViewModifier {
|
||||
let isActive: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if isActive {
|
||||
content.keyboardShortcut(.defaultAction)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
if nsError.domain == SUSparkleErrorDomain {
|
||||
switch nsError.code {
|
||||
case 4005:
|
||||
return "Updater Permission Error"
|
||||
case 2001:
|
||||
return "Couldn't Download Update"
|
||||
case 1000, 1002:
|
||||
|
|
@ -237,6 +239,8 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
if nsError.domain == SUSparkleErrorDomain {
|
||||
switch nsError.code {
|
||||
case 4005:
|
||||
return "macOS blocked cmuxterm from updating apps. Open System Settings → Privacy & Security → App Management, allow cmuxterm, then try again."
|
||||
case 2001:
|
||||
return "cmuxterm couldn’t download the update feed. Check your connection and try again."
|
||||
case 1000, 1002:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue