#prompt #key-input #interactive #console #ask #cli

promptuity

Promptuity 是一个提供交互式提示的库

5 个版本

0.0.5 2024年1月14日
0.0.4 2024年1月14日
0.0.3 2024年1月8日
0.0.2 2024年1月8日
0.0.1 2024年1月8日

#143 in 命令行界面

Download history 22/week @ 2024-05-20 23/week @ 2024-05-27 14/week @ 2024-06-03 16/week @ 2024-06-10 191/week @ 2024-06-17 62/week @ 2024-06-24 24/week @ 2024-07-01 1/week @ 2024-07-08 10/week @ 2024-07-15 38/week @ 2024-07-22 35/week @ 2024-07-29 20/week @ 2024-08-05

104 次每月下载
4 crates 中使用

MIT 许可证

1.5MB
3K SLoC

Promptuity

Promptuity = 提示 + 智慧

GitHub Actions Workflow Status Crates.io Version docs.rs MIT LICENSE

Promptuity 是一个提供交互式提示的库。它高度可扩展,允许您从头开始构建您自己的提示。它为各种项目带来了 智慧

目录

概念

  • 不易,但简单
    • 避免具有隐性行为的 API,旨在提供尽可能透明的 API。
    • 启动提示所需的代码量可能比其他库多。
  • 🔨 可扩展
    • 您可以自定义内置提示或从头开始构建提示。
    • 内置提示是最小的,假设提示需求因项目而异。
  • 💅 美观
    • 提供两种内置主题。
    • 主题也可以完全自定义,以适应您的理想。

快速开始

Quick Start DEMO

以下是一些基本用法。

use promptuity::prompts::{Confirm, Input, Select, SelectOption};
use promptuity::themes::FancyTheme;
use promptuity::{Error, Promptuity, Term};

fn main() -> Result<(), Error> {
    let mut term = Term::default();
    let mut theme = FancyTheme::default();
    let mut p = Promptuity::new(&mut term, &mut theme);

    p.term().clear()?;

    p.with_intro("Survey").begin()?;

    let name = p.prompt(Input::new("Please enter your username").with_placeholder("username"))?;

    let _ = p.prompt(Confirm::new("Are you a full-time software developer?").with_default(true))?;

    let _ = p.prompt(
        Select::new(
            "Select your primary programming language",
            vec![
                SelectOption::new("Rust", "rust"),
                SelectOption::new("Go", "go"),
                SelectOption::new("C++", "cpp"),
                SelectOption::new("C", "c"),
                SelectOption::new("TypeScript", "typescript"),
                SelectOption::new("JavaScript", "javascript"),
                SelectOption::new("Deno", "deno"),
                SelectOption::new("Python", "python"),
                SelectOption::new("Java", "java"),
                SelectOption::new("Dart", "dart"),
                SelectOption::new("Other", "other"),
            ],
        )
        .with_hint("Submit with Space or Enter."),
    )?;

    p.with_outro(format!("Thank you for your response, {}!", name))
        .finish()?;

    Ok(())
}

示例

如果您想看到更多示例,请参阅 示例 目录。

文档

请参阅 文档

提示

promptuity::prompts 提供五个内置提示。
要实现您自己的提示,请参阅 构建您自己的提示 部分。

输入

Input Demo

用于一般文本输入的提示。

let name = p.prompt(
    Input::new("What is your accout name?")
        .with_placeholder("username")
        .with_hint("Only alphanumeric characters are allowed.")
        .with_validator(|value: &String| {
            if value.chars().all(|c| c.is_alphanumeric()) {
                Ok(())
            } else {
                Err("Invalid format".into())
            }
        }),
)?;

密码

Password Demo

不显示输入的文本输入提示。

let secret = p.prompt(
    Password::new("Set a password for your account")
        .with_hint("Please enter more than 6 alphanumeric characters.")
        .with_validator(|value: &String| {
            if value.len() < 6 {
                Err("Password must be at least 6 characters long".into())
            } else {
                Ok(())
            }
        }),
)?;

数字

Number Demo

仅输入整数值的提示。

let age = p.prompt(Number::new("How old are you?").with_min(0).with_max(120))?;

选择

Select Demo

