Rework input functionality
* Only Windows works for now.
This commit is contained in:
Родитель
8a79686958
Коммит
5ba38e544d
24
README.md
24
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
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -15,6 +15,8 @@ namespace Spectre.Terminals.Drivers
|
|||
set { /* Do nothing for now */ }
|
||||
}
|
||||
|
||||
public bool IsKeyAvailable => throw new NotSupportedException("Not yet supported");
|
||||
|
||||
public bool IsRedirected => !Syscall.isatty(UnixConstants.STDIN);
|
||||
|
||||
public UnixTerminalReader()
|
||||
|
@ -22,7 +24,22 @@ namespace Spectre.Terminals.Drivers
|
|||
_encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
}
|
||||
|
||||
public unsafe int Read(Span<byte> buffer)
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,3 +22,6 @@ GetLargestConsoleWindowSize
|
|||
SetConsoleTextAttribute
|
||||
SetConsoleCtrlHandler
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче