* Only Windows works for now.
This commit is contained in:
Patrik Svensson 2021-07-31 00:57:40 +02:00 коммит произвёл Patrik Svensson
Родитель 8a79686958
Коммит 5ba38e544d
30 изменённых файлов: 923 добавлений и 347 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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("");

Просмотреть файл

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<ExampleTitle>Input</ExampleTitle>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Terminals\Spectre.Terminals.csproj" />
</ItemGroup>
</Project>

45
examples/Input/Program.cs Normal file
Просмотреть файл

@ -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}]");
}
}
}
}

Просмотреть файл

@ -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}

Просмотреть файл

@ -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

Просмотреть файл

@ -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<byte> 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<byte> 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;
}
}
}

Просмотреть файл

@ -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<byte>(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<byte> 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");
}
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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()

Просмотреть файл

@ -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<INPUT_RECORD>(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<INPUT_RECORD>(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;
}
}
}

Просмотреть файл

@ -35,7 +35,7 @@ namespace Spectre.Terminals.Drivers
UninstallHandler();
}
public bool Emit(TerminalSignal signal)
public static bool Emit(TerminalSignal signal)
{
uint? ctrlEvent = signal switch
{

Просмотреть файл

@ -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;
}
}
}
}

Просмотреть файл

@ -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<byte> 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;
}
}

Просмотреть файл

@ -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:

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -7,26 +7,6 @@ namespace Spectre.Terminals
/// </summary>
public static partial class ITerminalExtensions
{
/// <summary>
/// Reads a single <see cref="byte"/> from the terminal's input handle.
/// </summary>
/// <remarks>Puts the terminal temporary in raw mode while reading occurs.</remarks>
/// <param name="terminal">The terminal.</param>
/// <returns>The read <see cref="byte"/>, or <c>null</c> if there was nothing to read.</returns>
public static byte? ReadRaw(this ITerminal terminal)
{
try
{
terminal.EnableRawMode();
Span<byte> span = stackalloc byte[1];
return terminal.Input.Read(span) == 1 ? span[0] : null;
}
finally
{
terminal.DisableRawMode();
}
}
/// <summary>
/// Writes the specified buffer to the terminal's output handle.
/// </summary>

Просмотреть файл

@ -1,77 +0,0 @@
using System;
#if NET5_0_OR_GREATER
using System.Buffers;
#endif
namespace Spectre.Terminals
{
/// <summary>
/// Contains extension methods for <see cref="ITerminalWriter"/>.
/// </summary>
public static class ITerminalWriterExtensions
{
/// <summary>
/// Writes the specified buffer.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value to write.</param>
public static void Write(this ITerminalWriter writer, ReadOnlySpan<char> value)
{
if (writer is null)
{
throw new ArgumentNullException(nameof(writer));
}
#if NET5_0_OR_GREATER
var len = writer.Encoding.GetByteCount(value);
var array = ArrayPool<byte>.Shared.Rent(len);
try
{
var span = array.AsSpan(0, len);
writer.Encoding.GetBytes(value, span);
writer.Write(span);
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
#else
var chars = value.ToArray();
var bytes = writer.Encoding.GetBytes(chars);
writer.Write(new Span<byte>(bytes));
#endif
}
/// <summary>
/// Writes the specified text.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value to write.</param>
public static void Write(this ITerminalWriter writer, string? value)
{
Write(writer, value.AsSpan());
}
/// <summary>
/// Writes an empty line.
/// </summary>
/// <param name="writer">The writer.</param>
public static void WriteLine(this ITerminalWriter writer)
{
Write(writer, Environment.NewLine);
}
/// <summary>
/// Writes the specified text followed by a line break.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value to write.</param>
public static void WriteLine(this ITerminalWriter writer, string? value)
{
Write(writer, value.AsSpan());
Write(writer, Environment.NewLine);
}
}
}

Просмотреть файл

@ -0,0 +1,60 @@
using System;
namespace Spectre.Terminals
{
/// <summary>
/// Contains extension methods for <see cref="TerminalOutput"/>.
/// </summary>
public static class TerminalOutputExtensions
{
/// <summary>
/// Writes the specified text.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value to write.</param>
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());
}
/// <summary>
/// Writes an empty line.
/// </summary>
/// <param name="writer">The writer.</param>
public static void WriteLine(this TerminalOutput writer)
{
if (writer is null)
{
throw new ArgumentNullException(nameof(writer));
}
Write(writer, Environment.NewLine);
}
/// <summary>
/// Writes the specified text followed by a line break.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value to write.</param>
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);
}
}
}

