Add dev build branding and reload scripts
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 619 B After Width: | Height: | Size: 738 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 239 KiB |
|
|
@ -26,6 +26,10 @@
|
|||
A5001204 /* UpdateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001214 /* UpdateViewModel.swift */; };
|
||||
A5001205 /* UpdatePill.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001215 /* UpdatePill.swift */; };
|
||||
A5001206 /* UpdateBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001216 /* UpdateBadge.swift */; };
|
||||
A500120A /* UpdateTiming.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001220 /* UpdateTiming.swift */; };
|
||||
A500120B /* UpdateTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001221 /* UpdateTestSupport.swift */; };
|
||||
A500120C /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001222 /* WindowAccessor.swift */; };
|
||||
A500120D /* UpdateLogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001223 /* UpdateLogStore.swift */; };
|
||||
A5001207 /* UpdatePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001217 /* UpdatePopoverView.swift */; };
|
||||
A5001208 /* UpdateTitlebarAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001218 /* UpdateTitlebarAccessory.swift */; };
|
||||
A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; };
|
||||
|
|
@ -33,6 +37,7 @@
|
|||
A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; };
|
||||
84E00D47E4584162AE53BC8D /* xterm-ghostty in Resources */ = {isa = PBXBuildFile; fileRef = B2E7294509CC42FE9191870E /* xterm-ghostty */; };
|
||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */; };
|
||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -82,10 +87,15 @@
|
|||
A5001214 /* UpdateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateViewModel.swift; sourceTree = "<group>"; };
|
||||
A5001215 /* UpdatePill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdatePill.swift; sourceTree = "<group>"; };
|
||||
A5001216 /* UpdateBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateBadge.swift; sourceTree = "<group>"; };
|
||||
A5001220 /* UpdateTiming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTiming.swift; sourceTree = "<group>"; };
|
||||
A5001221 /* UpdateTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTestSupport.swift; sourceTree = "<group>"; };
|
||||
A5001217 /* UpdatePopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdatePopoverView.swift; sourceTree = "<group>"; };
|
||||
A5001218 /* UpdateTitlebarAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTitlebarAccessory.swift; sourceTree = "<group>"; };
|
||||
A5001219 /* WindowToolbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowToolbarController.swift; sourceTree = "<group>"; };
|
||||
A5001222 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
|
||||
A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = "<group>"; };
|
||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
|
||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
|
||||
A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
B2E7294509CC42FE9191870E /* xterm-ghostty */ = {isa = PBXFileReference; lastKnownFileType = file; path = "terminfo/78/xterm-ghostty"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
|
@ -164,9 +174,13 @@
|
|||
A5001214 /* UpdateViewModel.swift */,
|
||||
A5001215 /* UpdatePill.swift */,
|
||||
A5001216 /* UpdateBadge.swift */,
|
||||
A5001220 /* UpdateTiming.swift */,
|
||||
A5001221 /* UpdateTestSupport.swift */,
|
||||
A5001223 /* UpdateLogStore.swift */,
|
||||
A5001217 /* UpdatePopoverView.swift */,
|
||||
A5001218 /* UpdateTitlebarAccessory.swift */,
|
||||
A5001219 /* WindowToolbarController.swift */,
|
||||
A5001222 /* WindowAccessor.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -192,6 +206,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */,
|
||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */,
|
||||
);
|
||||
path = GhosttyTabsUITests;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -293,9 +308,13 @@
|
|||
A5001204 /* UpdateViewModel.swift in Sources */,
|
||||
A5001205 /* UpdatePill.swift in Sources */,
|
||||
A5001206 /* UpdateBadge.swift in Sources */,
|
||||
A500120A /* UpdateTiming.swift in Sources */,
|
||||
A500120B /* UpdateTestSupport.swift in Sources */,
|
||||
A500120D /* UpdateLogStore.swift in Sources */,
|
||||
A5001207 /* UpdatePopoverView.swift in Sources */,
|
||||
A5001208 /* UpdateTitlebarAccessory.swift in Sources */,
|
||||
A5001209 /* WindowToolbarController.swift in Sources */,
|
||||
A500120C /* WindowAccessor.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -304,6 +323,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */,
|
||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -390,8 +410,8 @@
|
|||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = cmux;
|
||||
INFOPLIST_KEY_CFBundleName = cmux;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "cmux DEV";
|
||||
INFOPLIST_KEY_CFBundleName = "cmux DEV";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = "";
|
||||
|
|
@ -416,8 +436,8 @@
|
|||
"-framework",
|
||||
Carbon,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmux.app;
|
||||
PRODUCT_NAME = cmux;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmux.app.debug;
|
||||
PRODUCT_NAME = "cmux DEV";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "cmux-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"originHash" : "e721da7f9826abdffcb6185e886155efa2514bd6234475f1afa893e29eb258d6",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"revision" : "5581748cef2bae787496fe6d61139aebe0a451f6",
|
||||
"version" : "2.8.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
|
|
@ -6,48 +6,146 @@ final class NonDraggableHostingView<Content: View>: NSHostingView<Content> {
|
|||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private struct DevTitlebarAccessoryView: View {
|
||||
var body: some View {
|
||||
Text("THIS IS A DEV BUILD")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundColor(.red)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
final class DevBuildAccessoryViewController: NSTitlebarAccessoryViewController {
|
||||
private let hostingView: NonDraggableHostingView<DevTitlebarAccessoryView>
|
||||
private let containerView = NSView()
|
||||
private var pendingSizeUpdate = false
|
||||
|
||||
init() {
|
||||
hostingView = NonDraggableHostingView(rootView: DevTitlebarAccessoryView())
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
view = containerView
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = true
|
||||
hostingView.translatesAutoresizingMaskIntoConstraints = true
|
||||
hostingView.autoresizingMask = [.width, .height]
|
||||
containerView.addSubview(hostingView)
|
||||
|
||||
scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidAppear() {
|
||||
super.viewDidAppear()
|
||||
scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
override func viewDidLayout() {
|
||||
super.viewDidLayout()
|
||||
scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
private func scheduleSizeUpdate() {
|
||||
guard !pendingSizeUpdate else { return }
|
||||
pendingSizeUpdate = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.pendingSizeUpdate = false
|
||||
self?.updateSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSize() {
|
||||
hostingView.invalidateIntrinsicContentSize()
|
||||
hostingView.layoutSubtreeIfNeeded()
|
||||
let labelSize = hostingView.fittingSize
|
||||
let titlebarHeight = view.window.map { window in
|
||||
window.frame.height - window.contentLayoutRect.height
|
||||
} ?? labelSize.height
|
||||
let containerHeight = max(labelSize.height, titlebarHeight)
|
||||
let yOffset = max(0, (containerHeight - labelSize.height) / 2.0)
|
||||
preferredContentSize = NSSize(width: labelSize.width, height: containerHeight)
|
||||
containerView.frame = NSRect(x: 0, y: 0, width: labelSize.width, height: containerHeight)
|
||||
hostingView.frame = NSRect(x: 0, y: yOffset, width: labelSize.width, height: labelSize.height)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private struct TitlebarAccessoryView: View {
|
||||
@ObservedObject var model: UpdateViewModel
|
||||
let onIdleTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
UpdatePill(
|
||||
model: model,
|
||||
showWhenIdle: false,
|
||||
onIdleTap: onIdleTap
|
||||
)
|
||||
.fixedSize()
|
||||
.padding(.top, 4)
|
||||
UpdatePill(model: model)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
|
||||
final class UpdateAccessoryViewController: NSTitlebarAccessoryViewController {
|
||||
private var sizeCancellable: AnyCancellable?
|
||||
private let hostingView: NonDraggableHostingView<TitlebarAccessoryView>
|
||||
private let containerView = NSView()
|
||||
private var stateCancellable: AnyCancellable?
|
||||
private var pendingSizeUpdate = false
|
||||
|
||||
init(model: UpdateViewModel) {
|
||||
hostingView = NonDraggableHostingView(rootView: TitlebarAccessoryView(model: model))
|
||||
|
||||
init(model: UpdateViewModel, onIdleTap: @escaping () -> Void) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let hostingView = NonDraggableHostingView(rootView: TitlebarAccessoryView(
|
||||
model: model,
|
||||
onIdleTap: onIdleTap
|
||||
))
|
||||
hostingView.setFrameSize(hostingView.fittingSize)
|
||||
view = hostingView
|
||||
view = containerView
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = true
|
||||
hostingView.translatesAutoresizingMaskIntoConstraints = true
|
||||
hostingView.autoresizingMask = [.width, .height]
|
||||
containerView.addSubview(hostingView)
|
||||
|
||||
sizeCancellable = model.$state
|
||||
stateCancellable = model.$state
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak hostingView] _ in
|
||||
guard let hostingView else { return }
|
||||
hostingView.invalidateIntrinsicContentSize()
|
||||
hostingView.layoutSubtreeIfNeeded()
|
||||
hostingView.setFrameSize(hostingView.fittingSize)
|
||||
.sink { [weak self] _ in
|
||||
self?.scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidAppear() {
|
||||
super.viewDidAppear()
|
||||
scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
override func viewDidLayout() {
|
||||
super.viewDidLayout()
|
||||
scheduleSizeUpdate()
|
||||
}
|
||||
|
||||
private func scheduleSizeUpdate() {
|
||||
guard !pendingSizeUpdate else { return }
|
||||
pendingSizeUpdate = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.pendingSizeUpdate = false
|
||||
self?.updateSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSize() {
|
||||
hostingView.invalidateIntrinsicContentSize()
|
||||
hostingView.layoutSubtreeIfNeeded()
|
||||
let pillSize = hostingView.fittingSize
|
||||
let titlebarHeight = view.window.map { window in
|
||||
window.frame.height - window.contentLayoutRect.height
|
||||
} ?? pillSize.height
|
||||
let containerHeight = max(pillSize.height, titlebarHeight)
|
||||
let yOffset = max(0, (containerHeight - pillSize.height) / 2.0)
|
||||
preferredContentSize = NSSize(width: pillSize.width, height: containerHeight)
|
||||
containerView.frame = NSRect(x: 0, y: 0, width: pillSize.width, height: containerHeight)
|
||||
hostingView.frame = NSRect(x: 0, y: yOffset, width: pillSize.width, height: pillSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
final class UpdateTitlebarAccessoryController {
|
||||
|
|
@ -55,6 +153,12 @@ final class UpdateTitlebarAccessoryController {
|
|||
private var didStart = false
|
||||
private let attachedWindows = NSHashTable<NSWindow>.weakObjects()
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var stateCancellable: AnyCancellable?
|
||||
private var lastIsIdle: Bool?
|
||||
private let updateIdentifier = NSUserInterfaceItemIdentifier("cmux.updateAccessory")
|
||||
#if DEBUG
|
||||
private let devIdentifier = NSUserInterfaceItemIdentifier("cmux.devAccessory")
|
||||
#endif
|
||||
|
||||
init(viewModel: UpdateViewModel) {
|
||||
self.updateViewModel = viewModel
|
||||
|
|
@ -71,6 +175,11 @@ final class UpdateTitlebarAccessoryController {
|
|||
didStart = true
|
||||
attachToExistingWindows()
|
||||
installObservers()
|
||||
installStateObserver()
|
||||
}
|
||||
|
||||
func attach(to window: NSWindow) {
|
||||
attachIfNeeded(to: window)
|
||||
}
|
||||
|
||||
private func installObservers() {
|
||||
|
|
@ -105,24 +214,58 @@ final class UpdateTitlebarAccessoryController {
|
|||
guard !attachedWindows.contains(window) else { return }
|
||||
guard window.styleMask.contains(.titled) else { return }
|
||||
|
||||
let identifier = NSUserInterfaceItemIdentifier("cmux.updateAccessory")
|
||||
if window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == identifier }) {
|
||||
attachedWindows.add(window)
|
||||
return
|
||||
#if DEBUG
|
||||
if !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == devIdentifier }) {
|
||||
let devAccessory = DevBuildAccessoryViewController()
|
||||
devAccessory.layoutAttribute = .left
|
||||
devAccessory.view.identifier = devIdentifier
|
||||
window.addTitlebarAccessoryViewController(devAccessory)
|
||||
}
|
||||
#endif
|
||||
|
||||
if !window.titlebarAccessoryViewControllers.contains(where: { $0.view.identifier == updateIdentifier }) {
|
||||
let accessory = UpdateAccessoryViewController(model: updateViewModel)
|
||||
accessory.layoutAttribute = .right
|
||||
accessory.view.identifier = updateIdentifier
|
||||
window.addTitlebarAccessoryViewController(accessory)
|
||||
}
|
||||
|
||||
let accessory = UpdateAccessoryViewController(
|
||||
model: updateViewModel,
|
||||
onIdleTap: {
|
||||
guard let delegate = NSApp.delegate as? AppDelegate else { return }
|
||||
delegate.checkForUpdates(nil)
|
||||
}
|
||||
)
|
||||
accessory.layoutAttribute = .right
|
||||
|
||||
accessory.view.identifier = identifier
|
||||
|
||||
window.addTitlebarAccessoryViewController(accessory)
|
||||
attachedWindows.add(window)
|
||||
}
|
||||
|
||||
private func installStateObserver() {
|
||||
guard let updateViewModel else { return }
|
||||
stateCancellable = Publishers.CombineLatest(updateViewModel.$state, updateViewModel.$overrideState)
|
||||
.map { state, override in
|
||||
override ?? state
|
||||
}
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] state in
|
||||
guard let self else { return }
|
||||
let isIdle = state.isIdle
|
||||
if let lastIsIdle, lastIsIdle == isIdle {
|
||||
return
|
||||
}
|
||||
self.lastIsIdle = isIdle
|
||||
self.refreshAccessories(isIdle: isIdle)
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshAccessories(isIdle: Bool) {
|
||||
guard let updateViewModel else { return }
|
||||
|
||||
for window in attachedWindows.allObjects {
|
||||
if let index = window.titlebarAccessoryViewControllers.firstIndex(where: { $0.view.identifier == updateIdentifier }) {
|
||||
window.removeTitlebarAccessoryViewController(at: index)
|
||||
}
|
||||
|
||||
guard !isIdle else { continue }
|
||||
|
||||
let accessory = UpdateAccessoryViewController(model: updateViewModel)
|
||||
accessory.layoutAttribute = .right
|
||||
accessory.view.identifier = updateIdentifier
|
||||
window.addTitlebarAccessoryViewController(accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
scripts/reload-prod.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
pkill -x cmux || true
|
||||
sleep 0.2
|
||||
open /Users/lawrencechen/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/cmux.app
|
||||
6
scripts/reload.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
pkill -x "cmux DEV" || true
|
||||
sleep 0.2
|
||||
open /Users/lawrencechen/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Debug/cmux\ DEV.app
|
||||