github.com/nogginly/termify.cr
0.7.0 / published Sep 10, 2026 / repository
A terminal rendering support shard for Crystal applications
Termify.cr
A Crystal shard for rendering Markdown to terminal, with an emphasis on streaming for the most part.
AI Usage
See DISCLOSURE for how I used AI for this project.
Installation
-
Add the dependency to your
shard.yml:dependencies: termify: github: nogginly/termify.cr -
Run
shards install
Usage
require "termify"
Setup platform Terminal
term = Termify.terminal
# Enable VT console
term.setup_console
# Ensure we restore terminal console settings
at_exit { term.restore_console }
Terminal stands on its own. It needs nothing from the Markdown renderer, so a
plain CLI tool can use it, and the ScrollRegion below, without rendering any
Markdown at all.
Scrolling within part of the screen
ScrollRegion confines output to a fixed number of lines, so streaming text
scrolls within them while the rest of the screen stays put. Height is clamped
to 3..10.
region = Termify::ScrollRegion.new(term, 5)
region.start
# ... anything printed now scrolls within those 5 lines
region.stop
See scrollcat for a working example.
Rendering Markdown
Termify.render_markdown do |io|
io.puts "# Hello"
io.puts "_Hello_, **World!**"
end
Custom stylesheet
Here's an example of a custom stylesheet (from md2term sample):
STYLESHEET = Termify.markdown_stylesheet({
"h1" => {
bold: true,
line_prefix: "# ".colorize(:dark_gray).to_s,
newline_after: true
},
"h2" => {bold: true, line_prefix: "## ".colorize(:dark_gray).to_s, newline_after: true, newline_before: true},
"h3" => {bold: true, line_prefix: "### ".colorize(:dark_gray).to_s, newline_after: true, newline_before: true},
"h4" => {bold: true, fg: "white", line_prefix: "#### ".colorize(:dark_gray).to_s},
"h5" => {bold: true},
"h6" => {bold: true},
"code_block" => {
fg: :light_cyan, line_number_format: "%3d: ",
highlight_theme: "catppuccin-macchiato",
gutter_style: {dim: true},
},
"code_inline" => {fg: :red},
"html_tag" => {dim: true},
"block_html" => {dim: true},
"list_item" => {newline_after: true, newline_before: true},
"block_quote" => {line_prefix: "│ ", newline_after: true, newline_before: true, bg: "Grey7"},
})
Termify.render_markdown(STDOUT, STYLESHEET) do |md_io|
# send your Markdown to `md_io`
end
Progress while gathering
Most Markdown renders as it arrives. Two things cannot: a table needs every row before it can size its columns, and a code block needs its whole body before it can be syntax highlighted. When Markdown is streaming in slowly, output stops for as long as that takes, and a reader cannot tell if the app has locked up.
Pass an on_gather handler to hear about it so you can control what to show and when.
handler = ->(event : Termify::Markdown::GatherEvent) {
case event.phase
in .started? then spinner.start(event.kind)
in .progressed? then spinner.count = event.units # rows, or lines
in .finished? then spinner.stop
end
}
Termify.render_markdown(STDOUT, STYLESHEET, on_gather: handler) do |md_io|
# send your Markdown to `md_io`
end
Nothing is written between Started and Finished, so a spinner drawn on the
current line can be cleared before the finished block lands on it. A Started is
always followed by a Finished, including when a document ends mid-table, and a
handler that raises will not stop the render.
See md2term for a working spinner, which reads slowly
enough that you can watch it.
Credits
- Tablo, for the table rendering
- Tartrazine, for the code syntax highlighting
Development
See DEVELOPMENT
Contributions, by invitation!
With apologies, at this time contributions are by invitation only and limited to people I know and see often.
These are early days for Termify and I am busy with family and work.
At this time I want to work on this at a manageable pace.