How To Build Beautiful Terminal UIs (TUIs) in JavaScript!
Sk
Posted on November 20, 2024
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:
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
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();
})();
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);
})();
We can also use hex code for colors(check the link to a full example in the outro)
Result:
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);
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);
The selected items will display the ✅ icon.
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"));
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,
});
To apply this style to text:
const styledText = lip.apply({ value: "Hello, charsm!", id: "primary" });
console.log(styledText);
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);
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"}))
The table and list will be wrapped in a border, with padding and margins!
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!
Posted on November 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.