7 TUI libraries for creating interactive terminal apps
Tags: webdev, tui
Published: 2024-06-20 15:33:42

**Written by [Yashodhan Joshi](https://blog.logrocket.com/author/yashodhan-joshi/)✏️**

When writing applications, a good user interface is just as important as the actual app’s functionality. A good user interface will make the user continue using the app, whereas a bad, clunky one will drive users away. 

This also applies to applications that are completely terminal-based, but making them can be trickier than normal due to the limitations of the terminal. 

In this post, we will review seven different TUI libraries that can help us with building interactive terminal applications. Specifically, we will go over:

*   Ratatui
*   Huh?
*   BubbleTea
*   Gum
*   Textual
*   Ink
*   Enquirer

Additionally, we will go through a brief comparison between them that will help you choose the library for your next terminal-based project.

## Introduction to terminal user interfaces

[Terminal user interfaces (TUIs)](https://blog.logrocket.com/rust-and-tui-building-a-command-line-interface-in-rust/) can be categorized into two different types: one that is completely flag-based and one that is more like a GUI application. 

Most of the Unix command-line utilities provide a flag-based interface. We specify the flags using `-` or `--` and a short or long flag name, and the application changes its behavior accordingly. These are extremely useful when the application is to be used in non-interactive way or as a part of a shell script. 

However, they can get notoriously complex — Git is feature-rich and incredibly useful, but its flag and sub-command-based interface can get unwieldy. There is also a website dedicated to generating random and remarkably [real-looking fake Git flags](https://git-man-page-generator.lokaltog.net). 

Another consideration for terminal apps is whether they are being used interactively or as part of a pipeline. For example, if you just run `ls` , it will output different colors for normal files, directories, and so on in most of the shells. If you run it as `ls | cat` , it will not output colors. 

For terminal-based apps, which are intended to be primarily used interactively, the choice is a bit simplified and there’s more flexibility. They can create GUI-like interfaces and take inputs from both the keyboard and mouse. 

In this article, we will focus on this second kind of TUI, which is meant to be more GUI-like and can provide a familiar experience to users who don’t have a lot of experience using terminals.

## 1\. Ratatui

Ratatui is a feature-rich library that can be used to create complex interfaces containing elements similar to graphical interfaces, such as lists, charts, tables scrollbars, etc. It is a powerful library with many options, and you can check out its [resource of examples](https://github.com/ratatui-org/ratatui/blob/main/examples/README.md) to see the possibilities. 

For our example, we will be creating a simple directory explorer application. Note that because this is an example, there are lot of `unwrap`s and `clone`s. You should handle these properly in actual applications. 

Start with a new project, and add `ratatui` and `crossterm` as dependencies:

```bash
cargo add ratatui crossterm
```

We then add the required imports to `src/main.rs`:

```rust
use crossterm::{
    event::{self, KeyCode, KeyEventKind},
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
use std::{
    io::{stdout, Result},
    path::PathBuf,
};
```

And re-write the main function as follows, but do not run this yet:

```rust
fn main() -> Result<()> {
    stdout().execute(EnterAlternateScreen)?;
    enable_raw_mode()?;
    let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
    terminal.clear()?;

    stdout().execute(LeaveAlternateScreen)?;
    disable_raw_mode()?;
    Ok(())
}
```

These are taken from the Ratatui example. 

Ratatui directly interfaces with the underlying terminal using crossterm. To correctly render the UI, it needs to set the terminal in raw mode where the typed characters, including arrow keys, are passed directly to the application and not intercepted. Thus, we first need to enable the raw mode and then disable it again before exiting. 

Now, we start by declaring some required variables in the main after the `terminal.clear()`:

```rust
let mut cwd = PathBuf::from(".");
let mut selected = 0;
let mut state = ListState::default();
let mut entries: Vec<String> = std::fs::read_dir(cwd.clone())
    .unwrap()
    .map(|entry| entry.unwrap().file_name())
    .map(|s| s.into_string().unwrap())
    .collect::<Vec<_>>();
```

We set the `cwd` to the current directory and `selected` to `0`. We also create a `state`, which will store the state information for our list widget and create the initial entries by reading the current directory. 

Now we add an infinite loop below this:

```rust
loop {
...
}
```

This will act as the main driving loop for the application. We first add in a line to check for events by doing the following:

```rust
if event::poll(std::time::Duration::from_millis(16))? {
  if let event::Event::Key(key) = event::read()? {
    if key.kind == KeyEventKind::Press {
      match key.code {
        KeyCode::Char('q') => {
            break;
        }
        _ => {}
      }
}}}
```

Here, we first poll for the event, waiting only for 16 milliseconds (similar to their example). This way, we do not block the rendering if there is no event, and instead, skip the processing and continue on with the loop. 

If there is an event, we read the event and check if it’s of type `Key`. If it is, we further check if the event is a key press, at which place we are three brackets in, and make sure that some key was in fact pressed. We check the key code, and if it is `q`, then we break out of the loop. Otherwise, we simply ignore it. 

Then, the code for rendering the list widget is added. First, we create the list widget:

```rust
let list = List::new(entries.clone())
  .block(Block::bordered().title("Directory Entries"))
  .style(Style::default().fg(Color::White))
  .highlight_style(
    Style::default()
      .add_modifier(Modifier::BOLD)
      .bg(Color::White)
      .fg(Color::Black),
  )
  .highlight_symbol(">");
```

We create a new list with `entries` as the list elements. We set the rendering style to `block` with the title `Directory Entries`. This will render the list with borders around it and a title on the top border. We set the element style as a default of white text, as well as the highlight style. The highlighted section will be bolded with black text on a white background. We also set the highlight symbol to `>` , which will be displayed before the selected item. 

Then we actually draw the list UI using:

```rust
terminal.draw(|frame| {
    let area = frame.size();
    state.select(Some(selected));
    frame.render_stateful_widget(list, area, &mut state);
})?;
```

Here, we use `state.select` method to set which item is selected, and then render it on the frame. 

Now, to handle the arrow inputs, we add the following to the `match` statement for `key.code`:

```rust
KeyCode::Up => selected = (entries.len() + selected - 1) % entries.len(),
KeyCode::Down => selected = (selected + 1) % entries.len(),
KeyCode::Enter => {
    cwd = cwd.join(entries[selected].clone());
    entries = std::fs::read_dir(cwd.clone())
        .unwrap()
        .map(|entry| entry.unwrap().file_name())
        .map(|s| s.into_string().unwrap())
        .collect::<Vec<_>>();
    selected = 0;
}
```

If the key is an up or down arrow, we change the selected to one item before or after, taking care of wrapping around the first and last item. If the key pressed is `Enter`, we update the `cwd` by joining the selected entry to it and reset the `entries` to entries of this new `cwd` . Finally, we reset the selected to `0` . 

You can run this by running `cargo run`. Note that this does not handle going a directory back, and panics if you press enter on a file instead of a directory. You can implement them yourself, by taking the above code as a starting point, which can be found in the repo linked at the end. 

You should also check out [the Ratatui website](https://ratatui.rs/) for more creative examples and detailed information on available widgets.

## 2\. Huh?

Huh? is a Go library, which can be used to take inputs from users in a form-like manner. It provides functions and classes to create specific types of prompts and take user input, such as select, text input, and confirmation dialogues. 

First, we create a Go project, add the library as a dependency, and in `main.go`, import it as:

```go
package main
import "github.com/charmbracelet/huh"
```

For this example, we will create an interface for a program, which searches for a package with a given name. Users can also provide a version needed to select a registry from a predefined list. We start by declaring the variables for these and set the version to `*` as the default:

```go
func main() {
  var name string
  version := "*"
  var registry string
}
```

Then, we create a new `form`, which is the top-level input class in the library:

```go
form := huh.NewForm(...)
```

A form can contain multiple groups of prompts. You can think of a group as a “page” in real-life forms. Each group will be rendered to the screen separately and will clear the screen of questions from previous groups. A form must have at least one group:

```go
form := huh.NewForm(huh.NewGroup(...))
```

In this group, we will add individual questions, starting with name, which is a simple string:

```go
huh.NewInput().
    Title("Package name to search for").
    CharLimit(100).
    Value(&name),
```

We create an `Input` element, which takes a single line of text. The `Title` is displayed as the prompt question to the user. For `Input`, we can also set the character limit if needed using `CharLimit` . Finally, we give the reference of the variable to store the user input. 

The `version` input is similar to the `name` input:

```go
huh.NewInput().
    Title("Version").
    CharLimit(400).
    Validate(validateVersion).
    Value(&version),
```

If the variable has a value set (like in this case `*`), then that value will be displayed as the default answer. `Validate` is used to specify a validation function for the input. The function should take a single parameter typed according to the field’s value and should return `nil` if input is valid, or an error if it is not. For example, we can define the function as:

```go
func validateVersion(v string) error {
    if v == "test" {
      return errors.New("Test Error")
    }
    return nil
}
```

This takes a string, because `version` is of the type string, and returns an error for our special value `test` . Note that this error will be displayed directly to users and they will be prevented from answering further questions until the error is corrected. 

Finally, for the registry input, we use the selection input as:

```go
huh.NewSelect[string]().
    Title("Select Registry to search in").
    Options(
            huh.NewOption("Registry 1", "https://reg1.com"),
            huh.NewOption("Registry 2", "https://reg2.com"),
            huh.NewOption("Registry 3", "https://reg3.com"),
    ).
    Value(&registry)
```

Each option takes two values — the first is what will be displayed to the user and the second is what will be actually stored in the variable when that option is selected. 

Finally, to actually take the input, we use the `Run` on the form created:

```go
err := form.Run()
if err != nil {
    log.Fatal(err)
}
fmt.Println(name, version, registry)
```

Error will be returned if there are any issues when taking input (but this does not include any error returned by validation functions.) 

We finish the example by printing the variables, but in the actual program, you can use these values to connect to the registry and find the package.

## 3\. BubbleTea

BubbleTea is a Go library inspired by the model-view-update architecture of Elm applications. This separates the model (the data structure), update (the method used for processing the input and updating the state), and view (code for rendering the state to the terminal). Thus, we first define a structure, implement the `init`, `update`, and `view` methods on it, and use that to render the TUI. 

For this example, we will create a very simple file explorer that displays a list of files and directories in the current directory. If you select a directory, it changes the list to show the contents of that directory and so on. 

We start by creating the Go module using `go mod init bubbletea-example` and adding the package. We then declare our imports:

```go
package main
import (
        "fmt"
        tea "github.com/charmbracelet/bubbletea"
        "log"
        "os"
)
```

We define our model as:

```go
type model struct {
  cwd      string
  entries  []string
  selected int
}
```

Here, the `cwd` field will be used to store the current directory path, the `entries` field will be used to store the directory entries, and `selected` field stores the index of entry where the user cursor is currently. 

We define a function to get an instance of our struct with default values:

```go
func initialModel() model {
  entries, err := os.ReadDir(".")
  if err != nil {
    log.Fatal(err)
  }
  var dirs = make([]string, len(entries))
  for i, e := range entries {
    dirs[i] = e.Name()
  }
  return model{
    cwd:      ".",
    entries:  dirs,
    selected: 0,
  }
}
```

We first read the `dir` from which the program was invoked, then create a list of the entry names, and create a model using this data. 

We also need to add a function `Init`, which will be called the first time the TUI is created for the struct. It should be used to perform any I/O if needed. As we don’t need any, we simply return `nil` from it.

```go
func (m model) Init() tea.Cmd {
        return nil
}
```

We then add the `Update` method as:

```go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  switch msg := msg.(type) {
    case tea.KeyMsg:
      switch msg.String() {
      case "ctrl+c", "q":
        return m, tea.Quit
      case "up":
        if m.selected > 0 {
                m.selected--
        }
      case "down":
        if m.selected < len(m.entries)-1 {
                m.selected++
        }
      case "enter", " ":
        entry := m.entries[m.selected]
        m.selected = 0
        m.cwd = m.cwd + "/" + entry
        entries, err := os.ReadDir(m.cwd)
        if err != nil {
                log.Fatal(err)
        }
        var dirs = make([]string, len(entries))
        for i, e := range entries {
                dirs[i] = e.Name()
        }
        m.entries = dirs
      }
    }
    return m, nil
}
```

This method gets a parameter of type `Msg` , which is an interface implemented by both keyboard and mouse input events. We check if the concrete type of the `msg` is `KeyMsg`, indicating that the user has pressed some key, and then switch on its value. 

If the key pressed is `q` or `Ctrl+C` , we return a special message of `Quit`, which quits the application. If the key is up or down, we update the `selected` value accordingly. If the key is `Enter`, we update the `cwd` by appending the currently selected entry with the current `cwd` and then update the entry list. Finally, we return the model struct itself. 

Then, for the rendering method:

```go
func (m model) View() string {
  s := "Directory List\n\n"
  for i, dir := range m.entries {
    cursor := " "
    if m.selected == i {
            cursor = ">"
    }
    s += fmt.Sprintf("%s %s\n", cursor, dir)
  }
  s += "\nPress q to quit.\n"
  return s
}
```

We start with a fixed string, then iterate over the entries — adding them one by one to the string. If the entry currently has the cursor on it, we indicate that with `>` and, finally, indicate the quitting information. We return the string to be displayed from the function. 

In `main`, we invoke this app as:

```go
func main() {
  p := tea.NewProgram(initialModel())
  if _, err := p.Run(); err != nil {
    fmt.Printf("Error : %v", err)
    os.Exit(1)
  }
}
```

We create a new program with the default state of our model, and call `run` on it. This will display the TUI application and handle the input provided using the update method.

## 4\. Gum

Gum is a batteries-included library that can be used to take inputs for a shell script. When using Gum, you don’t have to write any Go code, and you can use various inbuilt types of prompts to take input. Of course, for this to work, the `gum` binary must be installed on the user’s machine. This library is also by the same developers who wrote Huh? and BubbleTea. 

Here, we will recreate the same program as Huh? example, but using Gum in shell scripts. First, install the Gum binary as per instructions on their [GitHub repo here](https://github.com/charmbracelet/gum). 

We can use Gum’s sub-commands to take specific types of inputs, such as:

*   `gum input` — Takes a single-line text input
*   `gum input --password` — Displays `*` instead of what is typed
*   `gum choose` — To select one of the given options
*   `gum file` — To select a file from a given directory

…and so on. We can recreate the `huh` example as:

```bash
#! /bin/bash

echo "Name of the package to serach for :"
NAME=$(gum input)
echo "Version of package to find"
VERSION=$(gum input --value="*")
echo "Select registry :"
REGISTRY=$(gum choose https://reg{1..3}.com)

echo "name $NAME, version $VERSION, registry $REGISTRY"
```

We first use `echo` to display a prompt to the user, take the input, and store it in the corresponding variables. We use the `--value` flag to set the default value for the input. 

Gum prints out the value given by the user, so the `$(…)` expression evaluates to the user input and is subsequently stored in the variable. Make sure that the input is stored somewhere — either in a variable or redirected to a file using `>` — otherwise it is printed on the terminal. This can be dangerous in the case of password inputs.

## 5\. Textual

Textual is a Python library that can be used for creating rich user interfaces in the terminal. It has similar elements to GUI. For this example, we create a simple directory explorer similar to the Ratatui and BubbleTea examples. 

We start by installing the library:

```bash
>pip install textual textual-dev
```

Then, we can add the imports and a helper function:

```python
import os
from textual.app import App 
from textual.widgets import Header, Footer, Button, Static

def create_id(s):
    return s.replace(".","_").replace("@","_")
```

We create a class that extends Textual’s `Static` class. This will be used to display the list of directory entries and handle user input:

```python
class DirDisplay(Static):
    directory = "."
    dir_list = [Button(x,id=x) for x in os.listdir(".")]

    def on_button_pressed(self, event):
        self.directory = os.path.join(self.directory,str(event.button.label))
        self.dir_list = [];
        for dir in os.listdir(self.directory):
            self.dir_list.append(Button(dir,id=create_id(dir)))
        self.remove_children()
        self.mount_all(self.dir_list)

    def compose(self):
       return self.dir_list 
```

We start by defining the `directory` initialized with `.` , corresponding to the current directory. We then initialize the `dir_list` using `os.listdir()` and creating a button for each of the entries using list comprehension. 

The first parameter of the `Button` constructor is the label of the button, which can be any text. The `id` parameter needs to be unique for each button so we can figure out which button was pressed in the button click handler. The `id` has several restrictions, such as no special characters or starting with any numbers. We thus use the `create_id` helper function to convert the directory entry name to an id-compatible string. 

The `compose` method is called once at the beginning and must return the widgets to render. Here, we return the `dir_list`, which is the list of buttons. 

`on_button_pressed` is a fixed method name and is used as the button-click handler by the `Textual` library. It gets an `event`, which has details of the button clicked. In this, we handle the button click by first concatenating the existing `directory` with the label of the pressed button, which indicates the directory entry name). We then re-initialize the `dir_list` by creating buttons for each entry in this new path. 

We then explicitly remove the current children to clear out the old entries and then call `mount_all`to render buttons for the new entries. 

Our main app has a separate class:

```python
class Explorer(App):
   def compose(self):
        yield Header()
        yield DirDisplay()
        yield Footer()
```

This is a pretty simple class extending the `App` class from the library. Here, instead of returning a complete list, we use `yield` to return the components one by one. For the components, we use the inbuilt `Header`, then our `DirDisplay`, and, finally, the inbuilt `Footer` components. 

Finally, we run this app as:

```python
if __name__ == "__main__":
    app = Explorer()
    app.run()
```

And run it as `python path/to/main.py`. Textual also allows the use of CSS to style the components. You can view the detailed guide in [their documentation](https://textual.textualize.io).

## 6\. Ink

[Ink is a JavaScript library](https://blog.logrocket.com/using-ink-ui-react-build-interactive-custom-clis/) that uses React and React components to develop the TUI. If you are already familiar with the React ecosystem and are developing your application in Node, this can be a great option for you to take user input. 

To get started, we use their scaffolding command to set up our project:

```bash
npx create-ink-app ink-example
```

This will create the project directory, `npm project`, install the dependencies, etc. As this is a React-based project, it will also set up the Babel to transpile JSX into JS. If you already have a Node app, you can also manually add `ink` and `react` as dependencies and set up Babel to compile it. The instructions for that can [be found here](https://github.com/vadimdemedes/ink?tab=readme-ov-file#getting-started). 

The default template installs several other dependencies, such as `xo`, `meow`, and `ava`, which can be ignored for this example. In the `app.js` file under `source` dir, we have the example component that prints `hello name` with the name part in green. 

The `Text` component renders text with various styling options such as color, background color, italics, bold, etc. The `Box` component can be used for creating layouts similar to flexbox. 

For this example, we will create a simple app that takes text as input and saves it in a file. We will change the `source/cli.js` as:

```javascript
import React from 'react';
import {render} from 'ink';
import App from './app.js';

render(<App />);
```

In the `source/app.js` , we will update imports and app definition to:

```javascript
import React, {useState} from 'react';
import {Text, Box, Newline, useInput, useApp} from 'ink';

export default function App() {}
```

In the `App` function, we will declare a state using `useState` hook to store the inputted text. We also get the exit function from `useApp` hook to exit the app when the user presses `Ctrl+D` . Note that the `App` in hook’s name is not related to the component’s name:

```javascript
const [ text, setText ] = useState(' ');
const { exit } = useApp();
```

We will return JSX from the function, which will be rendered as our terminal UI. We will start by printing a heading:

```javascript
return (
    <>
      <Box justifyContent="center">
        <Text color="green" bold>
                Input your text
        </Text>
        <Newline />
      </Box>
    </>
);
```

The `Box` component is given `justifyContent` to center align the header, and we put the text inside of it with a color green and in bold. We also add a new line to separate our heading from the use inputted text. 

Next, we add the actual text. Below is the header’s box component:

```javascript
{text.split('\n').map((t, i, arr) => (
  <Text key={i}>
    {'>'}
    {t}
    {i == arr.length - 1 ? '_' : ''}
  </Text>
))}
```

We split the `text` content by a new line and map it to a `Text` component. We prepend `>` to indicate the input, then the actual text line. For the last line, we append `_` as an indication of a cursor. 

To handle the input, we use the `useInput` hook provided by the library, before the `return` statement:

```javascript
useInput((input, key) => {
    if (key.ctrl && input === 'd') {
      // save the text in a file
      exit();
    } else {
      let newText;
      if (key.return) {
              newText = text + '\n ';
      } else {
              newText = text + input;
      }
      setText(newText);
    }
});
```

We get two parameters: `input` and `key` . `input` stores the entered text when there is a key-press. However, if the key is a non-text key, such as `Ctrl`, `Esc` , or `Return`, we get that information in the second argument as a boolean property. You can [see the docs](https://github.com/vadimdemedes/ink?tab=readme-ov-file#key) for a list of available keys, but in the example, we use `Ctrl` and `Return`. 

If `Ctrl+D` is pressed, we save the text entered until then in a file and then exit. Otherwise, we check if the `Return` key is pressed and append a new line to the text. For all other key presses, we append the `input` to the text. Finally, we set the text using `setText` call. 

You can see the output by running `npm run build && node dist/cli.js` : ![Ink Library Output](https://blog.logrocket.com/wp-content/uploads/2024/06/ink-library-output.png) The header is centered and the inputted text is displayed correctly. This does not handle backspace yet. You can take the code from the repo linked below, and implement the backspace and delete key handling.

## 7\. Enquirer

[Enquirer is a JavaScript library](https://blog.logrocket.com/creating-a-node-cli/) for designing question-based TUIs, somewhat similar to Huh?. Enquirer allows you to create a prompt-based interface similar to the one presented when we run `npm init`. It has various kinds of prompts, including text input, list input, password, selections, etc. There are some other similar libraries, such as Inquirer, which you might want to check if this does not fit your requirements. 

Start by creating a new package and running `npm init`. Add `enquirer` as a dependency by running `npm i enquirer`. We will use this to take initial player data for a game. 

We import the `prompt` from the library and create a prompt object:

```javascript
const { prompt } = require("enquirer");
const results = prompt(...);
```

If we want a single question, we can directly use the individual prompt classes such as `input`, `select`, etc., but for multiple questions, we can pass an array of objects with appropriate fields defined to the prompt function:

```javascript
const results = prompt([...]);
```

We will define a main function, call await on the results, and then call the main function itself:

```javascript
async function main() {
  const response = await results;
  console.log(response);
}
main();
```

Now, let us construct the questions one by one. First up is the character name. Type `input` is used for single-line text:

```javascript
{
  type: "input",
  name: "name",
  message: "What is name of your  character?",
},
```

We give the `type` as `input`. The value of `name` field will be used as the key in the results object returned and the `message` will be displayed to the user as the prompt for this question. 

Similarly, we add the next question to select the class of the character:

```javascript
{
  type: "select",
  name: "class",
  message: "What is your character class?",
  choices: [
    {
      name: "Dwarf",
      value: "dwarf",
    },
    { name: "Wizard", value: "wizard" },
    { name: "Dragon", value: "dragon" },
  ],
},
```

`type`, `name`, and `message` are similar, and here, we also provide `choices`. This is an array, with each object having a `name` that’s displayed to the user and a `value` that’s set in the `answers` object when a user selects the option. 

Then, we give the user an option to customize the experience with “advanced” options by using a toggle (a yes or no question):

```javascript
{
  type: "Toggle",
  name: "custom",
  message: "Do advance customization?",
  enabled: "Yes",
  disabled: "No",
},
```

The `enabled`/`disabled` strings are shown to the users, but the actual value is set to `true`/`false`, depending on whether the user selects the enabled or disabled option. 

Then, we give the user two options to customize the experience: difficulty and item randomness:

```javascript
{
  type: "select",
  name: "difficulty",
  message: "Select difficulty level",
  choices: [
    { message: "Easy", value: "1" },
    { message: "Medium", value: "2" },
    { message: "Hard", value: "3" },
  ],
  initial: "2",
  skip: function () {
    return !this.state.answers.custom;
  },
},
```

This is again a `select` type, but two fields are added — `initial` and `skip` . The `initial` field must be a string or a function returning a string, which will be set as the default value. The `skip` function must return a boolean and will be used to decide if the question should be skipped. 

Note that this is an anonymous function and not an arrow function. This is because we need the `this` object to be bound correctly. We can then access the previous answers by using `this.state.answers` and use the answer `custom` to decide if this question should be asked to the user or not. 

If the user has selected `No` for the customization, then this question will not be displayed. Its answer will be set to the `initial` value. 

Similarly, we define the item randomness:

```javascript
{
  type: "select",
  name: "random",
  message: "Select item randomness level",
  choices: [
    { message: "Minimum", value: "1" },
    { message: "Low", value: "2" },
    { message: "Medium", value: "3" },
    { message: "High", value: "4" },
    { message: "Maximum", value: "5" },
  ],
  initial: "3",
  skip: function () {
packages    return !this.state.answers.custom;
  },
},
```

Now, if you run the project, you will get the prompts, and finally, the answer object will be logged.

## Comparison table

Below is a comparison of the seven TUI libraries we discussed. Keep in mind that some columns in the following table are subjective, such as ease of use:

<table class="tg">

<thead>

<tr>

<th class="tg-0pky">Library</th>

<th class="tg-0pky">Language</th>

<th class="tg-0pky">Ease of use</th>

<th class="tg-0pky">Inbuilt components</th>

<th class="tg-0pky">End user requirements</th>

</tr>

</thead>

<tbody>

<tr>

<td class="tg-0pky">Ratatui</td>

<td class="tg-0pky">Rust</td>

<td class="tg-0pky">Complex</td>

<td class="tg-0pky">Yes</td>

<td class="tg-0pky">Final compiled Binary</td>

</tr>

<tr>

<td class="tg-0pky">Huh?</td>

<td class="tg-0pky">Go</td>

<td class="tg-0pky">Easy</td>

<td class="tg-0pky">Yes</td>

<td class="tg-0pky">Final compiled Binary</td>

</tr>

<tr>

<td class="tg-0pky">BubbleTea</td>

<td class="tg-0pky">Go</td>

<td class="tg-0pky">Medium</td>

<td class="tg-0pky">No</td>

<td class="tg-0pky">Final compiled Binary</td>

</tr>

<tr>

<td class="tg-0pky">Gum</td>

<td class="tg-0pky">Shell</td>

<td class="tg-0pky">Easy</td>

<td class="tg-0pky">Yes</td>

<td class="tg-0pky">User needs the Gum binary along with script</td>

</tr>

<tr>

<td class="tg-0pky">Textual</td>

<td class="tg-0pky">Python</td>

<td class="tg-0pky">Complex</td>

<td class="tg-0pky">Yes</td>

<td class="tg-0pky">User needs the textual Python library along with the script</td>

</tr>

<tr>

<td class="tg-0pky">Ink</td>

<td class="tg-0pky">JS</td>

<td class="tg-0pky">Easy if familiar With React</td>

<td class="tg-0pky">Yes</td>

<td class="tg-0pky">User needs node and npm dependencies</td>

</tr>

<tr>

<td class="tg-0pky">Enquirer</td>

<td class="tg-0pky">JS</td>

<td class="tg-0pky">Easy</td>

<td class="tg-0pky">Yes</td>

<td class="tg-0pky">User needs node and npm dependencies</td>

</tr>

</tbody>

</table>

## Comparison summary

Before we wrap up, let’s go into more depth and discuss specific use cases, etc.:

*   **Ratatui**: If your application is in Rust or you want to create a TUI using some Rust libraries, then Ratatui is obviously a good fit. It has a lot of inbuilt widgets that can satisfy most needs and also provides granular control options if needed. On the other hand, it can get verbose and might be a bit tricky if you are a beginner in Rust
*   **Huh?**: If your application is in Go and your input can be done in a simple question-based format, this might be for you. It provides several inbuilt prompt options. However, if you need a more GUI-like interface, this might not fit the requirements
*   **BubbleTea**: This gives you a complete model-view-update architecture like Elm, so if you are familiar with Elm, this can be an easy way in Go’s TUI libraries. However, the final output needs to be constructed as a string, so if you want a fancy display, you will need to construct it yourself by string construction
*   **Gum**: Gum is extremely useful for taking input in shell scripts without having to write code in other languages or deal with shell-specific quirks while taking input. It provides some good inbuilt input types, however, you will need the Gum binary installed on the user’s system. That is an added dependency you need to care about. This can be a great fit if you are writing shell scripts for yourself and want robust input handling without having to deal with shell’s input methods
*   **Textual**: If you want TUI in Python, this would be a good library. While you need the `Textual` package to be installed on the user’s machine, if you are using any Python dependencies apart from standard lib, you need to instruct the user to install them anyway. This might not be that problematic
*   **Ink**: Ink is great if you are familiar with the React ecosystem and want to write TUI in JS. It needs a compile and run cycle, but so does any React application
*   **Enquirer**: Similar to Huh?, if your input needs can be satisfied by just question-based input, then this is a great library to use in JS for TUI. It provides a lot of inbuilt prompts and ways to write custom prompts as well

## Conclusion

In this post, we went over seven different libraries that can be used to implement various kinds of TUIs . Now you can use the appropriate library in your project to make your interface beautiful and helpful for the user in the terminal. 

You can find the example code for these examples [in the repo here](https://github.com/YJDoc2/LogRocket-Blog-Code/tree/main/tui-libraries-for-interactive-apps). Thank you for reading!

---

##Get set up with LogRocket's modern error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

NPM:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)