Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 48 additions & 30 deletions src/Fallout.Build/Utilities/ConsoleUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,40 @@
using System.Drawing;
using System.Linq;
using System.Text;
using Fallout.Build.Utilities;

namespace Fallout.Common.Utilities;

public class ConsoleUtility
{
private static int BufferWidth => EnvironmentInfo.IsWin ? Console.BufferWidth - 1 : Console.BufferWidth;
public static IConsole ConsoleWrapper { get; set; } = new SystemConsole();

private static int BufferWidth => ConsoleWrapper.BufferWidth;

private static string Default => "[default: {0}]";

private static string Confirmed => "¬";

private static string Selected => "»";

private static string Unselected => " ";

private const ConsoleKey ConfirmationKey = ConsoleKey.Enter;
private const ConsoleKey InterruptKey = ConsoleKey.F8;

private static bool s_interrupted;
internal static bool IsInterrupted;
private static readonly char[] AllowedSpecialCharacters = ['.', '/', '\\', '_', '-'];

internal static bool IsValidInputKey(ConsoleKeyInfo key) =>
key.Key is >= ConsoleKey.A and <= ConsoleKey.Z
|| key.Key is >= ConsoleKey.D0 and <= ConsoleKey.D9
|| AllowedSpecialCharacters.Any(x => x == key.KeyChar)
|| char.IsLetterOrDigit(key.KeyChar);

// ReSharper disable once CognitiveComplexity
public static string PromptForInput(string question, string defaultValue)
{
if (s_interrupted)
if (IsInterrupted)
return defaultValue;

Host.Information(question);
Expand All @@ -31,40 +44,40 @@ public static string PromptForInput(string question, string defaultValue)
var input = new StringBuilder();
do
{
Console.CursorLeft = 0;
Console.WriteLine(Selected.PadRight(BufferWidth), Color.DeepSkyBlue);
Console.CursorTop--;
Console.CursorLeft = 3;
ConsoleWrapper.CursorLeft = 0;
ConsoleWrapper.WriteLine(Selected.PadRight(BufferWidth), Color.DeepSkyBlue);
ConsoleWrapper.CursorTop--;
ConsoleWrapper.CursorLeft = 3;

if (input.Length > 0)
{
Console.Write(input.ToString());
ConsoleWrapper.Write(input.ToString());
}
else if (defaultValue != null)
{
Console.Write($" {string.Format(Default, defaultValue)}", Color.DarkGray);
Console.CursorLeft = 3;
ConsoleWrapper.Write($" {string.Format(Default, defaultValue)}", Color.DarkGray);
ConsoleWrapper.CursorLeft = 3;
}

key = Console.ReadKey(intercept: true);
if (key.IsValidInputKey())
key = ConsoleWrapper.ReadKey(intercept: true);
if (IsValidInputKey(key))
input.Append(key.KeyChar);
else if (key.Key == ConsoleKey.Backspace && input.Length > 0)
input.Remove(input.Length - 1, length: 1);
else if (key.Key == InterruptKey)
s_interrupted = true;
IsInterrupted = true;
} while (key.Key is not (ConfirmationKey or InterruptKey));

var result = input.Length > 0 ? input.ToString() : defaultValue;
Console.CursorLeft = 0;
Console.WriteLine($"{Confirmed} {result ?? "<null>"}".PadRight(BufferWidth), Color.Lime);
ConsoleWrapper.CursorLeft = 0;
ConsoleWrapper.WriteLine($"{Confirmed} {result ?? "<null>"}".PadRight(BufferWidth), Color.Lime);
return result;
}

// ReSharper disable once CognitiveComplexity
public static T PromptForChoice<T>(string question, params (T Value, string Description)[] options)
{
if (s_interrupted)
if (IsInterrupted)
return options.First().Value;

var selection = 0;
Expand All @@ -79,25 +92,28 @@ public static T PromptForChoice<T>(string question, params (T Value, string Desc
var selected = i == selection;
var prefix = selected ? Selected : Unselected;
var color = selected ? Color.DeepSkyBlue : Color.DarkGray;
Console.WriteLine($"{prefix} {option.Description}", color);
ConsoleWrapper.WriteLine($"{prefix} {option.Description}", color);
}

key = Console.ReadKey(intercept: true).Key;
key = ConsoleWrapper.ReadKey(intercept: true).Key;
if (key == ConsoleKey.UpArrow)
selection--;
else if (key == ConsoleKey.DownArrow)
selection++;
else if (key == InterruptKey)
s_interrupted = true;
IsInterrupted = true;

selection = Math.Max(val1: 0, Math.Min(options.Length - 1, selection));

Console.CursorTop -= options.Length;
foreach (var option in options)
Console.WriteLine(' '.Repeat(BufferWidth));
Console.CursorTop -= options.Length;
} while (!(key == ConfirmationKey || key == InterruptKey));
ConsoleWrapper.CursorTop -= options.Length;
foreach (var unused in options)
ConsoleWrapper.WriteLine(' '.Repeat(BufferWidth));

Console.WriteLine($"{Confirmed} {options[selection].Description}", Color.Lime);
ConsoleWrapper.CursorTop -= options.Length;
}
while (key is not (ConfirmationKey or InterruptKey));

ConsoleWrapper.WriteLine($"{Confirmed} {options[selection].Description}", Color.Lime);

return options[selection].Value;
}
Expand All @@ -109,7 +125,7 @@ public static string ReadSecret()

