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
24 changes: 16 additions & 8 deletions Shellraiser.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@
A1000050 /* GitBranchResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000050; };
A1000051 /* WorkspaceManager+GitBranches.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000051; };
A1000052 /* SurfaceInputEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000052; };
A1000053 /* SurfaceSearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000053; };
A1000054 /* TerminalSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000054; };
A1000053 /* SurfaceProgressReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000053; };
A1000054 /* SurfaceProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000054; };
A1000055 /* SurfaceSearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000055; };
A1000056 /* TerminalSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000056; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -112,8 +114,10 @@
A2000050 /* GitBranchResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Infrastructure/Git/GitBranchResolver.swift; sourceTree = "<group>"; };
A2000051 /* WorkspaceManager+GitBranches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Services/Workspaces/WorkspaceManager+GitBranches.swift"; sourceTree = "<group>"; };
A2000052 /* SurfaceInputEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models/SurfaceInputEvent.swift; sourceTree = "<group>"; };
A2000053 /* SurfaceSearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models/SurfaceSearchState.swift; sourceTree = "<group>"; };
A2000054 /* TerminalSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/WorkspaceDetail/TerminalSearchOverlay.swift; sourceTree = "<group>"; };
A2000053 /* SurfaceProgressReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models/SurfaceProgressReport.swift; sourceTree = "<group>"; };
A2000054 /* SurfaceProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/WorkspaceDetail/SurfaceProgressBar.swift; sourceTree = "<group>"; };
A2000055 /* SurfaceSearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models/SurfaceSearchState.swift; sourceTree = "<group>"; };
A2000056 /* TerminalSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/WorkspaceDetail/TerminalSearchOverlay.swift; sourceTree = "<group>"; };
A3000001 /* Shellraiser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Shellraiser.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -151,7 +155,8 @@
A2000018 /* TerminalPanelConfig.swift */,
A2000019 /* SurfaceModel.swift */,
A2000052 /* SurfaceInputEvent.swift */,
A2000053 /* SurfaceSearchState.swift */,
A2000053 /* SurfaceProgressReport.swift */,
A2000055 /* SurfaceSearchState.swift */,
A2000020 /* PaneNodeModel.swift */,
A2000041 /* PaneNodeModel+Operations.swift */,
A2000021 /* WorkspaceModel.swift */,
Expand All @@ -175,8 +180,9 @@
A2000037 /* PaneNodeView.swift */,
A2000038 /* PaneSplitView.swift */,
A2000039 /* PaneLeafView.swift */,
A2000054 /* TerminalSearchOverlay.swift */,
A2000056 /* TerminalSearchOverlay.swift */,
A2000040 /* SurfaceTabButton.swift */,
A2000054 /* SurfaceProgressBar.swift */,
A2000008 /* GhosttyTerminalView.swift */,
A2000012 /* LibghosttySurfaceView.swift */,
A2000013 /* GhosttyRuntime.swift */,
Expand Down Expand Up @@ -291,7 +297,9 @@
A1000018 /* TerminalPanelConfig.swift in Sources */,
A1000019 /* SurfaceModel.swift in Sources */,
A1000052 /* SurfaceInputEvent.swift in Sources */,
A1000053 /* SurfaceSearchState.swift in Sources */,
A1000053 /* SurfaceProgressReport.swift in Sources */,
A1000054 /* SurfaceProgressBar.swift in Sources */,
A1000055 /* SurfaceSearchState.swift in Sources */,
A1000020 /* PaneNodeModel.swift in Sources */,
A1000041 /* PaneNodeModel+Operations.swift in Sources */,
A1000021 /* WorkspaceModel.swift in Sources */,
Expand All @@ -311,7 +319,7 @@
A1000037 /* PaneNodeView.swift in Sources */,
A1000038 /* PaneSplitView.swift in Sources */,
A1000039 /* PaneLeafView.swift in Sources */,
A1000054 /* TerminalSearchOverlay.swift in Sources */,
A1000056 /* TerminalSearchOverlay.swift in Sources */,
A1000040 /* SurfaceTabButton.swift in Sources */,
A1000008 /* GhosttyTerminalView.swift in Sources */,
A1000009 /* WorkspaceCatalogManager.swift in Sources */,
Expand Down
24 changes: 17 additions & 7 deletions Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ protocol GhosttyTerminalHostView: GhosttyFocusableHost {
onTitleChange: @escaping (String) -> Void,
onWorkingDirectoryChange: @escaping (String) -> Void,
onChildExited: @escaping () -> Void,
onPaneNavigationRequest: @escaping (PaneNodeModel.PaneFocusDirection) -> Void
onPaneNavigationRequest: @escaping (PaneNodeModel.PaneFocusDirection) -> Void,
onProgressReport: @escaping (SurfaceProgressReport?) -> Void
)
}

