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));
+ }
+ }
+}