BACK TO ENGINEERING
Engineering 6 min read

Porting Go's Terminal UI Ecosystem to TypeScript: Glamour and Lipgloss Are Now on npm

Go has the best terminal UI ecosystem in any language. This isn't opinion — it's a statement you can verify by trying to build a beautiful CLI tool in anything other than Go.

The reason is Charm. Charmbracelet built five interlocking libraries — Lip Gloss for styling, Glamour for markdown rendering, Bubble Tea for the application framework, Bubbles for components, and Glow for a markdown reader — that together make terminal UI development feel like building a modern web app.

TypeScript developers have nothing like this.

There are ANSI color libraries. There are markdown parsers. There are scattered TUI experiments. But there's no integrated ecosystem where styled terminal output, markdown rendering, and interactive UI components all share a design language and work together.

I'm building one. The first two packages are live.


I – What Shipped

@oakoliver/lipgloss — a style-definition library for terminal layouts. Padding, margins, borders, alignment, color — everything you need to build structured terminal output. 328 tests, 595 assertions.

@oakoliver/glamour — stylesheet-based markdown rendering for terminals. Parse markdown into styled ANSI output with theme support. 317 tests, 688 assertions. Custom GFM parser built from scratch. Seven built-in themes.

npm install @oakoliver/lipgloss
npm install @oakoliver/glamour

Both are zero-dependency, multi-runtime (Node.js, Bun, Deno), and published under the @oakoliver scope because the bare names are taken on npm by abandoned packages.


II – Why Port Charm Instead of Building from Scratch

I considered building original libraries. The Charm API surface isn't complicated — it's approachable. I could design my own interfaces.

But Charm's value isn't in the API. It's in the hundreds of edge cases they've already solved.

How should a word wrapper handle ANSI escape sequences that span across a line break? What happens when a CJK wide character falls on a column boundary? How do you calculate margins when block elements are nested three levels deep? What should a table look like when one column has ANSI-styled content that throws off width calculations?

Charmbracelet has 4+ years of answers to these questions baked into their code. Every function has been battle-tested by thousands of Go developers building real CLI tools. Re-inventing those solutions would mean re-discovering those edge cases one by one.

Porting preserves the institutional knowledge. I get their correctness for free and only need to adapt it to TypeScript idioms.


III – Lipgloss: The Foundation Layer

Lipgloss is the styling primitive. Every other Charm library uses it for rendering styled text.

The Go version uses a fluent API with method chaining. My port preserves this pattern:

import { NewStyle, Color, AdaptiveColor } from '@oakoliver/lipgloss';

const style = NewStyle()
  .Bold(true)
  .Foreground(Color('#FF6600'))
  .Background(Color('#1a1a2e'))
  .Padding(1, 2)
  .Border('rounded')
  .BorderForeground(Color('#7B2FBE'));

console.log(style.Render('Hello from Lipgloss'));

Underneath, this is building ANSI escape sequences with proper SGR parameter ordering, handling 24-bit color, 256-color, and basic 16-color profiles, managing border characters from Unicode box-drawing sets, and calculating visible string widths with ANSI-stripping and CJK awareness.

The port supports all border styles from the Go original: normal, rounded, thick, double, hidden, and block borders. Color handling includes named ANSI colors, 256-color palette, and full RGB hex.

328 tests verify parity with the Go implementation across all of these behaviors.


IV – Glamour: The Hard Part

Glamour is significantly more complex than Lipgloss. It's a full markdown-to-ANSI rendering pipeline with a stylesheet engine.

Here's the architecture:

  1. Markdown text enters a custom GFM parser that produces an AST
  2. An AST walker visits each node (entering and exiting)
  3. Each node type maps to an Element class with render/finish methods
  4. Block elements push frames onto a BlockStack, managing nested contexts
  5. Text is styled through a cascade system (parent styles flow to children)
  6. Layout uses nested MarginWriter/IndentWriter/PaddingWriter chains
  7. The final output is ANSI-styled terminal text

