DEV Community

Cover image for How To Build Beautiful Terminal UIs (TUIs) in JavaScript!
Sk
Sk

Posted on • Edited on

How To Build Beautiful Terminal UIs (TUIs) in JavaScript!

If you're like me and completely obsessed with CLIs and Terminal UIs, this post is for you!

Unfortunately, there isn’t a native way to build beautiful Terminal UIs in JavaScript—at least none that I know of! This was a problem I ran into myself, which eventually led me to port one of the most stunning TUI libraries out there: Lipgloss, created by the folks at Charm.

Don’t believe me? Check this out:

Charm Cli Ligloss

Gorgeous, right?

Here’s the catch: Lipgloss is written in Go. While I usually work in Go, I recently needed to write a web monitoring tool in Node.js. I wasn’t willing to give up my sleek, beautiful UIs, so I found myself in a classic developer challenge mode.

You know those magical moments when you’re deep in code, and something unexpected just clicks? That’s how I ended up porting parts of Lipgloss to WebAssembly (Wasm). And that’s how charsm was born.

What is charsm?

Charsm is short for Charm CLI + Wasm. Cool, right? Let’s dive into how you can use it to build stunning TUIs in JavaScript.


Getting Started

Install charsm with a simple npm command:

npm install charsm
Enter fullscreen mode Exit fullscreen mode

Create a Simple Table

To get started, import charsm and initialize it in your script:

import { initLip, Lipgloss } from "charsm";

(async function () {
  const ini = await initLip();
})();
Enter fullscreen mode Exit fullscreen mode

The initLip function loads the Wasm file, prepping everything for rendering. Let’s try printing a table:

const rows = [
  ["Chinese", "您好", "你好"],
  ["Japanese", "こんにちは", "やあ"],
  ["Arabic", "أهلين", "أهلا"],
  ["Russian", "Здравствуйте", "Привет"],
  ["Spanish", "Hola", "¿Qué tal?"],
];

const tabledata = { 
  headers: ["LANGUAGE", "FORMAL", "INFORMAL"], 
  rows: rows 
};

(async function () {
  const ini = await initLip();
  const lip = new Lipgloss();

  const table = lip.newTable({
    data: tabledata,
    table: { border: "rounded", color: "99", width: 100 },
    header: { color: "212", bold: true },
    rows: { even: { color: "246" } },
  });

  console.log(table);
})();
Enter fullscreen mode Exit fullscreen mode

We can also use hex code for colors(check the link to a full example in the outro)

Result:

table

Simple, right? Now, let’s move on to rendering a list.


Rendering a List

Currently, we can render a simple list. Here’s how it works:

const subtle = { Light: "#D9DCCF", Dark: "#383838" };
const special = { Light: "#43BF6D", Dark: "#73F59F" };

const list = lip.List({
  data: ["Grapefruit", "Yuzu", "Citron", "Pomelo", "Kumquat"],
  selected: [],
  listStyle: "alphabet",
  styles: {
    numeratorColor: special.Dark,
    itemColor: subtle.Dark,
    marginRight: 1,
  },
});
const combined = table + "\n\n" + list
console.log(combined);
Enter fullscreen mode Exit fullscreen mode

Image description

Customizing Selected Items

Let’s make it fancier by using a custom enumerator icon (e.g., ✅) for selected items:

const customList = lip.List({
  data: ["Grapefruit", "Yuzu", "Citron", "Pomelo", "Kumquat"],
  selected: ["Grapefruit", "Yuzu"],
  listStyle: "custom",
  customEnum: "",
  styles: {
    numeratorColor: special.Dark,
    itemColor: subtle.Dark,
    marginRight: 1,
  },
});

console.log(customList);
Enter fullscreen mode Exit fullscreen mode

The selected items will display the ✅ icon.

Image description


Rendering Markdown

Charsm wraps the Glamour library from Charm to handle markdown rendering:

const content = `
# Today’s Menu

## Appetizers
| Name        | Price | Notes                           |
| ----------- | ----- | ------------------------------- |
| Tsukemono   | $2    | Just an appetizer               |
| Tomato Soup | $4    | Made with San Marzano tomatoes  |

## Desserts
| Name         | Price | Notes                 |
| ------------ | ----- | --------------------- |
| Dorayaki     | $4    | Looks good on rabbits |
| Cream Puff   | $3    | Pretty creamy!        |

Enjoy your meal!
`;

