diff --git a/SteamPrefill/CliCommands/Converters.cs b/SteamPrefill/CliCommands/Converters.cs index 2d9e9ea8..216171c4 100644 --- a/SteamPrefill/CliCommands/Converters.cs +++ b/SteamPrefill/CliCommands/Converters.cs @@ -80,4 +80,49 @@ public override PresetWorkload Convert(string rawValue) return PresetWorkload.FromName(rawValue); } } + + public sealed class SortOrderValidator : BindingValidator + { + public override BindingValidationError Validate(SortOrder value) + { + if (value == null) + { + AnsiConsole.MarkupLine(Red($"A sort order must be specified when using {LightYellow("--sort-order")}")); + AnsiConsole.Markup(Red($"Valid sort orders include : {LightYellow("ascending/descending")}")); + throw new CommandException(".", 1, true); + } + return Ok(); + } + } + + public sealed class SortOrderConverter : BindingConverter + { + public override SortOrder Convert(string rawValue) + { + if (!SortOrder.TryFromValue(rawValue, out var _)) + { + AnsiConsole.MarkupLine(Red($"{White(rawValue)} is not a valid sort order!")); + AnsiConsole.Markup(Red($"Valid sort orders include : {LightYellow("ascending/descending")}")); + throw new CommandException(".", 1, true); + } + return SortOrder.FromValue(rawValue); + } + } + + public sealed class SortColumnValidator : BindingValidator + { + public override BindingValidationError Validate(string value) + { + if (string.IsNullOrEmpty(value) + && (!value.Equals("app", StringComparison.OrdinalIgnoreCase) + || !value.Equals("size", StringComparison.OrdinalIgnoreCase))) + { + AnsiConsole.MarkupLine($"Test: {value}"); + AnsiConsole.MarkupLine(Red($"A sort column must be specified when using {LightYellow("--sort-column")}")); + AnsiConsole.Markup(Red($"Valid sort orders include : {LightYellow("app/size")}")); + throw new CommandException(".", 1, true); + } + return Ok(); + } + } } \ No newline at end of file diff --git a/SteamPrefill/CliCommands/StatusCommand.cs b/SteamPrefill/CliCommands/StatusCommand.cs new file mode 100644 index 00000000..3680f200 --- /dev/null +++ b/SteamPrefill/CliCommands/StatusCommand.cs @@ -0,0 +1,69 @@ + +namespace SteamPrefill.CliCommands +{ + [UsedImplicitly] + [Command("status", Description = "List all currently selected apps and the used disk space.")] + public class StatusCommand : ICommand + { + [CommandOption("no-ansi", + Description = "Application output will be in plain text. " + + "Should only be used if terminal does not support Ansi Escape sequences, or when redirecting output to a file.", + Converter = typeof(NullableBoolConverter))] + public bool? NoAnsiEscapeSequences { get; init; } + + [CommandOption("os", Description = "Specifies which operating system(s) games should be downloaded for. Can be windows/linux/macos", + Converter = typeof(OperatingSystemConverter), Validators = new[] { typeof(OperatingSystemValidator) })] + public IReadOnlyList OperatingSystems { get; init; } = new List { OperatingSystem.Windows }; + + [CommandOption("sort-order", Description = "Specifies in which way the data should be sorted. Can be ascending/descending", + Converter = typeof(SortOrderConverter), Validators = new [] { typeof(SortOrderValidator) })] + public SortOrder SortOrder { get; init; } = SortOrder.Ascending; + + [CommandOption("sort-column", Description = "Specifies by which column the data should be sorted. Can be app/size", + Validators = new [] { typeof(SortColumnValidator) })] + public string SortColumn { get; init; } = "app"; + + private IAnsiConsole _ansiConsole; + + public async ValueTask ExecuteAsync(IConsole console) + { + _ansiConsole = console.CreateAnsiConsole(); + // Property must be set to false in order to disable ansi escape sequences + _ansiConsole.Profile.Capabilities.Ansi = !NoAnsiEscapeSequences ?? true; + + var downloadArgs = new DownloadArguments + { + NoCache = AppConfig.NoLocalCache, + OperatingSystems = OperatingSystems.ToList() + }; + + using var steamManager = new SteamManager(_ansiConsole, downloadArgs); + ValidateUserHasSelectedApps(steamManager); + + try + { + await steamManager.InitializeAsync(); + await steamManager.CurrentlyDownloadedAsync(SortOrder, SortColumn); + } + finally + { + steamManager.Shutdown(); + } + } + + // Validates that the user has selected at least 1 app + private void ValidateUserHasSelectedApps(SteamManager steamManager) + { + var userSelectedApps = steamManager.LoadPreviouslySelectedApps(); + + if (!userSelectedApps.Any()) + { + // User hasn't selected any apps yet + _ansiConsole.MarkupLine(Red("No apps have been selected for benchmark! At least 1 app is required!")); + _ansiConsole.Markup(Red($"See flags {LightYellow("--appid")}, {LightYellow("--all")} and {LightYellow("--use-selected")} to interactively choose which apps to prefill")); + + throw new CommandException(".", 1, true); + } + } + } +} \ No newline at end of file diff --git a/SteamPrefill/Models/Enums/SortOrder.cs b/SteamPrefill/Models/Enums/SortOrder.cs new file mode 100644 index 00000000..23f2b9eb --- /dev/null +++ b/SteamPrefill/Models/Enums/SortOrder.cs @@ -0,0 +1,9 @@ +namespace SteamPrefill.Models.Enums +{ + [Intellenum(typeof(string))] + public sealed partial class SortOrder + { + public static readonly SortOrder Ascending = new("ascending"); + public static readonly SortOrder Descending = new("descending"); + } +} \ No newline at end of file diff --git a/SteamPrefill/SteamManager.cs b/SteamPrefill/SteamManager.cs index 596730c0..bb9d3518 100644 --- a/SteamPrefill/SteamManager.cs +++ b/SteamPrefill/SteamManager.cs @@ -346,5 +346,80 @@ await _ansiConsole.CreateSpectreProgress(TransferSpeedUnit.Bytes, displayTransfe } #endregion + + #region Status + + public async Task CurrentlyDownloadedAsync(SortOrder sortOrder, string sortColumn) + { + await _cdnPool.PopulateAvailableServersAsync(); + + // Pre-Load all selected apps and their manifests + List appIds = LoadPreviouslySelectedApps(); + await _appInfoHandler.RetrieveAppMetadataAsync(appIds); + + ByteSize totalSize = new ByteSize(); + Dictionary index = new Dictionary(); + + var timer = Stopwatch.StartNew(); + _ansiConsole.LogMarkupLine("Loading Manifests"); + foreach (uint appId in appIds) + { + AppInfo appInfo = await _appInfoHandler.GetAppInfoAsync(appId); + + var filteredDepots = await _depotHandler.FilterDepotsToDownloadAsync(_downloadArgs, appInfo.Depots); + await _depotHandler.BuildLinkedDepotInfoAsync(filteredDepots); + + var allChunksForApp = await _depotHandler.BuildChunkDownloadQueueAsync(filteredDepots); + var size = ByteSize.FromBytes(allChunksForApp.Sum(e => e.CompressedLength)); + totalSize += size; + + index.Add(appInfo.Name, size); + } + _ansiConsole.LogMarkupLine("Manifests Loaded", timer); + + var table = new Table { Border = TableBorder.MinimalHeavyHead }; + table.AddColumns(new TableColumn("App"), new TableColumn("Size")); + + foreach (KeyValuePair data in SortData(index, sortOrder, sortColumn)) + { + string appName = data.Key; + ByteSize size = data.Value; + table.AddRow(appName, size.ToDecimalString()); + } + + table.AddEmptyRow(); + table.AddRow("Total Size", totalSize.ToDecimalString()); + + _ansiConsole.Write(table); + } + + private IOrderedEnumerable> SortData( + Dictionary index, + SortOrder sortOrder, + string sortColumn) + { + if (sortOrder == SortOrder.Ascending) + { + if (sortColumn.Equals("app", StringComparison.OrdinalIgnoreCase)) + { + return index.OrderBy(o => o.Key); + } else if (sortColumn.Equals("size", StringComparison.OrdinalIgnoreCase)) + { + return index.OrderBy(o => o.Value); + } + } else if (sortOrder == SortOrder.Descending) + { + if (sortColumn.Equals("app", StringComparison.OrdinalIgnoreCase)) + { + return index.OrderByDescending(o => o.Key); + } else if (sortColumn.Equals("size", StringComparison.OrdinalIgnoreCase)) + { + return index.OrderByDescending(o => o.Value); + } + } + return index.OrderBy(o => o.Key); + } + + #endregion } } \ No newline at end of file diff --git a/docs/mkdocs/detailed-command-usage/Status.md b/docs/mkdocs/detailed-command-usage/Status.md new file mode 100644 index 00000000..231111b5 --- /dev/null +++ b/docs/mkdocs/detailed-command-usage/Status.md @@ -0,0 +1,30 @@ +# status + +## Overview + +Lists all selected apps and their disk usage. + +----- + +## Example usage + +Checking the `status` is as simple as running the following from the terminal: +```powershell +./{{prefillName}} status +``` + +### Customized the sorting + +An advanced usage with customized sorting can be used as the following from the terminal: +```powershell +./{{prefillName}} status --sort-order descending --sort-column size +``` + +## Options + +| Option | | Values | Default | | +| --------------- | --- | --------------------- | ------------- | --- | +| --os | | windows, linux, macos | **windows** | Specifies which operating system(s) chunks should be filtered for. | +| --no-ansi | | | | Application output will be in plain text, rather than using the visually appealing colors and progress bars. Should only be used if terminal does not support Ansi Escape sequences, or when redirecting output to a file. | +| --sort-order | | ascending, descending | **ascending** | Specifies which sorting should be used for the data. | +| --sort-column | | app, size | **app** | Specifies which column should be used for the sorting. | \ No newline at end of file