crysterm

Console / terminal GUI toolkit for Crystal.

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.

Apple silicon hardware is recommended for development, for quickest iterations.

Main features:

Widgets

More than 90 concrete widgets with Qt-modeled APIs and behavior. Some demoed below:

Sources: tests/misc/widgets.cr and tests/misc/widgets2.cr. See also the full MainWindow chrome (menus, toolbars, docks, tabs) in tests/misc/qt_widgets.cr, per-widget demos under tests/widget/, and the directory with widgets in src/widget/.

Layouts

As in Qt, widgets are best positioned by layout engines rather than by absolute coordinates. There are 12 layouts supported.

Sources: tests/misc/layouts.cr and tests/misc/layouts2.cr. See also per-layout demos under tests/layout/, and the directory with layouts in src/layout/.

CSS and QSS styling

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/.

Source: tests/misc/themes.cr

For the minimal author-your-own-stylesheet form — one CSS string styling a whole app, including :focus restyling — see examples/css/css.cr.

Borders, Shadow, and Light

The full presentation of borders, shadows, and light can be seen in README-borders.md.

Source: tests/misc/styling.cr, tests/misc/styling2.cr, and other numbered styling files in tests/misc/

Rich text — Markdown with GFM

TextDocument (a QTextDocument work-alike) supports Markdown (CommonMark + GitHub-Featured Markdown), 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

Syntax highlighting

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

Full Unicode

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.

(The demo program below shows unupgraded glyph chrome due to the screen capturing method used.)

Source: examples/text/editor/editor.cr

Native TrueColor

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

Hardware and artificial cursors

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.

Source: tests/misc/cursors.cr

Border junctions with color blending

Adjacent and overlapping borders can merge 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 merge can be skipped.

Source: tests/misc/junctions.cr

Parseable tags in strings

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

Loadable fonts

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

Rich media — images, animated images, and video

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

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

Videos play 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

Terminal feature auto-detection

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 real pixels on a Kitty-graphics terminal and downgrade to braille sub-cells on a plain terminal. All autodetected, no user action required.

Source: tests/misc/detect.cr — probe internals: Tput#probe!, Tput::Features, Tput::Emulator.

Everything in the toolkit tunable

Every internal setting in the toolkit has a default value and can be overriden. Unless left at (pre-set or auto-detected) defaults, values can be specified in config file (~/.config/crysterm/config.yml), env (CRYSTERM_*), CLI flag, or code — currently ~70 total low-level settings available in any Crysterm program out of the box.

crystal run app.cr -- --crysterm-dump-config=pretty  # every option + provenance
crystal run app.cr -- --media-backend=sixel          # force a graphics backend
CRYSTERM_MEDIA_BACKEND=kitty crystal run app.cr

Crysterm reads these from a copy of ARGV without consuming anything, and its two framework flags are namespaced --crysterm-config / --crysterm-dump-config, so an app's own --config stays its own.

Multiple screens

One process can drive several completely separate terminal screens.

Below, a value assigned on the left screen updates the right one, running inside a single program but on different TTYs.

Source: examples/screen/multiple/multiple.cr

Full mouse support

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.

Paste buffer, kill-ring, undo/redo

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.

Reactive programming

An opt-in app-state layer — the toolkit itself never requires it, and plain property assignment always works. When you want it: a fine-grained reactive system (SolidJS-style): Reactive::Property, Reactive.computed, effects with automatic dependency tracking, Reactive.bind scoped to any event handler's lifetime, ObservableList for collection views, batching, and a reactive_property macro for widget classes. Assign property.value = x — every bound widget repaints itself:

Source: tests/misc/reactive.cr — full tour: tests/reactive/reactive.cr

High-performance rendering

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

Screen capture

Any window or single widget can record itself and produce a still PNG (encoded in-process, no external tools), any other format — APNG, GIF, MP4, WebM, JPEG, … — by piping frames to 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: tests/misc/minesweeper.cr — the game from examples/games/minesweeper/minesweeper.cr, playing itself through the scripted input paths below

