Crates.io | r3bl_terminal_async |
lib.rs | r3bl_terminal_async |
version | |
source | src |
created_at | 2024-04-16 19:43:09.2069 |
updated_at | 2024-10-21 16:27:49.768772 |
description | Async non-blocking read_line implemenation with multiline editor, with concurrent display output from tasks, and colorful animated spinners |
homepage | https://r3bl.com |
repository | https://github.com/r3bl-org/r3bl-open-core/tree/main/terminal_async |
max_upload_size | |
id | 1210619 |
Cargo.toml error: | TOML parse error at line 21, column 1 | 21 | autolib = false | ^^^^^^^ unknown field `autolib`, expected one of `name`, `version`, `edition`, `authors`, `description`, `readme`, `license`, `repository`, `homepage`, `documentation`, `build`, `resolver`, `links`, `default-run`, `default_dash_run`, `rust-version`, `rust_dash_version`, `rust_version`, `license-file`, `license_dash_file`, `license_file`, `licenseFile`, `license_capital_file`, `forced-target`, `forced_dash_target`, `autobins`, `autotests`, `autoexamples`, `autobenches`, `publish`, `metadata`, `keywords`, `categories`, `exclude`, `include` |
size | 0 |
R3BL TUI library & suite of apps focused on developer productivity
We are working on building command line apps in Rust which have rich text user interfaces (TUI). We want to lean into the terminal as a place of productivity, and build all kinds of awesome apps for it.
🔮 Instead of just building one app, we are building a library to enable any kind of rich TUI development w/ a twist: taking concepts that work really well for the frontend mobile and web development world and re-imagining them for TUI & Rust.
🌎 We are building apps to enhance developer productivity & workflows.
tmux
in Rust (separate processes mux'd onto a
single terminal window). Rather it is to build a set of integrated "apps" (or
"tasks") that run in the same process that renders to one terminal window.All the crates in the r3bl-open-core
repo provide lots of useful
functionality to help you build TUI (text user interface) apps, along w/ general
niceties & ergonomics that all Rustaceans 🦀 can enjoy 🎉.
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.
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 in
read_line()
).
read_line()
unblocked once it is blocked.process::exit()
or
panic!()
to kill the entire
process. This is not appealing.thread::spawn()
or
thread::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.Another problem is that when a thread is blocked in read_line()
, and you have to
display output to stdout
concurrently, this poses some challenges.
read_line()
and it blocks.stdout
concurrently, it assumes that the
caret is at row 0 of a new line.read_line()
output, which assumes that no other output will be produced, while
is blocking for user input, resulting in a bad user experience.Here is a video of the terminal_async
and spinner
examples in this crate, in
action:
Please check out the changelog to see how the library has evolved over time.
To learn how we built this crate, please take a look at the following resources.
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 async stdout
writers ([r3bl_core::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 [r3bl_core::SharedWriter
] directly. But you can if you
need to. [r3bl_core::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 [r3bl_core::SharedWriter
]
instances that are associated with one [Readline
] instance. Typically a spawned
task clones its own [r3bl_core::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 running
cargo run --example terminal_async
. Then type starttask1
, press Enter. Then
type spinner
, press Enter.
Use tokio tracing with support for concurrent stout
writes. If you choose to log
to stdout
then the concurrent version ([r3bl_core::SharedWriter
]) from
this crate will be used. This ensures that the concurrent output is supported even
for your tracing logs to stdout
.
You can also plug in your own terminal, like stdout
, or stderr
, 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.
cargo run --examples terminal_async
# This won't do anything. Just exits with no error.
echo "hello" | cargo run --examples terminal_async
The pause and resume functionality is implemented using:
The [Readline::new] or [TerminalAsync::try_new] create a line_channel
to send and
receive [r3bl_core::LineStateControlSignal]:
While the [Readline] is suspended, no input is possible, and only Ctrl+C and Ctrl+D are allowed to make it through, the rest of the keypresses are ignored.
See [Readline] module docs for more implementation details on this.
While entering text, the user can edit and navigate through the current input line with the following key bindings:
crossterm
.Eof
event.Interrupt
event.crossterm
's event-stream
feature.cargo run --example terminal_async
cargo run --example spinner
cargo run --example shell_async
TerminalAsync::try_new()
], which is the main entry point for most use casesTerminalAsync::get_readline_event()
].TerminalAsync::clone_shared_writer()
] to get a
[r3bl_core::SharedWriter
] instance that you can use to write to stdout
concurrently, using [std::write!
] or [std::writeln!
].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()
].TerminalAsync::println
] and [TerminalAsync::println_prefixed
]
methods to easily write concurrent output to the stdout
([r3bl_core::SharedWriter
]).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:
crossterm::event::EventStream
.std::io::Stdout
].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
[r3bl_core::SharedWriter
] instances. Lines written to an associated
[r3bl_core::SharedWriter
] are output to the raw terminal.
Call [Readline::new()
] to create a [Readline
] instance and associated
[r3bl_core::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 [r3bl_core::SharedWriter
] while
readline()
is in progress will be output to the screen above the input line.
When done, call [crate::manage_shared_writer_output::flush_internal()
] to ensure
that all lines written to the [r3bl_core::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 [r3bl_core::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
).
[Spinner
]s also has cancellation support. Once a spinner is started,
Ctrl+C and Ctrl+D are directed to the spinner, to cancel it.
Spinners can also be checked for completion or cancellation by long running tasks, to
ensure that they exit as a response to user cancellation. Take a look at the
examples/terminal_async.rs
file to get an understanding of how to use this API.
The third change is that [TerminalAsync::try_new()
] now accepts prompts that can
have ANSI escape sequences in them. Here's an example of this.
let prompt = {
let user = "naz";
let prompt_seg_1 = "â•".magenta().on_dark_grey().to_string();
let prompt_seg_2 = format!("┤{user}├").magenta().on_dark_grey().to_string();
let prompt_seg_3 = "â•®".magenta().on_dark_grey().to_string();
format!("{}{}{} ", prompt_seg_1, prompt_seg_2, prompt_seg_3)
};
let maybe_terminal_async = TerminalAsync::try_new(prompt.as_str()).await?;
let Some(mut terminal_async) = maybe_terminal_async else {
return Err(miette::miette!("Failed to create terminal").into());
};
Ok(())
Watch the following videos to learn more about how this crate was built:
The following playlists are relevant to this 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:
r3bl_core::SharedWriter
]s and
the [Readline
], to properly support pause and resume, and other control functions.tokio
. Rewrite all the code for
this.pin-project
, thingbuf
in favor of tokio
. Rewrite all the code
for this.simplelog
and log
dependencies. Add support for tokio-tracing
. Rewrite
all the code for this, and add tracing_setup.rs
.spinner_impl
, readline_impl
, and public_api
modules.stdin
Thread::cancel()
License: Apache-2.0