import SwiftUI /// The progress bar to show a surface progress report. We implement this from scratch because the /// standard ProgressView is broken on macOS 26 and this is simple anyways and gives us a ton of /// control. struct SurfaceProgressBar: View { let report: Ghostty.Action.ProgressReport private var color: Color { switch report.state { case .error: return .red case .pause: return .orange default: return .accentColor } } private var progress: UInt8? { // If we have an explicit progress use that. if let v = report.progress { return v } // Otherwise, if we're in the pause state, we act as if we're at 100%. if report.state == .pause { return 100 } return nil } private var accessibilityLabel: String { switch report.state { case .error: return "Terminal progress - Error" case .pause: return "Terminal progress - Paused" case .indeterminate: return "Terminal progress - In progress" default: return "Terminal progress" } } private var accessibilityValue: String { if let progress { return "\(progress) percent complete" } else { switch report.state { case .error: return "Operation failed" case .pause: return "Operation paused at completion" case .indeterminate: return "Operation in progress" default: return "Indeterminate progress" } } } var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { if let progress { // Determinate progress bar with specific percentage Rectangle() .fill(color) .frame( width: geometry.size.width * CGFloat(progress) / 100, height: geometry.size.height ) .animation(.easeInOut(duration: 0.2), value: progress) } else { // Indeterminate states without specific progress - all use bouncing animation BouncingProgressBar(color: color) } } } .frame(height: 2) .clipped() .allowsHitTesting(false) .accessibilityElement(children: .ignore) .accessibilityAddTraits(.updatesFrequently) .accessibilityLabel(accessibilityLabel) .accessibilityValue(accessibilityValue) } } /// Bouncing progress bar for indeterminate states private struct BouncingProgressBar: View { let color: Color @State private var position: CGFloat = 0 private let barWidthRatio: CGFloat = 0.25 var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { Rectangle() .fill(color.opacity(0.3)) Rectangle() .fill(color) .frame( width: geometry.size.width * barWidthRatio, height: geometry.size.height ) .offset(x: position * (geometry.size.width * (1 - barWidthRatio))) } } .onAppear { withAnimation( .easeInOut(duration: 1.2) .repeatForever(autoreverses: true) ) { position = 1 } } .onDisappear { position = 0 } } }