Crysterm is a console/terminal toolkit (TUI), inspired by Qt, Blessed, and Blessed-contrib.
It is implemented in Crystal. Apps using Crysterm can be written using AI.
Main features:
More than 90 concrete widgets with Qt-modeled APIs and behavior.
Source: tests/misc/qt_widgets.cr. See also tests/misc/widgets.cr, per-widget demos under tests/widget/, and the directory with widgets in src/wiget/.
As in Qt, widgets are best positioned by layout engines rather than by absolute coordinates. There are 11 layouts supported.
Source: tests/misc/layouts.cr. See also src/layout/.
A complete CSS engine is used for styling -- paddings, margins, borders (including per-side and radius), colors, shadows, opacity, transitions, and pixel measures (when possible) translated to cells via the terminal's real cell geometry.
It also reads Qt QSS dialect directly — unmodified desktop Qt themes can be used to style apps.
Six independent windows below run the same scene, each loaded
with a different theme from data/css/
(--colors-stylesheet data/css/<name>.qss on any Crysterm program, or
window.load_stylesheet).
Source: tests/misc/themes.cr
Source: tests/misc/styling.cr
TextDocument (a QTextDocument work-alike) supports Markdown
(CommonMark + GFM), HTML and Crysterm's native tags, and
TextEdit/TextBrowser render it: headings, bold/italic/strikethrough,
inline code and links, fenced code blocks, blockquotes, GFM tables laid out
as real box-drawing tables, GFM task lists, and GFM alert admonitions —
themable via TextTheme, editable with full undo, navigable links.
Below is an example of a Claude-style session that is streaming a reply in Markdown (note: this is not Claude, it is Crysterm's Claude-like example):
Source: examples/claude/claude.cr
SyntaxHighlighter mirrors Qt's QSyntaxHighlighter. Syntax formats overlay
the text without touching content, undo or export; multi-line constructs carry
state between blocks, and multiple highlighters can stack on one document.
Source: tests/misc/syntax.cr
Unicode-native end to end: grapheme clusters (ZWJ emoji, combining marks), wide CJK cells, ambiguous-width resolution probed from the live terminal, East Asian width, and glyph chrome that auto-upgrades on modern-font terminals.
Source: examples/text/editor/editor.cr
Colors are natively stored as full 24-bit RGB and reduced to the terminal's real capability only at output time (16.7M → 256 → 16 → 8 colors, automatically). Alpha compositing blends colors per channel in RGB space; translucent widgets, soft shadows, and smooth gradients are supported.
Source: tests/misc/truecolor.cr
Cursor shape (block / underline / bar), blink, and color are settable
per window and per widget — each focused widget can present its own cursor.
When the terminal can't style its hardware cursor (or a custom glyph/style is requested), Crysterm transparently composites an artificial cursor into the cell buffer instead, re-deciding hardware-vs-artificial every frame:
Source: tests/misc/cursors.cr
Adjacent and overlapping borders can dock into shared junction glyphs/chars
(├ ┬ ┼ …). When the touching borders differ in color, the difference can
be ignored or blended (for the smoothest seam), or the docking can be skipped.
Source: tests/misc/docking.cr
Any widget content can carry inline tags (when parse_tags: true), for in-band
colors ({red-fg}, {#57c7ff-bg}), attributes ({bold}, {underline},
{inverse}, {blink}), and alignment
({center}, {right}, and {|} for left/right split).
Source: tests/misc/tags.cr
BitmapFont loads GNU Unifont .hex and ttystudio .json fonts —
BigText renders text with them in the terminal, and the capture pipeline
rasterizes screenshots through them. Terminus and Unifont faces ship in
data/font/.
Source: tests/misc/fonts.cr
One Media widget, twenty-one backends, selected automatically per terminal
and per content (--media-backend=auto) or forced by config/CLI/env/code:
- In-band pixels — Kitty graphics, iTerm2 inline images, Sixel, ReGIS
- Sub-cell glyphs — octant (2×4), braille (2×4), sextant (2×3), quadrant (2×2), half (1×2), block, ASCII
- ANSI cells — TrueColor, 256, 16, 8 colors
- External — w3m overlay, überzug, Tektronix 4014
Formats: PNG, APNG, GIF (stills and animations), JPEG (iTerm pass-through),
ANSI/BBS art (.ans, .nfo, …), and http(s):// sources. Fit modes:
stretch, contain (aspect-preserving), cover, none.
Sources: tests/misc/media_graphics.cr, tests/misc/media_glyph.cr, tests/misc/media_ansi.cr
Video plays through the same widget via ffmpeg (Widget::Video): mp4,
mkv, webm, mov, avi, mpeg, ts, 3gp and more, with eager or constant-memory
streaming decode (media.video_decode=auto|eager|stream):
Source: tests/misc/video.cr
APNG and GIF animations play in every backend, with per-frame delays honored and multiple widgets optionally driven in lockstep from one shared timer — here the same GIF in four backends at once:
Source: tests/misc/animated.cr — see also tests/misc/netscape.cr
At startup Crysterm probes the live terminal — truecolor (DECRQSS), graphics protocols (Kitty/iTerm/Sixel), Unicode and ambiguous width, palette and default colors, cursor styling, kitty keyboard protocol, in-band resize, pixel mouse, cell pixel geometry — and automatically picks the best supported settings.
E.g. the same gauges render as braille sub-cells on a plain terminal and as real pixels on a Kitty-graphics terminal, untouched:
Source: tests/misc/detect.cr — probe internals:
Tput#probe!, Tput::Features, Tput::Emulator.
Every value choice in the toolkit has a default value and can be overriden.
Unless left at (auto-detected) defaults, values can be specified in config file
(~/.config/crysterm/config.yml), env (CRYSTERM_*), CLI flag, or code —
currently 69 total low-level settings available in any Crysterm program
out of the box.
crystal run app.cr -- --dump-config=pretty # list every option + provenance
crystal run app.cr -- --media-backend=sixel # force a graphics backend
CRYSTERM_MEDIA_BACKEND=kitty crystal run app.crApps can also run on the command line without taking over the screen. Two
flavors: Crysterm::Direct for styled printing into the normal scrollback
and inline windows (Window.new inline: true, optionally
auto_grow: true, max_height:).
The complete widget stack, popups and
all, anchored at the shell cursor like fzf. For example, a completer on a command line:
Source: examples/direct/completer/completer.cr
One process can drive several terminal screens.
Below, a value assigned on the left screen updates the right one inside a single program.
Source: examples/screen/multiple/multiple.cr
All the protocols — X10, SGR (1006), URxvt (1015), SGR-Pixels (1016, sub-cell pixel coordinates), and GPM on the Linux console — with hover, enter/leave, capture, double/triple-click counting, wheel scrolling, focus reporting (1004), and a GUI pointer shape over hovered widgets (OSC 22).
Real drag & drop with MIME-typed payloads (text/uri-list, …),
Move/Copy/Link actions negotiated by modifiers, and the same sessions driven
by keyboard. The editor demo above opens its menu and scrolls by mouse.
Text widgets share a process-wide Emacs kill-ring (C-k/C-u/C-w
kill, C-y yank, consecutive kills merge — kill in one field, yank in
another), a full undo/redo stack on the document model (C-z/M-z,
grouped edit blocks, format-preserving), GUI clipboard keys (C-c/C-x/
C-v), bracketed paste (DEC 2004), and the system clipboard over
OSC 52 — copy/paste that works through SSH and tmux.
A fine-grained signals system (SolidJS-style): Reactive::Signal,
Reactive.computed, effects with automatic dependency tracking,
widget-lifetime Reactive.bind, ObservableList for collection views,
batching, and a reactive_property macro for widget classes. Assign
signal.value = x — every bound widget repaints itself:
Source: tests/misc/reactive.cr — full tour: tests/reactive/reactive.cr
The renderer supports compositing and damage tracking, and automatically uses whichever is faster for the workload.
Output is a minimal cell-level diff against what the
terminal already shows, with CSR/BCE scroll optimizations and optional DEC
2026 synchronized output. The Fps widget reports render/draw/flush times
and terminal bandwidth live:
Source: tests/misc/quicktro.cr — see also tests/misc/concurrent_rendering.cr
Any window or single widget can record itself and produce PNG or
APNG/GIF/MP4/WebM/JPEG via ffmpeg, and a textual .dump format,
including live recording of a running UI at a chosen fps and compositing of
in-band graphics (sixel/kitty/iterm) into the capture.
Setting
CRYSTERM_SHOT / CRYSTERM_ANIM / CRYSTERM_DUMP makes any Crysterm
program capture itself headlessly — every image on this page was produced
that way.
Source: examples/games/minesweeper/minesweeper.cr
Scripted input goes through the exact code paths that real input uses —
window.emit Event::KeyPress, ... and window.dispatch_mouse ....
The
bundled harness (tests/widget/example.cr) builds
on that with a scripting driver — d.key, d.type, d.click, d.act,
dwell timing — used by ~200 widget demos to film themselves, and equally
usable for in-app automation and testing.
Built with -Dremote, an app exposes its widget tree as a DOM over
HTTP: JSON-RPC commands (setContent, addClass, focus, append,
query, snapshot, …) addressed by full CSS selectors.
UI events are streamed out over Server-Sent Events — so app behavior can live in another process, written in any language.
The layout side works without the network,
too: Window#load_layout builds the UI from HTML + CSS, queryable and
updatable in-process:
Source: tests/misc/dom.cr — the bundled crysterm run app.html --handler "python3 app.py" CLI is in src/remote/bin/.
(Note: the above feature is not browser output in HTML; it is control of a native program/TUI from an external program, and is currently experimental.)
Programs under examples/ are complete, usable applications, meant as templates for your own.
- Terminal widget — a real VT-emulating terminal inside your UI (ptys, colors, mouse), enabling multiplexers and embedded shells.
- Charts — line/bar/pie/donut/sparkline graph widgets on a backend-agnostic canvas that upgrades from braille to real pixels.
- Effects — animated widget effects (e.g. Matrix rain), marquees, gradients, shadows with true alpha.
- Hyperlinks — OSC 8 clickable links tracked per cell.
- Keyboard — kitty keyboard protocol and modifyOtherKeys, probed and enabled automatically.
- GPM — mouse on the bare Linux console.
- ANSI art — CP437
.ans/.nfoart decoded and rendered with sub-cell detail. - Docs with pictures —
crystal docsembeds each widget's capture into its API documentation automatically.
Crysterm is supported by the event model in event_handler, color routines in term_colors, terminal handling in tput.cr, GPM mouse in gpm.cr, a terminfo library in unibilium.cr, and an animated PNG/GIF parser in pnggif.
tput.cr implements all the terminal routines, and does not use ncurses. For terminfo bindings it uses unibilium, but it also supports a built-in, standard mode which does not use terminfo at all. (A lot of modern software just hardcodes the sequences.) The other important module at Crysterm's core is event_handler. through which all app events and input are routed.
require "crysterm"
alias C = Crysterm
# A `Window` is the surface your widgets live on.
window = C::Window.new title: "hello"
C::Widget::Box.new \
parent: window,
top: "center", left: "center", width: 20, height: 5,
content: "{center}'Hello {bold}world{/bold}!'\nPress q to quit.{/center}",
parse_tags: true,
style: C::Style.new(fg: "yellow", bg: "blue", border: true)
# `q` / Ctrl-Q quit by default. Run the main loop:
window.execgit clone https://github.com/crystallabs/crysterm
cd crysterm
shards
crystal tests/hellos/hello.cr # the program above
crystal tests/hellos/hello2.cr # the Qt shape: MainWindow + a layout
crystal tests/misc/qt_widgets.cr # tour of the Qt-inspired widget set
crystal tests/misc/widgets.cr # tour of the general widget set
Larger, complete applications:
crystal examples/mutt/mutt.cr # a Mutt-style mail client
crystal examples/pine/pine.cr # a Pine/Alpine-style mail client
crystal examples/terminal/tid/tid.cr # a terminal multiplexer
crystal examples/games/minesweeper/minesweeper.cr
crystal examples/games/pong/pong.cr
crystal examples/games/commando/commando.cr
crystal examples/games/wumpus/wumpus.cr
(And many more under examples/ and tests/.)
Run crystal spec as usual.
Run crystal docs as usual.
- All the fine folks in the Crystal community.






