[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    labels:\n      - enhancement\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n  - package-ecosystem: nuget\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    labels:\n      - enhancement\n    groups:\n      nuget:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  exclude:\n    authors:\n      - dependabot\n      - dependabot[bot]\n\n  categories:\n    - title: Enhancements\n      labels:\n        - enhancement\n\n    - title: Bugs\n      labels:\n        - bug\n\n    - title: Other\n      labels:\n        - \"*\"\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: main\n\non:\n  workflow_dispatch:\n    inputs:\n      package-version:\n        type: string\n        description: Package version\n        required: false\n      deploy:\n        type: boolean\n        description: Deploy package\n        required: false\n        default: false\n  push:\n    branches:\n      - prime\n    tags:\n      - \"*\"\n  pull_request:\n    branches:\n      - prime\n\njobs:\n  main:\n    uses: Tyrrrz/.github/.github/workflows/nuget.yml@prime\n    permissions:\n      actions: write\n      contents: write\n    with:\n      deploy: ${{ inputs.deploy || github.ref_type == 'tag' }}\n      package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }}\n    secrets:\n      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n      NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}\n      DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# User-specific files\n.vs/\n.idea/\n*.suo\n*.user\n\n# Build results\nbin/\nobj/\n\n# Test results\nTestResults/"
  },
  {
    "path": "Directory.Build.props",
    "content": "<Project>\n  <PropertyGroup>\n    <Version>0.0.0-dev</Version>\n    <Company>Tyrrrz</Company>\n    <Copyright>Copyright (C) Oleksii Holub</Copyright>\n    <Nullable>enable</Nullable>\n    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>\n    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    <CheckEolTargetFramework>false</CheckEolTargetFramework>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <!-- Disable nullability warnings on older frameworks because there is no nullability info for BCL -->\n  <PropertyGroup Condition=\"!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netstandard2.1'))\">\n    <Nullable>annotations</Nullable>\n  </PropertyGroup>\n\n  <!-- Disable trim and AOT analyzers on older frameworks as they're not compatible with newer language features -->\n  <PropertyGroup Condition=\"!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))\">\n    <EnableTrimAnalyzer>false</EnableTrimAnalyzer>\n    <EnableAotAnalyzer>false</EnableAotAnalyzer>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <Authors>$(Company)</Authors>\n    <Description>Progress reporting toolbox</Description>\n    <PackageTags>progress aggregation muxing transform filter net standard core</PackageTags>\n    <PackageProjectUrl>https://github.com/Tyrrrz/Gress</PackageProjectUrl>\n    <PackageReleaseNotes>https://github.com/Tyrrrz/Gress/releases</PackageReleaseNotes>\n    <PackageLicenseExpression>MIT</PackageLicenseExpression>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageVersion Include=\"Avalonia\" Version=\"12.0.2\" />\n    <PackageVersion Include=\"Avalonia.Desktop\" Version=\"12.0.2\" />\n    <PackageVersion Include=\"Avalonia.Diagnostics\" Version=\"11.3.14\" />\n    <PackageVersion Include=\"Avalonia.Fonts.Inter\" Version=\"12.0.2\" />\n    <PackageVersion Include=\"Avalonia.Themes.Fluent\" Version=\"12.0.2\" />\n    <PackageVersion Include=\"CommunityToolkit.Mvvm\" Version=\"8.4.2\" />\n    <PackageVersion Include=\"coverlet.collector\" Version=\"10.0.0\" />\n    <PackageVersion Include=\"CSharpier.MsBuild\" Version=\"1.2.5\" />\n    <PackageVersion Include=\"FluentAssertions\" Version=\"8.9.0\" />\n    <PackageVersion Include=\"GitHubActionsTestLogger\" Version=\"3.0.3\" />\n    <PackageVersion Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.5.1\" />\n    <PackageVersion Include=\"PolyShim\" Version=\"2.11.0\" />\n    <PackageVersion Include=\"PowerKit\" Version=\"1.1.1\" />\n    <PackageVersion Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageVersion Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Gress/Completable/AutoResetProgressMuxer.cs",
    "content": "using System;\nusing System.Threading;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Aggregates multiple progress updates into a single handler.\n/// Resets itself once all inputs report completion.\n/// </summary>\npublic partial class AutoResetProgressMuxer(ProgressMuxer muxer)\n{\n    private readonly Lock _lock = new();\n    private readonly ProgressMuxer _muxer = muxer;\n\n    private int _pendingInputs;\n\n    /// <summary>\n    /// Creates a progress handler that reports progress to this muxer.\n    /// Specified weight determines the priority of this handler relative to other\n    /// handlers connected to this muxer. Progress reported on a handler with higher\n    /// weight influences the final progress to a greater degree.\n    /// </summary>\n    /// <remarks>\n    /// Returned progress handler can report completion. Once all linked handlers\n    /// report completion, the progress is reset, and existing handlers are disconnected.\n    /// </remarks>\n    public ICompletableProgress<Percentage> CreateInput(double weight = 1.0)\n    {\n        using (_lock.EnterScope())\n        {\n            var item = new Item(this, _muxer.CreateInput(weight));\n\n            // Make sure the item was created successfully before incrementing\n            _pendingInputs++;\n\n            return item;\n        }\n    }\n}\n\npublic partial class AutoResetProgressMuxer\n{\n    private class Item : ICompletableProgress<Percentage>\n    {\n        private readonly AutoResetProgressMuxer _parent;\n        private readonly IProgress<Percentage> _target;\n\n        public Item(AutoResetProgressMuxer parent, IProgress<Percentage> target)\n        {\n            _parent = parent;\n            _target = target;\n        }\n\n        public void Report(Percentage value)\n        {\n            using (_parent._lock.EnterScope())\n            {\n                _target.Report(value);\n            }\n        }\n\n        public void ReportCompletion()\n        {\n            using (_parent._lock.EnterScope())\n            {\n                if (--_parent._pendingInputs <= 0)\n                    _parent._muxer.Reset();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Gress/Completable/CompletableProgressExtensions.cs",
    "content": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Extensions for <see cref=\"CompletableProgressExtensions\" />.\n/// </summary>\npublic static class CompletableProgressExtensions\n{\n    /// <inheritdoc cref=\"CompletableProgressExtensions\" />\n    extension<T>(IProgress<T> progress)\n    {\n        /// <summary>\n        /// Converts a regular progress handler into a progress handler with explicit completion.\n        /// </summary>\n        public ICompletableProgress<T> ToCompletable(Action reportCompletion) =>\n            new DelegateCompletableProgress<T>(progress.Report, reportCompletion);\n    }\n\n    /// <inheritdoc cref=\"CompletableProgressExtensions\" />\n    extension<T>(ICompletableProgress<T> progress)\n    {\n        /// <summary>\n        /// Wraps the specified completable progress handler in a disposable container.\n        /// Disposing the container reports completion on the handler.\n        /// </summary>\n        public DisposableCompletableProgress<T> ToDisposable() => new(progress);\n    }\n\n    /// <inheritdoc cref=\"CompletableProgressExtensions\" />\n    extension(ProgressMuxer muxer)\n    {\n        /// <summary>\n        /// Wraps the muxer in a special adapter that disconnects all inputs from the muxer\n        /// after they all report completion.\n        /// </summary>\n        public AutoResetProgressMuxer WithAutoReset() => new(muxer);\n    }\n}\n"
  },
  {
    "path": "Gress/Completable/DelegateCompletableProgress.cs",
    "content": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Simple implementation of <see cref=\"ICompletableProgress{T}\" /> that uses the provided\n/// delegates to report progress updates and signal completion.\n/// </summary>\npublic class DelegateCompletableProgress<T>(Action<T> report, Action reportCompletion)\n    : ICompletableProgress<T>\n{\n    /// <inheritdoc />\n    public void Report(T value) => report(value);\n\n    /// <inheritdoc />\n    public void ReportCompletion() => reportCompletion();\n}\n"
  },
  {
    "path": "Gress/Completable/DisposableCompletableProgress.cs",
    "content": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Convenience wrapper for <see cref=\"ICompletableProgress{T}\" /> that reports completion\n/// on disposal.\n/// </summary>\npublic class DisposableCompletableProgress<T>(ICompletableProgress<T> target)\n    : ICompletableProgress<T>,\n        IDisposable\n{\n    /// <inheritdoc />\n    public void Report(T value) => target.Report(value);\n\n    /// <inheritdoc />\n    public void ReportCompletion() => target.ReportCompletion();\n\n    /// <inheritdoc cref=\"ReportCompletion\" />\n    public void Dispose() => ReportCompletion();\n}\n"
  },
  {
    "path": "Gress/Completable/ICompletableProgress.cs",
    "content": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Progress handler with explicit completion feedback.\n/// </summary>\npublic interface ICompletableProgress<in T> : IProgress<T>\n{\n    /// <summary>\n    /// Reports overall completion of the operation.\n    /// </summary>\n    void ReportCompletion();\n}\n"
  },
  {
    "path": "Gress/DelegateProgress.cs",
    "content": "using System;\n\nnamespace Gress;\n\n/// <summary>\n/// Simple implementation of <see cref=\"IProgress{T}\" /> that uses the provided\n/// delegate to report progress updates.\n/// </summary>\n/// <remarks>\n/// Unlike <see cref=\"Progress{T}\" />, this implementation does not post updates\n/// through a synchronization context, but rather calls the delegate directly.\n/// </remarks>\npublic class DelegateProgress<T>(Action<T> report) : IProgress<T>\n{\n    /// <inheritdoc />\n    public void Report(T value) => report(value);\n}\n"
  },
  {
    "path": "Gress/Gress.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>netstandard2.0;net6.0;net7.0;net10.0</TargetFrameworks>\n    <IsPackable>true</IsPackable>\n    <IsTrimmable\n      Condition=\"$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))\"\n      >true</IsTrimmable\n    >\n    <IsAotCompatible\n      Condition=\"$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))\"\n      >true</IsAotCompatible\n    >\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <PackageIcon>favicon.png</PackageIcon>\n    <GenerateDocumentationFile>true</GenerateDocumentationFile>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"../favicon.png\" Pack=\"true\" PackagePath=\"\" Visible=\"false\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"PolyShim\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"PowerKit\" PrivateAssets=\"all\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Gress/Integrations/HttpClientExtensions.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Gress.Integrations;\nusing PowerKit.Extensions;\n\nnamespace Gress.Integrations;\n\n/// <summary>\n/// Provides progress-aware extensions for <see cref=\"HttpClient\" />.\n/// </summary>\npublic static class HttpClientExtensions\n{\n    /// <inheritdoc cref=\"HttpClientExtensions\" />\n    extension(HttpClient client)\n    {\n        /// <summary>\n        /// Sends a GET request and returns the response body as a byte array.\n        /// </summary>\n        public async Task<byte[]> GetByteArrayAsync(\n            Uri requestUri,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        )\n        {\n            using var response = await client\n                .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)\n                .ConfigureAwait(false);\n\n            response.EnsureSuccessStatusCode();\n\n            return await response\n                .Content.ReadAsByteArrayAsync(progress, cancellationToken)\n                .ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Sends a GET request and returns the response body as a byte array.\n        /// </summary>\n        public async Task<byte[]> GetByteArrayAsync(\n            string requestUri,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await client\n                .GetByteArrayAsync(\n                    new Uri(requestUri, UriKind.RelativeOrAbsolute),\n                    progress,\n                    cancellationToken\n                )\n                .ConfigureAwait(false);\n\n        /// <summary>\n        /// Sends a GET request and returns the response body as text.\n        /// </summary>\n        public async Task<string> GetStringAsync(\n            Uri requestUri,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        )\n        {\n            using var response = await client\n                .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)\n                .ConfigureAwait(false);\n\n            response.EnsureSuccessStatusCode();\n\n            return await response\n                .Content.ReadAsStringAsync(progress, cancellationToken)\n                .ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Sends a GET request and returns the response body as text.\n        /// </summary>\n        public async Task<string> GetStringAsync(\n            string requestUri,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await client\n                .GetStringAsync(\n                    new Uri(requestUri, UriKind.RelativeOrAbsolute),\n                    progress,\n                    cancellationToken\n                )\n                .ConfigureAwait(false);\n\n        /// <summary>\n        /// Sends a GET request and saves the response body to a file.\n        /// </summary>\n        public async Task DownloadAsync(\n            Uri requestUri,\n            string filePath,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await client\n                .DownloadAsync(requestUri, filePath, progress?.ToDoubleBased(), cancellationToken)\n                .ConfigureAwait(false);\n\n        /// <summary>\n        /// Sends a GET request and saves the response body to a file.\n        /// </summary>\n        public async Task DownloadAsync(\n            string requestUri,\n            string filePath,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await client\n                .DownloadAsync(\n                    new Uri(requestUri, UriKind.RelativeOrAbsolute),\n                    filePath,\n                    progress,\n                    cancellationToken\n                )\n                .ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "Gress/Integrations/HttpContentExtensions.cs",
    "content": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing PowerKit.Extensions;\n\nnamespace Gress.Integrations;\n\n/// <summary>\n/// Provides progress-aware extensions for <see cref=\"HttpContent\" />.\n/// </summary>\npublic static class HttpContentExtensions\n{\n    /// <inheritdoc cref=\"HttpContentExtensions\" />\n    extension(HttpContent content)\n    {\n        /// <summary>\n        /// Serializes the HTTP content and writes it to the specified stream.\n        /// </summary>\n        public async Task CopyToAsync(\n            Stream destination,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await content\n                .CopyToStreamAsync(destination, progress?.ToDoubleBased(), cancellationToken)\n                .ConfigureAwait(false);\n\n        private async Task<MemoryStream> ReadAsMemoryStreamAsync(\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        )\n        {\n            var destination = new MemoryStream();\n\n            await content\n                .CopyToAsync(destination, progress, cancellationToken)\n                .ConfigureAwait(false);\n\n            destination.Position = 0;\n\n            return destination;\n        }\n\n        /// <summary>\n        /// Reads the HTTP content and returns it as a byte array.\n        /// </summary>\n        public async Task<byte[]> ReadAsByteArrayAsync(\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        )\n        {\n            using var buffer = await content\n                .ReadAsMemoryStreamAsync(progress, cancellationToken)\n                .ConfigureAwait(false);\n\n            return buffer.ToArray();\n        }\n\n        /// <summary>\n        /// Reads the HTTP content and returns it as a string.\n        /// </summary>\n        public async Task<string> ReadAsStringAsync(\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        )\n        {\n            var encoding =\n                content\n                    .Headers.ContentType?.CharSet?.Pipe(c =>\n                        Encoding\n                            .GetEncodings()\n                            .FirstOrDefault(e =>\n                                string.Equals(e.Name, c, StringComparison.OrdinalIgnoreCase)\n                            )\n                    )\n                    ?.GetEncoding()\n                ?? Encoding.UTF8;\n\n            using var buffer = await content\n                .ReadAsMemoryStreamAsync(progress, cancellationToken)\n                .ConfigureAwait(false);\n\n            using var reader = new StreamReader(buffer, encoding, true);\n\n            return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);\n        }\n    }\n}\n"
  },
  {
    "path": "Gress/Integrations/StreamExtensions.cs",
    "content": "using System;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Gress.Integrations;\nusing PowerKit.Extensions;\n\nnamespace Gress.Integrations;\n\n/// <summary>\n/// Provides progress-aware extensions for <see cref=\"Stream\" />.\n/// </summary>\npublic static class StreamExtensions\n{\n    /// <inheritdoc cref=\"StreamExtensions\" />\n    extension(Stream source)\n    {\n        /// <summary>\n        /// Asynchronously copies bytes from the source stream to the destination stream.\n        /// </summary>\n        public async Task CopyToAsync(\n            Stream destination,\n            long sourceLength,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await source\n                .CopyToAsync(\n                    destination,\n                    sourceLength,\n                    progress?.ToDoubleBased(),\n                    cancellationToken\n                )\n                .ConfigureAwait(false);\n\n        /// <summary>\n        /// Asynchronously copies bytes from the source stream to the destination stream.\n        /// </summary>\n        public async Task CopyToAsync(\n            Stream destination,\n            IProgress<Percentage>? progress,\n            CancellationToken cancellationToken = default\n        ) =>\n            await source\n                .CopyToAsync(\n                    destination,\n                    source.CanSeek ? source.Length : -1,\n                    progress,\n                    cancellationToken\n                )\n                .ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "Gress/Percentage.cs",
    "content": "using System;\n\nnamespace Gress;\n\n/// <summary>\n/// Unit of progress.\n/// </summary>\npublic readonly partial struct Percentage(double value)\n{\n    /// <summary>\n    /// Percentage value.\n    /// </summary>\n    public double Value { get; } = value;\n\n    /// <summary>\n    /// Percentage value in decimal form (e.g. 0.75 for 75%).\n    /// </summary>\n    public double Fraction => Value / 100.0;\n\n    /// <summary>\n    /// Formats the value of this instance to a string.\n    /// </summary>\n    public string ToString(IFormatProvider? formatProvider) =>\n        Fraction.ToString(\"P1\", formatProvider);\n\n    /// <inheritdoc cref=\"ToString(IFormatProvider?)\" />\n    public override string ToString() => ToString(null);\n}\n\npublic partial struct Percentage\n{\n    /// <summary>\n    /// Creates a percentage from its value.\n    /// </summary>\n    public static Percentage FromValue(double value) => new(value);\n\n    /// <summary>\n    /// Creates a percentage from its value in decimal form (e.g. from 0.75 to 75%).\n    /// </summary>\n    public static Percentage FromFraction(double fraction) => FromValue(fraction * 100.0);\n}\n\npublic partial struct Percentage : IEquatable<Percentage>, IComparable<Percentage>\n{\n    /// <inheritdoc />\n    public int CompareTo(Percentage other) => Value.CompareTo(other.Value);\n\n    /// <inheritdoc />\n    public bool Equals(Percentage other) => Value.CompareTo(other.Value) == 0;\n\n    /// <inheritdoc />\n    public override bool Equals(object? obj) => obj is Percentage other && Equals(other);\n\n    /// <inheritdoc />\n    public override int GetHashCode() => Value.GetHashCode();\n\n    /// <summary>\n    /// Greater than operator.\n    /// </summary>\n    public static bool operator >(Percentage left, Percentage right) => left.CompareTo(right) > 0;\n\n    /// <summary>\n    /// Lesser than operator.\n    /// </summary>\n    public static bool operator <(Percentage left, Percentage right) => left.CompareTo(right) < 0;\n\n    /// <summary>\n    /// Equality operator.\n    /// </summary>\n    public static bool operator ==(Percentage left, Percentage right) => left.Equals(right);\n\n    /// <summary>\n    /// Inequality operator.\n    /// </summary>\n    public static bool operator !=(Percentage left, Percentage right) => !(left == right);\n}\n\npublic partial struct Percentage : IFormattable\n{\n    string IFormattable.ToString(string? format, IFormatProvider? formatProvider) =>\n        ToString(formatProvider);\n}\n"
  },
  {
    "path": "Gress/ProgressCollector.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\n\nnamespace Gress;\n\n/// <summary>\n/// Terminal progress handler that stores all reported progress updates in a collection.\n/// </summary>\npublic class ProgressCollector<T> : IProgress<T>\n{\n    private readonly Lock _lock = new();\n    private readonly List<T> _reports = [];\n\n    /// <summary>\n    /// Clears the reported progress updates.\n    /// </summary>\n    public void Reset()\n    {\n        using (_lock.EnterScope())\n            _reports.Clear();\n    }\n\n    /// <summary>\n    /// Returns the progress updates reported so far.\n    /// </summary>\n    public IReadOnlyList<T> GetValues()\n    {\n        using (_lock.EnterScope())\n            return _reports.ToArray();\n    }\n\n    /// <inheritdoc />\n    public void Report(T value)\n    {\n        using (_lock.EnterScope())\n            _reports.Add(value);\n    }\n}\n"
  },
  {
    "path": "Gress/ProgressContainer.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Runtime.CompilerServices;\n\nnamespace Gress;\n\n/// <summary>\n/// Terminal progress handler that records the last reported progress update.\n/// </summary>\npublic partial class ProgressContainer<T>(T initial) : IProgress<T>\n{\n    /// <summary>\n    /// Initializes an instance of <see cref=\"ProgressContainer{T}\" />.\n    /// </summary>\n    /// <remarks>\n    /// If <typeparamref name=\"T\" /> is a reference type, the initial value of the\n    /// <see cref=\"Current\"/> property will be <c>null</c>.\n    /// Consider using the other constructor overload to provide a non-null initial value.\n    /// </remarks>\n    public ProgressContainer()\n        : this(default!) { }\n\n    /// <summary>\n    /// Last reported progress update.\n    /// </summary>\n    /// <remarks>\n    /// If this property is accessed before any progress has been reported,\n    /// it will evaluate to the initial value provided by the constructor.\n    /// </remarks>\n    public T Current\n    {\n        get => initial;\n        private set\n        {\n            if (EqualityComparer<T>.Default.Equals(value, initial))\n                return;\n\n            initial = value;\n            OnPropertyChanged();\n        }\n    }\n\n    /// <inheritdoc />\n    public void Report(T value) => Current = value;\n}\n\npublic partial class ProgressContainer<T> : INotifyPropertyChanged\n{\n    private event PropertyChangedEventHandler? PropertyChanged;\n\n    event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged\n    {\n        add => PropertyChanged += value;\n        remove => PropertyChanged -= value;\n    }\n\n    // Instrumented automatically by Fody\n    // ReSharper disable once UnusedMember.Local\n    private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n}\n"
  },
  {
    "path": "Gress/ProgressExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing PowerKit;\n\nnamespace Gress;\n\n/// <summary>\n/// Extensions for <see cref=\"IProgress{T}\" />.\n/// </summary>\npublic static class ProgressExtensions\n{\n    /// <inheritdoc cref=\"ProgressExtensions\" />\n    extension<T>(IProgress<T> progress)\n    {\n        /// <summary>\n        /// Projects progress updates into a different shape.\n        /// </summary>\n        public IProgress<TTransformed> WithTransform<TTransformed>(Func<TTransformed, T> map) =>\n            new DelegateProgress<TTransformed>(p => progress.Report(map(p)));\n\n        /// <summary>\n        /// Projects progress updates into a different shape.\n        /// </summary>\n        public IProgress<T> WithTransform(Func<T, T> map) => progress.WithTransform<T, T>(map);\n\n        /// <summary>\n        /// Filters progress updates based on the specified predicate.\n        /// </summary>\n        public IProgress<T> WithFilter(Func<T, bool> shouldReport) =>\n            new DelegateProgress<T>(p =>\n            {\n                if (shouldReport(p))\n                    progress.Report(p);\n            });\n\n        /// <summary>\n        /// Filters out consecutive progress updates with the same value of the specified key.\n        /// </summary>\n        public IProgress<T> WithDeduplication<TKey>(\n            Func<T, TKey> getKey,\n            IEqualityComparer<TKey>? comparer = null\n        )\n        {\n            var syncRoot = new Lock();\n            var actualComparer = comparer ?? EqualityComparer<TKey>.Default;\n            var lastValueCell = new Cell<TKey>();\n\n            return new DelegateProgress<T>(p =>\n            {\n                using (syncRoot.EnterScope())\n                {\n                    var value = getKey(p);\n\n                    if (\n                        lastValueCell.TryOpen(out var lastValue)\n                        && actualComparer.Equals(lastValue, value)\n                    )\n                    {\n                        return;\n                    }\n\n                    progress.Report(p);\n                    lastValueCell.Store(value);\n                }\n            });\n        }\n\n        /// <summary>\n        /// Filters out consecutive progress updates with the same value.\n        /// </summary>\n        public IProgress<T> WithDeduplication(IEqualityComparer<T>? comparer = null) =>\n            progress.WithDeduplication(p => p, comparer);\n\n        /// <summary>\n        /// Filters out progress updates that arrive out of order.\n        /// </summary>\n        public IProgress<T> WithOrdering(IComparer<T>? comparer = null)\n        {\n            var syncRoot = new Lock();\n            var actualComparer = comparer ?? Comparer<T>.Default;\n            var lastValueCell = new Cell<T>();\n\n            return new DelegateProgress<T>(p =>\n            {\n                using (syncRoot.EnterScope())\n                {\n                    if (\n                        lastValueCell.TryOpen(out var lastValue)\n                        && actualComparer.Compare(lastValue, p) > 0\n                    )\n                    {\n                        return;\n                    }\n\n                    progress.Report(p);\n                    lastValueCell.Store(p);\n                }\n            });\n        }\n\n        /// <summary>\n        /// Merges two progress handlers into one.\n        /// </summary>\n        public IProgress<T> Merge(IProgress<T> otherProgress) =>\n            new DelegateProgress<T>(p =>\n            {\n                progress.Report(p);\n                otherProgress.Report(p);\n            });\n\n        /// <summary>\n        /// Converts the specified progress handler into a <see cref=\"Percentage\" />-based progress handler.\n        /// </summary>\n        public IProgress<Percentage> ToPercentageBased(Func<Percentage, T> map) =>\n            progress.WithTransform(map);\n    }\n\n    /// <inheritdoc cref=\"ProgressExtensions\" />\n    extension(IProgress<double> progress)\n    {\n        /// <summary>\n        /// Converts the specified <see cref=\"double\" />-based progress handler into a\n        /// <see cref=\"Percentage\" />-based progress handler.\n        /// Parameter <paramref name=\"asFraction\" /> specifies whether the percentage-based\n        /// progress is reported in its decimal form (true) or in percentage form (false).\n        /// </summary>\n        public IProgress<Percentage> ToPercentageBased(bool asFraction = true) =>\n            asFraction\n                ? progress.ToPercentageBased(p => p.Fraction)\n                : progress.ToPercentageBased(p => p.Value);\n    }\n\n    /// <inheritdoc cref=\"ProgressExtensions\" />\n    extension(IProgress<int> progress)\n    {\n        /// <summary>\n        /// Converts the specified <see cref=\"int\" />-based progress handler into a\n        /// <see cref=\"Percentage\" />-based progress handler.\n        /// </summary>\n        public IProgress<Percentage> ToPercentageBased() =>\n            progress.ToPercentageBased(p => (int)p.Value);\n    }\n\n    /// <inheritdoc cref=\"ProgressExtensions\" />\n    extension(IProgress<Percentage> progress)\n    {\n        /// <summary>\n        /// Converts the specified <see cref=\"Percentage\" />-based progress handler into a\n        /// <see cref=\"double\" />-based progress handler.\n        /// Parameter <paramref name=\"asFraction\" /> specifies whether the percentage-based\n        /// progress is reported in its decimal form (true) or in percentage form (false).\n        /// </summary>\n        public IProgress<double> ToDoubleBased(bool asFraction = true) =>\n            asFraction\n                ? progress.WithTransform((double p) => Percentage.FromFraction(p))\n                : progress.WithTransform((double p) => Percentage.FromValue(p));\n\n        /// <summary>\n        /// Converts the specified <see cref=\"Percentage\" />-based progress handler into an\n        /// <see cref=\"int\" />-based progress handler.\n        /// </summary>\n        public IProgress<int> ToInt32Based() =>\n            progress.WithTransform((int p) => Percentage.FromValue(p));\n\n        /// <summary>\n        /// Creates a muxer for the specified progress handler, allowing it to aggregate\n        /// reports from multiple sources.\n        /// </summary>\n        public ProgressMuxer CreateMuxer() => new(progress);\n    }\n\n    /// <inheritdoc cref=\"ProgressExtensions\" />\n    extension<T>(IReadOnlyList<IProgress<T>> progresses)\n    {\n        /// <summary>\n        /// Merges multiple progress handlers into one.\n        /// </summary>\n        public IProgress<T> Merge() =>\n            new DelegateProgress<T>(p =>\n            {\n                foreach (var progress in progresses)\n                    progress.Report(p);\n            });\n    }\n}\n"
  },
  {
    "path": "Gress/ProgressMuxer.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\n\nnamespace Gress;\n\n/// <summary>\n/// Aggregates multiple progress updates into a single handler.\n/// </summary>\npublic partial class ProgressMuxer(IProgress<Percentage> target)\n{\n    private readonly Lock _lock = new();\n    private readonly HashSet<Input> _inputs = [];\n\n    private bool _anyInputReported;\n\n    private void ReportAggregatedProgress()\n    {\n        using (_lock.EnterScope())\n        {\n            var weightedSum = 0.0;\n            var weightedMax = 0.0;\n\n            foreach (var input in _inputs)\n            {\n                weightedSum += input.Weight * input.Progress.Fraction;\n                weightedMax += input.Weight * 1.0;\n            }\n\n            target.Report(\n                Percentage.FromFraction(weightedSum != 0 ? weightedSum / weightedMax : 0)\n            );\n        }\n    }\n\n    /// <summary>\n    /// Creates a progress handler that reports progress to this muxer.\n    /// Specified weight determines the priority of this handler relative to other\n    /// handlers connected to this muxer. Progress reported on a handler with higher\n    /// weight influences the final progress to a greater degree.\n    /// </summary>\n    public IProgress<Percentage> CreateInput(double weight = 1.0)\n    {\n        if (weight <= 0)\n            throw new ArgumentOutOfRangeException(nameof(weight), \"Weight must be positive.\");\n\n        using (_lock.EnterScope())\n        {\n            var input = new Input(this, weight);\n            _inputs.Add(input);\n\n            // Recalculate and report new progress, taking into account the new input.\n            // Don't do it if none of the inputs have reported progress yet, because\n            // we would just be reporting zeroes for each call to this method.\n            if (_anyInputReported)\n                ReportAggregatedProgress();\n\n            return input;\n        }\n    }\n\n    /// <summary>\n    /// Disconnects all progress handlers from this muxer.\n    /// </summary>\n    public void Reset()\n    {\n        using (_lock.EnterScope())\n        {\n            _inputs.Clear();\n            _anyInputReported = false;\n\n            ReportAggregatedProgress();\n        }\n    }\n}\n\npublic partial class ProgressMuxer\n{\n    private class Input : IProgress<Percentage>\n    {\n        private readonly ProgressMuxer _parent;\n\n        public double Weight { get; }\n\n        public Percentage Progress { get; private set; }\n\n        public Input(ProgressMuxer parent, double weight)\n        {\n            _parent = parent;\n            Weight = weight;\n        }\n\n        public void Report(Percentage value)\n        {\n            using (_parent._lock.EnterScope())\n            {\n                Progress = value;\n\n                // This input could have been removed from the muxer.\n                // If that's the case, don't do anything.\n                if (!_parent._inputs.Contains(this))\n                    return;\n\n                _parent.ReportAggregatedProgress();\n                _parent._anyInputReported = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Gress.Demo/App.axaml",
    "content": "<Application\n    x:Class=\"Gress.Demo.App\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <Application.Styles>\n        <FluentTheme />\n    </Application.Styles>\n</Application>"
  },
  {
    "path": "Gress.Demo/App.axaml.cs",
    "content": "using Avalonia;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.Markup.Xaml;\nusing Gress.Demo.Views;\n\nnamespace Gress.Demo;\n\npublic class App : Application\n{\n    public override void Initialize()\n    {\n        base.Initialize();\n\n        AvaloniaXamlLoader.Load(this);\n    }\n\n    public override void OnFrameworkInitializationCompleted()\n    {\n        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)\n            desktopLifetime.MainWindow = new MainWindow();\n\n        base.OnFrameworkInitializationCompleted();\n    }\n}\n"
  },
  {
    "path": "Gress.Demo/Gress.Demo.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <ApplicationIcon>../favicon.ico</ApplicationIcon>\n    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <AvaloniaResource Include=\"../favicon.ico\" Link=\"favicon.ico\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Avalonia\" />\n    <PackageReference Include=\"Avalonia.Desktop\" />\n    <PackageReference Include=\"Avalonia.Themes.Fluent\" />\n    <PackageReference Include=\"Avalonia.Fonts.Inter\" />\n    <PackageReference Include=\"Avalonia.Diagnostics\" Condition=\"'$(Configuration)' == 'Debug'\" />\n    <PackageReference Include=\"CommunityToolkit.Mvvm\" />\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../Gress/Gress.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Gress.Demo/Program.cs",
    "content": "using System;\nusing Avalonia;\n\nnamespace Gress.Demo;\n\npublic static class Program\n{\n    public static AppBuilder BuildAvaloniaApp() =>\n        AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();\n\n    [STAThread]\n    public static int Main(string[] args) =>\n        BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);\n}\n"
  },
  {
    "path": "Gress.Demo/Readme.md",
    "content": "# Gress Avalonia demo\n\nThis demo features a simple Avalonia application that lets you start new operations and track their progress."
  },
  {
    "path": "Gress.Demo/ViewModels/MainViewModel.cs",
    "content": "using System;\nusing System.Collections.ObjectModel;\nusing System.Threading.Tasks;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing CommunityToolkit.Mvvm.Input;\nusing Gress.Completable;\n\nnamespace Gress.Demo.ViewModels;\n\npublic partial class MainViewModel : ObservableObject\n{\n    private readonly AutoResetProgressMuxer _progressMuxer;\n\n    public MainViewModel() => _progressMuxer = Progress.CreateMuxer().WithAutoReset();\n\n    public ProgressContainer<Percentage> Progress { get; } = new();\n\n    public ObservableCollection<OperationViewModel> Operations { get; } = [];\n\n    // Start an operation that simulates some work and reports progress\n    [RelayCommand(AllowConcurrentExecutions = true)]\n    private async Task PerformWorkAsync(double weight)\n    {\n        using var progress = _progressMuxer.CreateInput(weight).ToDisposable();\n\n        var operation = new OperationViewModel(weight);\n        var mergedProgress = progress.Merge(operation.Progress);\n\n        Operations.Add(operation);\n\n        for (var i = 1; i <= 100; i++)\n        {\n            // Simulate work\n            await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(1, 5) / 10.0));\n\n            // Report progress as a value in the 0..100 range\n            mergedProgress.Report(Percentage.FromValue(i));\n        }\n\n        Operations.Remove(operation);\n    }\n}\n"
  },
  {
    "path": "Gress.Demo/ViewModels/OperationViewModel.cs",
    "content": "using CommunityToolkit.Mvvm.ComponentModel;\n\nnamespace Gress.Demo.ViewModels;\n\npublic class OperationViewModel(double weight) : ObservableObject\n{\n    public double Weight { get; } = weight;\n\n    public ProgressContainer<Percentage> Progress { get; } = new();\n}\n"
  },
  {
    "path": "Gress.Demo/Views/MainWindow.axaml",
    "content": "<Window\n    x:Class=\"Gress.Demo.Views.MainWindow\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:viewModels=\"clr-namespace:Gress.Demo.ViewModels\"\n    Title=\"Gress Demo\"\n    Width=\"500\"\n    Height=\"400\"\n    x:DataType=\"viewModels:MainViewModel\"\n    Icon=\"/favicon.ico\"\n    WindowStartupLocation=\"CenterScreen\">\n    <Window.DataContext>\n        <viewModels:MainViewModel />\n    </Window.DataContext>\n\n    <Grid RowDefinitions=\"Auto,*,Auto\">\n        <!--  Aggregated progress  -->\n        <StackPanel\n            Grid.Row=\"0\"\n            Margin=\"16\"\n            Orientation=\"Vertical\">\n            <TextBlock\n                FontSize=\"18\"\n                FontWeight=\"SemiBold\"\n                Text=\"Summary\" />\n            <StackPanel Margin=\"16\" Orientation=\"Vertical\">\n                <!--  Overview  -->\n                <TextBlock>\n                    <Run Text=\"Operations:\" />\n                    <Run FontWeight=\"SemiBold\" Text=\"{Binding Operations.Count, Mode=OneWay}\" />\n                    <Run />\n                    <Run Text=\"Progress:\" />\n                    <Run FontWeight=\"SemiBold\" Text=\"{Binding Progress.Current, Mode=OneWay}\" />\n                </TextBlock>\n\n                <!--  Progress  -->\n                <ProgressBar\n                    Height=\"4\"\n                    Margin=\"0,16,0,0\"\n                    Maximum=\"1\"\n                    Minimum=\"0\"\n                    Value=\"{Binding Progress.Current.Fraction, Mode=OneWay}\" />\n            </StackPanel>\n        </StackPanel>\n\n        <!--  Active operations  -->\n        <StackPanel\n            Grid.Row=\"1\"\n            Margin=\"16\"\n            Orientation=\"Vertical\">\n            <TextBlock\n                FontSize=\"18\"\n                FontWeight=\"SemiBold\"\n                Text=\"Operations\" />\n            <ScrollViewer\n                Margin=\"16\"\n                HorizontalScrollBarVisibility=\"Disabled\"\n                VerticalScrollBarVisibility=\"Auto\">\n                <Panel>\n                    <!--  List  -->\n                    <ItemsControl ItemsSource=\"{Binding Operations}\">\n                        <ItemsControl.ItemTemplate>\n                            <DataTemplate>\n                                <DockPanel Margin=\"0,0,0,8\" LastChildFill=\"False\">\n                                    <!--  Overview  -->\n                                    <TextBlock Margin=\"0,2,0,0\" DockPanel.Dock=\"Left\">\n                                        <Run Text=\"Weight:\" />\n                                        <Run FontWeight=\"SemiBold\" Text=\"{Binding Weight, Mode=OneWay}\" />\n                                        <Run />\n                                        <Run Text=\"Progress:\" />\n                                        <Run FontWeight=\"SemiBold\" Text=\"{Binding Progress.Current, Mode=OneWay}\" />\n                                    </TextBlock>\n\n                                    <!--  Progress  -->\n                                    <ProgressBar\n                                        DockPanel.Dock=\"Right\"\n                                        Maximum=\"1\"\n                                        Minimum=\"0\"\n                                        Value=\"{Binding Progress.Current.Fraction, Mode=OneWay}\" />\n                                </DockPanel>\n                            </DataTemplate>\n                        </ItemsControl.ItemTemplate>\n                    </ItemsControl>\n\n                    <!--  Placeholder  -->\n                    <TextBlock IsVisible=\"{Binding !Operations.Count}\" Text=\"Start a new operation by clicking the button below...\" />\n                </Panel>\n            </ScrollViewer>\n        </StackPanel>\n\n        <!--  New operation  -->\n        <DockPanel\n            Grid.Row=\"2\"\n            Margin=\"16\"\n            LastChildFill=\"False\">\n            <!--  Weight  -->\n            <StackPanel DockPanel.Dock=\"Left\" Orientation=\"Horizontal\">\n                <TextBlock VerticalAlignment=\"Center\">\n                    <Run Text=\"Weight:\" />\n                    <Run FontWeight=\"SemiBold\" Text=\"{Binding Value, ElementName=WeightSlider, Mode=OneWay}\" />\n                </TextBlock>\n                <Slider\n                    x:Name=\"WeightSlider\"\n                    Width=\"200\"\n                    Margin=\"24,0,0,0\"\n                    IsSnapToTickEnabled=\"True\"\n                    Maximum=\"10\"\n                    Minimum=\"1\"\n                    TickFrequency=\"1\" />\n            </StackPanel>\n\n            <!--  Start button  -->\n            <Button\n                Command=\"{Binding PerformWorkCommand}\"\n                CommandParameter=\"{Binding Value, ElementName=WeightSlider}\"\n                Content=\"Start Operation\"\n                DockPanel.Dock=\"Right\" />\n        </DockPanel>\n    </Grid>\n</Window>"
  },
  {
    "path": "Gress.Demo/Views/MainWindow.axaml.cs",
    "content": "using Avalonia.Controls;\n\nnamespace Gress.Demo.Views;\n\npublic partial class MainWindow : Window\n{\n    public MainWindow() => InitializeComponent();\n}\n"
  },
  {
    "path": "Gress.Tests/CompositionSpecs.cs",
    "content": "using System.Linq;\nusing FluentAssertions;\nusing Gress.Completable;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npublic class CompositionSpecs\n{\n    [Fact]\n    public void I_can_transform_progress_updates_into_a_different_type()\n    {\n        // Arrange\n        var collector = new ProgressCollector<int>();\n\n        // Act\n        var progress = collector.WithTransform((string p) => p.Length);\n\n        progress.Report(\"\");\n        progress.Report(\"a\");\n        progress.Report(\"abc\");\n        progress.Report(\"abcdef\");\n\n        // Assert\n        collector.GetValues().Should().Equal(0, 1, 3, 6);\n    }\n\n    [Fact]\n    public void I_can_transform_progress_updates_within_the_same_type()\n    {\n        // Arrange\n        var collector = new ProgressCollector<int>();\n\n        // Act\n        var progress = collector.WithTransform(p => p * 2);\n\n        progress.Report(1);\n        progress.Report(2);\n        progress.Report(3);\n\n        // Assert\n        collector.GetValues().Should().Equal(2, 4, 6);\n    }\n\n    [Fact]\n    public void I_can_filter_out_progress_updates_that_do_not_satisfy_a_predicate()\n    {\n        // Arrange\n        var collector = new ProgressCollector<int>();\n\n        // Act\n        var progress = collector.WithFilter(p => p % 2 == 0);\n\n        progress.Report(0);\n        progress.Report(1);\n        progress.Report(2);\n        progress.Report(3);\n        progress.Report(4);\n        progress.Report(5);\n\n        // Assert\n        collector.GetValues().Should().Equal(0, 2, 4);\n    }\n\n    [Fact]\n    public void I_can_filter_out_progress_updates_with_duplicate_values()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector.WithDeduplication();\n\n        progress.Report(Percentage.FromFraction(0.0));\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n        progress.Report(Percentage.FromFraction(0.6));\n        progress.Report(Percentage.FromFraction(0.6));\n        progress.Report(Percentage.FromFraction(0.9));\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.0),\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5),\n                Percentage.FromFraction(0.6),\n                Percentage.FromFraction(0.9)\n            );\n    }\n\n    [Fact]\n    public void I_can_filter_out_progress_updates_that_arrive_out_of_order()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector.WithOrdering();\n\n        progress.Report(Percentage.FromFraction(0.0));\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.2));\n        progress.Report(Percentage.FromFraction(0.5));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.6));\n        progress.Report(Percentage.FromFraction(0.9));\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.0),\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5),\n                Percentage.FromFraction(0.6),\n                Percentage.FromFraction(0.9)\n            );\n    }\n\n    [Fact]\n    public void I_can_merge_two_progress_handlers_together()\n    {\n        // Arrange\n        var collector1 = new ProgressCollector<Percentage>();\n        var collector2 = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector1.Merge(collector2);\n\n        progress.Report(Percentage.FromFraction(0.0));\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n\n        // Assert\n        collector1\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.0),\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3)\n            );\n\n        collector2\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.0),\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3)\n            );\n    }\n\n    [Fact]\n    public void I_can_merge_multiple_progress_handlers_together()\n    {\n        // Arrange\n        var collectors = Enumerable\n            .Range(0, 10)\n            .Select(_ => new ProgressCollector<Percentage>())\n            .ToArray();\n\n        // Act\n        var progress = collectors.Merge();\n\n        progress.Report(Percentage.FromFraction(0.0));\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n\n        // Assert\n        foreach (var collector in collectors)\n        {\n            collector\n                .GetValues()\n                .Should()\n                .Equal(\n                    Percentage.FromFraction(0.0),\n                    Percentage.FromFraction(0.1),\n                    Percentage.FromFraction(0.3)\n                );\n        }\n    }\n\n    [Fact]\n    public void I_can_convert_a_double_based_progress_handler_into_a_percentage_based_handler_using_fraction_mapping()\n    {\n        // Arrange\n        var collector = new ProgressCollector<double>();\n\n        // Act\n        var progress = collector.ToPercentageBased();\n\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        collector.GetValues().Should().Equal(0.1, 0.3, 0.5);\n    }\n\n    [Fact]\n    public void I_can_convert_a_double_based_progress_handler_into_a_percentage_based_handler_using_value_mapping()\n    {\n        // Arrange\n        var collector = new ProgressCollector<double>();\n\n        // Act\n        var progress = collector.ToPercentageBased(false);\n\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        collector.GetValues().Should().Equal(10, 30, 50);\n    }\n\n    [Fact]\n    public void I_can_convert_an_integer_based_progress_handler_into_a_percentage_based_handler()\n    {\n        // Arrange\n        var collector = new ProgressCollector<int>();\n\n        // Act\n        var progress = collector.ToPercentageBased();\n\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        collector.GetValues().Should().Equal(10, 30, 50);\n    }\n\n    [Fact]\n    public void I_can_convert_a_percentage_based_progress_handler_into_a_double_based_handler_using_fraction_mapping()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector.ToDoubleBased();\n\n        progress.Report(0.1);\n        progress.Report(0.3);\n        progress.Report(0.5);\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5)\n            );\n    }\n\n    [Fact]\n    public void I_can_convert_a_percentage_based_progress_handler_into_a_double_based_handler_using_value_mapping()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector.ToDoubleBased(false);\n\n        progress.Report(10);\n        progress.Report(30);\n        progress.Report(50);\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5)\n            );\n    }\n\n    [Fact]\n    public void I_can_convert_a_percentage_based_progress_handler_into_an_integer_based_handler()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector.ToInt32Based();\n\n        progress.Report(10);\n        progress.Report(30);\n        progress.Report(50);\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5)\n            );\n    }\n\n    [Fact]\n    public void I_can_convert_a_normal_progress_handler_into_a_completable_handler()\n    {\n        // Arrange\n        var isCompleted = false;\n        var collector = new ProgressCollector<Percentage>();\n\n        // Act\n        var progress = collector.ToCompletable(() => isCompleted = true);\n\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n        progress.ReportCompletion();\n\n        // Assert\n        isCompleted.Should().BeTrue();\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5)\n            );\n    }\n}\n"
  },
  {
    "path": "Gress.Tests/Gress.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Content Include=\"xunit.runner.json\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"FluentAssertions\" />\n    <PackageReference Include=\"PowerKit\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"GitHubActionsTestLogger\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../Gress/Gress.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Gress.Tests/IntegrationSpecs.cs",
    "content": "using System;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing FluentAssertions;\nusing Gress.Integrations;\nusing PowerKit;\nusing PowerKit.Extensions;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npublic class IntegrationSpecs\n{\n    [Fact]\n    public async Task I_can_copy_a_stream_to_another_stream_with_progress()\n    {\n        // Arrange\n        using var buffer = SpanPool<byte>.Shared.Rent(\n            // Longer buffer to ensure multiple progress reports\n            81920 * 2\n                + 1000\n        );\n\n        Random.Shared.NextBytes(buffer.Span);\n        var data = buffer.Span.ToArray();\n\n        using var source = new MemoryStream(data);\n        using var destination = new MemoryStream();\n        var progress = new ProgressCollector<Percentage>();\n\n        // Act\n        await source.CopyToAsync(destination, progress);\n\n        // Assert\n        destination.ToArray().Should().Equal(data);\n        progress.GetValues().Should().NotBeEmpty();\n        progress.GetValues().Should().BeInAscendingOrder();\n    }\n\n    [Fact]\n    public async Task I_can_download_a_web_resource_as_a_byte_array_with_progress()\n    {\n        // Arrange\n        using var http = new HttpClient();\n        var progress = new ProgressCollector<Percentage>();\n\n        // Act\n        var result = await http.GetByteArrayAsync(\n            // Need something that reports content length\n            \"https://github.com/Tyrrrz/Gress/releases/download/2.2/Gress.2.2.0.nupkg\",\n            progress\n        );\n\n        // Assert\n        result.Should().NotBeNullOrEmpty();\n        progress.GetValues().Should().NotBeEmpty();\n    }\n\n    [Fact]\n    public async Task I_can_download_a_web_resource_as_a_string_with_progress()\n    {\n        // Arrange\n        using var http = new HttpClient();\n        var progress = new ProgressCollector<Percentage>();\n\n        // Act\n        var result = await http.GetStringAsync(\n            // Need something that reports content length\n            \"https://github.com/Tyrrrz/Gress/releases/download/2.2/Gress.2.2.0.nupkg\",\n            progress\n        );\n\n        // Assert\n        result.Should().NotBeNullOrEmpty();\n        progress.GetValues().Should().NotBeEmpty();\n    }\n\n    [Fact]\n    public async Task I_can_download_a_web_resource_to_a_file_with_progress()\n    {\n        // Arrange\n        using var http = new HttpClient();\n        using var tempFile = TempFile.Create();\n        var progress = new ProgressCollector<Percentage>();\n\n        // Act\n        await http.DownloadAsync(\n            // Need something that reports content length\n            \"https://github.com/Tyrrrz/Gress/releases/download/2.2/Gress.2.2.0.nupkg\",\n            tempFile.Path,\n            progress\n        );\n\n        // Assert\n        new FileInfo(tempFile.Path)\n            .Length.Should()\n            .BeGreaterThan(0);\n\n        progress.GetValues().Should().NotBeEmpty();\n    }\n}\n"
  },
  {
    "path": "Gress.Tests/MuxingSpecs.cs",
    "content": "using FluentAssertions;\nusing Gress.Completable;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npublic class MuxingSpecs\n{\n    [Fact]\n    public void I_can_mux_progress_updates_from_a_single_operation()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n        var muxer = collector.CreateMuxer();\n\n        // Act\n        var progress = muxer.CreateInput();\n\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5)\n            );\n    }\n\n    [Fact]\n    public void I_can_mux_progress_updates_from_multiple_operations()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n        var muxer = collector.CreateMuxer();\n\n        // Act\n        var progress1 = muxer.CreateInput();\n        var progress2 = muxer.CreateInput();\n        var progress3 = muxer.CreateInput();\n\n        progress1.Report(Percentage.FromFraction(0.65));\n        progress2.Report(Percentage.FromFraction(0.25));\n        progress3.Report(Percentage.FromFraction(0.09));\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.65 / 3),\n                Percentage.FromFraction((0.65 + 0.25) / 3),\n                Percentage.FromFraction((0.65 + 0.25 + 0.09) / 3)\n            );\n    }\n\n    [Fact]\n    public void I_can_mux_progress_updates_from_multiple_operations_with_different_weights()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n        var muxer = collector.CreateMuxer();\n\n        // Act\n        var progress1 = muxer.CreateInput();\n        var progress2 = muxer.CreateInput(2);\n        var progress3 = muxer.CreateInput(7);\n\n        progress1.Report(Percentage.FromFraction(1));\n        progress2.Report(Percentage.FromFraction(0.5));\n        progress3.Report(Percentage.FromFraction(0.25));\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(1.0 * 1 / 10),\n                Percentage.FromFraction((1.0 * 1 + 2.0 * 0.5) / 10),\n                Percentage.FromFraction((1.0 * 1 + 2.0 * 0.5 + 7.0 * 0.25) / 10)\n            );\n    }\n\n    [Fact]\n    public void I_can_mux_progress_updates_from_multiple_operations_with_auto_reset_behavior()\n    {\n        // Arrange\n        var collector = new ProgressCollector<Percentage>();\n        var muxer = collector.CreateMuxer().WithAutoReset();\n\n        // Act\n        using (var progress1 = muxer.CreateInput().ToDisposable())\n        using (var progress2 = muxer.CreateInput().ToDisposable())\n        using (var progress3 = muxer.CreateInput().ToDisposable())\n        {\n            progress1.Report(Percentage.FromFraction(1));\n            progress2.Report(Percentage.FromFraction(0.5));\n            progress3.Report(Percentage.FromFraction(0.25));\n        }\n\n        using (var progress4 = muxer.CreateInput().ToDisposable())\n        using (var progress5 = muxer.CreateInput().ToDisposable())\n        using (var progress6 = muxer.CreateInput().ToDisposable())\n        {\n            progress4.Report(Percentage.FromFraction(0.65));\n            progress5.Report(Percentage.FromFraction(0.25));\n            progress6.Report(Percentage.FromFraction(0.09));\n        }\n\n        // Assert\n        collector\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(1.0 / 3),\n                Percentage.FromFraction((1.0 + 0.5) / 3),\n                Percentage.FromFraction((1.0 + 0.5 + 0.25) / 3),\n                Percentage.FromFraction(0),\n                Percentage.FromFraction(0.65 / 3),\n                Percentage.FromFraction((0.65 + 0.25) / 3),\n                Percentage.FromFraction((0.65 + 0.25 + 0.09) / 3),\n                Percentage.FromFraction(0)\n            );\n    }\n}\n"
  },
  {
    "path": "Gress.Tests/TerminalSpecs.cs",
    "content": "using System.ComponentModel;\nusing System.Threading;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npublic class TerminalSpecs\n{\n    [Fact]\n    public void I_can_route_progress_updates_into_a_collection()\n    {\n        // Arrange\n        var progress = new ProgressCollector<Percentage>();\n        progress.Reset();\n\n        // Act\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        progress\n            .GetValues()\n            .Should()\n            .Equal(\n                Percentage.FromFraction(0.1),\n                Percentage.FromFraction(0.3),\n                Percentage.FromFraction(0.5)\n            );\n    }\n\n    [Fact]\n    public void I_can_route_progress_updates_into_a_property()\n    {\n        // Arrange\n        var progress = new ProgressContainer<Percentage>();\n\n        // Act\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        progress.Current.Should().Be(Percentage.FromFraction(0.5));\n    }\n\n    [Fact]\n    public void I_can_route_progress_updates_into_a_property_with_change_notifications()\n    {\n        // Arrange\n        var progress = new ProgressContainer<Percentage>();\n\n        var triggerCount = 0;\n        ((INotifyPropertyChanged)progress).PropertyChanged += (_, _) =>\n            Interlocked.Increment(ref triggerCount);\n\n        // Act\n        progress.Report(Percentage.FromFraction(0.1));\n        progress.Report(Percentage.FromFraction(0.3));\n        progress.Report(Percentage.FromFraction(0.5));\n\n        // Assert\n        triggerCount.Should().Be(3);\n    }\n}\n"
  },
  {
    "path": "Gress.Tests/xunit.runner.json",
    "content": "{\n  \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n  \"methodDisplayOptions\": \"all\",\n  \"methodDisplay\": \"method\"\n}"
  },
  {
    "path": "Gress.slnx",
    "content": "<Solution>\n  <Folder Name=\"/Misc/\">\n    <File Path=\"Directory.Build.props\" />\n    <File Path=\"Directory.Packages.props\" />\n    <File Path=\"global.json\" />\n    <File Path=\"License.txt\" />\n    <File Path=\"NuGet.config\" />\n    <File Path=\"Readme.md\" />\n  </Folder>\n\n  <Project Path=\"Gress/Gress.csproj\" />\n  <Project Path=\"Gress.Demo/Gress.Demo.csproj\" />\n  <Project Path=\"Gress.Tests/Gress.Tests.csproj\" />\n</Solution>\n"
  },
  {
    "path": "License.txt",
    "content": "MIT License\n\nCopyright (c) 2019-2026 Oleksii Holub\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "NuGet.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear />\n    <add key=\"nuget.org\" value=\"https://api.nuget.org/v3/index.json\" protocolVersion=\"3\" />\n  </packageSources>\n  <config>\n    <add key=\"defaultPushSource\" value=\"https://api.nuget.org/v3/index.json\" />\n  </config>\n</configuration>\n"
  },
  {
    "path": "Readme.md",
    "content": "# Gress\n\n[![Status](https://img.shields.io/badge/status-active-47c219.svg)](https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md)\n[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://tyrrrz.me/ukraine)\n[![Build](https://img.shields.io/github/actions/workflow/status/Tyrrrz/Gress/main.yml?branch=prime)](https://github.com/Tyrrrz/Gress/actions)\n[![Coverage](https://img.shields.io/codecov/c/github/Tyrrrz/Gress/prime)](https://codecov.io/gh/Tyrrrz/Gress)\n[![Version](https://img.shields.io/nuget/v/Gress.svg)](https://nuget.org/packages/Gress)\n[![Downloads](https://img.shields.io/nuget/dt/Gress.svg)](https://nuget.org/packages/Gress)\n[![Discord](https://img.shields.io/discord/869237470565392384?label=discord)](https://discord.gg/2SUWKFnHSm)\n[![Fuck Russia](https://img.shields.io/badge/fuck-russia-e4181c.svg?labelColor=000000)](https://twitter.com/tyrrrz/status/1495972128977571848)\n\n<table>\n    <tr>\n        <td width=\"99999\" align=\"center\">Development of this project is entirely funded by the community. <b><a href=\"https://tyrrrz.me/donate\">Consider donating to support!</a></b></td>\n    </tr>\n</table>\n\n<p align=\"center\">\n    <img src=\"favicon.png\" alt=\"Icon\" />\n</p>\n\n**Gress** is a library that extends the standard [`IProgress<T>`](https://learn.microsoft.com/dotnet/api/system.iprogress-1) interface with a set of utilities for collecting, transforming, filtering, and multiplexing progress updates in your code.\n\n## Terms of use<sup>[[?]](https://github.com/Tyrrrz/.github/blob/prime/docs/why-so-political.md)</sup>\n\nBy using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements:\n\n- You **condemn Russia and its military aggression against Ukraine**\n- You **recognize that Russia is an occupant that unlawfully invaded a sovereign state**\n- You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas**\n- You **reject false narratives perpetuated by Russian state propaganda**\n\nTo learn more about the war and how you can help, [click here](https://tyrrrz.me/ukraine). Glory to Ukraine! 🇺🇦\n\n## Install\n\n- 📦 [NuGet](https://nuget.org/packages/Gress): `dotnet add package Gress`\n\n## Screenshots\n\n![demo](.assets/demo.png)\n\n## Usage\n\n### Percentage type\n\nTo make progress updates more explicit, **Gress** provides a universal progress unit — the `Percentage` type.\nUnlike the primitive numeric types that are commonly paired with `IProgress<T>`, this type can be used to unambiguously represent progress as a ratio of completed work to the total amount of work encapsulated by a given operation.\n\nAn instance of `Percentage` can be created from either a value or a fraction:\n\n```csharp\nusing Gress;\n\n// Mapped from value\nvar fiftyPercent = Percentage.FromValue(50); // 50%\n\n// Mapped from fractional representation\nvar twentyPercent = Percentage.FromFraction(0.2); // 20%\n```\n\nSimilarly, both value and fraction can be extracted from an initialized `Percentage` by accessing the corresponding properties:\n\n```csharp\nusing Gress;\n\nvar fiftyPercent = Percentage.FromValue(50);\n\nvar asValue = fiftyPercent.Value; // 50.0 (double)\nvar asFraction = fiftyPercent.Fraction; // 0.5 (double)\n```\n\nUsing `Percentage` in your `IProgress<T>` handlers lets you communicate progress updates without making any assumptions about their semantics:\n\n```csharp\nusing Gress;\n\nasync Task PerformWorkAsync(IProgress<Percentage> progrss)\n{\n    await Task.Delay(100);\n\n    // Half-way done\n    progress.Report(Percentage.FromValue(50));\n\n    await Task.Delay(100);\n\n    // Finished\n    progress.Report(Percentage.FromFraction(1));\n}\n\n// ...\n\nvar progress = new Progress<Percentage>(p => Console.WriteLine(p));\nawait PerformWorkAsync(progress);\n\n// Console output:\n// 50,0%\n// 100,0%\n```\n\nWhen interfacing with external methods, however, you may need to provide a specific progress handler required by their signature.\nIn such cases, you can convert an existing percentage-based handler into another type using one of the available extension methods:\n\n```csharp\nusing Gress;\n\nasync Task FooAsync(IProgress<double> progress) { /* ... */ }\nasync Task BarAsync(IProgress<int> progress) { /* ... */ }\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\nawait FooAsync(progress.ToDoubleBased());\nawait BarAsync(progress.ToInt32Based());\n```\n\nLikewise, you can also perform conversions in the other direction, which can be useful for preserving backwards-compatibility in your own methods:\n\n```csharp\nusing Gress;\n\nasync Task FooAsync(IProgress<double> progress)\n{\n    var actualProgress = progress.ToPercentageBased(); // IProgress<Percentage>\n\n    // Reports 0.5 on the original progress handler\n    actualProgress.Report(Percentage.FromFraction(0.5));\n}\n\nasync Task BarAsync(IProgress<int> progress)\n{\n    var actualProgress = progress.ToPercentageBased(); // IProgress<Percentage>\n\n    // Reports 50 on the original progress handler\n    actualProgress.Report(Percentage.FromFraction(0.5));\n}\n```\n\n> [!NOTE]\n> When converting between percentage-based and double-based handlers, percentages are mapped using their fractional form by default.\n> To override this behavior and map by value instead, use `ToDoubleBased(asFraction: false)` and `ToPercentageBased(asFraction: false)`.\n\n> [!NOTE]\n> For more complex conversion scenarios, consider using the [`WithTransform(...)`](#transformation) method.\n\n### Terminal handlers\n\nEvery progress reporting chain ultimately ends with a terminal handler, which usually relays the information to the user or stores it somewhere else.\nTo simplify some of the most common scenarios, **Gress** comes with two terminal handlers built in.\n\n#### Progress container\n\nThis handler is an object with a single property, whose value is overwritten with every new progress update that gets reported.\nIt also implements the `INotifyPropertyChanged` interface, allowing the property to be bound from XAML-based user interfaces.\n\nHere's a very basic example of how you would use it in a typical Avalonia application:\n\n```csharp\npublic class MainViewModel\n{\n    public MainViewModel() =>\n        PerformWorkCommand = new AsyncRelayCommand(PerformWorkAsync);\n\n    public ProgressContainer<Percentage> Progress { get; } = new();\n\n    public IRelayCommand PerformWorkCommand { get; }\n\n    public async Task PerformWorkAsync()\n    {\n        for (var i = 1; i <= 100; i++)\n        {\n            // Simulate work\n            await Task.Delay(200);\n\n            // Report progress as a value in the 0..100 range\n            Progress.Report(Percentage.FromValue(i));\n        }\n    }\n}\n```\n\n```xml\n<Window\n    x:Class=\"MainWindow\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    x:DataType=\"MainViewModel\">\n    <StackPanel>\n        <Button\n            Margin=\"32\"\n            Content=\"Execute\"\n            Command=\"{Binding PerformWorkCommand}\" />\n\n        <ProgressBar\n            Margin=\"32\"\n            Height=\"10\"\n            Minimum=\"0\"\n            Maximum=\"100\"\n            Value=\"{Binding Progress.Current.Value, Mode=OneWay}\" />\n    </StackPanel>\n</Window>\n```\n\n#### Progress collector\n\nThis handler works by storing all reported progress updates in a collection, whose values can be retrieved later.\nIt's primarily designed for testing purposes.\n\nHere's how you can use it to verify that a method reported its progress correctly:\n\n```csharp\n[Fact]\npublic async Task My_method_reports_progress_correctly()\n{\n    // Arrange\n    var progress = new ProgressCollector<Percentage>();\n    var worker = new Worker();\n\n    // Act\n    await worker.PerformWorkAsync(progress);\n\n    // Assert\n    var values = progress.GetValues();\n\n    values.Should().NotBeEmpty(); // not empty\n    values.Should().OnlyHaveUniqueItems(); // no redundant updates\n}\n```\n\n### Composing handlers\n\nExisting progress handlers can be composed into more complex handlers using some of the extension methods that **Gress** offers.\nThese can be used to easily apply transformations, inject filtering logic, or merge multiple handlers together.\n\n#### Transformation\n\nYou can use `WithTransform(...)` to create a handler that transforms all reported progress updates into a different form:\n\n```csharp\nusing Gress;\n\nenum Status { Started, HalfWay, Completed }\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\n// Transform into a progress handler that accepts an enum value and maps\n// it into a value of the original type\nvar transformedProgress = progress.WithTransform((Status s) => s switch\n{\n    Status.Completed => Percentage.FromValue(100), // 100%\n    Status.HalfWay => Percentage.FromValue(50), // 50%\n    _ => Percentage.FromValue(0) // 0%\n}); // IProgress<Status>\n\n// Effectively reports 50% on the original handler\ntransformedProgress.Report(Status.HalfWay);\n```\n\nA simpler overload of the above method can also be used when transforming between values of the same type:\n\n```csharp\nusing Gress;\n\nvar progress = new Progress<int>(p => /* ... */);\n\nvar transformedProgress = progress.WithTransform(p => 5 * p); // IProgress<int>\n\n// Effectively reports 50% on the original handler\ntransformedProgress.Report(10);\n```\n\n> [!NOTE]\n> Method `WithTransform(...)` bears some resemblance to LINQ's `Select(...)`, however they are not completely equivalent.\n> The main difference is that the flow of data in `IProgress<T>` is inverse to that of `IEnumerable<T>`, which means that the transformations in `WithTransform(...)` are applied in the opposite direction.\n\n#### Filtering\n\nYou can use `WithFilter(...)` to create a handler that drops progress updates that don't satisfy a predicate:\n\n```csharp\nusing Gress;\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\n// Filter out values below 10%\nvar filteredProgress = progress.WithFilter(p => p.Fraction >= 0.1);\n\nfilteredProgress.Report(Percentage.FromFraction(0.05)); // ✖\nfilteredProgress.Report(Percentage.FromFraction(0.25)); // ✓\n```\n\n#### Deduplication\n\nYou can use `WithDeduplication(...)` to create a handler that filters out consecutive progress updates with the same value:\n\n```csharp\nusing Gress;\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\nvar deduplicatedProgress = progress.WithDeduplication();\n\ndeduplicatedProgress.Report(Percentage.FromFraction(0.1)); // ✓\ndeduplicatedProgress.Report(Percentage.FromFraction(0.3)); // ✓\ndeduplicatedProgress.Report(Percentage.FromFraction(0.3)); // ✖\ndeduplicatedProgress.Report(Percentage.FromFraction(0.3)); // ✖\ndeduplicatedProgress.Report(Percentage.FromFraction(0.5)); // ✓\n```\n\n#### Ordering\n\nYou can use `WithOrdering(...)` to create a handler that filters out progress updates that arrive out of order:\n\n```csharp\nusing Gress;\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\nvar orderedProgress = progress.WithOrdering();\n\norderedProgress.Report(Percentage.FromFraction(0.1)); // ✓\norderedProgress.Report(Percentage.FromFraction(0.3)); // ✓\norderedProgress.Report(Percentage.FromFraction(0.2)); // ✖\norderedProgress.Report(Percentage.FromFraction(0.5)); // ✓\norderedProgress.Report(Percentage.FromFraction(0.4)); // ✖\n```\n\n#### Merging\n\nYou can use `Merge(...)` to combine multiple progress handlers into one:\n\n```csharp\nusing Gress;\n\nvar progress1 = new Progress<Percentage>(p => /* ... */);\nvar progress2 = new Progress<Percentage>(p => /* ... */);\n\nvar mergedProgress = progress1.Merge(progress2); // IProgress<Percentage>\n\n// Reports 50% on both progress handlers\nmergedProgress.Report(Percentage.FromFraction(0.5));\n```\n\nThis method can also be called on collections:\n\n```csharp\nusing Gress;\n\nvar progresses = new[]\n{\n    new Progress<Percentage>(p => /* ... */),\n    new Progress<Percentage>(p => /* ... */),\n    new Progress<Percentage>(p => /* ... */),\n    new Progress<Percentage>(p => /* ... */)\n};\n\nvar mergedProgress = progresses.Merge(); // IProgress<Percentage>\n\n// Reports 50% on all progress handlers\nmergedProgress.Report(Percentage.FromFraction(0.5));\n```\n\n### Multiplexing\n\nMultiplexing allows a single handler to aggregate progress updates from multiple input sources.\nThis is useful when you want to encapsulate several progress-reporting operations in a single higher-order operation.\n\nTo do this, create a muxer for the target progress handler and then use it to assign an input for each operation:\n\n```csharp\nusing Gress;\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\nvar muxer = progress.CreateMuxer();\nvar subProgress1 = muxer.CreateInput(); // IProgress<Percentage>\nvar subProgress2 = muxer.CreateInput(); // IProgress<Percentage>\nvar subProgress3 = muxer.CreateInput(); // IProgress<Percentage>\n```\n\nWhen a progress update is reported on any of these inputs, all the updates up to that point are aggregated into one and routed to the target handler.\nThe sample below illustrates this process in detail:\n\n```csharp\n// ...\n\nsubProgress1.Report(Percentage.FromFraction(0.5));\n\n// Input 1 ->  50%\n// Input 2 ->   0%\n// Input 3 ->   0%\n// Total   -> ~17%\n\nsubProgress1.Report(Percentage.FromFraction(1));\nsubProgress2.Report(Percentage.FromFraction(0.75));\n\n// Input 1 -> 100%\n// Input 2 ->  75%\n// Input 3 ->   0%\n// Total   -> ~58%\n\nsubProgress2.Report(Percentage.FromFraction(1));\nsubProgress3.Report(Percentage.FromFraction(0.9));\n\n// Input 1 -> 100%\n// Input 2 -> 100%\n// Input 3 ->  90%\n// Total   -> ~97%\n\nsubProgress3.Report(Percentage.FromFraction(1));\n\n// Input 1 -> 100%\n// Input 2 -> 100%\n// Input 3 -> 100%\n// Total   -> 100%\n```\n\nAdditionally, since muxer inputs are progress handlers themselves, they can be multiplexed further as well.\nDoing this allows you to create hierarchical progress reporting chains:\n\n```csharp\nusing Gress;\n\nasync Task PerformWorkAsync(IProgress<Percentage> progress)\n{\n    for (var i = 1; i <= 100; i++)\n    {\n        await Task.Delay(200);\n        progress.Report(Percentage.FromValue(i));\n    }\n}\n\nasync Task FooAsync(IProgress<Percentage> progress)\n{\n    var muxer = progress.CreateMuxer();\n    var subProgress1 = muxer.CreateInput();\n    var subProgress2 = muxer.CreateInput();\n\n    await Task.WhenAll(\n        PerformWorkAsync(subProgress1),\n        PerformWorkAsync(subProgress2)\n    );\n}\n\nasync Task BarAsync(IProgress<Percentage> progress)\n{\n    var muxer = progress.CreateMuxer();\n    var subProgress1 = muxer.CreateInput();\n    var subProgress2 = muxer.CreateInput();\n    var subProgress3 = muxer.CreateInput();\n\n    await Task.WhenAll(\n        FooAsync(subProgress1),\n        FooAsync(subProgress2),\n        FooAsync(subProgress3)\n    );\n}\n```\n\n> [!NOTE]\n> Muxing is only available on percentage-based handlers because it relies on their ability to represent progress as a relative fraction.\n> If required, you can convert certain other handlers into percentage-based handlers using the `ToPercentageBased()` extension method.\n\n#### Custom weights\n\nA muxer input may be assigned a custom weight modifier, which determines its priority in relation to others.\nProgress reported on an input with higher weight influences the aggregated progress to a greater degree and vice versa.\n\nYou can specify the input weight by passing it to the `CreateInput(...)` method:\n\n```csharp\nusing Gress;\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\nvar muxer = progress.CreateMuxer();\nvar subProgress1 = muxer.CreateInput(1);\nvar subProgress2 = muxer.CreateInput(4);\n\n// Weight split:\n// Input 1 -> 20% of total\n// Input 2 -> 80% of total\n\nsubProgress1.Report(Percentage.FromFraction(0.9));\nsubProgress2.Report(Percentage.FromFraction(0.3));\n\n// Input 1 -> 90% (less important)\n// Input 2 -> 30% (more important)\n// Total   -> 42% (would've been 60% without weights)\n```\n\n#### Auto-reset muxer\n\nIn some cases, you may need to report progress on an infinite workflow where new operations are started and completed in a continuous fashion.\nThis can be achieved by using an auto-reset muxer.\n\nInputs to an auto-reset muxer implement the `ICompletableProgress<T>` interface and are capable of reporting completion after all of the underlying work is finished.\nOnce all connected inputs report completion, they are disconnected from the muxer and the latter is reset back to the initial state.\n\nTo create an auto-reset muxer, call `WithAutoReset()` on an existing instance:\n\n```csharp\nusing Gress;\nusing Gress.Completable;\n\nvar progress = new Progress<Percentage>(p => /* ... */);\n\nvar muxer = progress.CreateMuxer().WithAutoReset();\nvar subProgress1 = muxer.CreateInput(); // ICompletableProgress<Percentage>\nvar subProgress2 = muxer.CreateInput(); // ICompletableProgress<Percentage>\n\nsubProgress1.Report(Percentage.FromFraction(0.3));\nsubProgress2.Report(Percentage.FromFraction(0.9));\n\n// Input 1 -> 30%\n// Input 2 -> 90%\n// Total   -> 60%\n\nsubProgress1.Report(Percentage.FromFraction(1));\nsubProgress1.ReportCompletion();\n\n// Input 1 -> 100% (completed)\n// Input 2 -> 90%\n// Total   -> 95%\n\nsubProgress2.Report(Percentage.FromFraction(1));\nsubProgress2.ReportCompletion();\n\n// All inputs disconnected\n// Total   -> 0%\n\nvar subProgress3 = muxer.CreateInput();\nsubProgress3.Report(Percentage.FromFraction(0.5));\n\n// Input 3 -> 50%\n// Total   -> 50%\n```\n\n> [!NOTE]\n> You can wrap an instance of `ICompletableProgress<T>` in a disposable container by calling `ToDisposable()`.\n> This allows you to place the handler in a `using (...)` block, which ensures that the completion is always reported at the end.\n\n### Integration extensions\n\n**Gress** also provides extensions for several built-in .NET types that integrate `IProgress<Percentage>` into their existing APIs.\nFor example, you can use the below `Stream.CopyToAsync(...)` overload to copy data between two streams while tracking the operation's progress:\n\n```csharp\nusing Gress;\nusing Gress.Integrations;\n\nawait using var source = File.OpenRead(\"input.bin\");\nawait using var destination = File.Create(\"output.bin\");\n\nawait source.CopyToAsync(\n    destination,\n    new Progress<Percentage>(p => Console.WriteLine($\"Copied: {p}\"))\n);\n\n// Console output:\n// Copied: 37,5%\n// Copied: 62,5%\n// Copied: 87,5%\n// Copied: 100,0%\n```\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"10.0.100\",\n    \"rollForward\": \"latestFeature\"\n  }\n}"
  }
]