Просмотреть файл

@ -28,19 +28,19 @@ namespace Spectre.Terminals
TerminalSize? Size { get; }
/// <summary>
/// Gets a <see cref="ITerminalInput"/> for <c>STDIN</c>.
/// Gets a <see cref="TerminalOutput"/> for <c>STDIN</c>.
/// </summary>
ITerminalInput Input { get; }
TerminalInput Input { get; }
/// <summary>
/// Gets a <see cref="ITerminalOutput"/> for <c>STDOUT</c>.
/// Gets a <see cref="TerminalOutput"/> for <c>STDOUT</c>.
/// </summary>
ITerminalOutput Output { get; }
TerminalOutput Output { get; }
/// <summary>
/// Gets a <see cref="ITerminalOutput"/> for <c>STDERR</c>.
/// Gets a <see cref="TerminalOutput"/> for <c>STDERR</c>.
/// </summary>
ITerminalOutput Error { get; }
TerminalOutput Error { get; }
/// <summary>
/// Emits a signal.

Просмотреть файл

@ -1,14 +0,0 @@
namespace Spectre.Terminals
{
/// <summary>
/// Represents terminal input.
/// </summary>
public interface ITerminalInput : ITerminalReader
{
/// <summary>
/// Redirects the input to a specific reader.
/// </summary>
/// <param name="reader">The reader to redirect to.</param>
void Redirect(ITerminalReader? reader);
}
}

Просмотреть файл

@ -1,14 +0,0 @@
namespace Spectre.Terminals
{
/// <summary>
/// Represents terminal input.
/// </summary>
public interface ITerminalOutput : ITerminalWriter
{
/// <summary>
/// Redirects the input to a specific writer.
/// </summary>
/// <param name="reader">The writer to redirect to.</param>
void Redirect(ITerminalWriter? reader);
}
}

Просмотреть файл

@ -13,24 +13,44 @@ namespace Spectre.Terminals
/// </summary>
Encoding Encoding { get; set; }
/// <summary>
/// Gets a value indicating whether a key press is available in the input stream.
/// </summary>
bool IsKeyAvailable { get; }
/// <summary>
/// Gets a value indicating whether or not the reader has been redirected.
/// </summary>
bool IsRedirected { get; }
/// <summary>
/// Reads a sequence of bytes from the current reader.
/// Reads the next character from the standard input stream.
/// </summary>
/// <param name="buffer">
/// A region of memory. When this method returns, the contents of this region are
/// replaced by the bytes read from the current source.
/// </param>
/// <returns>
/// 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.
/// </returns>
int Read(Span<byte> buffer);
int Read();
/// <summary>
/// Reads the next line of characters from the standard input stream.
/// </summary>
/// <returns>
/// The next line of characters from the input stream, or null if
/// no more lines are available.
/// </returns>
string? ReadLine();
/// <summary>
/// Obtains the next character or function key pressed by the user.
/// </summary>
/// <returns>
/// 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.
/// </returns>
ConsoleKeyInfo ReadKey();
}
}

Просмотреть файл

@ -21,4 +21,7 @@ CloseHandle
GetLargestConsoleWindowSize
SetConsoleTextAttribute
SetConsoleCtrlHandler
GenerateConsoleCtrlEvent
GenerateConsoleCtrlEvent
ReadConsole
ReadConsoleInput
PeekConsoleInput

Просмотреть файл

