26 releases
0.1.26 | Apr 16, 2024 |
---|---|
0.1.25 | Jan 15, 2024 |
0.1.23 | Dec 23, 2023 |
0.1.21 | Oct 22, 2023 |
0.1.13 | Sep 24, 2023 |
#39 in Command-line interface
12,879 downloads per month
Used in 5 crates
(4 directly)
340KB
5K
SLoC
r3bl_tuify
- What is r3bl_tuify?
- How to use it as a library?
- APIs
- How to use it as a binary?
- Style the components
- Build, run, test tasks
- References
What is r3bl_tuify?
r3bl_tuify is a Rust crate that allows you to add simple interactivity to your CLI app.
r3bl_tuify crate can be used in two ways:
- As a library. This is useful if you want to add simple interactivity to your CLI app written in
Rust. You can see an example of this in the
examples
folder in themain_interactive.rs
file. You can run it usingcargo run --example main_interactive
or using nunu run examples
. - As a binary. This is useful if you want to use this crate as a command line tool. The binary
target is called
rt
.
How to use it as a library?
Here's a demo of the library target of this crate in action.
https://github.com/r3bl-org/r3bl-open-core/assets/22040032/46850043-4973-49fa-9824-58f32f21e96e
To install the crate as a library, add the following to your Cargo.toml
file:
[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.
The following example illustrates how you can use this as a library. The function that does the work
of rendering the UI is called select_from_list
. It takes a list of
items and returns the selected item or items (depending on the selection mode). If the user does not
select anything, it returns None
. The function also takes the maximum height and width of the
display, and the selection mode (single select or multiple select).
It works on macOS, Linux, and Windows. And is aware of the terminal color output limitations of each. For eg, it uses Windows API on Windows for keyboard input. And on macOS Terminal.app it restricts color output to a 256 color palette.
APIs
We provide 2 APIs:
select_from_list
: Use this API if you want to display a list of items with a single line header.select_from_list_with_multi_line_header
: Use this API if you want to display a list of items with a multi line header.
select_from_list
Use this API if you want to display a list of items with a single line header.
select_from_list
signature:
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
Use the select_from_list_with_multi_line_header
API if you want to display a list of items with a
multi line header. The first 5 lines are all part of the multi line header.
select_from_list_with_multi_line_header
signature:
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(())
}
How to use it as a binary?
Here's a demo of the binary target of this crate in action.
You can install the binary using cargo install r3bl_tuify
(from crates.io). Or
cargo install --path .
from source. rt
is a command line tool that allows you to select one of
the options from the list that is passed into it via stdin
. It supports both stdin
and stdout
piping.
Here are the command line arguments that it accepts:
-s
or--selection-mode
- Allows you to select the selection mode. There are two options:single
andmultiple
.-c
or--command-to-run-with-selection
- Allows you to specify the command to run with the selected item. For example"echo foo \'%\'"
simply prints each selected item.-t
or--tui-height
- Optionally allows you to set the height of the TUI. The default is 5.
Interactive user experience
Typically a CLI app is not interactive. You can pass commands, subcommands, options, and arguments to it, but if you get something wrong, then you get an error and have to start all over again. This "conversation" style interface might require a lot of trial and error to get the desired result.
The following is an example of using the binary with many subcommands, options, and arguments.
cat TODO.todo | cargo run -- select-from-list \
--selection-mode single \
--command-to-run-with-each-selection "echo %"
Here's a video of this in action.
https://github.com/r3bl-org/r3bl-open-core/assets/2966499/c9b49bfb-b811-460e-a844-fe260eaa860a
What does this do?
cat TODO.todo
- prints the contents of theTODO.todo
file tostdout
.|
- pipes the output of the previous command to the next command, which isrt
(ie, the binary target of this crate).cargo run --
- runs thert
debug binary in the target folder.select-from-list
- runs thert
binary with theselect-from-list
subcommand. This subcommand requires 2 arguments:--selection-mode
and--command-to-run-with-each-selection
. Whew! This is getting long!--selection-mode single
- sets the selection mode tosingle
. This means that the user can only select one item from the list. What list? The list that is piped in from the previous command (ie,cat TODO.todo
).--command-to-run-with-each-selection "echo %"
- sets the command to run with each selection. In this case, it isecho %
. The%
is a placeholder for the selected item. So if the user selectsitem 1
, then the command that will be run isecho item 1
. Theecho
command simply prints the selected item tostdout
.
Now that is a lot to remember. It is helpful to use clap
to provide nice command line help but
that are still quite a few things that you have to get right for this command to work.
It doesn't have to be this way. The binary can be interactive along with the use of clap
to
specify some of the subcommands, and arguments. It doesn't have to be an all or nothing approach. We
can have the best of both worlds. The following videos illustrate what happens when:
-
--selection-mode
and--command-to-run-with-each-selection
are not passed in the command line.cat TODO.todo | cargo run -- select-from-list
Here are the 3 scenarios that can happen:
-
The user first chooses
single
selection mode (using a list selection component), and then types inecho %
in the terminal, as the command to run with each selection. This is an interactive scenario since the user has to provide 2 pieces of information: the selection mode, and the command to run with each selection. They didn't provide this upfront when they ran the command.https://github.com/r3bl-org/r3bl-open-core/assets/2966499/51de8867-513b-429f-aff2-63dd25d71c82
-
Another scenario is that the user does not provide the required information even when prompted interactively. In this scenario, the program exits with an error and help message.
Here they don't provide what
selection-mode
they want. And they don't provide whatcommand-to-run-with-each-selection
they want. Without this information the program can't continue, so it exits and provides some help message.https://github.com/r3bl-org/r3bl-open-core/assets/2966499/664d0367-90fd-4f0a-ad87-3f4745642ad0
-
-
--selection-mode
is not passed in the command line. So it only interactively prompts the user for this piece of information. Similarly, if the user does not provide this information, the app exits and provides a help message.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
-
--command-to-run-with-each-selection
is not passed in the command line. So it only interactively prompts the user for this piece of information. Similarly, if the user does not provide this information, the app exits and provides a help message.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
Paths
There are a lot of different execution paths that you can take with this relatively simple program. Here is a list.
-
Happy paths:
rt
- prints help.cat Cargo.toml | rt -s single -c "echo foo \'%\'"
-stdin
is piped in, and it prints the user selected option tostdout
.cat Cargo.toml | rt -s multiple -c "echo foo \'%\'"
-stdin
is piped in, and it prints the user selected option tostdout
.
-
Unhappy paths (
stdin
is not piped in and, orstdout
is piped out):rt -s single
- expectsstdin
to be piped in, and prints help.rt -s multiple
- expectsstdin
to be piped in, and prints help.ls -la | rt -s single | xargs -0
- does not expectstdout
to be piped out, and prints help.ls -la | rt -s multiple | xargs -0
- does not expectstdout
to be piped out, and prints help.
Due to how unix pipes are implemented, it is not possible to pipe the
stdout
of this command to anything else. Unix pipes are nonblocking. So there is no way to stop the pipe "midway". This is whyrt
displays an error when thestdout
is piped out. It is not possible to pipe thestdout
ofrt
to another command. Instead, thert
binary simply takes a command that will run after the user has made their selection. Using the selected item(s) and applying them to this command.
Style the components
Choose one of the 3 built-in styles
Built-in styles are called default
, sea_foam_style
, and hot_pink_style
. You can find them in
the style.rs
file (tuify/src/components/style.rs).
default style
sea_foam_style
hot_pink_style
To use one of the built-in styles, simply pass it as an argument to the select_from_list
function.
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(())
}
Create your style
To create your style, you need to create a StyleSheet
struct and pass it as an argument to the
select_from_list
function.
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(())
}
Build, run, test tasks
Prerequisites
🌠 For these to work you have to install the Rust toolchain, nu
, cargo-watch
, bat
, and
flamegraph
on your system. Here are the instructions:
- Install the Rust toolchain using
rustup
by following the instructions here. - Install
cargo-watch
usingcargo install cargo-watch
. - Install
flamegraph
usingcargo install flamegraph
. - Install
bat
usingcargo install bat
. - Install
nu
shell on your system usingcargo install nu
. It is available for Linux, macOS, and Windows.
Nu shell scripts to build, run, test, etc.
Go to the tuify
folder and run the commands below. These commands are defined in the ./run
folder.
Command | Description |
---|---|
nu run examples |
Run examples in the ./examples folder |
nu run piped |
Run binary with piped input |
nu run build |
Build |
nu run clean |
Clean |
nu run all |
All |
nu run examples-with-flamegraph-profiling |
Run examples with flamegraph profiling |
nu run test |
Run tests |
nu run clippy |
Run clippy |
nu run docs |
Build docs |
nu run serve-docs |
Serve docs over VSCode Remote SSH session. |
nu run upgrade-deps |
Upgrade deps |
nu run rustfmt |
Run rustfmt |
The following commands will watch for changes in the source folder and re-run:
Command | Description |
---|---|
nu run watch-examples |
Watch run examples |
nu run watch-all-tests |
Watch all test |
nu run watch-one-test <test_name> |
Watch one test |
nu run watch-clippy |
Watch clippy |
nu run watch-macro-expansion-one-test <test_name> |
Watch macro expansion for one test |
There's also a run
script at the top level folder of the repo. It is intended to be used in a
CI/CD environment w/ all the required arguments supplied or in interactive mode, where the user will
be prompted for input.
Command | Description |
---|---|
nu run all |
Run all the tests, linting, formatting, etc. in one go. Used in CI/CD |
nu run build-full |
This will build all the crates in the Rust workspace. It will install all the required pre-requisite tools needed to work with this crate (what install-cargo-tools does) and clear the cargo cache, cleaning, and then do a really clean build. |
nu run install-cargo-tools |
This will install all the required pre-requisite tools needed to work with this crate (things like cargo-deny , and flamegraph will all be installed in one go) |
nu run check-licenses |
Use cargo-deny to audit all licenses used in the Rust workspace |
References
CLI UX guidelines:
- https://rust-cli-recommendations.sunshowers.io/handling-arguments.html
- https://rust-cli-recommendations.sunshowers.io/configuration.html
- https://rust-cli-recommendations.sunshowers.io/hierarchical-config.html
- https://rust-cli-recommendations.sunshowers.io/hierarchical-config.html
- https://docs.rs/clap/latest/clap/_derive/#overview
- https://clig.dev/#foreword
ANSI escape codes:
- https://notes.burke.libbey.me/ansi-escape-codes/
- https://en.wikipedia.org/wiki/ANSI_escape_code
- https://www.asciitable.com/
- https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg
- https://www.ditig.com/256-colors-cheat-sheet
- https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
- https://www.compuphase.com/cmetric.htm
Dependencies
~14–25MB
~285K SLoC