console.log(lip.RenderMD(content, "tokyo-night"));
Enter fullscreen mode Exit fullscreen mode

Custom Styles

Think of styles in charsm as CSS for terminals. Here’s how you can create your own style:

  lip.createStyle({
    id: "primary",
    canvasColor: { color: "#7D56F4" },
    border: { type: "rounded", sides: [true] },
    // for both margin and padding top, right, bottom, left
    padding: [6, 8, 6, 8],
    margin: [0, 8, 8, 8],
    bold: true,
    // align: 'center',
    width: 10,
    height: 12,

  });
 lip.createStyle({
    id: "secondary",
    canvasColor: { color: "#7D56F4" },
    border: { type: "rounded", background: "#0056b3", sides: [true, false]},
    padding: [6, 8, 6, 8],
    margin: [0, 0, 8, 1],
    bold: true,

    alignV: "bottom",
    width: 10,
    height: 12,

  });

Enter fullscreen mode Exit fullscreen mode

To apply this style to text:

const styledText = lip.apply({ value: "Hello, charsm!", id: "primary" });
console.log(styledText);
Enter fullscreen mode Exit fullscreen mode

consult the readme on github for more options or better yet here is a "full" example

Want a layout? Charsm supports simple flex-like layout:


  const a = lip.apply({ value: "Charsmmm", id: "secondary" })

  const b = lip.apply({ value: "🔥🦾🍕", id: "primary" })
  const c = lip.apply({ value: 'Charsmmm', id: "secondary" })

const res = lip.join({
  direction: "horizontal",
  elements: [a, b, c],
  position: "left",
});

console.log(res);
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

And there you have it! With charsm, you can render tables, lists, markdown, and even create custom styles—all within the terminal which by the way you can wrap around the list or markdown since it is text

  console.log(lip.apply({value: combined, id: "primary"}))
Enter fullscreen mode Exit fullscreen mode

The table and list will be wrapped in a border, with padding and margins!

wrapped list and table

This is just the beginning. I’ll be adding interactive components (like forms) soon, so stay tuned. Have fun experimenting and building your own beautiful Terminal UIs in JavaScript!

you can find me on substack for shorter and personal articles, and x

Cheers!

Top comments (7)

Collapse
 
sfundomhlungu profile image
Sk

Shout-out to the Charm team, for these tools, you can check them out from the following links:

Site: charm.sh
Youtube: youtube.com/@charmcli
Repo: github.com/charmbracelet

Collapse
 
gurukulkarni profile image
Guruprasad Kulkarni

Looks all beautiful 😍, I'm missing libraries like this in kotlin or Java where we can also create nice command line apps using Graal VM and I would love to have a library that can also do similar things, just markdown rendering also would be amazing 😅

Collapse
 
sfundomhlungu profile image
Sk

Props to the Charm team all their tools are amazing!🔥 in terms of Java, I know Java can target wasm not the other way around, there only way to get this is to build an executable that produces styled text using charsm in node or charm lipgloss itself in Go, load the executable in Java as a process, ingest it using standard input/output, practically the same results just more work.

Collapse
 
danielo515 profile image
Daniel Rodríguez Rivero

Impressive. Thank you for taking the time to not only build this, but also write about it and showcase. I always envy go devs for charm, not I have no longer to

Collapse
 
sfundomhlungu profile image
Sk

Thank you for the kind words, Daniel 🫡, it truly means a lot! Believe me, I felt the same way; I couldn’t go any longer without Charm tools! I’ll keep iterating on the tool to make it better, less buggy, and, of course, add interactive forms soon! 🚀

Collapse
 
anmolbaranwal profile image
Anmol Baranwal

I use Hyper with awesome plugins :)

Image description

Collapse
 
sfundomhlungu profile image
Sk • Edited

looks nice, Hyper is a terminal?, charsm is a tool to develop for terminals and ship exe's etc to other users, and hyper is electron based I believe making it huge already, compared to wasm which is only 16 mb