从选项列表中选择单个元素的提示。

let color = p.prompt(
    Select::new(
        "What is your favorite color?",
        vec![
            SelectOption::new("Red", "#ff0000"),
            SelectOption::new("Green", "#00ff00").with_hint("recommended"),
            SelectOption::new("Blue", "#0000ff"),
        ],
    )
    .as_mut(),
)?;

多选

MultiSelect Demo

从选项列表中选择多个元素的提示。

let color = p.prompt(
    MultiSelect::new(
        "What are your favorite colors?",
        vec![
            MultiSelectOption::new("Red", "#ff0000"),
            MultiSelectOption::new("Green", "#00ff00").with_hint("recommended"),
            MultiSelectOption::new("Blue", "#0000ff"),
        ],
    )
    .as_mut(),
)?;

确认

Confirm Demo

输入 Yes/No 选择的提示。

let like = p.prompt(
    Confirm::new("Do you like dogs?")
        .with_hint("This is just a sample prompt :)")
        .with_default(true),
)?;

自动完成

[注意] 自动补全不是一个内置功能。这是因为模糊匹配和快捷键的行为因项目而异。
虽然不是内置功能,但提供了一个参考实现,位于 examples/autocomplete.rs。请根据您项目的需求进行修改。

主题

Promptuity提供了两种不同的内置主题。
要实现您自己的主题,请参阅构建自己的主题部分。

MinimalTheme

MinimalTheme与Inquirer类似。它提供了一种紧凑的用户界面。

MinimalTheme Screenshot

use promptuity::themes::MinimalTheme;

fn main() {
    let mut theme = MinimalTheme::default();
    // ...
}

FancyTheme

FancyTheme与clack类似。它提供了一种丰富的用户界面。

FancyTheme Screenshot

use promptuity::themes::FancyTheme;

fn main() {
    let mut theme = FancyTheme::default();
    // ...
}

自定义

本节提供了如何构建原始提示和主题的指导。

构建您自己的提示

通过实现Prompt特质,可以创建原始提示。通过实现三个生命周期方法,您可以使用Promptuity::prompt构建可用的提示。

Promptuity提示由以下元素组成

项目 描述
信息 显示提示的问题内容。
输入 单行项目,接受用户按键输入。
正文 多行项目,接受用户按键输入。
提示 显示帮助提示信息。
  • 接受单行输入的提示,如InputPassword,不使用正文
  • 不接受输入的提示,如SelectMultiSelect,不使用输入

在构建您的提示时请注意这些要点。

0. 设置自定义提示

让我们以与Confirm类似的自定义提示的实现为例。

use promptuity::Prompt;

struct CustomConfirm {
    message: String,
    hint: Option<String>,
    value: bool,
}

impl Prompt for CustomConfirm {
    type Output = bool;

    // TODO
}

定义一个包含消息、提示和值的结构。在Output中指定最终结果类型。

首先,让我们实现按键输入的接收。

1. 接收按键输入

Prompt::handle方法中处理按键输入。

例如,让我们实现它,以便按下y为是,按下n为否,最终确定结果。

use promptuity::event::{KeyCode, KeyModifiers};
use promptuity::{Prompt, PromptState};

// ...

impl Prompt for CustomConfirm {
    // ...

    fn handle(&mut self, code: KeyCode, modifiers: KeyModifiers) -> PromptState {
        match (code, modifiers) {
            (KeyCode::Enter, _) => PromptState::Submit,
            (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => PromptState::Cancel,
            (KeyCode::Char('y'), KeyModifiers::NONE) | (KeyCode::Char('Y'), KeyModifiers::NONE) => {
                self.value = true;
                PromptState::Submit
            }
            (KeyCode::Char('n'), KeyModifiers::NONE) | (KeyCode::Char('N'), KeyModifiers::NONE) => {
                self.value = false;
                PromptState::Submit
            }
            _ => PromptState::Active,
        }
    }
}

您可以自由组合按键码和修饰符,以构建符合特定要求的复杂提示。

[重要] 通常,提示会被Ctrl + C中断,但Promptuity不会自动处理。
如果省略了实现,则会导致提示无法中断,从而影响可用性。因此,在构建原始提示时,您必须自己明确实现中断过程。

2. 渲染提示

Prompt::render方法中构建渲染内容。以下是一个仅使用Input而没有Body的简单示例。

use promptuity::event::{KeyCode, KeyModifiers};
use promptuity::{Prompt, PromptState, RenderPayload};

// ...

impl Prompt for CustomConfirm {
    // ...