Scripting and automation

Scripted input goes through the exact code paths that real input uses — window.press 'x' / window.press Key::Down / window.type "hello" for keys, window.click x, y / window.wheel :up, x, y for the mouse (with window.emit some_event and window.dispatch_mouse ... as the general escape hatches).

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.

Direct (inline) mode

Apps 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

(Note: the above feature is currently experimental.)

Remote control

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.)

Ready-to-use example apps

Programs under examples/ are complete, usable applications, meant as templates for your own.

And more

  • 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/.nfo art decoded and rendered with sub-cell detail.
  • Docs with pictures — crystal docs embeds each widget's capture into its API documentation automatically.

Other contributed shards

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.

Hello world

require "crysterm"

include Crysterm
include Crysterm::Widgets

# A `Window` is the surface your widgets live on.
window = Window.new title: "hello"

# (`Widget::Box`, not bare `Box` — that name is taken by Crystal's stdlib;
# most other widgets are directly visible through `include Crysterm::Widgets`.)
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: Style.new(fg: "yellow", bg: "blue", border: true)

# `q` / Ctrl-Q quit by default. Run the main loop:
window.exec

Examples

git 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      # 6 representative widgets, animated (1/2)
crystal tests/misc/widgets2.cr     # 6 more representative widgets (2/2)

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/claude/claude.cr           # a Claude-style rich-text chat session
crystal examples/text/editor/editor.cr      # a working Unicode text editor
crystal examples/terminal/emulator/emulator.cr  # a minimal real terminal emulator
crystal examples/terminal/tid/tid.cr        # terminal identification, live
crystal examples/screen/multiple/multiple.cr    # two windows, one app
crystal examples/direct/completer/completer.cr  # inline (fzf-style) mode
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/.)

Dependencies and versioning

Crysterm and associated shards (tput, event_handler, term_colors, etc.) follow semantic versioning, with releases tagged vX.Y.Z. Crysterm's shard.yml pins each of those dependencies to its major version (e.g. version: ~> 2.0), so a plain clone plus shards install resolves to released versions, with minor and patch updates picked up.

To develop against the current master HEADs instead, place shard.override.yml next to shard.yml:

# shard.override.yml — track master HEADs instead of releases
dependencies:
  tput:
    github: crystallabs/tput
    branch: master
  crystallabs-helpers:
    github: crystallabs/crystallabs-helpers.cr
    branch: master
  term_colors:
    github: crystallabs/term_colors
    branch: master
  event_handler:
    github: crystallabs/event_handler
    branch: master
  gpm:
    github: crystallabs/gpm.cr
    branch: master
  pnggif:
    github: crystallabs/pnggif
    branch: master
  superconf:
    github: crystallabs/superconf.cr
    branch: master
  unibilium:
    github: crystallabs/unibilium.cr
    branch: master
  unibilium-shim:
    github: crystallabs/unibilium-shim.cr
    branch: master

For working with local checkouts, path: ../tput.cr entries instead of branch: ... work the same way.

Testing

Run crystal spec as usual.

Documentation

Run crystal docs as usual for plain API docs.

For the full docs — with every widget's screenshot or animation embedded into its class documentation — run the docs pipeline instead:

crystal tools/test.cr

Invoked as shown, it (re)captures any stale example outputs, embeds each widget's capture into its doc comment, and builds crystal docs with the images copied into the docs/ tree. Such improved documentation is available as usual in docs/index.html.

To narrow the run, name a target and/or an action, e.g. re-make just one widget's captures with crystal tools/test.cr -- tests/widget/button.cr, or only re-insert image links and rebuild docs with crystal tools/test.cr -- --doc-comments --docs.

Thanks

Repository

crysterm

Owner
Statistic
  • 149
  • 10
  • 0
  • 6
  • 10
  • 1 day ago
  • October 24, 2019
License

GNU Affero General Public License v3.0

Links
Synced at

Mon, 17 Aug 2026 17:32:21 GMT

Languages