Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ 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 {
let unreadCount: Int
var badgeContrast: WorkspaceBackButtonBadgeContrast = .lightBackground
let action: () -> Void

var body: some View {
Expand All @@ -22,13 +21,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)
Expand All @@ -43,6 +39,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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ struct WorkspaceShellView: View {
ToolbarItem(placement: .topBarLeading) {
WorkspaceBackButton(
unreadCount: unreadWorkspaceCount(excluding: workspaceID),
badgeContrast: .darkBackground,
action: popCompactStack
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ public final class CanvasRootView: NSView {
static var didShowCommandScrollHintThisSession = false
var commandScrollHintTask: Task<Void, Never>?
var commandScrollHintHost: NSHostingView<CanvasCommandScrollHint>?
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading