diff --git a/.github/workflows/BuildDeploy.yml b/.github/workflows/BuildDeploy.yml
new file mode 100644
index 0000000..f396595
--- /dev/null
+++ b/.github/workflows/BuildDeploy.yml
@@ -0,0 +1,47 @@
+# ------------------------------------------------------------------------------
+#
+#
+# This code was generated.
+#
+# - To turn off auto-generation set:
+#
+# [GitHubActions (AutoGenerate = false)]
+#
+# - To trigger manual generation invoke:
+#
+# nuke --generate-configuration GitHubActions_BuildDeploy --host GitHubActions
+#
+#
+# ------------------------------------------------------------------------------
+
+name: BuildDeploy
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ windows-latest:
+ name: windows-latest
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: 'Cache: .nuke/temp, ~/.nuget/packages'
+ uses: actions/cache@v3
+ with:
+ path: |
+ .nuke/temp
+ ~/.nuget/packages
+ key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
+ - name: 'Run: Compile, Deploy'
+ run: ./build.cmd Compile Deploy
+ env:
+ NuGetApiKey: ${{ secrets.NUGET_API_KEY }}
+ - name: 'Publish: output'
+ uses: actions/upload-artifact@v3
+ with:
+ name: output
+ path: output
diff --git a/.github/workflows/BuildOnly.yml b/.github/workflows/BuildOnly.yml
new file mode 100644
index 0000000..86be818
--- /dev/null
+++ b/.github/workflows/BuildOnly.yml
@@ -0,0 +1,40 @@
+# ------------------------------------------------------------------------------
+#
+#
+# This code was generated.
+#
+# - To turn off auto-generation set:
+#
+# [GitHubActions (AutoGenerate = false)]
+#
+# - To trigger manual generation invoke:
+#
+# nuke --generate-configuration GitHubActions_BuildOnly --host GitHubActions
+#
+#
+# ------------------------------------------------------------------------------
+
+name: BuildOnly
+
+on:
+ push:
+ branches-ignore:
+ - main
+
+jobs:
+ windows-latest:
+ name: windows-latest
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: 'Cache: .nuke/temp, ~/.nuget/packages'
+ uses: actions/cache@v3
+ with:
+ path: |
+ .nuke/temp
+ ~/.nuget/packages
+ key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
+ - name: 'Run: Compile'
+ run: ./build.cmd Compile
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
new file mode 100644
index 0000000..4d608a6
--- /dev/null
+++ b/.nuke/build.schema.json
@@ -0,0 +1,120 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/build",
+ "title": "Build Schema",
+ "definitions": {
+ "build": {
+ "type": "object",
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "Continue": {
+ "type": "boolean",
+ "description": "Indicates to continue a previously failed build attempt"
+ },
+ "Help": {
+ "type": "boolean",
+ "description": "Shows the help text for this build assembly"
+ },
+ "Host": {
+ "type": "string",
+ "description": "Host for execution. Default is 'automatic'",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitbucket",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "Rider",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI",
+ "VisualStudio",
+ "VSCode"
+ ]
+ },
+ "NoLogo": {
+ "type": "boolean",
+ "description": "Disables displaying the NUKE logo"
+ },
+ "NuGetApiKey": {
+ "type": "string",
+ "default": "Secrets must be entered via 'nuke :secrets [profile]'"
+ },
+ "Partition": {
+ "type": "string",
+ "description": "Partition to use on CI"
+ },
+ "Plan": {
+ "type": "boolean",
+ "description": "Shows the execution plan (HTML)"
+ },
+ "Profile": {
+ "type": "array",
+ "description": "Defines the profiles to load",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Root": {
+ "type": "string",
+ "description": "Root directory during build execution"
+ },
+ "Skip": {
+ "type": "array",
+ "description": "List of targets to be skipped. Empty list skips all dependencies",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "Deploy",
+ "Pack",
+ "Print",
+ "Restore"
+ ]
+ }
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ },
+ "Target": {
+ "type": "array",
+ "description": "List of targets to be invoked. Default is '{default_target}'",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "Deploy",
+ "Pack",
+ "Print",
+ "Restore"
+ ]
+ }
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "Logging verbosity during build execution. Default is 'Normal'",
+ "enum": [
+ "Minimal",
+ "Normal",
+ "Quiet",
+ "Verbose"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/LICENSE b/LICENSE
index 88ac044..5d94e67 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,13 @@
MIT License
+A large portion of this code is based on the work of the following people:
+Copyright (c) 2020 Patrik Svensson, Phil Scott, Nils Andresen
+
+The original code can be found at https://github.com/spectreconsole/spectre.console
+
+The derivitive work is as follows:
+Added support for Reactive Extensions (Rx) with a scheduler to make things work as expected.
+
Copyright (c) 2023 Chris Pulman
Permission is hereby granted, free of charge, to any person obtaining a copy
diff --git a/README.md b/README.md
index 3e995a2..be1d2c9 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,221 @@
+![Alt](https://repobeats.axiom.co/api/embed/a6232388fa6d7a8d647834b31d9d87a88d046ef9.svg "Repobeats analytics image")
+
# Spectre.Console.Rx
-A reactive extension of Spectre.Console Live, Progress, and Status
+A reactive version of Spectre.Console Live, Progress, and Status
+
+visit https://spectreconsole.net/ for more information on Spectre.Console
+visit https://github.com/spectreconsole/spectre.console for the original source code of Spectre.Console
+
+## Usage
+
+An example of using Spectre.Console.Rx to animate some status, a table and a progress bar.
+
+```csharp
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using ProgressDemo;
+using Spectre.Console.Rx;
+
+// NOTE: This code executes synchronously, only one instance of the console context can be executing at a time.
+// This is because the console context is not thread safe.
+// Remember to call IsFinished() on the console context when you're done with it.
+var table = new Table().LeftAligned();
+
+AnsiConsoleRx
+ .Status(
+ "[yellow]Initializing warp drive[/]",
+ p => p.AutoRefresh(true).Spinner(Spinner.Known.Default))
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Subscribe(
+ ctx => ctx.Schedule(async scheduler =>
+ {
+ // Initialize
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ WriteLogMessage("Starting gravimetric field displacement manifold");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(1000));
+ WriteLogMessage("Warming up deuterium chamber");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(2000));
+ WriteLogMessage("Generating antideuterium");
+
+ // Warp nacelles
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.BouncingBar);
+ ctx.Status("[bold blue]Unfolding warp nacelles[/]");
+ WriteLogMessage("Unfolding left warp nacelle");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(2000));
+ WriteLogMessage("Left warp nacelle [green]online[/]");
+ WriteLogMessage("Unfolding right warp nacelle");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(1000));
+ WriteLogMessage("Right warp nacelle [green]online[/]");
+
+ // Warp bubble
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.Star2);
+ ctx.Status("[bold blue]Generating warp bubble[/]");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.Star);
+ ctx.Status("[bold blue]Stabilizing warp bubble[/]");
+
+ // Safety
+ ctx.Spinner(Spinner.Known.Monkey);
+ ctx.Status("[bold blue]Performing safety checks[/]");
+ WriteLogMessage("Enabling interior dampening");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(2000));
+ WriteLogMessage("Interior dampening [green]enabled[/]");
+
+ // Warp!
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.Moon);
+ WriteLogMessage("Preparing for warp");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(1000));
+ for (var warp = 1; warp < 10; warp++)
+ {
+ ctx.Status($"[bold blue]Warp {warp}[/]");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(500));
+ }
+
+ // Done
+ WriteLogMessage("[bold green]Crusing at Warp 9[/]");
+
+ // Finish animation and allow next animation to start
+ ctx.IsFinished();
+ }));
+
+// Animate
+AnsiConsoleRx.Live(table, ld => ld.AutoClear(false).Overflow(VerticalOverflow.Ellipsis).Cropping(VerticalOverflowCropping.Top))
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Subscribe(
+ ctx =>
+
+ // Columns
+ ctx.Update(230, () => table.AddColumn("Release date"))
+ .Update(230, () => table.AddColumn("Title"))
+ .Update(230, () => table.AddColumn("Budget"))
+ .Update(230, () => table.AddColumn("Opening Weekend"))
+ .Update(230, () => table.AddColumn("Box office"))
+
+ // Rows
+ .Update(70, () => table.AddRow("May 25, 1977", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IV[/]", "$11,000,000", "$1,554,475", "$775,398,007"))
+ .Update(70, () => table.AddRow("May 21, 1980", "[yellow]Star Wars[/] [grey]Ep.[/] [u]V[/]", "$18,000,000", "$4,910,483", "$547,969,004"))
+ .Update(70, () => table.AddRow("May 25, 1983", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VI[/]", "$32,500,000", "$23,019,618", "$475,106,177"))
+ .Update(70, () => table.AddRow("May 19, 1999", "[yellow]Star Wars[/] [grey]Ep.[/] [u]I[/]", "$115,000,000", "$64,810,870", "$1,027,044,677"))
+ .Update(70, () => table.AddRow("May 16, 2002", "[yellow]Star Wars[/] [grey]Ep.[/] [u]II[/]", "$115,000,000", "$80,027,814", "$649,436,358"))
+ .Update(70, () => table.AddRow("May 19, 2005", "[yellow]Star Wars[/] [grey]Ep.[/] [u]III[/]", "$113,000,000", "$108,435,841", "$850,035,635"))
+ .Update(70, () => table.AddRow("Dec 18, 2015", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VII[/]", "$245,000,000", "$247,966,675", "$2,068,223,624"))
+ .Update(70, () => table.AddRow("Dec 15, 2017", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VIII[/]", "$317,000,000", "$220,009,584", "$1,333,539,889"))
+ .Update(70, () => table.AddRow("Dec 20, 2019", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IX[/]", "$245,000,000", "$177,383,864", "$1,074,114,248"))
+
+ // Column footer
+ .Update(230, () => table.Columns[2].Footer("$1,633,000,000"))
+ .Update(230, () => table.Columns[3].Footer("$928,119,224"))
+ .Update(400, () => table.Columns[4].Footer("$10,318,030,576"))
+
+ // Column alignment
+ .Update(230, () => table.Columns[2].RightAligned())
+ .Update(230, () => table.Columns[3].RightAligned())
+ .Update(400, () => table.Columns[4].RightAligned())
+
+ // Column titles
+ .Update(70, () => table.Columns[0].Header("[bold]Release date[/]"))
+ .Update(70, () => table.Columns[1].Header("[bold]Title[/]"))
+ .Update(70, () => table.Columns[2].Header("[red bold]Budget[/]"))
+ .Update(70, () => table.Columns[3].Header("[green bold]Opening Weekend[/]"))
+ .Update(400, () => table.Columns[4].Header("[blue bold]Box office[/]"))
+
+ // Footers
+ .Update(70, () => table.Columns[2].Footer("[red bold]$1,633,000,000[/]"))
+ .Update(70, () => table.Columns[3].Footer("[green bold]$928,119,224[/]"))
+ .Update(400, () => table.Columns[4].Footer("[blue bold]$10,318,030,576[/]"))
+
+ // Title
+ .Update(500, () => table.Title("Star Wars Movies"))
+ .Update(400, () => table.Title("[[ [yellow]Star Wars Movies[/] ]]"))
+
+ // Borders
+ .Update(230, () => table.BorderColor(Color.Yellow))
+ .Update(230, () => table.MinimalBorder())
+ .Update(230, () => table.SimpleBorder())
+ .Update(230, () => table.SimpleHeavyBorder())
+
+ // Caption
+ .Update(400, () => table.Caption("[[ [blue]THE END[/] ]]"))
+
+ // Finish animation and allow next animation to start
+ .IsFinished());
+
+AnsiConsole.MarkupLine("[yellow]Initializing warp drive[/]...");
+
+// Show progress
+AnsiConsoleRx.Progress(p =>
+ p.AutoClear(false)
+ .Columns(new ProgressColumn[]
+ {
+ new TaskDescriptionColumn(), // Task description
+ new ProgressBarColumn(), // Progress bar
+ new PercentageColumn(), // Percentage
+ new RemainingTimeColumn(), // Remaining time
+ new SpinnerColumn(), // Spinner
+ }))
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Subscribe(async ctx =>
+ {
+ var random = new Random(DateTime.Now.Millisecond);
+
+ // Create some tasks
+ var tasks = CreateTasks(ctx, random);
+ var warpTask = ctx.AddTask("Going to warp", autoStart: false).IsIndeterminate();
+
+ await ctx.Schedule(
+ TimeSpan.FromMilliseconds(100),
+ () => ctx.IsFinished,
+ () =>
+ {
+ // Increment progress
+ foreach (var (task, increment) in tasks)
+ {
+ task.Increment(random.NextDouble() * increment);
+ }
+
+ // Write some random things to the terminal
+ if (random.NextDouble() < 0.1)
+ {
+ GenerateLogMessage();
+ }
+ });
+
+ // Now start the "warp" task
+ warpTask.StartTask().IsIndeterminate(false);
+ await ctx.Schedule(
+ TimeSpan.FromMilliseconds(100),
+ () => warpTask.Increment(12 * random.NextDouble()));
+
+ // Done
+ await ctx.Schedule(_ => WriteLogMessage("[green]Done![/]"));
+
+ // Progress is finished and will automatically exit
+ });
+
+WriteLogMessage("[red]Press ctrl+c to exit[/]");
+
+static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext context, Random random)
+{
+ var tasks = new List<(ProgressTask, int)>();
+ while (tasks.Count < 5)
+ {
+ if (DescriptionGenerator.TryGenerate(out var name))
+ {
+ tasks.Add((context.AddTask(name), random.Next(2, 10)));
+ }
+ }
+
+ return tasks;
+}
+
+static void GenerateLogMessage() => AnsiConsole.MarkupLine(
+ "[grey]LOG:[/] " +
+ DescriptionGenerator.Generate() +
+ "[grey]...[/]");
+
+static void WriteLogMessage(string message) => AnsiConsole.MarkupLine($"[grey]LOG:[/] {message}[grey]...[/]");
+
+```
diff --git a/build/Build.cs b/build/Build.cs
index 66031d4..82d8096 100644
--- a/build/Build.cs
+++ b/build/Build.cs
@@ -56,7 +56,6 @@ partial class Build : NukeBuild
}
PackagesDirectory.CreateOrCleanDirectory();
- ////await this.UpdateVisualStudio();
await this.InstallDotNetSdk("3.1.x", "5.x.x", "6.x.x", "7.x.x");
});
@@ -65,8 +64,6 @@ partial class Build : NukeBuild
.Executes(() =>
{
DotNetRestore(s => s.SetProjectFile(Solution));
-
- Solution.RestoreSolutionWorkloads();
});
Target Compile => _ => _
diff --git a/src/Shared/Shared.csproj b/src/CombinedExample/CombinedExample.csproj
similarity index 66%
rename from src/Shared/Shared.csproj
rename to src/CombinedExample/CombinedExample.csproj
index 3104dc0..4e225c1 100644
--- a/src/Shared/Shared.csproj
+++ b/src/CombinedExample/CombinedExample.csproj
@@ -1,15 +1,12 @@
+ Exe
net7.0
+ enable
enable
- False
-
-
-
-
diff --git a/src/CombinedExample/DescriptionGenerator.cs b/src/CombinedExample/DescriptionGenerator.cs
new file mode 100644
index 0000000..0d60e6f
--- /dev/null
+++ b/src/CombinedExample/DescriptionGenerator.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Chris Pulman. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace ProgressDemo;
+
+///
+/// DescriptionGenerator.
+///
+public static class DescriptionGenerator
+{
+ private static readonly string[] _verbs = new[] { "Downloading", "Rerouting", "Retriculating", "Collapsing", "Folding", "Solving", "Colliding", "Measuring" };
+ private static readonly string[] _nouns = new[] { "internet", "splines", "space", "capacitators", "quarks", "algorithms", "data structures", "spacetime" };
+
+ private static readonly Random _random;
+ private static readonly HashSet _used;
+
+ static DescriptionGenerator()
+ {
+ _random = new Random(DateTime.Now.Millisecond);
+ _used = new HashSet();
+ }
+
+ ///
+ /// Tries the generate.
+ ///
+ /// The name.
+ /// A bool.
+ public static bool TryGenerate(out string name)
+ {
+ for (var iterations = 0; iterations < 25; iterations++)
+ {
+ name = Generate();
+ if (!_used.Contains(name))
+ {
+ _used.Add(name);
+ return true;
+ }
+ }
+
+ name = Generate();
+ return false;
+ }
+
+ ///
+ /// Generates this instance.
+ ///
+ /// A string.
+ public static string Generate() =>
+ _verbs[_random.Next(0, _verbs.Length)]
+ + " "
+ + _nouns[_random.Next(0, _nouns.Length)];
+}
diff --git a/src/CombinedExample/Program.cs b/src/CombinedExample/Program.cs
new file mode 100644
index 0000000..071b4fb
--- /dev/null
+++ b/src/CombinedExample/Program.cs
@@ -0,0 +1,209 @@
+// Copyright (c) Chris Pulman. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using ProgressDemo;
+using Spectre.Console.Rx;
+
+// NOTE: This code executes synchronously, only one instance of the console context can be executing at a time.
+// This is because the console context is not thread safe.
+// Remember to call IsFinished() on the console context when you're done with it.
+var table = new Table().LeftAligned();
+
+AnsiConsoleRx
+ .Status(
+ "[yellow]Initializing warp drive[/]",
+ p => p.AutoRefresh(true).Spinner(Spinner.Known.Default))
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Subscribe(
+ ctx => ctx.Schedule(async scheduler =>
+ {
+ // Initialize
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ WriteLogMessage("Starting gravimetric field displacement manifold");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(1000));
+ WriteLogMessage("Warming up deuterium chamber");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(2000));
+ WriteLogMessage("Generating antideuterium");
+
+ // Warp nacelles
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.BouncingBar);
+ ctx.Status("[bold blue]Unfolding warp nacelles[/]");
+ WriteLogMessage("Unfolding left warp nacelle");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(2000));
+ WriteLogMessage("Left warp nacelle [green]online[/]");
+ WriteLogMessage("Unfolding right warp nacelle");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(1000));
+ WriteLogMessage("Right warp nacelle [green]online[/]");
+
+ // Warp bubble
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.Star2);
+ ctx.Status("[bold blue]Generating warp bubble[/]");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.Star);
+ ctx.Status("[bold blue]Stabilizing warp bubble[/]");
+
+ // Safety
+ ctx.Spinner(Spinner.Known.Monkey);
+ ctx.Status("[bold blue]Performing safety checks[/]");
+ WriteLogMessage("Enabling interior dampening");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(2000));
+ WriteLogMessage("Interior dampening [green]enabled[/]");
+
+ // Warp!
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(3000));
+ ctx.Spinner(Spinner.Known.Moon);
+ WriteLogMessage("Preparing for warp");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(1000));
+ for (var warp = 1; warp < 10; warp++)
+ {
+ ctx.Status($"[bold blue]Warp {warp}[/]");
+ await scheduler.Sleep(TimeSpan.FromMilliseconds(500));
+ }
+
+ // Done
+ WriteLogMessage("[bold green]Crusing at Warp 9[/]");
+
+ // Finish animation and allow next animation to start
+ ctx.IsFinished();
+ }));
+
+// Animate
+AnsiConsoleRx.Live(table, ld => ld.AutoClear(false).Overflow(VerticalOverflow.Ellipsis).Cropping(VerticalOverflowCropping.Top))
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Subscribe(
+ ctx =>
+
+ // Columns
+ ctx.Update(230, () => table.AddColumn("Release date"))
+ .Update(230, () => table.AddColumn("Title"))
+ .Update(230, () => table.AddColumn("Budget"))
+ .Update(230, () => table.AddColumn("Opening Weekend"))
+ .Update(230, () => table.AddColumn("Box office"))
+
+ // Rows
+ .Update(70, () => table.AddRow("May 25, 1977", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IV[/]", "$11,000,000", "$1,554,475", "$775,398,007"))
+ .Update(70, () => table.AddRow("May 21, 1980", "[yellow]Star Wars[/] [grey]Ep.[/] [u]V[/]", "$18,000,000", "$4,910,483", "$547,969,004"))
+ .Update(70, () => table.AddRow("May 25, 1983", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VI[/]", "$32,500,000", "$23,019,618", "$475,106,177"))
+ .Update(70, () => table.AddRow("May 19, 1999", "[yellow]Star Wars[/] [grey]Ep.[/] [u]I[/]", "$115,000,000", "$64,810,870", "$1,027,044,677"))
+ .Update(70, () => table.AddRow("May 16, 2002", "[yellow]Star Wars[/] [grey]Ep.[/] [u]II[/]", "$115,000,000", "$80,027,814", "$649,436,358"))
+ .Update(70, () => table.AddRow("May 19, 2005", "[yellow]Star Wars[/] [grey]Ep.[/] [u]III[/]", "$113,000,000", "$108,435,841", "$850,035,635"))
+ .Update(70, () => table.AddRow("Dec 18, 2015", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VII[/]", "$245,000,000", "$247,966,675", "$2,068,223,624"))
+ .Update(70, () => table.AddRow("Dec 15, 2017", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VIII[/]", "$317,000,000", "$220,009,584", "$1,333,539,889"))
+ .Update(70, () => table.AddRow("Dec 20, 2019", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IX[/]", "$245,000,000", "$177,383,864", "$1,074,114,248"))
+
+ // Column footer
+ .Update(230, () => table.Columns[2].Footer("$1,633,000,000"))
+ .Update(230, () => table.Columns[3].Footer("$928,119,224"))
+ .Update(400, () => table.Columns[4].Footer("$10,318,030,576"))
+
+ // Column alignment
+ .Update(230, () => table.Columns[2].RightAligned())
+ .Update(230, () => table.Columns[3].RightAligned())
+ .Update(400, () => table.Columns[4].RightAligned())
+
+ // Column titles
+ .Update(70, () => table.Columns[0].Header("[bold]Release date[/]"))
+ .Update(70, () => table.Columns[1].Header("[bold]Title[/]"))
+ .Update(70, () => table.Columns[2].Header("[red bold]Budget[/]"))
+ .Update(70, () => table.Columns[3].Header("[green bold]Opening Weekend[/]"))
+ .Update(400, () => table.Columns[4].Header("[blue bold]Box office[/]"))
+
+ // Footers
+ .Update(70, () => table.Columns[2].Footer("[red bold]$1,633,000,000[/]"))
+ .Update(70, () => table.Columns[3].Footer("[green bold]$928,119,224[/]"))
+ .Update(400, () => table.Columns[4].Footer("[blue bold]$10,318,030,576[/]"))
+
+ // Title
+ .Update(500, () => table.Title("Star Wars Movies"))
+ .Update(400, () => table.Title("[[ [yellow]Star Wars Movies[/] ]]"))
+
+ // Borders
+ .Update(230, () => table.BorderColor(Color.Yellow))
+ .Update(230, () => table.MinimalBorder())
+ .Update(230, () => table.SimpleBorder())
+ .Update(230, () => table.SimpleHeavyBorder())
+
+ // Caption
+ .Update(400, () => table.Caption("[[ [blue]THE END[/] ]]"))
+
+ // Finish animation and allow next animation to start
+ .IsFinished());
+
+AnsiConsole.MarkupLine("[yellow]Initializing warp drive[/]...");
+
+// Show progress
+AnsiConsoleRx.Progress(p =>
+ p.AutoClear(false)
+ .Columns(new ProgressColumn[]
+ {
+ new TaskDescriptionColumn(), // Task description
+ new ProgressBarColumn(), // Progress bar
+ new PercentageColumn(), // Percentage
+ new RemainingTimeColumn(), // Remaining time
+ new SpinnerColumn(), // Spinner
+ }))
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Subscribe(async ctx =>
+ {
+ var random = new Random(DateTime.Now.Millisecond);
+
+ // Create some tasks
+ var tasks = CreateTasks(ctx, random);
+ var warpTask = ctx.AddTask("Going to warp", autoStart: false).IsIndeterminate();
+
+ await ctx.Schedule(
+ TimeSpan.FromMilliseconds(100),
+ () => ctx.IsFinished,
+ () =>
+ {
+ // Increment progress
+ foreach (var (task, increment) in tasks)
+ {
+ task.Increment(random.NextDouble() * increment);
+ }
+
+ // Write some random things to the terminal
+ if (random.NextDouble() < 0.1)
+ {
+ GenerateLogMessage();
+ }
+ });
+
+ // Now start the "warp" task
+ warpTask.StartTask().IsIndeterminate(false);
+ await ctx.Schedule(
+ TimeSpan.FromMilliseconds(100),
+ () => warpTask.Increment(12 * random.NextDouble()));
+
+ // Done
+ await ctx.Schedule(_ => WriteLogMessage("[green]Done![/]"));
+
+ // Progress is finished and will automatically exit
+ });
+
+WriteLogMessage("[red]Press ctrl+c to exit[/]");
+
+static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext context, Random random)
+{
+ var tasks = new List<(ProgressTask, int)>();
+ while (tasks.Count < 5)
+ {
+ if (DescriptionGenerator.TryGenerate(out var name))
+ {
+ tasks.Add((context.AddTask(name), random.Next(2, 10)));
+ }
+ }
+
+ return tasks;
+}
+
+static void GenerateLogMessage() => AnsiConsole.MarkupLine(
+ "[grey]LOG:[/] " +
+ DescriptionGenerator.Generate() +
+ "[grey]...[/]");
+
+static void WriteLogMessage(string message) => AnsiConsole.MarkupLine($"[grey]LOG:[/] {message}[grey]...[/]");
diff --git a/src/LiveExample/LiveExample.csproj b/src/LiveExample/LiveExample.csproj
index 82384a3..4289849 100644
--- a/src/LiveExample/LiveExample.csproj
+++ b/src/LiveExample/LiveExample.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/LiveExample/Program.cs b/src/LiveExample/Program.cs
index aa60567..231e11f 100644
--- a/src/LiveExample/Program.cs
+++ b/src/LiveExample/Program.cs
@@ -3,7 +3,6 @@
using System;
using System.Reactive.Linq;
-using Spectre.Console;
using Spectre.Console.Rx;
namespace Live;
@@ -24,7 +23,7 @@ public static void Main()
AnsiConsoleRx.Live(table, ld => ld.AutoClear(false).Overflow(VerticalOverflow.Ellipsis).Cropping(VerticalOverflowCropping.Top))
.ObserveOn(AnsiConsoleRx.Scheduler)
.Subscribe(ctx =>
- {
+
// Columns
ctx.Update(230, () => table.AddColumn("Release date"))
.Update(230, () => table.AddColumn("Title"))
@@ -76,9 +75,6 @@ public static void Main()
.Update(230, () => table.SimpleHeavyBorder())
// Caption
- .Update(400, () => table.Caption("[[ [blue]THE END[/] ]]"));
- });
-
- Console.ReadLine();
+ .Update(400, () => table.Caption("[[ [blue]THE END[/] ]]")).IsFinished());
}
}
diff --git a/src/LiveTableExample/LiveTableExample.csproj b/src/LiveTableExample/LiveTableExample.csproj
index 82384a3..4289849 100644
--- a/src/LiveTableExample/LiveTableExample.csproj
+++ b/src/LiveTableExample/LiveTableExample.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/LiveTableExample/Program.cs b/src/LiveTableExample/Program.cs
index 27cbf7b..d0b150c 100644
--- a/src/LiveTableExample/Program.cs
+++ b/src/LiveTableExample/Program.cs
@@ -3,8 +3,7 @@
using System;
using System.Linq;
-using System.Threading.Tasks;
-using Spectre.Console;
+using System.Reactive.Linq;
using Spectre.Console.Rx;
namespace LiveTable;
@@ -45,31 +44,31 @@ public static void Main(string[] args)
p.AutoClear(false)
.Overflow(VerticalOverflow.Ellipsis)
.Cropping(VerticalOverflowCropping.Bottom))
- .Subscribe(async ctx =>
+ .ObserveOn(AnsiConsoleRx.Scheduler)
+ .Do(_ =>
{
// Add some initial rows
foreach (var a in Enumerable.Range(0, NumberOfRows))
{
AddExchangeRateRow(table);
}
-
+ })
+ .CombineLatest(Observable.Interval(TimeSpan.FromMilliseconds(400)), (ctx, _) => ctx)
+ .Subscribe(ctx =>
+ {
// Continously update the table
- while (true)
+ // More rows than we want?
+ if (table.Rows.Count > NumberOfRows)
{
- // More rows than we want?
- if (table.Rows.Count > NumberOfRows)
- {
- // Remove the first one
- table.Rows.RemoveAt(0);
- }
+ // Remove the first one
+ table.Rows.RemoveAt(0);
+ }
- // Add a new row
- AddExchangeRateRow(table);
+ // Add a new row
+ AddExchangeRateRow(table);
- // Refresh and wait for a while
- ctx.Refresh();
- await Task.Delay(400);
- }
+ // Refresh and wait for a while
+ ctx.Refresh();
});
Console.ReadLine();
diff --git a/src/ProgressExample/Program.cs b/src/ProgressExample/Program.cs
index 8d482bd..194722e 100644
--- a/src/ProgressExample/Program.cs
+++ b/src/ProgressExample/Program.cs
@@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
-using System.Threading;
-using Spectre.Console;
using Spectre.Console.Rx;
namespace Progress;
@@ -23,7 +21,8 @@ public static void Main()
AnsiConsole.MarkupLine("[yellow]Initializing warp drive[/]...");
// Show progress
- AnsiConsoleRx.Progress(p => p.AutoClear(false)
+ AnsiConsoleRx.Progress(p =>
+ p.AutoClear(false)
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(), // Task description
@@ -33,7 +32,7 @@ public static void Main()
new SpinnerColumn(), // Spinner
}))
.ObserveOn(AnsiConsoleRx.Scheduler)
- .Subscribe(ctx =>
+ .Subscribe(async ctx =>
{
var random = new Random(DateTime.Now.Millisecond);
@@ -41,64 +40,56 @@ public static void Main()
var tasks = CreateTasks(ctx, random);
var warpTask = ctx.AddTask("Going to warp", autoStart: false).IsIndeterminate();
- // Wait for all tasks (except the indeterminate one) to complete
- while (!ctx.IsFinished)
- {
- // Increment progress
- foreach (var (task, increment) in tasks)
+ await ctx.Schedule(
+ TimeSpan.FromMilliseconds(100),
+ () => ctx.IsFinished,
+ () =>
{
- task.Increment(random.NextDouble() * increment);
- }
-
- // Write some random things to the terminal
- if (random.NextDouble() < 0.1)
- {
- WriteLogMessage();
- }
-
- ctx.Refresh();
-
- // Simulate some delay
- Thread.Sleep(100);
- }
+ // Increment progress
+ foreach (var (task, increment) in tasks)
+ {
+ task.Increment(random.NextDouble() * increment);
+ }
+
+ // Write some random things to the terminal
+ if (random.NextDouble() < 0.1)
+ {
+ WriteLogMessage();
+ }
+ });
// Now start the "warp" task
- warpTask.StartTask();
- warpTask.IsIndeterminate(false);
- while (!ctx.IsFinished)
- {
- warpTask.Increment(12 * random.NextDouble());
-
- // Simulate some delay
- Thread.Sleep(100);
- }
+ warpTask.StartTask().IsIndeterminate(false);
+ await ctx.Schedule(
+ TimeSpan.FromMilliseconds(100),
+ () => warpTask.Increment(12 * random.NextDouble()));
// Done
- AnsiConsole.MarkupLine("[green]Done![/]");
+ await ctx.Schedule(_ => WriteLogMessage("[green]Done![/]"));
});
-
- Console.ReadLine();
}
- private static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext progress, Random random)
+ private static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext context, Random random)
{
var tasks = new List<(ProgressTask, int)>();
while (tasks.Count < 5)
{
if (DescriptionGenerator.TryGenerate(out var name))
{
- tasks.Add((progress.AddTask(name), random.Next(2, 10)));
+ tasks.Add((context.AddTask(name), random.Next(2, 10)));
}
}
return tasks;
}
- private static void WriteLogMessage()
- {
- AnsiConsole.MarkupLine(
+ private static void WriteLogMessage() => AnsiConsole.MarkupLine(
"[grey]LOG:[/] " +
DescriptionGenerator.Generate() +
"[grey]...[/]");
- }
+
+ private static void WriteLogMessage(string message) => AnsiConsole.MarkupLine(
+ "[grey]LOG:[/] " +
+ message +
+ "[grey]...[/]");
}
diff --git a/src/ProgressExample/ProgressExample.csproj b/src/ProgressExample/ProgressExample.csproj
index 0e87f93..73df831 100644
--- a/src/ProgressExample/ProgressExample.csproj
+++ b/src/ProgressExample/ProgressExample.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/ProgressThreadExample/Program.cs b/src/ProgressThreadExample/Program.cs
index d01b89f..27f9276 100644
--- a/src/ProgressThreadExample/Program.cs
+++ b/src/ProgressThreadExample/Program.cs
@@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
-using Spectre.Console;
using Spectre.Console.Rx;
namespace Progress;
@@ -33,28 +32,18 @@ public static void Main()
{
ctx.AddTask("Deleting Custom Formats").MaxValue(cfs.Count),
})
+ .ObserveOn(AnsiConsoleRx.Scheduler)
.Subscribe(
ctx =>
{
var rng = new Random();
cfs.ToObservable()
- .Select(x => Observable.FromAsync(async ct =>
- {
- await Task.Delay(TimeSpan.FromSeconds(rng.Next(1, 5)), ct);
- }))
+ .Select(_ => Observable.FromAsync(async ct => await Task.Delay(TimeSpan.FromSeconds(rng.Next(1, 5)), ct), AnsiConsoleRx.Scheduler))
.Merge(8)
- .Subscribe(_ =>
- {
- ctx.tasks[0].Increment(1); // Not thread safe; needs to invoke on the thread that subscribed; not sure if this is ObserveOn or SubscribeOn
- });
+ .ObserveOn(SpectreConsoleScheduler.Instance)
+ .Subscribe(_ => ctx.tasks[0].Increment(1));
},
- () =>
- {
- // Render a message when the sequence completes
- AnsiConsole.MarkupLine("[blue]Done![/]");
- });
-
- Console.ReadLine();
+ () => AnsiConsole.MarkupLine("[blue]Done![/]")); // Render a message when the sequence completes
}
}
diff --git a/src/ProgressThreadExample/ProgressThreadExample.csproj b/src/ProgressThreadExample/ProgressThreadExample.csproj
index 0e87f93..73df831 100644
--- a/src/ProgressThreadExample/ProgressThreadExample.csproj
+++ b/src/ProgressThreadExample/ProgressThreadExample.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/Spectre.Console.Rx.sln b/src/Spectre.Console.Rx.sln
index 5149e32..c76f9c2 100644
--- a/src/Spectre.Console.Rx.sln
+++ b/src/Spectre.Console.Rx.sln
@@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Rx", "Spect
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveExample", "LiveExample\LiveExample.csproj", "{3EBF3829-4483-48ED-AD1E-E057CC7DDFF0}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{725C299C-42CE-405F-85CC-6DEFEA8487B9}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProgressExample", "ProgressExample\ProgressExample.csproj", "{D3195DF3-668F-4BF0-9020-22C6649924EF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{E17E245C-D501-46B4-A804-A68241982088}"
@@ -21,14 +19,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProgressThreadExample", "Pr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{15A30A6D-9A94-4A7C-A74B-D7A9CB36C216}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CombinedExample", "CombinedExample\CombinedExample.csproj", "{00E6E161-7DD9-49BB-AEDC-FFFD0447368E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {15A30A6D-9A94-4A7C-A74B-D7A9CB36C216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {15A30A6D-9A94-4A7C-A74B-D7A9CB36C216}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43DC5A6B-B066-4CCF-90A8-680432C139D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43DC5A6B-B066-4CCF-90A8-680432C139D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43DC5A6B-B066-4CCF-90A8-680432C139D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -37,10 +35,6 @@ Global
{3EBF3829-4483-48ED-AD1E-E057CC7DDFF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3EBF3829-4483-48ED-AD1E-E057CC7DDFF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3EBF3829-4483-48ED-AD1E-E057CC7DDFF0}.Release|Any CPU.Build.0 = Release|Any CPU
- {725C299C-42CE-405F-85CC-6DEFEA8487B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {725C299C-42CE-405F-85CC-6DEFEA8487B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {725C299C-42CE-405F-85CC-6DEFEA8487B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {725C299C-42CE-405F-85CC-6DEFEA8487B9}.Release|Any CPU.Build.0 = Release|Any CPU
{D3195DF3-668F-4BF0-9020-22C6649924EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D3195DF3-668F-4BF0-9020-22C6649924EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3195DF3-668F-4BF0-9020-22C6649924EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -57,17 +51,23 @@ Global
{CB209C31-2CD2-4188-A088-18E68DF23788}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB209C31-2CD2-4188-A088-18E68DF23788}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB209C31-2CD2-4188-A088-18E68DF23788}.Release|Any CPU.Build.0 = Release|Any CPU
+ {15A30A6D-9A94-4A7C-A74B-D7A9CB36C216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {15A30A6D-9A94-4A7C-A74B-D7A9CB36C216}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3EBF3829-4483-48ED-AD1E-E057CC7DDFF0} = {E17E245C-D501-46B4-A804-A68241982088}
- {725C299C-42CE-405F-85CC-6DEFEA8487B9} = {E17E245C-D501-46B4-A804-A68241982088}
{D3195DF3-668F-4BF0-9020-22C6649924EF} = {E17E245C-D501-46B4-A804-A68241982088}
{9B8D02EB-C0CB-42B1-B44C-74471D83A8C2} = {E17E245C-D501-46B4-A804-A68241982088}
{51ACCE14-F44D-455B-A5E9-AEC71F7C6F61} = {E17E245C-D501-46B4-A804-A68241982088}
{CB209C31-2CD2-4188-A088-18E68DF23788} = {E17E245C-D501-46B4-A804-A68241982088}
+ {00E6E161-7DD9-49BB-AEDC-FFFD0447368E} = {E17E245C-D501-46B4-A804-A68241982088}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2FADF055-54C6-4DE5-98D0-D6F352AB1EFA}
diff --git a/src/Spectre.Console.Rx/AnsiConsoleRx.cs b/src/Spectre.Console.Rx/AnsiConsoleRx.cs
index 90844d7..77a9066 100644
--- a/src/Spectre.Console.Rx/AnsiConsoleRx.cs
+++ b/src/Spectre.Console.Rx/AnsiConsoleRx.cs
@@ -1,10 +1,9 @@
// Copyright (c) Chris Pulman. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-using System.Reactive.Concurrency;
+using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
-using Spectre.Console.Rendering;
namespace Spectre.Console.Rx;
@@ -15,6 +14,7 @@ public static class AnsiConsoleRx
{
private static readonly object _lock = new();
private static readonly List _tasks = new();
+ private static readonly SemaphoreSlim _lockTillComplete = new(1, 1);
///
/// Gets the scheduler.
@@ -22,7 +22,7 @@ public static class AnsiConsoleRx
///
/// The scheduler.
///
- public static IScheduler Scheduler { get; } = new SpectreConsoleScheduler();
+ public static ISpectreConsoleScheduler Scheduler => SpectreConsoleScheduler.Instance;
///
/// Creates a new instance.
@@ -31,31 +31,36 @@ public static class AnsiConsoleRx
///
/// A instance.
///
- public static IObservable Progress(Func