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
1 change: 1 addition & 0 deletions Muxy/Views/MainWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ struct MainWindow: View {

ProjectStatusBar(
activePane: activeTerminalPane,
activeWorktree: activeProject.flatMap { resolvedActiveWorktree(for: $0) },
isInteractive: activeTerminalPane != nil && !overlayAnimatingOut,
richInputVisible: richInputPanelVisible,
richInputFontSize: $richInputFontSize
Expand Down
61 changes: 55 additions & 6 deletions Muxy/Views/Terminal/ProjectStatusBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI

struct ProjectStatusBar: View {
let activePane: TerminalPaneState?
let activeWorktree: Worktree?
let isInteractive: Bool
let richInputVisible: Bool
@Binding var richInputFontSize: Double
Expand All @@ -14,7 +15,11 @@ struct ProjectStatusBar: View {
var body: some View {
HStack(spacing: 8) {
if let pane = activePane {
cwdLabel(pane)
pathButton(pane)
if let worktree = activeWorktree {
separator
worktreeLabel(worktree)
}
if let branch = pane.branchObserver.branch {
separator
branchLabel(branch)
Expand Down Expand Up @@ -42,17 +47,42 @@ struct ProjectStatusBar: View {
.accessibilityLabel("Status bar")
}

private func cwdLabel(_ pane: TerminalPaneState) -> some View {
private func pathButton(_ pane: TerminalPaneState) -> some View {
let fullPath = pane.currentWorkingDirectory ?? pane.projectPath
let displayPath = abbreviatePath(fullPath)
let truncated = ProjectStatusBar.truncatePath(displayPath, maxCharacters: ProjectStatusBar.pathMaxCharacters)
return Button {
revealInFinder(fullPath)
} label: {
HStack(spacing: 4) {
Image(systemName: "folder")
.font(.system(size: 10, weight: .semibold))
Text(truncated)
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
}
.foregroundStyle(MuxyTheme.fgMuted)
}
.buttonStyle(.plain)
.help(fullPath)
.accessibilityLabel("Reveal \(fullPath) in Finder")
.contextMenu {
Button("Copy Path") { copyToPasteboard(fullPath) }
Button("Reveal in Finder") { revealInFinder(fullPath) }
}
}

private func worktreeLabel(_ worktree: Worktree) -> some View {
HStack(spacing: 4) {
Image(systemName: "folder")
Image(systemName: "square.stack.3d.up")
.font(.system(size: 10, weight: .semibold))
Text(abbreviatePath(pane.currentWorkingDirectory ?? pane.projectPath))
Text(worktree.name)
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
.truncationMode(.middle)
.truncationMode(.tail)
}
.foregroundStyle(MuxyTheme.fgMuted)
.help(pane.currentWorkingDirectory ?? pane.projectPath)
.help("Worktree: \(worktree.name)")
}

private func branchLabel(_ branch: String) -> some View {
Expand Down Expand Up @@ -160,9 +190,28 @@ struct ProjectStatusBar: View {
NotificationCenter.default.post(name: .toggleRichInput, object: nil)
}

private func revealInFinder(_ path: String) {
let url = URL(fileURLWithPath: path)
NSWorkspace.shared.activateFileViewerSelecting([url])
}

private func copyToPasteboard(_ string: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(string, forType: .string)
}

private func abbreviatePath(_ path: String) -> String {
let home = NSHomeDirectory()
guard !home.isEmpty, path.hasPrefix(home) else { return path }
return "~" + path.dropFirst(home.count)
}

static let pathMaxCharacters = 40

static func truncatePath(_ path: String, maxCharacters: Int) -> String {
guard path.count > maxCharacters, maxCharacters > 1 else { return path }
let suffix = path.suffix(maxCharacters - 1)
return "…" + suffix
}
}
28 changes: 28 additions & 0 deletions Tests/MuxyTests/Views/ProjectStatusBarTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Testing

@testable import Muxy

@Suite("ProjectStatusBar")
@MainActor
struct ProjectStatusBarTests {
@Test("short paths are returned unchanged")
func shortPathUnchanged() {
let path = "~/Projects/muxy"
#expect(ProjectStatusBar.truncatePath(path, maxCharacters: 40) == path)
}

@Test("long paths keep the trailing portion with leading ellipsis")
func longPathTruncatedFromStart() {
let path = "~/Projects/muxy/worktree-checkouts/some-very-long-feature-branch/sources"
let result = ProjectStatusBar.truncatePath(path, maxCharacters: 40)
#expect(result.count == 40)
#expect(result.hasPrefix("…"))
#expect(path.hasSuffix(String(result.dropFirst())))
}

@Test("path exactly at the limit is unchanged")
func pathAtBoundary() {
let path = String(repeating: "a", count: 40)
#expect(ProjectStatusBar.truncatePath(path, maxCharacters: 40) == path)
}
}
Loading