r3bl_terminal_async
The r3bl_terminal_async
library lets your CLI program be asynchronous and
interactive without blocking the main thread. Your spawned tasks can use it to
concurrently write to the display output, pause and resume it. You can also display of
colorful animated spinners ⌛🌈 for long running tasks. With it, you can create
beautiful, powerful, and interactive REPLs (read execute print loops) with ease.
Why use this crate
-
Because
read_line()
is blocking. And there is no way to terminate an OS thread that is blocking in Rust. To do this you have to exit the process (who's thread is blocked inread_line()
).- There is no way to get
read_line()
unblocked once it is blocked. - You can use
process::exit()
orpanic!()
to kill the entire process. This is not appealing. - Even if that task is wrapped in a
thread::spawn()
orthread::spawn_blocking()
, it isn't possible to cancel or abort that thread, without cooperatively asking it to exit. To see what this type of code looks like, take a look at this.
- There is no way to get
-
Another annoyance is that when a thread is blocked in
read_line()
, and you have to display output tostdout
concurrently, this poses some challenges.- This is because the caret is moved by
read_line()
and it blocks. - When another thread / task writes to
stdout
concurrently, it assumes that the caret is at row 0 of a new line. - This results in output that doesn't look good.
- This is because the caret is moved by
Here is a video of the terminal_async
and spinner
examples in this crate, in
action:
Features
-
Read user input from the terminal line by line, while your program concurrently writes lines to the same terminal. One [
Readline
] instance can be used to spawn many asyncstdout
writers ([SharedWriter]) that can write to the terminal concurrently. For most users the [TerminalAsync
] struct is the simplest way to use this crate. You rarely have to access the underlying [Readline
] or [SharedWriter
] directly. But you can if you need to. [SharedWriter
] can be cloned and is thread-safe. However, there is only one instance of [Readline
] per [TerminalAsync
] instance. -
Generate a spinner (indeterminate progress indicator). This spinner works concurrently with the rest of your program. When the [
Spinner
] is active it automatically pauses output from all the [SharedWriter
] instances that are associated with one [Readline
] instance. Typically a spawned task clones its own [SharedWriter
] to generate its output. This is useful when you want to show a spinner while waiting for a long-running task to complete. Please look at the example to see this in action, by runningcargo run --example terminal_async
. Then typestarttask1
, press Enter. Then typespinner
, press Enter. -
Use tokio tracing with support for concurrent
stout
writes. If you choose to log tostdout
then the concurrent version ([SharedWriter
]) from this crate will be used. This ensures that the concurrent output is supported even for your tracing logs tostdout
. -
You can also plug in your own terminal, like
stdout
, orstderr
, or any other terminal that implements [SendRawTerminal
] trait for more details.
This crate can detect when your terminal is not in interactive mode. Eg: when you pipe
the output of your program to another program. In this case, the readline
feature is
disabled. Both the [TerminalAsync
] and [Spinner
] support this functionality. So if
you run the examples in this crate, and pipe something into them, they won't do
anything. Here's an example:
# This will work.
# This won't do anything. Just exits with no error.
|
To learn more about how this crate itself was built, please checkout the Build with Naz
video series on developerlife.com YT
channel:
Input Editing Behavior
While entering text, the user can edit and navigate through the current input line with the following key bindings:
- Works on all platforms supported by
crossterm
. - Full Unicode Support (Including Grapheme Clusters).
- Multiline Editing.
- In-memory History.
- Left, Right: Move cursor left/right.
- Up, Down: Scroll through input history.
- Ctrl-W: Erase the input from the cursor to the previous whitespace.
- Ctrl-U: Erase the input before the cursor.
- Ctrl-L: Clear the screen.
- Ctrl-Left / Ctrl-Right: Move to previous/next whitespace.
- Home: Jump to the start of the line.
- When the "emacs" feature (on by default) is enabled, Ctrl-A has the same effect.
- End: Jump to the end of the line.
- When the "emacs" feature (on by default) is enabled, Ctrl-E has the same effect.
- Ctrl-C, Ctrl-D: Send an
Eof
event. - Ctrl-C: Send an
Interrupt
event. - Extensible design based on
crossterm
'sevent-stream
feature.
Examples
How to use this crate
[TerminalAsync::try_new()
], which is the main entry point for most use cases
- To read user input, call [
TerminalAsync::get_readline_event()
]. - You can call [
TerminalAsync::clone_shared_writer()
] to get a [SharedWriter
] instance that you can use to write tostdout
concurrently, using [std::write!
] or [std::writeln!
]. - If you use [
std::writeln!
] then there's no need to [TerminalAsync::flush()
] because the\n
will flush the buffer. When there's no\n
in the buffer, or you are using [std::write!
] then you might need to call [TerminalAsync::flush()
]. - You can use the [
TerminalAsync::println
] and [TerminalAsync::println_prefixed
] methods to easily write concurrent output to thestdout
([SharedWriter
]). - You can also get access to the underlying [
Readline
] via the [Readline::readline
] field. Details on this struct are listed below. For most use cases you won't need to do this.
[Readline
] overview (please see the docs for this struct for details)
-
Structure for reading lines of input from a terminal while lines are output to the terminal concurrently. It uses dependency injection, allowing you to supply resources that can be used to:
- Read input from the user, typically
crossterm::event::EventStream
. - Generate output to the raw terminal, typically [
std::io::Stdout
].
- Read input from the user, typically
-
Terminal input is retrieved by calling [
Readline::readline()
], which returns each complete line of input once the user presses Enter. -
Each [
Readline
] instance is associated with one or more [SharedWriter
] instances. Lines written to an associated [SharedWriter
] are output to the raw terminal. -
Call [
Readline::new()
] to create a [Readline
] instance and associated [SharedWriter
]. -
Call [
Readline::readline()
] (most likely in a loop) to receive a line of input from the terminal. The user entering the line can edit their input using the key bindings listed under "Input Editing" below. -
After receiving a line from the user, if you wish to add it to the history (so that the user can retrieve it while editing a later line), call [
Readline::add_history_entry()
]. -
Lines written to the associated [
SharedWriter
] whilereadline()
is in progress will be output to the screen above the input line. -
When done, call [
crate::pause_and_resume_support::flush_internal()
] to ensure that all lines written to the [SharedWriter
] are output.
[Spinner::try_start()
]
This displays an indeterminate spinner while waiting for a long-running task to
complete. The intention with displaying this spinner is to give the user an indication
that the program is still running and hasn't hung up or become unresponsive. When
other tasks produce output concurrently, this spinner's output will not be clobbered.
Neither will the spinner output clobber the output from other tasks. It suspends the
output from all the [SharedWriter
] instances that are associated with one
[Readline
] instance. Both the terminal_async.rs
and spinner.rs
examples shows
this (cargo run --example terminal_async
and cargo run --example spinner
).
[tracing_setup::init()
]
This is a convenience method to setup Tokio [tracing_subscriber
] with stdout
as
the output destination. This method also ensures that the [SharedWriter
] is used for
concurrent writes to stdout
. You can also use the [TracingConfig
] struct to
customize the behavior of the tracing setup, by choosing whether to display output to
stdout
, stderr
, or a [SharedWriter
]. By default, both display and file logging
are enabled. You can also customize the log level, and the file path and prefix for
the log file.
Video series on developerlife.com YT channel on building this crate with Naz
- Part 1: Why?
- Part 2: What?
- Part 3: Do the refactor and rename the crate
- Part 4: Build the spinner
- Part 5: Add color gradient animation to spinner
- Part 6: Publish the crate and overview
- Testing playlist
- Playlists
Why another async readline crate?
This crate & repo is forked from rustyline-async. However it has mostly been rewritten and re-architected. Here are some changes made to the code:
- Rearchitect the entire crate from the ground up to operate in a totally different manner than the original. All the underlying mental models are different, and simpler. The main event loop is redone. And a task is used to monitor the line channel for communication between multiple [SharedWriter]s and the [Readline], to properly support pause and resume, and other control functions.
- Drop support for all async runtimes other than
tokio
. Rewrite all the code for this. - Drop crates like
pin-project
,thingbuf
in favor oftokio
. Rewrite all the code for this. - Drop
simplelog
andlog
dependencies. Add support fortokio-tracing
. Rewrite all the code for this, and addtracing_setup.rs
. - Remove all examples and create new ones to mimic a real world CLI application.
- Add
spinner_impl
,readline_impl
, andpublic_api
modules. - Add tests.
More info on blocking and thread cancellation in Rust
- Docs: tokio's
stdin
- Discussion: Stopping a thread in Rust
- Discussion: Support for
Thread::cancel()
- Discussion: stdin, stdout redirection for spawned processes
License: Apache-2.0