The theme system is the interesting part. Every markdown element — headings, paragraphs, code blocks, lists, blockquotes, tables, links, images — has a configurable StyleBlock with properties for color, bold, italic, underline, prefix, suffix, margin, indent, and more.

import { render, renderWithTheme } from '@oakoliver/glamour';

// Default dark theme
console.log(render('# Hello World\n\n> A blockquote\n\n**Bold** and *italic*.'));

// Dracula theme
console.log(renderWithTheme('# Dracula\n\n```js\nconsole.log("styled")\n```', 'dracula'));

Seven themes ship built-in: dark, light, ascii, dracula, tokyo-night, pink, and notty (no-color for piped output).


V – Building a Markdown Parser from Scratch

The most provocative decision: I didn't use an existing markdown parser.

The JavaScript ecosystem has excellent markdown parsers — markdown-it, remark, unified. But glamour needs specific things from its parser that standard parsers make awkward:

Node relationships. Glamour's isChildNode() check needs to walk up the parent chain to determine if a node should be rendered by its parent (links render their children, emphasis renders its children, etc.). Standard parsers either don't expose parent pointers or use different traversal models.

Specific node metadata. Glamour needs level on headings, ordered and start on lists, checked on task items, destination on links/images, info on code blocks, alignments on tables. Different parsers expose these differently.

Zero dependencies. The whole point is a self-contained package. Adding markdown-it would bring in a dependency tree.

The custom parser handles full GFM: ATX and setext headings, fenced and indented code blocks, blockquotes, ordered and unordered lists with task checkboxes, tables with alignment, emphasis/strong/strikethrough, links (inline, reference, autolink), images, inline code, thematic breaks, hard and soft breaks, HTML blocks and inline HTML.

It's 590 lines. It passes 60 dedicated parser tests covering every GFM construct.


VI – The Cascade System

Glamour's most subtle feature is its style cascade. Like CSS, styles inherit from parents to children.

A heading inherits colors from the document style. An h2 inherits from the generic heading style, then overrides with h2-specific properties. A link inside a blockquote inherits the blockquote's indentation.

This is implemented through cascadeStyle() and cascadeStylePrimitive() functions that merge StyleBlock objects, with child properties overriding parent properties:

// Parent: blue text, bold
// Child: red text, italic
// Result: red text, bold, italic
const merged = cascadeStyle(parentBlock, childBlock, false);

The boolean parameter controls whether block-level properties (margin, indent) are inherited. For inline elements, only text properties cascade. For block elements, layout properties cascade too.

Getting this right is what makes themes work. A single StyleConfig object with ~20 element definitions produces correct styling for any markdown document, because the cascade fills in the gaps.


VII – What's Next: The Full Stack

Glamour and Lipgloss are the rendering layers. The plan has three more packages:

Bubble Tea (Phase 3) — The Elm Architecture for terminal applications. A message-passing framework where your application is a model with an update function and a view function. This is what makes interactive TUI apps possible.

Bubbles (Phase 4) — Pre-built components: text inputs, spinners, progress bars, viewports, lists, tables, file pickers. Each component is a Bubble Tea model that you compose into larger applications.

Glow (Phase 5) — A CLI markdown reader. The capstone that uses all four libraries. Glamour renders the markdown, Lipgloss styles the chrome, Bubble Tea handles interaction, Bubbles provides the viewport scrolling.

When all five are published, TypeScript developers will have the same terminal UI toolkit that Go developers have been enjoying for years. Same API patterns. Same visual quality. Native performance.


VIII – Get Them

npm install @oakoliver/lipgloss
npm install @oakoliver/glamour

Source code:

Both MIT licensed with attribution to Charmbracelet, Inc.

Original Go libraries:

645 tests across both packages. Zero dependencies. Multi-runtime.

If you've ever tried to build a beautiful CLI tool in JavaScript and given up, these are for you.

– Antonio

"Simplicity is the ultimate sophistication."