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

sfundomhlungu

Sk

Posted on November 20, 2024

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!

💖 💪 🙅 🚩
sfundomhlungu
Sk

Posted on November 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related