lipgloss

Lipgloss Crystal port

Crystal port of Lip Gloss — style definitions for nice terminal layouts
Built with TUIs in mind.

Architecture · Development · Guidelines · Testing · PR Workflow · Porting Parity · Upgrade Guide


Lip Gloss takes an expressive, declarative approach to terminal rendering. Users familiar with CSS will feel at home with Lip Gloss.

require "lipgloss"

style = Lipgloss::Style.new
  .bold(true)
  .foreground(Lipgloss::Color.from_hex("#FAFAFA"))
  .background(Lipgloss::Color.from_hex("#7D56F4"))
  .padding_top(2)
  .padding_left(4)
  .width(22)

Lipgloss.println style.render("Hello, kitty")

Installation

Add the dependency to your shard.yml:

dependencies:
  lipgloss:
    github: dsisnero/lipgloss

Then run shards install.

Crystal port of Go Lip Gloss v2.0.0. See the Upgrade Guide if migrating from the Go v1 API.

Colors

Lip Gloss supports the following color profiles:

ANSI 16 colors (4-bit)

Lipgloss::Color.indexed(5)   # magenta
Lipgloss::Color.indexed(9)   # red
Lipgloss::Color.indexed(12)  # light blue

Or using the string-based shorthand:

Lipgloss.color("5")   # magenta
Lipgloss.color("9")   # red
Lipgloss.color("12")  # light blue

ANSI 256 Colors (8-bit)

Lipgloss::Color.indexed(86)   # aqua
Lipgloss::Color.indexed(201)  # hot pink
Lipgloss::Color.indexed(202)  # orange

True Color (16,777,216 colors; 24-bit)

Lipgloss::Color.from_hex("#0000FF")  # good ol' 100% blue
Lipgloss::Color.from_hex("#04B575")  # a green
Lipgloss::Color.from_hex("#3C3C3C")  # a dark gray

...as well as a 1-bit ASCII profile, which is black and white only.

There are also named constants for the 16 standard ANSI colors:

Lipgloss::Color::BLACK
Lipgloss::Color::RED
Lipgloss::Color::GREEN
Lipgloss::Color::YELLOW
Lipgloss::Color::BLUE
Lipgloss::Color::MAGENTA
Lipgloss::Color::CYAN
Lipgloss::Color::WHITE
Lipgloss::Color::BRIGHT_BLACK
Lipgloss::Color::BRIGHT_RED
Lipgloss::Color::BRIGHT_GREEN
Lipgloss::Color::BRIGHT_YELLOW
Lipgloss::Color::BRIGHT_BLUE
Lipgloss::Color::BRIGHT_MAGENTA
Lipgloss::Color::BRIGHT_CYAN
Lipgloss::Color::BRIGHT_WHITE

Automatically Downsampling Colors

Some users don't have Truecolor terminals. Other times, output might not support color at all (for example, in logs). Lip Gloss was designed to handle this gracefully by automatically downsampling colors to the best available profile.

If you're using Lip Gloss standalone, just use Lipgloss.println or Lipgloss.sprint (and their variants).

Color Utilities

Lip Gloss ships with a handful of handy tools for working with colors:

c = Lipgloss::Color.from_hex("#EB4268")        # Sriracha sauce color
dark = Lipgloss.darken(c, 0.5)                 # dark Sriracha sauce
light = Lipgloss.lighten(c, 0.35)              # light Sriracha sauce
green = Lipgloss.complementary(c)              # greenish Sriracha sauce
with_alpha = Lipgloss.alpha(c, 0.2)            # watered down Sriracha sauce

Advanced Color Tooling

Lip Gloss also supports color blending, automatically choosing light or dark variants of colors at runtime, and a lot more. For details, see the architecture docs.

Inline Formatting

Lip Gloss supports the usual ANSI text formatting options:

style = Lipgloss::Style.new
  .bold(true)
  .italic(true)
  .faint(true)
  .blink(true)
  .strikethrough(true)
  .underline(true)
  .reverse(true)

Underline Styles

Beyond simple on/off, underlines support multiple styles and custom colors:

s = Lipgloss::Style.new
  .underline_style(Lipgloss::UnderlineCurly)
  .underline_color(Lipgloss::Color.from_hex("#FF0000"))

