diff --git a/CommandDotNet.TestTools/TestConsole.TestConsoleReader.cs b/CommandDotNet.TestTools/TestConsole.TestConsoleReader.cs new file mode 100644 index 000000000..8d7fbedef --- /dev/null +++ b/CommandDotNet.TestTools/TestConsole.TestConsoleReader.cs @@ -0,0 +1,65 @@ +using System; +using CommandDotNet.Prompts; +using CommandDotNet.Rendering; + +namespace CommandDotNet.TestTools +{ + public partial class TestConsole + { + private class TestConsoleReader : IConsoleReader + { + private readonly IConsoleWriter _standardOut; + private readonly Func? _onReadLine; + private readonly Func? _onReadKey; + + public TestConsoleReader(IConsoleWriter standardOut, Func? onReadLine, Func? onReadKey) + { + _standardOut = standardOut ?? throw new ArgumentNullException(nameof(standardOut)); + _onReadLine = onReadLine; + _onReadKey = onReadKey; + } + + public bool KeyAvailable { get; } + public bool NumberLock { get; } + public bool CapsLock { get; } + public bool TreatControlCAsInput { get; set; } + + /// + /// Read a key from the input + /// + /// When true, the key is not displayed in the output + /// + public ConsoleKeyInfo ReadKey(bool intercept) + { + ConsoleKeyInfo consoleKeyInfo; + + do + { + consoleKeyInfo = _onReadKey?.Invoke() + ?? new ConsoleKeyInfo('\u0000', ConsoleKey.Enter, false, false, false); + + // mimic System.Console which does not interrupt during ReadKey + // and does not return Ctrl+C unless TreatControlCAsInput == true. + } while (!TreatControlCAsInput && consoleKeyInfo.IsCtrlC()); + + if (!intercept) + { + if (consoleKeyInfo.Key == ConsoleKey.Enter) + { + _standardOut.WriteLine(""); + } + else + { + _standardOut.Write(consoleKeyInfo.KeyChar.ToString()); + } + } + return consoleKeyInfo; + } + + public string? ReadLine() + { + return _onReadLine?.Invoke(); + } + } + } +} \ No newline at end of file diff --git a/CommandDotNet.TestTools/TestConsole.TestConsoleWriter.cs b/CommandDotNet.TestTools/TestConsole.TestConsoleWriter.cs new file mode 100644 index 000000000..fa9667e70 --- /dev/null +++ b/CommandDotNet.TestTools/TestConsole.TestConsoleWriter.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Text; +using CommandDotNet.Rendering; + +namespace CommandDotNet.TestTools +{ + public partial class TestConsole + { + private class TestConsoleWriter : TextWriter, IConsoleWriter + { + private readonly TestConsoleWriter? _inner; + private readonly StringBuilder _stringBuilder = new StringBuilder(); + + public TestConsoleWriter( + TestConsoleWriter? inner = null) + { + _inner = inner; + } + + public override void Write(char value) + { + _inner?.Write(value); + if (value == '\b' && _stringBuilder.Length > 0) + { + _stringBuilder.Length = _stringBuilder.Length - 1; + } + else + { + _stringBuilder.Append(value); + } + } + + public override Encoding Encoding { get; } = Encoding.Unicode; + + public override string ToString() => _stringBuilder.ToString(); + } + } +} \ No newline at end of file diff --git a/CommandDotNet.TestTools/TestConsole.cs b/CommandDotNet.TestTools/TestConsole.cs index 42ad4a8e0..be1282ea2 100644 --- a/CommandDotNet.TestTools/TestConsole.cs +++ b/CommandDotNet.TestTools/TestConsole.cs @@ -5,12 +5,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; -using CommandDotNet.Extensions; using CommandDotNet.Logging; -using CommandDotNet.Prompts; using CommandDotNet.Rendering; namespace CommandDotNet.TestTools @@ -21,18 +17,32 @@ namespace CommandDotNet.TestTools /// - provide piped input
/// - handle ReadLine and ReadToEnd requests /// - public class TestConsole : IConsole + public partial class TestConsole : IConsole { - private static ILog Log = LogProvider.GetCurrentClassLogger(); + private readonly Action _onClear; + private static readonly ILog Log = LogProvider.GetCurrentClassLogger(); - private readonly Func? _onReadKey; + public static class Default + { + public static ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black; + public static ConsoleColor ForegroundColor { get; set; } = ConsoleColor.White; + + public static int WindowLeft { get; set; } = 0; + public static int WindowTop { get; set; } = 0; + public static int WindowWidth { get; set; } = 120; + public static int WindowHeight { get; set; } = 30; + + public static int BufferWidth { get; set; } = WindowWidth; + public static int BufferHeight { get; set; } = WindowHeight; + } public TestConsole( Func? onReadLine = null, IEnumerable? pipedInput = null, - Func? onReadKey = null) + Func? onReadKey = null, + Action onClear = null) { - _onReadKey = onReadKey; + _onClear = onClear; IsInputRedirected = pipedInput != null; if (pipedInput != null) @@ -54,29 +64,53 @@ public TestConsole( } } - var all = new StandardStreamWriter(); + var all = new TestConsoleWriter(); All = all; - Out = new StandardStreamWriter(all); - Error = new StandardStreamWriter(all); - In = new StandardStreamReader( + Out = new TestConsoleWriter(all); + Error = new TestConsoleWriter(all); + In = new TestConsoleReader(Out, () => { var input = onReadLine?.Invoke(this); Log.Info($"IConsole.ReadLine > {input}"); return input; + }, + onReadKey switch + { + { } _ => () => onReadKey!.Invoke(this), + _ => null }); } + public string Title { get; set; } - /// - /// This is the combined output for and in the order the lines were output. - /// - public IStandardStreamWriter All { get; } + #region IStandardError - public IStandardStreamWriter Out { get; } + public IConsoleWriter Error { get; } - public IStandardStreamWriter Error { get; } + public bool IsErrorRedirected { get; } = false; - public string OutLastLine => Out.ToString().SplitIntoLines().Last(); + #endregion + + #region IStandardOut + + public IConsoleWriter Out { get; } + + public bool IsOutputRedirected { get; } = false; + + #endregion + + #region IStandardIn + + public IConsoleReader In { get; } + + public bool IsInputRedirected { get; } + + #endregion + + /// + /// This is the combined output for and in the order the lines were output. + /// + public IConsoleWriter All { get; } /// /// The combination of and @@ -95,95 +129,73 @@ public TestConsole( /// public string ErrorText() => Error.ToString(); - public bool IsOutputRedirected { get; } = false; - - public bool IsErrorRedirected { get; } = false; + public string OutLastLine => Out.ToString().SplitIntoLines().Last(); - public IStandardStreamReader In { get; } + #region IConsoleColor - public bool IsInputRedirected { get; } + public ConsoleColor BackgroundColor { get; set; } = Default.BackgroundColor; + public ConsoleColor ForegroundColor { get; set; } = Default.ForegroundColor; - /// - /// Read a key from the input - /// - /// When true, the key is not displayed in the output - /// - public ConsoleKeyInfo ReadKey(bool intercept) + public void ResetColor() { - ConsoleKeyInfo consoleKeyInfo; + BackgroundColor = Default.BackgroundColor; + ForegroundColor = Default.ForegroundColor; + } - do - { - consoleKeyInfo = _onReadKey?.Invoke(this) - ?? new ConsoleKeyInfo('\u0000', ConsoleKey.Enter, false, false, false); + #endregion - // mimic System.Console which does not interrupt during ReadKey - // and does not return Ctrl+C unless TreatControlCAsInput == true. - } while (!TreatControlCAsInput && consoleKeyInfo.IsCtrlC()); + #region IConsoleBuffer - if (!intercept) - { - if (consoleKeyInfo.Key == ConsoleKey.Enter) - { - Out.WriteLine(""); - } - else - { - Out.Write(consoleKeyInfo.KeyChar.ToString()); - } - } - return consoleKeyInfo; - } + public int BufferWidth { get; set; } = Default.BufferWidth; + public int BufferHeight { get; set; } = Default.BufferHeight; - public bool TreatControlCAsInput { get; set; } + public void SetBufferSize(int width, int height) + { + BufferWidth = width; + BufferHeight = height; + } - private class StandardStreamReader : IStandardStreamReader + public void Clear() { - private readonly Func? _onReadLine; + _onClear?.Invoke(this); + } - public StandardStreamReader(Func? onReadLine) - { - _onReadLine = onReadLine; - } + #endregion - public string? ReadLine() - { - return _onReadLine?.Invoke(); - } + #region IConsoleWindow - public string? ReadToEnd() - { - return _onReadLine?.EnumeratePipedInput().ToCsv(Environment.NewLine); - } + public int WindowLeft { get; set; } = Default.WindowLeft; + public int WindowTop { get; set; } = Default.WindowTop; + public int WindowWidth { get; set; } = Default.WindowWidth; + public int WindowHeight { get; set; } = Default.WindowHeight; + + public void SetWindowPosition(int left, int top) + { + WindowLeft = left; + WindowTop = top; } - private class StandardStreamWriter : TextWriter, IStandardStreamWriter + public void SetWindowSize(int width, int height) { - private readonly StandardStreamWriter? _inner; - private readonly StringBuilder _stringBuilder = new StringBuilder(); + WindowWidth = width; + WindowHeight = height; + } - public StandardStreamWriter( - StandardStreamWriter? inner = null) - { - _inner = inner; - } + #endregion - public override void Write(char value) - { - _inner?.Write(value); - if (value == '\b' && _stringBuilder.Length > 0) - { - _stringBuilder.Length = _stringBuilder.Length - 1; - } - else - { - _stringBuilder.Append(value); - } - } + #region IConsoleCursor - public override Encoding Encoding { get; } = Encoding.Unicode; + public int CursorSize { get; set; } + public bool CursorVisible { get; set; } + public int CursorLeft { get; set; } + public int CursorTop { get; set; } - public override string ToString() => _stringBuilder.ToString(); + public void SetCursorPosition(int left, int top) + { + CursorLeft = left; + CursorTop = top; } + + #endregion } } \ No newline at end of file diff --git a/CommandDotNet/ConsoleExtensions.cs b/CommandDotNet/ConsoleReadLineExtensions.cs similarity index 87% rename from CommandDotNet/ConsoleExtensions.cs rename to CommandDotNet/ConsoleReadLineExtensions.cs index f906c9791..b66ca91df 100644 --- a/CommandDotNet/ConsoleExtensions.cs +++ b/CommandDotNet/ConsoleReadLineExtensions.cs @@ -2,7 +2,7 @@ namespace CommandDotNet { - public static class ConsoleExtensions + public static class ConsoleReadLineExtensions { public static void Write(this IConsole console, object? value = null) { diff --git a/CommandDotNet/StandardStreamWriter.cs b/CommandDotNet/ConsoleWriter.cs similarity index 67% rename from CommandDotNet/StandardStreamWriter.cs rename to CommandDotNet/ConsoleWriter.cs index 47f0d5d50..324b66c90 100644 --- a/CommandDotNet/StandardStreamWriter.cs +++ b/CommandDotNet/ConsoleWriter.cs @@ -10,22 +10,22 @@ namespace CommandDotNet { - public static class StandardStreamWriter + public static class ConsoleWriter { // the WriteLine extension methods will be frequently used by developers // keep class in CommandDotNet namespace to avoid force reference to Rendering namespace - public static IStandardStreamWriter Create(TextWriter writer) + public static IConsoleWriter Create(TextWriter writer) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } - return new AnonymousStandardStreamWriter(writer.Write); + return new AnonymousConsoleWriter(writer.Write); } - public static void WriteLine(this IStandardStreamWriter writer) + public static void WriteLine(this IConsoleWriter writer) { if (writer == null) { @@ -35,17 +35,17 @@ public static void WriteLine(this IStandardStreamWriter writer) writer.Write(NewLine); } - public static void WriteLine(this IStandardStreamWriter writer, object? value) + public static void WriteLine(this IConsoleWriter writer, object? value) { WriteLine(writer, value?.ToString()); } - public static void WriteLine(this IStandardStreamWriter writer, string? value) + public static void WriteLine(this IConsoleWriter writer, string? value) { writer.WriteLine(value, avoidExtraNewLine: false); } - internal static void WriteLine(this IStandardStreamWriter writer, string? value, bool avoidExtraNewLine) + internal static void WriteLine(this IConsoleWriter writer, string? value, bool avoidExtraNewLine) { if (writer == null) { @@ -59,16 +59,16 @@ internal static void WriteLine(this IStandardStreamWriter writer, string? value, } } - public static void Write(this IStandardStreamWriter writer, object? value) + public static void Write(this IConsoleWriter writer, object? value) { writer.Write(value?.ToString()); } - private class AnonymousStandardStreamWriter : IStandardStreamWriter + private class AnonymousConsoleWriter : IConsoleWriter { private readonly Action _write; - public AnonymousStandardStreamWriter(Action write) + public AnonymousConsoleWriter(Action write) { _write = write; } diff --git a/CommandDotNet/Execution/CancellationHandlers.cs b/CommandDotNet/Execution/CancellationHandlers.cs index 09f3b778c..223ce9f97 100644 --- a/CommandDotNet/Execution/CancellationHandlers.cs +++ b/CommandDotNet/Execution/CancellationHandlers.cs @@ -27,7 +27,11 @@ public Handler(CancellationTokenSource source) } private static Handler? GetHandler(this CommandContext ctx) => ctx.Services.GetOrDefault(); - private static void SetHandler(this CommandContext ctx, CancellationTokenSource src) => ctx.Services.Add(new Handler(src)); + private static void SetHandler(this CommandContext ctx, CancellationTokenSource src) + { + ctx.Services.Add(new Handler(src)); + ctx.Services.Add(src); + } static CancellationHandlers() { @@ -58,7 +62,7 @@ internal static void EndRun() } /// - /// Prefer when possible. + /// Prefer when possible. /// Use this in cases where another component depends on the /// event and CommandDotNet should ignore the event during this time. /// diff --git a/CommandDotNet/Prompting/ConsoleExtensions.cs b/CommandDotNet/Prompting/ConsoleExtensions.cs new file mode 100644 index 000000000..82f1965bf --- /dev/null +++ b/CommandDotNet/Prompting/ConsoleExtensions.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using CommandDotNet.Extensions; +using CommandDotNet.Rendering; + +namespace CommandDotNet.Prompting +{ + public static class ConsoleReadLineExtensions + { + public static IEnumerable ReadLines(this IConsole console, bool isSecret = false, string? promptText = null, + string? historyKey = null, ICollection? history = null, ReadLineConfig? readLineConfig = null) + { + if (promptText is { }) + { + console.WriteLine(promptText); + } + + // use a single config for each line + readLineConfig ??= ReadLineConfig.Factory(); + + while (true) + { + var result = console.ReadLine( + isSecret: isSecret, + historyKey: historyKey, + history: history, + readLineConfig: readLineConfig); + + if (result.IsNullOrEmpty()) + { + yield break; + } + + yield return result; + } + } + + // TODO: consider adding a Prompt extension method in the CommandDotNet space w/o ReadLineConfig for simpler use + public static string ReadLine(this IConsole console, bool isSecret = false, string? promptText = null, + string? historyKey = null, ICollection? history = null, ReadLineConfig? readLineConfig = null) + { + if (promptText is { }) + { + console.Write(promptText); + } + + var context = new ReadLineContext(new Line(console, isSecret), readLineConfig); + + if (history is { }) + { + if (historyKey is { }) + { + throw new ArgumentException($"only one can be provided: {nameof(historyKey)} OR {nameof(history)}."); + } + + context.History = history; + } + else if (historyKey is { }) + { + context.History = context.Config.History?.GetOrAdd(historyKey, key => new List()); + } + + return console.ReadLine(context); + } + + private static string ReadLine(this IConsole console, ReadLineContext ctx) + { + var original = console.In.TreatControlCAsInput; + using var restoreOriginal = new DisposableAction(() => console.In.TreatControlCAsInput = original); + console.In.TreatControlCAsInput = true; + + foreach (var info in console.ReadKeys()) + { + + if (ctx.Config.KeyHandlers.TryGet(info, out KeyHandlerDelegate? action)) + { + action!.Invoke(info, ctx); + } + else + { + ctx.Line.Write(info); + } + + if (ctx.ShouldExitPrompt) + { + break; + } + } + console.WriteLine(); + + var result = ctx.Line.Text; + ctx.History?.Add(result); + + return result; + } + + private static IEnumerable ReadKeys(this IConsole console) + { + while(true) + { + yield return console.In.ReadKey(true); + } + } + } +} + \ No newline at end of file diff --git a/CommandDotNet/Prompting/ConsoleKeyInfoExtensions.cs b/CommandDotNet/Prompting/ConsoleKeyInfoExtensions.cs new file mode 100644 index 000000000..5fed60633 --- /dev/null +++ b/CommandDotNet/Prompting/ConsoleKeyInfoExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Text; + +namespace CommandDotNet.Prompting +{ + public static class ConsoleKeyInfoExtensions + { + private const string ControlMod = "Ctrl+"; + private const string AltMod = "Alt+"; + private const string ShiftMod = "Shift+"; + + public static string ToFriendlyName(this ConsoleKeyInfo info) + { + if (info.Modifiers == 0) + { + return info.Key.ToString(); + } + + var sb = new StringBuilder(); + if (info.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + sb.Append(ControlMod); + } + + if (info.Modifiers.HasFlag(ConsoleModifiers.Alt)) + { + sb.Append(AltMod); + } + + if (info.Modifiers.HasFlag(ConsoleModifiers.Shift)) + { + sb.Append(ShiftMod); + } + + sb.Append(info.Key); + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/CursorPosition.cs b/CommandDotNet/Prompting/CursorPosition.cs new file mode 100644 index 000000000..6d6a6bb1a --- /dev/null +++ b/CommandDotNet/Prompting/CursorPosition.cs @@ -0,0 +1,20 @@ +using CommandDotNet.Rendering; + +namespace CommandDotNet.Prompting +{ + internal class CursorPosition + { + public int Left { get; } + public int Top { get; } + + private CursorPosition(IConsole console) + { + Left = console.CursorLeft; + Top = console.CursorTop; + } + + public static CursorPosition Snapshot(IConsole console) => new CursorPosition(console); + + public void Restore(IConsole console) => console.SetCursorPosition(Left, Top); + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/EditKeyHandlers.cs b/CommandDotNet/Prompting/EditKeyHandlers.cs new file mode 100644 index 000000000..fd1a4c3eb --- /dev/null +++ b/CommandDotNet/Prompting/EditKeyHandlers.cs @@ -0,0 +1,67 @@ +using System; + +namespace CommandDotNet.Prompting +{ + public static class EditKeyHandlers + { + public static ReadLineConfig UseDefaultEditing(this ReadLineConfig config) + { + config.KeyHandlers.Map(ConsoleKey.Backspace, Backspace); + config.KeyHandlers.MapControl(ConsoleKey.Backspace, BackspaceToStartOfWord); + config.KeyHandlers.MapControlShift(ConsoleKey.Backspace, BackspaceToStartOfLine); + config.KeyHandlers.Map(ConsoleKey.Delete, Delete); + config.KeyHandlers.MapControl(ConsoleKey.Delete, DeleteToEndOfWord); + config.KeyHandlers.MapControlShift(ConsoleKey.Delete, DeleteToEndOfLine); + config.KeyHandlers.Map(ConsoleKey.Escape, ClearLine); + config.KeyHandlers.Map(ConsoleKey.Clear, ClearLine); + config.KeyHandlers.Map(ConsoleKey.OemClear, ClearLine); + return config; + } + + public static ReadLineConfig UseReadLineEditing(this ReadLineConfig config) + { + config.KeyHandlers.MapControl(ConsoleKey.H, Backspace); + config.KeyHandlers.MapControl(ConsoleKey.D, Delete); + config.KeyHandlers.MapControl(ConsoleKey.L, ClearLine); + config.KeyHandlers.MapControl(ConsoleKey.U, BackspaceToStartOfLine); + config.KeyHandlers.MapControl(ConsoleKey.K, DeleteToEndOfLine); + config.KeyHandlers.MapControl(ConsoleKey.W, BackspaceToStartOfWord); + return config; + } + + public static void Backspace(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.Backspace(); + } + + public static void Delete(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.Delete(); + } + + public static void ClearLine(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.ClearLine(); + } + + public static void BackspaceToStartOfLine(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.Backspace(ctx.Line.PositionInText); + } + + public static void DeleteToEndOfLine(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.Delete(ctx.Line.Text.Length - ctx.Line.PositionInText); + } + + public static void BackspaceToStartOfWord(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.Backspace(ctx.Line.StartOfWordDistance()); + } + + public static void DeleteToEndOfWord(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.Delete(ctx.Line.EndOfWordDistance()); + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/HistoryKeyHandlers.cs b/CommandDotNet/Prompting/HistoryKeyHandlers.cs new file mode 100644 index 000000000..4edd62bc4 --- /dev/null +++ b/CommandDotNet/Prompting/HistoryKeyHandlers.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CommandDotNet.Extensions; + +namespace CommandDotNet.Prompting +{ + public static class HistoryKeyHandlers + { + public static ReadLineConfig UseDefaultHistory(this ReadLineConfig config) + { + config.KeyHandlers.Map(ConsoleKey.UpArrow, PrevHistory); + config.KeyHandlers.Map(ConsoleKey.DownArrow, NextHistory); + config.KeyHandlers.Map(ConsoleKey.PageUp, FirstHistory); + config.KeyHandlers.Map(ConsoleKey.PageDown, LastHistory); + config.KeyHandlers.MapControlShift(ConsoleKey.H, DisplayHistory); + return config; + } + + public static ReadLineConfig UseReadLineHistory(this ReadLineConfig config) + { + config.KeyHandlers.MapControl(ConsoleKey.P, PrevHistory); + config.KeyHandlers.MapControl(ConsoleKey.N, NextHistory); + return config; + } + + private class HistoryContext + { + public ICollection History; + public int Index; + + public HistoryContext(ICollection history) + { + History = history; + + // start at last item in the history + Index = History.Count - 1; + } + } + + public static void PrevHistory(ConsoleKeyInfo info, ReadLineContext ctx) + { + var historyContext = ctx.GetHistoryContext(); + if (historyContext.History.Any()) + { + ctx.Line.ClearLineAndWrite(historyContext.History.ElementAt(historyContext.Index)); + if (historyContext.Index > 0) + { + historyContext.Index--; + } + } + } + + public static void NextHistory(ConsoleKeyInfo info, ReadLineContext ctx) + { + var historyContext = ctx.GetHistoryContext(); + if (historyContext.History.Any()) + { + if (historyContext.Index < historyContext.History.Count-1) + { + historyContext.Index++; + if (historyContext.Index == historyContext.History.Count) + { + ctx.Line.ClearLine(); + } + else + { + ctx.Line.ClearLineAndWrite(historyContext.History.ElementAt(historyContext.Index)); + } + } + } + } + + private static void FirstHistory(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.GetHistoryContext().Index = 0; + PrevHistory(info, ctx); + } + + private static void LastHistory(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.GetHistoryContext().Index = 0; + NextHistory(info, ctx); + } + + private static void DisplayHistory(ConsoleKeyInfo info, ReadLineContext ctx) + { + var history = ctx.GetHistoryContext().History.ToCsv(Environment.NewLine); + ctx.Line.Notify(history, true); + } + + private static HistoryContext GetHistoryContext(this ReadLineContext ctx) + { + return (HistoryContext) ctx.State.GetOrAdd(typeof(HistoryContext), + type => new HistoryContext(ctx.History ?? new List()))!; + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/KeyHandlerDelegate.cs b/CommandDotNet/Prompting/KeyHandlerDelegate.cs new file mode 100644 index 000000000..545714d0f --- /dev/null +++ b/CommandDotNet/Prompting/KeyHandlerDelegate.cs @@ -0,0 +1,6 @@ +using System; + +namespace CommandDotNet.Prompting +{ + public delegate void KeyHandlerDelegate(ConsoleKeyInfo info, ReadLineContext ctx); +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/KeyHandlerMap.cs b/CommandDotNet/Prompting/KeyHandlerMap.cs new file mode 100644 index 000000000..3a94cb70e --- /dev/null +++ b/CommandDotNet/Prompting/KeyHandlerMap.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CommandDotNet.Prompting +{ + public class KeyHandlerMap + { + private Dictionary HandlersByKey = + new Dictionary(KeyComparer); + + private Dictionary HandlersByKeyChar = + new Dictionary(KeyCharComparer); + + #region Map methods + + public void Map(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, false, false, false); + HandlersByKey[info] = handler; + } + + public void MapAlt(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, false, true, false); + HandlersByKey[info] = handler; + } + + public void MapShift(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, true, false, false); + HandlersByKey[info] = handler; + } + + public void MapShiftAlt(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, true, true, false); + HandlersByKey[info] = handler; + } + + public void MapControl(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, false, false, true); + HandlersByKey[info] = handler; + } + + public void MapControlShift(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, true, false, true); + HandlersByKey[info] = handler; + } + + public void MapControlShiftAlt(ConsoleKey key, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(' ', key, true, true, true); + HandlersByKey[info] = handler; + } + + public void Map(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, false, false, false); + HandlersByKeyChar[info] = handler; + } + + public void MapAlt(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, false, true, false); + HandlersByKeyChar[info] = handler; + } + + public void MapShift(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, true, false, false); + HandlersByKeyChar[info] = handler; + } + + public void MapShiftAlt(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, true, true, false); + HandlersByKeyChar[info] = handler; + } + + public void MapControl(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, false, false, true); + HandlersByKeyChar[info] = handler; + } + + public void MapControlShift(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, true, false, true); + HandlersByKeyChar[info] = handler; + } + + public void MapControlShiftAlt(char keyChar, KeyHandlerDelegate handler) + { + var info = new ConsoleKeyInfo(keyChar, ConsoleKey.Zoom, true, true, true); + HandlersByKeyChar[info] = handler; + } + + #endregion + + public bool TryGet(ConsoleKeyInfo info, out KeyHandlerDelegate? handler) + { + return HandlersByKey.TryGetValue(info, out handler) + || HandlersByKeyChar.TryGetValue(info, out handler); + } + + public IEnumerable<(ConsoleKeyInfo info, KeyHandlerDelegate handler)> AllHandlers => + HandlersByKey.Select(kvp => (kvp.Key, kvp.Value)) + .Concat(HandlersByKeyChar.Select(kvp => (kvp.Key, kvp.Value))); + + #region Comparers + + public static IEqualityComparer KeyComparer { get; } = new KeyEqualityComparer(); + + public static IEqualityComparer KeyCharComparer { get; } = new KeyCharEqualityComparer(); + + private sealed class KeyEqualityComparer : IEqualityComparer + { + public bool Equals(ConsoleKeyInfo x, ConsoleKeyInfo y) => + x.Key == y.Key && x.Modifiers == y.Modifiers; + + public int GetHashCode(ConsoleKeyInfo info) + { + unchecked + { + return (info.Key.GetHashCode() * 397) ^ info.Modifiers.GetHashCode(); + } + } + } + + private sealed class KeyCharEqualityComparer : IEqualityComparer + { + public bool Equals(ConsoleKeyInfo x, ConsoleKeyInfo y) => + x.KeyChar == y.KeyChar && x.Modifiers == y.Modifiers; + + public int GetHashCode(ConsoleKeyInfo info) + { + unchecked + { + return (info.KeyChar.GetHashCode() * 397) ^ info.Modifiers.GetHashCode(); + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/Line.cs b/CommandDotNet/Prompting/Line.cs new file mode 100644 index 000000000..2ec5f04f5 --- /dev/null +++ b/CommandDotNet/Prompting/Line.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommandDotNet.Logging.LogProviders; +using CommandDotNet.Rendering; + +namespace CommandDotNet.Prompting +{ + public class Line + { + private readonly IConsole _console; + private readonly bool _isSecret; + private readonly StringBuilder _line = new StringBuilder(); + private readonly CursorPosition _startingPosition; + + private struct BufferRow + { + public int Top { get; } + public int StartOfLine { get; } + public int EndOfLine { get; } + public int Length => EndOfLine - StartOfLine; + + public BufferRow(int top, int startOfLine, int endOfLine) + { + Top = top; + StartOfLine = startOfLine; + EndOfLine = endOfLine; + } + + public static IEnumerable GetBufferRows(IConsole console, CursorPosition startingPosition) + { + var currentTop = console.CursorTop; + var currentLeft = console.CursorLeft; + + if (currentTop == startingPosition.Top) + { + yield return new BufferRow(currentTop, startingPosition.Left, currentLeft); + yield break; + } + var bufferWidth = console.BufferWidth; + + yield return new BufferRow(startingPosition.Top, startingPosition.Left, bufferWidth); + if (currentTop - startingPosition.Top > 1) + { + for (int i = startingPosition.Top + 1; i < (currentTop - 1); i++) + { + yield return new BufferRow(startingPosition.Top, 0, bufferWidth); + } + } + yield return new BufferRow(currentTop, 0, currentLeft); + } + } + + public string Text => _line.ToString(); + + public int PositionInText => DistanceFromStartOfLine(); + + public Line(IConsole console, bool isSecret = false) + { + _console = console; + _isSecret = isSecret; + _startingPosition = CursorPosition.Snapshot(console); + } + + private IEnumerable GetBufferRows() => BufferRow.GetBufferRows(_console, _startingPosition); + + public int DistanceFromStartOfLine() => GetBufferRows().Sum(r => r.Length); + + public bool IsStartOfLine => PositionInText == 0; + public bool IsEndOfLine => PositionInText == _line.Length; + + public void MoveToStartOfLine() + { + _startingPosition.Restore(_console); + } + + public void MoveToEndOfLine() + { + MoveRight(int.MaxValue); + } + + public void MoveLeft(int count = 1) + { + var distanceFromBol = PositionInText; + _console.CursorLeft -= Math.Min(count, distanceFromBol); + } + + public void MoveRight(int count = 1) + { + var distanceFromEol = _line.Length - PositionInText; + _console.CursorLeft += Math.Min(count, distanceFromEol); + } + + public int EndOfWordDistance() + { + var index = PositionInText; + var text = Text; + var i = index; + + while (i < text.Length && char.IsWhiteSpace(text[i])) + { + i++; + } + while (i < text.Length && !char.IsWhiteSpace(text[i])) + { + i++; + } + + return i - index; + } + + public int StartOfWordDistance() + { + var index = PositionInText; + var text = Text; + var i = index; + + if (i >= text.Length) + { + i--; + } + + // already at the start of a word? + if (i > 0 && !char.IsWhiteSpace(text[i]) && char.IsWhiteSpace(text[i - 1])) + { + i--; + } + + while (i >= 0 && char.IsWhiteSpace(text[i])) + { + i--; + } + while (i >= 0 && !char.IsWhiteSpace(text[i])) + { + i--; + } + + if (i < 0 || char.IsWhiteSpace(text[i])) + { + i++; + } + + return index - i; + } + + public void ClearLine() + { + MoveToStartOfLine(); + Delete(_line.Length); + } + + public void Backspace(int count = 1) + { + if (IsStartOfLine) return; + + MoveLeft(count); + Delete(count); + } + + public void Delete(int count = 1) + { + if (IsEndOfLine) return; + + _line.Remove(PositionInText, count); + + using var _ = RestoreCursorAndAdjust(); + _console.Write(_line.ToString().Substring(PositionInText)); + // remove trailing characters + _console.Write(new string(' ', count)); + _console.Write(new string('\b', count)); + } + + public void Write(ConsoleKeyInfo consoleKey) => Write(consoleKey.KeyChar); + + public void Write(char c) + { + Write(c.ToString()); + } + + public void Write(string text) + { + void WriteToConsole(string output) + { + if (!_isSecret) + { + _console.Write(output); + } + } + + if (IsEndOfLine) + { + _line.Append(text); + WriteToConsole(text); + } + else + { + _line.Insert(PositionInText, text); + var output = _line.ToString().Substring(PositionInText); + + // windows moves cursor to EOL + using var _ = RestoreCursorAndAdjust(() => MoveRight(text.Length)); + WriteToConsole(output); + + } + } + + private IDisposable RestoreCursorAndAdjust(Action postAdjustment = null) + { + // windows moves cursor to EOL after edits + var cursorPosition = CursorPosition.Snapshot(_console); + return new DisposableAction(() => + { + cursorPosition.Restore(_console); + postAdjustment?.Invoke(); + }); + } + + public void ClearLineAndWrite(string newText) + { + ClearLine(); + Write(newText); + } + + public void Notify(string notification, bool preserveText) + { + var value = _line.ToString(); + + MoveToStartOfLine(); + Delete(_line.Length); + _console.WriteLine(notification); + + if (preserveText && !value.IsNullOrEmpty()) + { + Write(value); + } + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/NavigationKeyHandlers.cs b/CommandDotNet/Prompting/NavigationKeyHandlers.cs new file mode 100644 index 000000000..c25be244f --- /dev/null +++ b/CommandDotNet/Prompting/NavigationKeyHandlers.cs @@ -0,0 +1,68 @@ +using System; + +namespace CommandDotNet.Prompting +{ + public static class NavigationKeyHandlers + { + public static ReadLineConfig UseDefaultNavigation(this ReadLineConfig config) + { + config.KeyHandlers.Map(ConsoleKey.LeftArrow, MoveCursorLeft); + config.KeyHandlers.Map(ConsoleKey.RightArrow, MoveCursorRight); + config.KeyHandlers.Map(ConsoleKey.Home, MoveCursorHome); + config.KeyHandlers.Map(ConsoleKey.End, MoveCursorEnd); + config.KeyHandlers.MapControl(ConsoleKey.LeftArrow, MoveToStartOfWord); + config.KeyHandlers.MapControl(ConsoleKey.RightArrow, MoveToEndOfWord); + return config; + } + + public static ReadLineConfig UseReadLineNavigation(this ReadLineConfig config) + { + config.KeyHandlers.MapControl(ConsoleKey.B, MoveCursorLeft); + config.KeyHandlers.MapControl(ConsoleKey.F, MoveCursorRight); + config.KeyHandlers.MapControl(ConsoleKey.A, MoveCursorHome); + config.KeyHandlers.MapControl(ConsoleKey.E, MoveCursorEnd); + return config; + } + + public static void Exit(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.ShouldExitPrompt = true; + } + + public static void Cancel(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.ShouldExitPrompt = true; + ctx.OnCtrlC?.Invoke(); + } + + public static void MoveToEndOfWord(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.MoveRight(ctx.Line.EndOfWordDistance()); + } + + public static void MoveToStartOfWord(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.MoveLeft(ctx.Line.StartOfWordDistance()); + } + + public static void MoveCursorEnd(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.MoveToEndOfLine(); + } + + public static void MoveCursorHome(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.MoveToStartOfLine(); + } + + public static void MoveCursorRight(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.MoveRight(); + } + + public static void MoveCursorLeft(ConsoleKeyInfo info, ReadLineContext ctx) + { + ctx.Line.MoveLeft(); + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/ReadLineConfig.cs b/CommandDotNet/Prompting/ReadLineConfig.cs new file mode 100644 index 000000000..fc4897f90 --- /dev/null +++ b/CommandDotNet/Prompting/ReadLineConfig.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommandDotNet.Extensions; + +namespace CommandDotNet.Prompting +{ + public class ReadLineConfig + { + private static Func factory = () => new ReadLineConfig() + .UseAllDefaultKeyHandlers() + .With(c => + { + c.History = DefaultHistory; + c.OnCtrlC = DefaultOnCtrlC; + }); + + public static Func Factory + { + get => factory; + set => factory = value ?? throw new ArgumentNullException(nameof(value)); + } + + public static IDictionary>? DefaultHistory { get; set; } + public static Action? DefaultOnCtrlC { get; set; } + + public ReadLineConfig() + { + KeyHandlers.Map(ConsoleKey.Enter, NavigationKeyHandlers.Exit); + KeyHandlers.MapControl(ConsoleKey.C, NavigationKeyHandlers.Cancel); + KeyHandlers.MapShiftAlt('?', ListKeyHandlers); + } + + private void ListKeyHandlers(ConsoleKeyInfo info, ReadLineContext ctx) + { + int maxKeyLength = 0; + int maxHandlerLength = 0; + + var sb = new StringBuilder(); + + var values = KeyHandlers.AllHandlers + .Select(h => + { + var handler = h.handler.Method.Name; + return (h.info, handler); + }) + .GroupBy(h => h.handler) + .Select(group => ( + handler:@group.Key, + keys:@group + .Select(h => (h.info.Modifiers,name:h.info.ToFriendlyName())) + .OrderBy(h => h.Modifiers) + .ThenBy(h => h.name) + .Select(h => h.name) + .ToCsv())) + .OrderBy(h => h.handler) + .Select(h => + { + maxKeyLength = Math.Max(maxKeyLength, h.keys.Length); + maxHandlerLength = Math.Max(maxHandlerLength, h.handler.Length); + return h; + }) + .ToList(); + values.ForEach(h => + { + sb.AppendLine($"{h.keys.PadRight(maxKeyLength)} > {h.handler.PadRight(maxHandlerLength)}"); + }); + + ctx.Line.Notify(sb.ToString(), preserveText: true); + } + + public IDictionary>? History { get; set; } + + public Action? OnCtrlC { get; set; } + + public KeyHandlerMap KeyHandlers { get; } = new KeyHandlerMap(); + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/ReadLineConfigExtensions.cs b/CommandDotNet/Prompting/ReadLineConfigExtensions.cs new file mode 100644 index 000000000..6c8f1658d --- /dev/null +++ b/CommandDotNet/Prompting/ReadLineConfigExtensions.cs @@ -0,0 +1,39 @@ +using System; + +namespace CommandDotNet.Prompting +{ + public static class ReadLineConfigExtensions + { + public static ReadLineConfig With(this ReadLineConfig config, Action alter) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (alter == null) + { + throw new ArgumentNullException(nameof(alter)); + } + + alter(config); + return config; + } + + public static ReadLineConfig UseAllDefaultKeyHandlers(this ReadLineConfig config) + { + return config + .UseDefaultNavigation() + .UseDefaultEditing() + .UseDefaultHistory(); + } + + public static ReadLineConfig UseAllReadLineKeyHandlers(this ReadLineConfig config) + { + return config + .UseReadLineNavigation() + .UseReadLineEditing() + .UseReadLineHistory(); + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompting/ReadLineContext.cs b/CommandDotNet/Prompting/ReadLineContext.cs new file mode 100644 index 000000000..2a2829c00 --- /dev/null +++ b/CommandDotNet/Prompting/ReadLineContext.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using CommandDotNet.Extensions; + +namespace CommandDotNet.Prompting +{ + public class ReadLineContext + { + public Line Line { get; } + public ReadLineConfig Config { get; } + public Dictionary State { get; } + + public ICollection? History { get; set; } + public Action? OnCtrlC { get; set; } + public bool ShouldExitPrompt { get; set; } + + public ReadLineContext(Line line, ReadLineConfig? readLineConfig = null) + { + Line = line ?? throw new ArgumentNullException(nameof(line)); + Config = readLineConfig ?? ReadLineConfig.Factory(); + State = new Dictionary(); + + History = Config.History?.GetValueOrDefault("common", () => new List()); + OnCtrlC = Config.OnCtrlC; + } + } +} \ No newline at end of file diff --git a/CommandDotNet/Prompts/Prompter.cs b/CommandDotNet/Prompts/Prompter.cs index 96c905898..32a9a1a83 100644 --- a/CommandDotNet/Prompts/Prompter.cs +++ b/CommandDotNet/Prompts/Prompter.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using CommandDotNet.Extensions; using CommandDotNet.Logging; +using CommandDotNet.Prompting; using CommandDotNet.Rendering; -using CommandDotNet.Tokens; namespace CommandDotNet.Prompts { @@ -55,17 +54,17 @@ private ICollection PromptForValueImpl( // Using Console.TreatControlCAsInput, the request can be captured and returned // via the isCancellationRequested parameter. - if (_console.TreatControlCAsInput) + if (_console.In.TreatControlCAsInput) { return PromptForValueRobustImpl(promptText, isPassword, isList, out isCancellationRequested); } Log.Debug("setting console.TreatControlCAsInput = true"); - _console.TreatControlCAsInput = true; + _console.In.TreatControlCAsInput = true; using (new DisposableAction(() => { Log.Debug("setting console.TreatControlCAsInput = false"); - _console.TreatControlCAsInput = false; + _console.In.TreatControlCAsInput = false; })) { return PromptForValueRobustImpl(promptText, isPassword, isList, out isCancellationRequested); @@ -73,138 +72,30 @@ private ICollection PromptForValueImpl( } private ICollection PromptForValueRobustImpl( - string promptText, bool isPassword, - bool isList, out bool isCancellationRequested) - { - // cannot: navigate with arrow keys, insert characters - - isCancellationRequested = false; - var consoleOut = _console.Out; - consoleOut.Write(promptText); - if (isList) - { - consoleOut.Write(" [ once to begin new value. twice to finish]"); - } - consoleOut.Write(": "); - if (isList) - { - consoleOut.WriteLine(); - } - - var values = new List(); - var sb = new StringBuilder(); - - do - { - var key = _console.ReadKey(true); - - if (key.IsCtrlC()) - { - sb.Length = 0; - isCancellationRequested = true; - break; - } - - if (key.Key == ConsoleKey.Enter) - { - if(!isList || key.Modifiers.HasFlag(ConsoleModifiers.Control)) - //if (key.Modifiers == 0 || !isList) - { - break; - } - - // isList == true - - if (sb.Length == 0) - { - // if user is confused by ctrl+enter, they may hit enter multiple times. - // enter for an empty entry can also exit. - break; - } - - values.Add(sb.ToString()); - sb.Length = 0; - consoleOut.WriteLine(); - } - else if (ConsoleKeys.PassThroughKeys.Contains(key.Key)) - { - sb.Append(key.KeyChar); - consoleOut.Write(isPassword ? "" : key.KeyChar.ToString()); - } - else if (key.Key == ConsoleKey.Backspace) - { - if (sb.Length > 0) - { - // if isPassword, there is nothing to backspace because passwords - // characters are immediately removed from the console - if (!isPassword) - { - consoleOut.Write("\b \b"); - } - sb.Length = sb.Length - 1; - } - } - else if (key.Key == ConsoleKey.Escape) - { - // if no prompt has been entered, the user wants to exit the prompt - if (sb.Length == 0) - { - break; - } - - // otherwise the user wants to clear the current entry - ClearCurrent(sb, consoleOut); - } - else if (key.Key == ConsoleKey.Clear || key.Key == ConsoleKey.OemClear) - { - ClearCurrent(sb, consoleOut); - } - - } while (true); - - if (sb.Length > 0) - { - values.Add(sb.ToString()); - } - consoleOut.WriteLine(); - return values; - } - - private ICollection? PromptForValueSimpleImpl( string promptText, bool isPassword, bool isList, out bool isCancellationRequested) { - // cannot: - // - skip prompts w/ escape - // - skip out of prompting and exit app with ctrl+c - // - support multiple entry w/ enter+modifier - - if (isPassword) - { - return PromptForValueRobustImpl(promptText, isPassword, isList, out isCancellationRequested); - } - - isCancellationRequested = false; - var consoleOut = _console.Out; - consoleOut.Write(promptText); - if (isList) - { - consoleOut.Write(" [separate values by space]"); - } - consoleOut.Write(": "); + // cannot: navigate with arrow keys, insert characters - var value = _console.In.ReadLine(); - consoleOut.WriteLine(); - if (value is null) - { - return null; - } - return isList - ? CommandLineStringSplitter.Instance.Split(value).ToCollection() - : new[] { value }; + bool ctrlC = false; + var readLineConfig = ReadLineConfig.Factory().With(c => c.OnCtrlC = () => ctrlC = true); + + var values = isList + ? _console.ReadLines( + promptText: promptText, + isSecret: isPassword, + readLineConfig: readLineConfig) + : _console.ReadLine( + promptText: promptText, + isSecret: isPassword, + readLineConfig: readLineConfig) + .ToEnumerable(); + + isCancellationRequested = ctrlC; + return values.ToCollection(); } - private static void ClearCurrent(StringBuilder sb, IStandardStreamWriter consoleOut) + private static void ClearCurrent(StringBuilder sb, IConsoleWriter consoleOut) { if (sb.Length > 0) { diff --git a/CommandDotNet/Rendering/IConsole.cs b/CommandDotNet/Rendering/IConsole.cs index d884bc5ec..2034ef660 100644 --- a/CommandDotNet/Rendering/IConsole.cs +++ b/CommandDotNet/Rendering/IConsole.cs @@ -4,62 +4,16 @@ // copied from System.CommandLine using System; +using System.Collections.Generic; +using CommandDotNet.Execution; +using CommandDotNet.Prompting; namespace CommandDotNet.Rendering { public interface IConsole : - IStandardOut, - IStandardError, - IStandardIn + IStandardOut, IStandardError, IStandardIn, + IConsoleColor, IConsoleBuffer, IConsoleWindow, IConsoleCursor { - /// - /// Read a key from the input - /// - /// When true, the key is not displayed in the output - /// - ConsoleKeyInfo ReadKey(bool intercept); - - /// - /// Gets or Sets value determine whether Ctrl+C is treated as ordinary input.
- /// When using System.Console, set this to true before or - /// Ctrl+C is not captured and does not interrupt. - ///
- bool TreatControlCAsInput { get; set; } - } - - public interface IStandardOut - { - IStandardStreamWriter Out { get; } - - bool IsOutputRedirected { get; } - } - - public interface IStandardError - { - IStandardStreamWriter Error { get; } - - bool IsErrorRedirected { get; } - } - - public interface IStandardStream - { - } - - public interface IStandardIn : IStandardStream - { - IStandardStreamReader In { get; } - - bool IsInputRedirected { get; } - } - - public interface IStandardStreamWriter : IStandardStream - { - void Write(string? value); - } - - public interface IStandardStreamReader : IStandardStream - { - string? ReadLine(); - string? ReadToEnd(); + string Title { get; set; } } } \ No newline at end of file diff --git a/CommandDotNet/Rendering/IConsoleBuffer.cs b/CommandDotNet/Rendering/IConsoleBuffer.cs new file mode 100644 index 000000000..2cde40f37 --- /dev/null +++ b/CommandDotNet/Rendering/IConsoleBuffer.cs @@ -0,0 +1,10 @@ +namespace CommandDotNet.Rendering +{ + public interface IConsoleBuffer + { + int BufferWidth { get; set; } + int BufferHeight { get; set; } + void SetBufferSize(int width, int height); + void Clear(); + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IConsoleColor.cs b/CommandDotNet/Rendering/IConsoleColor.cs new file mode 100644 index 000000000..a77437fcb --- /dev/null +++ b/CommandDotNet/Rendering/IConsoleColor.cs @@ -0,0 +1,11 @@ +using System; + +namespace CommandDotNet.Rendering +{ + public interface IConsoleColor + { + ConsoleColor BackgroundColor { get; set; } + ConsoleColor ForegroundColor { get; set; } + void ResetColor(); + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IConsoleCursor.cs b/CommandDotNet/Rendering/IConsoleCursor.cs new file mode 100644 index 000000000..8dafce582 --- /dev/null +++ b/CommandDotNet/Rendering/IConsoleCursor.cs @@ -0,0 +1,11 @@ +namespace CommandDotNet.Rendering +{ + public interface IConsoleCursor + { + int CursorSize { get; set; } + bool CursorVisible { get; set; } + int CursorLeft { get; set; } + int CursorTop { get; set; } + void SetCursorPosition(int left, int top); + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IConsoleReader.cs b/CommandDotNet/Rendering/IConsoleReader.cs new file mode 100644 index 000000000..c7f533f43 --- /dev/null +++ b/CommandDotNet/Rendering/IConsoleReader.cs @@ -0,0 +1,27 @@ +using System; + +namespace CommandDotNet.Rendering +{ + public interface IConsoleReader + { + bool KeyAvailable { get; } + bool NumberLock { get; } + bool CapsLock { get; } + + /// + /// Gets or Sets value determine whether Ctrl+C is treated as ordinary input.
+ /// When using System.Console, set this to true before or + /// Ctrl+C is not captured and does not interrupt. + ///
+ bool TreatControlCAsInput { get; set; } + + /// + /// Read a key from the input + /// + /// When true, the key is not displayed in the output + /// + ConsoleKeyInfo ReadKey(bool intercept); + + string? ReadLine(); + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IConsoleWindow.cs b/CommandDotNet/Rendering/IConsoleWindow.cs new file mode 100644 index 000000000..e9f2ab5a8 --- /dev/null +++ b/CommandDotNet/Rendering/IConsoleWindow.cs @@ -0,0 +1,12 @@ +namespace CommandDotNet.Rendering +{ + public interface IConsoleWindow + { + int WindowLeft { get; set; } + int WindowTop { get; set; } + int WindowWidth { get; set; } + int WindowHeight { get; set; } + void SetWindowPosition(int left, int top); + void SetWindowSize(int width, int height); + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IConsoleWriter.cs b/CommandDotNet/Rendering/IConsoleWriter.cs new file mode 100644 index 000000000..0230d1949 --- /dev/null +++ b/CommandDotNet/Rendering/IConsoleWriter.cs @@ -0,0 +1,7 @@ +namespace CommandDotNet.Rendering +{ + public interface IConsoleWriter + { + void Write(string? value); + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IStandardError.cs b/CommandDotNet/Rendering/IStandardError.cs new file mode 100644 index 000000000..aae4612e2 --- /dev/null +++ b/CommandDotNet/Rendering/IStandardError.cs @@ -0,0 +1,9 @@ +namespace CommandDotNet.Rendering +{ + public interface IStandardError + { + IConsoleWriter Error { get; } + + bool IsErrorRedirected { get; } + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IStandardIn.cs b/CommandDotNet/Rendering/IStandardIn.cs new file mode 100644 index 000000000..55de64f13 --- /dev/null +++ b/CommandDotNet/Rendering/IStandardIn.cs @@ -0,0 +1,9 @@ +namespace CommandDotNet.Rendering +{ + public interface IStandardIn + { + IConsoleReader In { get; } + + bool IsInputRedirected { get; } + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/IStandardOut.cs b/CommandDotNet/Rendering/IStandardOut.cs new file mode 100644 index 000000000..51f7047b0 --- /dev/null +++ b/CommandDotNet/Rendering/IStandardOut.cs @@ -0,0 +1,9 @@ +namespace CommandDotNet.Rendering +{ + public interface IStandardOut + { + IConsoleWriter Out { get; } + + bool IsOutputRedirected { get; } + } +} \ No newline at end of file diff --git a/CommandDotNet/Rendering/SystemConsole.cs b/CommandDotNet/Rendering/SystemConsole.cs index f81399997..80e3c1d13 100644 --- a/CommandDotNet/Rendering/SystemConsole.cs +++ b/CommandDotNet/Rendering/SystemConsole.cs @@ -9,34 +9,134 @@ namespace CommandDotNet.Rendering { public class SystemConsole : IConsole { - public SystemConsole() + public string Title { - Error = StandardStreamWriter.Create(Console.Error); - Out = StandardStreamWriter.Create(Console.Out); - In = StandardStreamReader.Create(Console.In); + get => Console.Title; + set => Console.Title = value; } - public IStandardStreamWriter Error { get; } + #region IStandardError + + public IConsoleWriter Error { get; } = ConsoleWriter.Create(Console.Error); public bool IsErrorRedirected => Console.IsErrorRedirected; - public IStandardStreamWriter Out { get; } + #endregion + + #region IStandardOut + + public IConsoleWriter Out { get; } = ConsoleWriter.Create(Console.Out); public bool IsOutputRedirected => Console.IsOutputRedirected; - public IStandardStreamReader In { get; } + #endregion + + #region IStandardIn + + public IConsoleReader In { get; } = new SystemConsoleReader(); public bool IsInputRedirected => Console.IsInputRedirected; - public ConsoleKeyInfo ReadKey(bool intercept = false) + #endregion + + #region IConsoleColor + + public ConsoleColor BackgroundColor + { + get => Console.BackgroundColor; + set => Console.BackgroundColor = value; + } + + public ConsoleColor ForegroundColor + { + get => Console.ForegroundColor; + set => Console.ForegroundColor = value; + } + + public void ResetColor() => Console.ResetColor(); + + #endregion + + #region IConsoleBuffer + + public int BufferWidth + { + get => Console.BufferWidth; + set => Console.BufferWidth = value; + } + + public int BufferHeight + { + get => Console.BufferHeight; + set => Console.BufferHeight = value; + } + + public void SetBufferSize(int width, int height) => Console.SetBufferSize(width, height); + + public void Clear() => Console.Clear(); + + #endregion + + #region IConsoleWindow + + public int WindowLeft + { + get => Console.WindowLeft; + set => Console.WindowLeft = value; + } + + public int WindowTop + { + get => Console.WindowTop; + set => Console.WindowTop = value; + } + + public int WindowWidth { - return Console.ReadKey(intercept); + get => Console.WindowWidth; + set => Console.WindowWidth = value; } - public bool TreatControlCAsInput + public int WindowHeight { - get => Console.TreatControlCAsInput; - set => Console.TreatControlCAsInput = value; + get => Console.WindowHeight; + set => Console.WindowHeight = value; } + + public void SetWindowPosition(int left, int top) => Console.SetWindowPosition(left, top); + + public void SetWindowSize(int width, int height) => Console.SetBufferSize(width, height); + + #endregion + + #region IConsoleCursor + + public int CursorSize + { + get => Console.CursorSize; + set => Console.CursorSize = value; + } + + public bool CursorVisible + { + get => Console.CursorVisible; + set => Console.CursorVisible = value; + } + + public int CursorLeft + { + get => Console.CursorLeft; + set => Console.CursorLeft = value; + } + + public int CursorTop + { + get => Console.CursorTop; + set => Console.CursorTop = value; + } + + public void SetCursorPosition(int left, int top) => Console.SetCursorPosition(left, top); + + #endregion } } \ No newline at end of file diff --git a/CommandDotNet/StandardStreamReader.cs b/CommandDotNet/StandardStreamReader.cs deleted file mode 100644 index 99f8fce2b..000000000 --- a/CommandDotNet/StandardStreamReader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.IO; -using CommandDotNet.Rendering; - -namespace CommandDotNet -{ - public static class StandardStreamReader - { - public static IStandardStreamReader Create(TextReader reader) - { - if (reader == null) - { - throw new ArgumentNullException(nameof(reader)); - } - return new AnonymousStandardStreamReader(reader); - } - - private class AnonymousStandardStreamReader : IStandardStreamReader - { - private readonly TextReader _reader; - - public AnonymousStandardStreamReader(TextReader reader) - { - _reader = reader; - } - - public string ReadLine() - { - return _reader.ReadLine(); - } - - public string ReadToEnd() - { - return _reader.ReadToEnd(); - } - } - } -} \ No newline at end of file diff --git a/CommandDotNet/SystemConsoleReader.cs b/CommandDotNet/SystemConsoleReader.cs new file mode 100644 index 000000000..98249f245 --- /dev/null +++ b/CommandDotNet/SystemConsoleReader.cs @@ -0,0 +1,35 @@ +using System; +using CommandDotNet.Rendering; + +namespace CommandDotNet +{ + public class SystemConsoleReader : IConsoleReader + { + public bool KeyAvailable => Console.KeyAvailable; + + public bool NumberLock => Console.NumberLock; + + public bool CapsLock => Console.CapsLock; + + public bool TreatControlCAsInput + { + get => Console.TreatControlCAsInput; + set => Console.TreatControlCAsInput = value; + } + + public ConsoleKeyInfo ReadKey(bool intercept = false) + { + return Console.ReadKey(intercept); + } + + public int Read() + { + return Console.Read(); + } + + public string ReadLine() + { + return Console.ReadLine(); + } + } +} \ No newline at end of file