diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6f97d1..e8fbd9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,9 +28,9 @@ jobs: run: nbgv get-version - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + dotnet-version: '10.x' - name: Restore packages run: dotnet restore @@ -60,9 +60,9 @@ jobs: name: push-to-nuget-org steps: - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + dotnet-version: '10.x' - name: Download artifacts uses: actions/download-artifact@v4 diff --git a/ConsoleTools/BootstrapperExtensions.cs b/ConsoleTools/BootstrapperExtensions.cs index 275f6ec..4be30f5 100644 --- a/ConsoleTools/BootstrapperExtensions.cs +++ b/ConsoleTools/BootstrapperExtensions.cs @@ -7,6 +7,7 @@ public static class BootstrapperExtensions { //Microsoft.Extensions.DependencyInjection + [Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public static ITypeRegistrar ToTypeRegistrar(this IServiceCollection services) => new SpectreInfrastructure.ServiceCollectionTypeRegistrar(services); diff --git a/ConsoleTools/Interop/PowerShellHelper.cs b/ConsoleTools/Interop/PowerShellHelper.cs index 12495fb..621e0a3 100644 --- a/ConsoleTools/Interop/PowerShellHelper.cs +++ b/ConsoleTools/Interop/PowerShellHelper.cs @@ -5,6 +5,7 @@ namespace aemarcoCommons.ConsoleTools.Interop; public static class PowerShellHelper { + [Obsolete("Use aemarcoCommons.ToolboxConsole.Shell.PowerShellHelper instead.")] public static async Task RunCommand( string command, params string[] args) diff --git a/ConsoleTools/Menu/ConsoleMenu.cs b/ConsoleTools/Menu/ConsoleMenu.cs index 1f8d51f..a7735cb 100644 --- a/ConsoleTools/Menu/ConsoleMenu.cs +++ b/ConsoleTools/Menu/ConsoleMenu.cs @@ -5,6 +5,7 @@ // ReSharper disable once CheckNamespace namespace aemarcoCommons.ConsoleTools; +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public class ConsoleMenu { private readonly ConsoleMenuItem[] _menuItems; @@ -120,6 +121,7 @@ private void WriteConsoleItem(int itemIndex, int selectedItemIndex) } } +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public abstract class ConsoleMenuItem { protected ConsoleMenuItem(string label) @@ -131,6 +133,7 @@ public virtual void Execute() { } public virtual Task ExecuteAsync() => Task.CompletedTask; } +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public class ConsoleMenuItem : ConsoleMenuItem { private readonly Action? _callBack; @@ -165,6 +168,7 @@ public override async Task ExecuteAsync() } } +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public class ConsoleMenuSeparator : ConsoleMenuItem { public ConsoleMenuSeparator(char separatorChar = '-') diff --git a/ConsoleTools/Power/Commands/Position.cs b/ConsoleTools/Power/Commands/Position.cs index 52de81a..09694b0 100644 --- a/ConsoleTools/Power/Commands/Position.cs +++ b/ConsoleTools/Power/Commands/Position.cs @@ -3,6 +3,7 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static void ClearCurrentLine() { var currentLineCursor = Console.CursorTop; diff --git a/ConsoleTools/Power/Commands/Window.cs b/ConsoleTools/Power/Commands/Window.cs index 623335c..d995ad2 100644 --- a/ConsoleTools/Power/Commands/Window.cs +++ b/ConsoleTools/Power/Commands/Window.cs @@ -7,12 +7,14 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] [SupportedOSPlatform("windows")] public static void ShowWindow() { WindowVisibility.Show(); } + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] [SupportedOSPlatform("windows")] public static void HideWindow() { diff --git a/ConsoleTools/Power/Input/ClassBasedSelection.cs b/ConsoleTools/Power/Input/ClassBasedSelection.cs index 68e1359..4583d49 100644 --- a/ConsoleTools/Power/Input/ClassBasedSelection.cs +++ b/ConsoleTools/Power/Input/ClassBasedSelection.cs @@ -14,6 +14,7 @@ public static partial class PowerConsole /// Displayed as /// Selected item + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static T EnsureSelection(string header, IEnumerable selectable, Func displayProperty) where T : class { @@ -34,6 +35,7 @@ public static T EnsureSelection(string header, IEnumerable selectable, Fun /// Displayed as /// Selected item or null on abort + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static T? AbortableSelection(string header, IEnumerable selectable, Func displayProperty) where T : class { diff --git a/ConsoleTools/Power/Input/Decision.cs b/ConsoleTools/Power/Input/Decision.cs index 092a0bd..a197f91 100644 --- a/ConsoleTools/Power/Input/Decision.cs +++ b/ConsoleTools/Power/Input/Decision.cs @@ -11,6 +11,7 @@ public static partial class PowerConsole /// /// question to ask /// /// clear console before asking /// true if decision is Yes + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static bool EnsureDecision(string question, bool clear = true) { if (clear) diff --git a/ConsoleTools/Power/Input/InputHelpers.cs b/ConsoleTools/Power/Input/InputHelpers.cs index 3e2bde7..0880e92 100644 --- a/ConsoleTools/Power/Input/InputHelpers.cs +++ b/ConsoleTools/Power/Input/InputHelpers.cs @@ -6,6 +6,7 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] private static T? MenuSelectionHelper(string header, IEnumerable selectable, Func displayProperty, bool abortable, bool clear = true) where T : class { diff --git a/ConsoleTools/Power/Input/Numbers.cs b/ConsoleTools/Power/Input/Numbers.cs index c4ca382..451a3f0 100644 --- a/ConsoleTools/Power/Input/Numbers.cs +++ b/ConsoleTools/Power/Input/Numbers.cs @@ -12,6 +12,7 @@ public static partial class PowerConsole /// inclusive maximum of the range /// console should be cleared? /// input number in range + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static int EnsureIntInput(string prompt, int min = int.MinValue, int max = int.MaxValue, bool clear = false) { if (clear) @@ -41,6 +42,7 @@ public static int EnsureIntInput(string prompt, int min = int.MinValue, int max /// inclusive maximum of the range /// console should be cleared? /// nullable input number in range + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static int? EnsureNullableIntInput(string prompt, int min = int.MinValue, int max = int.MaxValue, bool clear = false) { if (clear) diff --git a/ConsoleTools/Power/Input/PathNavigation.cs b/ConsoleTools/Power/Input/PathNavigation.cs index 8caadab..44974d1 100644 --- a/ConsoleTools/Power/Input/PathNavigation.cs +++ b/ConsoleTools/Power/Input/PathNavigation.cs @@ -18,6 +18,7 @@ public static partial class PowerConsole /// servers to include for share selection /// shot hidden folders /// selected path + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static string PathSelector(string path, IEnumerable? includedServers = null, bool showHidden = false) { var serverItems = includedServers?.Distinct().ToArray(); @@ -71,6 +72,7 @@ x is null /// Selection of a drive in a ConsoleMenu /// /// + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static string DriveSelector(IEnumerable? servers = null) { string? path = null; @@ -109,6 +111,7 @@ public static string DriveSelector(IEnumerable? servers = null) /// /// servers which can be selected /// + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] [SupportedOSPlatform("windows")] public static string? ServerSelector(IEnumerable servers) { @@ -129,6 +132,7 @@ public static string DriveSelector(IEnumerable? servers = null) /// Selection of a share for given server /// /// unc path to share or null when no shares are available + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] [SupportedOSPlatform("windows")] public static string? ShareSelector(string? server) { diff --git a/ConsoleTools/Power/Input/Text.cs b/ConsoleTools/Power/Input/Text.cs index 3f0cbf5..5dc8949 100644 --- a/ConsoleTools/Power/Input/Text.cs +++ b/ConsoleTools/Power/Input/Text.cs @@ -12,6 +12,7 @@ public static partial class PowerConsole /// min length /// console should be cleared? /// input string + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static string EnsureTextInput(string prompt, int minLength = 1, bool clear = false) { if (clear) diff --git a/ConsoleTools/Power/Spectre/Json.cs b/ConsoleTools/Power/Spectre/Json.cs index 4cd6b5a..fa24edf 100644 --- a/ConsoleTools/Power/Spectre/Json.cs +++ b/ConsoleTools/Power/Spectre/Json.cs @@ -6,6 +6,7 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static void WriteAsJson(object o) { AnsiConsole.Write(new JsonText(JsonConvert.SerializeObject(o))); diff --git a/ConsoleTools/Power/Spectre/Progress.cs b/ConsoleTools/Power/Spectre/Progress.cs index e3b991c..63a63d7 100644 --- a/ConsoleTools/Power/Spectre/Progress.cs +++ b/ConsoleTools/Power/Spectre/Progress.cs @@ -7,6 +7,7 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static void StartProgress(Action work) { AnsiConsole.Progress() @@ -19,6 +20,7 @@ public static void StartProgress(Action work) .Start(work); } + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static T StartProgress(Func work) { return AnsiConsole.Progress() @@ -32,6 +34,7 @@ public static T StartProgress(Func work) } + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static async Task StartProgressAsync(Func work) { await AnsiConsole.Progress() @@ -44,6 +47,7 @@ await AnsiConsole.Progress() .StartAsync(work); } + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static async Task StartProgressAsync( Func work, T request, @@ -54,6 +58,7 @@ await StartProgressAsync(async ctx => } + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static async Task StartProgressAsync(Func> work) { return await AnsiConsole.Progress() diff --git a/ConsoleTools/Power/Spectre/ProgressTask.cs b/ConsoleTools/Power/Spectre/ProgressTask.cs index 209a7ca..9952c25 100644 --- a/ConsoleTools/Power/Spectre/ProgressTask.cs +++ b/ConsoleTools/Power/Spectre/ProgressTask.cs @@ -4,6 +4,7 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static void CompleteTask(this ProgressTask task) { task.Value = task.MaxValue; diff --git a/ConsoleTools/Power/Spectre/Status.cs b/ConsoleTools/Power/Spectre/Status.cs index dcd73e2..af2fc03 100644 --- a/ConsoleTools/Power/Spectre/Status.cs +++ b/ConsoleTools/Power/Spectre/Status.cs @@ -5,6 +5,7 @@ namespace aemarcoCommons.ConsoleTools; public static partial class PowerConsole { + [Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static async Task StartStatusAsync( Func> work) { diff --git a/ConsoleTools/Progress/ConsoleInlineProgressBar.cs b/ConsoleTools/Progress/ConsoleInlineProgressBar.cs index 66857f5..ff520f7 100644 --- a/ConsoleTools/Progress/ConsoleInlineProgressBar.cs +++ b/ConsoleTools/Progress/ConsoleInlineProgressBar.cs @@ -4,6 +4,7 @@ namespace aemarcoCommons.ConsoleTools; /// /// This progress bar fits, if console outputs are written in between progress steps /// +[Obsolete("Use Spectre.Console progress via PowerConsole.StartProgress in aemarcoCommons.ToolboxConsole instead.")] public class ConsoleInlineProgressBar : ConsoleProgressBar { public override void UpdateProgress(long done, long total) diff --git a/ConsoleTools/Progress/ConsoleProgressBar.cs b/ConsoleTools/Progress/ConsoleProgressBar.cs index 076aab3..9c3eda9 100644 --- a/ConsoleTools/Progress/ConsoleProgressBar.cs +++ b/ConsoleTools/Progress/ConsoleProgressBar.cs @@ -3,6 +3,7 @@ // ReSharper disable once CheckNamespace namespace aemarcoCommons.ConsoleTools; +[Obsolete("Use Spectre.Console progress via PowerConsole.StartProgress in aemarcoCommons.ToolboxConsole instead.")] public abstract class ConsoleProgressBar { public abstract void UpdateProgress(long done, long total); @@ -88,6 +89,7 @@ protected void DrawProgressLine(long done, long total) /// /// This progress bar fits, if no console outputs are written in between progress steps /// +[Obsolete("Use Spectre.Console progress via PowerConsole.StartProgress in aemarcoCommons.ToolboxConsole instead.")] public class ConsoleOneLineProgressBar : ConsoleProgressBar { public override void UpdateProgress(long done, long total) diff --git a/ConsoleTools/Progress/ConsoleTopProgressBar.cs b/ConsoleTools/Progress/ConsoleTopProgressBar.cs index 2dfd819..4f0ee67 100644 --- a/ConsoleTools/Progress/ConsoleTopProgressBar.cs +++ b/ConsoleTools/Progress/ConsoleTopProgressBar.cs @@ -5,6 +5,7 @@ namespace aemarcoCommons.ConsoleTools; /// This progress bar fits, if a lot of console outputs are written in between progress steps. /// Place the number of max. lines of content in between steps into the constructor. defaults to 1. /// +[Obsolete("Use Spectre.Console progress via PowerConsole.StartProgress in aemarcoCommons.ToolboxConsole instead.")] public class ConsoleTopProgressBar : ConsoleProgressBar { private readonly int _contentHeight; diff --git a/ConsoleTools/SpectreHost/HostApplicationBuilderExtensions.cs b/ConsoleTools/SpectreHost/HostApplicationBuilderExtensions.cs index 3d06d2a..5f14b35 100644 --- a/ConsoleTools/SpectreHost/HostApplicationBuilderExtensions.cs +++ b/ConsoleTools/SpectreHost/HostApplicationBuilderExtensions.cs @@ -10,11 +10,12 @@ namespace aemarcoCommons.ConsoleTools.SpectreHost; - +[Obsolete("Use aemarcoCommons.ToolboxConsole.PowerConsole instead.")] public static class HostApplicationBuilderExtensions { private static AppTypeRegistrar? _registrar; + [Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public static async Task RunAsSpectreCommandApp( this HostApplicationBuilder app, Action? configureCommandApp = null) @@ -26,6 +27,7 @@ await app.RunCommandApp( (_, x) => configureCommandApp?.Invoke(x)); } + [Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public static async Task RunAsSpectreCommandApp( this HostApplicationBuilder app, Action? configureCommandApp = null) @@ -39,6 +41,7 @@ await app.RunCommandApp( } + [Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public static async Task RunAsSpectreCommandApp( this HostApplicationBuilder app, Action? configureCommandApp = null) @@ -47,6 +50,7 @@ public static async Task RunAsSpectreCommandApp( var commandApp = new CommandApp(_registrar); await app.RunCommandApp(commandApp, configureCommandApp); } + [Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public static async Task RunAsSpectreCommandApp( this HostApplicationBuilder app, Action? configureCommandApp = null) diff --git a/ConsoleTools/SpectreInfrastructure/AppTypeRegistrar.cs b/ConsoleTools/SpectreInfrastructure/AppTypeRegistrar.cs index 06bb4c9..1b7555a 100644 --- a/ConsoleTools/SpectreInfrastructure/AppTypeRegistrar.cs +++ b/ConsoleTools/SpectreInfrastructure/AppTypeRegistrar.cs @@ -4,6 +4,7 @@ namespace aemarcoCommons.ConsoleTools.SpectreInfrastructure; +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public sealed class AppTypeRegistrar : ITypeRegistrar { private readonly HostApplicationBuilder _builder; diff --git a/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeRegistrar.cs b/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeRegistrar.cs index c16d7ba..af29a01 100644 --- a/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeRegistrar.cs +++ b/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeRegistrar.cs @@ -6,6 +6,7 @@ namespace aemarcoCommons.ConsoleTools.SpectreInfrastructure; //https://spectreconsole.net/cli/commandapp //https://github.com/spectreconsole/examples/blob/main/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public sealed class ServiceCollectionTypeRegistrar : ITypeRegistrar { private readonly IServiceCollection _builder; diff --git a/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeResolver.cs b/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeResolver.cs index b64483d..5b57a01 100644 --- a/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeResolver.cs +++ b/ConsoleTools/SpectreInfrastructure/ServiceCollectionTypeResolver.cs @@ -6,6 +6,7 @@ namespace aemarcoCommons.ConsoleTools.SpectreInfrastructure; //https://spectreconsole.net/cli/commandapp //https://github.com/spectreconsole/examples/blob/main/examples/Cli/Injection/Infrastructure/TypeResolver.cs +[Obsolete("Use aemarcoCommons.ToolboxConsole instead.")] public sealed class ServiceCollectionTypeResolver : ITypeResolver, IDisposable { private readonly IServiceProvider _provider; diff --git a/TestsManual/ConsoleToolsManualTests/Program.cs b/TestsManual/ConsoleToolsManualTests/Program.cs deleted file mode 100644 index 0586dc2..0000000 --- a/TestsManual/ConsoleToolsManualTests/Program.cs +++ /dev/null @@ -1,376 +0,0 @@ -using aemarcoCommons.ConsoleTools; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - - -namespace ConsoleToolsManualTests; - - - -internal static class Program -{ - #region menu - - private static Task Main() - { - var done = false; - while (!done) - { - Console.Clear(); - var items = new List(); - AddMenuItems(items); - //Exit - items.AddRange(new List - { - //Exit - new ConsoleMenuSeparator(), - new ConsoleMenuItem("Exit", _ => - { - done = true; - }) - }); - var menu = new ConsoleMenu("Test ConsoleTools stuff", items); - menu.RunConsoleMenu(); - } - - return Task.CompletedTask; - } - - - #endregion - - #region Testhelpers - - private static void Describe(string message, bool clear = true) - { - if (clear) Console.Clear(); - Console.Write(message); - Console.Write(". \tPress any key to proceed"); - Console.ReadKey(); - } - - #endregion - - private static void AddMenuItems(ICollection items) - { - AddPositionTests(items); - AddWindowTests(items); - AddSelectionTests(items); - AddDecisionTests(items); - AddNumbersTests(items); - AddNullableNumbersTests(items); - AddTextTests(items); - AddPathNavigationTests(items); - AddNetworkNavigationTests(items); - AddProgressTests(items); - } - - - - private static void AddWindowTests(ICollection items) - { - if (OperatingSystem.IsWindows()) - { - items.Add(new ConsoleMenuItem("Hiding", async _ => - { - if (!OperatingSystem.IsWindows()) return; - - Describe("Console should hide for 2 seconds"); - - PowerConsole.HideWindow(); - await Task.Delay(2000); - PowerConsole.ShowWindow(); - })); - } - } - - private static void AddPositionTests(ICollection items) - { - items.Add(new ConsoleMenuItem("ClearLine", async _ => - { - Console.Clear(); - var words = new[] { "Some", " text", " should", " appear", " and", " disappear", " like", " magic." }; - foreach (var word in words) - { - Console.Write(word); - await Task.Delay(1000); - } - PowerConsole.ClearCurrentLine(); - Describe("See only this... nice!!!"); - })); - } - - private static void AddSelectionTests(ICollection items) - { - items.Add(new ConsoleMenuItem("Selection", _ => - { - var persons = new[] - { - "Bob", - "Alice", - "Tim" - }; - - var alice = PowerConsole.EnsureSelection("Select Alice here", persons, x => x); - if (alice != "Alice") - { - Describe("Test failed"); - return; - } - - - var tim = PowerConsole.AbortableSelection("Select Tim here", persons, x => x); - if (tim != "Tim") - { - Describe("Test failed"); - return; - } - - var nobody = PowerConsole.AbortableSelection("Select Abort here", persons, x => x); - if (nobody is not null) - { - Describe("Test failed"); - return; - } - - Describe("Test successful"); - - })); - } - - private static void AddDecisionTests(ICollection items) - { - items.Add(new ConsoleMenuItem("Decision", _ => - { - var trueResult = PowerConsole.EnsureDecision("Select Yes here."); - if (!trueResult) - { - Describe("Test failed"); - return; - } - var falseResult = PowerConsole.EnsureDecision("Select No here."); - if (falseResult) - { - Describe("Test failed"); - return; - } - Describe("Test successful"); - - })); - } - - private static void AddNumbersTests(ICollection items) - { - items.Add(new ConsoleMenuItem("Numbers", _ => - { - Console.WriteLine(); - var answer = PowerConsole.EnsureIntInput("Input 42 here"); - if (answer != 42) - { - Describe("Test failed"); - return; - } - var seven = PowerConsole.EnsureIntInput("Numbers 5 - 10 are working. Try outside outside as well, but use 7 finally", 5, 10); - if (seven != 7) - { - Describe("Test failed"); - return; - } - - Console.WriteLine(); - Describe("Test successful", false); - - })); - } - - private static void AddNullableNumbersTests(ICollection items) - { - items.Add(new ConsoleMenuItem("NullableNumbers", _ => - { - Console.WriteLine(); - - - //number - var five = PowerConsole.EnsureNullableIntInput("Input 5 here"); - if (five != 5) - { - Describe("Test failed"); - return; - } - - //null - var notANumber = PowerConsole.EnsureNullableIntInput("Fail with input 'aaa' here, then input nothing"); - if (notANumber is not null) - { - Describe("Test failed"); - return; - } - - //range - var eight = PowerConsole.EnsureNullableIntInput("Input 8 here", 5, 8); - if (eight != 8) - { - Describe("Test failed"); - return; - } - - //outside range - var six = PowerConsole.EnsureNullableIntInput("Fail with 9 first, then use 6", 5, 8); - if (six != 6) - { - Describe("Test failed"); - return; - } - - //null - var notANumber2 = PowerConsole.EnsureNullableIntInput("Fail with 'aaa' here, then input nothing", 5, 10); - if (notANumber2 is not null) - { - Describe("Test failed"); - return; - } - - Console.WriteLine(); - Describe("Test successful", false); - - })); - } - - private static void AddTextTests(ICollection items) - { - items.Add(new ConsoleMenuItem("Text", _ => - { - Console.WriteLine(); - - var aaa = PowerConsole.EnsureTextInput("Input 'aaa' here"); - if (aaa != "aaa") - { - Describe("Test failed"); - return; - } - - var aa = PowerConsole.EnsureTextInput("Fail with 'a' here, then input 'aa'", 2); - if (aa != "aa") - { - Describe("Test failed"); - return; - } - - Console.WriteLine(); - Describe("Test successful", false); - - })); - } - - private static void AddPathNavigationTests(ICollection items) - { - items.Add(new ConsoleMenuItem("PathNavigation", _ => - { - var currentPath = Environment.CurrentDirectory; - - Describe("Navigate 2 folders up"); - var twoFoldersUp = PowerConsole.PathSelector(currentPath); - if (twoFoldersUp != new DirectoryInfo(currentPath).Parent!.Parent!.FullName) - { - Describe("Test failed"); - return; - } - - Describe("Navigate to the root. Use Drive navigation also"); - var root = PowerConsole.PathSelector(currentPath); - var rootDir = new DirectoryInfo(currentPath).Root; - if (root != rootDir.FullName) - { - Describe("Test failed"); - return; - } - - Describe("Test successful"); - })); - } - - private static void AddNetworkNavigationTests(ICollection items) - { - if (OperatingSystem.IsWindows()) - { - items.Add(new ConsoleMenuItem("NetworkNavigation", _ => - { - Console.WriteLine(); - var share = PowerConsole.EnsureTextInput("Input server name here", 3).ToUpper(); - var root = new DirectoryInfo(Environment.CurrentDirectory).Root; - Describe(@"Navigate to any share on given server"); - var home = PowerConsole.PathSelector(root.FullName, new[] { share }); - if (!home.StartsWith($"\\\\{share}\\")) - { - Describe("Test failed"); - return; - } - - Describe("Test successful"); - })); - } - } - - private static void AddProgressTests(ICollection items) - { - items.Add(new ConsoleMenuItem("ConsoleTopProgress", async _ => - { - Console.WriteLine(); - - // ReSharper disable StringLiteralTypo - var text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure"; - // ReSharper restore StringLiteralTypo - - var progress = new ConsoleTopProgressBar(4); - for (var i = 0; i < 25; i++) - { - for (var y = 0; y < 4; y++) - { - var line = text[..(text.IndexOf(' ') + 1)]; - if (text.Length > line.Length) text = text[line.Length..]; - Console.WriteLine(line); - } - - await Task.Delay(150); - progress.UpdateProgress(i + 1, 25); - } - Describe("Test successful if you always saw the progress bar, and it reached 100%", false); - })); - - - items.Add(new ConsoleMenuItem("ConsoleInlineProgress", async _ => - { - Console.WriteLine(); - - var progress = new ConsoleInlineProgressBar(); - - for (var i = 0; i < 25; i++) - { - await Task.Delay(150); - progress.UpdateProgress(i + 1, 25); - } - Describe("Test successful if you see progress to 100% in multiple lines", false); - })); - - - items.Add(new ConsoleMenuItem("ConsoleOneLineProgress", async _ => - { - Console.WriteLine(); - - var progress = new ConsoleOneLineProgressBar(); - - for (var i = 0; i < 25; i++) - { - await Task.Delay(150); - progress.UpdateProgress(i + 1, 25); - } - Describe("Test successful if you see progress to 100% in one line", false); - })); - - - } - - -} \ No newline at end of file diff --git a/TestsManual/ToolboxConsoleManualTests/GlobalUsings.cs b/TestsManual/ToolboxConsoleManualTests/GlobalUsings.cs new file mode 100644 index 0000000..e2ddf26 --- /dev/null +++ b/TestsManual/ToolboxConsoleManualTests/GlobalUsings.cs @@ -0,0 +1,7 @@ +// Global using directives + +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Threading.Tasks; +global using aemarcoCommons.ToolboxConsole; \ No newline at end of file diff --git a/TestsManual/ToolboxConsoleManualTests/Program.cs b/TestsManual/ToolboxConsoleManualTests/Program.cs new file mode 100644 index 0000000..bb48286 --- /dev/null +++ b/TestsManual/ToolboxConsoleManualTests/Program.cs @@ -0,0 +1,307 @@ +namespace ToolboxConsoleManualTests; + +internal static class Program +{ + private static async Task Main() + { + var done = false; + while (!done) + { + Console.Clear(); + var items = new List(); + + // Commands + items.Add(new ConsoleMenuItem(nameof(ClearLine), ClearLine)); + if (OperatingSystem.IsWindows()) + items.Add(new ConsoleMenuItem(nameof(Hiding), Hiding)); + + // Inputs + items.Add(new ConsoleMenuItem(nameof(Decision), Decision)); + items.Add(new ConsoleMenuItem(nameof(Numbers), Numbers)); + items.Add(new ConsoleMenuItem(nameof(NullableNumbers), NullableNumbers)); + items.Add(new ConsoleMenuItem(nameof(Text), Text)); + + // PathNavigation + items.Add(new ConsoleMenuItem(nameof(PathNavigation), PathNavigation)); + if (OperatingSystem.IsWindows()) + items.Add(new ConsoleMenuItem(nameof(NetworkNavigation), NetworkNavigation)); + + // Progress + items.Add(new ConsoleMenuItem(nameof(SpectreProgress), SpectreProgress)); + items.Add(new ConsoleMenuItem(nameof(SpectreMultiProgress), SpectreMultiProgress)); + items.Add(new ConsoleMenuItem(nameof(SpectreStatus), SpectreStatus)); + + // Selection + items.Add(new ConsoleMenuItem(nameof(Selection), Selection)); + + // Exit + items.Add(new ConsoleMenuSeparator()); + items.Add(new ConsoleMenuItem("Exit", _ => { done = true; })); + + var menu = new ConsoleMenu("Test ToolboxConsole stuff", items); + await menu.RunConsoleMenuAsync(); + } + } + + // Commands + private static async Task ClearLine(string? _) + { + Console.Clear(); + var words = new[] { "Some", " text", " should", " appear", " and", " disappear", " like", " magic." }; + foreach (var word in words) + { + Console.Write(word); + await Task.Delay(1000); + } + PowerConsole.ClearCurrentLine(); + Describe("See only this... nice!!!"); + } + private static async Task Hiding(string? _) + { + if (!OperatingSystem.IsWindows()) return; + + Describe("Console should hide for 2 seconds"); + + PowerConsole.HideWindow(); + await Task.Delay(2000); + PowerConsole.ShowWindow(); + } + + + // Inputs + private static void Decision(string? _) + { + var trueResult = PowerConsole.EnsureDecision("Select Yes here."); + if (!trueResult) + { + Describe("Test failed"); + return; + } + var falseResult = PowerConsole.EnsureDecision("Select No here."); + if (falseResult) + { + Describe("Test failed"); + return; + } + Describe("Test successful"); + } + private static void Numbers(string? _) + { + Console.WriteLine(); + var answer = PowerConsole.EnsureIntInput("Input 42 here"); + if (answer != 42) + { + Describe("Test failed"); + return; + } + var seven = PowerConsole.EnsureIntInput("Numbers 5 - 10 are working. Try outside outside as well, but use 7 finally", 5, 10); + if (seven != 7) + { + Describe("Test failed"); + return; + } + + Console.WriteLine(); + Describe("Test successful", false); + } + private static void NullableNumbers(string? _) + { + Console.WriteLine(); + + //number + var five = PowerConsole.EnsureNullableIntInput("Input 5 here"); + if (five != 5) + { + Describe("Test failed"); + return; + } + + //null + var notANumber = PowerConsole.EnsureNullableIntInput("Fail with input 'aaa' here, then input nothing"); + if (notANumber is not null) + { + Describe("Test failed"); + return; + } + + //range + var eight = PowerConsole.EnsureNullableIntInput("Input 8 here", 5, 8); + if (eight != 8) + { + Describe("Test failed"); + return; + } + + //outside range + var six = PowerConsole.EnsureNullableIntInput("Fail with 9 first, then use 6", 5, 8); + if (six != 6) + { + Describe("Test failed"); + return; + } + + //null + var notANumber2 = PowerConsole.EnsureNullableIntInput("Fail with 'aaa' here, then input nothing", 5, 10); + if (notANumber2 is not null) + { + Describe("Test failed"); + return; + } + + Console.WriteLine(); + Describe("Test successful", false); + } + private static void Text(string? _) + { + Console.WriteLine(); + + var aaa = PowerConsole.EnsureTextInput("Input 'aaa' here"); + if (aaa != "aaa") + { + Describe("Test failed"); + return; + } + + var aa = PowerConsole.EnsureTextInput("Fail with 'a' here, then input 'aa'", 2); + if (aa != "aa") + { + Describe("Test failed"); + return; + } + + Console.WriteLine(); + Describe("Test successful", false); + } + + + // PathNavigation + private static void PathNavigation(string? _) + { + var currentPath = Environment.CurrentDirectory; + + Describe("Navigate 2 folders up"); + var twoFoldersUp = PowerConsole.PathSelector(currentPath); + if (twoFoldersUp != new DirectoryInfo(currentPath).Parent!.Parent!.FullName) + { + Describe("Test failed"); + return; + } + + Describe("Navigate to the root. Use Drive navigation also"); + var root = PowerConsole.PathSelector(currentPath); + var rootDir = new DirectoryInfo(currentPath).Root; + if (root != rootDir.FullName) + { + Describe("Test failed"); + return; + } + + Describe("Test successful"); + } + private static void NetworkNavigation(string? _) + { + Console.WriteLine(); + var share = PowerConsole.EnsureTextInput("Input server name here", 3).ToUpper(); + var root = new DirectoryInfo(Environment.CurrentDirectory).Root; + Describe(@"Navigate to any share on given server"); + var home = PowerConsole.PathSelector(root.FullName, new[] { share }); + if (!home.StartsWith($"\\\\{share}\\")) + { + Describe("Test failed"); + return; + } + + Describe("Test successful"); + } + + + // Progress + private static async Task SpectreProgress(string? _) + { + Console.Clear(); + await PowerConsole.StartProgressAsync(async ctx => + { + var task = ctx.AddTask("Processing items", maxValue: 25); + for (var i = 0; i < 25; i++) + { + await Task.Delay(150); + task.Increment(1); + } + }); + Describe("Test successful if progress bar reached 100%", false); + } + private static async Task SpectreMultiProgress(string? _) + { + Console.Clear(); + await PowerConsole.StartProgressAsync(async ctx => + { + var taskA = ctx.AddTask("Task A", maxValue: 20); + var taskB = ctx.AddTask("Task B", maxValue: 10); + for (var i = 0; i < 20; i++) + { + await Task.Delay(100); + taskA.Increment(1); + if (i % 2 == 0) + taskB.Increment(1); + } + }); + Describe("Test successful if both progress bars completed", false); + } + private static async Task SpectreStatus(string? _) + { + Console.Clear(); + await PowerConsole.StartStatusAsync(async _ => + { + await Task.Delay(3000); + return true; + }); + Describe("Test successful if you saw a spinner for ~3 seconds", false); + } + + + // Selection + private static void Selection(string? _) + { + var persons = new[] + { + "Bob", + "Alice", + "Tim" + }; + + var alice = PowerConsole.EnsureSelection("Select Alice here", persons, x => x); + if (alice != "Alice") + { + Describe("Test failed"); + return; + } + + var tim = PowerConsole.AbortableSelection("Select Tim here", persons, x => x); + if (tim != "Tim") + { + Describe("Test failed"); + return; + } + + var nobody = PowerConsole.AbortableSelection("Select Abort here", persons, x => x); + if (nobody is not null) + { + Describe("Test failed"); + return; + } + + Describe("Test successful"); + } + + + + + // helpers + private static void Describe(string message, bool clear = true) + { + if (clear) Console.Clear(); + Console.Write(message); + Console.Write(". \tPress any key to proceed"); + Console.ReadKey(); + } +} diff --git a/TestsManual/ConsoleToolsManualTests/ConsoleToolsManualTests.csproj b/TestsManual/ToolboxConsoleManualTests/ToolboxConsoleManualTests.csproj similarity index 59% rename from TestsManual/ConsoleToolsManualTests/ConsoleToolsManualTests.csproj rename to TestsManual/ToolboxConsoleManualTests/ToolboxConsoleManualTests.csproj index 80906b6..eec59bc 100644 --- a/TestsManual/ConsoleToolsManualTests/ConsoleToolsManualTests.csproj +++ b/TestsManual/ToolboxConsoleManualTests/ToolboxConsoleManualTests.csproj @@ -2,13 +2,13 @@ - $(DefaultNetVersion)-windows + $(DefaultNetVersion) Exe - + diff --git a/TestsManual/ToolboxTopServiceManualTests/ToolboxTopServiceManualTests.csproj b/TestsManual/ToolboxTopServiceManualTests/ToolboxTopServiceManualTests.csproj index f0860d5..e2f6f08 100644 --- a/TestsManual/ToolboxTopServiceManualTests/ToolboxTopServiceManualTests.csproj +++ b/TestsManual/ToolboxTopServiceManualTests/ToolboxTopServiceManualTests.csproj @@ -1,16 +1,18 @@  - - Exe - $(DefaultNetVersion)-windows - false + + + Exe + $(DefaultNetVersion)-windows + false - SomeServiceTest - + SomeServiceTest + + + + + + - - - - diff --git a/ToolboxConsole/CommandApp/AppTypeRegistrar.cs b/ToolboxConsole/CommandApp/AppTypeRegistrar.cs index 9ee4f66..1f183b7 100644 --- a/ToolboxConsole/CommandApp/AppTypeRegistrar.cs +++ b/ToolboxConsole/CommandApp/AppTypeRegistrar.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Spectre.Console.Cli; -using System; namespace aemarcoCommons.ToolboxConsole.CommandApp; diff --git a/ToolboxConsole/CommandApp/ScTypeResolver.cs b/ToolboxConsole/CommandApp/ScTypeResolver.cs index 326c155..5ac6b06 100644 --- a/ToolboxConsole/CommandApp/ScTypeResolver.cs +++ b/ToolboxConsole/CommandApp/ScTypeResolver.cs @@ -1,5 +1,4 @@ using Spectre.Console.Cli; -using System; namespace aemarcoCommons.ToolboxConsole.CommandApp; diff --git a/ToolboxConsole/GlobalUsings.cs b/ToolboxConsole/GlobalUsings.cs index 6b174a7..5b3be33 100644 --- a/ToolboxConsole/GlobalUsings.cs +++ b/ToolboxConsole/GlobalUsings.cs @@ -1,5 +1,6 @@ // Global using directives +global using Spectre.Console; +global using System; global using System.Threading; global using System.Threading.Tasks; -global using Spectre.Console; \ No newline at end of file diff --git a/ToolboxConsole/HostApplicationBuilderExtensions.cs b/ToolboxConsole/HostApplicationBuilderExtensions.cs index 3bae9a7..bb42f52 100644 --- a/ToolboxConsole/HostApplicationBuilderExtensions.cs +++ b/ToolboxConsole/HostApplicationBuilderExtensions.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Hosting; using Serilog; using Spectre.Console.Cli; -using System; using System.Linq; namespace aemarcoCommons.ToolboxConsole; diff --git a/ToolboxConsole/Menu/ConsoleMenu.cs b/ToolboxConsole/Menu/ConsoleMenu.cs new file mode 100644 index 0000000..5936d47 --- /dev/null +++ b/ToolboxConsole/Menu/ConsoleMenu.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable once CheckNamespace +namespace aemarcoCommons.ToolboxConsole; + +public class ConsoleMenu +{ + private readonly ConsoleMenuItem[] _menuItems; + private readonly string _description; + private int _selectedItemIndex; + private bool _itemIsSelected; + + public ConsoleMenu(string description, IEnumerable menuItems) + { + _menuItems = menuItems.ToArray(); + _description = description; + } + + public void RunConsoleMenu() + { + if (!string.IsNullOrEmpty(_description)) + { + Console.WriteLine($"{_description}: {Environment.NewLine}"); + } + + StartConsoleDrawInLoopUntilInputIsMade(); + + _itemIsSelected = false; + _menuItems[_selectedItemIndex].Execute(); + } + + public async Task RunConsoleMenuAsync() + { + if (!string.IsNullOrEmpty(_description)) + { + AnsiConsole.MarkupLine(_description); + Console.WriteLine(); + } + + StartConsoleDrawInLoopUntilInputIsMade(); + + _itemIsSelected = false; + await _menuItems[_selectedItemIndex].ExecuteAsync(); + } + + + private void StartConsoleDrawInLoopUntilInputIsMade() + { + var topOffset = Console.CursorTop; + var bottomOffset = 0; + Console.CursorVisible = false; + + while (!_itemIsSelected) + { + for (var i = 0; i < _menuItems.Length; i++) + WriteConsoleItem(i, _selectedItemIndex); + + bottomOffset = Console.CursorTop; + var kb = Console.ReadKey(true); + HandleKeyPress(kb.Key); + + Console.SetCursorPosition(0, topOffset); + } + + Console.SetCursorPosition(0, bottomOffset); + Console.CursorVisible = true; + } + + private void HandleKeyPress(ConsoleKey pressedKey) + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (pressedKey) + { + case ConsoleKey.UpArrow: + _selectedItemIndex = (_selectedItemIndex == 0) ? _menuItems.Length - 1 : _selectedItemIndex - 1; + CheckForUnselectable(pressedKey); + break; + case ConsoleKey.DownArrow: + _selectedItemIndex = (_selectedItemIndex == _menuItems.Length - 1) ? 0 : _selectedItemIndex + 1; + CheckForUnselectable(pressedKey); + break; + case ConsoleKey.Enter: + _itemIsSelected = true; + break; + } + } + + private void CheckForUnselectable(ConsoleKey pressedKey) + { + if (_menuItems[_selectedItemIndex].GetType() == typeof(ConsoleMenuSeparator)) + HandleKeyPress(pressedKey); + } + + private void WriteConsoleItem(int itemIndex, int selectedItemIndex) + { + if (itemIndex == selectedItemIndex) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + } + + var text = _menuItems[itemIndex].Label; + if (_menuItems[itemIndex].GetType() == typeof(ConsoleMenuSeparator)) + text = text.PadRight(_menuItems.Max(x => x.Label.Length), _menuItems[itemIndex].Label[0]); + + Console.WriteLine(" {0,-20}", text); + Console.ResetColor(); + } +} + +public abstract class ConsoleMenuItem +{ + protected ConsoleMenuItem(string label) + { + Label = label; + } + + public string Label { get; } + public virtual void Execute() { } + public virtual Task ExecuteAsync() => Task.CompletedTask; +} + +public class ConsoleMenuItem : ConsoleMenuItem +{ + private readonly Action? _callBack; + private readonly Func? _asyncCallBack; + private readonly T? _underlyingObject; + + public ConsoleMenuItem(string label, Action callback, T? underlyingObject = default) + : base(label) + { + _callBack = callback; + _underlyingObject = underlyingObject; + } + + public ConsoleMenuItem(string label, Func callback, T? underlyingObject = default) + : base(label) + { + _asyncCallBack = callback; + _underlyingObject = underlyingObject; + } + + public override void Execute() + { + _callBack?.Invoke(_underlyingObject); + _asyncCallBack?.Invoke(_underlyingObject).GetAwaiter().GetResult(); + } + + public override async Task ExecuteAsync() + { + _callBack?.Invoke(_underlyingObject); + if (_asyncCallBack is not null) + await _asyncCallBack.Invoke(_underlyingObject); + } +} + +public class ConsoleMenuSeparator : ConsoleMenuItem +{ + public ConsoleMenuSeparator(char separatorChar = '-') + : base(separatorChar.ToString()) + { } +} diff --git a/ToolboxConsole/Power/Commands.cs b/ToolboxConsole/Power/Commands.cs new file mode 100644 index 0000000..a1b7f14 --- /dev/null +++ b/ToolboxConsole/Power/Commands.cs @@ -0,0 +1,42 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +// ReSharper disable once CheckNamespace +namespace aemarcoCommons.ToolboxConsole; + +public static partial class PowerConsole +{ + public static void ClearCurrentLine() + { + var currentLineCursor = Console.CursorTop; + Console.SetCursorPosition(0, Console.CursorTop); + Console.Write(new string(' ', Console.WindowWidth)); + Console.SetCursorPosition(0, currentLineCursor); + } + + [SupportedOSPlatform("windows")] + public static void ShowWindow() + { + var handle = GetConsoleWindow(); + ShowWindow(handle, SwShow); + } + + + [SupportedOSPlatform("windows")] + public static void HideWindow() + { + var handle = GetConsoleWindow(); + ShowWindow(handle, SwHide); + } + + [LibraryImport("kernel32.dll")] + private static partial nint GetConsoleWindow(); + + private const int SwShow = 5; + private const int SwHide = 0; + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + // ReSharper disable once UnusedMethodReturnValue.Local + private static partial bool ShowWindow(nint hWnd, int nCmdShow); +} diff --git a/ToolboxConsole/Power/Inputs.cs b/ToolboxConsole/Power/Inputs.cs new file mode 100644 index 0000000..e6cfaa7 --- /dev/null +++ b/ToolboxConsole/Power/Inputs.cs @@ -0,0 +1,76 @@ +// ReSharper disable once CheckNamespace +namespace aemarcoCommons.ToolboxConsole; + +public static partial class PowerConsole +{ + public static string EnsureTextInput(string prompt, int minLength = 1, bool clear = false) + { + if (clear) + Console.Clear(); + + var textPrompt = new TextPrompt($"[purple]{prompt}[/]") + .PromptStyle("green") + .Validate(x => + x.Length < minLength + ? ValidationResult.Error($"[red]Must be at least {minLength} letter(s)[/]") + : ValidationResult.Success()); + textPrompt.AllowEmpty = minLength == 0; + + return AnsiConsole.Prompt(textPrompt); + } + + public static int EnsureIntInput(string prompt, int min = int.MinValue, int max = int.MaxValue, bool clear = false) + { + if (clear) + Console.Clear(); + + return AnsiConsole.Prompt( + new TextPrompt($"[purple]{prompt.EscapeMarkup()}[/]") + .PromptStyle("green") + .Validate(number => + { + if (number < min) + return ValidationResult.Error($"Number must be equal or greater than {min}."); + if (number > max) + return ValidationResult.Error($"Number must be smaller or equal than {max}."); + return ValidationResult.Success(); + })); + } + + public static int? EnsureNullableIntInput(string prompt, int min = int.MinValue, int max = int.MaxValue, bool clear = false) + { + if (clear) + Console.Clear(); + + return AnsiConsole.Prompt( + new TextPrompt($"[purple]{prompt}[/]") + .PromptStyle("green") + .AllowEmpty() + .DefaultValue(null) + .ShowDefaultValue(false) + .Validate(number => + { + if (number < min) + return ValidationResult.Error($"Number must be equal or greater than {min}."); + if (number > max) + return ValidationResult.Error($"Number must be smaller or equal than {max}."); + return ValidationResult.Success(); + })); + } + + public static bool EnsureDecision(string question, bool clear = true) + { + if (clear) + Console.Clear(); + + static string GetText(bool x) => x ? "Yes" : "No"; + var result = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[purple]{question}[/]") + .UseConverter(GetText) + .AddChoices([true, false])); + AnsiConsole.MarkupLine($"[purple]{question}[/] [green]{GetText(result)}[/]"); + + return result; + } +} diff --git a/ToolboxConsole/Power/PathNavigation.cs b/ToolboxConsole/Power/PathNavigation.cs new file mode 100644 index 0000000..f037f8f --- /dev/null +++ b/ToolboxConsole/Power/PathNavigation.cs @@ -0,0 +1,152 @@ +using aemarcoCommons.ToolboxConsole.Shares; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; + +// ReSharper disable once CheckNamespace +namespace aemarcoCommons.ToolboxConsole; + +public static partial class PowerConsole +{ + /// + /// Selection of a path in a ConsoleMenu + /// + /// path to start at + /// servers to include for share selection + /// show hidden folders + /// selected path + public static string PathSelector(string path, IEnumerable? includedServers = null, bool showHidden = false) + { + var serverItems = includedServers?.Distinct().ToArray(); + var dir = new DirectoryInfo(path); + if (!dir.Exists) + throw new DirectoryNotFoundException($"{path}"); + Console.Clear(); + + //one level up + var dirItems = new List + { + new ConsoleMenuItem("..", x => + { + path = PathSelector( + x is null + ? DriveSelector(serverItems) + : x.FullName, + serverItems, + showHidden); + }, dir.Parent) + }; + + //one level down + foreach (var sub in dir + .GetDirectories() + .Where(x => showHidden || !x.Attributes.HasFlag(FileAttributes.Hidden))) + { + var temp = sub; + dirItems.Add(new ConsoleMenuItem(temp.Name, _ => + { + path = PathSelector(temp.FullName, serverItems, showHidden); + })); + } + + //select + dirItems.Add(new ConsoleMenuSeparator()); + dirItems.Add(new ConsoleMenuItem("Select", _ => + { + path = dir.FullName; + })); + + var menu = new ConsoleMenu(dir.FullName, dirItems); + menu.RunConsoleMenu(); + + return path; + } + + /// + /// Selection of a drive in a ConsoleMenu + /// + public static string DriveSelector(IEnumerable? servers = null) + { + string? path = null; + Console.Clear(); + + var driveItems = new List(); + foreach (var drive in DriveInfo.GetDrives().Where(x => x.IsReady)) + { + var temp = drive; + driveItems.Add(new ConsoleMenuItem($"{temp.Name} ({temp.VolumeLabel})", _ => + { + path = temp.RootDirectory.FullName; + })); + } + + if (OperatingSystem.IsWindows() && servers?.ToList() is { Count: > 0 } list) + { + driveItems.Add(new ConsoleMenuSeparator()); + driveItems.Add(new ConsoleMenuItem("Network", _ => + { + if (!OperatingSystem.IsWindows()) + throw new NotSupportedException(); + path = ServerSelector(list) ?? DriveSelector(list); + })); + } + + var menu = new ConsoleMenu("Drives", driveItems); + menu.RunConsoleMenu(); + + return path!; + } + + /// + /// Selection of a share for given servers + /// + [SupportedOSPlatform("windows")] + public static string? ServerSelector(IEnumerable servers) + { + var serverItems = servers.ToList(); + if (serverItems.Count == 1) + return ShareSelector(serverItems[0]); + + var serverItem = AbortableSelection("Server", serverItems, x => x.TrimStart('\\')); + return ShareSelector(serverItem); + } + + /// + /// Selection of a share for given server + /// + /// unc path to share or null when no shares are available + [SupportedOSPlatform("windows")] + public static string? ShareSelector(string? server) + { + if (server is null) + return null; + + server = server.TrimStart('\\'); + Console.Clear(); + string? path = null; + var shares = new List>(); + + foreach (Share share in ShareCollection.GetShares(server)) + { + if (!share.IsFileSystem || + share.ShareType != ShareType.Disk) + continue; + + var temp = share.Root; + if (temp is null) + continue; + shares.Add(new ConsoleMenuItem($"{share.Server} --> {share.NetName}", _ => + { + path = temp.FullName; + })); + } + + if (shares.Count == 0) + return path; + + var menu = new ConsoleMenu($"{server} shares", shares); + menu.RunConsoleMenu(); + return path; + } +} diff --git a/ToolboxConsole/Power/Progress.cs b/ToolboxConsole/Power/Progress.cs index 9bfd184..abefcce 100644 --- a/ToolboxConsole/Power/Progress.cs +++ b/ToolboxConsole/Power/Progress.cs @@ -6,7 +6,7 @@ namespace aemarcoCommons.ToolboxConsole; public static partial class PowerConsole { - public static void StartProgress(System.Action work) + public static void StartProgress(Action work) { AnsiConsole.Progress() .Columns( @@ -18,7 +18,7 @@ public static void StartProgress(System.Action work) .Start(work); } - public static T StartProgress(System.Func work) + public static T StartProgress(Func work) { return AnsiConsole.Progress() .Columns( @@ -31,7 +31,7 @@ public static T StartProgress(System.Func work) } - public static async Task StartProgressAsync(System.Func work) + public static async Task StartProgressAsync(Func work) { await AnsiConsole.Progress() .Columns( @@ -43,7 +43,7 @@ await AnsiConsole.Progress() .StartAsync(work); } - public static async Task StartProgressAsync(System.Func work, + public static async Task StartProgressAsync(Func work, T request, CancellationToken cancellationToken) { @@ -52,7 +52,7 @@ await StartProgressAsync(async ctx => } - public static async Task StartProgressAsync(System.Func> work) + public static async Task StartProgressAsync(Func> work) { return await AnsiConsole.Progress() .Columns( @@ -64,4 +64,18 @@ public static async Task StartProgressAsync(System.Func StartStatusAsync(Func> work) + { + return await AnsiConsole.Status() + .SpinnerStyle(Style.Parse("green")) + .StartAsync("[purple]Doing stuff...[/]", work); + } + } \ No newline at end of file diff --git a/ToolboxConsole/Power/Selection.cs b/ToolboxConsole/Power/Selection.cs new file mode 100644 index 0000000..4de4ac1 --- /dev/null +++ b/ToolboxConsole/Power/Selection.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace aemarcoCommons.ToolboxConsole; + +public static partial class PowerConsole +{ + /// + /// Ensures a selection via Spectre SelectionPrompt + /// + public static T EnsureSelection(string header, IEnumerable selectable, Func displayProperty) + where T : class + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[purple]{header}[/]") + .UseConverter(displayProperty) + .AddChoices([.. selectable])); + } + + /// + /// Allows selection via ConsoleMenu, adds Abort at the end — returns null on abort + /// + public static T? AbortableSelection(string header, IEnumerable selectable, Func displayProperty) + where T : class + { + return MenuSelectionHelper(header, selectable, displayProperty, abortable: true); + } + + private static T? MenuSelectionHelper(string header, IEnumerable selectable, Func displayProperty, bool abortable, bool clear = true) + where T : class + { + T? result = null; + + if (clear) Console.Clear(); + var items = new List(); + + foreach (var item in selectable) + { + var temp = item; + items.Add(new ConsoleMenuItem(displayProperty(temp), x => + { + result = x; + }, temp)); + } + + if (abortable) + { + items.Add(new ConsoleMenuSeparator()); + items.Add(new ConsoleMenuItem("Abort", x => + { + result = x; + })); + } + + var menu = new ConsoleMenu(header, items); + menu.RunConsoleMenu(); + + return result; + } +} diff --git a/ToolboxConsole/Shares/Shares.cs b/ToolboxConsole/Shares/Shares.cs new file mode 100644 index 0000000..168a6e6 --- /dev/null +++ b/ToolboxConsole/Shares/Shares.cs @@ -0,0 +1,545 @@ +using System.Collections; +using System.IO; +using System.Runtime.InteropServices; +// ReSharper disable All +// author and credits to https://www.codeproject.com/Articles/2939/Network-Shares-and-UNC-paths + +namespace aemarcoCommons.ToolboxConsole.Shares; + +#region Share Type + +/// +/// Type of share +/// +[Flags] +public enum ShareType +{ + /// Disk share + Disk = 0, + /// Printer share + Printer = 1, + /// Device share + Device = 2, + /// IPC share + IPC = 3, + /// Special share + Special = -2147483648, // 0x80000000, +} + +#endregion + +#region Share + +/// +/// Information about a local share +/// +public class Share +{ + #region Private data + + private readonly string _server; + private readonly string _netName; + private readonly string _path; + private readonly ShareType _shareType; + private readonly string _remark; + + #endregion + + #region Constructor + + public Share(string server, string netName, string path, ShareType shareType, string remark) + { + if (ShareType.Special == shareType && "IPC$" == netName) + { + shareType |= ShareType.IPC; + } + + _server = server; + _netName = netName; + _path = path; + _shareType = shareType; + _remark = remark; + } + + #endregion + + #region Properties + + public string Server => _server; + public string NetName => _netName; + public string Path => _path; + public ShareType ShareType => _shareType; + public string Remark => _remark; + + /// + /// Returns true if this is a file system share + /// + public bool IsFileSystem + { + get + { + if (0 != (_shareType & ShareType.Device)) return false; + if (0 != (_shareType & ShareType.IPC)) return false; + if (0 != (_shareType & ShareType.Printer)) return false; + if (0 == (_shareType & ShareType.Special)) return true; + if (ShareType.Special == _shareType && null != _netName && 0 != _netName.Length) + return true; + else + return false; + } + } + + /// + /// Get the root of a disk-based share + /// + public DirectoryInfo? Root + { + get + { + if (IsFileSystem) + { + if (null == _server || 0 == _server.Length) + if (null == _path || 0 == _path.Length) + return new DirectoryInfo(ToString()); + else + return new DirectoryInfo(_path); + else + return new DirectoryInfo(ToString()); + } + else + return null; + } + } + + #endregion + + public override string ToString() + { + if (null == _server || 0 == _server.Length) + return string.Format(@"\\{0}\{1}", Environment.MachineName, _netName); + else + return string.Format(@"\\{0}\{1}", _server, _netName); + } + + public bool MatchesPath(string? path) + { + if (!IsFileSystem) return false; + if (null == path || 0 == path.Length) return true; + return path.ToLower().StartsWith(_path.ToLower()); + } +} + +#endregion + +#region Share Collection + +/// +/// A collection of shares +/// +public class ShareCollection : ReadOnlyCollectionBase +{ + #region Platform + + protected static bool IsNT => PlatformID.Win32NT == Environment.OSVersion.Platform; + + protected static bool IsW2KUp + { + get + { + OperatingSystem os = Environment.OSVersion; + return PlatformID.Win32NT == os.Platform && os.Version.Major >= 5; + } + } + + #endregion + + #region Interop + + #region Constants + + protected const int MAX_PATH = 260; + protected const int NO_ERROR = 0; + protected const int ERROR_ACCESS_DENIED = 5; + protected const int ERROR_WRONG_LEVEL = 124; + protected const int ERROR_MORE_DATA = 234; + protected const int ERROR_NOT_CONNECTED = 2250; + protected const int UNIVERSAL_NAME_INFO_LEVEL = 1; + protected const int MAX_SI50_ENTRIES = 20; + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + protected struct UNIVERSAL_NAME_INFO + { + [MarshalAs(UnmanagedType.LPTStr)] + public string lpUniversalName; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + protected struct SHARE_INFO_2 + { + [MarshalAs(UnmanagedType.LPWStr)] + public string NetName; + public ShareType ShareType; + [MarshalAs(UnmanagedType.LPWStr)] + public string Remark; + public int Permissions; + public int MaxUsers; + public int CurrentUsers; + [MarshalAs(UnmanagedType.LPWStr)] + public string Path; + [MarshalAs(UnmanagedType.LPWStr)] + public string Password; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + protected struct SHARE_INFO_1 + { + [MarshalAs(UnmanagedType.LPWStr)] + public string NetName; + public ShareType ShareType; + [MarshalAs(UnmanagedType.LPWStr)] + public string Remark; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] + protected struct SHARE_INFO_50 + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 13)] + public string NetName; + public byte bShareType; + public ushort Flags; + [MarshalAs(UnmanagedType.LPTStr)] + public string Remark; + [MarshalAs(UnmanagedType.LPTStr)] + public string Path; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)] + public string PasswordRW; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)] + public string PasswordRO; + public ShareType ShareType => (ShareType)((int)bShareType & 0x7F); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] + protected struct SHARE_INFO_1_9x + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 13)] + public string NetName; + public byte Padding; + public ushort bShareType; + [MarshalAs(UnmanagedType.LPTStr)] + public string Remark; + public ShareType ShareType => (ShareType)((int)bShareType & 0x7FFF); + } + + #endregion + + #region Functions + + [DllImport("mpr", CharSet = CharSet.Auto)] + protected static extern int WNetGetUniversalName(string lpLocalPath, + int dwInfoLevel, ref UNIVERSAL_NAME_INFO lpBuffer, ref int lpBufferSize); + + [DllImport("mpr", CharSet = CharSet.Auto)] + protected static extern int WNetGetUniversalName(string lpLocalPath, + int dwInfoLevel, IntPtr lpBuffer, ref int lpBufferSize); + + [DllImport("netapi32", CharSet = CharSet.Unicode)] + protected static extern int NetShareEnum(string? lpServerName, int dwLevel, + out IntPtr lpBuffer, int dwPrefMaxLen, out int entriesRead, + out int totalEntries, ref int hResume); + + [DllImport("svrapi", CharSet = CharSet.Ansi)] + protected static extern int NetShareEnum( + [MarshalAs(UnmanagedType.LPTStr)] string? lpServerName, int dwLevel, + IntPtr lpBuffer, ushort cbBuffer, out ushort entriesRead, + out ushort totalEntries); + + [DllImport("netapi32")] + protected static extern int NetApiBufferFree(IntPtr lpBuffer); + + #endregion + + #region Enumerate shares + + protected static void EnumerateSharesNT(string? server, ShareCollection shares) + { + int level = 2; + IntPtr pBuffer = IntPtr.Zero; + + try + { + var hResume = 0; + var nRet = NetShareEnum(server, level, out pBuffer, -1, + out var entriesRead, out var totalEntries, ref hResume); + + if (ERROR_ACCESS_DENIED == nRet) + { + level = 1; + nRet = NetShareEnum(server, level, out pBuffer, -1, + out entriesRead, out totalEntries, ref hResume); + } + + if (NO_ERROR == nRet && entriesRead > 0) + { + Type t = (2 == level) ? typeof(SHARE_INFO_2) : typeof(SHARE_INFO_1); + int offset = Marshal.SizeOf(t); + + IntPtr pItem = pBuffer; + for (int i = 0; i < entriesRead; i++, pItem += offset) + { + if (1 == level) + { + SHARE_INFO_1 si = (SHARE_INFO_1)Marshal.PtrToStructure(pItem, t)!; + shares.Add(si.NetName, string.Empty, si.ShareType, si.Remark); + } + else + { + SHARE_INFO_2 si = (SHARE_INFO_2)Marshal.PtrToStructure(pItem, t)!; + shares.Add(si.NetName, si.Path, si.ShareType, si.Remark); + } + } + } + } + finally + { + if (IntPtr.Zero != pBuffer) + NetApiBufferFree(pBuffer); + } + } + + protected static void EnumerateShares9x(string? server, ShareCollection shares) + { + int level = 50; + + Type t = typeof(SHARE_INFO_50); + int size = Marshal.SizeOf(t); + ushort cbBuffer = (ushort)(MAX_SI50_ENTRIES * size); + IntPtr pBuffer = Marshal.AllocHGlobal(cbBuffer); + + try + { + var nRet = NetShareEnum(server, level, pBuffer, cbBuffer, + out var entriesRead, out var totalEntries); + + if (ERROR_WRONG_LEVEL == nRet) + { + level = 1; + t = typeof(SHARE_INFO_1_9x); + size = Marshal.SizeOf(t); + + nRet = NetShareEnum(server, level, pBuffer, cbBuffer, + out entriesRead, out totalEntries); + } + + if (NO_ERROR == nRet || ERROR_MORE_DATA == nRet) + { + for (long i = 0, lpItem = pBuffer.ToInt64(); i < entriesRead; i++, lpItem += size) + { + IntPtr pItem = new IntPtr(lpItem); + + if (1 == level) + { + SHARE_INFO_1_9x si = (SHARE_INFO_1_9x)Marshal.PtrToStructure(pItem, t)!; + shares.Add(si.NetName, string.Empty, si.ShareType, si.Remark); + } + else + { + SHARE_INFO_50 si = (SHARE_INFO_50)Marshal.PtrToStructure(pItem, t)!; + shares.Add(si.NetName, si.Path, si.ShareType, si.Remark); + } + } + } + else + Console.WriteLine(nRet); + } + finally + { + Marshal.FreeHGlobal(pBuffer); + } + } + + protected static void EnumerateShares(string server, ShareCollection shares) + { + if (null != server && 0 != server.Length && !IsW2KUp) + { + server = server.ToUpper(); + if (!('\\' == server[0] && '\\' == server[1])) + server = @"\\" + server; + } + + if (IsNT) + EnumerateSharesNT(server, shares); + else + EnumerateShares9x(server, shares); + } + + #endregion + + #endregion + + #region Static methods + + public static bool IsValidFilePath(string fileName) + { + if (null == fileName || 0 == fileName.Length) return false; + + char drive = char.ToUpper(fileName[0]); + if ('A' > drive || drive > 'Z') return false; + else if (Path.VolumeSeparatorChar != fileName[1]) return false; + else if (Path.DirectorySeparatorChar != fileName[2]) return false; + else return true; + } + + public static string PathToUnc(string fileName) + { + if (null == fileName || 0 == fileName.Length) return string.Empty; + + fileName = Path.GetFullPath(fileName); + if (!IsValidFilePath(fileName)) return fileName; + + UNIVERSAL_NAME_INFO rni = new UNIVERSAL_NAME_INFO(); + int bufferSize = Marshal.SizeOf(rni); + + var nRet = WNetGetUniversalName(fileName, UNIVERSAL_NAME_INFO_LEVEL, ref rni, ref bufferSize); + + if (ERROR_MORE_DATA == nRet) + { + IntPtr pBuffer = Marshal.AllocHGlobal(bufferSize); + try + { + nRet = WNetGetUniversalName(fileName, UNIVERSAL_NAME_INFO_LEVEL, pBuffer, ref bufferSize); + if (NO_ERROR == nRet) + rni = (UNIVERSAL_NAME_INFO)Marshal.PtrToStructure(pBuffer, typeof(UNIVERSAL_NAME_INFO))!; + } + finally + { + Marshal.FreeHGlobal(pBuffer); + } + } + + switch (nRet) + { + case NO_ERROR: + return rni.lpUniversalName; + case ERROR_NOT_CONNECTED: + ShareCollection shi = LocalShares; + if (null != shi) + { + Share? share = shi[fileName]; + if (null != share) + { + string path = share.Path; + if (null != path && 0 != path.Length) + { + int index = path.Length; + if (Path.DirectorySeparatorChar != path[path.Length - 1]) + index++; + if (index < fileName.Length) + fileName = fileName.Substring(index); + else + fileName = string.Empty; + fileName = Path.Combine(share.ToString(), fileName); + } + } + } + return fileName; + default: + Console.WriteLine("Unknown return value: {0}", nRet); + return string.Empty; + } + } + + public static Share? PathToShare(string? fileName) + { + if (null == fileName || 0 == fileName.Length) return null; + fileName = Path.GetFullPath(fileName); + if (!IsValidFilePath(fileName)) return null; + ShareCollection shi = LocalShares; + return null == shi ? null : shi[fileName]; + } + + #endregion + + #region Local shares + + private static ShareCollection? _local; + + public static ShareCollection LocalShares => _local ??= new ShareCollection(); + + public static ShareCollection GetShares(string server) => new ShareCollection(server); + + #endregion + + #region Private Data + + private readonly string _server; + + #endregion + + #region Constructor + + private ShareCollection() + { + _server = string.Empty; + EnumerateShares(_server, this); + } + + private ShareCollection(string server) + { + _server = server; + EnumerateShares(_server, this); + } + + #endregion + + #region Add + + protected void Add(Share share) => InnerList.Add(share); + + protected void Add(string netName, string path, ShareType shareType, string remark) => + InnerList.Add(new Share(_server, netName, path, shareType, remark)); + + #endregion + + #region Properties + + public string Server => _server; + + public Share this[int index] => (Share)InnerList[index]!; + + public Share? this[string? path] + { + get + { + if (null == path || 0 == path.Length) return null; + path = Path.GetFullPath(path); + if (!IsValidFilePath(path)) return null; + + Share? match = null; + for (int i = 0; i < InnerList.Count; i++) + { + Share s = (Share)InnerList[i]!; + if (s.IsFileSystem && s.MatchesPath(path)) + { + if (null == match) + match = s; + else if (match.Path.Length < s.Path.Length) + { + if (ShareType.Disk == s.ShareType || ShareType.Disk != match.ShareType) + match = s; + } + } + } + return match; + } + } + + #endregion + + public void CopyTo(Share[] array, int index) => InnerList.CopyTo(array, index); +} + +#endregion diff --git a/ToolboxConsole/Shell/PowerShellHelper.cs b/ToolboxConsole/Shell/PowerShellHelper.cs index 0420045..c0f58b7 100644 --- a/ToolboxConsole/Shell/PowerShellHelper.cs +++ b/ToolboxConsole/Shell/PowerShellHelper.cs @@ -1,5 +1,4 @@ using CliWrap; -using System; using System.Diagnostics; using System.Text; diff --git a/ToolboxConsole/ToolboxConsole.csproj b/ToolboxConsole/ToolboxConsole.csproj index d90cffc..61fa619 100644 --- a/ToolboxConsole/ToolboxConsole.csproj +++ b/ToolboxConsole/ToolboxConsole.csproj @@ -5,6 +5,7 @@ $(DefaultNetVersion) aemarcoCommons.ToolboxConsole aemarcoCommons.ToolboxConsole + true diff --git a/aemarcoCommons.sln b/aemarcoCommons.sln index e7550ad..bc9ef21 100644 --- a/aemarcoCommons.sln +++ b/aemarcoCommons.sln @@ -59,7 +59,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestsManual", "TestsManual" TestsManual\Directory.Package.props = TestsManual\Directory.Package.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleToolsManualTests", "TestsManual\ConsoleToolsManualTests\ConsoleToolsManualTests.csproj", "{6630E679-714B-ECD8-4ED7-3748057B43A2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToolboxConsoleManualTests", "TestsManual\ToolboxConsoleManualTests\ToolboxConsoleManualTests.csproj", "{992539E6-B78C-4393-A734-7443A1A6CA3B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToolboxTopServiceManualTests", "TestsManual\ToolboxTopServiceManualTests\ToolboxTopServiceManualTests.csproj", "{8CEA2807-70CD-149A-C925-A8E82F63EA89}" EndProject @@ -145,10 +145,10 @@ Global {ABA4F7FC-1805-4ABA-0D83-B8F6DEA28020}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABA4F7FC-1805-4ABA-0D83-B8F6DEA28020}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABA4F7FC-1805-4ABA-0D83-B8F6DEA28020}.Release|Any CPU.Build.0 = Release|Any CPU - {6630E679-714B-ECD8-4ED7-3748057B43A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6630E679-714B-ECD8-4ED7-3748057B43A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6630E679-714B-ECD8-4ED7-3748057B43A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6630E679-714B-ECD8-4ED7-3748057B43A2}.Release|Any CPU.Build.0 = Release|Any CPU +{992539E6-B78C-4393-A734-7443A1A6CA3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {992539E6-B78C-4393-A734-7443A1A6CA3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {992539E6-B78C-4393-A734-7443A1A6CA3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {992539E6-B78C-4393-A734-7443A1A6CA3B}.Release|Any CPU.Build.0 = Release|Any CPU {8CEA2807-70CD-149A-C925-A8E82F63EA89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CEA2807-70CD-149A-C925-A8E82F63EA89}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CEA2807-70CD-149A-C925-A8E82F63EA89}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -179,7 +179,7 @@ Global {CB560619-425D-4A0B-B376-735090AFA1AE} = {284B28FC-9401-48CF-B5D1-27449B9BF167} {DFAD2524-3112-5992-3DAC-331C670AD4B7} = {284B28FC-9401-48CF-B5D1-27449B9BF167} {ABA4F7FC-1805-4ABA-0D83-B8F6DEA28020} = {284B28FC-9401-48CF-B5D1-27449B9BF167} - {6630E679-714B-ECD8-4ED7-3748057B43A2} = {79920A53-2275-4774-A137-CFA7029190A5} +{992539E6-B78C-4393-A734-7443A1A6CA3B} = {79920A53-2275-4774-A137-CFA7029190A5} {8CEA2807-70CD-149A-C925-A8E82F63EA89} = {79920A53-2275-4774-A137-CFA7029190A5} {6CB7F8D0-44D5-2625-F361-207CB42D701A} = {79920A53-2275-4774-A137-CFA7029190A5} {7CE234BD-4E8A-871E-42A6-52DB5175AF6D} = {284B28FC-9401-48CF-B5D1-27449B9BF167}