An open API service indexing awesome lists of open source software.

https://github.com/wadackel/promptuity

Promptuity is a library that provides interactive prompts.
https://github.com/wadackel/promptuity

ask cli command-line command-line-tool console interactive prompt rust

Last synced: 10 months ago
JSON representation

Promptuity is a library that provides interactive prompts.

Awesome Lists containing this project

README

          

Promptuity


Promptuity = Prompt + Ingenuity



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


Promptuity is a library that provides interactive prompts. It is highly extensible, allowing you to build your original prompts from scratch. It brings ingenuity to various projects.

## Table Of Contents

- [Concept](#concept)
- [Quick Start](#quick-start)
- [Examples](#examples)
- [Documentation](#documentation)
- [Prompts](#prompts)
- [Input](#input)
- [Password](#password)
- [Number](#number)
- [Select](#select)
- [MultiSelect](#multiselect)
- [Confirm](#confirm)
- [Autocomplete](#autocomplete)
- [Themes](#themes)
- [MinimalTheme](#minimaltheme)
- [FancyTheme](#fancytheme)
- [Customize](#customize)
- [Build your own Prompt](#build-your-own-prompt)
- [Build your own Theme](#build-your-own-theme)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [Alternatives](#alternatives)
- [Inspired](#inspired)
- [Contributing](#contributing)
- [CHANGELOG](#changelog)
- [License](#license)

## Concept

- :zap: **Not easy, But simple**
- Avoids APIs with implicit behavior, aiming to provide as transparent APIs as possible.
- The amount of code required to start a prompt may be more compared to other libraries.
- :hammer: **Extensible**
- You can customize built-in prompts or build your prompts from scratch.
- The built-in prompts are minimal, assuming that prompt requirements vary by project.
- :nail_care: **Beautiful**
- Offers two types of built-in Themes.
- Themes can also be fully customized to fit your ideal.

## Quick Start

![Quick Start DEMO](./assets/quick_start.gif)

The basic usage is as follows.

```rust
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(())
}
```

## Examples

If you want to see more examples, please refer to the [examples](./examples/) directory.

## Documentation

Please refer to the [documentation](https://docs.rs/promptuity).

## Prompts

[`promptuity::prompts`](https://docs.rs/promptuity/latest/promptuity/prompts/index.html) offers five built-in prompts.
To implement your original prompt, please see the [Build your own Prompt](#build-your-own-prompt) section.

### Input

![Input Demo](./assets/prompt_input.gif)

A prompt for general text input.

```rust
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

![Password Demo](./assets/prompt_password.gif)

A text input prompt where the input is not displayed.

```rust
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

![Number Demo](./assets/prompt_number.gif)

A prompt for inputting only integer values.

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

### Select

![Select Demo](./assets/prompt_select.gif)

A prompt for selecting a single element from a list of options.

```rust
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

![MultiSelect Demo](./assets/prompt_multi_select.gif)

A prompt for selecting multiple elements from a list of options.

```rust
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

![Confirm Demo](./assets/prompt_confirm.gif)

A prompt for inputting a Yes/No choice.

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

### Autocomplete

> [!NOTE]
> Autocomplete is not provided as a built-in feature. This is because the optimal behavior for Fuzzy Match and key bindings varies by project.
> While not provided as a built-in, a reference implementation is available in [examples/autocomplete.rs](./examples/autocomplete.rs). Please adapt this to suit your project's needs.

## Themes

Promptuity offers two different built-in themes.
To implement your original Theme, please see the [Build your own Theme](#build-your-own-theme) section.

### MinimalTheme

MinimalTheme is similar to [Inquirer](https://github.com/SBoudrias/Inquirer.js). It provides a compact UI.

![MinimalTheme Screenshot](./assets/theme_minimal.png)

```rust
use promptuity::themes::MinimalTheme;

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

### FancyTheme

FancyTheme is similar to [clack](https://github.com/natemoo-re/clack). It provides a rich UI.

![FancyTheme Screenshot](./assets/theme_fancy.png)

```rust
use promptuity::themes::FancyTheme;

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

## Customize

This section provides guidance on how to construct original prompts and Themes.

### Build your own Prompt

Creating an original prompt can be achieved by implementing the [`Prompt`](https://docs.rs/promptuity/latest/promptuity/trait.Prompt.html) trait. By implementing three lifecycle methods, you can build prompts that are usable with [`Promptuity::prompt`](https://docs.rs/promptuity/latest/promptuity/struct.Promptuity.html#method.prompt).

Promptuity prompts consist of the following elements:

| Item | Description |
| :-- | :-- |
| **Message** | Displays the question content of the prompt. |
| **Input** | A single-line item that accepts user key inputs. |
| **Body** | A multi-line item that accepts user key inputs. |
| **Hint** | Displays a message to assist with prompt input. |

- Prompts that accept single-line inputs, like `Input` or `Password`, do not utilize **Body**.
- Prompts that do not accept inputs, like `Select` or `MultiSelect`, do not utilize **Input**.

Keep these points in mind when building your prompts.

#### 0. Setting Up a Custom Prompt

Let's use the implementation of a custom prompt similar to `Confirm` as an example.

```rust
use promptuity::Prompt;

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

impl Prompt for CustomConfirm {
type Output = bool;

// TODO
}
```

Define a struct with a message, hint, and value. Specify the final result type in `Output`.

First, let's implement the reception of key inputs.

#### 1. Receiving Key Input

Handle key inputs in the [`Prompt::handle`](https://docs.rs/promptuity/latest/promptuity/trait.Prompt.html#tymethod.handle) method.

For example, let's implement it so that pressing y for Yes and n for No finalizes the result.

```rust
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,
}
}
}
```

You can freely combine key codes and modifiers, allowing the construction of complex prompts tailored to specific requirements.

> [!IMPORTANT]
> Commonly, prompts are interrupted with Ctrl + C, but Promptuity does not automatically handle this.
> If the implementation is omitted, it results in a prompt that cannot be interrupted, leading to poor usability. Therefore, when building an original prompt, you must explicitly implement the interruption process yourself.

#### 2. Rendering the Prompt

Construct the rendering content in the [`Prompt::render`](https://docs.rs/promptuity/latest/promptuity/trait.Prompt.html#tymethod.render) method. Here's a simple example using only **Input** without a **Body**.

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

// ...

impl Prompt for CustomConfirm {
// ...

fn render(&mut self, state: &PromptState) -> Result {
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"))),
}
}
}
```

Determine the appropriate rendering content based on the [`PromptState`](https://docs.rs/promptuity/latest/promptuity/enum.PromptState.html) returned by [`Prompt::handle`](https://docs.rs/promptuity/latest/promptuity/trait.Prompt.html#tymethod.handle). The above implementation achieves the following requirements:

- The result displays either `Yes` or `No`.
- If the prompt is interrupted, only the message is displayed.
- During user input reception, it displays `Y/n`.

#### 3. Returning Submission Results

This is the final step in constructing a custom prompt.

Implement the [`Prompt::submit`](https://docs.rs/promptuity/latest/promptuity/trait.Prompt.html#tymethod.submit) method, which returns the final value for the received key input.

```rust
impl Prompt for CustomConfirm {
// ...

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

`Prompt::submit` is a lifecycle method called immediately after `Prompt::handle` returns `PromptState::Submit`.

---

Handling key inputs and rendering based on input state form the foundation of prompt construction.

For building more complex prompts, [examples/autocomplete.rs](./examples/autocomplete.rs) should serve as a useful reference.

### Build your own Theme

Just like prompts, you can build an original Theme by implementing the `Theme` trait.

For a complete example, please refer to [examples/custom_theme.rs](./examples/custom_theme.rs).

## Error Handling

All errors are consolidated into [`promptuity::Error`](https://docs.rs/promptuity/latest/promptuity/enum.Error.html).

In many cases, prompt interruptions will need to be handled individually. Interruptions occur during user input reception, typically through inputs like Ctrl + C or ESC.

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

fn ask() -> Result {
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),
}
}
```

Prompt interruptions can be handled as `Error::Cancel`. In the above examples, no message is displayed in the event of an interruption.

## Testing

Generally, validations involving user input are costly. Since Promptuity implements terminal behaviors as the [`Terminal`](https://docs.rs/promptuity/latest/promptuity/trait.Terminal.html) trait, it's easy to replace with a Fake.

The `Terminal` that simulates key inputs, used in Promptuity's integration tests, can be referenced in [`Term`](./tests/fake_term.rs).

Below is an example of testing prompts using a Fake `Terminal`.

```rust
#[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);
});
}
```

## Alternatives

The Rust ecosystem contains many wonderful crates.

- [console-rs/dialoguer](https://github.com/console-rs/dialoguer)
- [axelvc/asky](https://github.com/axelvc/asky/)
- [mikaelmello/inquire](https://github.com/mikaelmello/inquire)
- [fadeevab/cliclack](https://github.com/fadeevab/cliclack)

### Inspired

Promptuity's various prompts and design have been greatly inspired by these projects. We are very grateful for their development.

- [SBoudrias/Inquirer.js](https://github.com/SBoudrias/Inquirer.js)
- [natemoo-re/clack](https://github.com/natemoo-re/clack)
- [terkelg/prompts](https://github.com/terkelg/prompts)

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md).

## CHANGELOG

See [CHANGELOG.md](./CHANGELOG.md).

## License

[MIT © wadackel](./LICENSE)