Skip to content
Closed
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
14 changes: 14 additions & 0 deletions app/Sources/NotProtonApp/Model/SteamRepair.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ enum SteamRepair {
static func replace(_ app: URL, with staged: URL) throws {
let files = FileManager.default
try WriteRefused.catching(app) {
// The swap below cannot cross drives, so Steam on an external drive gets a copy beside it first.
var staged = staged
let folder = app.deletingLastPathComponent()
if volume(of: staged) != volume(of: folder) {
let copy = folder.appending(path: ".\(app.lastPathComponent).\(UUID().uuidString)")
try files.copyItem(at: staged, to: copy)
staged = copy
}
defer { try? files.removeItem(at: staged) }

guard files.fileExists(atPath: app.path(percentEncoded: false)) else {
try files.moveItem(at: staged, to: app)
return
Expand All @@ -186,6 +196,10 @@ enum SteamRepair {
}
}

private static func volume(of url: URL) -> URL? {
try? url.resourceValues(forKeys: [.volumeURLKey]).volume
}

static func clearInsert(at plist: URL) throws -> Bool {
guard var dict = SteamBundle.readInfoPlist(at: plist),
var environment = dict[SteamBundle.environmentKey] as? [String: Any]
Expand Down
8 changes: 7 additions & 1 deletion app/Sources/NotProtonApp/Support/SupportPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ enum SupportPaths {
static let bridgeCacheFolder = "bridge"

enum Steam {
static var app: URL { URL(filePath: "/Applications/Steam.app") }
static let defaultApp = URL(filePath: "/Applications/Steam.app")
// Set from the Status page when Steam lives somewhere else, like an external drive.
static let appPathKey = "steamAppPath"

static var app: URL {
UserDefaults.standard.string(forKey: appPathKey).map { URL(filePath: $0) } ?? defaultApp
}
static var infoPlist: URL { infoPlist(inBundle: app) }
static var executable: URL { app.appending(path: "Contents/MacOS/steam_osx") }
static var deployedDylib: URL { deployedDylib(inBundle: app) }
Expand Down
43 changes: 43 additions & 0 deletions app/Sources/NotProtonApp/SystemStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,49 @@ final class SystemStatus {
await refresh()
}

func chooseSteam() async {
let current = SupportPaths.Steam.app
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.allowedContentTypes = [.application]
panel.prompt = "Choose"
panel.message = "Select your copy of Steam."
panel.directoryURL = current.deletingLastPathComponent()

guard panel.runModal() == .OK, let url = panel.url else { return }
// The panel returns bundles as directories, with a trailing slash.
let picked = URL(filePath: url.path)

clearFailure()
outcome = nil

guard Bundle(url: picked)?.bundleIdentifier == SteamRepair.valveIdentifier else {
setFailure("\(picked.lastPathComponent) is not Steam.")
AppLog.note("steam choice refused: \(picked.path(percentEncoded: false))")
return
}
guard !CrossOverSource.same(picked, current) else { return }

// Moving on would leave the old copy patched, out of reach of Repair and Remove Everything.
let deployed = SupportPaths.Steam.deployedDylib(inBundle: current).path(percentEncoded: false)
if let insert = SteamBundle.currentInsert(), insert.split(separator: ":").contains(Substring(deployed)) {
setFailure("NotProton is installed in \(current.path(percentEncoded: false)). "
+ "Use Repair Steam to remove it before choosing another copy of Steam.")
AppLog.note("steam choice refused, still patched: \(current.path(percentEncoded: false))")
return
}

if CrossOverSource.same(picked, SupportPaths.Steam.defaultApp) {
UserDefaults.standard.removeObject(forKey: SupportPaths.Steam.appPathKey)
} else {
UserDefaults.standard.set(picked.path(percentEncoded: false), forKey: SupportPaths.Steam.appPathKey)
}
AppLog.note("steam chosen: \(picked.path(percentEncoded: false))")
await refresh()
}

private func highlight(_ row: CrossOverRow) {
AccessibilityNotification.Announcement("\(row.title) is already listed.").post()
highlightReset?.cancel()
Expand Down
9 changes: 9 additions & 0 deletions app/Sources/NotProtonApp/Views/StatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,15 @@ struct StatusView: View {
}

Section("Steam") {
StatusRow(
title: "Location",
value: SupportPaths.Steam.app.path(percentEncoded: false),
action: StatusAction(
label: "Choose\u{2026}",
help: "Use a copy of Steam from another folder or drive.",
isEnabled: status.isIdle
) { Task { await status.chooseSteam() } }
)
steamRow(snapshot.steam, payload: snapshot.payload)
if snapshot.steamRunning {
StatusRow(
Expand Down
Loading