< Summary

Class:Towel.ConsoleHelper
Assembly:Towel
File(s):File 1: /home/runner/work/Towel/Towel/Sources/Towel/ConsoleHelper.cs
Covered lines:0
Uncovered lines:392
Coverable lines:392
Total lines:528
Line coverage:0% (0 of 392)
Covered branches:0
Total branches:182
Branch coverage:0% (0 of 182)

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
File 1: FlushInputBuffer(...)0%20%
File 1: PressToContinue(...)0%40%
File 1: PromptPressToContinue(...)0%40%
File 1: IntMenu(...)100%10%
File 1: IntMenu(...)0%220%
File 1: GetInput(...)0%280%
File 1: HiddenReadLine(...)100%10%
File 1: HiddenReadLineBase(...)0%800%
File 1: AnimatedEllipsis(...)0%100%
File 1: ProgressBar(...)0%60%
File 1: MoveNegative(...)0%40%
File 1: MovePositive(...)0%40%
File 1: ConsoleWriteChar(...)0%20%
File 1: ConsoleWriteString(...)0%20%

File(s)

/home/runner/work/Towel/Towel/Sources/Towel/ConsoleHelper.cs

#LineLine coverage
 1namespace Towel;
 2
 3/// <summary>Contains static helper methods for <see cref="Console"/>.</summary>
 4public static class ConsoleHelper
 5{
 6  /// <summary>Flushes the console input buffer.</summary>
 7  /// <param name="intercept">
 8  /// Determines whether to display the pressed key in the console window.
 9  /// true to not display the pressed key; otherwise, false.
 10  /// </param>
 11  public static void FlushInputBuffer(bool intercept = true)
 012  {
 013    while (Console.KeyAvailable)
 014    {
 015      Console.ReadKey(intercept);
 016    }
 017  }
 18
 19  /// <summary>Prompts the user to press [enter] in the console before continuing.</summary>
 20  /// <param name="key">The key to wait for the user to press before continuing.</param>
 21  public static void PressToContinue(ConsoleKey key = ConsoleKey.Enter)
 022  {
 023    if (!key.IsDefined())
 024    {
 025      throw new ArgumentOutOfRangeException(nameof(key), key, $"{nameof(key)} is not a defined value in the {nameof(Cons
 26    }
 027    while (Console.ReadKey(true).Key != key)
 028    {
 029      continue;
 30    }
 031  }
 32
 33  /// <summary>Prompts the user to press [enter] in the console before continuing.</summary>
 34  /// <param name="prompt">The prompt to display to the user. Default: "Press [enter] to continue...".</param>
 35  /// <param name="key">The key to wait for the user to press before continuing.</param>
 36  public static void PromptPressToContinue(string? prompt = null, ConsoleKey key = ConsoleKey.Enter)
 037  {
 038    if (!key.IsDefined())
 039    {
 040      throw new ArgumentOutOfRangeException(nameof(key), key, $"{nameof(key)} is not a defined value in the {nameof(Cons
 41    }
 042    prompt ??= $"Press [{key}] to continue...";
 043    Console.Write(prompt);
 044    PressToContinue(key);
 045  }
 46
 47  /// <summary>Prompts the user to select a menu option in the console before continuing.</summary>
 48  /// <param name="options">The options of the menu.</param>
 49  public static void IntMenu(params (string DisplayName, Action Action)[] options) =>
 050    IntMenu(null, null, null, options);
 51
 52  /// <summary>Prompts the user to select a menu option in the console before continuing.</summary>
 53  /// <param name="title">The title of the menu.</param>
 54  /// <param name="prompt">The prompt message to display when requesting console input from the user.</param>
 55  /// <param name="invalidMessage">The message to display if invalid input is detected.</param>
 56  /// <param name="options">The options of the menu.</param>
 57  public static void IntMenu(
 58    string? title = null,
 59    string? prompt = null,
 60    string? invalidMessage = null,
 61    params (string DisplayName, Action Action)[] options)
 062  {
 063    if (options is null) throw new ArgumentNullException(nameof(options));
 064    if (options.Length <= 0)
 065    {
 066      throw new ArgumentException($"{nameof(options)} is empty", nameof(options));
 67    }
 068    prompt ??= $"Choose an option (1-{options.Length}): ";
 069    invalidMessage ??= "Invalid Input. Try Again...";
 070    if (title is not null)
 071    {
 072      Console.WriteLine(title);
 073    }
 074    for (int i = 0; i < options.Length; i++)
 075    {
 076      Console.WriteLine($"{i + 1}. {options[i].DisplayName ?? "null"}");
 077    }
 78    int inputValue;
 079    Console.Write(prompt);
 080    while (!int.TryParse(Console.ReadLine(), out inputValue) || inputValue < 1 || options.Length < inputValue)
 081    {
 082      Console.WriteLine(invalidMessage);
 083      Console.Write(prompt);
 084    }
 085    options[inputValue - 1].Action?.Invoke();
 086  }
 87
 88  /// <summary>Gets console input from the user.</summary>
 89  /// <typeparam name="T">The generic type of console input to get from the user.</typeparam>
 90  /// <param name="prompt">The prompt message to display when requesting console input from the user.</param>
 91  /// <param name="invalidMessage">The message to display if invalid input is detected.</param>
 92  /// <param name="tryParse">The <see cref="TryParse"/> method for converting <see cref="string"/> into a <typeparamref 
 93  /// <param name="validation">The predicate for validating the value of the input.</param>
 94  /// <returns>The validated value of the console input provided by the user.</returns>
 95  public static T? GetInput<T>(
 96    string? prompt = null,
 97    string? invalidMessage = null,
 98    Func<string, (bool Success, T? Value)>? tryParse = null,
 99    Predicate<T?>? validation = null)
 0100  {
 0101    if (tryParse is null && (typeof(T) != typeof(string) && !typeof(T).IsEnum && Meta.GetTryParseMethod<T>() is null))
 0102    {
 0103      throw new InvalidOperationException($"Using {nameof(ConsoleHelper)}.{nameof(GetInput)} without providing a {nameof
 104    }
 0105    tryParse ??= typeof(T) == typeof(string)
 0106      ? s => (true, (T)(object)s)
 0107      : TryParse<T>;
 0108    validation ??= v => true;
 0109  GetInput:
 0110    Console.Write(prompt ?? $"Input a {typeof(T).Name} value: ");
 0111    string? readLine = Console.ReadLine();
 0112    if (readLine is null)
 0113    {
 0114      throw new ArgumentException($"{nameof(System)}.{nameof(Console)}.{nameof(Console.ReadLine)} returned null");
 115    }
 0116    var (success, value) = tryParse(readLine);
 0117    if (!success || !validation(value))
 0118    {
 0119      Console.WriteLine(invalidMessage ?? $"Invalid input. Try again...");
 0120      goto GetInput;
 121    }
 0122    return value;
 0123  }
 124
 125  /// <summary>Similar to <see cref="Console.ReadLine"/> but with hidden input characters.</summary>
 126  /// <param name="shownCharacter">The display character to use for all input.</param>
 127  /// <returns>The <see cref="string"/> input provided by the user.</returns>
 128  public static string HiddenReadLine(char shownCharacter = '*')
 0129  {
 0130    System.Collections.Generic.List<char> list = new();
 0131    HiddenReadLineBase(
 0132      shownCharacter: shownCharacter,
 0133      GetLength: () => list.Count,
 0134      Append: list.Add,
 0135      InsertAt: list.Insert,
 0136      RemoveAt: list.RemoveAt,
 0137      RemoveRange: list.RemoveRange,
 0138      Clear: list.Clear);
 0139    return string.Concat(list);
 0140  }
 141
 142  internal static void HiddenReadLineBase(
 143    char shownCharacter,
 144    Func<int> GetLength,
 145    Action<char> Append,
 146    Action<int, char> InsertAt,
 147    Action<int> RemoveAt,
 148    Action<int, int>? RemoveRange = null,
 149    Action? Clear = null)
 0150  {
 0151    int position = 0;
 152
 0153    RemoveRange ??= (index, length) =>
 0154    {
 0155      for (int i = 0; i < length; i++)
 0156      {
 0157        RemoveAt(index);
 0158      }
 0159    };
 160
 0161    Clear ??= () => RemoveRange(0, GetLength());
 162
 163    void MoveToOrigin() => MoveNegative(position);
 164
 165    void MoveToTail() => MovePositive(GetLength() - position);
 166
 0167    while (true)
 0168    {
 0169      ConsoleKeyInfo keyInfo = Console.ReadKey(true);
 0170      if (keyInfo.Key is ConsoleKey.Enter)
 0171      {
 0172        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) &&
 0173          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0174          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0175        {
 0176          MovePositive(GetLength() - position);
 0177          Console.WriteLine();
 0178          break;
 179        }
 0180      }
 0181      else if (keyInfo.Key is ConsoleKey.Backspace)
 0182      {
 0183        if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
 0184        {
 0185          MoveToOrigin();
 0186          ConsoleWriteString(new string(shownCharacter, GetLength() - position) + new string(' ', position));
 0187          MoveNegative(GetLength());
 0188          RemoveRange(0, position);
 0189          position = 0;
 0190        }
 0191        else if (position > 0)
 0192        {
 0193          if (position == GetLength())
 0194          {
 0195            MoveNegative(1);
 0196            ConsoleWriteChar(' ');
 0197            MoveNegative(1);
 0198          }
 199          else
 0200          {
 0201            MoveToTail();
 0202            MoveNegative(1);
 0203            ConsoleWriteChar(' ');
 0204            MoveNegative(GetLength() - position + 1);
 0205          }
 0206          RemoveAt(position - 1);
 0207          position--;
 0208        }
 0209      }
 0210      else if (keyInfo.Key is ConsoleKey.Delete)
 0211      {
 0212        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) &&
 0213          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0214          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0215        {
 0216          if (position < GetLength())
 0217          {
 0218            int left = Console.CursorLeft;
 0219            int top = Console.CursorTop;
 0220            MoveToTail();
 0221            MoveNegative(1);
 0222            ConsoleWriteChar(' ');
 0223            Console.CursorLeft = left;
 0224            Console.CursorTop = top;
 0225            RemoveAt(position);
 0226            continue;
 227          }
 0228        }
 0229      }
 0230      else if (keyInfo.Key is ConsoleKey.Escape)
 0231      {
 0232        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) &&
 0233          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0234          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0235        {
 0236          MoveToOrigin();
 0237          int left = Console.CursorLeft;
 0238          int top = Console.CursorTop;
 0239          ConsoleWriteString(new string(' ', GetLength()));
 0240          Console.CursorLeft = left;
 0241          Console.CursorTop = top;
 0242          Clear();
 0243          position = 0;
 0244        }
 0245      }
 0246      else if (keyInfo.Key is ConsoleKey.Home)
 0247      {
 0248        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0249          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0250        {
 0251          if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
 0252          {
 0253            MoveToOrigin();
 0254            ConsoleWriteString(new string(shownCharacter, GetLength() - position) + new string(' ', position));
 0255            MoveNegative(GetLength());
 0256            RemoveRange(0, position);
 0257            position = 0;
 0258          }
 259          else
 0260          {
 0261            MoveToOrigin();
 0262            position = 0;
 0263          }
 0264        }
 0265      }
 0266      else if (keyInfo.Key is ConsoleKey.End)
 0267      {
 0268        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0269          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0270        {
 0271          if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
 0272          {
 0273            MoveToOrigin();
 0274            ConsoleWriteString(new string(shownCharacter, position) + new string(' ', GetLength() - position));
 0275            MoveNegative(GetLength() - position);
 0276            RemoveRange(position, GetLength() - position);
 0277          }
 278          else
 0279          {
 0280            MoveToTail();
 0281            position = GetLength();
 0282          }
 0283        }
 0284      }
 0285      else if (keyInfo.Key is ConsoleKey.LeftArrow)
 0286      {
 0287        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0288          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0289        {
 0290          if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
 0291          {
 0292            MoveToOrigin();
 0293            position = 0;
 0294          }
 295          else
 0296          {
 0297            if (position > 0)
 0298            {
 0299              MoveNegative(1);
 0300              position--;
 0301            }
 0302          }
 0303        }
 0304      }
 0305      else if (keyInfo.Key is ConsoleKey.RightArrow)
 0306      {
 0307        if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) &&
 0308          !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Alt))
 0309        {
 0310          if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
 0311          {
 0312            MoveToTail();
 0313            position = GetLength();
 0314          }
 315          else
 0316          {
 0317            if (position < GetLength())
 0318            {
 0319              MovePositive(1);
 0320              position++;
 0321            }
 0322          }
 0323        }
 0324      }
 325      else
 0326      {
 0327        if (keyInfo.KeyChar is not '\0')
 0328        {
 0329          if (position == GetLength())
 0330          {
 0331            ConsoleWriteChar(shownCharacter);
 0332            Append(keyInfo.KeyChar);
 0333            position++;
 0334          }
 335          else
 0336          {
 0337            int left = Console.CursorLeft;
 0338            int top = Console.CursorTop;
 0339            MoveToTail();
 0340            ConsoleWriteChar(shownCharacter);
 0341            Console.CursorLeft = left;
 0342            Console.CursorTop = top;
 0343            MovePositive(1);
 0344            InsertAt(position, keyInfo.KeyChar);
 0345            position++;
 0346          }
 0347        }
 0348      }
 0349    }
 0350  }
 351
 352  /// <summary>Animates an elipsis in the console to indicate processing.</summary>
 353  /// <param name="condition">The condition of the loop.</param>
 354  /// <param name="delay">The delay function.</param>
 355  /// <param name="length">The length of the ellipsis.</param>
 356  public static void AnimatedEllipsis(
 357    Func<bool> condition,
 358    Action delay,
 359    int length = 3)
 0360  {
 0361    if (condition is null) throw new ArgumentNullException(nameof(condition));
 0362    if (delay is null) throw new ArgumentNullException(nameof(delay));
 0363    if (sourceof(length < 1, out string c1)) throw new ArgumentOutOfRangeException(nameof(length), length, c1);
 364
 0365    void MoveToOrigin() => MoveNegative(length);
 366
 367    void Render(int frame)
 0368    {
 0369      for (int i = 0; i < frame; i++)
 0370      {
 0371        ConsoleWriteChar('.');
 0372      }
 0373      for (int i = frame; i < length; i++)
 0374      {
 0375        ConsoleWriteChar(' ');
 0376      }
 0377    }
 378
 0379    int frame = 0;
 0380    Render(frame++);
 0381    while (condition())
 0382    {
 0383      MoveToOrigin();
 0384      Render(frame++);
 0385      delay();
 0386      if (frame > length)
 0387      {
 0388        frame = 0;
 0389      }
 0390    }
 0391    MoveToOrigin();
 0392    ConsoleWriteString(new string(' ', length));
 0393    MoveToOrigin();
 0394  }
 395
 396  /// <summary>Displays a progress bar in the console.</summary>
 397  /// <param name="action">The action to track the progress of.</param>
 398  /// <param name="length">The character length of the progress bar (must be >= 6).</param>
 399  /// <param name="header">The header character of the progress bar.</param>
 400  /// <param name="footer">The footer character of the progress bar.</param>
 401  /// <param name="done">The character for represening completed progress.</param>
 402  /// <param name="remaining">The character representing ongoing processing.</param>
 403  /// <param name="errorDigit">The characters to display in the numerical display when an invalid percentage is recieved
 404  /// <param name="postClear">Whether or not to clear the progress bar from the view when complete.</param>
 405  public static void ProgressBar(
 406    Action<Action<double>> action,
 407    int length = 17,
 408    char header = '[',
 409    char footer = ']',
 410    char done = '=',
 411    char remaining = '-',
 412    char errorDigit = '?',
 413    bool postClear = true)
 0414  {
 0415    if (action is null) throw new ArgumentNullException(nameof(action));
 0416    if (sourceof(length < 6, out string c1)) throw new ArgumentOutOfRangeException(nameof(length), length, c1);
 417
 0418    void MoveToOrigin() => MoveNegative(length);
 419
 420    void Render(double percentage)
 0421    {
 0422      ConsoleWriteChar(header);
 0423      if (percentage < 0 || percentage > 100)
 0424      {
 0425        ConsoleWriteString(new string(remaining, length - 7));
 0426      }
 427      else
 0428      {
 0429        int doneCount = (int)(percentage / 100 * (length - 7));
 0430        int remainingCount = length - 7 - doneCount;
 0431        ConsoleWriteString(new string(done, doneCount));
 0432        ConsoleWriteString(new string(remaining, remainingCount));
 0433      }
 0434      ConsoleWriteChar(footer);
 0435      ConsoleWriteChar(' ');
 0436      if (percentage < 0 || percentage > 100)
 0437      {
 0438        ConsoleWriteString(new string(errorDigit, 2));
 0439        ConsoleWriteChar('%');
 0440        ConsoleWriteChar(' ');
 0441      }
 442      else
 0443      {
 0444        string percentString = ((int)percentage).ToString(System.Globalization.CultureInfo.InvariantCulture);
 0445        ConsoleWriteString(percentString);
 0446        ConsoleWriteChar('%');
 0447        for (int i = percentString.Length; i < 3; i++)
 0448        {
 0449          ConsoleWriteChar(' ');
 0450        }
 0451      }
 0452    }
 453
 0454    Render(0);
 0455    action(percent => { MoveToOrigin(); Render(percent); });
 0456    if (postClear)
 0457    {
 0458      MoveToOrigin();
 0459      ConsoleWriteString(new string(' ', length));
 0460      MoveToOrigin();
 0461    }
 462    else
 0463    {
 0464      MoveToOrigin();
 0465      Render(100);
 0466    }
 0467  }
 468
 469  internal static void MoveNegative(int count)
 0470  {
 0471    int bufferWidth = Console.BufferWidth;
 0472    int left = Console.CursorLeft;
 0473    int top = Console.CursorTop;
 0474    for (int i = 0; i < count; i++)
 0475    {
 0476      if (left > 0)
 0477      {
 0478        left--;
 0479      }
 480      else
 0481      {
 0482        top--;
 0483        left = bufferWidth - 1;
 0484      }
 0485    }
 0486    Console.CursorLeft = left;
 0487    Console.CursorTop = top;
 0488  }
 489
 490  internal static void MovePositive(int count)
 0491  {
 0492    int bufferWidth = Console.BufferWidth;
 0493    int left = Console.CursorLeft;
 0494    int top = Console.CursorTop;
 0495    for (int i = 0; i < count; i++)
 0496    {
 0497      if (left == bufferWidth - 1)
 0498      {
 0499        top++;
 0500        left = 0;
 0501      }
 502      else
 0503      {
 0504        left++;
 0505      }
 0506    }
 0507    Console.CursorLeft = left;
 0508    Console.CursorTop = top;
 0509  }
 510
 511  internal static void ConsoleWriteChar(char @char)
 0512  {
 0513    int temp = Console.CursorLeft;
 0514    Console.Write(@char);
 0515    if (Console.CursorLeft == temp)
 0516    {
 0517      MovePositive(1);
 0518    }
 0519  }
 520
 521  internal static void ConsoleWriteString(string @string)
 0522  {
 0523    foreach (char c in @string)
 0524    {
 0525      ConsoleWriteChar(c);
 0526    }
 0527  }
 528}