Available styles: Lipgloss::UnderlineNone, Lipgloss::UnderlineSingle, Lipgloss::UnderlineDouble, Lipgloss::UnderlineCurly, Lipgloss::UnderlineDotted, Lipgloss::UnderlineDashed.

Hyperlinks

Styles can render clickable hyperlinks in supporting terminals:

s = Lipgloss::Style.new
  .foreground(Lipgloss::Color.from_hex("#7B2FBE"))
  .hyperlink("https://charm.land")

Lipgloss.println s.render("Visit Charm")

In unsupported terminals this will degrade gracefully and hyperlinks will simply not render.

Block-Level Formatting

Lip Gloss also supports rules for block-level formatting:

# Padding
style = Lipgloss::Style.new
  .padding_top(2)
  .padding_right(4)
  .padding_bottom(2)
  .padding_left(4)

# Margins
style = Lipgloss::Style.new
  .margin_top(2)
  .margin_right(4)
  .margin_bottom(2)
  .margin_left(4)

There is also shorthand syntax for margins and padding, which follows the same format as CSS:

# 2 cells on all sides
Lipgloss::Style.new.padding(2)

# 2 cells on the top and bottom, 4 cells on the left and right
Lipgloss::Style.new.margin(2, 4)

# 1 cell on the top, 4 cells on the sides, 2 cells on the bottom
Lipgloss::Style.new.padding(1, 4, 2)

# Clockwise, starting from the top: 2 cells on the top, 4 on the right, 3 on
# the bottom, and 1 on the left
Lipgloss::Style.new.margin(2, 4, 3, 1)

You can also customize the characters used for padding and margin fill:

s = Lipgloss::Style.new
  .padding(1, 2)
  .padding_char('·')
  .margin(1, 2)
  .margin_char('░')

Aligning Text

You can align paragraphs of text to the left, right, or center.

style = Lipgloss::Style.new
  .width(24)
  .align(Lipgloss::Position::Left)    # align it left
  .align(Lipgloss::Position::Right)   # no wait, align it right
  .align(Lipgloss::Position::Center)  # just kidding, align it in the center

Width and Height

Setting a minimum width and height is simple and straightforward.

style = Lipgloss::Style.new
  .set_string("What's for lunch?")
  .width(24)
  .height(32)
  .foreground(Lipgloss::Color.indexed(63))

Borders

Adding borders is easy:

# Add a purple, rectangular border
style = Lipgloss::Style.new
  .border_style(Lipgloss.normal_border)
  .border_foreground(Lipgloss::Color.indexed(63))

# Set a rounded, yellow-on-purple border to the top and left
another_style = Lipgloss::Style.new
  .border_style(Lipgloss.rounded_border)
  .border_foreground(Lipgloss::Color.indexed(228))
  .border_background(Lipgloss::Color.indexed(63))
  .border_top(true)
  .border_left(true)

# Make your own border
my_cute_border = Lipgloss::Border.new(
  top: "._.:*:", bottom: "._.:*:",
  left: "|*", right: "|*",
  top_left: "*", top_right: "*",
  bottom_left: "*", bottom_right: "*"
)

There are also shorthand functions for defining borders, which follow a similar pattern to the margin and padding shorthand functions.

# Add a thick border to the top and bottom
Lipgloss::Style.new
  .border(Lipgloss.thick_border, true, false)

# Add a double border to the top and left sides. Rules are set clockwise
# from top.
Lipgloss::Style.new
  .border(Lipgloss.double_border, true, false, false, true)

You can also pass multiple colors to a border for a gradient effect:

s = Lipgloss::Style.new
  .border(Lipgloss.rounded_border)
  .border_foreground_blend(
    Lipgloss::Color.from_hex("#FF0000"),
    Lipgloss::Color.from_hex("#0000FF")
  )

Copying Styles

Just use assignment:

style = Lipgloss::Style.new.foreground(Lipgloss::Color.indexed(219))
copied_style = style  # this is a true copy
wild_style = style.bold(true)  # also a true copy with blink added

Since Style is a pure value type (struct), assigning a style to another effectively creates a new copy of the style without mutating the original.

Inheritance

Styles can inherit rules from other styles. When inheriting, only unset rules on the receiver are inherited.

style_a = Lipgloss::Style.new
  .foreground(Lipgloss::Color.indexed(229))
  .background(Lipgloss::Color.indexed(63))

# Only the background color will be inherited here, because the foreground
# color will have been already set:
style_b = Lipgloss::Style.new
  .foreground(Lipgloss::Color.indexed(201))
  .inherit(style_a)

Unsetting Rules

All rules can be unset:

style = Lipgloss::Style.new
  .bold(true)                                   # make it bold
  .unset_bold                                    # jk don't make it bold
  .background(Lipgloss::Color.indexed(227))      # yellow background
  .unset_background                              # never mind

When a rule is unset, it won't be inherited or copied.

Enforcing Rules

Sometimes, such as when developing a component, you want to make sure style definitions respect their intended purpose in the UI. This is where Inline and MaxWidth, and MaxHeight come in:

# Force rendering onto a single line, ignoring margins, padding, and borders.
some_style.inline(true).render("yadda yadda")

# Also limit rendering to five cells
some_style.inline(true).max_width(5).render("yadda yadda")

# Limit rendering to a 5x5 cell block
some_style.max_width(5).max_height(5).render("yadda yadda")

Tabs

The tab character (\t) is rendered differently in different terminals (often as 8 spaces, sometimes 4). Because of this inconsistency, Lip Gloss converts tabs to 4 spaces at render time. This behavior can be changed on a per-style basis, however:

style = Lipgloss::Style.new       # tabs will render as 4 spaces, the default
style = style.tab_width(2)          # render tabs as 2 spaces
style = style.tab_width(0)          # remove tabs entirely
style = style.tab_width(-1)         # leave tabs intact (NoTabConversion)

Wrapping

The wrap function wraps text while preserving ANSI styles and hyperlinks across line boundaries:

wrapped = Lipgloss.wrap(styled_text, 40, " ")

Rendering

Generally, you just call the render method on a Lipgloss::Style:

style = Lipgloss::Style.new.bold(true)
Lipgloss.println style.render("Hello, kitty")
Lipgloss.println style.render("Hello, puppy")

Utilities

In addition to pure styling, Lip Gloss also ships with some utilities to help assemble your layouts.

Joining Paragraphs

Horizontally and vertically joining paragraphs is a cinch.

# Horizontally join three paragraphs along their bottom edges
Lipgloss::Style.join_horizontal(Lipgloss::Position::Bottom, paragraph_a, paragraph_b, paragraph_c)

# Vertically join two paragraphs along their center axes
Lipgloss::Style.join_vertical(Lipgloss::Position::Center, paragraph_a, paragraph_b)

# Horizontally join three paragraphs, with the shorter ones aligning 20%
# from the top of the tallest
Lipgloss::Style.join_horizontal(0.2, paragraph_a, paragraph_b, paragraph_c)

Measuring Width and Height

Sometimes you'll want to know the width and height of text blocks when building your layouts.

# Render a block of text.
style = Lipgloss::Style.new
  .width(40)
  .padding(2)
block = style.render(some_long_string)

# Get the actual, physical dimensions of the text block.
width = Lipgloss.width(block)
height = Lipgloss.height(block)

# Here's a shorthand function.
w, h = Lipgloss.size(block)

Blending Colors

You can blend colors in one or two dimensions for gradient effects:

# 1-dimensional gradient
colors = Lipgloss.blend1d(10, Lipgloss::Color.from_hex("#FF0000"), Lipgloss::Color.from_hex("#0000FF"))

# 2-dimensional gradient with rotation
colors = Lipgloss.blend2d(80, 24, 45.0, color1, color2, color3)

Placing Text in Whitespace

Sometimes you'll simply want to place a block of text in whitespace.

# Center a paragraph horizontally in a space 80 cells wide.
block = Lipgloss.place_horizontal(80, Lipgloss::Position::Center, fancy_styled_paragraph)

# Place a paragraph at the bottom of a space 30 cells tall.
block = Lipgloss.place_vertical(30, Lipgloss::Position::Bottom, fancy_styled_paragraph)

