diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 9065d5971758..43fb94e0b954 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -10855,19 +10855,42 @@ private struct SidebarEmptyArea: View { enum SidebarPathFormatter { static let homeDirectoryPath: String = FileManager.default.homeDirectoryForCurrentUser.path + /// Maximum number of path segments shown before adding a leading ellipsis. + /// e.g. `~/a/b/c/d` → `…/b/c/d` when maxSegments == 3. + static let maxDisplaySegments: Int = 3 + static func shortenedPath( _ path: String, homeDirectoryPath: String = Self.homeDirectoryPath ) -> String { let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return path } + + // Replace home directory prefix with ~ + let tildeReplaced: String if trimmed == homeDirectoryPath { return "~" - } - if trimmed.hasPrefix(homeDirectoryPath + "/") { - return "~" + trimmed.dropFirst(homeDirectoryPath.count) - } - return trimmed + } else if trimmed.hasPrefix(homeDirectoryPath + "/") { + tildeReplaced = "~" + trimmed.dropFirst(homeDirectoryPath.count) + } else { + tildeReplaced = trimmed + } + + // Apply smart truncation: keep only the last maxDisplaySegments segments + // so paths with a long common prefix remain distinguishable in the sidebar. + // e.g. `~/Desktop/YOKE/Claude Code/Projects/Athlete Merch/nilclub` + // → `…/Projects/Athlete Merch/nilclub` + let segments = tildeReplaced.split(separator: "/", omittingEmptySubsequences: false) + // For a tilde-replaced path like `~/a/b/c`, split on "/" yields + // ["~", "a", "b", "c"]. The tilde token is always a single leading segment. + // For an absolute path like `/tmp/a/b`, split yields ["", "tmp", "a", "b"]. + // We only truncate when there are strictly more segments than allowed. + let effectiveMax = maxDisplaySegments + 1 // +1 for the leading "~" or "" token + guard segments.count > effectiveMax else { + return tildeReplaced + } + let tail = segments.suffix(maxDisplaySegments).joined(separator: "/") + return "…/" + tail } } diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index d7820ce35ed1..88ff90f6b0e8 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -38,6 +38,85 @@ final class SidebarPathFormatterTests: XCTestCase { "/tmp/cmux" ) } + + // MARK: - Smart truncation (issue #2061) + + func testShortenedPathKeepsThreeSegmentsUnchanged() { + // ~/a/b/c — exactly maxDisplaySegments; no ellipsis added + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/Users/example/a/b/c", + homeDirectoryPath: "/Users/example" + ), + "~/a/b/c" + ) + } + + func testShortenedPathTruncatesLongHomePath() { + // ~/Desktop/YOKE/Claude Code/Projects/Athlete Merch/nilclub → …/Projects/Athlete Merch/nilclub + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/Users/example/Desktop/YOKE/Claude Code/Projects/Athlete Merch/nilclub", + homeDirectoryPath: "/Users/example" + ), + "…/Projects/Athlete Merch/nilclub" + ) + } + + func testShortenedPathTruncatesFourSegmentHomePath() { + // ~/a/b/c/d — one segment over limit → …/b/c/d + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/Users/example/a/b/c/d", + homeDirectoryPath: "/Users/example" + ), + "…/b/c/d" + ) + } + + func testShortenedPathLeavesShortHomePathUnchanged() { + // ~/projects/cmux — only 2 segments; no ellipsis + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/Users/example/projects/cmux", + homeDirectoryPath: "/Users/example" + ), + "~/projects/cmux" + ) + } + + func testShortenedPathTruncatesLongAbsolutePath() { + // /a/b/c/d/e — 5 segments (no home match) → …/c/d/e + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/a/b/c/d/e", + homeDirectoryPath: "/Users/example" + ), + "…/c/d/e" + ) + } + + func testShortenedPathLeavesShortAbsolutePathUnchanged() { + // /tmp/a/b — exactly 3 segments; no truncation + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/tmp/a/b", + homeDirectoryPath: "/Users/example" + ), + "/tmp/a/b" + ) + } + + func testShortenedPathTruncatesLongAbsolutePathBeyondThree() { + // /tmp/a/b/c — 4 segments; triggers truncation → …/a/b/c + XCTAssertEqual( + SidebarPathFormatter.shortenedPath( + "/tmp/a/b/c", + homeDirectoryPath: "/Users/example" + ), + "…/a/b/c" + ) + } } final class GhosttyConfigTests: XCTestCase {