-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update showkeys to have multiple input methods
- Loading branch information
1 parent
98bfba1
commit 45e0d64
Showing
4 changed files
with
337 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
using System.Buffers; | ||
using System.Diagnostics; | ||
|
||
namespace OkTools.Core; | ||
|
||
// TODO: consider deleting this class in favor of System.IO.Pipelines.Pipe (move useful methods from here to extensions on Pipe) | ||
[PublicAPI] | ||
public class StreamableBuffer<T> : IBufferWriter<T> where T : struct | ||
{ | ||
T[] _buffer; | ||
int _read, _write; | ||
|
||
public StreamableBuffer(int capacity = 128) | ||
{ _buffer = new T[capacity]; } | ||
|
||
// terminology: | ||
// | ||
// * writable: how much space is available to write to without a realloc (write functions that take a size or items will realloc on demand) | ||
// * unread: how much data has been written that is available to read | ||
|
||
public int TotalCapacity => _buffer.Length; | ||
|
||
public bool HasUnread => _write != _read; | ||
public int UnreadCount => _write - _read; | ||
public Memory<T> UnreadMemory => new(_buffer, _read, UnreadCount); | ||
public Span<T> UnreadSpan => new(_buffer, _read, UnreadCount); | ||
|
||
public Span<T> GetUnreadSpan(int size) | ||
{ ThrowIfUnreadLessThan(size); return new(_buffer, _read, size); } | ||
public Memory<T> GetUnreadMemory(int size) | ||
{ ThrowIfUnreadLessThan(size); return new(_buffer, _read, size); } | ||
|
||
public bool HasWritable => _write != _buffer.Length; | ||
public int WritableCount => _buffer.Length - _write; | ||
public Memory<T> WritableMemory => new(_buffer, _write, WritableCount); | ||
public Span<T> WritableSpan => new(_buffer, _write, WritableCount); | ||
|
||
public Span<T> GetWritableSpan(int size) | ||
{ | ||
if (WritableCount < size) | ||
ExpandByAtLeast(size); | ||
return new(_buffer, _write, size); | ||
} | ||
|
||
public Memory<T> GetWritableMemory(int size) | ||
{ | ||
if (WritableCount < size) | ||
ExpandByAtLeast(size); | ||
return new(_buffer, _write, size); | ||
} | ||
|
||
public void Reset() | ||
{ | ||
_read = _write = 0; | ||
} | ||
|
||
public void Reset(int capacity) | ||
{ | ||
Reset(); | ||
if (TotalCapacity != capacity) | ||
_buffer = new T[capacity]; | ||
} | ||
|
||
public void Write(ReadOnlyMemory<T> memory) | ||
{ | ||
memory.Span.CopyTo(GetWritableSpan(memory.Length)); | ||
_write += memory.Length; | ||
} | ||
|
||
public void Write(T value) | ||
{ | ||
if (WritableCount < 1) | ||
ExpandByAtLeast(1); | ||
_buffer[_write++] = value; | ||
} | ||
|
||
public void AdvanceWriter(int count) | ||
{ | ||
if (WritableCount < count) | ||
throw new InvalidOperationException("Can't advance writer past the end"); | ||
_write += count; | ||
} | ||
|
||
public T Read() | ||
{ ThrowIfUnreadLessThan(); return _buffer[_read++]; } | ||
public T Peek() | ||
{ ThrowIfUnreadLessThan(); return _buffer[_read]; } | ||
public T TryPeek(T defValue) => | ||
HasUnread ? _buffer[_read] : defValue; | ||
public T? TryPeek() => | ||
HasUnread ? _buffer[_read] : null; | ||
|
||
public void SeekReader(int offset) | ||
{ | ||
var newOffset = _read + offset; | ||
if (newOffset < 0) | ||
throw new ArgumentOutOfRangeException(nameof(offset), "Can't rewind past the beginning"); | ||
if (newOffset > _write) | ||
throw new ArgumentOutOfRangeException(nameof(offset), "Can't skip past the end"); | ||
_read += offset; | ||
} | ||
|
||
public void AdvanceReader() | ||
{ | ||
if (_read == _write) | ||
throw new InvalidOperationException("Can't advance reader past the end"); | ||
++_read; | ||
} | ||
|
||
void ThrowIfUnreadLessThan(int count = 1) | ||
{ | ||
if (UnreadCount < count) | ||
throw new OverflowException($"Insufficient data to read, need {count} but have {UnreadCount}"); | ||
} | ||
|
||
void ExpandByAtLeast(int count) | ||
{ | ||
var newCapacity = _buffer.Length; | ||
while (_write + count > newCapacity) | ||
newCapacity += Math.Max(newCapacity / 2, 1); | ||
|
||
Debug.Assert(newCapacity != _buffer.Length); | ||
Array.Resize(ref _buffer, newCapacity); | ||
} | ||
|
||
// TODO: change these from explicit to public, and align the rest of the API with it. | ||
// TODO: also consider implementing Stream (possibly via an AsStream() given the API is old and may not fit well) | ||
|
||
void IBufferWriter<T>.Advance(int count) => SeekReader(count); | ||
Memory<T> IBufferWriter<T>.GetMemory(int sizeHint) => GetWritableMemory(sizeHint); | ||
Span<T> IBufferWriter<T>.GetSpan(int sizeHint) => GetWritableSpan(sizeHint); | ||
} | ||
|
||
public static class StreamableBufferExtensions | ||
{ | ||
// TODO: consider moving this to *SpanExtensions. except lose the nicer exception from GetUnreadSpan | ||
|
||
public static void Write<T>(this StreamableBuffer<T> @this, ReadOnlySequence<T> sequence) where T : struct | ||
{ | ||
foreach (var segment in sequence) | ||
@this.Write(segment); | ||
} | ||
|
||
public static (T a, T b) Peek2<T>(this StreamableBuffer<T> @this) where T : struct | ||
{ | ||
var span = @this.GetUnreadSpan(2); | ||
return (span[0], span[1]); | ||
} | ||
|
||
public static (T a, T b, T c) Peek3<T>(this StreamableBuffer<T> @this) where T : struct | ||
{ | ||
var span = @this.GetUnreadSpan(3); | ||
return (span[0], span[1], span[2]); | ||
} | ||
|
||
// TODO: yeah these could go into span extensions | ||
|
||
public static (T a, T b) TryPeek2<T>(this StreamableBuffer<T> @this, T defValue) where T : struct | ||
{ | ||
var span = @this.UnreadSpan; | ||
return span.Length switch | ||
{ | ||
0 => (defValue, defValue), | ||
1 => (span[0], defValue), | ||
_ => (span[0], span[1]) | ||
}; | ||
} | ||
|
||
public static (T a, T b, T c) TryPeek3<T>(this StreamableBuffer<T> @this, T defValue) where T : struct | ||
{ | ||
var span = @this.UnreadSpan; | ||
return span.Length switch | ||
{ | ||
0 => (defValue, defValue, defValue), | ||
1 => (span[0], defValue, defValue), | ||
2 => (span[0], span[1], defValue), | ||
_ => (span[0], span[1], span[2]) | ||
}; | ||
} | ||
|
||
public static (T a, T b)? TryPeek2<T>(this StreamableBuffer<T> @this) where T : struct | ||
{ | ||
var span = @this.UnreadSpan; | ||
if (span.Length >= 2) | ||
return (span[0], span[1]); | ||
return null; | ||
} | ||
|
||
public static (T a, T b, T c)? TryPeek3<T>(this StreamableBuffer<T> @this) where T : struct | ||
{ | ||
var span = @this.UnreadSpan; | ||
if (span.Length >= 3) | ||
return (span[0], span[1], span[2]); | ||
return null; | ||
} | ||
|
||
// helpful for parsing | ||
public static bool AdvanceIfNextEquals<T>(StreamableBuffer<T> @this, T value) | ||
where T : struct, IEquatable<T> | ||
{ | ||
var unread = @this.UnreadSpan; | ||
if (unread.Length == 0 || !value.Equals(unread[0])) | ||
return false; | ||
|
||
@this.AdvanceReader(); | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,139 @@ | ||
using System.Collections.Concurrent; | ||
using System.Runtime.CompilerServices; | ||
using DocoptNet; | ||
using OkTools.Core.Terminal; | ||
using Vezel.Cathode.Text.Control; | ||
using static Vezel.Cathode.Terminal; | ||
|
||
OutLine("Ctrl-C to quit"); | ||
OutLine(); | ||
const string help = """ | ||
showkeys, the terminal utils thingy | ||
|
||
EnableRawMode(); | ||
Usage: | ||
showkeys show [--dotnet | --ansi] | ||
showkeys drive | ||
showkeys --help | ||
|
||
try | ||
Commands: | ||
show Print out the ansi keycodes of the keys you press. | ||
drive Exercise the terminal to see what different vt100 sequences do. | ||
|
||
Options: | ||
--dotnet Use the built-in dotnet Console key reader in cooked mode, rather than Cathode (ansi sequences, raw mode). | ||
--ansi Use OkTools.Core.Terminal.AnsiInputReaderReader to receive key events. | ||
"""; | ||
|
||
var cb = new ControlBuilder(); | ||
|
||
void Flush() | ||
{ | ||
Out(cb.Span); | ||
cb.Clear(); | ||
} | ||
|
||
return Docopt.CreateParser(help).Parse(args) switch | ||
{ | ||
var queue = new BlockingCollection<byte>(); | ||
IArgumentsResult<IDictionary<string, ArgValue>> result | ||
=> (int)await Run(result.Arguments), | ||
IHelpResult | ||
=> ShowHelp(), | ||
IInputErrorResult error | ||
=> ShowError(error.Usage), | ||
var unknown | ||
=> throw new SwitchExpressionException(unknown) | ||
}; | ||
|
||
int ShowHelp() { OutLine(help); return (int)CliExitCode.Success; } | ||
int ShowError(string usage) { ErrorLine(usage); return (int)CliExitCode.ErrorUsage; } | ||
|
||
var task = Task.Run(() => | ||
async Task<CliExitCode> Run(IDictionary<string, ArgValue> options) | ||
{ | ||
OutLine("Ctrl-C to quit"); | ||
OutLine(); | ||
|
||
try | ||
{ | ||
var array = new byte[1]; | ||
for (;;) | ||
if (options["show"].IsTrue) | ||
{ | ||
Read(array); | ||
queue.Add(array[0]); | ||
if (options["--dotnet"].IsTrue) | ||
return ShowKeysDotnet(); | ||
|
||
EnableRawMode(); | ||
|
||
return options["--ansi"].IsTrue | ||
? await ShowKeysParserRaw() | ||
: ShowKeysCathodeRaw(); | ||
} | ||
|
||
// ReSharper disable once FunctionNeverReturns | ||
}); | ||
if (options["drive"].IsTrue) | ||
return await Drive(); | ||
|
||
throw new InvalidOperationException("Invalid command"); | ||
} | ||
catch (CliErrorException ex) | ||
{ | ||
return ex.Code; | ||
} | ||
finally | ||
{ | ||
cb.Clear(); | ||
cb.SoftReset(); | ||
Flush(); | ||
|
||
DisableRawMode(); | ||
} | ||
} | ||
|
||
#pragma warning disable RS0030 // bypass Console.* analyzers | ||
CliExitCode ShowKeysDotnet() | ||
{ | ||
Console.CancelKeyPress += (_, _) => Environment.Exit((int)UnixSignal.KeyboardInterrupt.AsCliExitCode()); | ||
|
||
for (;;) | ||
{ | ||
var keyInfo = Console.ReadKey(true); | ||
|
||
OutLine($"key={keyInfo.Key} char='{CharUtils.ToNiceString(keyInfo.KeyChar)}' mod={keyInfo.Modifiers}"); | ||
} | ||
} | ||
#pragma warning restore RS0030 | ||
|
||
for (var next = 0; next != 3; ) | ||
CliExitCode ShowKeysCathodeRaw() | ||
{ | ||
var buffer = new byte[100]; | ||
for (;;) | ||
{ | ||
Out((next = queue.Take()) switch | ||
var read = Read(buffer); | ||
for (var i = 0; i < read; ++i) | ||
{ | ||
3 => "^C", | ||
0x1b => "^[", | ||
'\r' => "\n", | ||
_ => ((char)next).ToString() | ||
}); | ||
var c = (char)buffer[i]; | ||
Out(CharUtils.ToNiceString(c)); | ||
|
||
if (c == ControlConstants.ETX) | ||
return UnixSignal.KeyboardInterrupt.AsCliExitCode(); | ||
} | ||
Out("\r\n"); | ||
} | ||
} | ||
|
||
async Task<CliExitCode> ShowKeysParserRaw() | ||
{ | ||
await foreach (var item in AnsiInput.SelectReadKeysAsync(TerminalIn)) | ||
{ | ||
await OutAsync($"{item}\r\n"); | ||
if (item is { Char: 'c', Ctrl: true }) | ||
return UnixSignal.KeyboardInterrupt.AsCliExitCode(); | ||
} | ||
|
||
await task; | ||
return CliExitCode.Success; | ||
} | ||
finally | ||
|
||
async Task<CliExitCode> Drive() | ||
{ | ||
DisableRawMode(); | ||
OutLine("Ctrl-C to quit"); | ||
OutLine(); | ||
|
||
EnableRawMode(); | ||
|
||
await Task.Delay(0); | ||
|
||
return CliExitCode.Success; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters