sheen
sheen
Shiny crystal terminal styles.
Sheen is a declarative terminal styling and layout library for Crystal. It lets you build styled CLI output and TUI views with an immutable, CSS-like API instead of hand-rolled ANSI escape sequences.
Inspired by our friends Charm who built lipgloss.
TODO: screenshot output here.
Why sheen?
- Immutable styles. Every setter returns a new style, so you can safely reuse a base style across components without side effects.
- Terminal-aware color rendering. Hex and ANSI 256 colors degrade automatically to the capabilities of the terminal.
NO_COLORandFORCE_COLORare honored. - Built-in terminal primitives. ANSI/SGR generation, color-space math and downsampling, color-profile detection, and Unicode-aware width measurement are included, so styling and layout behave correctly across terminals.
- CSS-like box model. Padding, margins, width, height, alignment, and borders compose the way you expect.
- Layout primitives. Join blocks horizontally or vertically, place content inside sized boxes, and measure rendered dimensions.
Installation
- Add sheen to your
shard.yml:
dependencies:
sheen:
github: lowkeyliesmyth/sheen
- Then run
shards install
Quick start
require "sheen"
box = Sheen.style do |s|
s.bold
s.foreground "#FAFAFA"
s.background "#7D56F4"
s.padding 2
s.width 24
s.height 7
s.align Sheen::Position::CENTER, Sheen::Position::CENTER
end
puts box.render("Hello, Crystal")
TODO: screenshot output here.
The same style built fluently:
box = Sheen::Style.new
.bold
.foreground("#FAFAFA")
.background("#7D56F4")
.padding(2)
.width(24)
.height(7)
.align(Sheen::Position::CENTER, Sheen::Position::CENTER)
Examples
Take a look at some fully-fledged and runnable examples in the examples/ directory. Run one with:
crystal run examples/main.cr -- <example-name>
Run the command without arguments to see the available example consumers.
Feature tour
Declarative styles
Build a style by chaining rules or by using the block builder.
error = Sheen::Style.new
.bold
.foreground("#FF5F87")
.background("#262626")
.padding(1, 2)
puts error.render("Disk full")
Bind reusable content with #string and render the style with ad-hoc content later:
label = Sheen::Style.new
.foreground("#7D56F4")
.bold
.string("status:")
puts label.render(" ready")
puts label.render(" busy")
Color profiles and adaptive colors
Sheen detects the terminal's color capability and automatically downsamples to the most robust option available. You can also force a profile if you want to.
# Force True Color output
Sheen.renderer.color_profile = Foundation::Profile::TrueColor
puts Sheen::Style.new.foreground("#7D56F4").render("rich purple")
For light and dark terminal backgrounds, use adaptive colors:
fg = Sheen::AdaptiveColor.new(light: "#000000", dark: "#FFFFFF")
puts Sheen::Style.new.foreground(fg).render("legible everywhere")
Or use complete colors if you need to control on a per-profile basis:
accent = Sheen::CompleteColor.new(
true_color: "#7D56F4",
ansi256: "99",
ansi: "5"
)
puts Sheen::Style.new.foreground(accent).render("consistent")
Box model
Padding, margins, width, height, and alignment follow CSS shorthand conventions. So if you can dodge a wrench write CSS you'll be right at home.
card = Sheen::Style.new
.width(40)
.padding(1, 2)
.margin(1)
.border(Sheen::Border.rounded)
.border_foreground("#7D56F4")
puts card.render("Look at all the padding this baby can hold inside! And a margin outside too!")
Borders
Pick from built-in border styles or define your own.
Sheen::Border.normal # ┌─┐
Sheen::Border.rounded # ╭─╮
Sheen::Border.thick # ┏━┓
Sheen::Border.double # ╔═╗
Sheen::Border.hidden # invisible frame
Sheen::Border.ascii # +-+
Toggle activating individual sides with the border shorthand:
Sheen::Style.new
.border(Sheen::Border.thick, true, false, true, false) # top and bottom only
.render("section divider")
Inheritance and composition
Styles inherit only unset rules from a parent style, which makes it simple to define a base theme and then override each component as you need to.
base = Sheen::Style.new.foreground("#CCCCCC").italic
notice = base.foreground("#FFD700") # overrides foreground, keeps italic
quiet = base.faint # keeps base foreground and italic
Layout utilities
Join rendered blocks along an edge:
left = Sheen::Style.new.foreground("#FF5F87").render("ERROR")
right = Sheen::Style.new.render("Something went wrong")
puts Sheen.join_horizontal(Sheen::Position::CENTER, left, right)
Place content inside a sized box:
Sheen.place(
30, 10,
Sheen::Position::CENTER,
Sheen::Position::CENTER,
"Loading..."
)
Measure rendered blocks:
block = card.render("Some text")
w, h = Sheen.size(block)
Rendering control
Bind content, render on demand, and constrain output:
tag = Sheen::Style.new
.bold
.foreground("#262626")
.background("#7D56F4")
.string("v1.0.0")
puts tag.render # v1.0.0
Force single-line output or cap rendered dimensions:
Sheen::Style.new.inline.max_width(10).render("long sentence")
Sheen::Style.new.max_width(20).max_height(4).render(a_long_paragraph)
Custom renderers
When rendering to multiple outputs, sheen supports creating a renderer per target so each gets its own profile and background detection.
renderer = Sheen::Renderer.new(io)
style = Sheen::Style.new(renderer)
.background(Sheen::AdaptiveColor.new(light: "63", dark: "228"))
io << style.render("client-specific output")
Coming next
Table, list, and tree rendering sub-packages are WIP, fam.
Development
# Run the test suite
task spec
# Run the linter
task lint
# Format code
crystal tool format
Updating Unicode data
More of a note to self, as nobody else should ever have to run this.
The vendored Unicode Character Database source for terminal-width measurement is stored at data/unicode/EastAsianWidth.txt. Sheen uses the generated src/foundation/unicode/east_asian_width.cr and not the downloaded data file.
To bump the supported Unicode version:
- Update the
UNICODE_VERSIONconstant inscripts/gen_unicode.cr. - Run
task unicodeto download the updated data file and generate theeast_asian_width.crtable. - Commit both the refreshed data source and the updated lookup table + query method.
Contributing
Ran into a problem? Issues are welcome. Or if you're inclined to file a PR, see CONTRIBUTING.md to get set up.
Contributors
- lowkey — creator and maintainer
License
MIT License — see LICENSE for details.
References
sheen
- 0
- 0
- 0
- 0
- 1
- about 8 hours ago
- May 23, 2026
MIT License
Wed, 15 Jul 2026 09:35:19 GMT