26 个版本

0.1.26 2024 年 4 月 16 日
0.1.25 2024 年 1 月 15 日
0.1.23 2023 年 12 月 23 日
0.1.21 2023 年 10 月 22 日
0.1.13 2023 年 9 月 24 日

#71命令行界面

Download history 4607/week @ 2024-04-28 4061/week @ 2024-05-05 2926/week @ 2024-05-12 3009/week @ 2024-05-19 3468/week @ 2024-05-26 2593/week @ 2024-06-02 2898/week @ 2024-06-09 3273/week @ 2024-06-16 4005/week @ 2024-06-23 3767/week @ 2024-06-30 3933/week @ 2024-07-07 2744/week @ 2024-07-14 2536/week @ 2024-07-21 2208/week @ 2024-07-28 1792/week @ 2024-08-04 1321/week @ 2024-08-11

8,154 每月下载量
5 个 Crates 中使用 (直接使用 3 个)

Apache-2.0

340KB
5K SLoC

r3bl_tuify

什么是 r3bl_tuify?

r3bl_tuify 是一个 Rust crate,允许您为您的 CLI 应用添加简单的交互功能。

r3bl_tuify crate 可以以两种方式使用

  1. 作为库。如果您想在用 Rust 编写的 CLI 应用中添加简单的交互功能,这将很有用。您可以在 examples 文件夹中的 main_interactive.rs 文件中看到示例。您可以使用 cargo run --example main_interactive 或使用 nu nu run examples 来运行它。
  2. 作为二进制文件。如果您想将此 crate 作为命令行工具使用,这将很有用。二进制目标名为 rt

如何将其作为库使用?

这是此 crate 的库目标的演示。

https://github.com/r3bl-org/r3bl-open-core/assets/22040032/46850043-4973-49fa-9824-58f32f21e96e

要将 crate 作为库安装,请将以下内容添加到您的 Cargo.toml 文件中

[dependencies]
r3bl_tuify = "0.1.24" # Get the latest version at the time you get this.
r3bl_rs_utils_core = "0.9.11" # Get the latest version at the time you get this.

以下示例说明了如何将其作为库使用。执行UI渲染工作的函数称为select_from_list。它接受一个项目列表,并返回选定的项目或项目(取决于选择模式)。如果用户没有选择任何内容,它将返回None。该函数还接受显示的最大高度和宽度以及选择模式(单选或多选)。

它在 macOS、Linux 和 Windows 上运行,并且了解每个终端颜色输出限制。例如,在 Windows 上,它使用 Windows API 进行键盘输入。在 macOS Terminal.app 中,它将颜色输出限制为 256 色调板。

APIs

我们提供了 2 个 API

  • select_from_list:如果您想显示具有单行标题的项目列表,请使用此 API。
  • select_from_list_with_multi_line_header:如果您想显示具有多行标题的项目列表,请使用此 API。

select_from_list

使用此 API 如果您想显示具有单行标题的项目列表。

image

select_from_list签名

fn select_from_list(
    header: String,
    items: Vec<String>,
    max_height_row_count: usize,
    // If you pass 0, then the width of your terminal gets set as max_width_col_count.
    max_width_col_count: usize,
    selection_mode: SelectionMode,
    style: StyleSheet,
) -> Option<Vec<String>>
use r3bl_rs_utils_core::*;
use r3bl_tuify::*;
use std::io::Result;

fn main() -> Result<()> {
    // Get display size.
    let max_height_row_count: usize = 5;
    let user_input = select_from_list(
        "Select an item".to_string(),
        [
            "item 1", "item 2", "item 3", "item 4", "item 5", "item 6", "item 7", "item 8",
            "item 9", "item 10",
        ]
        .iter()
        .map(|it| it.to_string())
        .collect(),
        max_height_row_count,
        0,
        SelectionMode::Single,
        StyleSheet::default(),
    );

    match &user_input {
        Some(it) => {
            println!("User selected: {:?}", it);
        }
        None => println!("User did not select anything"),
    }

    Ok(())
}

select_from_list_with_multi_line_header

如果您想显示具有多行标题的项目列表,请使用select_from_list_with_multi_line_header API。前 5 行都是多行标题的一部分。

image

select_from_list_with_multi_line_header签名

fn select_from_list_with_multi_line_header(
    multi_line_header: Vec<Vec<AnsiStyledText<'_>>>,
    items: Vec<String>,
    maybe_max_height_row_count: Option<usize>,
    // If you pass None, then the width of your terminal gets used.
    maybe_max_width_col_count: Option<usize>,
    selection_mode: SelectionMode,
    style: StyleSheet,
) -> Option<Vec<String>>
use std::{io::Result, vec};

