#read-line #spinner #async #async-io #read-input #user-input #terminal

r3bl_terminal_async

具有多行编辑器、并发显示任务输出和彩色动画旋转器的异步非阻塞 read_line 实现

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命令行界面

Download history 50/week @ 2024-04-29 177/week @ 2024-05-06 144/week @ 2024-05-20 8/week @ 2024-05-27 9/week @ 2024-06-03 4/week @ 2024-06-10 198/week @ 2024-07-08 25/week @ 2024-07-15 47/week @ 2024-07-29 137/week @ 2024-08-12

每月 184 次下载

Apache-2.0 许可

11MB
28K SLoC

r3bl_terminal_async

r3bl_terminal_async 库可以让您的 CLI 程序异步和交互式,而不阻塞主线程。您生成的任务可以使用它来并发地向显示输出写入、暂停和恢复。您还可以显示彩色动画旋转器 ⌛🌈 以便长时间运行的任务。使用它,您可以轻松创建美丽、强大且交互式的 REPL(读取执行打印循环)。

为什么要使用这个包

  1. 因为 read_line 是阻塞的。在 Rust 中,没有方法可以终止在 read_line 中阻塞的 OS 线程。为此,您必须退出进程(该进程的线程在 read_line 中阻塞)。

  2. 另一个烦恼是,当一个线程在 read_line() 中阻塞,并且您必须同时向 stdout 显示输出时,这会带来一些挑战。

    • 这是因为光标被 read_line() 移动并阻塞。
    • 当另一个线程/任务并发地向 stdout 写入时,它假定光标位于新行的第0行。
    • 这导致输出看起来不好。

这是一个视频,展示了本库中的 terminal_asyncspinner 示例在实际运行中的情况

terminal_async_video

特性

  1. 逐行从终端读取用户输入,同时您的程序并发地向同一终端写入行。一个 Readline 实例可以用于创建许多异步 stdout 写入器 ([SharedWriter]),它们可以并发地向终端写入。对于大多数用户来说,TerminalAsync 结构体是使用此库的最简单方式。您很少需要直接访问底层的 ReadlineSharedWriter。但如果有需要,您也可以这样做。 SharedWriter 可以被克隆,并且是线程安全的。但是,每个 Readline 实例只有一个 TerminalAsync 实例。

  2. 生成一个旋转器(不确定的进度指示器)。这个旋转器可以与您的程序并发工作。当 Spinner 处于活动状态时,它会自动暂停与一个 Readline 实例关联的所有 SharedWriter 实例的输出。通常,被创建的任务会克隆自己的 SharedWriter 来生成输出。当您想在等待长时间运行的任务完成时显示旋转器时,这很有用。请运行示例来查看其运行情况,运行 cargo run --example terminal_async。然后输入 starttask1,按回车。然后输入 spinner,按回车。

  3. 使用支持并发stout写入的tokio tracing。如果您选择将日志记录到stdout,则将使用此crate中的并发版本(SharedWriter)。这确保了即使是对stdout的跟踪日志,也能支持并发输出。

  4. 您还可以连接自己的终端,如stdoutstderr或任何实现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频道

暂停和恢复支持

暂停和恢复功能是通过以下方式实现的

Readline::newTerminalAsync::try_new创建一个line_channel来发送和接收[LineStateControlSignal]。

  1. 此通道的发送端被移动到[SharedWriter]。因此,任何[SharedWriter]都可以用来向通道发送[LineStateControlSignal],这些信号将在启动的任务中处理,仅为此目的,在Readline::new中。这是在暂停和恢复之间切换的主要机制。在TerminalAsync::pauseTerminalAsync::resume中提供了某些辅助函数,尽管您可以直接通过SharedWriter::line_state_control_channel_sender将信号直接发送到通道的发送端。
  2. tokio::sync::mpsc::channel的接收端被移动到由Readline::new创建的任务。这是发送端(如上所述)发送信号时实际工作的地方。

当[Readline]被挂起时,无法进行输入,只有 Ctrl+CCtrl+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 事件。
  • 基于 crosstermevent-stream 功能的可扩展设计。

示例

cargo run --example terminal_async
cargo run --example spinner
cargo run --example shell_async

如何使用此crate

使用 [TerminalAsync::try_new()],这是大多数用例的主要入口点

  1. 要读取用户输入,调用 [TerminalAsync::get_readline_event()]。
  2. 您可以通过调用 [TerminalAsync::clone_shared_writer()] 获取一个 SharedWriter 实例,您可以使用它来并发地向 stdout 写入,使用 std::write!std::writeln!
  3. 如果您使用 std::writeln!,则不需要 [TerminalAsync::flush()],因为 \n 将刷新缓冲区。当缓冲区中没有 \n 或您正在使用 std::write! 时,您可能需要调用 [TerminalAsync::flush()]。
  4. 您可以使用 TerminalAsync::printlnTerminalAsync::println_prefixed 方法轻松地将并发输出写入 stdout (SharedWriter)。
  5. 您还可以通过 ReadlineReadline::readline 字段获取底层 Readline。以下是该结构的详细信息。对于大多数用例,您不需要这样做。

Readline 概述(请参阅该结构的文档以获取详细信息)

  • 这是一个用于在终端并发输出行时读取终端输入的结构。它使用依赖注入,允许您提供可以用于

    1. 从用户读取输入,通常 crossterm::event::EventStream
    2. 将输出生成到原始终端,通常 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_asynccargo run --example spinner)。

Spinner 还支持取消。一旦开始旋转器,Ctrl+CCtrl+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。此方法还确保使用 SharedWriterstdout 进行并发写入。您还可以使用 TracingConfig 结构来定制跟踪设置的行为,通过选择是否将输出显示到 stdoutstderrSharedWriter。默认情况下,显示和文件记录都是启用的。您还可以自定义日志级别以及日志文件的路径和前缀。

developerlife.comYouTube 频道 上关于与 Naz 构建 crate 的视频系列

为什么还需要另一个异步readline crate?

这个crate和repo是从rustyline-async分叉而来。然而,它已经大部分被重写和重新设计。以下是代码中做出的某些更改

  • 从头开始重构整个crate,使其以与原始版本完全不同的方式运行。所有的底层思维模型都不同,更简单。主事件循环被重新编写。并使用一个任务来监控行通道,以便在多个SharedWriterReadline之间进行通信,以正确支持暂停和恢复以及其他控制功能。
  • 放弃对除tokio之外的所有异步运行时的支持。重写所有代码。
  • 放弃如pin-projectthingbuf等crate,改用tokio。重写所有代码。
  • 放弃simpleloglog依赖。添加对tokio-tracing的支持。重写所有代码,并添加tracing_setup.rs
  • 删除所有示例并创建新的示例,以模拟现实世界的CLI应用程序。
  • 添加spinner_implreadline_implpublic_api模块。
  • 添加测试。

Rust中关于阻塞和线程取消的更多信息

关于Linux TTY和异步Rust的更多信息

许可证:Apache-2.0

依赖关系

~29–43MB
~589K SLoC