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.
- Host: GitHub
- URL: https://github.com/wadackel/promptuity
- Owner: wadackel
- License: mit
- Created: 2024-01-08T07:10:49.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2025-08-17T12:47:06.000Z (10 months ago)
- Last Synced: 2025-08-17T14:38:07.104Z (10 months ago)
- Topics: ask, cli, command-line, command-line-tool, console, interactive, prompt, rust
- Language: Rust
- Homepage: https://crates.io/crates/promptuity
- Size: 2.87 MB
- Stars: 81
- Watchers: 1
- Forks: 0
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
Promptuity
Promptuity = Prompt + Ingenuity
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

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

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

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

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

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

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

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.

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

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