use r3bl_ansi_color::{AnsiStyledText, Color, Style as RStyle};
use r3bl_rs_utils_core::*;
use r3bl_tuify::{
    components::style::StyleSheet,
    select_from_list_with_multi_line_header,
    SelectionMode,
};

fn multi_select_instructions() -> Vec<Vec<AnsiStyledText<'static>>> {
    let up_and_down = AnsiStyledText {
        text: " Up or down:",
        style: &[
            RStyle::Foreground(Color::Rgb(9, 238, 211)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };
    let navigate = AnsiStyledText {
        text: "     navigate",
        style: &[
            RStyle::Foreground(Color::Rgb(94, 103, 111)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };

    let line_1 = vec![up_and_down, navigate];

    let space = AnsiStyledText {
        text: " Space:",
        style: &[
            RStyle::Foreground(Color::Rgb(255, 216, 9)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };
    let select = AnsiStyledText {
        text: "          select or deselect item",
        style: &[
            RStyle::Foreground(Color::Rgb(94, 103, 111)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };

    let line_2 = vec![space, select];

    let esc = AnsiStyledText {
        text: " Esc or Ctrl+C:",
        style: &[
            RStyle::Foreground(Color::Rgb(255, 132, 18)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };
    let exit = AnsiStyledText {
        text: "  exit program",
        style: &[
            RStyle::Foreground(Color::Rgb(94, 103, 111)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };

    let line_3 = vec![esc, exit];
    let return_key = AnsiStyledText {
        text: " Return:",
        style: &[
            RStyle::Foreground(Color::Rgb(234, 0, 196)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };
    let confirm = AnsiStyledText {
        text: "         confirm selection",
        style: &[
            RStyle::Foreground(Color::Rgb(94, 103, 111)),
            RStyle::Background(Color::Rgb(14, 17, 23)),
        ],
    };
    let line_4 = vec![return_key, confirm];
    vec![line_1, line_2, line_3, line_4]
}

fn main() -> Result<()> {
    let header = AnsiStyledText {
        text: " Please select one or more items. This is a really long heading that just keeps going and if your terminal viewport is small enough, this heading will be clipped",
        style: &[
            RStyle::Foreground(Color::Rgb(171, 204, 242)),
            RStyle::Background(Color::Rgb(31, 36, 46)),
        ],
    };

    let mut instructions_and_header: Vec<Vec<AnsiStyledText>> = multi_select_instructions();
    instructions_and_header.push(vec![header]);

    let user_input = select_from_list_with_multi_line_header(
        instructions_and_header,
        [
            "item 1 of 13",
            "item 2 of 13",
            "item 3 of 13",
            "item 4 of 13",
            "item 5 of 13",
            "item 6 of 13",
            "item 7 of 13",
            "item 8 of 13",
            "item 9 of 13",
            "item 10 of 13",
            "item 11 of 13",
            "item 12 of 13",
            "item 13 of 13",
        ]
        .iter()
        .map(|it| it.to_string())
        .collect(),
        Some(6),
        None,
        SelectionMode::Multiple,
        StyleSheet::default(),
    );
    match &user_input {
        Some(it) => {
            println!("User selected: {:?}", it);
        }
        None => println!("User did not select anything"),
    }
   Ok(())
}

如何将其作为二进制文件使用?

以下是一个展示此crate二进制目标效果的演示。

https://github-production-user-asset-6210df.s3.amazonaws.com/2966499/267427392-2b42db72-cd62-4ea2-80ae-ccc01008190c.mp4

您可以使用以下命令安装二进制文件:使用cargo install r3bl_tuify(来自crates.io)。或者,从源代码安装:cargo install --path .rt是一个命令行工具,允许您从通过stdin传入它的列表中选择一个选项。它支持stdinstdout管道。

以下是可以接受的命令行参数

  1. -s--selection-mode - 允许您选择选择模式。有两种选项:singlemultiple
  2. -c--command-to-run-with-selection - 允许您指定与选定项目一起运行的命令。例如,"echo foo \'%\'"将简单地打印每个选定的项目。
  3. -t--tui-height - 可选地允许您设置 TUI 的高度。默认为 5。

交互式用户体验

通常,CLI 应用程序不是交互式的。您可以将其传递命令、子命令、选项和参数,但如果您出错,则会出现错误,并且必须从头开始。这种“对话”风格的界面可能需要大量的尝试和错误才能得到所需的结果。

以下是一个使用许多子命令、选项和参数的示例。

cat TODO.todo | cargo run -- select-from-list \
    --selection-mode single \
    --command-to-run-with-each-selection "echo %"

以下是一个展示其效果的演示视频。

https://github.com/r3bl-org/r3bl-open-core/assets/2966499/c9b49bfb-b811-460e-a844-fe260eaa860a

这是做什么的?

  1. cat TODO.todo - 将TODO.todo文件的 内容打印到stdout
  2. | - 将上一个命令的输出传递到下一个命令,即 rt(即,此crate的二进制目标)。
  3. cargo run -- - 在目标文件夹中运行 rt 调试二进制。
  4. select-from-list - 使用 select-from-list 子命令运行 rt 二进制。此子命令需要两个参数:--selection-mode--command-to-run-with-each-selection。哇!这有点长!
  5. --selection-mode single - 将选择模式设置为 single。这意味着用户只能从列表中选择一个项目。哪个列表?就是从上一个命令管道传递进来的列表(即,cat TODO.todo)。
  6. --command-to-run-with-each-selection "echo %" - 设置与每个选择一起运行的命令。在这种情况下,它是 echo %。这里的 % 是占位符,用于表示所选的项目。所以如果用户选择了 item 1,那么将运行的命令是 echo item 1echo 命令只是将所选的项目打印到 stdout

这要记住的东西确实很多。使用 clap 提供友好的命令行帮助很有用,但你还是需要正确设置一些东西才能使该命令正常工作。

不一定要这样。二进制可以交互式地使用 clap 指定一些子命令和参数。这不一定是全有或全无的方法。我们可以两者兼得。以下视频展示了当

  1. --selection-mode--command-to-run-with-each-selection 未通过命令行传递时会发生什么。

    cat TODO.todo | cargo run -- select-from-list
    

    以下是可能出现的三种情况:

  2. --selection-mode 未通过命令行传递。因此,它只交互式地提示用户提供此信息。同样,如果用户不提供此信息,则应用程序退出并提供帮助信息。

    cat TODO.todo | cargo run -- select-from-list --command-to-run-with-each-selection "echo %"
    

    https://github.com/r3bl-org/r3bl-open-core/assets/2966499/be65d9b2-575b-47c0-8291-110340bd2fe7

  3. --command-to-run-with-each-selection 不会在命令行中传递。因此,它只交互式地提示用户输入此信息。同样,如果用户没有提供此信息,应用程序会退出并显示帮助信息。

    cat TODO.todo | cargo run -- select-from-list --selection-mode single
    

    https://github.com/r3bl-org/r3bl-open-core/assets/2966499/d8d7d419-c85e-4c10-bea5-345aa31a92a3

路径

您可以使用这个相对简单的程序进行许多不同的执行路径。以下是一个列表。

  • 快乐路径

    1. rt - 打印帮助信息。
    2. cat Cargo.toml | rt -s single -c "echo foo \'%\'" - 从 stdin 中读取输入,并将其打印到 stdout
    3. cat Cargo.toml | rt -s multiple -c "echo foo \'%\'" - 从 stdin 中读取输入,并将其打印到 stdout
  • 不快乐的路径(stdin 未连接到管道,或 stdout 连接到管道)

    1. rt -s single - 期望 stdin 连接到管道,并打印帮助信息。
    2. rt -s multiple - 期望 stdin 连接到管道,并打印帮助信息。
    3. ls -la | rt -s single | xargs -0 - 不期望 stdout 连接到管道,并打印帮助信息。
    4. ls -la | rt -s multiple | xargs -0 - 不期望 stdout 连接到管道,并打印帮助信息。

由于 Unix 管道的实现方式,无法将此命令的 stdout 连接到其他任何东西。Unix 管道是非阻塞的。因此,无法在管道“中途”停止管道。这就是为什么当 stdout 连接到管道时,rt 会显示错误。无法将 rtstdout 连接到另一个命令。相反,rt 二进制文件简单地接受一个将在用户做出选择后运行的命令。使用所选项并将它们应用于此命令。

组件样式

选择 3 个内置样式之一

内置样式称为 defaultsea_foam_stylehot_pink_style。您可以在 style.rs 文件(tuify/src/components/style.rs)中找到它们。

默认样式

image

sea_foam_style

image

hot_pink_style

image

要使用内置样式之一,只需将其作为参数传递给 select_from_list 函数。

use r3bl_rs_utils_core::*;
use r3bl_tuify::*;
use std::io::Result;

fn main() -> Result<()> {
    // 🎨 Uncomment the lines below to choose the other 2 built-in styles.
    // let default_style = StyleSheet::default();
    // let hot_pink_style = StyleSheet::hot_pink_style();
    let sea_foam_style = StyleSheet::sea_foam_style();

    let max_width_col_count: usize = get_size().map(|it| it.col_count).unwrap_or(ch!(80)).into();
    let max_height_row_count: usize = 5;

    let user_input = select_from_list(
        "Select an item".to_string(),
        [
            "item 1", "item 2", "item 3", "item 4", "item 5", "item 6", "item 7", "item 8",
            "item 9", "item 10",
        ]
        .iter()
        .map(|it| it.to_string())
        .collect(),
        max_height_row_count,
        max_width_col_count,
        SelectionMode::Single,
        sea_foam_style,  // 🖌️ or default_style or hot_pink_style
    );

    match &user_input {
        Some(it) => {
            println!("User selected: {:?}", it);
        }
        None => println!("User did not select anything"),
    }

    Ok(())
}

创建您的样式

要创建您的样式,您需要创建一个 StyleSheet 结构体,并将其作为参数传递给 select_from_list 函数。

use std::io::Result;
use r3bl_ansi_color::{AnsiStyledText, Color, Style as RStyle};
use r3bl_tuify::{components::style::{Style, StyleSheet},
                 select_from_list,
                 SelectionMode};

fn main() -> Result<()> {
   // This is how you can define your custom style.
   // For each Style struct, you can define different style overrides.
   // Please take a look at the Style struct to see what you can override.
   let my_custom_style = StyleSheet {
      focused_and_selected_style: Style {
            fg_color: Color::Rgb(255, 244, 0),
            bg_color: Color::Rgb(15, 32, 66),
            ..Style::default()
      },
      focused_style: Style {
            fg_color: Color::Rgb(255, 244, 0),
            ..Style::default()
      },
      unselected_style: Style { ..Style::default() },
      selected_style: Style {
            fg_color: Color::Rgb(203, 170, 250),
            bg_color: Color::Rgb(15, 32, 66),
            ..Style::default()
      },
      header_style: Style {
            fg_color: Color::Rgb(171, 204, 242),
            bg_color: Color::Rgb(31, 36, 46),
            ..Style::default()
      },
   };

   // Then pass `my_custom_style` as the last argument to the `select_from_list` function.
   let user_input = select_from_list(
      "Multiple select".to_string(),
      ["item 1 of 3", "item 2 of 3", "item 3 of 3"]
         .iter()
         .map(|it| it.to_string())
         .collect(),
      6, // max_height_row_count
      80, // max_width_col_count
      SelectionMode::Multiple,
      my_custom_style,
   );

   match &user_input {
      Some(it) => {
         println!("User selected: {:?}", it);
      }
      None => println!("User did not select anything"),
   }
   Ok(())
}

构建、运行、测试任务

先决条件

🌠 为了使这些功能正常工作,您需要在您的系统上安装 Rust 工具链、nucargo-watchbatflamegraph。以下是说明

  1. 使用 rustup 安装 Rust 工具链,按照 这里 的说明进行。
  2. 使用以下命令安装 cargo-watch
  3. 使用以下命令安装 flamegraph
  4. 使用以下命令安装 bat
  5. 使用以下命令在您的系统上安装 nu shell:。它支持 Linux、macOS 和 Windows。

Nu 脚本来构建、运行、测试等。

进入 tuify 文件夹,并运行以下命令。这些命令在 ./run 文件夹中定义。

命令 描述
nu run examples 运行 ./examples 文件夹中的示例
nu run piped 运行具有管道输入的二进制文件
nu run build 构建
nu run clean 清理
nu run all 全部
nu run examples--flamegraph-分析 使用 flamegraph 分析运行示例
nu run test 运行测试
nu run clippy 运行 clippy
nu run docs 构建文档
nu run serve-文档 通过 VSCode Remote SSH 会话提供文档服务
nu run upgrade-依赖 升级依赖
nu run rustfmt 运行 rustfmt

以下命令将监视源文件夹中的更改并重新运行

命令 描述
nu run watch-examples 监视运行示例
nu run watch-all-tests 监视所有测试
nu run watch-one-test<test_name> 监视单个测试
nu run watch-clippy 监视 clippy
nu run watch-macro-expansion-one-test<test_name> 监视单个测试的宏展开

在存储库的顶层文件夹中还有一个 run 脚本。它旨在在 CI/CD 环境中使用,或者以交互模式使用,用户将被提示输入。

命令 描述
nu run all 一次性运行所有测试、代码质量检查、格式化等。用于 CI/CD。
nu run build-full 这将构建 Rust 工作空间中的所有 crate。它将安装所有必需的先决工具(如 install-cargo-tools 所做的)并清除 cargo 缓存,清理并执行一个真正干净的构建。
nu run install-cargo-tools 这将安装所有必需的先决工具,以用于此 crate(例如,cargo-denyflamegraph 将一次性安装)
nu run check-licenses 使用 cargo-deny 检查 Rust 工作空间中使用的所有许可证

参考

CLI 用户体验指南

ANSI 转义代码

依赖项

~17–29MB
~352K SLoC