Skip to content

Commit

Permalink
library: add purge, label; staging: add list
Browse files Browse the repository at this point in the history
  • Loading branch information
yretenai committed Dec 22, 2024
1 parent 2f27e1c commit c183682
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
.vscode/launch.json
22 changes: 0 additions & 22 deletions .vscode/launch.json

This file was deleted.

2 changes: 0 additions & 2 deletions .vscode/launch.json.license

This file was deleted.

8 changes: 8 additions & 0 deletions Sources/Starvalve/Format/TextVDF.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ public struct TextVDF {
throw TextVDFError.truncated
}

public static func write(url: URL, vdf: ValveKeyValue) throws {
guard let data = try? write(vdf: vdf) else {
return
}

try data.write(to: url, atomically: true, encoding: .utf8)
}

public static func write(vdf: ValveKeyValue) throws -> String {
return try write(vdf: vdf, indent: 0)
}
Expand Down
15 changes: 11 additions & 4 deletions Sources/Starvalve/SteamLibraryFolders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import Foundation

/// A steam library folder and it's associated metadata.
public struct SteamLibraryFolder: VDFContent {
public class SteamLibraryFolder: VDFContent {
public var path: URL
public var label: String? = nil
public var contentID: UInt = 0
Expand All @@ -15,7 +15,7 @@ public struct SteamLibraryFolder: VDFContent {
public var timeLastUpdateVerified: Date = Date(timeIntervalSince1970: 0)
public var apps: [UInt: UInt] = [:]

public init?(vdf: ValveKeyValue) {
public required init?(vdf: ValveKeyValue) {
guard let path = vdf["path"]?.string else {
return nil
}
Expand Down Expand Up @@ -52,13 +52,20 @@ public struct SteamLibraryFolder: VDFContent {
vdf.append(ValveKeyValue(key: "apps", map: apps))
return vdf
}

public func singleVdf() -> ValveKeyValue {
let vdf = ValveKeyValue("libraryfolder")
vdf["contentid"] = ValveKeyValueNode(unsigned: contentID)
vdf["label"] = ValveKeyValueNode(label ?? "")
return vdf
}
}

/// All steam library folders.
public struct SteamLibraryFolders: VDFContent {
public class SteamLibraryFolders: VDFContent {
public var entries: [SteamLibraryFolder] = []

public init?(vdf: ValveKeyValue) {
public required init?(vdf: ValveKeyValue) {
entries = vdf.to(sequence: SteamLibraryFolder.self)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/StarvalveControl/Commands/LibraryCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct LibrariesCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "library",
abstract: "Commands relating to Steam libraries",
subcommands: [ListLibrariesCommand.self],
subcommands: [ListLibrariesCommand.self, PurgeLibraryCommand.self, LibraryLabelCommand.self],
defaultSubcommand: ListLibrariesCommand.self
)
}
43 changes: 43 additions & 0 deletions Sources/StarvalveControl/Commands/LibraryLabelCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]>
// SPDX-License-Identifier: EUPL-1.2

import ArgumentParser
import Foundation
import Starvalve

struct LibraryLabelCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "label",
abstract: "Updates the label for Steam Libraries."
)

@Argument(help: "The path of the library to update.", completion: .directory)
var path: URL

@Argument(help: "The label to apply to the library.")
var label: String? = nil

@OptionGroup var globals: GlobalOptions

func run() {
var steam = SteamHelper(steamPath: globals.steamPath)

guard let libraries = steam.libraryFolders else {
preconditionFailure("Steam libraries failed to parse.")
}

let target = path.canonicalPath.path

for library in libraries.entries {
if library.path.canonicalPath.path == target {
library.label = label ?? ""
try? TextVDF.write(url: library.path.appending(path: "libraryfolder.vdf", directoryHint: .notDirectory), vdf: library.singleVdf())
steam.libraryFolders = libraries
print("set library the label of \(library.path.path, color: .green) to \"\(label ?? "", color: .green)\"")
return
}
}

print("could not find library library \(target, color: .red)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ struct ListLibrariesCommand: ParsableCommand {
print("library \(library.path.path, color: .green)")

if let label = library.label {
print("label: \(label, color: .default)")
print("label: \(label, color: .green)")
} else {
print("label: \("<no label>", color: .red)")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]>
// SPDX-License-Identifier: EUPL-1.2

import ArgumentParser
import Foundation
import Starvalve

struct ListStagingLibrariesCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List games with their corresponding staging directories."
)

@OptionGroup var globals: GlobalOptions

func run() {
let steam = SteamHelper(steamPath: globals.steamPath)

guard let libraries = steam.libraryFolders else {
preconditionFailure("Steam libraries failed to parse.")
}

for library in libraries.entries {
for (appId, _) in library.apps {
guard let appInfo = AppInfo(libraryPath: library.path, appId: appId) else {
continue
}

let acf = appInfo.acf
guard let stagingIndex = acf.stagingFolder else {
continue
}

guard let stagingFolder = libraries.entries[optionally: stagingIndex] else {
print("⚠️ \(acf.name, color: .green) (\(acf.appId, color: .magenta)) has an invalid staging folder!")
continue
}
print("\(acf.name, color: .green) (\(acf.appId, color: .magenta)) has staging library set to \(stagingFolder.path.path).")
}
}
}
}
87 changes: 87 additions & 0 deletions Sources/StarvalveControl/Commands/PurgeLibraryCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]>
// SPDX-License-Identifier: EUPL-1.2

import ArgumentParser
import Foundation
import Starvalve

struct PurgeLibraryCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "purge",
abstract: "Removes the Steam Library and updates staging directories."
)

@Argument(help: "The path of the library to remove.", completion: .directory)
var path: URL

@Option(help: "The staging library to use for any libraries that rely on the specified library.", completion: .directory)
var stagingPath: URL?

@OptionGroup var globals: GlobalOptions

func run() {
var steam = SteamHelper(steamPath: globals.steamPath)

guard let libraries = steam.libraryFolders else {
preconditionFailure("Steam libraries failed to parse.")
}

let target = path.canonicalPath.path
let stagingTarget = stagingPath?.canonicalPath.path
var index: Int?
var selectedIndex: Int?

for libraryIndex in 0...libraries.entries.count - 1 {
let library = libraries.entries[libraryIndex]
let libraryPath = library.path.canonicalPath.path
if libraryPath == target && index == nil {
index = libraryIndex
}

if libraryPath == stagingTarget {
selectedIndex = libraryIndex
}
}

guard let index = index else {
print("could not find library path \(target, color: .red)")
return
}

libraries.entries.remove(at: index)

// for library in libraries.entries {
// for (appId, _) in library.apps {
// guard let appInfo = AppInfo(libraryPath: library.path, appId: appId) else {
// continue
// }

// var acf = appInfo.acf
// guard var stagingIndex = acf.stagingFolder else {
// continue
// }

// if stagingIndex == index {
// stagingIndex = selectedIndex ?? 0
// print("updated staging folder for app \(acf.name)")
// } else if stagingIndex > index {
// stagingIndex -= 1
// print("adjusted staging folder for app \(acf.name)")
// } else {
// continue
// }

// acf.stagingFolder = stagingIndex

// guard let _ = try? TextVDF.write(url: appInfo.acfPath, vdf: acf.vdf()) else {
// print("could not write file for appmanifest \(acf.name)")
// continue
// }
// }
// }

// todo: delete folder if it exists?

steam.libraryFolders = libraries
}
}
14 changes: 14 additions & 0 deletions Sources/StarvalveControl/Commands/StagingCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]>
// SPDX-License-Identifier: EUPL-1.2

