Add dev build branding and reload scripts

This commit is contained in:
Lawrence Chen 2026-01-28 01:20:48 -08:00
parent e3ee246930
commit 17a3e2033f
15 changed files with 232 additions and 42 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 B

After

Width:  |  Height:  |  Size: 738 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Before After
Before After

View file

@ -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;

View file

@ -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
}

View file

@ -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
View 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
View 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