term2
Term2
A Crystal port of the Bubble Tea terminal UI library, built with concurrent ML (cml) for concurrency.
Features
- Elm Architecture: Model-Update-View pattern for building terminal applications
- Concurrent ML: Built on cml for efficient concurrency
- 200+ Key Sequences: Support for xterm, urxvt, linux console, VT100/VT220
- Mouse Support: SGR and legacy X10 mouse protocols
- Focus Reporting: FocusIn/FocusOut events
- Alternate Screen: Clean terminal restoration
- Components: Spinner, ProgressBar, TextInput, CountdownTimer
- Cross-platform: Works on Linux, macOS, and Windows
Installation
-
Add the dependency to your
shard.yml:dependencies: term2: github: dsisnero/term2 cml: github: dsisnero/cml -
Run
shards install
Quick Start
require "term2"
include Term2::Prelude
class CounterModel < Model
getter count : Int32
def initialize(@count = 0); end
def init : Cmd
Cmd.none
end
def update(msg : Message) : {Model, Cmd}
case msg
when KeyMsg
case msg.key.to_s
when "q", "ctrl+c"
{self, Term2.quit}
when "+", "="
{CounterModel.new(@count + 1), Cmd.none}
when "-", "_"
{CounterModel.new(@count - 1), Cmd.none}
else
{self, Cmd.none}
end
else
{self, Cmd.none}
end
end
def view : String
"Count: #{@count}\n\nPress +/- to change, q to quit"
end
end
Term2.run(CounterModel.new)
Prelude & Aliases
Including Term2::Prelude provides convenient aliases for common types:
Model,Cmd,Message- Core typesTC- Alias forTerm2::Components(e.g.,TC::TextInput)KeyMsg,WindowSizeMsg,QuitMsg- Common messagesS- Alias forTerm2::S(Style builder)
Program Options
Configure your application by passing options to Term2.run:
Term2.run(model, [
WithAltScreen.new, # Use alternate screen buffer
WithMouseAllMotion.new, # Enable mouse tracking
WithReportFocus.new, # Enable focus reporting
] of Term2::ProgramOption)
Available options:
WithAltScreen- Use alternate screen bufferWithMouseAllMotion- Track all mouse motion (hover)WithMouseCellMotion- Track mouse drag onlyWithReportFocus- Report focus in/out eventsWithInput(io)- Custom input sourceWithOutput(io)- Custom output destinationWithFPS(fps)- Set frame rateWithoutRenderer- Disable rendering (headless mode)WithoutCatchPanics- Disable panic recoveryWithoutBracketedPaste- Disable bracketed paste
Text Styling DSL
Term2 provides a fluent API for styled text output without raw escape codes:
Method 1: String Extensions (Simple Single Styles)
puts "Hello".bold
puts "Error!".red
puts "Warning".yellow.bold # Note: chaining returns nested codes
Method 2: S Builder (Chained/Composed Styles)
# Method chaining with .apply()
puts S.bold.cyan.apply("Styled text")
puts S.red.on_white.underline.apply("Alert!")
# Pipe operator shorthand
puts S.green.bold | "Success!"
puts S.bright_magenta.italic | "Fancy"
# 256-color palette
puts S.fg(208).bold | "Orange"
# True color RGB
puts S.fg(100, 149, 237).bold | "Cornflower Blue"
puts S.bg(30, 30, 30).white | "Dark background"
Available Styles
Text Attributes:
.bold,.faint/.dim,.italic,.underline.blink,.reverse,.hidden,.strike
Foreground Colors:
- Standard:
.black,.red,.green,.yellow,.blue,.magenta,.cyan,.white,.gray - Bright:
.bright_red,.bright_green,.bright_yellow,.bright_blue,.bright_magenta,.bright_cyan,.bright_white - 256-color:
.fg(0-255) - RGB:
.fg(r, g, b)
Background Colors:
- Standard:
.on_black,.on_red,.on_green,.on_yellow,.on_blue,.on_magenta,.on_cyan,.on_white - 256-color:
.bg(0-255) - RGB:
.bg(r, g, b)
Handling Input
Keyboard
def update(msg : Message) : {Model, Cmd}
case msg
when KeyMsg
case msg.key.to_s
when "q"
{self, Term2.quit}
when "up", "k"
# ...
end
end
end
Handling Input
Keyboard
def update(msg : Message, model : Model)
case msg
when KeyPress
case msg.key
when "up", "k" then move_up(model)
when "down", "j" then move_down(model)
when "enter" then select(model)
when "ctrl+c" then {model, Cmd.quit}
else {model, nil}
end
else
{model, nil}
end
end
Mouse
def update(msg : Message, model : Model)
case msg
when MouseEvent
case msg.action
when MouseEvent::Action::Press
handle_click(model, msg.x, msg.y, msg.button)
when MouseEvent::Action::Move
handle_hover(model, msg.x, msg.y)
end
else
{model, nil}
end
end
Focus
def update(msg : Message, model : Model)
case msg
when FocusMsg
# Window gained focus
{model.with_focused(true), nil}
when BlurMsg
# Window lost focus
{model.with_focused(false), nil}
else
{model, nil}
end
end
Commands
# No-op
Cmd.none
nil
# Quit the application
Cmd.quit
# Batch multiple commands
Cmd.batch(cmd1, cmd2, cmd3)
# Run commands in sequence
Cmd.sequence(cmd1, cmd2, cmd3)
# Tick command for timers
Cmd.tick(1.second) { |t| TickMsg.new(t) }
# Custom command
Cmd.new { MyCustomMsg.new(result) }
Components
Spinner
spinner = Components::Spinner.new(
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
interval: 100.milliseconds
)
model, cmd = spinner.init("Loading...")
ProgressBar
progress = Components::ProgressBar.new(
width: 30,
complete_char: '█',
incomplete_char: '░',
show_percentage: true
)
model, cmd = progress.init
TextInput
input = Components::TextInput.new(
placeholder: "Type here...",
max_length: 50
)
model, cmd = input.init(focused: true)
Documentation
Development
Running Tests
# Run all tests
crystal spec
# Run interactive tests (requires TTY)
TERM2_TEST_TTY=1 crystal spec --tag interactive
# Run benchmarks
crystal run --release benchmarks/benchmark.cr
Project Structure
src/
term2.cr # Main module, Program, Cmd, KeyReader
base_types.cr # Model, Message, Key, KeyType
key_sequences.cr # Escape sequence mappings
mouse.cr # Mouse event handling
renderer.cr # StandardRenderer, NilRenderer
terminal.cr # Terminal control utilities
view.cr # View layout system
program_options.cr # Program configuration
Contributing
- Fork it (https://github.com/dsisnero/term2/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT License - see LICENSE
Repository
term2
Owner
Statistic
- 0
- 0
- 0
- 0
- 1
- about 6 hours ago
- November 26, 2025
License
MIT License
Links
Synced at
Wed, 26 Nov 2025 08:26:42 GMT
Languages