You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
18 KiB
467 lines
18 KiB
using System;
|
|
using System.Threading.Tasks;
|
|
using System.Threading;
|
|
using System.Threading.Channels;
|
|
|
|
using termsync.Tools;
|
|
using System.Collections.Generic;
|
|
|
|
namespace termsync
|
|
{
|
|
/// <summary>
|
|
/// A Write container that aquires a mutex on `Terminal`, preventing other writes while held.
|
|
/// </summary>
|
|
public sealed class TermLock : IDisposable
|
|
{
|
|
private readonly CancellationTokenSource cancel;
|
|
private readonly AsyncMutex GlobalMutex;
|
|
private IDisposable held_lock = null;
|
|
internal TermLock(AsyncMutex mutex, CancellationToken token=default)
|
|
{
|
|
GlobalMutex = mutex;
|
|
|
|
cancel = CancellationTokenSource.CreateLinkedTokenSource(token);
|
|
}
|
|
|
|
internal async Task Acquire()
|
|
{
|
|
held_lock = await GlobalMutex.AcquireAsync(cancel.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a line with lock held.
|
|
/// </summary>
|
|
public Task WriteLine(string line)
|
|
{
|
|
return Terminal.WriteLineAndWait(line, cancel.Token);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
cancel.Cancel();
|
|
cancel.Dispose();
|
|
if(held_lock!=null)
|
|
{
|
|
held_lock.Dispose();
|
|
held_lock = null;
|
|
}
|
|
}
|
|
|
|
~TermLock()
|
|
{
|
|
if (held_lock != null)
|
|
{
|
|
held_lock.Dispose();
|
|
held_lock = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class TermStage : IAsyncDisposable
|
|
{
|
|
private readonly AsyncMutex mutex = new AsyncMutex();
|
|
private readonly List<string> lines = new List<string>();
|
|
private readonly CancellationTokenSource cancel;
|
|
private readonly AsyncMutex globalMutex;
|
|
private readonly CancellationToken originalToken;
|
|
|
|
internal TermStage(AsyncMutex globalM, CancellationToken token)
|
|
{
|
|
originalToken = token;
|
|
globalMutex = globalM;
|
|
cancel = CancellationTokenSource.CreateLinkedTokenSource(token);
|
|
}
|
|
|
|
public async Task WriteLine(string line)
|
|
{
|
|
using (await mutex.AcquireAsync(cancel.Token))
|
|
{
|
|
lines.Add(line);
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await mutex.AcquireAsync(cancel.Token);
|
|
|
|
cancel.Cancel();
|
|
|
|
mutex.Dispose();
|
|
cancel.Dispose();
|
|
|
|
using (await globalMutex.AcquireAsync(originalToken))
|
|
{
|
|
foreach (var line in lines)
|
|
{
|
|
await Terminal.WriteLineAndWait(line, originalToken);
|
|
}
|
|
}
|
|
|
|
lines.Clear();
|
|
}
|
|
}
|
|
|
|
//TODO: WriteLine staging that doesn't block until read to commit all.
|
|
|
|
/// <summary>
|
|
/// Terminal control global state.
|
|
/// </summary>
|
|
public static partial class Terminal
|
|
{
|
|
#region Sync
|
|
private static readonly CancellationTokenSource CancelAll = new CancellationTokenSource();
|
|
private static ChannelReader<string> Input;
|
|
private static ChannelWriter<ControlValue> Output;
|
|
|
|
private static readonly AsyncMutex ConsoleMutex = new AsyncMutex();
|
|
|
|
#endregion
|
|
#region Buffer
|
|
private static readonly List<char> InputBuffer = new List<char>();
|
|
/// <summary>
|
|
/// The place in the <see cref="InputBuffer"/> that the user is writing to.
|
|
/// </summary>
|
|
private static int InputAt = -1;
|
|
public static string Prompt { get; private set; } = "> ";
|
|
private static bool WriteLineOnFlush = true;
|
|
|
|
#endregion
|
|
|
|
#region Control
|
|
private static readonly AsyncMutex UserWriteMutex = new AsyncMutex();
|
|
private static readonly AsyncMutex UserReadMutex = new AsyncMutex();
|
|
|
|
/// <summary>
|
|
/// Acquire global Write lock mutex.
|
|
/// </summary>
|
|
public static async Task<TermLock> Lock()
|
|
{
|
|
var l = new TermLock(UserWriteMutex, CancelAll.Token);
|
|
await l.Acquire();
|
|
return l;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a staging container that will write all lines at once on DisposeAsync().
|
|
/// </summary>
|
|
public static TermStage Stage()
|
|
{
|
|
return new TermStage(UserWriteMutex, CancelAll.Token);
|
|
}
|
|
|
|
private static async Task<object> SendAndWait(ControlValue value, CancellationToken token = default)
|
|
{
|
|
TaskCompletionSource<bool> onCancel = new TaskCompletionSource<bool>();
|
|
using var reg = token.Register(() =>
|
|
{
|
|
onCancel.SetResult(false);
|
|
});
|
|
await Output.WriteAsync(value, token);
|
|
|
|
|
|
if (await Task.WhenAny(value.Processed.Task, onCancel.Task) == value.Processed.Task)
|
|
{
|
|
return value.Processed.Task.Result;
|
|
}
|
|
else throw new OperationCanceledException();
|
|
}
|
|
/// <summary>
|
|
/// Write a line to the Terminal and wait for it to appear.
|
|
/// </summary>
|
|
/// <param name="line">The line</param>
|
|
public static async Task WriteLine(string line)
|
|
{
|
|
using(await UserWriteMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
await SendAndWait(new ControlValue(ControlType.Print, line), CancelAll.Token);
|
|
}
|
|
}
|
|
internal static ValueTask WriteLine(string str, CancellationToken token)
|
|
{
|
|
return Output.WriteAsync(new ControlValue(ControlType.Print, str), token);
|
|
}
|
|
internal static Task WriteLineAndWait(string str, CancellationToken token)
|
|
{
|
|
return SendAndWait(new ControlValue(ControlType.Print, str), token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read a line from the user.
|
|
/// </summary>
|
|
public static async Task<string> ReadLine(CancellationToken token=default)
|
|
{
|
|
using var cancel = CancellationTokenSource.CreateLinkedTokenSource(CancelAll.Token, token);
|
|
token = cancel.Token;
|
|
|
|
using (await UserReadMutex.AcquireAsync(token))
|
|
{
|
|
return await Input.ReadAsync(token);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change the user prompt.
|
|
/// </summary>
|
|
/// <param name="prompt">New prompt.</param>
|
|
public static async Task ChangePrompt(string prompt)
|
|
{
|
|
using (await UserWriteMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
await SendAndWait(new ControlValue(ControlType.ChangePrompt, prompt), CancelAll.Token);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Initialise terminal control. Writes go through <see cref="Terminal"/> after this.
|
|
/// </summary>
|
|
/// <returns>The control task.</returns>
|
|
public static Task Initialise()
|
|
{
|
|
|
|
var input = Channel.CreateUnbounded<string>();
|
|
var output = Channel.CreateBounded<ControlValue>(10);
|
|
|
|
Input = input.Reader;
|
|
Output = output.Writer;
|
|
|
|
AppDomain.CurrentDomain.ProcessExit += (_, __) => Cleanup();
|
|
var t_output = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await ctrl_output(output.Reader, input.Writer);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception ex)
|
|
{
|
|
if (!CancelAll.IsCancellationRequested)
|
|
CancelAll.Cancel();
|
|
}
|
|
});
|
|
|
|
var t_input = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await ctrl_intput();
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception ex)
|
|
{
|
|
if(!CancelAll.IsCancellationRequested)
|
|
CancelAll.Cancel();
|
|
}
|
|
});
|
|
|
|
return Task.Run(async () =>
|
|
{
|
|
await Task.WhenAll(t_input, t_output);
|
|
});
|
|
}
|
|
|
|
private static async Task ctrl_output(ChannelReader<ControlValue> output, ChannelWriter<string> lines)
|
|
{
|
|
await foreach(var value in output.ReadAllAsync(CancelAll.Token))
|
|
{
|
|
object result = null;
|
|
try
|
|
{
|
|
switch (value.Type)
|
|
{
|
|
case ControlType.Print when value.Value is string line:
|
|
using (await ConsoleMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
Mutation.ClearInputBuffer();
|
|
Console.WriteLine(line);
|
|
Mutation.RedrawInputBuffer();
|
|
}
|
|
break;
|
|
case ControlType.Move:
|
|
using (await ConsoleMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
if (InputAt == -1) InputAt = InputBuffer.Count;
|
|
switch ((MoveDirection)value.Value)
|
|
{
|
|
case MoveDirection.Left:
|
|
if (InputAt > 0)
|
|
{
|
|
InputAt -= 1;
|
|
Console.CursorLeft -= 1;
|
|
}
|
|
break;
|
|
case MoveDirection.Right:
|
|
if (InputAt < InputBuffer.Count)
|
|
{
|
|
InputAt += 1;
|
|
Console.CursorLeft += 1;
|
|
}
|
|
break;
|
|
}
|
|
if (InputAt >= InputBuffer.Count)
|
|
InputAt = -1;
|
|
}
|
|
break;
|
|
case ControlType.Delete:
|
|
using (await ConsoleMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
switch ((DeleteLocation)value.Value)
|
|
{
|
|
case DeleteLocation.After:
|
|
if (InputBuffer.Count > 0)
|
|
{
|
|
if (InputAt >= 0 && InputAt < InputBuffer.Count)
|
|
{
|
|
InputBuffer.RemoveAt(InputAt);
|
|
|
|
Console.Write(" \b");
|
|
Mutation.MoveBackOne();
|
|
}
|
|
}
|
|
break;
|
|
case DeleteLocation.Before:
|
|
if (InputBuffer.Count > 0)
|
|
{
|
|
if (InputAt > 0 && InputAt < InputBuffer.Count)
|
|
{
|
|
InputBuffer.RemoveAt(InputAt - 1);
|
|
InputAt -= 1;
|
|
|
|
Console.Write("\b");
|
|
Mutation.MoveBackOne();
|
|
}
|
|
else if (InputAt != 0)
|
|
{
|
|
InputBuffer.RemoveAt(InputBuffer.Count - 1);
|
|
Console.Write("\b \b");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case ControlType.Commit:
|
|
using (await ConsoleMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
await Mutation.FlushAsync(lines, CancelAll.Token);
|
|
}
|
|
break;
|
|
case ControlType.Echo:
|
|
using (await ConsoleMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
var input = value.Value as string;
|
|
if (input == null) continue;
|
|
for (int i = 0; i < input.Length; i++)
|
|
{
|
|
switch (input[i])
|
|
{
|
|
case '\n':
|
|
case '\r':
|
|
//Should be handled by `Commit`, something went wrong if they end up here.
|
|
break;
|
|
case '\b':
|
|
//Ditto for `Delete`.
|
|
break;
|
|
default:
|
|
{
|
|
if (((InputAt == -1) ? Console.CursorLeft : Console.CursorLeft + (InputBuffer.Count - InputAt)) >= Console.BufferWidth - 1) break; //TODO: Scrolling buffer?
|
|
|
|
if (InputAt >= 0 && InputAt < InputBuffer.Count)
|
|
{
|
|
InputBuffer.Insert(InputAt, input[i]);
|
|
InputAt += 1;
|
|
|
|
Mutation.MoveForwardOne();
|
|
}
|
|
else
|
|
{
|
|
InputBuffer.Add(input[i]);
|
|
}
|
|
Console.Write(input[i]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case ControlType.ChangePrompt:
|
|
using (await ConsoleMutex.AcquireAsync(CancelAll.Token))
|
|
{
|
|
Prompt = (string)value.Value;
|
|
|
|
Mutation.ClearInputBuffer();
|
|
Mutation.RedrawInputBuffer();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
value.Processed.SetResult(result);
|
|
}
|
|
}
|
|
|
|
lines.Complete();
|
|
InputBuffer.Clear();
|
|
}
|
|
/// <summary>
|
|
/// Read from the Console and pipe buffer control events to `Output`.
|
|
/// </summary>
|
|
private static async Task ctrl_intput()
|
|
{
|
|
while (!CancelAll.IsCancellationRequested)
|
|
{
|
|
var key = Console.ReadKey(true); // This blocks.
|
|
|
|
if (CancelAll.IsCancellationRequested) break;
|
|
switch (key.Key)
|
|
{
|
|
case ConsoleKey.LeftArrow:
|
|
await Output.WriteAsync(new ControlValue(ControlType.Move, MoveDirection.Left), CancelAll.Token);
|
|
|
|
break;
|
|
case ConsoleKey.RightArrow:
|
|
await Output.WriteAsync(new ControlValue(ControlType.Move, MoveDirection.Right), CancelAll.Token);
|
|
|
|
break;
|
|
case ConsoleKey.Delete:
|
|
await Output.WriteAsync(new ControlValue(ControlType.Delete, DeleteLocation.After), CancelAll.Token);
|
|
break;
|
|
case ConsoleKey.Backspace:
|
|
await Output.WriteAsync(new ControlValue(ControlType.Delete, DeleteLocation.Before), CancelAll.Token);
|
|
break;
|
|
case ConsoleKey.Enter:
|
|
await Output.WriteAsync(new ControlValue(ControlType.Commit, null), CancelAll.Token);
|
|
break;
|
|
default:
|
|
await Output.WriteAsync(new ControlValue(ControlType.Echo, new string(new[] { key.KeyChar })), CancelAll.Token);
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close all <see cref="Terminal"/> channels.
|
|
/// </summary>
|
|
public static void Close()
|
|
{
|
|
Output.Complete();
|
|
}
|
|
private static void Cleanup()
|
|
{
|
|
try
|
|
{
|
|
Output.Complete();
|
|
}
|
|
catch { }
|
|
if (!CancelAll.IsCancellationRequested)
|
|
CancelAll.Cancel();
|
|
|
|
CancelAll.Dispose();
|
|
ConsoleMutex.Dispose();
|
|
}
|
|
}
|
|
}
|