    fn render(&mut self, state: &PromptState) -> Result<RenderPayload, String> {
        let payload = RenderPayload::new(self.message.clone(), self.hint.clone(), None);

        match state {
            PromptState::Submit => {
                let raw = if self.value { "Yes" } else { "No" };
                Ok(payload.input(PromptInput::Raw(raw.into())))
            }

            PromptState::Cancel => Ok(payload),

            _ => Ok(payload.input(PromptInput::Raw("Y/n"))),
        }
    }
}

根据Prompt::handle返回的PromptState确定适当的渲染内容。上述实现实现了以下要求

  • 结果显示YesNo
  • 如果提示被中断,只显示消息。
  • 在用户输入接收期间,它显示Y/n

3. 返回提交结果

这是构建自定义提示的最后一步。

实现 Prompt::submit 方法,该方法返回接收到的键入的最终值。

impl Prompt for CustomConfirm {
    // ...

    fn submit(&mut self) -> Self::Output {
        self.value
    }
}

Prompt::submit 是一个生命周期方法,在 Prompt::handle 返回 PromptState::Submit 后立即调用。


处理键入和根据输入状态进行渲染是构建提示的基础。

对于构建更复杂的提示,可以参考 examples/autocomplete.rs

构建您自己的主题

就像提示一样,你可以通过实现 Theme 特性来构建一个原始的主题。

有关完整示例,请参阅 examples/custom_theme.rs

错误处理

所有错误都汇总到 promptuity::Error

在许多情况下,需要单独处理提示中断。中断通常发生在用户输入接收期间,通常通过像 Ctrl + CESC 这样的输入。

use promptuity::prompts::Input;
use promptuity::themes::MinimalTheme;
use promptuity::{Error, Promptuity, Term};

fn ask() -> Result<String, Error> {
    let mut term = Term::default();
    let mut theme = MinimalTheme::default();
    let mut p = Promptuity::new(&mut term, &mut theme);

    p.begin()?;
    let name = p.prompt(Input::new("Please enter your username").with_placeholder("username"))?;
    p.finish()?;

    Ok(name)
}

fn main() {
    match ask() {
        Ok(name) => println!("Hello, {}!", name),
        Err(Error::Cancel) => {}
        Err(e) => eprintln!("Error: {}", e),
    }
}

提示中断可以处理为 Error::Cancel。在上面的示例中,发生中断时不会显示任何消息。

测试

通常,涉及用户输入的验证是昂贵的。由于 Promptuity 将终端行为作为 Terminal 特性实现,因此很容易用假值替换。

用于 Promptuity 集成测试的模拟键入输入的 Terminal,可以在 Term 中参考。

以下是使用假 Terminal 测试提示的示例。

#[test]
fn test_prompts() {
    let mut term = fake_term::Term::new(&[
        (KeyCode::Char('a'), KeyModifiers::NONE),
        (KeyCode::Char('b'), KeyModifiers::NONE),
        (KeyCode::Char('c'), KeyModifiers::NONE),
        (KeyCode::Enter, KeyModifiers::NONE),
    ]);

    let mut theme = MinimalTheme::default();

    let result = {
        let mut p = Promptuity::new(&mut term, &mut theme);
        p.prompt(Input::new("Input Message").as_mut()).unwrap()
    };

    let output = term.output();

    assert_eq!(result, String::from("abc"));

    // This is an example of performing snapshots on outputs.
    insta::with_settings!({ omit_expression => true }, {
        insta::assert_snapshot!(output);
    });
}

替代方案

Rust 生态系统包含许多优秀的crate。

灵感来源

Promptuity 的各种提示和设计受到了这些项目的极大启发。我们对他们的开发表示衷心的感谢。

贡献

请参阅 CONTRIBUTING.md

变更日志

请参阅 CHANGELOG.md

许可证

MIT © wadackel

依赖项

~1.6–7MB
~41K SLoC