diff --git a/README.md b/README.md index 2c6c8de..a718e4f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A terminal abstraction with platform specific drivers. +## Disclaimer +This is a work in progress, and usage is not yet recommended. +Things will change, move around and break. + ## Acknowledgement Inspired by [system-terminal](https://github.com/alexrp/system-terminal) written by Alex Rønne Petersen. @@ -10,7 +14,9 @@ Inspired by [system-terminal](https://github.com/alexrp/system-terminal) written - [x] **Windows** - [x] STDIN - - [x] Read + - [x] Read Key + - [x] Read Single Character (if raw mode is enabled) + - [x] Read Line - [x] Get encoding - [x] Set encoding - [x] Redirect to custom reader @@ -23,8 +29,8 @@ Inspired by [system-terminal](https://github.com/alexrp/system-terminal) written - [x] Is handle redirected? - [x] Raw mode (enable/disable) - [x] Signals - - [x] SIGINT - - [x] SIGQUIT + - [x] SIGINT (CTRL+C) + - [x] SIGQUIT (CTRL+BREAK) - [x] Window - [x] Get width - [x] Get height @@ -48,8 +54,10 @@ Inspired by [system-terminal](https://github.com/alexrp/system-terminal) written - [x] DECSET 1049 (Disable alternative buffer) - [x] **Linux** - - [x] STDIN - - [x] Read + - [ ] STDIN + - [ ] Read Key + - [ ] Read Single Character + - [ ] Read Line - [x] Get encoding - [x] Set encoding (NOT SUPPORTED) - [x] Redirect to custom reader @@ -69,8 +77,10 @@ Inspired by [system-terminal](https://github.com/alexrp/system-terminal) written - [x] Get height - [x] **macOS** - - [x] STDIN - - [x] Read + - [ ] STDIN + - [ ] Read Key + - [ ] Read Single Character + - [ ] Read Line - [x] Get encoding - [x] Set encoding (NOT SUPPORTED) - [x] Redirect to custom reader diff --git a/examples/Ansi/Program.cs b/examples/Ansi/Program.cs index 3378c1d..a59783b 100644 --- a/examples/Ansi/Program.cs +++ b/examples/Ansi/Program.cs @@ -22,20 +22,15 @@ namespace Examples terminal.WriteLine(); terminal.WriteLine("Press ANY key"); terminal.WriteLine(); - terminal.ReadRaw(); // Do some line manipulation terminal.Write("\u001b[6;8H[Delete after]\u001b[0K"); - terminal.ReadRaw(); terminal.Write("\u001b[5;15H\u001b[1K[Delete before]"); - terminal.ReadRaw(); terminal.Write("\u001b[4;15H\u001b[2K[Delete line]"); - terminal.ReadRaw(); // Write some text in an alternate buffer terminal.Write("\u001b[?1049h"); terminal.WriteLine("HELLO WORLD!"); - terminal.ReadRaw(); terminal.Write("\u001b[?1049l"); terminal.Write(""); diff --git a/examples/Input/Input.csproj b/examples/Input/Input.csproj new file mode 100644 index 0000000..90c17ed --- /dev/null +++ b/examples/Input/Input.csproj @@ -0,0 +1,14 @@ + + + + Exe + net5.0 + false + Input + + + + + + + diff --git a/examples/Input/Program.cs b/examples/Input/Program.cs new file mode 100644 index 0000000..dd939ba --- /dev/null +++ b/examples/Input/Program.cs @@ -0,0 +1,45 @@ +using System; +using Spectre.Terminals; + +namespace Input +{ + public static class Program + { + public static void Main(string[] args) + { + ReadLine(); + ReadKeys(); + } + + private static void ReadLine() + { + Terminal.Shared.Write("Write something> "); + var line = Terminal.Shared.Input.ReadLine(); + Terminal.Shared.WriteLine($"Read = {line}"); + } + + private static void ReadKeys() + { + Terminal.Shared.WriteLine(); + Terminal.Shared.WriteLine("[Press any keys]"); + + while (true) + { + // Read a key from the keyboard + var key = Terminal.Shared.Input.ReadKey(); + if (key.Key == ConsoleKey.Escape) + { + break; + } + + // Get the character representation + var character = !char.IsWhiteSpace(key.KeyChar) + ? key.KeyChar : '*'; + + // Write to terminal + Terminal.Shared.WriteLine( + $"{character} [KEY={key.Key} MOD={key.Modifiers}]"); + } + } + } +} diff --git a/src/Spectre.Terminals.sln b/src/Spectre.Terminals.sln index 4119c52..2047a04 100644 --- a/src/Spectre.Terminals.sln +++ b/src/Spectre.Terminals.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Info", "..\examples\Info\In EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Signals", "..\examples\Signals\Signals.csproj", "{731549D8-F3E9-4B1C-89B3-455875FDAB30}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Input", "..\examples\Input\Input.csproj", "{67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +87,18 @@ Global {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x64.Build.0 = Release|Any CPU {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x86.ActiveCfg = Release|Any CPU {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x86.Build.0 = Release|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x64.ActiveCfg = Debug|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x64.Build.0 = Debug|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x86.Build.0 = Debug|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|Any CPU.Build.0 = Release|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x64.ActiveCfg = Release|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x64.Build.0 = Release|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x86.ActiveCfg = Release|Any CPU + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,6 +107,7 @@ Global {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E} = {02C78B96-9010-48EC-ADB5-4F48884F8937} {DEE537E6-84FE-4314-81B3-FEBA5021A2A2} = {02C78B96-9010-48EC-ADB5-4F48884F8937} {731549D8-F3E9-4B1C-89B3-455875FDAB30} = {02C78B96-9010-48EC-ADB5-4F48884F8937} + {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF} = {02C78B96-9010-48EC-ADB5-4F48884F8937} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3CAB9C8-0317-476E-AEA8-66EF76BA8661} diff --git a/src/Spectre.Terminals/Drivers/Unix/UnixDriver.cs b/src/Spectre.Terminals/Drivers/Unix/UnixDriver.cs index 5ec4e77..1fb9db6 100644 --- a/src/Spectre.Terminals/Drivers/Unix/UnixDriver.cs +++ b/src/Spectre.Terminals/Drivers/Unix/UnixDriver.cs @@ -122,7 +122,7 @@ namespace Spectre.Terminals.Drivers if (!signalEventArguments.Cancel) { //// Get the value early to avoid ObjectDisposedException. - var num = ((UnixSignal)signal).Signum; + var num = signal.Signum; //// Remove our signal handler and send the signal again. Since we //// have overwritten the signal handlers in CoreCLR and diff --git a/src/Spectre.Terminals/Drivers/Unix/UnixTerminalReader.cs b/src/Spectre.Terminals/Drivers/Unix/UnixTerminalReader.cs index 125bfff..d178e1b 100644 --- a/src/Spectre.Terminals/Drivers/Unix/UnixTerminalReader.cs +++ b/src/Spectre.Terminals/Drivers/Unix/UnixTerminalReader.cs @@ -1,94 +1,111 @@ -using System; -using System.Text; -using System.Threading; -using Mono.Unix.Native; - -namespace Spectre.Terminals.Drivers -{ - internal sealed class UnixTerminalReader : ITerminalReader - { - private readonly Encoding _encoding; - - public Encoding Encoding - { - get => _encoding; - set { /* Do nothing for now */ } - } - - public bool IsRedirected => !Syscall.isatty(UnixConstants.STDIN); - +using System; +using System.Text; +using System.Threading; +using Mono.Unix.Native; + +namespace Spectre.Terminals.Drivers +{ + internal sealed class UnixTerminalReader : ITerminalReader + { + private readonly Encoding _encoding; + + public Encoding Encoding + { + get => _encoding; + set { /* Do nothing for now */ } + } + + public bool IsKeyAvailable => throw new NotSupportedException("Not yet supported"); + + public bool IsRedirected => !Syscall.isatty(UnixConstants.STDIN); + public UnixTerminalReader() { _encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - } - - public unsafe int Read(Span buffer) - { - if (buffer.IsEmpty) - { - return 0; - } - - long ret; - - while (true) - { - fixed (byte* p = buffer) - { - while ((ret = Syscall.read(UnixConstants.STDIN, p, (ulong)buffer.Length)) == -1 && - Stdlib.GetLastError() == Errno.EINTR) - { - // Retry in case we get interrupted by a signal. - } - - if (ret != -1) - { - break; - } - - var err = Stdlib.GetLastError(); - - // The descriptor was probably redirected to a program that ended. Just - // silently ignore this situation. - // - // The strange condition where errno is zero happens e.g. on Linux if - // the process is killed while blocking in the read system call. - if (err == 0 || err == Errno.EPIPE) - { - ret = 0; - - break; - } - - // The file descriptor has been configured as non-blocking. Instead of - // busily trying to read over and over, poll until we can write and then - // try again. - if (err == Errno.EAGAIN) - { - _ = Syscall.poll( - new[] - { - new Pollfd - { - fd = UnixConstants.STDIN, - events = PollEvents.POLLIN, - }, - }, 1, Timeout.Infinite); - - continue; - } - - if (err == 0) - { - err = Errno.EBADF; - } - - throw new InvalidOperationException( - $"Could not read from STDIN: {Stdlib.strerror(err)}"); - } - } - - return (int)ret; - } - } -} + } + + public int Read() + { + throw new NotSupportedException("Not yet supported"); + } + + public string? ReadLine() + { + throw new NotSupportedException("Not yet supported"); + } + + public ConsoleKeyInfo ReadKey() + { + throw new NotSupportedException("Not yet supported"); + } + + private static unsafe int Read(Span buffer) + { + if (buffer.IsEmpty) + { + return 0; + } + + long ret; + + while (true) + { + fixed (byte* p = buffer) + { + while ((ret = Syscall.read(UnixConstants.STDIN, p, (ulong)buffer.Length)) == -1 && + Stdlib.GetLastError() == Errno.EINTR) + { + // Retry in case we get interrupted by a signal. + } + + if (ret != -1) + { + break; + } + + var err = Stdlib.GetLastError(); + + // The descriptor was probably redirected to a program that ended. Just + // silently ignore this situation. + // + // The strange condition where errno is zero happens e.g. on Linux if + // the process is killed while blocking in the read system call. + if (err == 0 || err == Errno.EPIPE) + { + ret = 0; + + break; + } + + // The file descriptor has been configured as non-blocking. Instead of + // busily trying to read over and over, poll until we can write and then + // try again. + if (err == Errno.EAGAIN) + { + _ = Syscall.poll( + new[] + { + new Pollfd + { + fd = UnixConstants.STDIN, + events = PollEvents.POLLIN, + }, + }, 1, Timeout.Infinite); + + continue; + } + + if (err == 0) + { + err = Errno.EBADF; + } + + throw new InvalidOperationException( + $"Could not read from STDIN: {Stdlib.strerror(err)}"); + } + } + + return (int)ret; + } + } +} diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsConsoleStream.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsConsoleStream.cs new file mode 100644 index 0000000..bb4655b --- /dev/null +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsConsoleStream.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Windows.Sdk; + +namespace Spectre.Terminals.Drivers +{ + internal sealed class WindowsConsoleStream : Stream + { + private const int BytesPerWChar = 2; + + private readonly SafeHandle _handle; + private readonly bool _useFileAPIs; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public WindowsConsoleStream(SafeHandle handle, bool useFileApis) + { + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); + _useFileAPIs = useFileApis; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return ReadFromHandle(new Span(buffer, offset, count)); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + private unsafe int ReadFromHandle(Span buffer) + { + if (buffer.IsEmpty) + { + return 0; + } + + bool readSuccess; + var bytesRead = 0; + + fixed (byte* p = buffer) + { + if (_useFileAPIs) + { + uint result; + var ptrResult = &result; + readSuccess = PInvoke.ReadFile(_handle, p, (uint)buffer.Length, ptrResult, null); + bytesRead = (int)result; + } + else + { + uint result; + var ptrResult = &result; + readSuccess = PInvoke.ReadConsole(_handle, p, (uint)buffer.Length, out var charsRead, null); + bytesRead = (int)(charsRead * BytesPerWChar); + } + } + + if (readSuccess) + { + return bytesRead; + } + + throw new InvalidOperationException("Could not read from console input"); + } + } +} diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsConstants.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsConstants.cs index 96c75d9..5fd3c45 100644 --- a/src/Spectre.Terminals/Drivers/Windows/WindowsConstants.cs +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsConstants.cs @@ -9,6 +9,8 @@ namespace Spectre.Terminals.Drivers public const int ERROR_BROKEN_PIPE = 109; public const int ERROR_NO_DATA = 232; + public const int KEY_EVENT = 0x0001; + public const uint GENERIC_READ = 0x80000000; public const uint GENERIC_WRITE = 0x40000000; diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsDriver.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsDriver.cs index dc8c523..fa0159c 100644 --- a/src/Spectre.Terminals/Drivers/Windows/WindowsDriver.cs +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsDriver.cs @@ -1,11 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Windows.Sdk; -using Spectre.Terminals.Emulation; namespace Spectre.Terminals.Drivers { - internal sealed class WindowsDriver : ITerminalDriver, IDisposable + internal sealed class WindowsDriver : ITerminalDriver { [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")] private const CONSOLE_MODE IN_MODE = CONSOLE_MODE.ENABLE_PROCESSED_INPUT | CONSOLE_MODE.ENABLE_LINE_INPUT | CONSOLE_MODE.ENABLE_ECHO_INPUT; @@ -37,11 +36,10 @@ namespace Spectre.Terminals.Drivers _input = new WindowsTerminalReader(this); _input.AddMode( - CONSOLE_MODE.ENABLE_PROCESSED_INPUT | CONSOLE_MODE.ENABLE_LINE_INPUT | CONSOLE_MODE.ENABLE_ECHO_INPUT | - CONSOLE_MODE.ENABLE_INSERT_MODE | - CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_INPUT); + CONSOLE_MODE.ENABLE_PROCESSED_INPUT | + CONSOLE_MODE.ENABLE_INSERT_MODE); _output = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_OUTPUT_HANDLE); _output.AddMode( @@ -82,7 +80,7 @@ namespace Spectre.Terminals.Drivers public bool EmitSignal(TerminalSignal signal) { - return _signals.Emit(signal); + return WindowsSignals.Emit(signal); } public bool EnableRawMode() diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsKeyReader.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsKeyReader.cs new file mode 100644 index 0000000..563c47b --- /dev/null +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsKeyReader.cs @@ -0,0 +1,208 @@ +// Parts of this code used from: https://github.com/dotnet/runtime +// Licensed to the .NET Foundation under one or more agreements. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Windows.Sdk; +using static Spectre.Terminals.Drivers.WindowsConstants; + +namespace Spectre.Terminals.Drivers +{ + internal sealed class WindowsKeyReader + { + private const short AltVKCode = 0x12; + + private readonly object _lock; + private readonly SafeHandle _handle; + private INPUT_RECORD _cachedInputRecord; + + [Flags] + private enum ControlKeyState + { + Unknown = 0, + RightAltPressed = 0x0001, + LeftAltPressed = 0x0002, + RightCtrlPressed = 0x0004, + LeftCtrlPressed = 0x0008, + ShiftPressed = 0x0010, + NumLockOn = 0x0020, + ScrollLockOn = 0x0040, + CapsLockOn = 0x0080, + EnhancedKey = 0x0100, + } + + public WindowsKeyReader(SafeHandle handle) + { + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); + _lock = new object(); + } + + public unsafe bool IsKeyAvailable() + { + if (_cachedInputRecord.EventType == KEY_EVENT) + { + return true; + } + + INPUT_RECORD ir; + var buffer = new Span(new INPUT_RECORD[1]); + + while (true) + { + var r = PInvoke.PeekConsoleInput(_handle, buffer, out var numEventsRead); + if (!r) + { + throw new InvalidOperationException(); + } + + if (numEventsRead == 0) + { + return false; + } + + ir = buffer[0]; + + // Skip non key-down && mod key events. + if (!IsKeyDownEvent(ir) || IsModKey(ir)) + { + r = PInvoke.ReadConsoleInput(_handle, buffer, out _); + if (!r) + { + throw new InvalidOperationException(); + } + } + else + { + return true; + } + } + } + + public ConsoleKeyInfo ReadKey() + { + INPUT_RECORD ir; + var buffer = new Span(new INPUT_RECORD[1]); + + lock (_lock) + { + if (_cachedInputRecord.EventType == KEY_EVENT) + { + // We had a previous keystroke with repeated characters. + ir = _cachedInputRecord; + if (_cachedInputRecord.Event.KeyEvent.wRepeatCount == 0) + { + _cachedInputRecord.EventType = ushort.MaxValue; + } + else + { + _cachedInputRecord.Event.KeyEvent.wRepeatCount--; + } + + // We will return one key from this method, so we decrement the + // repeatCount here, leaving the cachedInputRecord in the "queue". + } + else + { + // We did NOT have a previous keystroke with repeated characters: + while (true) + { + var r = PInvoke.ReadConsoleInput(_handle, buffer, out var numEventsRead); + if (!r || numEventsRead != 1) + { + // This will fail when stdin is redirected from a file or pipe. + // We could theoretically call Console.Read here, but I + // think we might do some things incorrectly then. + throw new InvalidOperationException("Could not read from STDIN. Has it been redirected?"); + } + + ir = buffer[0]; + var keyCode = ir.Event.KeyEvent.wVirtualKeyCode; + + // First check for non-keyboard events & discard them. Generally we tap into only KeyDown events and ignore the KeyUp events + // but it is possible that we are dealing with a Alt+NumPad unicode key sequence, the final unicode char is revealed only when + // the Alt key is released (i.e when the sequence is complete). To avoid noise, when the Alt key is down, we should eat up + // any intermediate key strokes (from NumPad) that collectively forms the Unicode character. + if (!IsKeyDownEvent(ir)) + { + // REVIEW: Unicode IME input comes through as KeyUp event with no accompanying KeyDown. + if (keyCode != AltVKCode) + { + continue; + } + } + + var ch = (char)ir.Event.KeyEvent.uChar.UnicodeChar; + + // In a Alt+NumPad unicode sequence, when the alt key is released uChar will represent the final unicode character, we need to + // surface this. VirtualKeyCode for this event will be Alt from the Alt-Up key event. This is probably not the right code, + // especially when we don't expose ConsoleKey.Alt, so this will end up being the hex value (0x12). VK_PACKET comes very + // close to being useful and something that we could look into using for this purpose... + if (ch == 0) + { + // Skip mod keys. + if (IsModKey(ir)) + { + continue; + } + } + + // When Alt is down, it is possible that we are in the middle of a Alt+NumPad unicode sequence. + // Escape any intermediate NumPad keys whether NumLock is on or not (notepad behavior) + var key = (ConsoleKey)keyCode; + if (IsAltKeyDown(ir) && ((key >= ConsoleKey.NumPad0 && key <= ConsoleKey.NumPad9) + || (key == ConsoleKey.Clear) || (key == ConsoleKey.Insert) + || (key >= ConsoleKey.PageUp && key <= ConsoleKey.DownArrow))) + { + continue; + } + + if (ir.Event.KeyEvent.wRepeatCount > 1) + { + ir.Event.KeyEvent.wRepeatCount--; + _cachedInputRecord = ir; + } + + break; + } + } + + // we did NOT have a previous keystroke with repeated characters. + } + + var state = (ControlKeyState)ir.Event.KeyEvent.dwControlKeyState; + var shift = (state & ControlKeyState.ShiftPressed) != 0; + var alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0; + var control = (state & (ControlKeyState.LeftCtrlPressed | ControlKeyState.RightCtrlPressed)) != 0; + + return new ConsoleKeyInfo( + (char)ir.Event.KeyEvent.uChar.UnicodeChar, + (ConsoleKey)ir.Event.KeyEvent.wVirtualKeyCode, + shift, alt, control); + } + + private static bool IsKeyDownEvent(INPUT_RECORD ir) + { + return ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown.Value != 0; + } + + private static bool IsModKey(INPUT_RECORD ir) + { + // We should also skip over Shift, Control, and Alt, as well as caps lock. + // Apparently we don't need to check for 0xA0 through 0xA5, which are keys like + // Left Control & Right Control. See the ConsoleKey enum for these values. + var keyCode = ir.Event.KeyEvent.wVirtualKeyCode; + return (keyCode >= 0x10 && keyCode <= 0x12) || keyCode == 0x14 || keyCode == 0x90 || keyCode == 0x91; + } + + // For tracking Alt+NumPad unicode key sequence. When you press Alt key down + // and press a numpad unicode decimal sequence and then release Alt key, the + // desired effect is to translate the sequence into one Unicode KeyPress. + // We need to keep track of the Alt+NumPad sequence and surface the final + // unicode char alone when the Alt key is released. + private static bool IsAltKeyDown(INPUT_RECORD ir) + { + return (((ControlKeyState)ir.Event.KeyEvent.dwControlKeyState) + & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsSignals.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsSignals.cs index d04d846..3c41edf 100644 --- a/src/Spectre.Terminals/Drivers/Windows/WindowsSignals.cs +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsSignals.cs @@ -35,7 +35,7 @@ namespace Spectre.Terminals.Drivers UninstallHandler(); } - public bool Emit(TerminalSignal signal) + public static bool Emit(TerminalSignal signal) { uint? ctrlEvent = signal switch { diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalHandle.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalHandle.cs index 75d719e..25b740c 100644 --- a/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalHandle.cs +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalHandle.cs @@ -7,11 +7,15 @@ namespace Spectre.Terminals.Drivers { internal abstract class WindowsTerminalHandle : IDisposable { + private readonly object _lock; + public SafeHandle Handle { get; set; } public bool IsRedirected { get; } - public WindowsTerminalHandle(STD_HANDLE_TYPE handle) + protected WindowsTerminalHandle(STD_HANDLE_TYPE handle) { + _lock = new object(); + Handle = PInvoke.GetStdHandle_SafeHandle(handle); IsRedirected = !GetMode(out _) || (PInvoke.GetFileType(Handle) & 2) == 0; } @@ -35,22 +39,28 @@ namespace Spectre.Terminals.Drivers public unsafe bool AddMode(CONSOLE_MODE mode) { - if (GetMode(out var currentMode)) + lock (_lock) { - return PInvoke.SetConsoleMode(Handle, currentMode.Value | mode); - } + if (GetMode(out var currentMode)) + { + return PInvoke.SetConsoleMode(Handle, currentMode.Value | mode); + } - return false; + return false; + } } public unsafe bool RemoveMode(CONSOLE_MODE mode) { - if (GetMode(out var currentMode)) + lock (_lock) { - return PInvoke.SetConsoleMode(Handle, currentMode.Value & ~mode); - } + if (GetMode(out var currentMode)) + { + return PInvoke.SetConsoleMode(Handle, currentMode.Value & ~mode); + } - return false; + return false; + } } } } diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalReader.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalReader.cs index 259a265..fbedebd 100644 --- a/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalReader.cs +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalReader.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Runtime.InteropServices; using System.Text; using Microsoft.Windows.Sdk; @@ -8,6 +9,8 @@ namespace Spectre.Terminals.Drivers internal sealed class WindowsTerminalReader : WindowsTerminalHandle, ITerminalReader { private readonly WindowsDriver _driver; + private readonly WindowsKeyReader _keyReader; + private readonly SynchronizedTextReader _reader; private Encoding _encoding; public Encoding Encoding @@ -17,45 +20,68 @@ namespace Spectre.Terminals.Drivers } public bool IsRawMode => _driver.IsRawMode; + public bool IsKeyAvailable => _keyReader.IsKeyAvailable(); public WindowsTerminalReader(WindowsDriver driver) : base(STD_HANDLE_TYPE.STD_INPUT_HANDLE) { _encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleCP()); _driver = driver; + _keyReader = new WindowsKeyReader(Handle); + _reader = CreateReader(Handle, _encoding, IsRedirected); } - public unsafe int Read(Span buffer) + public int Read() { - uint result; - uint* ptrResult = &result; + return _reader.Read(); + } - fixed (byte* p = buffer) + public string? ReadLine() + { + return _reader.ReadLine(); + } + + public ConsoleKeyInfo ReadKey() + { + return _keyReader.ReadKey(); + } + + private static SynchronizedTextReader CreateReader(SafeHandle handle, Encoding encoding, bool isRedirected) + { + static Stream Create(SafeHandle handle, bool useFileApis) { - if (PInvoke.ReadFile(Handle, p, (uint)buffer.Length, ptrResult, null)) + if (handle.IsInvalid || handle.IsClosed) { - return (int)result; + return Stream.Null; + } + else + { + return new WindowsConsoleStream(handle, useFileApis); } } - var error = Marshal.GetLastWin32Error(); - switch (error) + var useFileApis = !encoding.IsUnicode() || isRedirected; + + var stream = Create(handle, useFileApis); + if (stream == null || stream == Stream.Null) { - case WindowsConstants.ERROR_HANDLE_EOF: - case WindowsConstants.ERROR_BROKEN_PIPE: - case WindowsConstants.ERROR_NO_DATA: - break; - default: - throw new InvalidOperationException("Could not read from STDIN"); + return new SynchronizedTextReader(StreamReader.Null); } - return (int)result; + return new SynchronizedTextReader( + new StreamReader( + stream: stream, + encoding: new EncodingWithoutPreamble(encoding), + detectEncodingFromByteOrderMarks: false, + bufferSize: 4096, + leaveOpen: true)); } private void SetEncoding(Encoding encoding) { if (PInvoke.SetConsoleCP((uint)encoding.CodePage)) { + // TODO 2021-07-31: Recreate text reader _encoding = encoding; } } diff --git a/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalWriter.cs b/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalWriter.cs index 722e7f2..5aab0b9 100644 --- a/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalWriter.cs +++ b/src/Spectre.Terminals/Drivers/Windows/WindowsTerminalWriter.cs @@ -46,8 +46,7 @@ namespace Spectre.Terminals.Drivers } } - var error = Marshal.GetLastWin32Error(); - switch (error) + switch (Marshal.GetLastWin32Error()) { case WindowsConstants.ERROR_HANDLE_EOF: case WindowsConstants.ERROR_BROKEN_PIPE: diff --git a/src/Spectre.Terminals/Extensions/EncodingExtensions.cs b/src/Spectre.Terminals/Extensions/EncodingExtensions.cs new file mode 100644 index 0000000..071148c --- /dev/null +++ b/src/Spectre.Terminals/Extensions/EncodingExtensions.cs @@ -0,0 +1,14 @@ +using System.Text; + +namespace Spectre.Terminals +{ + internal static class EncodingExtensions + { + private const int UnicodeCodePage = 1200; + + public static bool IsUnicode(this Encoding encoding) + { + return encoding.CodePage == UnicodeCodePage; + } + } +} diff --git a/src/Spectre.Terminals/Extensions/ITerminalExtensions.cs b/src/Spectre.Terminals/Extensions/ITerminalExtensions.cs index 81f36e1..01ec788 100644 --- a/src/Spectre.Terminals/Extensions/ITerminalExtensions.cs +++ b/src/Spectre.Terminals/Extensions/ITerminalExtensions.cs @@ -7,26 +7,6 @@ namespace Spectre.Terminals /// public static partial class ITerminalExtensions { - /// - /// Reads a single from the terminal's input handle. - /// - /// Puts the terminal temporary in raw mode while reading occurs. - /// The terminal. - /// The read , or null if there was nothing to read. - public static byte? ReadRaw(this ITerminal terminal) - { - try - { - terminal.EnableRawMode(); - Span span = stackalloc byte[1]; - return terminal.Input.Read(span) == 1 ? span[0] : null; - } - finally - { - terminal.DisableRawMode(); - } - } - /// /// Writes the specified buffer to the terminal's output handle. /// diff --git a/src/Spectre.Terminals/Extensions/ITerminalWriterExtensions.cs b/src/Spectre.Terminals/Extensions/ITerminalWriterExtensions.cs deleted file mode 100644 index 57e0bd5..0000000 --- a/src/Spectre.Terminals/Extensions/ITerminalWriterExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; - -#if NET5_0_OR_GREATER -using System.Buffers; -#endif - -namespace Spectre.Terminals -{ - /// - /// Contains extension methods for . - /// - public static class ITerminalWriterExtensions - { - /// - /// Writes the specified buffer. - /// - /// The writer. - /// The value to write. - public static void Write(this ITerminalWriter writer, ReadOnlySpan value) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - -#if NET5_0_OR_GREATER - var len = writer.Encoding.GetByteCount(value); - var array = ArrayPool.Shared.Rent(len); - - try - { - var span = array.AsSpan(0, len); - writer.Encoding.GetBytes(value, span); - writer.Write(span); - } - finally - { - ArrayPool.Shared.Return(array); - } -#else - var chars = value.ToArray(); - var bytes = writer.Encoding.GetBytes(chars); - writer.Write(new Span(bytes)); -#endif - } - - /// - /// Writes the specified text. - /// - /// The writer. - /// The value to write. - public static void Write(this ITerminalWriter writer, string? value) - { - Write(writer, value.AsSpan()); - } - - /// - /// Writes an empty line. - /// - /// The writer. - public static void WriteLine(this ITerminalWriter writer) - { - Write(writer, Environment.NewLine); - } - - /// - /// Writes the specified text followed by a line break. - /// - /// The writer. - /// The value to write. - public static void WriteLine(this ITerminalWriter writer, string? value) - { - Write(writer, value.AsSpan()); - Write(writer, Environment.NewLine); - } - } -} diff --git a/src/Spectre.Terminals/Extensions/TerminalOutputExtensions.cs b/src/Spectre.Terminals/Extensions/TerminalOutputExtensions.cs new file mode 100644 index 0000000..19190a0 --- /dev/null +++ b/src/Spectre.Terminals/Extensions/TerminalOutputExtensions.cs @@ -0,0 +1,60 @@ +using System; + +namespace Spectre.Terminals +{ + /// + /// Contains extension methods for . + /// + public static class TerminalOutputExtensions + { + /// + /// Writes the specified text. + /// + /// The writer. + /// The value to write. + public static void Write(this TerminalOutput writer, string? value) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (string.IsNullOrEmpty(value)) + { + return; + } + + writer.Write(value.AsSpan()); + } + + /// + /// Writes an empty line. + /// + /// The writer. + public static void WriteLine(this TerminalOutput writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + Write(writer, Environment.NewLine); + } + + /// + /// Writes the specified text followed by a line break. + /// + /// The writer. + /// The value to write. + public static void WriteLine(this TerminalOutput writer, string? value) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + Write(writer, value); + Write(writer, Environment.NewLine); + } + } +} diff --git a/src/Spectre.Terminals/ITerminal.cs b/src/Spectre.Terminals/ITerminal.cs index 04d34fc..295f880 100644 --- a/src/Spectre.Terminals/ITerminal.cs +++ b/src/Spectre.Terminals/ITerminal.cs @@ -28,19 +28,19 @@ namespace Spectre.Terminals TerminalSize? Size { get; } /// - /// Gets a for STDIN. + /// Gets a for STDIN. /// - ITerminalInput Input { get; } + TerminalInput Input { get; } /// - /// Gets a for STDOUT. + /// Gets a for STDOUT. /// - ITerminalOutput Output { get; } + TerminalOutput Output { get; } /// - /// Gets a for STDERR. + /// Gets a for STDERR. /// - ITerminalOutput Error { get; } + TerminalOutput Error { get; } /// /// Emits a signal. diff --git a/src/Spectre.Terminals/ITerminalInput.cs b/src/Spectre.Terminals/ITerminalInput.cs deleted file mode 100644 index e018718..0000000 --- a/src/Spectre.Terminals/ITerminalInput.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Spectre.Terminals -{ - /// - /// Represents terminal input. - /// - public interface ITerminalInput : ITerminalReader - { - /// - /// Redirects the input to a specific reader. - /// - /// The reader to redirect to. - void Redirect(ITerminalReader? reader); - } -} diff --git a/src/Spectre.Terminals/ITerminalOutput.cs b/src/Spectre.Terminals/ITerminalOutput.cs deleted file mode 100644 index fbd6c94..0000000 --- a/src/Spectre.Terminals/ITerminalOutput.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Spectre.Terminals -{ - /// - /// Represents terminal input. - /// - public interface ITerminalOutput : ITerminalWriter - { - /// - /// Redirects the input to a specific writer. - /// - /// The writer to redirect to. - void Redirect(ITerminalWriter? reader); - } -} diff --git a/src/Spectre.Terminals/ITerminalReader.cs b/src/Spectre.Terminals/ITerminalReader.cs index c4d8ae8..b95ede9 100644 --- a/src/Spectre.Terminals/ITerminalReader.cs +++ b/src/Spectre.Terminals/ITerminalReader.cs @@ -13,24 +13,44 @@ namespace Spectre.Terminals /// Encoding Encoding { get; set; } + /// + /// Gets a value indicating whether a key press is available in the input stream. + /// + bool IsKeyAvailable { get; } + /// /// Gets a value indicating whether or not the reader has been redirected. /// bool IsRedirected { get; } /// - /// Reads a sequence of bytes from the current reader. + /// Reads the next character from the standard input stream. /// - /// - /// A region of memory. When this method returns, the contents of this region are - /// replaced by the bytes read from the current source. - /// /// - /// The total number of bytes read into the buffer. - /// This can be less than the number of bytes allocated in the buffer if - /// that many bytes are not currently available, or zero (0) if the end - /// of the stream has been reached. + /// The next character from the input stream, or negative one (-1) + /// if there are currently no more characters to be read. /// - int Read(Span buffer); + int Read(); + + /// + /// Reads the next line of characters from the standard input stream. + /// + /// + /// The next line of characters from the input stream, or null if + /// no more lines are available. + /// + string? ReadLine(); + + /// + /// Obtains the next character or function key pressed by the user. + /// + /// + /// An object that describes the System.ConsoleKey constant and Unicode character, + /// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo + /// object also describes, in a bitwise combination of System.ConsoleModifiers values, + /// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously + /// with the console key. + /// + ConsoleKeyInfo ReadKey(); } } diff --git a/src/Spectre.Terminals/NativeMethods.txt b/src/Spectre.Terminals/NativeMethods.txt index ffed0b1..bbf069c 100644 --- a/src/Spectre.Terminals/NativeMethods.txt +++ b/src/Spectre.Terminals/NativeMethods.txt @@ -21,4 +21,7 @@ CloseHandle GetLargestConsoleWindowSize SetConsoleTextAttribute SetConsoleCtrlHandler -GenerateConsoleCtrlEvent \ No newline at end of file +GenerateConsoleCtrlEvent +ReadConsole +ReadConsoleInput +PeekConsoleInput \ No newline at end of file diff --git a/src/Spectre.Terminals/Terminal.cs b/src/Spectre.Terminals/Terminal.cs index bc38a94..db2a8e2 100644 --- a/src/Spectre.Terminals/Terminal.cs +++ b/src/Spectre.Terminals/Terminal.cs @@ -34,13 +34,13 @@ namespace Spectre.Terminals public TerminalSize? Size => _driver.Size; /// - public ITerminalInput Input { get; } + public TerminalInput Input { get; } /// - public ITerminalOutput Output { get; } + public TerminalOutput Output { get; } /// - public ITerminalOutput Error { get; } + public TerminalOutput Error { get; } static Terminal() { diff --git a/src/Spectre.Terminals/TerminalInput.cs b/src/Spectre.Terminals/TerminalInput.cs index 1d44321..2572790 100644 --- a/src/Spectre.Terminals/TerminalInput.cs +++ b/src/Spectre.Terminals/TerminalInput.cs @@ -3,26 +3,51 @@ using System.Text; namespace Spectre.Terminals { - internal sealed class TerminalInput : ITerminalInput + /// + /// Represents a mechanism to read input from the terminal. + /// + public sealed class TerminalInput { private readonly ITerminalReader _reader; private readonly object _lock; private ITerminalReader? _redirected; + /// + /// Gets or sets the encoding. + /// public Encoding Encoding { get => GetEncoding(); set => SetEncoding(value); } + /// + /// Gets a value indicating whether or not input has been redirected. + /// public bool IsRedirected => GetIsRedirected(); + /// + /// Gets a value indicating whether a key press is available in the input stream. + /// + public bool IsKeyAvailable => throw new NotSupportedException(); + + /// + /// Initializes a new instance of the class. + /// + /// The terminal reader. public TerminalInput(ITerminalReader reader) { _reader = reader ?? throw new ArgumentNullException(nameof(reader)); _lock = new object(); } + /// + /// Redirects input to the specified . + /// + /// + /// The reader to redirect to, + /// or null, if the current redirected reader should be removed. + /// public void Redirect(ITerminalReader? reader) { lock (_lock) @@ -31,29 +56,50 @@ namespace Spectre.Terminals } } - public int Read(Span buffer) + /// + /// Reads the next character from the standard input stream. + /// + /// + /// The next character from the input stream, or negative one (-1) + /// if there are currently no more characters to be read. + /// + public int Read() { - lock (_lock) - { - if (_redirected != null) - { - return _redirected.Read(buffer); - } + return _reader.Read(); + } - return _reader.Read(buffer); - } + /// + /// Reads the next line of characters from the standard input stream. + /// + /// + /// The next line of characters from the input stream, or null if + /// no more lines are available. + /// + public string? ReadLine() + { + return _reader.ReadLine(); + } + + /// + /// Obtains the next character or function key pressed by the user. + /// + /// + /// An object that describes the System.ConsoleKey constant and Unicode character, + /// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo + /// object also describes, in a bitwise combination of System.ConsoleModifiers values, + /// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously + /// with the console key. + /// + public ConsoleKeyInfo ReadKey() + { + return _reader.ReadKey(); } private bool GetIsRedirected() { lock (_lock) { - if (_redirected != null) - { - return _redirected.IsRedirected; - } - - return _reader.IsRedirected; + return GetReader().IsRedirected; } } @@ -61,12 +107,7 @@ namespace Spectre.Terminals { lock (_lock) { - if (_redirected != null) - { - return _redirected.Encoding; - } - - return _reader.Encoding; + return GetReader().Encoding; } } @@ -79,15 +120,13 @@ namespace Spectre.Terminals lock (_lock) { - if (_redirected != null) - { - _redirected.Encoding = encoding; - } - else - { - _reader.Encoding = encoding; - } + GetReader().Encoding = encoding; } } + + private ITerminalReader GetReader() + { + return _redirected ?? _reader; + } } } diff --git a/src/Spectre.Terminals/TerminalOutput.cs b/src/Spectre.Terminals/TerminalOutput.cs index fb6cc22..b032c82 100644 --- a/src/Spectre.Terminals/TerminalOutput.cs +++ b/src/Spectre.Terminals/TerminalOutput.cs @@ -1,28 +1,52 @@ using System; using System.Text; +#if NET5_0_OR_GREATER +using System.Buffers; +#endif + namespace Spectre.Terminals { - internal sealed class TerminalOutput : ITerminalOutput + /// + /// Represents a mechanism to write to a terminal output handle. + /// + public sealed class TerminalOutput { private readonly ITerminalWriter _writer; private readonly object _lock; private ITerminalWriter? _redirected; + /// + /// Gets or sets the encoding. + /// public Encoding Encoding { get => GetEncoding(); set => SetEncoding(value); } + /// + /// Gets a value indicating whether or not output has been redirected. + /// public bool IsRedirected => GetIsRedirected(); - public TerminalOutput(ITerminalWriter reader) + /// + /// Initializes a new instance of the class. + /// + /// The terminal writer. + public TerminalOutput(ITerminalWriter writer) { - _writer = reader ?? throw new ArgumentNullException(nameof(reader)); + _writer = writer ?? throw new ArgumentNullException(nameof(writer)); _lock = new object(); } + /// + /// Redirects input to the specified . + /// + /// + /// The writer to redirect to, + /// or null, if the current redirected writer should be removed. + /// public void Redirect(ITerminalWriter? writer) { lock (_lock) @@ -31,18 +55,33 @@ namespace Spectre.Terminals } } - public void Write(ReadOnlySpan buffer) + /// + /// Writes the specified buffer. + /// + /// The value to write. + public void Write(ReadOnlySpan value) { lock (_lock) { - if (_redirected != null) +#if NET5_0_OR_GREATER + var len = Encoding.GetByteCount(value); + var array = ArrayPool.Shared.Rent(len); + + try { - _redirected.Write(buffer); + var span = array.AsSpan(0, len); + Encoding.GetBytes(value, span); + GetWriter().Write(span); } - else + finally { - _writer.Write(buffer); + ArrayPool.Shared.Return(array); } +#else + var chars = value.ToArray(); + var bytes = Encoding.GetBytes(chars); + GetWriter().Write(new Span(bytes)); +#endif } } @@ -50,12 +89,7 @@ namespace Spectre.Terminals { lock (_lock) { - if (_redirected != null) - { - return _redirected.Encoding; - } - - return _writer.Encoding; + return GetWriter().Encoding; } } @@ -68,14 +102,7 @@ namespace Spectre.Terminals lock (_lock) { - if (_redirected != null) - { - _redirected.Encoding = encoding; - } - else - { - _writer.Encoding = encoding; - } + GetWriter().Encoding = encoding; } } @@ -83,13 +110,13 @@ namespace Spectre.Terminals { lock (_lock) { - if (_redirected != null) - { - return _redirected.IsRedirected; - } - - return _writer.IsRedirected; + return GetWriter().IsRedirected; } } + + private ITerminalWriter GetWriter() + { + return _redirected ?? _writer; + } } } diff --git a/src/Spectre.Terminals/Utilities/EncodingHelper.Unix.cs b/src/Spectre.Terminals/Utilities/EncodingHelper.Unix.cs index 3d44cf5..a66086b 100644 --- a/src/Spectre.Terminals/Utilities/EncodingHelper.Unix.cs +++ b/src/Spectre.Terminals/Utilities/EncodingHelper.Unix.cs @@ -1,10 +1,11 @@ +// Parts of this code used from: https://github.com/dotnet/runtime +// Licensed to the .NET Foundation under one or more agreements. + using System; using System.Text; namespace Spectre.Terminals { - // Adapted from: https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/EncodingHelper.Unix.cs - // Licensed to the .NET Foundation under one or more agreements. internal static partial class EncodingHelper { /// Environment variables that should be checked, in order, for locale. diff --git a/src/Spectre.Terminals/Utilities/EncodingWithoutPreamble.cs b/src/Spectre.Terminals/Utilities/EncodingWithoutPreamble.cs index a2aec40..264bbe0 100644 --- a/src/Spectre.Terminals/Utilities/EncodingWithoutPreamble.cs +++ b/src/Spectre.Terminals/Utilities/EncodingWithoutPreamble.cs @@ -11,8 +11,15 @@ namespace Spectre.Terminals public override int CodePage => _encoding.CodePage; + public override bool IsSingleByte => _encoding.IsSingleByte; + public override string EncodingName => _encoding.EncodingName; + public override string WebName + { + get { return _encoding.WebName; } + } + public EncodingWithoutPreamble(Encoding encoding) { _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); @@ -38,6 +45,11 @@ namespace Spectre.Terminals return _encoding.GetByteCount(s); } + public override int GetByteCount(char[] chars, int index, int count) + { + return _encoding.GetByteCount(chars, index, count); + } + public override unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount) { return _encoding.GetBytes(chars, charCount, bytes, byteCount); @@ -113,11 +125,6 @@ namespace Spectre.Terminals return _encoding.GetString(bytes, index, count); } - public override int GetByteCount(char[] chars, int index, int count) - { - return _encoding.GetByteCount(chars, index, count); - } - public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) { return _encoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex); @@ -144,6 +151,8 @@ namespace Spectre.Terminals } #if NET5_0_OR_GREATER + public override ReadOnlySpan Preamble => ReadOnlySpan.Empty; + public override int GetByteCount(ReadOnlySpan chars) { return _encoding.GetByteCount(chars); diff --git a/src/Spectre.Terminals/Utilities/SynchronizedTextReader.cs b/src/Spectre.Terminals/Utilities/SynchronizedTextReader.cs new file mode 100644 index 0000000..95db627 --- /dev/null +++ b/src/Spectre.Terminals/Utilities/SynchronizedTextReader.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Spectre.Terminals +{ + internal sealed class SynchronizedTextReader : TextReader + { + private readonly TextReader _inner; + private readonly object _lock; + + public SynchronizedTextReader(TextReader reader) + { + _inner = reader ?? throw new ArgumentNullException(nameof(reader)); + _lock = new object(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inner.Dispose(); + } + } + + public override int Peek() + { + lock (_lock) + { + return _inner.Peek(); + } + } + + public override int Read() + { + lock (_lock) + { + return _inner.Peek(); + } + } + + public override int Read(char[] buffer, int index, int count) + { + lock (_lock) + { + // TODO 2021-07-31: Validate input + return _inner.Read(buffer, index, count); + } + } + + public override int ReadBlock(char[] buffer, int index, int count) + { + lock (_lock) + { + // TODO 2021-07-31: Validate input + return _inner.ReadBlock(buffer, index, count); + } + } + + public override string? ReadLine() + { + lock (_lock) + { + return _inner.ReadLine(); + } + } + + public override string ReadToEnd() + { + lock (_lock) + { + return _inner.ReadToEnd(); + } + } + + public override Task ReadLineAsync() + { + return Task.FromResult(ReadLine()); + } + + public override Task ReadToEndAsync() + { + return Task.FromResult(ReadToEnd()); + } + + public override Task ReadBlockAsync(char[] buffer, int index, int count) + { + // TODO 2021-07-31: Validate input + return Task.FromResult(ReadBlock(buffer, index, count)); + } + + public override Task ReadAsync(char[] buffer, int index, int count) + { + // TODO 2021-07-31: Validate input + return Task.FromResult(Read(buffer, index, count)); + } + } +}