From 9a9fe6acc2f593319e2117b5836ae5db1665d3f8 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:41:55 -0700 Subject: [PATCH 1/3] Fix iOS unread count contrast --- .../WorkspaceBackButton.swift | 37 +++++++++++++++---- .../WorkspaceShellView.swift | 1 + 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift index 284c4f3e2232..796277698cf0 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift @@ -4,11 +4,17 @@ import SwiftUI /// Custom back control for the workspace detail that folds the unread-workspace /// count INTO the back button itself (one button), instead of a separate pill. /// When other workspaces are unread it reads as "‹ 3"; otherwise it is just the -/// chevron. Tinted with the primary label color (white on the dark terminal bar, -/// black on a light bar) rather than the accent blue, so the count reads as plain -/// text with no colored background. The button widens to fit the count. +/// chevron. struct WorkspaceBackButton: View { + enum BadgeContrast { + /// Use on dark terminal chrome: white circle, black text. + case darkBackground + /// Use on light chrome: black circle, white text. + case lightBackground + } + let unreadCount: Int + var badgeContrast: BadgeContrast = .lightBackground let action: () -> Void var body: some View { @@ -22,13 +28,10 @@ struct WorkspaceBackButton: View { // Smaller than the chevron, on a small mono circle. .font(.caption2.weight(.semibold)) .monospacedDigit() - // Number contrasts the circle: dark on the white circle - // (dark bar) / light on the black circle (light bar). - .foregroundStyle(Color(.systemBackground)) + .foregroundStyle(badgeTextColor) .padding(2) .frame(minWidth: 18, minHeight: 18) - // White/black circle (adapts), not the accent blue. - .background(.primary, in: .circle) + .background(badgeFillColor, in: .circle) } } .contentShape(.rect) @@ -43,6 +46,24 @@ struct WorkspaceBackButton: View { unreadCount > 99 ? "99+" : "\(unreadCount)" } + private var badgeFillColor: Color { + switch badgeContrast { + case .darkBackground: + .white + case .lightBackground: + .black + } + } + + private var badgeTextColor: Color { + switch badgeContrast { + case .darkBackground: + .black + case .lightBackground: + .white + } + } + private var accessibilityLabel: String { let back = L10n.string("mobile.workspace.back", defaultValue: "Back") guard unreadCount > 0 else { return back } diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift index ee434a5e2a47..0d9961f29802 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift @@ -104,6 +104,7 @@ struct WorkspaceShellView: View { ToolbarItem(placement: .topBarLeading) { WorkspaceBackButton( unreadCount: unreadWorkspaceCount(excluding: workspaceID), + badgeContrast: .darkBackground, action: popCompactStack ) } From 6316ca810946b3f0cfdba925e061c8606721c457 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:49:00 -0700 Subject: [PATCH 2/3] Move iOS back button badge contrast type --- .../Sources/CmuxMobileShellUI/WorkspaceBackButton.swift | 9 +-------- .../WorkspaceBackButtonBadgeContrast.swift | 6 ++++++ 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButtonBadgeContrast.swift diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift index 796277698cf0..19be3e26f5d4 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButton.swift @@ -6,15 +6,8 @@ import SwiftUI /// When other workspaces are unread it reads as "‹ 3"; otherwise it is just the /// chevron. struct WorkspaceBackButton: View { - enum BadgeContrast { - /// Use on dark terminal chrome: white circle, black text. - case darkBackground - /// Use on light chrome: black circle, white text. - case lightBackground - } - let unreadCount: Int - var badgeContrast: BadgeContrast = .lightBackground + var badgeContrast: WorkspaceBackButtonBadgeContrast = .lightBackground let action: () -> Void var body: some View { diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButtonBadgeContrast.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButtonBadgeContrast.swift new file mode 100644 index 000000000000..b26d82faab82 --- /dev/null +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceBackButtonBadgeContrast.swift @@ -0,0 +1,6 @@ +enum WorkspaceBackButtonBadgeContrast { + /// Use on dark terminal chrome: white circle, black text. + case darkBackground + /// Use on light chrome: black circle, white text. + case lightBackground +} From 1afa9c8625b3a08f1a0005b67b1aa67bdebd85da Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:04:20 -0700 Subject: [PATCH 3/3] Validate GhosttyKit package test cache --- .github/workflows/ci.yml | 22 ++++++++++++++----- .../CanvasRootView+Viewport.swift | 2 +- .../Sources/CmuxCanvasUI/CanvasRootView.swift | 4 ++-- .../CanvasViewportZoomTests.swift | 18 +++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e6513eff21e..0e6c2eaa1b35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -781,8 +781,20 @@ jobs: path: GhosttyKit.xcframework key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }} + - name: Validate cached GhosttyKit.xcframework + id: validate-ghosttykit-package-tests + shell: bash + run: | + set -euo pipefail + if [ -f GhosttyKit.xcframework/Info.plist ]; then + echo "valid=true" >> "$GITHUB_OUTPUT" + else + rm -rf GhosttyKit.xcframework + echo "valid=false" >> "$GITHUB_OUTPUT" + fi + - name: Download pre-built GhosttyKit.xcframework - if: steps.cache-ghosttykit-package-tests.outputs.cache-hit != 'true' + if: steps.validate-ghosttykit-package-tests.outputs.valid != 'true' run: | ./scripts/download-prebuilt-ghosttykit.sh @@ -850,16 +862,16 @@ jobs: echo "::group::swift test $pkgdir" case "$pkg" in CmuxTerminal|CmuxTerminalCore) - status=0 - output="$(swift test --package-path "$pkgdir" 2>&1)" || status=$? + test_status=0 + output="$(swift test --package-path "$pkgdir" 2>&1)" || test_status=$? printf '%s\n' "$output" - if [ "$status" -ne 0 ]; then + if [ "$test_status" -ne 0 ]; then if printf '%s\n' "$output" | grep -Eq 'Test run with [0-9]+ tests( in [0-9]+ suites)? passed' \ && ! printf '%s\n' "$output" | grep -Eq 'with [1-9][0-9]* failures?' \ && ! printf '%s\n' "$output" | grep -v 'unexpected binary' | grep -Eq '(^|[^a-zA-Z])error:'; then echo "Tolerated cosmetic GhosttyKit binaryTarget diagnostic; all tests passed." else - exit "$status" + exit "$test_status" fi fi ;; diff --git a/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView+Viewport.swift b/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView+Viewport.swift index c096623ff14f..6138182711a3 100644 --- a/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView+Viewport.swift +++ b/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView+Viewport.swift @@ -146,7 +146,7 @@ extension CanvasRootView: CanvasViewportControlling { cancelDiscreteZoomAnimation(commitPending: true) guard magnification != scrollView.magnification else { return } let center = currentCenterInCanvas - if NSWorkspace.shared.accessibilityDisplayShouldReduceMotion { + if shouldReduceMotionForDiscreteZoom() { applyViewport(center: center, magnification: magnification, notifySettled: true) return } diff --git a/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift b/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift index e97231fab37f..102c722fd166 100644 --- a/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift +++ b/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift @@ -48,11 +48,11 @@ public final class CanvasRootView: NSView { static var didShowCommandScrollHintThisSession = false var commandScrollHintTask: Task? var commandScrollHintHost: NSHostingView? - /// A saved viewport waiting to be applied once the scroll view is laid - /// out (contentSize settled). Cleared when successfully applied. + /// A saved viewport waiting for contentSize to settle. Cleared when applied. private var pendingViewportRestore: (canvasCenter: CGPoint, magnification: CGFloat)? var pendingDiscreteZoomAnimation: (canvasCenter: CGPoint, magnification: CGFloat)? var discreteZoomAnimationGeneration: UInt64 = 0 + var shouldReduceMotionForDiscreteZoom: () -> Bool = { NSWorkspace.shared.accessibilityDisplayShouldReduceMotion } /// True while programmatically applying a saved viewport, so the scroll /// events that causes don't overwrite the saved value with transients. private var isApplyingSavedViewport = false diff --git a/Packages/macOS/CmuxCanvasUI/Tests/CmuxCanvasUITests/CanvasViewportZoomTests.swift b/Packages/macOS/CmuxCanvasUI/Tests/CmuxCanvasUITests/CanvasViewportZoomTests.swift index 086bd5cc18f0..acbf20f85629 100644 --- a/Packages/macOS/CmuxCanvasUI/Tests/CmuxCanvasUITests/CanvasViewportZoomTests.swift +++ b/Packages/macOS/CmuxCanvasUI/Tests/CmuxCanvasUITests/CanvasViewportZoomTests.swift @@ -9,6 +9,7 @@ import CmuxCanvas struct CanvasViewportZoomTests { @Test func discreteZoomOutAnimatesThenCommitsAroundCurrentCenter() throws { let root = makeRoot() + root.shouldReduceMotionForDiscreteZoom = { false } root.setViewport(center: CGPoint(x: 420, y: 180), magnification: 1, notifySettled: false) let centerBefore = root.currentCenterInCanvas @@ -23,8 +24,23 @@ struct CanvasViewportZoomTests { #expect(abs(root.currentCenterInCanvas.y - centerBefore.y) < 0.5) } + @Test func reduceMotionZoomAppliesImmediately() throws { + let root = makeRoot() + root.shouldReduceMotionForDiscreteZoom = { true } + root.setViewport(center: CGPoint(x: 420, y: 180), magnification: 1, notifySettled: false) + let centerBefore = root.currentCenterInCanvas + + root.zoom(by: 0.8) + + #expect(root.pendingDiscreteZoomAnimation == nil) + #expect(abs(root.currentMagnification - 0.8) < 0.0001) + #expect(abs(root.currentCenterInCanvas.x - centerBefore.x) < 0.5) + #expect(abs(root.currentCenterInCanvas.y - centerBefore.y) < 0.5) + } + @Test func repeatedDiscreteZoomOutClampsAtMinimumWithoutStackingAnimations() throws { let root = makeRoot() + root.shouldReduceMotionForDiscreteZoom = { false } root.setViewport(center: CGPoint(x: 420, y: 180), magnification: 1, notifySettled: false) for _ in 0..<12 { @@ -37,6 +53,7 @@ struct CanvasViewportZoomTests { @Test func overviewCancelsPendingDiscreteZoomCompletion() throws { let root = makeRoot() + root.shouldReduceMotionForDiscreteZoom = { false } root.setViewport(center: CGPoint(x: 420, y: 180), magnification: 1, notifySettled: false) root.zoom(by: 0.8) @@ -60,6 +77,7 @@ struct CanvasViewportZoomTests { (panelA, CGRect(x: 0, y: 0, width: 640, height: 360)), (panelB, CGRect(x: 1_600, y: 0, width: 640, height: 360)), ]) + root.shouldReduceMotionForDiscreteZoom = { false } root.setViewport(center: CGPoint(x: 320, y: 180), magnification: 1, notifySettled: false) root.zoom(by: 0.8)