diff --git a/DepotDownloader/Ansi.cs b/DepotDownloader/Ansi.cs
new file mode 100644
index 00000000..7041debc
--- /dev/null
+++ b/DepotDownloader/Ansi.cs
@@ -0,0 +1,51 @@
+using System;
+using Spectre.Console;
+
+namespace DepotDownloader;
+
+static class Ansi
+{
+ // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
+ // https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
+ public enum ProgressState
+ {
+ Hidden = 0,
+ Default = 1,
+ Error = 2,
+ Indeterminate = 3,
+ Warning = 4,
+ }
+
+ const char ESC = (char)0x1B;
+ const char BEL = (char)0x07;
+
+ private static bool useProgress;
+
+ public static void Init()
+ {
+ if (Console.IsInputRedirected || Console.IsOutputRedirected)
+ {
+ return;
+ }
+
+ var (supportsAnsi, legacyConsole) = AnsiDetector.Detect(stdError: false, upgrade: true);
+
+ useProgress = supportsAnsi && !legacyConsole;
+ }
+
+ public static void Progress(ulong downloaded, ulong total)
+ {
+ var progress = (byte)MathF.Round(downloaded / (float)total * 100.0f);
+ Progress(ProgressState.Default, progress);
+ }
+
+ public static void Progress(ProgressState state, byte progress = 0)
+ {
+ if (!useProgress)
+ {
+ return;
+ }
+
+ Console.Write($"{ESC}]9;4;{(byte)state};{progress}{BEL}");
+ }
+}
diff --git a/DepotDownloader/AnsiDetector.cs b/DepotDownloader/AnsiDetector.cs
new file mode 100644
index 00000000..2110d9cc
--- /dev/null
+++ b/DepotDownloader/AnsiDetector.cs
@@ -0,0 +1,134 @@
+// Copied from https://github.com/spectreconsole/spectre.console/blob/d79e6adc5f8e637fb35c88f987023ffda6707243/src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
+// MIT License - Copyright(c) 2020 Patrik Svensson, Phil Scott, Nils Andresen
+// which is partially based on https://github.com/keqingrong/supports-ansi/blob/master/index.js
+//
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+
+namespace Spectre.Console;
+
+internal static class AnsiDetector
+{
+ private static readonly Regex[] _regexes =
+ [
+ new("^xterm"), // xterm, PuTTY, Mintty
+ new("^rxvt"), // RXVT
+ new("^eterm"), // Eterm
+ new("^screen"), // GNU screen, tmux
+ new("tmux"), // tmux
+ new("^vt100"), // DEC VT series
+ new("^vt102"), // DEC VT series
+ new("^vt220"), // DEC VT series
+ new("^vt320"), // DEC VT series
+ new("ansi"), // ANSI
+ new("scoansi"), // SCO ANSI
+ new("cygwin"), // Cygwin, MinGW
+ new("linux"), // Linux console
+ new("konsole"), // Konsole
+ new("bvterm"), // Bitvise SSH Client
+ new("^st-256color"), // Suckless Simple Terminal, st
+ new("alacritty"), // Alacritty
+ ];
+
+ public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
+ {
+ // Running on Windows?
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // Running under ConEmu?
+ var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
+ if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
+ {
+ return (true, false);
+ }
+
+ var supportsAnsi = Windows.SupportsAnsi(upgrade, stdError, out var legacyConsole);
+ return (supportsAnsi, legacyConsole);
+ }
+
+ return DetectFromTerm();
+ }
+
+ private static (bool SupportsAnsi, bool LegacyConsole) DetectFromTerm()
+ {
+ // Check if the terminal is of type ANSI/VT100/xterm compatible.
+ var term = Environment.GetEnvironmentVariable("TERM");
+ if (!string.IsNullOrWhiteSpace(term))
+ {
+ if (_regexes.Any(regex => regex.IsMatch(term)))
+ {
+ return (true, false);
+ }
+ }
+
+ return (false, true);
+ }
+
+ private static class Windows
+ {
+ private const int STD_OUTPUT_HANDLE = -11;
+ private const int STD_ERROR_HANDLE = -12;
+ private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
+ private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
+
+ [DllImport("kernel32.dll")]
+ private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
+
+ [DllImport("kernel32.dll")]
+ private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr GetStdHandle(int nStdHandle);
+
+ [DllImport("kernel32.dll")]
+ public static extern uint GetLastError();
+
+ public static bool SupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
+ {
+ isLegacy = false;
+
+ try
+ {
+ var @out = GetStdHandle(stdError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
+ if (!GetConsoleMode(@out, out var mode))
+ {
+ // Could not get console mode, try TERM (set in cygwin, WSL-Shell).
+ var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
+
+ isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
+ return ansiFromTerm;
+ }
+
+ if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
+ {
+ isLegacy = true;
+
+ if (!upgrade)
+ {
+ return false;
+ }
+
+ // Try enable ANSI support.
+ mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
+ if (!SetConsoleMode(@out, mode))
+ {
+ // Enabling failed.
+ return false;
+ }
+
+ isLegacy = false;
+ }
+
+ return true;
+ }
+ catch
+ {
+ // All we know here is that we don't support ANSI.
+ return false;
+ }
+ }
+ }
+}
diff --git a/DepotDownloader/ContentDownloader.cs b/DepotDownloader/ContentDownloader.cs
index e68a3a02..6e528b91 100644
--- a/DepotDownloader/ContentDownloader.cs
+++ b/DepotDownloader/ContentDownloader.cs
@@ -610,6 +610,7 @@ namespace DepotDownloader
private class GlobalDownloadCounter
{
+ public ulong completeDownloadSize;
public ulong totalBytesCompressed;
public ulong totalBytesUncompressed;
}
@@ -624,6 +625,8 @@ namespace DepotDownloader
private static async Task DownloadSteam3Async(List depots)
{
+ Ansi.Progress(Ansi.ProgressState.Indeterminate);
+
var cts = new CancellationTokenSource();
cdnPool.ExhaustedToken = cts;
@@ -634,7 +637,7 @@ namespace DepotDownloader
// First, fetch all the manifests for each depot (including previous manifests) and perform the initial setup
foreach (var depot in depots)
{
- var depotFileData = await ProcessDepotManifestAndFiles(cts, depot);
+ var depotFileData = await ProcessDepotManifestAndFiles(cts, depot, downloadCounter);
if (depotFileData != null)
{
@@ -665,11 +668,13 @@ namespace DepotDownloader
await DownloadSteam3AsyncDepotFiles(cts, downloadCounter, depotFileData, allFileNamesAllDepots);
}
+ Ansi.Progress(Ansi.ProgressState.Hidden);
+
Console.WriteLine("Total downloaded: {0} bytes ({1} bytes uncompressed) from {2} depots",
downloadCounter.totalBytesCompressed, downloadCounter.totalBytesUncompressed, depots.Count);
}
- private static async Task ProcessDepotManifestAndFiles(CancellationTokenSource cts, DepotDownloadInfo depot)
+ private static async Task ProcessDepotManifestAndFiles(CancellationTokenSource cts, DepotDownloadInfo depot, GlobalDownloadCounter downloadCounter)
{
var depotCounter = new DepotDownloadCounter();
@@ -751,7 +756,7 @@ namespace DepotDownloader
}
else
{
- Console.Write("Downloading depot manifest...");
+ Console.Write("Downloading depot manifest... ");
DepotManifest depotManifest = null;
ulong manifestRequestCode = 0;
@@ -814,7 +819,7 @@ namespace DepotDownloader
if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
{
- Console.WriteLine("Encountered 401 for depot manifest {0} {1}. Aborting.", depot.DepotId, depot.ManifestId);
+ Console.WriteLine("Encountered {2} for depot manifest {0} {1}. Aborting.", depot.DepotId, depot.ManifestId, (int)e.StatusCode);
break;
}
@@ -889,6 +894,7 @@ namespace DepotDownloader
Directory.CreateDirectory(Path.GetDirectoryName(fileFinalPath));
Directory.CreateDirectory(Path.GetDirectoryName(fileStagingPath));
+ downloadCounter.completeDownloadSize += file.TotalSize;
depotCounter.completeDownloadSize += file.TotalSize;
}
});
@@ -918,7 +924,7 @@ namespace DepotDownloader
await Util.InvokeAsync(
files.Select(file => new Func(async () =>
- await Task.Run(() => DownloadSteam3AsyncDepotFile(cts, depotFilesData, file, networkChunkQueue)))),
+ await Task.Run(() => DownloadSteam3AsyncDepotFile(cts, downloadCounter, depotFilesData, file, networkChunkQueue)))),
maxDegreeOfParallelism: Config.MaxDownloads
);
@@ -966,6 +972,7 @@ namespace DepotDownloader
private static void DownloadSteam3AsyncDepotFile(
CancellationTokenSource cts,
+ GlobalDownloadCounter downloadCounter,
DepotFilesData depotFilesData,
ProtoManifest.FileData file,
ConcurrentQueue<(FileStreamData, ProtoManifest.FileData, ProtoManifest.ChunkData)> networkChunkQueue)
@@ -1128,6 +1135,11 @@ namespace DepotDownloader
Console.WriteLine("{0,6:#00.00}% {1}", (depotDownloadCounter.sizeDownloaded / (float)depotDownloadCounter.completeDownloadSize) * 100.0f, fileFinalPath);
}
+ lock (downloadCounter)
+ {
+ downloadCounter.completeDownloadSize -= file.TotalSize;
+ }
+
return;
}
@@ -1136,6 +1148,11 @@ namespace DepotDownloader
{
depotDownloadCounter.sizeDownloaded += sizeOnDisk;
}
+
+ lock (downloadCounter)
+ {
+ downloadCounter.completeDownloadSize -= sizeOnDisk;
+ }
}
var fileIsExecutable = file.Flags.HasFlag(EDepotFileFlag.Executable);
@@ -1217,7 +1234,7 @@ namespace DepotDownloader
if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
{
- Console.WriteLine("Encountered 401 for chunk {0}. Aborting.", chunkID);
+ Console.WriteLine("Encountered {1} for chunk {0}. Aborting.", chunkID, (int)e.StatusCode);
break;
}
@@ -1281,6 +1298,8 @@ namespace DepotDownloader
{
downloadCounter.totalBytesCompressed += chunk.CompressedLength;
downloadCounter.totalBytesUncompressed += chunk.UncompressedLength;
+
+ Ansi.Progress(downloadCounter.totalBytesUncompressed, downloadCounter.completeDownloadSize);
}
if (remainingChunks == 0)
diff --git a/DepotDownloader/Program.cs b/DepotDownloader/Program.cs
index 14f7f88d..ae44b765 100644
--- a/DepotDownloader/Program.cs
+++ b/DepotDownloader/Program.cs
@@ -26,6 +26,8 @@ namespace DepotDownloader
return 1;
}
+ Ansi.Init();
+
DebugLog.Enabled = false;
AccountSettingsStore.LoadFromFile("account.config");