Repository: Tyrrrz/Gress
Branch: prime
Commit: 39db230235f1
Files: 41
Total size: 80.9 KB
Directory structure:
gitextract_yhxilno4/
├── .github/
│ ├── dependabot.yml
│ ├── release.yml
│ └── workflows/
│ └── main.yml
├── .gitignore
├── Directory.Build.props
├── Directory.Packages.props
├── Gress/
│ ├── Completable/
│ │ ├── AutoResetProgressMuxer.cs
│ │ ├── CompletableProgressExtensions.cs
│ │ ├── DelegateCompletableProgress.cs
│ │ ├── DisposableCompletableProgress.cs
│ │ └── ICompletableProgress.cs
│ ├── DelegateProgress.cs
│ ├── Gress.csproj
│ ├── Integrations/
│ │ ├── HttpClientExtensions.cs
│ │ ├── HttpContentExtensions.cs
│ │ └── StreamExtensions.cs
│ ├── Percentage.cs
│ ├── ProgressCollector.cs
│ ├── ProgressContainer.cs
│ ├── ProgressExtensions.cs
│ └── ProgressMuxer.cs
├── Gress.Demo/
│ ├── App.axaml
│ ├── App.axaml.cs
│ ├── Gress.Demo.csproj
│ ├── Program.cs
│ ├── Readme.md
│ ├── ViewModels/
│ │ ├── MainViewModel.cs
│ │ └── OperationViewModel.cs
│ └── Views/
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
├── Gress.Tests/
│ ├── CompositionSpecs.cs
│ ├── Gress.Tests.csproj
│ ├── IntegrationSpecs.cs
│ ├── MuxingSpecs.cs
│ ├── TerminalSpecs.cs
│ └── xunit.runner.json
├── Gress.slnx
├── License.txt
├── NuGet.config
├── Readme.md
└── global.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: monthly
labels:
- enhancement
groups:
actions:
patterns:
- "*"
- package-ecosystem: nuget
directory: "/"
schedule:
interval: monthly
labels:
- enhancement
groups:
nuget:
patterns:
- "*"
================================================
FILE: .github/release.yml
================================================
changelog:
exclude:
authors:
- dependabot
- dependabot[bot]
categories:
- title: Enhancements
labels:
- enhancement
- title: Bugs
labels:
- bug
- title: Other
labels:
- "*"
================================================
FILE: .github/workflows/main.yml
================================================
name: main
on:
workflow_dispatch:
inputs:
package-version:
type: string
description: Package version
required: false
deploy:
type: boolean
description: Deploy package
required: false
default: false
push:
branches:
- prime
tags:
- "*"
pull_request:
branches:
- prime
jobs:
main:
uses: Tyrrrz/.github/.github/workflows/nuget.yml@prime
permissions:
actions: write
contents: write
with:
deploy: ${{ inputs.deploy || github.ref_type == 'tag' }}
package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
================================================
FILE: .gitignore
================================================
# User-specific files
.vs/
.idea/
*.suo
*.user
# Build results
bin/
obj/
# Test results
TestResults/
================================================
FILE: Directory.Build.props
================================================
<Project>
<PropertyGroup>
<Version>0.0.0-dev</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (C) Oleksii Holub</Copyright>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<!-- Disable nullability warnings on older frameworks because there is no nullability info for BCL -->
<PropertyGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netstandard2.1'))">
<Nullable>annotations</Nullable>
</PropertyGroup>
<!-- Disable trim and AOT analyzers on older frameworks as they're not compatible with newer language features -->
<PropertyGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
<EnableAotAnalyzer>false</EnableAotAnalyzer>
</PropertyGroup>
<PropertyGroup>
<Authors>$(Company)</Authors>
<Description>Progress reporting toolbox</Description>
<PackageTags>progress aggregation muxing transform filter net standard core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/Gress</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/Gress/releases</PackageReleaseNotes>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
</Project>
================================================
FILE: Directory.Packages.props
================================================
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="12.0.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.0.2" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.14" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.2" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.2" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="CSharpier.MsBuild" Version="1.2.5" />
<PackageVersion Include="FluentAssertions" Version="8.9.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="PolyShim" Version="2.11.0" />
<PackageVersion Include="PowerKit" Version="1.1.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
================================================
FILE: Gress/Completable/AutoResetProgressMuxer.cs
================================================
using System;
using System.Threading;
namespace Gress.Completable;
/// <summary>
/// Aggregates multiple progress updates into a single handler.
/// Resets itself once all inputs report completion.
/// </summary>
public partial class AutoResetProgressMuxer(ProgressMuxer muxer)
{
private readonly Lock _lock = new();
private readonly ProgressMuxer _muxer = muxer;
private int _pendingInputs;
/// <summary>
/// Creates a progress handler that reports progress to this muxer.
/// Specified weight determines the priority of this handler relative to other
/// handlers connected to this muxer. Progress reported on a handler with higher
/// weight influences the final progress to a greater degree.
/// </summary>
/// <remarks>
/// Returned progress handler can report completion. Once all linked handlers
/// report completion, the progress is reset, and existing handlers are disconnected.
/// </remarks>
public ICompletableProgress<Percentage> CreateInput(double weight = 1.0)
{
using (_lock.EnterScope())
{
var item = new Item(this, _muxer.CreateInput(weight));
// Make sure the item was created successfully before incrementing
_pendingInputs++;
return item;
}
}
}
public partial class AutoResetProgressMuxer
{
private class Item : ICompletableProgress<Percentage>
{
private readonly AutoResetProgressMuxer _parent;
private readonly IProgress<Percentage> _target;
public Item(AutoResetProgressMuxer parent, IProgress<Percentage> target)
{
_parent = parent;
_target = target;
}
public void Report(Percentage value)
{
using (_parent._lock.EnterScope())
{
_target.Report(value);
}
}
public void ReportCompletion()
{
using (_parent._lock.EnterScope())
{
if (--_parent._pendingInputs <= 0)
_parent._muxer.Reset();
}
}
}
}
================================================
FILE: Gress/Completable/CompletableProgressExtensions.cs
================================================
using System;
namespace Gress.Completable;
/// <summary>
/// Extensions for <see cref="CompletableProgressExtensions" />.
/// </summary>
public static class CompletableProgressExtensions
{
/// <inheritdoc cref="CompletableProgressExtensions" />
extension<T>(IProgress<T> progress)
{
/// <summary>
/// Converts a regular progress handler into a progress handler with explicit completion.
/// </summary>
public ICompletableProgress<T> ToCompletable(Action reportCompletion) =>
new DelegateCompletableProgress<T>(progress.Report, reportCompletion);
}
/// <inheritdoc cref="CompletableProgressExtensions" />
extension<T>(ICompletableProgress<T> progress)
{
/// <summary>
/// Wraps the specified completable progress handler in a disposable container.
/// Disposing the container reports completion on the handler.
/// </summary>
public DisposableCompletableProgress<T> ToDisposable() => new(progress);
}
/// <inheritdoc cref="CompletableProgressExtensions" />
extension(ProgressMuxer muxer)
{
/// <summary>
/// Wraps the muxer in a special adapter that disconnects all inputs from the muxer
/// after they all report completion.
/// </summary>
public AutoResetProgressMuxer WithAutoReset() => new(muxer);
}
}
================================================
FILE: Gress/Completable/DelegateCompletableProgress.cs
================================================
using System;
namespace Gress.Completable;
/// <summary>
/// Simple implementation of <see cref="ICompletableProgress{T}" /> that uses the provided
/// delegates to report progress updates and signal completion.
/// </summary>
public class DelegateCompletableProgress<T>(Action<T> report, Action reportCompletion)
: ICompletableProgress<T>
{
/// <inheritdoc />
public void Report(T value) => report(value);
/// <inheritdoc />
public void ReportCompletion() => reportCompletion();
}
================================================
FILE: Gress/Completable/DisposableCompletableProgress.cs
================================================
using System;
namespace Gress.Completable;
/// <summary>
/// Convenience wrapper for <see cref="ICompletableProgress{T}" /> that reports completion
/// on disposal.
/// </summary>
public class DisposableCompletableProgress<T>(ICompletableProgress<T> target)
: ICompletableProgress<T>,
IDisposable
{
/// <inheritdoc />
public void Report(T value) => target.Report(value);
/// <inheritdoc />
public void ReportCompletion() => target.ReportCompletion();
/// <inheritdoc cref="ReportCompletion" />
public void Dispose() => ReportCompletion();
}
================================================
FILE: Gress/Completable/ICompletableProgress.cs
================================================
using System;
namespace Gress.Completable;
/// <summary>
/// Progress handler with explicit completion feedback.
/// </summary>
public interface ICompletableProgress<in T> : IProgress<T>
{
/// <summary>
/// Reports overall completion of the operation.
/// </summary>
void ReportCompletion();
}
================================================
FILE: Gress/DelegateProgress.cs
================================================
using System;
namespace Gress;
/// <summary>
/// Simple implementation of <see cref="IProgress{T}" /> that uses the provided
/// delegate to report progress updates.
/// </summary>
/// <remarks>
/// Unlike <see cref="Progress{T}" />, this implementation does not post updates
/// through a synchronization context, but rather calls the delegate directly.
/// </remarks>
public class DelegateProgress<T>(Action<T> report) : IProgress<T>
{
/// <inheritdoc />
public void Report(T value) => report(value);
}
================================================
FILE: Gress/Gress.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net10.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<IsTrimmable
Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))"
>true</IsTrimmable
>
<IsAotCompatible
Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))"
>true</IsAotCompatible
>
</PropertyGroup>
<PropertyGroup>
<PackageIcon>favicon.png</PackageIcon>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include="../favicon.png" Pack="true" PackagePath="" Visible="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
<PackageReference Include="PolyShim" PrivateAssets="all" />
<PackageReference Include="PowerKit" PrivateAssets="all" />
</ItemGroup>
</Project>
================================================
FILE: Gress/Integrations/HttpClientExtensions.cs
================================================
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Gress.Integrations;
using PowerKit.Extensions;
namespace Gress.Integrations;
/// <summary>
/// Provides progress-aware extensions for <see cref="HttpClient" />.
/// </summary>
public static class HttpClientExtensions
{
/// <inheritdoc cref="HttpClientExtensions" />
extension(HttpClient client)
{
/// <summary>
/// Sends a GET request and returns the response body as a byte array.
/// </summary>
public async Task<byte[]> GetByteArrayAsync(
Uri requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
using var response = await client
.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response
.Content.ReadAsByteArrayAsync(progress, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Sends a GET request and returns the response body as a byte array.
/// </summary>
public async Task<byte[]> GetByteArrayAsync(
string requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.GetByteArrayAsync(
new Uri(requestUri, UriKind.RelativeOrAbsolute),
progress,
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Sends a GET request and returns the response body as text.
/// </summary>
public async Task<string> GetStringAsync(
Uri requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
using var response = await client
.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response
.Content.ReadAsStringAsync(progress, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Sends a GET request and returns the response body as text.
/// </summary>
public async Task<string> GetStringAsync(
string requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.GetStringAsync(
new Uri(requestUri, UriKind.RelativeOrAbsolute),
progress,
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Sends a GET request and saves the response body to a file.
/// </summary>
public async Task DownloadAsync(
Uri requestUri,
string filePath,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.DownloadAsync(requestUri, filePath, progress?.ToDoubleBased(), cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Sends a GET request and saves the response body to a file.
/// </summary>
public async Task DownloadAsync(
string requestUri,
string filePath,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.DownloadAsync(
new Uri(requestUri, UriKind.RelativeOrAbsolute),
filePath,
progress,
cancellationToken
)
.ConfigureAwait(false);
}
}
================================================
FILE: Gress/Integrations/HttpContentExtensions.cs
================================================
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using PowerKit.Extensions;
namespace Gress.Integrations;
/// <summary>
/// Provides progress-aware extensions for <see cref="HttpContent" />.
/// </summary>
public static class HttpContentExtensions
{
/// <inheritdoc cref="HttpContentExtensions" />
extension(HttpContent content)
{
/// <summary>
/// Serializes the HTTP content and writes it to the specified stream.
/// </summary>
public async Task CopyToAsync(
Stream destination,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await content
.CopyToStreamAsync(destination, progress?.ToDoubleBased(), cancellationToken)
.ConfigureAwait(false);
private async Task<MemoryStream> ReadAsMemoryStreamAsync(
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
var destination = new MemoryStream();
await content
.CopyToAsync(destination, progress, cancellationToken)
.ConfigureAwait(false);
destination.Position = 0;
return destination;
}
/// <summary>
/// Reads the HTTP content and returns it as a byte array.
/// </summary>
public async Task<byte[]> ReadAsByteArrayAsync(
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
using var buffer = await content
.ReadAsMemoryStreamAsync(progress, cancellationToken)
.ConfigureAwait(false);
return buffer.ToArray();
}
/// <summary>
/// Reads the HTTP content and returns it as a string.
/// </summary>
public async Task<string> ReadAsStringAsync(
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
var encoding =
content
.Headers.ContentType?.CharSet?.Pipe(c =>
Encoding
.GetEncodings()
.FirstOrDefault(e =>
string.Equals(e.Name, c, StringComparison.OrdinalIgnoreCase)
)
)
?.GetEncoding()
?? Encoding.UTF8;
using var buffer = await content
.ReadAsMemoryStreamAsync(progress, cancellationToken)
.ConfigureAwait(false);
using var reader = new StreamReader(buffer, encoding, true);
return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
}
}
}
================================================
FILE: Gress/Integrations/StreamExtensions.cs
================================================
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Gress.Integrations;
using PowerKit.Extensions;
namespace Gress.Integrations;
/// <summary>
/// Provides progress-aware extensions for <see cref="Stream" />.
/// </summary>
public static class StreamExtensions
{
/// <inheritdoc cref="StreamExtensions" />
extension(Stream source)
{
/// <summary>
/// Asynchronously copies bytes from the source stream to the destination stream.
/// </summary>
public async Task CopyToAsync(
Stream destination,
long sourceLength,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await source
.CopyToAsync(
destination,
sourceLength,
progress?.ToDoubleBased(),
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Asynchronously copies bytes from the source stream to the destination stream.
/// </summary>
public async Task CopyToAsync(
Stream destination,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await source
.CopyToAsync(
destination,
source.CanSeek ? source.Length : -1,
progress,
cancellationToken
)
.ConfigureAwait(false);
}
}
================================================
FILE: Gress/Percentage.cs
================================================
using System;
namespace Gress;
/// <summary>
/// Unit of progress.
/// </summary>
public readonly partial struct Percentage(double value)
{
/// <summary>
/// Percentage value.
/// </summary>
public double Value { get; } = value;
/// <summary>
/// Percentage value in decimal form (e.g. 0.75 for 75%).
/// </summary>
public double Fraction => Value / 100.0;
/// <summary>
/// Formats the value of this instance to a string.
/// </summary>
public string ToString(IFormatProvider? formatProvider) =>
Fraction.ToString("P1", formatProvider);
/// <inheritdoc cref="ToString(IFormatProvider?)" />
public override string ToString() => ToString(null);
}
public partial struct Percentage
{
/// <summary>
/// Creates a percentage from its value.
/// </summary>
public static Percentage FromValue(double value) => new(value);
/// <summary>
/// Creates a percentage from its value in decimal form (e.g. from 0.75 to 75%).
/// </summary>
public static Percentage FromFraction(double fraction) => FromValue(fraction * 100.0);
}
public partial struct Percentage : IEquatable<Percentage>, IComparable<Percentage>
{
/// <inheritdoc />
public int CompareTo(Percentage other) => Value.CompareTo(other.Value);
/// <inheritdoc />
public bool Equals(Percentage other) => Value.CompareTo(other.Value) == 0;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is Percentage other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => Value.GetHashCode();
/// <summary>
/// Greater than operator.
/// </summary>
public static bool operator >(Percentage left, Percentage right) => left.CompareTo(right) > 0;
/// <summary>
/// Lesser than operator.
/// </summary>
public static bool operator <(Percentage left, Percentage right) => left.CompareTo(right) < 0;
/// <summary>
/// Equality operator.
/// </summary>
public static bool operator ==(Percentage left, Percentage right) => left.Equals(right);
/// <summary>
/// Inequality operator.
/// </summary>
public static bool operator !=(Percentage left, Percentage right) => !(left == right);
}
public partial struct Percentage : IFormattable
{
string IFormattable.ToString(string? format, IFormatProvider? formatProvider) =>
ToString(formatProvider);
}
================================================
FILE: Gress/ProgressCollector.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading;
namespace Gress;
/// <summary>
/// Terminal progress handler that stores all reported progress updates in a collection.
/// </summary>
public class ProgressCollector<T> : IProgress<T>
{
private readonly Lock _lock = new();
private readonly List<T> _reports = [];
/// <summary>
/// Clears the reported progress updates.
/// </summary>
public void Reset()
{
using (_lock.EnterScope())
_reports.Clear();
}
/// <summary>
/// Returns the progress updates reported so far.
/// </summary>
public IReadOnlyList<T> GetValues()
{
using (_lock.EnterScope())
return _reports.ToArray();
}
/// <inheritdoc />
public void Report(T value)
{
using (_lock.EnterScope())
_reports.Add(value);
}
}
================================================
FILE: Gress/ProgressContainer.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Gress;
/// <summary>
/// Terminal progress handler that records the last reported progress update.
/// </summary>
public partial class ProgressContainer<T>(T initial) : IProgress<T>
{
/// <summary>
/// Initializes an instance of <see cref="ProgressContainer{T}" />.
/// </summary>
/// <remarks>
/// If <typeparamref name="T" /> is a reference type, the initial value of the
/// <see cref="Current"/> property will be <c>null</c>.
/// Consider using the other constructor overload to provide a non-null initial value.
/// </remarks>
public ProgressContainer()
: this(default!) { }
/// <summary>
/// Last reported progress update.
/// </summary>
/// <remarks>
/// If this property is accessed before any progress has been reported,
/// it will evaluate to the initial value provided by the constructor.
/// </remarks>
public T Current
{
get => initial;
private set
{
if (EqualityComparer<T>.Default.Equals(value, initial))
return;
initial = value;
OnPropertyChanged();
}
}
/// <inheritdoc />
public void Report(T value) => Current = value;
}
public partial class ProgressContainer<T> : INotifyPropertyChanged
{
private event PropertyChangedEventHandler? PropertyChanged;
event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
add => PropertyChanged += value;
remove => PropertyChanged -= value;
}
// Instrumented automatically by Fody
// ReSharper disable once UnusedMember.Local
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
================================================
FILE: Gress/ProgressExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading;
using PowerKit;
namespace Gress;
/// <summary>
/// Extensions for <see cref="IProgress{T}" />.
/// </summary>
public static class ProgressExtensions
{
/// <inheritdoc cref="ProgressExtensions" />
extension<T>(IProgress<T> progress)
{
/// <summary>
/// Projects progress updates into a different shape.
/// </summary>
public IProgress<TTransformed> WithTransform<TTransformed>(Func<TTransformed, T> map) =>
new DelegateProgress<TTransformed>(p => progress.Report(map(p)));
/// <summary>
/// Projects progress updates into a different shape.
/// </summary>
public IProgress<T> WithTransform(Func<T, T> map) => progress.WithTransform<T, T>(map);
/// <summary>
/// Filters progress updates based on the specified predicate.
/// </summary>
public IProgress<T> WithFilter(Func<T, bool> shouldReport) =>
new DelegateProgress<T>(p =>
{
if (shouldReport(p))
progress.Report(p);
});
/// <summary>
/// Filters out consecutive progress updates with the same value of the specified key.
/// </summary>
public IProgress<T> WithDeduplication<TKey>(
Func<T, TKey> getKey,
IEqualityComparer<TKey>? comparer = null
)
{
var syncRoot = new Lock();
var actualComparer = comparer ?? EqualityComparer<TKey>.Default;
var lastValueCell = new Cell<TKey>();
return new DelegateProgress<T>(p =>
{
using (syncRoot.EnterScope())
{
var value = getKey(p);
if (
lastValueCell.TryOpen(out var lastValue)
&& actualComparer.Equals(lastValue, value)
)
{
return;
}
progress.Report(p);
lastValueCell.Store(value);
}
});
}
/// <summary>
/// Filters out consecutive progress updates with the same value.
/// </summary>
public IProgress<T> WithDeduplication(IEqualityComparer<T>? comparer = null) =>
progress.WithDeduplication(p => p, comparer);
/// <summary>
/// Filters out progress updates that arrive out of order.
/// </summary>
public IProgress<T> WithOrdering(IComparer<T>? comparer = null)
{
var syncRoot = new Lock();
var actualComparer = comparer ?? Comparer<T>.Default;
var lastValueCell = new Cell<T>();
return new DelegateProgress<T>(p =>
{
using (syncRoot.EnterScope())
{
if (
lastValueCell.TryOpen(out var lastValue)
&& actualComparer.Compare(lastValue, p) > 0
)
{
return;
}
progress.Report(p);
lastValueCell.Store(p);
}
});
}
/// <summary>
/// Merges two progress handlers into one.
/// </summary>
public IProgress<T> Merge(IProgress<T> otherProgress) =>
new DelegateProgress<T>(p =>
{
progress.Report(p);
otherProgress.Report(p);
});
/// <summary>
/// Converts the specified progress handler into a <see cref="Percentage" />-based progress handler.
/// </summary>
public IProgress<Percentage> ToPercentageBased(Func<Percentage, T> map) =>
progress.WithTransform(map);
}
/// <inheritdoc cref="ProgressExtensions" />
extension(IProgress<double> progress)
{
/// <summary>
/// Converts the specified <see cref="double" />-based progress handler into a
/// <see cref="Percentage" />-based progress handler.
/// Parameter <paramref name="asFraction" /> specifies whether the percentage-based
/// progress is reported in its decimal form (true) or in percentage form (false).
/// </summary>
public IProgress<Percentage> ToPercentageBased(bool asFraction = true) =>
asFraction
? progress.ToPercentageBased(p => p.Fraction)
: progress.ToPercentageBased(p => p.Value);
}
/// <inheritdoc cref="ProgressExtensions" />
extension(IProgress<int> progress)
{
/// <summary>
/// Converts the specified <see cref="int" />-based progress handler into a
/// <see cref="Percentage" />-based progress handler.
/// </summary>
public IProgress<Percentage> ToPercentageBased() =>
progress.ToPercentageBased(p => (int)p.Value);
}
/// <inheritdoc cref="ProgressExtensions" />
extension(IProgress<Percentage> progress)
{
/// <summary>
/// Converts the specified <see cref="Percentage" />-based progress handler into a
/// <see cref="double" />-based progress handler.
/// Parameter <paramref name="asFraction" /> specifies whether the percentage-based
/// progress is reported in its decimal form (true) or in percentage form (false).
/// </summary>
public IProgress<double> ToDoubleBased(bool asFraction = true) =>
asFraction
? progress.WithTransform((double p) => Percentage.FromFraction(p))
: progress.WithTransform((double p) => Percentage.FromValue(p));
/// <summary>
/// Converts the specified <see cref="Percentage" />-based progress handler into an
/// <see cref="int" />-based progress handler.
/// </summary>
public IProgress<int> ToInt32Based() =>
progress.WithTransform((int p) => Percentage.FromValue(p));
/// <summary>
/// Creates a muxer for the specified progress handler, allowing it to aggregate
/// reports from multiple sources.
/// </summary>
public ProgressMuxer CreateMuxer() => new(progress);
}
/// <inheritdoc cref="ProgressExtensions" />
extension<T>(IReadOnlyList<IProgress<T>> progresses)
{
/// <summary>
/// Merges multiple progress handlers into one.
/// </summary>
public IProgress<T> Merge() =>
new DelegateProgress<T>(p =>
{
foreach (var progress in progresses)
progress.Report(p);
});
}
}
================================================
FILE: Gress/ProgressMuxer.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading;
namespace Gress;
/// <summary>
/// Aggregates multiple progress updates into a single handler.
/// </summary>
public partial class ProgressMuxer(IProgress<Percentage> target)
{
private readonly Lock _lock = new();
private readonly HashSet<Input> _inputs = [];
private bool _anyInputReported;
private void ReportAggregatedProgress()
{
using (_lock.EnterScope())
{
var weightedSum = 0.0;
var weightedMax = 0.0;
foreach (var input in _inputs)
{
weightedSum += input.Weight * input.Progress.Fraction;
weightedMax += input.Weight * 1.0;
}
target.Report(
Percentage.FromFraction(weightedSum != 0 ? weightedSum / weightedMax : 0)
);
}
}
/// <summary>
/// Creates a progress handler that reports progress to this muxer.
/// Specified weight determines the priority of this handler relative to other
/// handlers connected to this muxer. Progress reported on a handler with higher
/// weight influences the final progress to a greater degree.
/// </summary>
public IProgress<Percentage> CreateInput(double weight = 1.0)
{
if (weight <= 0)
throw new ArgumentOutOfRangeException(nameof(weight), "Weight must be positive.");
using (_lock.EnterScope())
{
var input = new Input(this, weight);
_inputs.Add(input);
// Recalculate and report new progress, taking into account the new input.
// Don't do it if none of the inputs have reported progress yet, because
// we would just be reporting zeroes for each call to this method.
if (_anyInputReported)
ReportAggregatedProgress();
return input;
}
}
/// <summary>
/// Disconnects all progress handlers from this muxer.
/// </summary>
public void Reset()
{
using (_lock.EnterScope())
{
_inputs.Clear();
_anyInputReported = false;
ReportAggregatedProgress();
}
}
}
public partial class ProgressMuxer
{
private class Input : IProgress<Percentage>
{
private readonly ProgressMuxer _parent;
public double Weight { get; }
public Percentage Progress { get; private set; }
public Input(ProgressMuxer parent, double weight)
{
_parent = parent;
Weight = weight;
}
public void Report(Percentage value)
{
using (_parent._lock.EnterScope())
{
Progress = value;
// This input could have been removed from the muxer.
// If that's the case, don't do anything.
if (!_parent._inputs.Contains(this))
return;
_parent.ReportAggregatedProgress();
_parent._anyInputReported = true;
}
}
}
}
================================================
FILE: Gress.Demo/App.axaml
================================================
<Application
x:Class="Gress.Demo.App"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
================================================
FILE: Gress.Demo/App.axaml.cs
================================================
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Gress.Demo.Views;
namespace Gress.Demo;
public class App : Application
{
public override void Initialize()
{
base.Initialize();
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
desktopLifetime.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
}
================================================
FILE: Gress.Demo/Gress.Demo.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ApplicationIcon>../favicon.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="../favicon.ico" Link="favicon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Gress/Gress.csproj" />
</ItemGroup>
</Project>
================================================
FILE: Gress.Demo/Program.cs
================================================
using System;
using Avalonia;
namespace Gress.Demo;
public static class Program
{
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();
[STAThread]
public static int Main(string[] args) =>
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
================================================
FILE: Gress.Demo/Readme.md
================================================
# Gress Avalonia demo
This demo features a simple Avalonia application that lets you start new operations and track their progress.
================================================
FILE: Gress.Demo/ViewModels/MainViewModel.cs
================================================
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Gress.Completable;
namespace Gress.Demo.ViewModels;
public partial class MainViewModel : ObservableObject
{
private readonly AutoResetProgressMuxer _progressMuxer;
public MainViewModel() => _progressMuxer = Progress.CreateMuxer().WithAutoReset();
public ProgressContainer<Percentage> Progress { get; } = new();
public ObservableCollection<OperationViewModel> Operations { get; } = [];
// Start an operation that simulates some work and reports progress
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PerformWorkAsync(double weight)
{
using var progress = _progressMuxer.CreateInput(weight).ToDisposable();
var operation = new OperationViewModel(weight);
var mergedProgress = progress.Merge(operation.Progress);
Operations.Add(operation);
for (var i = 1; i <= 100; i++)
{
// Simulate work
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(1, 5) / 10.0));
// Report progress as a value in the 0..100 range
mergedProgress.Report(Percentage.FromValue(i));
}
Operations.Remove(operation);
}
}
================================================
FILE: Gress.Demo/ViewModels/OperationViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
namespace Gress.Demo.ViewModels;
public class OperationViewModel(double weight) : ObservableObject
{
public double Weight { get; } = weight;
public ProgressContainer<Percentage> Progress { get; } = new();
}
================================================
FILE: Gress.Demo/Views/MainWindow.axaml
================================================
<Window
x:Class="Gress.Demo.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Gress.Demo.ViewModels"
Title="Gress Demo"
Width="500"
Height="400"
x:DataType="viewModels:MainViewModel"
Icon="/favicon.ico"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<viewModels:MainViewModel />
</Window.DataContext>
<Grid RowDefinitions="Auto,*,Auto">
<!-- Aggregated progress -->
<StackPanel
Grid.Row="0"
Margin="16"
Orientation="Vertical">
<TextBlock
FontSize="18"
FontWeight="SemiBold"
Text="Summary" />
<StackPanel Margin="16" Orientation="Vertical">
<!-- Overview -->
<TextBlock>
<Run Text="Operations:" />
<Run FontWeight="SemiBold" Text="{Binding Operations.Count, Mode=OneWay}" />
<Run />
<Run Text="Progress:" />
<Run FontWeight="SemiBold" Text="{Binding Progress.Current, Mode=OneWay}" />
</TextBlock>
<!-- Progress -->
<ProgressBar
Height="4"
Margin="0,16,0,0"
Maximum="1"
Minimum="0"
Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
</StackPanel>
</StackPanel>
<!-- Active operations -->
<StackPanel
Grid.Row="1"
Margin="16"
Orientation="Vertical">
<TextBlock
FontSize="18"
FontWeight="SemiBold"
Text="Operations" />
<ScrollViewer
Margin="16"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Panel>
<!-- List -->
<ItemsControl ItemsSource="{Binding Operations}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel Margin="0,0,0,8" LastChildFill="False">
<!-- Overview -->
<TextBlock Margin="0,2,0,0" DockPanel.Dock="Left">
<Run Text="Weight:" />
<Run FontWeight="SemiBold" Text="{Binding Weight, Mode=OneWay}" />
<Run />
<Run Text="Progress:" />
<Run FontWeight="SemiBold" Text="{Binding Progress.Current, Mode=OneWay}" />
</TextBlock>
<!-- Progress -->
<ProgressBar
DockPanel.Dock="Right"
Maximum="1"
Minimum="0"
Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Placeholder -->
<TextBlock IsVisible="{Binding !Operations.Count}" Text="Start a new operation by clicking the button below..." />
</Panel>
</ScrollViewer>
</StackPanel>
<!-- New operation -->
<DockPanel
Grid.Row="2"
Margin="16"
LastChildFill="False">
<!-- Weight -->
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center">
<Run Text="Weight:" />
<Run FontWeight="SemiBold" Text="{Binding Value, ElementName=WeightSlider, Mode=OneWay}" />
</TextBlock>
<Slider
x:Name="WeightSlider"
Width="200"
Margin="24,0,0,0"
IsSnapToTickEnabled="True"
Maximum="10"
Minimum="1"
TickFrequency="1" />
</StackPanel>
<!-- Start button -->
<Button
Command="{Binding PerformWorkCommand}"
CommandParameter="{Binding Value, ElementName=WeightSlider}"
Content="Start Operation"
DockPanel.Dock="Right" />
</DockPanel>
</Grid>
</Window>
================================================
FILE: Gress.Demo/Views/MainWindow.axaml.cs
================================================
using Avalonia.Controls;
namespace Gress.Demo.Views;
public partial class MainWindow : Window
{
public MainWindow() => InitializeComponent();
}
================================================
FILE: Gress.Tests/CompositionSpecs.cs
================================================
using System.Linq;
using FluentAssertions;
using Gress.Completable;
using Xunit;
namespace Gress.Tests;
public class CompositionSpecs
{
[Fact]
public void I_can_transform_progress_updates_into_a_different_type()
{
// Arrange
var collector = new ProgressCollector<int>();
// Act
var progress = collector.WithTransform((string p) => p.Length);
progress.Report("");
progress.Report("a");
progress.Report("abc");
progress.Report("abcdef");
// Assert
collector.GetValues().Should().Equal(0, 1, 3, 6);
}
[Fact]
public void I_can_transform_progress_updates_within_the_same_type()
{
// Arrange
var collector = new ProgressCollector<int>();
// Act
var progress = collector.WithTransform(p => p * 2);
progress.Report(1);
progress.Report(2);
progress.Report(3);
// Assert
collector.GetValues().Should().Equal(2, 4, 6);
}
[Fact]
public void I_can_filter_out_progress_updates_that_do_not_satisfy_a_predicate()
{
// Arrange
var collector = new ProgressCollector<int>();
// Act
var progress = collector.WithFilter(p => p % 2 == 0);
progress.Report(0);
progress.Report(1);
progress.Report(2);
progress.Report(3);
progress.Report(4);
progress.Report(5);
// Assert
collector.GetValues().Should().Equal(0, 2, 4);
}
[Fact]
public void I_can_filter_out_progress_updates_with_duplicate_values()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
// Act
var progress = collector.WithDeduplication();
progress.Report(Percentage.FromFraction(0.0));
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
progress.Report(Percentage.FromFraction(0.6));
progress.Report(Percentage.FromFraction(0.6));
progress.Report(Percentage.FromFraction(0.9));
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.0),
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5),
Percentage.FromFraction(0.6),
Percentage.FromFraction(0.9)
);
}
[Fact]
public void I_can_filter_out_progress_updates_that_arrive_out_of_order()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
// Act
var progress = collector.WithOrdering();
progress.Report(Percentage.FromFraction(0.0));
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.2));
progress.Report(Percentage.FromFraction(0.5));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.6));
progress.Report(Percentage.FromFraction(0.9));
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.0),
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5),
Percentage.FromFraction(0.6),
Percentage.FromFraction(0.9)
);
}
[Fact]
public void I_can_merge_two_progress_handlers_together()
{
// Arrange
var collector1 = new ProgressCollector<Percentage>();
var collector2 = new ProgressCollector<Percentage>();
// Act
var progress = collector1.Merge(collector2);
progress.Report(Percentage.FromFraction(0.0));
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
// Assert
collector1
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.0),
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3)
);
collector2
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.0),
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3)
);
}
[Fact]
public void I_can_merge_multiple_progress_handlers_together()
{
// Arrange
var collectors = Enumerable
.Range(0, 10)
.Select(_ => new ProgressCollector<Percentage>())
.ToArray();
// Act
var progress = collectors.Merge();
progress.Report(Percentage.FromFraction(0.0));
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
// Assert
foreach (var collector in collectors)
{
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.0),
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3)
);
}
}
[Fact]
public void I_can_convert_a_double_based_progress_handler_into_a_percentage_based_handler_using_fraction_mapping()
{
// Arrange
var collector = new ProgressCollector<double>();
// Act
var progress = collector.ToPercentageBased();
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
collector.GetValues().Should().Equal(0.1, 0.3, 0.5);
}
[Fact]
public void I_can_convert_a_double_based_progress_handler_into_a_percentage_based_handler_using_value_mapping()
{
// Arrange
var collector = new ProgressCollector<double>();
// Act
var progress = collector.ToPercentageBased(false);
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
collector.GetValues().Should().Equal(10, 30, 50);
}
[Fact]
public void I_can_convert_an_integer_based_progress_handler_into_a_percentage_based_handler()
{
// Arrange
var collector = new ProgressCollector<int>();
// Act
var progress = collector.ToPercentageBased();
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
collector.GetValues().Should().Equal(10, 30, 50);
}
[Fact]
public void I_can_convert_a_percentage_based_progress_handler_into_a_double_based_handler_using_fraction_mapping()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
// Act
var progress = collector.ToDoubleBased();
progress.Report(0.1);
progress.Report(0.3);
progress.Report(0.5);
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5)
);
}
[Fact]
public void I_can_convert_a_percentage_based_progress_handler_into_a_double_based_handler_using_value_mapping()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
// Act
var progress = collector.ToDoubleBased(false);
progress.Report(10);
progress.Report(30);
progress.Report(50);
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5)
);
}
[Fact]
public void I_can_convert_a_percentage_based_progress_handler_into_an_integer_based_handler()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
// Act
var progress = collector.ToInt32Based();
progress.Report(10);
progress.Report(30);
progress.Report(50);
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5)
);
}
[Fact]
public void I_can_convert_a_normal_progress_handler_into_a_completable_handler()
{
// Arrange
var isCompleted = false;
var collector = new ProgressCollector<Percentage>();
// Act
var progress = collector.ToCompletable(() => isCompleted = true);
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
progress.ReportCompletion();
// Assert
isCompleted.Should().BeTrue();
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5)
);
}
}
================================================
FILE: Gress.Tests/Gress.Tests.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="PowerKit" PrivateAssets="all" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Gress/Gress.csproj" />
</ItemGroup>
</Project>
================================================
FILE: Gress.Tests/IntegrationSpecs.cs
================================================
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Gress.Integrations;
using PowerKit;
using PowerKit.Extensions;
using Xunit;
namespace Gress.Tests;
public class IntegrationSpecs
{
[Fact]
public async Task I_can_copy_a_stream_to_another_stream_with_progress()
{
// Arrange
using var buffer = SpanPool<byte>.Shared.Rent(
// Longer buffer to ensure multiple progress reports
81920 * 2
+ 1000
);
Random.Shared.NextBytes(buffer.Span);
var data = buffer.Span.ToArray();
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
var progress = new ProgressCollector<Percentage>();
// Act
await source.CopyToAsync(destination, progress);
// Assert
destination.ToArray().Should().Equal(data);
progress.GetValues().Should().NotBeEmpty();
progress.GetValues().Should().BeInAscendingOrder();
}
[Fact]
public async Task I_can_download_a_web_resource_as_a_byte_array_with_progress()
{
// Arrange
using var http = new HttpClient();
var progress = new ProgressCollector<Percentage>();
// Act
var result = await http.GetByteArrayAsync(
// Need something that reports content length
"https://github.com/Tyrrrz/Gress/releases/download/2.2/Gress.2.2.0.nupkg",
progress
);
// Assert
result.Should().NotBeNullOrEmpty();
progress.GetValues().Should().NotBeEmpty();
}
[Fact]
public async Task I_can_download_a_web_resource_as_a_string_with_progress()
{
// Arrange
using var http = new HttpClient();
var progress = new ProgressCollector<Percentage>();
// Act
var result = await http.GetStringAsync(
// Need something that reports content length
"https://github.com/Tyrrrz/Gress/releases/download/2.2/Gress.2.2.0.nupkg",
progress
);
// Assert
result.Should().NotBeNullOrEmpty();
progress.GetValues().Should().NotBeEmpty();
}
[Fact]
public async Task I_can_download_a_web_resource_to_a_file_with_progress()
{
// Arrange
using var http = new HttpClient();
using var tempFile = TempFile.Create();
var progress = new ProgressCollector<Percentage>();
// Act
await http.DownloadAsync(
// Need something that reports content length
"https://github.com/Tyrrrz/Gress/releases/download/2.2/Gress.2.2.0.nupkg",
tempFile.Path,
progress
);
// Assert
new FileInfo(tempFile.Path)
.Length.Should()
.BeGreaterThan(0);
progress.GetValues().Should().NotBeEmpty();
}
}
================================================
FILE: Gress.Tests/MuxingSpecs.cs
================================================
using FluentAssertions;
using Gress.Completable;
using Xunit;
namespace Gress.Tests;
public class MuxingSpecs
{
[Fact]
public void I_can_mux_progress_updates_from_a_single_operation()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
var muxer = collector.CreateMuxer();
// Act
var progress = muxer.CreateInput();
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5)
);
}
[Fact]
public void I_can_mux_progress_updates_from_multiple_operations()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
var muxer = collector.CreateMuxer();
// Act
var progress1 = muxer.CreateInput();
var progress2 = muxer.CreateInput();
var progress3 = muxer.CreateInput();
progress1.Report(Percentage.FromFraction(0.65));
progress2.Report(Percentage.FromFraction(0.25));
progress3.Report(Percentage.FromFraction(0.09));
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.65 / 3),
Percentage.FromFraction((0.65 + 0.25) / 3),
Percentage.FromFraction((0.65 + 0.25 + 0.09) / 3)
);
}
[Fact]
public void I_can_mux_progress_updates_from_multiple_operations_with_different_weights()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
var muxer = collector.CreateMuxer();
// Act
var progress1 = muxer.CreateInput();
var progress2 = muxer.CreateInput(2);
var progress3 = muxer.CreateInput(7);
progress1.Report(Percentage.FromFraction(1));
progress2.Report(Percentage.FromFraction(0.5));
progress3.Report(Percentage.FromFraction(0.25));
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(1.0 * 1 / 10),
Percentage.FromFraction((1.0 * 1 + 2.0 * 0.5) / 10),
Percentage.FromFraction((1.0 * 1 + 2.0 * 0.5 + 7.0 * 0.25) / 10)
);
}
[Fact]
public void I_can_mux_progress_updates_from_multiple_operations_with_auto_reset_behavior()
{
// Arrange
var collector = new ProgressCollector<Percentage>();
var muxer = collector.CreateMuxer().WithAutoReset();
// Act
using (var progress1 = muxer.CreateInput().ToDisposable())
using (var progress2 = muxer.CreateInput().ToDisposable())
using (var progress3 = muxer.CreateInput().ToDisposable())
{
progress1.Report(Percentage.FromFraction(1));
progress2.Report(Percentage.FromFraction(0.5));
progress3.Report(Percentage.FromFraction(0.25));
}
using (var progress4 = muxer.CreateInput().ToDisposable())
using (var progress5 = muxer.CreateInput().ToDisposable())
using (var progress6 = muxer.CreateInput().ToDisposable())
{
progress4.Report(Percentage.FromFraction(0.65));
progress5.Report(Percentage.FromFraction(0.25));
progress6.Report(Percentage.FromFraction(0.09));
}
// Assert
collector
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(1.0 / 3),
Percentage.FromFraction((1.0 + 0.5) / 3),
Percentage.FromFraction((1.0 + 0.5 + 0.25) / 3),
Percentage.FromFraction(0),
Percentage.FromFraction(0.65 / 3),
Percentage.FromFraction((0.65 + 0.25) / 3),
Percentage.FromFraction((0.65 + 0.25 + 0.09) / 3),
Percentage.FromFraction(0)
);
}
}
================================================
FILE: Gress.Tests/TerminalSpecs.cs
================================================
using System.ComponentModel;
using System.Threading;
using FluentAssertions;
using Xunit;
namespace Gress.Tests;
public class TerminalSpecs
{
[Fact]
public void I_can_route_progress_updates_into_a_collection()
{
// Arrange
var progress = new ProgressCollector<Percentage>();
progress.Reset();
// Act
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
progress
.GetValues()
.Should()
.Equal(
Percentage.FromFraction(0.1),
Percentage.FromFraction(0.3),
Percentage.FromFraction(0.5)
);
}
[Fact]
public void I_can_route_progress_updates_into_a_property()
{
// Arrange
var progress = new ProgressContainer<Percentage>();
// Act
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
progress.Current.Should().Be(Percentage.FromFraction(0.5));
}
[Fact]
public void I_can_route_progress_updates_into_a_property_with_change_notifications()
{
// Arrange
var progress = new ProgressContainer<Percentage>();
var triggerCount = 0;
((INotifyPropertyChanged)progress).PropertyChanged += (_, _) =>
Interlocked.Increment(ref triggerCount);
// Act
progress.Report(Percentage.FromFraction(0.1));
progress.Report(Percentage.FromFraction(0.3));
progress.Report(Percentage.FromFraction(0.5));
// Assert
triggerCount.Should().Be(3);
}
}
================================================
FILE: Gress.Tests/xunit.runner.json
================================================
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"methodDisplayOptions": "all",
"methodDisplay": "method"
}
================================================
FILE: Gress.slnx
================================================
<Solution>
<Folder Name="/Misc/">
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="global.json" />
<File Path="License.txt" />
<File Path="NuGet.config" />
<File Path="Readme.md" />
</Folder>
<Project Path="Gress/Gress.csproj" />
<Project Path="Gress.Demo/Gress.Demo.csproj" />
<Project Path="Gress.Tests/Gress.Tests.csproj" />
</Solution>
================================================
FILE: License.txt
================================================
MIT License
Copyright (c) 2019-2026 Oleksii Holub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: NuGet.config
================================================
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<config>
<add key="defaultPushSource" value="https://api.nuget.org/v3/index.json" />
</config>
</configuration>
================================================
FILE: Readme.md
================================================
# Gress
[](https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md)
[](https://tyrrrz.me/ukraine)
[](https://github.com/Tyrrrz/Gress/actions)
[](https://codecov.io/gh/Tyrrrz/Gress)
[](https://nuget.org/packages/Gress)
[](https://nuget.org/packages/Gress)
[](https://discord.gg/2SUWKFnHSm)
[](https://twitter.com/tyrrrz/status/1495972128977571848)
<table>
<tr>
<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>
</tr>
</table>
<p align="center">
<img src="favicon.png" alt="Icon" />
</p>
**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.
## Terms of use<sup>[[?]](https://github.com/Tyrrrz/.github/blob/prime/docs/why-so-political.md)</sup>
By 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:
- You **condemn Russia and its military aggression against Ukraine**
- You **recognize that Russia is an occupant that unlawfully invaded a sovereign state**
- You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas**
- You **reject false narratives perpetuated by Russian state propaganda**
To learn more about the war and how you can help, [click here](https://tyrrrz.me/ukraine). Glory to Ukraine! 🇺🇦
## Install
- 📦 [NuGet](https://nuget.org/packages/Gress): `dotnet add package Gress`
## Screenshots

## Usage
### Percentage type
To make progress updates more explicit, **Gress** provides a universal progress unit — the `Percentage` type.
Unlike 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.
An instance of `Percentage` can be created from either a value or a fraction:
```csharp
using Gress;
// Mapped from value
var fiftyPercent = Percentage.FromValue(50); // 50%
// Mapped from fractional representation
var twentyPercent = Percentage.FromFraction(0.2); // 20%
```
Similarly, both value and fraction can be extracted from an initialized `Percentage` by accessing the corresponding properties:
```csharp
using Gress;
var fiftyPercent = Percentage.FromValue(50);
var asValue = fiftyPercent.Value; // 50.0 (double)
var asFraction = fiftyPercent.Fraction; // 0.5 (double)
```
Using `Percentage` in your `IProgress<T>` handlers lets you communicate progress updates without making any assumptions about their semantics:
```csharp
using Gress;
async Task PerformWorkAsync(IProgress<Percentage> progrss)
{
await Task.Delay(100);
// Half-way done
progress.Report(Percentage.FromValue(50));
await Task.Delay(100);
// Finished
progress.Report(Percentage.FromFraction(1));
}
// ...
var progress = new Progress<Percentage>(p => Console.WriteLine(p));
await PerformWorkAsync(progress);
// Console output:
// 50,0%
// 100,0%
```
When interfacing with external methods, however, you may need to provide a specific progress handler required by their signature.
In such cases, you can convert an existing percentage-based handler into another type using one of the available extension methods:
```csharp
using Gress;
async Task FooAsync(IProgress<double> progress) { /* ... */ }
async Task BarAsync(IProgress<int> progress) { /* ... */ }
var progress = new Progress<Percentage>(p => /* ... */);
await FooAsync(progress.ToDoubleBased());
await BarAsync(progress.ToInt32Based());
```
Likewise, you can also perform conversions in the other direction, which can be useful for preserving backwards-compatibility in your own methods:
```csharp
using Gress;
async Task FooAsync(IProgress<double> progress)
{
var actualProgress = progress.ToPercentageBased(); // IProgress<Percentage>
// Reports 0.5 on the original progress handler
actualProgress.Report(Percentage.FromFraction(0.5));
}
async Task BarAsync(IProgress<int> progress)
{
var actualProgress = progress.ToPercentageBased(); // IProgress<Percentage>
// Reports 50 on the original progress handler
actualProgress.Report(Percentage.FromFraction(0.5));
}
```
> [!NOTE]
> When converting between percentage-based and double-based handlers, percentages are mapped using their fractional form by default.
> To override this behavior and map by value instead, use `ToDoubleBased(asFraction: false)` and `ToPercentageBased(asFraction: false)`.
> [!NOTE]
> For more complex conversion scenarios, consider using the [`WithTransform(...)`](#transformation) method.
### Terminal handlers
Every progress reporting chain ultimately ends with a terminal handler, which usually relays the information to the user or stores it somewhere else.
To simplify some of the most common scenarios, **Gress** comes with two terminal handlers built in.
#### Progress container
This handler is an object with a single property, whose value is overwritten with every new progress update that gets reported.
It also implements the `INotifyPropertyChanged` interface, allowing the property to be bound from XAML-based user interfaces.
Here's a very basic example of how you would use it in a typical Avalonia application:
```csharp
public class MainViewModel
{
public MainViewModel() =>
PerformWorkCommand = new AsyncRelayCommand(PerformWorkAsync);
public ProgressContainer<Percentage> Progress { get; } = new();
public IRelayCommand PerformWorkCommand { get; }
public async Task PerformWorkAsync()
{
for (var i = 1; i <= 100; i++)
{
// Simulate work
await Task.Delay(200);
// Report progress as a value in the 0..100 range
Progress.Report(Percentage.FromValue(i));
}
}
}
```
```xml
<Window
x:Class="MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:DataType="MainViewModel">
<StackPanel>
<Button
Margin="32"
Content="Execute"
Command="{Binding PerformWorkCommand}" />
<ProgressBar
Margin="32"
Height="10"
Minimum="0"
Maximum="100"
Value="{Binding Progress.Current.Value, Mode=OneWay}" />
</StackPanel>
</Window>
```
#### Progress collector
This handler works by storing all reported progress updates in a collection, whose values can be retrieved later.
It's primarily designed for testing purposes.
Here's how you can use it to verify that a method reported its progress correctly:
```csharp
[Fact]
public async Task My_method_reports_progress_correctly()
{
// Arrange
var progress = new ProgressCollector<Percentage>();
var worker = new Worker();
// Act
await worker.PerformWorkAsync(progress);
// Assert
var values = progress.GetValues();
values.Should().NotBeEmpty(); // not empty
values.Should().OnlyHaveUniqueItems(); // no redundant updates
}
```
### Composing handlers
Existing progress handlers can be composed into more complex handlers using some of the extension methods that **Gress** offers.
These can be used to easily apply transformations, inject filtering logic, or merge multiple handlers together.
#### Transformation
You can use `WithTransform(...)` to create a handler that transforms all reported progress updates into a different form:
```csharp
using Gress;
enum Status { Started, HalfWay, Completed }
var progress = new Progress<Percentage>(p => /* ... */);
// Transform into a progress handler that accepts an enum value and maps
// it into a value of the original type
var transformedProgress = progress.WithTransform((Status s) => s switch
{
Status.Completed => Percentage.FromValue(100), // 100%
Status.HalfWay => Percentage.FromValue(50), // 50%
_ => Percentage.FromValue(0) // 0%
}); // IProgress<Status>
// Effectively reports 50% on the original handler
transformedProgress.Report(Status.HalfWay);
```
A simpler overload of the above method can also be used when transforming between values of the same type:
```csharp
using Gress;
var progress = new Progress<int>(p => /* ... */);
var transformedProgress = progress.WithTransform(p => 5 * p); // IProgress<int>
// Effectively reports 50% on the original handler
transformedProgress.Report(10);
```
> [!NOTE]
> Method `WithTransform(...)` bears some resemblance to LINQ's `Select(...)`, however they are not completely equivalent.
> 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.
#### Filtering
You can use `WithFilter(...)` to create a handler that drops progress updates that don't satisfy a predicate:
```csharp
using Gress;
var progress = new Progress<Percentage>(p => /* ... */);
// Filter out values below 10%
var filteredProgress = progress.WithFilter(p => p.Fraction >= 0.1);
filteredProgress.Report(Percentage.FromFraction(0.05)); // ✖
filteredProgress.Report(Percentage.FromFraction(0.25)); // ✓
```
#### Deduplication
You can use `WithDeduplication(...)` to create a handler that filters out consecutive progress updates with the same value:
```csharp
using Gress;
var progress = new Progress<Percentage>(p => /* ... */);
var deduplicatedProgress = progress.WithDeduplication();
deduplicatedProgress.Report(Percentage.FromFraction(0.1)); // ✓
deduplicatedProgress.Report(Percentage.FromFraction(0.3)); // ✓
deduplicatedProgress.Report(Percentage.FromFraction(0.3)); // ✖
deduplicatedProgress.Report(Percentage.FromFraction(0.3)); // ✖
deduplicatedProgress.Report(Percentage.FromFraction(0.5)); // ✓
```
#### Ordering
You can use `WithOrdering(...)` to create a handler that filters out progress updates that arrive out of order:
```csharp
using Gress;
var progress = new Progress<Percentage>(p => /* ... */);
var orderedProgress = progress.WithOrdering();
orderedProgress.Report(Percentage.FromFraction(0.1)); // ✓
orderedProgress.Report(Percentage.FromFraction(0.3)); // ✓
orderedProgress.Report(Percentage.FromFraction(0.2)); // ✖
orderedProgress.Report(Percentage.FromFraction(0.5)); // ✓
orderedProgress.Report(Percentage.FromFraction(0.4)); // ✖
```
#### Merging
You can use `Merge(...)` to combine multiple progress handlers into one:
```csharp
using Gress;
var progress1 = new Progress<Percentage>(p => /* ... */);
var progress2 = new Progress<Percentage>(p => /* ... */);
var mergedProgress = progress1.Merge(progress2); // IProgress<Percentage>
// Reports 50% on both progress handlers
mergedProgress.Report(Percentage.FromFraction(0.5));
```
This method can also be called on collections:
```csharp
using Gress;
var progresses = new[]
{
new Progress<Percentage>(p => /* ... */),
new Progress<Percentage>(p => /* ... */),
new Progress<Percentage>(p => /* ... */),
new Progress<Percentage>(p => /* ... */)
};
var mergedProgress = progresses.Merge(); // IProgress<Percentage>
// Reports 50% on all progress handlers
mergedProgress.Report(Percentage.FromFraction(0.5));
```
### Multiplexing
Multiplexing allows a single handler to aggregate progress updates from multiple input sources.
This is useful when you want to encapsulate several progress-reporting operations in a single higher-order operation.
To do this, create a muxer for the target progress handler and then use it to assign an input for each operation:
```csharp
using Gress;
var progress = new Progress<Percentage>(p => /* ... */);
var muxer = progress.CreateMuxer();
var subProgress1 = muxer.CreateInput(); // IProgress<Percentage>
var subProgress2 = muxer.CreateInput(); // IProgress<Percentage>
var subProgress3 = muxer.CreateInput(); // IProgress<Percentage>
```
When 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.
The sample below illustrates this process in detail:
```csharp
// ...
subProgress1.Report(Percentage.FromFraction(0.5));
// Input 1 -> 50%
// Input 2 -> 0%
// Input 3 -> 0%
// Total -> ~17%
subProgress1.Report(Percentage.FromFraction(1));
subProgress2.Report(Percentage.FromFraction(0.75));
// Input 1 -> 100%
// Input 2 -> 75%
// Input 3 -> 0%
// Total -> ~58%
subProgress2.Report(Percentage.FromFraction(1));
subProgress3.Report(Percentage.FromFraction(0.9));
// Input 1 -> 100%
// Input 2 -> 100%
// Input 3 -> 90%
// Total -> ~97%
subProgress3.Report(Percentage.FromFraction(1));
// Input 1 -> 100%
// Input 2 -> 100%
// Input 3 -> 100%
// Total -> 100%
```
Additionally, since muxer inputs are progress handlers themselves, they can be multiplexed further as well.
Doing this allows you to create hierarchical progress reporting chains:
```csharp
using Gress;
async Task PerformWorkAsync(IProgress<Percentage> progress)
{
for (var i = 1; i <= 100; i++)
{
await Task.Delay(200);
progress.Report(Percentage.FromValue(i));
}
}
async Task FooAsync(IProgress<Percentage> progress)
{
var muxer = progress.CreateMuxer();
var subProgress1 = muxer.CreateInput();
var subProgress2 = muxer.CreateInput();
await Task.WhenAll(
PerformWorkAsync(subProgress1),
PerformWorkAsync(subProgress2)
);
}
async Task BarAsync(IProgress<Percentage> progress)
{
var muxer = progress.CreateMuxer();
var subProgress1 = muxer.CreateInput();
var subProgress2 = muxer.CreateInput();
var subProgress3 = muxer.CreateInput();
await Task.WhenAll(
FooAsync(subProgress1),
FooAsync(subProgress2),
FooAsync(subProgress3)
);
}
```
> [!NOTE]
> Muxing is only available on percentage-based handlers because it relies on their ability to represent progress as a relative fraction.
> If required, you can convert certain other handlers into percentage-based handlers using the `ToPercentageBased()` extension method.
#### Custom weights
A muxer input may be assigned a custom weight modifier, which determines its priority in relation to others.
Progress reported on an input with higher weight influences the aggregated progress to a greater degree and vice versa.
You can specify the input weight by passing it to the `CreateInput(...)` method:
```csharp
using Gress;
var progress = new Progress<Percentage>(p => /* ... */);
var muxer = progress.CreateMuxer();
var subProgress1 = muxer.CreateInput(1);
var subProgress2 = muxer.CreateInput(4);
// Weight split:
// Input 1 -> 20% of total
// Input 2 -> 80% of total
subProgress1.Report(Percentage.FromFraction(0.9));
subProgress2.Report(Percentage.FromFraction(0.3));
// Input 1 -> 90% (less important)
// Input 2 -> 30% (more important)
// Total -> 42% (would've been 60% without weights)
```
#### Auto-reset muxer
In some cases, you may need to report progress on an infinite workflow where new operations are started and completed in a continuous fashion.
This can be achieved by using an auto-reset muxer.
Inputs to an auto-reset muxer implement the `ICompletableProgress<T>` interface and are capable of reporting completion after all of the underlying work is finished.
Once all connected inputs report completion, they are disconnected from the muxer and the latter is reset back to the initial state.
To create an auto-reset muxer, call `WithAutoReset()` on an existing instance:
```csharp
using Gress;
using Gress.Completable;
var progress = new Progress<Percentage>(p => /* ... */);
var muxer = progress.CreateMuxer().WithAutoReset();
var subProgress1 = muxer.CreateInput(); // ICompletableProgress<Percentage>
var subProgress2 = muxer.CreateInput(); // ICompletableProgress<Percentage>
subProgress1.Report(Percentage.FromFraction(0.3));
subProgress2.Report(Percentage.FromFraction(0.9));
// Input 1 -> 30%
// Input 2 -> 90%
// Total -> 60%
subProgress1.Report(Percentage.FromFraction(1));
subProgress1.ReportCompletion();
// Input 1 -> 100% (completed)
// Input 2 -> 90%
// Total -> 95%
subProgress2.Report(Percentage.FromFraction(1));
subProgress2.ReportCompletion();
// All inputs disconnected
// Total -> 0%
var subProgress3 = muxer.CreateInput();
subProgress3.Report(Percentage.FromFraction(0.5));
// Input 3 -> 50%
// Total -> 50%
```
> [!NOTE]
> You can wrap an instance of `ICompletableProgress<T>` in a disposable container by calling `ToDisposable()`.
> This allows you to place the handler in a `using (...)` block, which ensures that the completion is always reported at the end.
### Integration extensions
**Gress** also provides extensions for several built-in .NET types that integrate `IProgress<Percentage>` into their existing APIs.
For example, you can use the below `Stream.CopyToAsync(...)` overload to copy data between two streams while tracking the operation's progress:
```csharp
using Gress;
using Gress.Integrations;
await using var source = File.OpenRead("input.bin");
await using var destination = File.Create("output.bin");
await source.CopyToAsync(
destination,
new Progress<Percentage>(p => Console.WriteLine($"Copied: {p}"))
);
// Console output:
// Copied: 37,5%
// Copied: 62,5%
// Copied: 87,5%
// Copied: 100,0%
```
================================================
FILE: global.json
================================================
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
gitextract_yhxilno4/ ├── .github/ │ ├── dependabot.yml │ ├── release.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Packages.props ├── Gress/ │ ├── Completable/ │ │ ├── AutoResetProgressMuxer.cs │ │ ├── CompletableProgressExtensions.cs │ │ ├── DelegateCompletableProgress.cs │ │ ├── DisposableCompletableProgress.cs │ │ └── ICompletableProgress.cs │ ├── DelegateProgress.cs │ ├── Gress.csproj │ ├── Integrations/ │ │ ├── HttpClientExtensions.cs │ │ ├── HttpContentExtensions.cs │ │ └── StreamExtensions.cs │ ├── Percentage.cs │ ├── ProgressCollector.cs │ ├── ProgressContainer.cs │ ├── ProgressExtensions.cs │ └── ProgressMuxer.cs ├── Gress.Demo/ │ ├── App.axaml │ ├── App.axaml.cs │ ├── Gress.Demo.csproj │ ├── Program.cs │ ├── Readme.md │ ├── ViewModels/ │ │ ├── MainViewModel.cs │ │ └── OperationViewModel.cs │ └── Views/ │ ├── MainWindow.axaml │ └── MainWindow.axaml.cs ├── Gress.Tests/ │ ├── CompositionSpecs.cs │ ├── Gress.Tests.csproj │ ├── IntegrationSpecs.cs │ ├── MuxingSpecs.cs │ ├── TerminalSpecs.cs │ └── xunit.runner.json ├── Gress.slnx ├── License.txt ├── NuGet.config ├── Readme.md └── global.json
SYMBOL INDEX (106 symbols across 23 files)
FILE: Gress.Demo/App.axaml.cs
class App (line 8) | public class App : Application
method Initialize (line 10) | public override void Initialize()
method OnFrameworkInitializationCompleted (line 17) | public override void OnFrameworkInitializationCompleted()
FILE: Gress.Demo/Program.cs
class Program (line 6) | public static class Program
method BuildAvaloniaApp (line 8) | public static AppBuilder BuildAvaloniaApp() =>
method Main (line 11) | [STAThread]
FILE: Gress.Demo/ViewModels/MainViewModel.cs
class MainViewModel (line 10) | public partial class MainViewModel : ObservableObject
method MainViewModel (line 14) | public MainViewModel() => _progressMuxer = Progress.CreateMuxer().With...
method PerformWorkAsync (line 21) | [RelayCommand(AllowConcurrentExecutions = true)]
FILE: Gress.Demo/ViewModels/OperationViewModel.cs
class OperationViewModel (line 5) | public class OperationViewModel(double weight) : ObservableObject
FILE: Gress.Demo/Views/MainWindow.axaml.cs
class MainWindow (line 5) | public partial class MainWindow : Window
method MainWindow (line 7) | public MainWindow() => InitializeComponent();
FILE: Gress.Tests/CompositionSpecs.cs
class CompositionSpecs (line 8) | public class CompositionSpecs
method I_can_transform_progress_updates_into_a_different_type (line 10) | [Fact]
method I_can_transform_progress_updates_within_the_same_type (line 28) | [Fact]
method I_can_filter_out_progress_updates_that_do_not_satisfy_a_predicate (line 45) | [Fact]
method I_can_filter_out_progress_updates_with_duplicate_values (line 65) | [Fact]
method I_can_filter_out_progress_updates_that_arrive_out_of_order (line 97) | [Fact]
method I_can_merge_two_progress_handlers_together (line 129) | [Fact]
method I_can_merge_multiple_progress_handlers_together (line 163) | [Fact]
method I_can_convert_a_double_based_progress_handler_into_a_percentage_based_handler_using_fraction_mapping (line 193) | [Fact]
method I_can_convert_a_double_based_progress_handler_into_a_percentage_based_handler_using_value_mapping (line 210) | [Fact]
method I_can_convert_an_integer_based_progress_handler_into_a_percentage_based_handler (line 227) | [Fact]
method I_can_convert_a_percentage_based_progress_handler_into_a_double_based_handler_using_fraction_mapping (line 244) | [Fact]
method I_can_convert_a_percentage_based_progress_handler_into_a_double_based_handler_using_value_mapping (line 268) | [Fact]
method I_can_convert_a_percentage_based_progress_handler_into_an_integer_based_handler (line 292) | [Fact]
method I_can_convert_a_normal_progress_handler_into_a_completable_handler (line 316) | [Fact]
FILE: Gress.Tests/IntegrationSpecs.cs
class IntegrationSpecs (line 13) | public class IntegrationSpecs
method I_can_copy_a_stream_to_another_stream_with_progress (line 15) | [Fact]
method I_can_download_a_web_resource_as_a_byte_array_with_progress (line 41) | [Fact]
method I_can_download_a_web_resource_as_a_string_with_progress (line 60) | [Fact]
method I_can_download_a_web_resource_to_a_file_with_progress (line 79) | [Fact]
FILE: Gress.Tests/MuxingSpecs.cs
class MuxingSpecs (line 7) | public class MuxingSpecs
method I_can_mux_progress_updates_from_a_single_operation (line 9) | [Fact]
method I_can_mux_progress_updates_from_multiple_operations (line 34) | [Fact]
method I_can_mux_progress_updates_from_multiple_operations_with_different_weights (line 61) | [Fact]
method I_can_mux_progress_updates_from_multiple_operations_with_auto_reset_behavior (line 88) | [Fact]
FILE: Gress.Tests/TerminalSpecs.cs
class TerminalSpecs (line 8) | public class TerminalSpecs
method I_can_route_progress_updates_into_a_collection (line 10) | [Fact]
method I_can_route_progress_updates_into_a_property (line 33) | [Fact]
method I_can_route_progress_updates_into_a_property_with_change_notifications (line 48) | [Fact]
FILE: Gress/Completable/AutoResetProgressMuxer.cs
class AutoResetProgressMuxer (line 10) | public partial class AutoResetProgressMuxer(ProgressMuxer muxer)
method CreateInput (line 27) | public ICompletableProgress<Percentage> CreateInput(double weight = 1.0)
class Item (line 43) | private class Item : ICompletableProgress<Percentage>
method Item (line 48) | public Item(AutoResetProgressMuxer parent, IProgress<Percentage> tar...
method Report (line 54) | public void Report(Percentage value)
method ReportCompletion (line 62) | public void ReportCompletion()
class AutoResetProgressMuxer (line 41) | public partial class AutoResetProgressMuxer
method CreateInput (line 27) | public ICompletableProgress<Percentage> CreateInput(double weight = 1.0)
class Item (line 43) | private class Item : ICompletableProgress<Percentage>
method Item (line 48) | public Item(AutoResetProgressMuxer parent, IProgress<Percentage> tar...
method Report (line 54) | public void Report(Percentage value)
method ReportCompletion (line 62) | public void ReportCompletion()
FILE: Gress/Completable/CompletableProgressExtensions.cs
class CompletableProgressExtensions (line 8) | public static class CompletableProgressExtensions
method ToCompletable (line 16) | public ICompletableProgress<T> ToCompletable(Action reportCompletion) =>
FILE: Gress/Completable/DelegateCompletableProgress.cs
class DelegateCompletableProgress (line 9) | public class DelegateCompletableProgress<T>(Action<T> report, Action rep...
method Report (line 13) | public void Report(T value) => report(value);
method ReportCompletion (line 16) | public void ReportCompletion() => reportCompletion();
FILE: Gress/Completable/DisposableCompletableProgress.cs
class DisposableCompletableProgress (line 9) | public class DisposableCompletableProgress<T>(ICompletableProgress<T> ta...
method Report (line 14) | public void Report(T value) => target.Report(value);
method ReportCompletion (line 17) | public void ReportCompletion() => target.ReportCompletion();
method Dispose (line 20) | public void Dispose() => ReportCompletion();
FILE: Gress/Completable/ICompletableProgress.cs
type ICompletableProgress (line 8) | public interface ICompletableProgress<in T> : IProgress<T>
method ReportCompletion (line 13) | void ReportCompletion();
FILE: Gress/DelegateProgress.cs
class DelegateProgress (line 13) | public class DelegateProgress<T>(Action<T> report) : IProgress<T>
method Report (line 16) | public void Report(T value) => report(value);
FILE: Gress/Integrations/HttpClientExtensions.cs
class HttpClientExtensions (line 13) | public static class HttpClientExtensions
method extension (line 16) | extension(HttpClient client)
FILE: Gress/Integrations/HttpContentExtensions.cs
class HttpContentExtensions (line 15) | public static class HttpContentExtensions
method extension (line 18) | extension(HttpContent content)
FILE: Gress/Integrations/StreamExtensions.cs
class StreamExtensions (line 13) | public static class StreamExtensions
method extension (line 16) | extension(Stream source)
FILE: Gress/Percentage.cs
type Percentage (line 8) | public readonly partial struct Percentage(double value)
method ToString (line 23) | public string ToString(IFormatProvider? formatProvider) =>
method ToString (line 27) | public override string ToString() => ToString(null);
method FromValue (line 35) | public static Percentage FromValue(double value) => new(value);
method FromFraction (line 40) | public static Percentage FromFraction(double fraction) => FromValue(fr...
method CompareTo (line 46) | public int CompareTo(Percentage other) => Value.CompareTo(other.Value);
method Equals (line 49) | public bool Equals(Percentage other) => Value.CompareTo(other.Value) =...
method Equals (line 52) | public override bool Equals(object? obj) => obj is Percentage other &&...
method GetHashCode (line 55) | public override int GetHashCode() => Value.GetHashCode();
method ToString (line 80) | string IFormattable.ToString(string? format, IFormatProvider? formatPr...
type Percentage (line 30) | public partial struct Percentage
method ToString (line 23) | public string ToString(IFormatProvider? formatProvider) =>
method ToString (line 27) | public override string ToString() => ToString(null);
method FromValue (line 35) | public static Percentage FromValue(double value) => new(value);
method FromFraction (line 40) | public static Percentage FromFraction(double fraction) => FromValue(fr...
method CompareTo (line 46) | public int CompareTo(Percentage other) => Value.CompareTo(other.Value);
method Equals (line 49) | public bool Equals(Percentage other) => Value.CompareTo(other.Value) =...
method Equals (line 52) | public override bool Equals(object? obj) => obj is Percentage other &&...
method GetHashCode (line 55) | public override int GetHashCode() => Value.GetHashCode();
method ToString (line 80) | string IFormattable.ToString(string? format, IFormatProvider? formatPr...
type Percentage (line 43) | public partial struct Percentage : IEquatable<Percentage>, IComparable<P...
method ToString (line 23) | public string ToString(IFormatProvider? formatProvider) =>
method ToString (line 27) | public override string ToString() => ToString(null);
method FromValue (line 35) | public static Percentage FromValue(double value) => new(value);
method FromFraction (line 40) | public static Percentage FromFraction(double fraction) => FromValue(fr...
method CompareTo (line 46) | public int CompareTo(Percentage other) => Value.CompareTo(other.Value);
method Equals (line 49) | public bool Equals(Percentage other) => Value.CompareTo(other.Value) =...
method Equals (line 52) | public override bool Equals(object? obj) => obj is Percentage other &&...
method GetHashCode (line 55) | public override int GetHashCode() => Value.GetHashCode();
method ToString (line 80) | string IFormattable.ToString(string? format, IFormatProvider? formatPr...
type Percentage (line 78) | public partial struct Percentage : IFormattable
method ToString (line 23) | public string ToString(IFormatProvider? formatProvider) =>
method ToString (line 27) | public override string ToString() => ToString(null);
method FromValue (line 35) | public static Percentage FromValue(double value) => new(value);
method FromFraction (line 40) | public static Percentage FromFraction(double fraction) => FromValue(fr...
method CompareTo (line 46) | public int CompareTo(Percentage other) => Value.CompareTo(other.Value);
method Equals (line 49) | public bool Equals(Percentage other) => Value.CompareTo(other.Value) =...
method Equals (line 52) | public override bool Equals(object? obj) => obj is Percentage other &&...
method GetHashCode (line 55) | public override int GetHashCode() => Value.GetHashCode();
method ToString (line 80) | string IFormattable.ToString(string? format, IFormatProvider? formatPr...
FILE: Gress/ProgressCollector.cs
class ProgressCollector (line 10) | public class ProgressCollector<T> : IProgress<T>
method Reset (line 18) | public void Reset()
method GetValues (line 27) | public IReadOnlyList<T> GetValues()
method Report (line 34) | public void Report(T value)
FILE: Gress/ProgressContainer.cs
class ProgressContainer (line 11) | public partial class ProgressContainer<T>(T initial) : IProgress<T>
method ProgressContainer (line 21) | public ProgressContainer()
method Report (line 45) | public void Report(T value) => Current = value;
method OnPropertyChanged (line 60) | private void OnPropertyChanged([CallerMemberName] string? propertyName...
class ProgressContainer (line 48) | public partial class ProgressContainer<T> : INotifyPropertyChanged
method ProgressContainer (line 21) | public ProgressContainer()
method Report (line 45) | public void Report(T value) => Current = value;
method OnPropertyChanged (line 60) | private void OnPropertyChanged([CallerMemberName] string? propertyName...
FILE: Gress/ProgressExtensions.cs
class ProgressExtensions (line 11) | public static class ProgressExtensions
method WithTransform (line 19) | public IProgress<TTransformed> WithTransform<TTransformed>(Func<TTrans...
method WithTransform (line 25) | public IProgress<T> WithTransform(Func<T, T> map) => progress.WithTran...
method WithFilter (line 30) | public IProgress<T> WithFilter(Func<T, bool> shouldReport) =>
method WithDeduplication (line 40) | public IProgress<T> WithDeduplication<TKey>(
method WithDeduplication (line 72) | public IProgress<T> WithDeduplication(IEqualityComparer<T>? comparer =...
method WithOrdering (line 78) | public IProgress<T> WithOrdering(IComparer<T>? comparer = null)
method Merge (line 105) | public IProgress<T> Merge(IProgress<T> otherProgress) =>
method ToPercentageBased (line 115) | public IProgress<Percentage> ToPercentageBased(Func<Percentage, T> map...
FILE: Gress/ProgressMuxer.cs
class ProgressMuxer (line 10) | public partial class ProgressMuxer(IProgress<Percentage> target)
method ReportAggregatedProgress (line 17) | private void ReportAggregatedProgress()
method CreateInput (line 42) | public IProgress<Percentage> CreateInput(double weight = 1.0)
method Reset (line 65) | public void Reset()
class Input (line 79) | private class Input : IProgress<Percentage>
method Input (line 87) | public Input(ProgressMuxer parent, double weight)
method Report (line 93) | public void Report(Percentage value)
class ProgressMuxer (line 77) | public partial class ProgressMuxer
method ReportAggregatedProgress (line 17) | private void ReportAggregatedProgress()
method CreateInput (line 42) | public IProgress<Percentage> CreateInput(double weight = 1.0)
method Reset (line 65) | public void Reset()
class Input (line 79) | private class Input : IProgress<Percentage>
method Input (line 87) | public Input(ProgressMuxer parent, double weight)
method Report (line 93) | public void Report(Percentage value)
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (88K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 385,
"preview": "version: 2\nupdates:\n - package-ecosystem: github-actions\n directory: \"/\"\n schedule:\n interval: monthly\n l"
},
{
"path": ".github/release.yml",
"chars": 250,
"preview": "changelog:\n exclude:\n authors:\n - dependabot\n - dependabot[bot]\n\n categories:\n - title: Enhancements\n "
},
{
"path": ".github/workflows/main.yml",
"chars": 885,
"preview": "name: main\n\non:\n workflow_dispatch:\n inputs:\n package-version:\n type: string\n description: Packag"
},
{
"path": ".gitignore",
"chars": 102,
"preview": "# 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",
"chars": 1492,
"preview": "<Project>\n <PropertyGroup>\n <Version>0.0.0-dev</Version>\n <Company>Tyrrrz</Company>\n <Copyright>Copyright (C) "
},
{
"path": "Directory.Packages.props",
"chars": 1169,
"preview": "<Project>\n <PropertyGroup>\n <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n </PropertyGroup>"
},
{
"path": "Gress/Completable/AutoResetProgressMuxer.cs",
"chars": 2113,
"preview": "using System;\nusing System.Threading;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Aggregates multiple progress upda"
},
{
"path": "Gress/Completable/CompletableProgressExtensions.cs",
"chars": 1381,
"preview": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Extensions for <see cref=\"CompletableProgressExtensions\" "
},
{
"path": "Gress/Completable/DelegateCompletableProgress.cs",
"chars": 505,
"preview": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Simple implementation of <see cref=\"ICompletableProgress{"
},
{
"path": "Gress/Completable/DisposableCompletableProgress.cs",
"chars": 581,
"preview": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Convenience wrapper for <see cref=\"ICompletableProgress{T"
},
{
"path": "Gress/Completable/ICompletableProgress.cs",
"chars": 312,
"preview": "using System;\n\nnamespace Gress.Completable;\n\n/// <summary>\n/// Progress handler with explicit completion feedback.\n/// <"
},
{
"path": "Gress/DelegateProgress.cs",
"chars": 515,
"preview": "using System;\n\nnamespace Gress;\n\n/// <summary>\n/// Simple implementation of <see cref=\"IProgress{T}\" /> that uses the pr"
},
{
"path": "Gress/Gress.csproj",
"chars": 969,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <TargetFrameworks>netstandard2.0;net6.0;net7.0;net10.0</TargetFr"
},
{
"path": "Gress/Integrations/HttpClientExtensions.cs",
"chars": 4106,
"preview": "using System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Gress.Integrations;\nusi"
},
{
"path": "Gress/Integrations/HttpContentExtensions.cs",
"chars": 2944,
"preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusin"
},
{
"path": "Gress/Integrations/StreamExtensions.cs",
"chars": 1606,
"preview": "using System;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Gress.Integrations;\nusing Pow"
},
{
"path": "Gress/Percentage.cs",
"chars": 2430,
"preview": "using System;\n\nnamespace Gress;\n\n/// <summary>\n/// Unit of progress.\n/// </summary>\npublic readonly partial struct Perce"
},
{
"path": "Gress/ProgressCollector.cs",
"chars": 886,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\n\nnamespace Gress;\n\n/// <summary>\n/// Terminal pr"
},
{
"path": "Gress/ProgressContainer.cs",
"chars": 1920,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Runtime.CompilerServices;\n\nnam"
},
{
"path": "Gress/ProgressExtensions.cs",
"chars": 6729,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing PowerKit;\n\nnamespace Gress;\n\n/// <summary>"
},
{
"path": "Gress/ProgressMuxer.cs",
"chars": 3095,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\n\nnamespace Gress;\n\n/// <summary>\n/// Aggregates "
},
{
"path": "Gress.Demo/App.axaml",
"chars": 233,
"preview": "<Application\n x:Class=\"Gress.Demo.App\"\n xmlns=\"https://github.com/avaloniaui\"\n xmlns:x=\"http://schemas.microsof"
},
{
"path": "Gress.Demo/App.axaml.cs",
"chars": 568,
"preview": "using Avalonia;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.Markup.Xaml;\nusing Gress.Demo.Views;\n\nnames"
},
{
"path": "Gress.Demo/Gress.Demo.csproj",
"chars": 959,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <OutputType>WinExe</OutputType>\n <TargetFramework>net10.0</Ta"
},
{
"path": "Gress.Demo/Program.cs",
"chars": 351,
"preview": "using System;\nusing Avalonia;\n\nnamespace Gress.Demo;\n\npublic static class Program\n{\n public static AppBuilder BuildAv"
},
{
"path": "Gress.Demo/Readme.md",
"chars": 132,
"preview": "# Gress Avalonia demo\n\nThis demo features a simple Avalonia application that lets you start new operations and track the"
},
{
"path": "Gress.Demo/ViewModels/MainViewModel.cs",
"chars": 1342,
"preview": "using System;\nusing System.Collections.ObjectModel;\nusing System.Threading.Tasks;\nusing CommunityToolkit.Mvvm.ComponentM"
},
{
"path": "Gress.Demo/ViewModels/OperationViewModel.cs",
"chars": 262,
"preview": "using CommunityToolkit.Mvvm.ComponentModel;\n\nnamespace Gress.Demo.ViewModels;\n\npublic class OperationViewModel(double we"
},
{
"path": "Gress.Demo/Views/MainWindow.axaml",
"chars": 4813,
"preview": "<Window\n x:Class=\"Gress.Demo.Views.MainWindow\"\n xmlns=\"https://github.com/avaloniaui\"\n xmlns:x=\"http://schemas."
},
{
"path": "Gress.Demo/Views/MainWindow.axaml.cs",
"chars": 150,
"preview": "using Avalonia.Controls;\n\nnamespace Gress.Demo.Views;\n\npublic partial class MainWindow : Window\n{\n public MainWindow("
},
{
"path": "Gress.Tests/CompositionSpecs.cs",
"chars": 9735,
"preview": "using System.Linq;\nusing FluentAssertions;\nusing Gress.Completable;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npublic class C"
},
{
"path": "Gress.Tests/Gress.Tests.csproj",
"chars": 881,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <TargetFramework>net10.0</TargetFramework>\n </PropertyGroup>\n\n "
},
{
"path": "Gress.Tests/IntegrationSpecs.cs",
"chars": 2917,
"preview": "using System;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing FluentAssertions;\nusing Gress."
},
{
"path": "Gress.Tests/MuxingSpecs.cs",
"chars": 4178,
"preview": "using FluentAssertions;\nusing Gress.Completable;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npublic class MuxingSpecs\n{\n [F"
},
{
"path": "Gress.Tests/TerminalSpecs.cs",
"chars": 1794,
"preview": "using System.ComponentModel;\nusing System.Threading;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace Gress.Tests;\n\npubli"
},
{
"path": "Gress.Tests/xunit.runner.json",
"chars": 138,
"preview": "{\n \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n \"methodDisplayOptions\": \"all\",\n \"methodDi"
},
{
"path": "Gress.slnx",
"chars": 417,
"preview": "<Solution>\n <Folder Name=\"/Misc/\">\n <File Path=\"Directory.Build.props\" />\n <File Path=\"Directory.Packages.props\" "
},
{
"path": "License.txt",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2019-2026 Oleksii Holub\n\nPermission is hereby granted, free of charge, to any person obtainin"
},
{
"path": "NuGet.config",
"chars": 320,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n <packageSources>\n <clear />\n <add key=\"nuget.org\" value=\""
},
{
"path": "Readme.md",
"chars": 18170,
"preview": "# Gress\n\n[](https://github.com/Tyrrrz/.github/blob/prime"
},
{
"path": "global.json",
"chars": 80,
"preview": "{\n \"sdk\": {\n \"version\": \"10.0.100\",\n \"rollForward\": \"latestFeature\"\n }\n}"
}
]
About this extraction
This page contains the full source code of the Tyrrrz/Gress GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (80.9 KB), approximately 18.9k tokens, and a symbol index with 106 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.