private/classes/PwshSpectreConsole.Recording.cs

using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using Spectre.Console;
using Spectre.Console.Rendering;
using System.Collections.Generic;
using System.Text.RegularExpressions;

// I tried this as a powershell class but ran into the main thread being deadlocked and hanging the terminal
namespace PwshSpectreConsole.Recording
{
    // Used to generate docs
    // https://github.com/spectreconsole/spectre.console/blob/main/resources/scripts/Generator/Commands/AsciiCast/AsciiCastOut.cs
    internal class AsciiCastWriter : TextWriter
    {
        private StringBuilder _builder = new StringBuilder();
        private decimal _firstTick;

        public AsciiCastWriter()
        {
            _firstTick = 0;
        }

        public override void Write(string value)
        {
            System.Console.Write(value);
            if (_firstTick == 0)
            {
                _firstTick = Environment.TickCount;
            }
            decimal tick = (Environment.TickCount - _firstTick) / 1000;
            _builder.Append('[').AppendFormat(CultureInfo.InvariantCulture, "{0}", tick).Append(", \"o\", \"").Append(JsonEncodedText.Encode(value)).AppendLine("\"]");
        }

        public override Encoding Encoding => Encoding.Default;

        public string GetOutput()
        {
            string json = _builder.ToString();
            _builder.Clear();
            _firstTick = 0;
            return json;
        }
    }

    public class AsciiCastInput : IAnsiConsoleInput
    {
        private readonly Queue<(ConsoleKeyInfo?, int)> _input;
        private readonly Random _random = new Random();
        private int _keyEntries = 0;

        public AsciiCastInput()
        {
            _input = new Queue<(ConsoleKeyInfo?, int)>();
        }

        public void PushText(string input, int keypressDelayMs)
        {
            if (input is null)
            {
                throw new ArgumentNullException(nameof(input));
            }

            foreach (var character in input)
            {
                PushCharacter(character, keypressDelayMs);
            }

            _keyEntries++;
        }

        public void PushTextWithEnter(string input, int keypressDelayMs)
        {
            PushText(input, keypressDelayMs);
            PushKey(ConsoleKey.Enter, keypressDelayMs);
        }

        public void PushCharacter(char input, int keypressDelayMs)
        {
            var delay = keypressDelayMs + _random.Next((int)(keypressDelayMs * -.2), (int)(keypressDelayMs * .2));

            switch (input)
            {
                case '↑':
                    PushKey(ConsoleKey.UpArrow, keypressDelayMs);
                    break;
                case '↓':
                    PushKey(ConsoleKey.DownArrow, keypressDelayMs);
                    break;
                case '↲':
                    PushKey(ConsoleKey.Enter, keypressDelayMs);
                    break;
                case '¦':
                    _input.Enqueue((null, delay));
                    break;
                default:
                    var control = char.IsUpper(input);
                    _input.Enqueue((new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control), delay));
                    break;
            }
        }

        public void PushKey(ConsoleKey input, int keypressDelayMs)
        {
            var delay = keypressDelayMs + _random.Next((int)(keypressDelayMs * -.2), (int)(keypressDelayMs * .2));
            _input.Enqueue((new ConsoleKeyInfo((char)input, input, false, false, false), delay));
            _keyEntries++;
        }

        public bool IsKeyAvailable()
        {
            return _input.Count > 0;
        }

        public ConsoleKeyInfo? ReadKey(bool intercept)
        {
            if (_input.Count == 0)
            {
                throw new InvalidOperationException("No input available.");
            }

            var result = _input.Dequeue();

            System.Threading.Thread.Sleep(result.Item2);
            return result.Item1;
        }

        public Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
        {
            return Task.FromResult(ReadKey(intercept));
        }

        public int KeyEntries => _keyEntries;
    }

    public class RecordingConsole : IAnsiConsole
    {
        private IAnsiConsole _ansiConsole;
        private AsciiCastWriter _writer;
        private bool _quiet;
        public AsciiCastInput Input { get; }

        public RecordingConsole(int width, int height, bool quiet)
        {
            var profileEnrichment = new ProfileEnrichment();
            profileEnrichment.UseDefaultEnrichers = false;

            var asciiCast = new AsciiCastWriter();
            var output = new AnsiConsoleOutput(asciiCast);

            var settings = new AnsiConsoleSettings
            {
                Ansi = AnsiSupport.Yes,
                ColorSystem = ColorSystemSupport.TrueColor,
                Interactive = InteractionSupport.Yes,
                Enrichment = profileEnrichment,
                Out = output
            };

            var console = AnsiConsole.Create(settings);
            console.Profile.Width = width;
            console.Profile.Height = height;
            console.Profile.Capabilities.Ansi = true;
            console.Profile.Capabilities.Unicode = true;
            console.Profile.Out = output;

            _quiet = quiet;
            _ansiConsole = console;
            _writer = asciiCast;
            Input = new AsciiCastInput();
        }

        public string GetAsciiCastRecording(string title)
        {
            string json = _writer.GetOutput();
            // count number of times [2A appears in the json
            var cursorUps = Regex.Matches(json, @"\[([0-9]+)A");
            var countOfCursorUps = 0;
            foreach (Match cursorUp in cursorUps)
            {
                countOfCursorUps += int.Parse(cursorUp.Groups[1].Value);
            }
            var KeyEntries = Input.KeyEntries;
            var countOfNewlines = Regex.Matches(json, @"\\n").Count;
            var jsonStrippedOfNewlines = Regex.Replace(json, @"(\\r)?\\n", "");
            var totalLines = countOfNewlines - countOfCursorUps + KeyEntries + 2;
            string header = $"{{\"version\": 2, \"width\": {_ansiConsole.Profile.Width}, \"height\": {totalLines}, \"title\": \"{JsonEncodedText.Encode(title)}\", \"env\": {{\"TERM\": \"Spectre.Console\"}}}}";
            return $"{header}{Environment.NewLine}{json}{Environment.NewLine}";
        }

        public Profile Profile => _ansiConsole.Profile;
        public IAnsiConsoleCursor Cursor => _ansiConsole.Cursor;
        IAnsiConsoleInput IAnsiConsole.Input => Input;
        public IExclusivityMode ExclusivityMode => _ansiConsole.ExclusivityMode;
        public RenderPipeline Pipeline => _ansiConsole.Pipeline;

        public void Clear(bool homeValue)
        {
            _ansiConsole.Clear(homeValue);
        }

        public void Write(IRenderable renderable)
        {
            if(_quiet)
            {
                _ansiConsole.Write(renderable);
            }
        }
    }
}