Expand Down Expand Up @@ -79,6 +80,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
let onWorkingDirectoryChange: (String) -> Void
let onChildExited: () -> Void
let onPaneNavigationRequest: (PaneNodeModel.PaneFocusDirection) -> Void
let onProgressReport: (SurfaceProgressReport?) -> Void
let onSearchStateChange: (SurfaceSearchState?) -> Void

/// Builds the AppKit surface host.
Expand All @@ -95,6 +97,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
onWorkingDirectoryChange: onWorkingDirectoryChange,
onChildExited: onChildExited,
onPaneNavigationRequest: onPaneNavigationRequest,
onProgressReport: onProgressReport,
onSearchStateChange: onSearchStateChange
)
Self.syncContainerView(
Expand All @@ -110,7 +113,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
onTitleChange: onTitleChange,
onWorkingDirectoryChange: onWorkingDirectoryChange,
onChildExited: onChildExited,
onPaneNavigationRequest: onPaneNavigationRequest
onPaneNavigationRequest: onPaneNavigationRequest,
onProgressReport: onProgressReport
)
return containerView
#else
Expand All @@ -134,6 +138,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
onWorkingDirectoryChange: onWorkingDirectoryChange,
onChildExited: onChildExited,
onPaneNavigationRequest: onPaneNavigationRequest,
onProgressReport: onProgressReport,
onSearchStateChange: onSearchStateChange
)
Self.syncContainerView(
Expand All @@ -149,7 +154,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
onTitleChange: onTitleChange,
onWorkingDirectoryChange: onWorkingDirectoryChange,
onChildExited: onChildExited,
onPaneNavigationRequest: onPaneNavigationRequest
onPaneNavigationRequest: onPaneNavigationRequest,
onProgressReport: onProgressReport
)
#endif
}
Expand All @@ -176,7 +182,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
onTitleChange: @escaping (String) -> Void,
onWorkingDirectoryChange: @escaping (String) -> Void,
onChildExited: @escaping () -> Void,
onPaneNavigationRequest: @escaping (PaneNodeModel.PaneFocusDirection) -> Void
onPaneNavigationRequest: @escaping (PaneNodeModel.PaneFocusDirection) -> Void,
onProgressReport: @escaping (SurfaceProgressReport?) -> Void
) {
if container.mountedSurfaceId != surface.id {
if let mountedSurfaceId = container.mountedSurfaceId {
Expand All @@ -198,7 +205,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
onTitleChange: onTitleChange,
onWorkingDirectoryChange: onWorkingDirectoryChange,
onChildExited: onChildExited,
onPaneNavigationRequest: onPaneNavigationRequest
onPaneNavigationRequest: onPaneNavigationRequest,
onProgressReport: onProgressReport
)
}

Expand All @@ -225,7 +233,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
onTitleChange: @escaping (String) -> Void,
onWorkingDirectoryChange: @escaping (String) -> Void,
onChildExited: @escaping () -> Void,
onPaneNavigationRequest: @escaping (PaneNodeModel.PaneFocusDirection) -> Void
onPaneNavigationRequest: @escaping (PaneNodeModel.PaneFocusDirection) -> Void,
onProgressReport: @escaping (SurfaceProgressReport?) -> Void
) {
host.update(
surfaceModel: surface,
Expand All @@ -236,7 +245,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
onTitleChange: onTitleChange,
onWorkingDirectoryChange: onWorkingDirectoryChange,
onChildExited: onChildExited,
onPaneNavigationRequest: onPaneNavigationRequest
onPaneNavigationRequest: onPaneNavigationRequest,
onProgressReport: onProgressReport
)
runtime.setSurfaceFocus(surfaceId: surface.id, focused: isFocused)
runtime.restorePendingFocusIfNeeded(surfaceId: surface.id, hostView: host)
Expand Down
18 changes: 18 additions & 0 deletions Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ struct PaneLeafView: View {
onPaneNavigationRequest: { direction in
manager.focusAdjacentPane(from: activeSurface.id, direction: direction)
},
onProgressReport: { report in
manager.setProgressReport(
workspaceId: workspaceId,
surfaceId: activeSurface.id,
report: report
)
},
onSearchStateChange: { state in
searchState = state
}
Expand All @@ -255,6 +262,17 @@ struct PaneLeafView: View {
.allowsHitTesting(false)
}

if let activeSurface,
let report = manager.progressBySurfaceId[activeSurface.id] {
VStack {
SurfaceProgressBar(report: report)
.padding(.horizontal, 8)
.padding(.top, 6)
Spacer()
}
.allowsHitTesting(false)
}

if let searchState {
TerminalSearchOverlay(
searchState: searchState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import SwiftUI

/// 2px progress bar rendered at the top of a terminal surface pane.
///
/// Renders a determinate fill for known percentages and a bouncing animation
/// for indeterminate states. Color reflects the report state: red for error,
/// orange for pause, accent for all others.
struct SurfaceProgressBar: View {
let report: SurfaceProgressReport

private var color: Color { report.state.tintColor }

/// Resolved percentage value, treating pause-without-progress as 100%.
private var progress: UInt8? {
if let v = report.progress { return v }
if report.state == .pause { return 100 }
return nil
}

var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
if let progress {
Rectangle()
.fill(color)
.frame(
width: geometry.size.width * CGFloat(progress) / 100,
height: geometry.size.height
)
.animation(.easeInOut(duration: 0.2), value: progress)
} else {
BouncingProgressBar(color: color)
}
}
}
.frame(height: 2)
.clipped()
.allowsHitTesting(false)
.accessibilityElement(children: .ignore)
.accessibilityAddTraits(.updatesFrequently)
.accessibilityLabel(accessibilityLabel)
.accessibilityValue(accessibilityValue)
}

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

/// Bouncing animated bar used for indeterminate progress 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ struct SurfaceTabButton: View {
Color(nsColor: chromeStyle.foregroundColor).opacity(isSelected ? 0.76 : 0.55)
}

/// Color for the tab status dot derived from progress state, completion, or default.
private var dotFill: AnyShapeStyle {
if let report = manager.progressBySurfaceId[surface.id] {
return AnyShapeStyle(report.state.tintColor)
}
if surface.hasPendingCompletion { return AnyShapeStyle(AppTheme.accentGradient) }
return AnyShapeStyle(Color.white.opacity(0.2))
}

var body: some View {
HStack(spacing: 6) {
Button(action: onSelect) {
HStack(spacing: 8) {
Circle()
.fill(surface.hasPendingCompletion ? AnyShapeStyle(AppTheme.accentGradient) : AnyShapeStyle(Color.white.opacity(0.2)))
.fill(dotFill)
.frame(width: 6, height: 6)

Text(surface.title)
Expand Down
Loading
Loading