@ -34,13 +34,13 @@ namespace Spectre.Terminals
public TerminalSize? Size => _driver.Size;
/// <inheritdoc/>
public ITerminalInput Input { get; }
public TerminalInput Input { get; }
/// <inheritdoc/>
public ITerminalOutput Output { get; }
public TerminalOutput Output { get; }
/// <inheritdoc/>
public ITerminalOutput Error { get; }
public TerminalOutput Error { get; }
static Terminal()
{

Просмотреть файл

@ -3,26 +3,51 @@ using System.Text;
namespace Spectre.Terminals
{
internal sealed class TerminalInput : ITerminalInput
/// <summary>
/// Represents a mechanism to read input from the terminal.
/// </summary>
public sealed class TerminalInput
{
private readonly ITerminalReader _reader;
private readonly object _lock;
private ITerminalReader? _redirected;
/// <summary>
/// Gets or sets the encoding.
/// </summary>
public Encoding Encoding
{
get => GetEncoding();
set => SetEncoding(value);
}
/// <summary>
/// Gets a value indicating whether or not input has been redirected.
/// </summary>
public bool IsRedirected => GetIsRedirected();
/// <summary>
/// Gets a value indicating whether a key press is available in the input stream.
/// </summary>
public bool IsKeyAvailable => throw new NotSupportedException();
/// <summary>
/// Initializes a new instance of the <see cref="TerminalInput"/> class.
/// </summary>
/// <param name="reader">The terminal reader.</param>
public TerminalInput(ITerminalReader reader)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_lock = new object();
}
/// <summary>
/// Redirects input to the specified <see cref="ITerminalReader"/>.
/// </summary>
/// <param name="reader">
/// The reader to redirect to,
/// or <c>null</c>, if the current redirected reader should be removed.
/// </param>
public void Redirect(ITerminalReader? reader)
{
lock (_lock)
@ -31,29 +56,50 @@ namespace Spectre.Terminals
}
}
public int Read(Span<byte> buffer)
/// <summary>
/// Reads the next character from the standard input stream.
/// </summary>
/// <returns>
/// The next character from the input stream, or negative one (-1)
/// if there are currently no more characters to be read.
/// </returns>
public int Read()
{
lock (_lock)
{
if (_redirected != null)
{
return _redirected.Read(buffer);
}
return _reader.Read();
}
return _reader.Read(buffer);
}
/// <summary>
/// Reads the next line of characters from the standard input stream.
/// </summary>
/// <returns>
/// The next line of characters from the input stream, or null if
/// no more lines are available.
/// </returns>
public string? ReadLine()
{
return _reader.ReadLine();
}
/// <summary>
/// Obtains the next character or function key pressed by the user.
/// </summary>
/// <returns>
/// 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.
/// </returns>
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;
}
}
}

Просмотреть файл

@ -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
/// <summary>
/// Represents a mechanism to write to a terminal output handle.
/// </summary>
public sealed class TerminalOutput
{
private readonly ITerminalWriter _writer;
private readonly object _lock;
private ITerminalWriter? _redirected;
/// <summary>
/// Gets or sets the encoding.
/// </summary>
public Encoding Encoding
{
get => GetEncoding();
set => SetEncoding(value);
}
/// <summary>
/// Gets a value indicating whether or not output has been redirected.
/// </summary>
public bool IsRedirected => GetIsRedirected();
public TerminalOutput(ITerminalWriter reader)
/// <summary>
/// Initializes a new instance of the <see cref="TerminalOutput"/> class.
/// </summary>
/// <param name="writer">The terminal writer.</param>
public TerminalOutput(ITerminalWriter writer)
{
_writer = reader ?? throw new ArgumentNullException(nameof(reader));
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
_lock = new object();
}
/// <summary>
/// Redirects input to the specified <see cref="ITerminalWriter"/>.
/// </summary>
/// <param name="writer">
/// The writer to redirect to,
/// or <c>null</c>, if the current redirected writer should be removed.
/// </param>
public void Redirect(ITerminalWriter? writer)
{
lock (_lock)
@ -31,18 +55,33 @@ namespace Spectre.Terminals
}
}
public void Write(ReadOnlySpan<byte> buffer)
/// <summary>
/// Writes the specified buffer.
/// </summary>
/// <param name="value">The value to write.</param>
public void Write(ReadOnlySpan<char> value)
{
lock (_lock)
{
if (_redirected != null)
#if NET5_0_OR_GREATER
var len = Encoding.GetByteCount(value);
var array = ArrayPool<byte>.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<byte>.Shared.Return(array);
}
#else
var chars = value.ToArray();
var bytes = Encoding.GetBytes(chars);
GetWriter().Write(new Span<byte>(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;
}
}
}

Просмотреть файл

@ -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
{
/// <summary>Environment variables that should be checked, in order, for locale.</summary>

Просмотреть файл

@ -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<byte> Preamble => ReadOnlySpan<byte>.Empty;
public override int GetByteCount(ReadOnlySpan<char> chars)
{
return _encoding.GetByteCount(chars);

Просмотреть файл

@ -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<string?> ReadLineAsync()
{
return Task.FromResult(ReadLine());
}
public override Task<string> ReadToEndAsync()
{
return Task.FromResult(ReadToEnd());
}
public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
{
// TODO 2021-07-31: Validate input
return Task.FromResult(ReadBlock(buffer, index, count));
}
public override Task<int> ReadAsync(char[] buffer, int index, int count)
{
// TODO 2021-07-31: Validate input
return Task.FromResult(Read(buffer, index, count));
}
}
}