# Place a paragraph in the bottom right corner of a 30x80 cell space.
block = Lipgloss.place(30, 80, Lipgloss::Position::Right, Lipgloss::Position::Bottom, fancy_styled_paragraph)

Compositing

Lip Gloss includes a cell-based compositor for rendering layered content:

# Create some layers.
a = Lipgloss.new_layer(pickles).x(4).y(2).z(1)
b = Lipgloss.new_layer(bitter_melon).x(22).y(1)
c = Lipgloss.new_layer(sriracha).x(11).y(7)

# Compose 'em and render.
compositor = Lipgloss.new_compositor(a, b, c)
output = compositor.render

Rendering Tables

Lip Gloss ships with a table rendering sub-package.

rows = [
  {"Chinese", "Nín hǎo", "Nǐ hǎo"},
  {"Japanese", "Konnichiwa", "Yā"},
  {"Arabic", "Ahlan", "Ahlan"},
  {"Russian", "Zdravstvuyte", "Privet"},
  {"Spanish", "Hola", "¿Qué tal?"},
]

Use the table to style and render.

purple    = Lipgloss::Color.indexed(99)
gray      = Lipgloss::Color.indexed(245)
light_gray = Lipgloss::Color.indexed(241)

header_style = Lipgloss::Style.new.foreground(purple).bold(true).align(Lipgloss::Position::Center)
cell_style   = Lipgloss::Style.new.padding(0, 1).width(14)
odd_row_style  = cell_style.foreground(gray)
even_row_style = cell_style.foreground(light_gray)

t = Lipgloss::StyleTable::Table.new
  .border(Lipgloss.normal_border)
  .border_style(Lipgloss::Style.new.foreground(purple))
  .style_func(->(row : Int32, col : Int32) {
    if row == Lipgloss::StyleTable::HEADER_ROW
      header_style
    elsif row % 2 == 0
      even_row_style
    else
      odd_row_style
    end
  })
  .headers("LANGUAGE", "FORMAL", "INFORMAL")
  .rows(rows)

Lipgloss.println t

Table Borders

There are helpers to generate tables in markdown or ASCII style:

# Markdown table
Lipgloss::StyleTable::Table.new
  .border(Lipgloss.markdown_border)
  .border_top(false)
  .border_bottom(false)

# ASCII table
Lipgloss::StyleTable::Table.new
  .border(Lipgloss.ascii_border)

Rendering Lists

Lip Gloss ships with a list rendering sub-package.

l = Lipgloss::List.new("A", "B", "C")
Lipgloss.println l
# • A
# • B
# • C

Lists have the ability to nest.

l = Lipgloss::List.new(
  "A", Lipgloss::List.new("Artichoke"),
  "B", Lipgloss::List.new("Baking Flour", "Bananas", "Barley", "Bean Sprouts"),
  "C", Lipgloss::List.new("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"),
)
Lipgloss.println l

Lists can be customized via their enumeration function as well as using Lipgloss::Styles.

enumerator_style = Lipgloss::Style.new.foreground(Lipgloss::Color.indexed(99)).margin_right(1)
item_style = Lipgloss::Style.new.foreground(Lipgloss::Color.indexed(212)).margin_right(1)

l = Lipgloss::List.new("Glossier", "Claire's Boutique", "Nyx", "Mac", "Milk")
  .enumerator(->(items : Lipgloss::List::Items, i : Int32) { Lipgloss::List.roman(items, i) })
  .enumerator_style(enumerator_style)
  .item_style(item_style)

Lipgloss.println l

In addition to the predefined enumerators (arabic, alphabet, roman, bullet, asterisk, dash), you may also define your own custom enumerator.

Rendering Trees

Lip Gloss ships with a tree rendering sub-package.

t = Lipgloss::Tree.root(".")
  .child("A", "B", "C")

Lipgloss.println t
# .
# ├── A
# ├── B
# └── C

Trees have the ability to nest.

t = Lipgloss::Tree.root(".")
  .child("macOS")
  .child(Lipgloss::Tree.root("Linux").child("NixOS").child("Arch Linux (btw)").child("Void Linux"))
  .child(Lipgloss::Tree.root("BSD").child("FreeBSD").child("OpenBSD"))

Lipgloss.println t