import ArgumentParser
import Starvalve

struct StagingCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "staging",
abstract: "Commands relating to Staging Directories",
subcommands: [ListStagingLibrariesCommand.self],
defaultSubcommand: ListStagingLibrariesCommand.self
)
}
18 changes: 18 additions & 0 deletions Sources/StarvalveControl/Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]>
// SPDX-License-Identifier: EUPL-1.2

import ArgumentParser
import Foundation

enum ByteFormatting: UInt {
Expand All @@ -20,6 +21,12 @@ enum ASCIIColor: String {
case `default` = "\u{001B}[0;0m"
}

extension Collection {
subscript(optionally index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

extension URL {
@inlinable var canonicalPath: URL {
guard let path = try? resourceValues(forKeys: [.canonicalPathKey]).canonicalPath else {
Expand Down Expand Up @@ -50,6 +57,17 @@ extension URL {
}
}

extension URL: @retroactive ExpressibleByArgument {
/// initializes a string via a string argument.
public init(argument: String) {
if let url = URL(string: argument) {
self = url
} else {
self = URL(filePath: argument)
}
}
}

extension FileManager {
func directorySize(atPath path: URL) throws -> UInt {
var size: UInt = 0
Expand Down
2 changes: 1 addition & 1 deletion Sources/StarvalveControl/GlobalOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
import ArgumentParser

struct GlobalOptions: ParsableCommand {
@Argument(help: "Path to the steam installation")
@Option(name: .customLong("steam"), help: "Path to the steam installation")
var steamPath: String? = nil
}
2 changes: 1 addition & 1 deletion Sources/StarvalveControl/StarvalveControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ArgumentParser
struct StarvalveControl: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "A utility for manipulating Steam installations.",
subcommands: [ListAppsCommand.self, LibrariesCommand.self],
subcommands: [ListAppsCommand.self, LibrariesCommand.self, StagingCommand.self],
defaultSubcommand: ListAppsCommand.self
)
}
Loading

0 comments on commit c183682

Please sign in to comment.