do
{
var key = Console.ReadKey(intercept: true);
var key = ConsoleWrapper.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Backspace)
{
if (secret.Length > 0)
Expand All @@ -119,22 +135,24 @@ public static string ReadSecret()
(key.Modifiers & ConsoleModifiers.Alt) != 0 && EnvironmentInfo.IsOsx
? secret.Length
: 1;

var length = secret.Length - charsToRemove;
secret = secret[..length];
Console.Write(string.Concat(Enumerable.Repeat("\b \b", charsToRemove)));
ConsoleWrapper.Write(string.Concat(Enumerable.Repeat("\b \b", charsToRemove)));
}
}
else if (key.Key == ConsoleKey.Enter)
{
Console.WriteLine();
ConsoleWrapper.WriteLine();
break;
}
else if (!char.IsControl(key.KeyChar))
{
secret += key.KeyChar;
Console.Write("*");
ConsoleWrapper.Write("*");
}
} while (true);
}
while (true);

return secret;
}
Expand Down
17 changes: 0 additions & 17 deletions src/Fallout.Build/Utilities/ConsoleUtilityExtensions.cs

This file was deleted.

15 changes: 15 additions & 0 deletions src/Fallout.Build/Utilities/IConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Drawing;

namespace Fallout.Build.Utilities;

public interface IConsole
{
int BufferWidth { get; }
int CursorLeft { get; set; }
int CursorTop { get; set; }
void Write(string value, Color? color = null);
void WriteLine();
void WriteLine(string value, Color? color = null);
ConsoleKeyInfo ReadKey(bool intercept);
}
40 changes: 40 additions & 0 deletions src/Fallout.Build/Utilities/SystemConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using Fallout.Common;

namespace Fallout.Build.Utilities;

public class SystemConsole : IConsole
{
public int BufferWidth => EnvironmentInfo.IsWin ? Console.BufferWidth - 1 : Console.BufferWidth;

public int CursorLeft
{
get => Console.CursorLeft;
set => Console.CursorLeft = value;
}

public int CursorTop
{
get => Console.CursorTop;
set => Console.CursorTop = value;
}

public void Write(string value, Color? color = null)
{
Console.Write(value);
}

public void WriteLine()
{
Console.WriteLine();
}

public void WriteLine(string value, Color? color = null)
{
Console.WriteLine(value);
}

public ConsoleKeyInfo ReadKey(bool intercept) => Console.ReadKey(intercept);
}
51 changes: 51 additions & 0 deletions tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Fallout.Common.Utilities;
using Xunit;
using System;
using System.Collections.Generic;
using System.Drawing;
using Fallout.Build.Utilities;
using FluentAssertions;

namespace Fallout.Build.Tests.Utilities;

public class ConsoleUtilitySpecs
{
private class MockConsole : IConsole
{
public int BufferWidth { get; set; } = 80;
public int CursorLeft { get; set; }
public int CursorTop { get; set; }
public Queue<ConsoleKeyInfo> Keys { get; } = new();
public void Write(string value, Color? color = null) { }
public void WriteLine() { }
public void WriteLine(string value, Color? color = null) { }
public ConsoleKeyInfo ReadKey(bool intercept) => Keys.Count > 0
? Keys.Dequeue()
: new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false);
}

[Theory]
[ClassData(typeof(ValidInputData))]
public void Valid_characters_are_accepted_as_input(ConsoleKey key, char keyChar, bool expected)
{
var keyInfo = new ConsoleKeyInfo(keyChar, key, false, false, false);
var result = ConsoleUtility.IsValidInputKey(keyInfo);
result.Should().Be(expected);
}

[Fact]
public void Prompt_returns_default_value_when_interrupted()
{
// Arrange
var mockConsole = new MockConsole();
mockConsole.Keys.Enqueue(new ConsoleKeyInfo('\0', ConsoleKey.F8, false, false, false));
ConsoleUtility.ConsoleWrapper = mockConsole;
ConsoleUtility.IsInterrupted = false;

// Act
var result = ConsoleUtility.PromptForInput("Question", "Default");

// Assert
result.Should().Be("Default");
}
}
31 changes: 31 additions & 0 deletions tests/Fallout.Build.Specs/Utilities/ValidInputData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections;
using System.Collections.Generic;
using System;
using Xunit;

namespace Fallout.Build.Tests.Utilities;

public class ValidInputData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return [ConsoleKey.A, 'a', true];
yield return [ConsoleKey.Z, 'z', true ];
yield return [ConsoleKey.D0, '0', true ];
yield return [ConsoleKey.D9, '9', true ];
yield return [ConsoleKey.Enter, '\r', false ];
yield return [ConsoleKey.Backspace, '\b', false ];
yield return [ConsoleKey.F8, '\0', false ];
yield return [ConsoleKey.Spacebar, ' ', false ];
yield return [ConsoleKey.OemPeriod, '.', true ];
yield return [ConsoleKey.Divide, '/', true ];
yield return [ConsoleKey.OemMinus, '-', true ];
yield return [ConsoleKey.NoName, 'ä', true ];
yield return [ConsoleKey.NoName, 'ö', true ];
yield return [ConsoleKey.NoName, '中', true ];
yield return [ConsoleKey.NoName, 'д', true ];
yield return [ConsoleKey.NoName, 'α', true ];
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

This file was deleted.

Loading