Trees can be customized via their enumeration function as well as using Lipgloss::Styles.

enumerator_style = Lipgloss::Style.new.foreground(Lipgloss::Color.indexed(63)).margin_right(1)
root_style = Lipgloss::Style.new.foreground(Lipgloss::Color.indexed(35))
item_style = Lipgloss::Style.new.foreground(Lipgloss::Color.indexed(212))

t = Lipgloss::Tree.root("⁜ Makeup")
  .child("Glossier", "Fenty Beauty",
    Lipgloss::Tree.new.child("Gloss Bomb Universal Lip Luminizer", "Hot Cheeks Velour Blushlighter"),
    "Nyx", "Mac", "Milk")
  .enumerator(->(children : Lipgloss::Tree::Children, i : Int32) { Lipgloss::Tree.rounded_enumerator(children, i) })
  .enumerator_style(enumerator_style)
  .root_style(root_style)
  .item_style(item_style)

Lipgloss.println t

The predefined enumerators for trees are default_enumerator and rounded_enumerator.

Advanced Color Usage

One of the most powerful features of Lip Gloss is the ability to render different colors at runtime depending on the user's terminal and environment, allowing you to present the best possible user experience.

Adaptive Colors

You can render different colors at runtime depending on whether the terminal has a light or dark background:

has_dark_bg = Lipgloss.has_dark_background?
light_dark = Lipgloss.light_dark(has_dark_bg)

my_color = light_dark.call(
  Lipgloss::Color.from_hex("#D7FFAE"),
  Lipgloss::Color.from_hex("#D75FEE")
)

Complete Colors

Specify exact values for each color profile (ANSI 16, ANSI 256, and TrueColor):

profile = Colorprofile.detect(STDOUT, ENV.to_a)
complete = Lipgloss.complete(profile)
color = complete.call(
  Lipgloss.color("1"),        # ANSI
  Lipgloss.color("124"),      # ANSI256
  Lipgloss.color("#ff34ac")   # TrueColor
)

Color Downsampling

One of the best things about Lip Gloss is that it can automatically downsample colors to the best available profile, stripping colors (and ANSI) entirely when output is not a TTY.

Use the Lip Gloss writer functions, which drop in wherever you use puts or print:

s = Lipgloss::Style.new
  .foreground(Lipgloss::Color.from_hex("#EB4268"))
  .render("Hello!")

# Downsample if needed and print to stdout.
Lipgloss.println(s)

# Render to a variable.
downsampled = Lipgloss.sprint(s)

# Print to stderr.
Lipgloss.fprint(STDERR, s)

The full set: print, println, printf, fprint, fprintln, fprintf, sprint, sprintln, sprintf.

What about Bubble Tea?

Lip Gloss doesn't replace Bubble Tea. Rather, it is an excellent Bubble Tea companion. It was designed to make assembling terminal user interface views as simple and fun as possible so that you can focus on building your application instead of concerning yourself with low-level layout details.

In simple terms, you can use Lip Gloss to help build your Bubble Tea views.

A Crystal port of Bubble Tea is available at dsisnero/bubbletea.

Development

make install      # Install dependencies
make spec         # Run tests
make format       # Format Crystal files
make docs         # Generate documentation

See Development Guide for full setup instructions.

Documentation

Document Purpose
Architecture System design and data flow
Development Setup and daily workflow
Coding Guidelines Code style and conventions
Testing Test commands and patterns
PR Workflow Commits, PRs, and review process
Porting Parity Upstream source tracking from Go v2.0.0
Upgrade Guide Migrating from Lip Gloss v1 to v2

Contributing

  1. Create an issue: /forge-create-issue
  2. Implement: /forge-implement-issue <number>
  3. Self-review: /forge-reflect-pr
  4. Address feedback: /forge-address-pr-feedback
  5. Update changelog: /forge-update-changelog

License

MIT — this Crystal port is based on Charm's Lip Gloss.

Contributors

Repository

lipgloss

Owner
Statistic
  • 0
  • 0
  • 0
  • 8
  • 10
  • 10 minutes ago
  • February 5, 2026
License

MIT License

Links
Synced at

Tue, 28 Jul 2026 07:04:44 GMT

Languages