Concurrent-aware terminal control for .NET Core
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.
Avril d8046b9994
nothing of note
1 year ago
termsync Initial commit 1 year ago
test Initial commit 1 year ago
.gitignore added readme 1 year ago
LICENSE Create LICENSE 1 year ago
README.md nothing of note 1 year ago
termsync.sln Initial commit 1 year ago

README.md

Terminal Synchroniser

termsync is a .NET Core library for synchronising terminal IO in an asynchronous contex. It guarantees user keystrokes into the console are never split up by WriteLines()s happening on other threads. Also exposed is a global Channel<string> object for reading user input lines asynchronously.

It spawns a "worker" Task when initialised that creates channels for piping info to and from the console atomically.

Documentation

See Terminal.cs for inline API documentation. (WIP)

Examples

Initialising the global state

Start the worker that reads and writes to Console.

	Task worker = Terminal.Initialise();
	
	// ... program code here
	
	Terminal.Close(); // Manually clean up resources. Not required as it is cleaned up on exist.
	await worker; // Wait for the worker to close if you want.

Write lines

Line writes are asynchronous, await them to yield until the line has been displayed in the Console.

	await Terminal.WriteLine("A line.");

Read lines

Line reads can take CancellationToken.

	string line = await Terminal.ReadLine(source.Token);

Change user prompt

User prompt can be changed. Await ChangePrompt to yield until the new prompt has been displayed.

	await Terminal.ChangePrompt("USER> ");
	
	await Terminal.ChangePrompt(""); //No prompt!

Other control

Multiple threads might often want to write multiple lines over a short period of time, and not have them messed up with another line(s) from other threads. The programmer might encounter a situation like this:

  1. Thread 1
  • WriteLine("Line 1 of important message");
  • WriteLine("Line 2 that we don't want");
  • WriteLine("Line 3 to be split up by another message");
  1. Thread 2
  • WriteLine("Line 4 of thread 2's important message");
  • WriteLine("Line 5 that it really doesn't want");
  • WriteLine("Line 6 to be split up by another message");
  1. Thread 3
  • WriteLine("A single line message from thread 3");

And she could see this:

	Line 1 of important message
	Line 4 of thread 2's important message
	Line 2 that we don't want
	Line 3 to be split up by another message
	Line 5 that it really doesn't want
	A single line message from thread 3
	Line 6 to be split up by another message

Oh no! This is quite annoying. For this we have 2 APIs.

Lock

Lock acquires the global write mutex as long as the lock handle is alive. You can write to the lock handle as you would to Terminal.

	using(var terminal = await Terminal.Lock()) // Lock is asynchronously acquired here.
	{
		await terminal.WriteLine("Line 1");
		await termianl.WriteLine("Line 2");
		await terminal.WriteLine("Line 3");
	} // The lock is released when `terminal`'s `Dispose()` method is called here.

These writes are now guaranteed to not be interrupted by other calls to WriteLine anywhere within Terminal. The drawback is it is a global mutex that can be acquired for any amount of time, potentially blocking other tasks where it wouldn't be ideal. For these situations there is Stage.

Stage

Stage stores the writes in a buffer temporarily. The only mutex it acquires is internal to the stage handle, and doesn't hold the global write mutex until it wants to push everything it has stored.

	await using(var terminal = Terminal.Stage()) // Create a new `Stage` here.
	{
		await terminal.WriteLine("Line 4"); // These are stored in `Stage` until its `DisposeAsync()` method is called.
		await terminal.WriteLine("Line 5");
		await terminal.WriteLine("Line 6");
	} // All buffered lines are written atomically here.

Pitfalls

  • The library respectfully asks the programmer to use the Terminal interface instead of the Console interface for all reads and writes in the program. I realise this is not ideal, or even possible in some cases. This can cause problems when dependencies write to Console and can break lines and such. Maybe I will try to fix this if I run into this problem.
  • Currently only full line writes are supported at a time. I will likely fix this in the future.

License

GPL'd with love <3