Skip to content

Commit

Permalink
Merge pull request XcodesOrg#134 from interstateone/format-installed-…
Browse files Browse the repository at this point in the history
…columns

Align installed command output into columns for interactive terminals
  • Loading branch information
Brandon Evans authored Feb 2, 2021
2 parents 09532d0 + ec9f655 commit 115de35
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 8 deletions.
2 changes: 2 additions & 0 deletions Sources/XcodesKit/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ public struct Shell {
}

public var exit: (Int32) -> Void = { Darwin.exit($0) }

public var isatty: () -> Bool = { Foundation.isatty(fileno(stdout)) != 0 }
}

public struct Files {
Expand Down
32 changes: 25 additions & 7 deletions Sources/XcodesKit/XcodeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -639,16 +639,34 @@ public final class XcodeInstaller {
public func printInstalledXcodes(directory: Path) -> Promise<Void> {
Current.shell.xcodeSelectPrintPath()
.done { pathOutput in
Current.files.installedXcodes(directory)
let installedXcodes = Current.files.installedXcodes(directory)
.sorted { $0.version < $1.version }
.forEach { installedXcode in
var output = installedXcode.version.appleDescriptionWithBuildIdentifier
if pathOutput.out.hasPrefix(installedXcode.path.string) {
output += " (Selected)"
}

// Add one so there's always at least one space between columns
let maxWidthOfFirstColumn = (installedXcodes.map(\.version.appleDescriptionWithBuildIdentifier.count).max() ?? 0) + 1

for installedXcode in installedXcodes {
let widthOfFirstColumnInThisRow = installedXcode.version.appleDescriptionWithBuildIdentifier.count
var spaceBetweenFirstAndSecondColumns = maxWidthOfFirstColumn - widthOfFirstColumnInThisRow

var output = installedXcode.version.appleDescriptionWithBuildIdentifier
let selectedString = " (Selected)"
if pathOutput.out.hasPrefix(installedXcode.path.string) {
output += selectedString
spaceBetweenFirstAndSecondColumns -= selectedString.count
}

// If outputting to an interactive terminal, align the columns so they're easier for a human to read
// Otherwise, separate columns by a tab character so it's easier for a computer to split up
if Current.shell.isatty() {
output += Array(repeating: " ", count: max(spaceBetweenFirstAndSecondColumns, 0))
output += "\(installedXcode.path.string)"
} else {
output += "\t\(installedXcode.path.string)"
Current.logging.log(output)
}

Current.logging.log(output)
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion Tests/XcodesKitTests/Environment+Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ extension Shell {
readLine: { _ in return nil },
readSecureLine: { _, _ in return nil },
env: { _ in nil },
exit: { _ in }
exit: { _ in },
isatty: { true }
)
}

Expand Down
111 changes: 111 additions & 0 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -906,4 +906,115 @@ final class XcodesKitTests: XCTestCase {
""")
}

func test_Installed_InteractiveTerminal() {
var log = ""
XcodesKit.Current.logging.log = { log.append($0 + "\n") }

// There are installed Xcodes
Current.files.contentsAtPath = { path in
if path == "/Applications/Xcode-0.0.0.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path == "/Applications/Xcode-2.0.0.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-2.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path == "/Applications/Xcode-2.0.1-Release.Candidate.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-2.0.1.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path.contains("version.plist") {
let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else {
return nil
}
}
let installedXcodes = [
InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!,
InstalledXcode(path: Path("/Applications/Xcode-2.0.0.app")!)!,
InstalledXcode(path: Path("/Applications/Xcode-2.0.1-Release.Candidate.app")!)!
]
Current.files.installedXcodes = { _ in installedXcodes }

// One is selected
Current.shell.xcodeSelectPrintPath = {
Promise.value((status: 0, out: "/Applications/Xcode-2.0.0.app/Contents/Developer", err: ""))
}

// Standard output is an interactive terminal
Current.shell.isatty = { true }

installer.printInstalledXcodes(directory: Path.root/"Applications")
.cauterize()

XCTAssertEqual(
log,
"""
0.0 (ABC123) /Applications/Xcode-0.0.0.app
2.0 (ABC123) (Selected) /Applications/Xcode-2.0.0.app
2.0.1 Release Candidate (ABC123) /Applications/Xcode-2.0.1-Release.Candidate.app
"""
)
}

func test_Installed_NonInteractiveTerminal() {
var log = ""
XcodesKit.Current.logging.log = { log.append($0 + "\n") }

// There are installed Xcodes
Current.files.contentsAtPath = { path in
if path == "/Applications/Xcode-0.0.0.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path == "/Applications/Xcode-2.0.0.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-2.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path == "/Applications/Xcode-2.0.1-Release.Candidate.app/Contents/Info.plist" {
let url = Bundle.module.url(forResource: "Stub-2.0.1.Info", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else if path.contains("version.plist") {
let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")!
return try? Data(contentsOf: url)
}
else {
return nil
}
}
let installedXcodes = [
InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!,
InstalledXcode(path: Path("/Applications/Xcode-2.0.0.app")!)!,
InstalledXcode(path: Path("/Applications/Xcode-2.0.1-Release.Candidate.app")!)!
]
Current.files.installedXcodes = { _ in installedXcodes }

// One is selected
Current.shell.xcodeSelectPrintPath = {
Promise.value((status: 0, out: "/Applications/Xcode-2.0.0.app/Contents/Developer", err: ""))
}

// Standard output is not an interactive terminal
Current.shell.isatty = { false }

installer.printInstalledXcodes(directory: Path.root/"Applications")
.cauterize()

XCTAssertEqual(
log,
"""
0.0 (ABC123)\t/Applications/Xcode-0.0.0.app
2.0 (ABC123) (Selected)\t/Applications/Xcode-2.0.0.app
2.0.1 Release Candidate (ABC123)\t/Applications/Xcode-2.0.1-Release.Candidate.app
"""
)
}

}

0 comments on commit 115de35

Please sign in to comment.