10 个版本
新增 0.5.6 | 2024 年 8 月 14 日 |
---|---|
0.5.5 | 2024 年 7 月 13 日 |
0.5.3 | 2024 年 5 月 22 日 |
0.5.1 | 2024 年 4 月 28 日 |
0.3.1 | 2024 年 4 月 18 日 |
#168 在 命令行界面
每月 184 次下载
11MB
28K SLoC
r3bl_terminal_async
r3bl_terminal_async
库可以让您的 CLI 程序异步和交互式,而不阻塞主线程。您生成的任务可以使用它来并发地向显示输出写入、暂停和恢复。您还可以显示彩色动画旋转器 ⌛🌈 以便长时间运行的任务。使用它,您可以轻松创建美丽、强大且交互式的 REPL(读取执行打印循环)。
为什么要使用这个包
-
因为
read_line
是阻塞的。在 Rust 中,没有方法可以终止在read_line
中阻塞的 OS 线程。为此,您必须退出进程(该进程的线程在read_line
中阻塞)。- 一旦
read_line
被阻塞,就没有方法可以将其解锁。 - 您可以使用
process::exit
或panic!
来终止整个进程。这并不吸引人。 - 即使该任务被包装在
thread::spawn()
或thread::spawn_blocking()
中,如果没有合作地请求它退出,也无法取消或终止该线程。要了解此类代码的示例,请查看 此处。
- 一旦
-
另一个烦恼是,当一个线程在
read_line()
中阻塞,并且您必须同时向stdout
显示输出时,这会带来一些挑战。- 这是因为光标被
read_line()
移动并阻塞。 - 当另一个线程/任务并发地向
stdout
写入时,它假定光标位于新行的第0行。 - 这导致输出看起来不好。
- 这是因为光标被
这是一个视频,展示了本库中的 terminal_async
和 spinner
示例在实际运行中的情况
特性
-
逐行从终端读取用户输入,同时您的程序并发地向同一终端写入行。一个
Readline
实例可以用于创建许多异步stdout
写入器 ([SharedWriter]),它们可以并发地向终端写入。对于大多数用户来说,TerminalAsync
结构体是使用此库的最简单方式。您很少需要直接访问底层的Readline
或SharedWriter
。但如果有需要,您也可以这样做。SharedWriter
可以被克隆,并且是线程安全的。但是,每个Readline
实例只有一个TerminalAsync
实例。 -
生成一个旋转器(不确定的进度指示器)。这个旋转器可以与您的程序并发工作。当
Spinner
处于活动状态时,它会自动暂停与一个Readline
实例关联的所有SharedWriter
实例的输出。通常,被创建的任务会克隆自己的SharedWriter
来生成输出。当您想在等待长时间运行的任务完成时显示旋转器时,这很有用。请运行示例来查看其运行情况,运行cargo run --example terminal_async
。然后输入starttask1
,按回车。然后输入spinner
,按回车。 -
使用支持并发
stout
写入的tokio tracing。如果您选择将日志记录到stdout
,则将使用此crate中的并发版本(SharedWriter
)。这确保了即使是对stdout
的跟踪日志,也能支持并发输出。 -
您还可以连接自己的终端,如
stdout
、stderr
或任何实现SendRawTerminal
trait的终端,以获取更多详细信息。
此crate可以检测您的终端是否不在交互式模式。例如:当您将程序的输出管道连接到另一个程序时。在这种情况下,禁用了readline
功能。无论是TerminalAsync
还是Spinner
都支持此功能。因此,如果您运行此crate中的示例,并将某些内容管道连接到它们,它们将不会执行任何操作。以下是一个示例
# 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
要了解更多关于此crate本身是如何构建的,请查看developerlife.com上的Build with Naz
视频系列YT频道
暂停和恢复支持
暂停和恢复功能是通过以下方式实现的
- LineState::is_paused - 用于检查行状态是否已暂停,并影响渲染和输入。
- LineState::set_paused - 通过以下[SharedWriter]设置暂停状态。这不能直接调用(在crate外部)。
- SharedWriter::line_state_control_channel_sender - 用于操纵暂停状态的机制。
Readline::new或TerminalAsync::try_new创建一个line_channel
来发送和接收[LineStateControlSignal]。
- 此通道的发送端被移动到[SharedWriter]。因此,任何[SharedWriter]都可以用来向通道发送[LineStateControlSignal],这些信号将在启动的任务中处理,仅为此目的,在Readline::new中。这是在暂停和恢复之间切换的主要机制。在TerminalAsync::pause和TerminalAsync::resume中提供了某些辅助函数,尽管您可以直接通过SharedWriter::line_state_control_channel_sender将信号直接发送到通道的发送端。
- 此tokio::sync::mpsc::channel的接收端被移动到由Readline::new创建的任务。这是发送端(如上所述)发送信号时实际工作的地方。
当[Readline]被挂起时,无法进行输入,只有 Ctrl+C 和 Ctrl+D 被允许通过,其余的按键将被忽略。
有关此功能的更多实现细节,请参阅[Readline]模块文档。
输入编辑行为
在输入文本时,用户可以使用以下键绑定来编辑和导航当前输入行
- 在所有支持的平台上都适用
crossterm
。 - 全Unicode支持(包括图形簇)。
- 多行编辑。
- 内存历史记录。
- 左,右:将光标移动到左侧或右侧。
- 上,下:滚动通过输入历史。
- Ctrl-W: 从光标位置删除到上一个空白字符之间的输入。
- Ctrl-U: 删除光标之前的输入。
- Ctrl-L: 清屏。
- Ctrl-左 / Ctrl-右:移动到上一个/下一个空白字符。
- Home: 跳到行的开头。
- 当“emacs”功能(默认启用)启用时,Ctrl-A具有相同的效果。
- End: 跳到行的末尾。
- 当“emacs”功能(默认启用)启用时,Ctrl-E具有相同的效果。
- Ctrl-C, Ctrl-D: 发送一个
Eof
事件。 - Ctrl-C: 发送一个
Interrupt
事件。 - 基于
crossterm
的event-stream
功能的可扩展设计。
示例
cargo run --example terminal_async
cargo run --example spinner
cargo run --example shell_async
如何使用此crate
使用 [TerminalAsync::try_new()
],这是大多数用例的主要入口点
- 要读取用户输入,调用 [
TerminalAsync::get_readline_event()
]。 - 您可以通过调用 [
TerminalAsync::clone_shared_writer()
] 获取一个SharedWriter
实例,您可以使用它来并发地向stdout
写入,使用std::write!
或std::writeln!
。 - 如果您使用
std::writeln!
,则不需要 [TerminalAsync::flush()
],因为\n
将刷新缓冲区。当缓冲区中没有\n
或您正在使用std::write!
时,您可能需要调用 [TerminalAsync::flush()
]。 - 您可以使用
TerminalAsync::println
和TerminalAsync::println_prefixed
方法轻松地将并发输出写入stdout
(SharedWriter
)。 - 您还可以通过
Readline
的Readline::readline
字段获取底层Readline
。以下是该结构的详细信息。对于大多数用例,您不需要这样做。
Readline
概述(请参阅该结构的文档以获取详细信息)
-
这是一个用于在终端并发输出行时读取终端输入的结构。它使用依赖注入,允许您提供可以用于
- 从用户读取输入,通常
crossterm::event::EventStream
。 - 将输出生成到原始终端,通常
std::io::Stdout
。
- 从用户读取输入,通常
-
通过调用 [
Readline::readline()
] 获取终端输入,它会在用户按下 Enter 键后返回每行完整的输入。 -
每个
Readline
实例都关联一个或多个SharedWriter
实例。写入相关SharedWriter
的行将被输出到原始终端。 -
调用 [
Readline::new()
] 创建Readline
实例和相关SharedWriter
。 -
调用 [
Readline::readline()
](很可能是在一个循环中)以从终端接收一行输入。用户可以按以下“输入编辑”中列出的键绑定编辑他们的输入。 -
在接收用户的一行输入后,如果要将它添加到历史记录中(以便用户在编辑后续行时可以检索它),请调用 [
Readline::add_history_entry()
]。 -
在
readline()
进程中进行时写入相关SharedWriter
的行将输出到输入行上方的屏幕上。 -
完成后,调用 [
crate::manage_shared_writer_output::flush_internal()
] 确保所有写入SharedWriter
的行都被输出。
[Spinner::try_start()
]
这将在等待长时间运行的任务完成时显示一个不确定的旋转器。显示此旋转器的目的是向用户指示程序仍在运行,尚未挂起或无响应。当其他任务并发产生输出时,此旋转器的输出不会被覆盖。旋转器的输出也不会覆盖其他任务的输出。它暂停了与一个 Readline
实例关联的所有 SharedWriter
实例的输出。`terminal_async.rs` 和 `spinner.rs` 示例都展示了这一点(cargo run --example terminal_async
和 cargo run --example spinner
)。
Spinner
还支持取消。一旦开始旋转器,Ctrl+C 和 Ctrl+D 将被导向旋转器以取消它。长时间运行的任务也可以检查旋转器的完成或取消,以确保它们作为用户取消的响应退出。查看 examples/terminal_async.rs
文件以了解如何使用此 API。
第三个变化是 [TerminalAsync::try_new()
] 现在接受包含 ANSI 转义序列的提示。以下是一个示例。
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(())
[tracing_setup::init()
]
这是一个方便的方法,用于使用 stdout
作为输出目标设置 Tokio tracing_subscriber
。此方法还确保使用 SharedWriter
对 stdout
进行并发写入。您还可以使用 TracingConfig
结构来定制跟踪设置的行为,通过选择是否将输出显示到 stdout
、stderr
或 SharedWriter
。默认情况下,显示和文件记录都是启用的。您还可以自定义日志级别以及日志文件的路径和前缀。
在 developerlife.com 的 YouTube 频道 上关于与 Naz 构建 crate 的视频系列
- 第 1 部分:为什么?
- 第 2 部分:是什么?
- 第 3 部分:重构并重命名 crate
- 第 4 部分:构建旋转器
- 第 5 部分:为旋转器添加颜色渐变动画
- 第6部分:发布crate和概述
- 测试播放列表
- 播放列表
为什么还需要另一个异步readline crate?
这个crate和repo是从rustyline-async分叉而来。然而,它已经大部分被重写和重新设计。以下是代码中做出的某些更改
- 从头开始重构整个crate,使其以与原始版本完全不同的方式运行。所有的底层思维模型都不同,更简单。主事件循环被重新编写。并使用一个任务来监控行通道,以便在多个
SharedWriter
和Readline
之间进行通信,以正确支持暂停和恢复以及其他控制功能。 - 放弃对除
tokio
之外的所有异步运行时的支持。重写所有代码。 - 放弃如
pin-project
、thingbuf
等crate,改用tokio
。重写所有代码。 - 放弃
simplelog
和log
依赖。添加对tokio-tracing
的支持。重写所有代码,并添加tracing_setup.rs
。 - 删除所有示例并创建新的示例,以模拟现实世界的CLI应用程序。
- 添加
spinner_impl
、readline_impl
和public_api
模块。 - 添加测试。
Rust中关于阻塞和线程取消的更多信息
关于Linux TTY和异步Rust的更多信息
许可证:Apache-2.0
依赖关系
~29–43MB
~589K SLoC