[*] Opal

Next-Generation Terminal User Interface (TUI) & CLI DSL Framework for Crystal

CI Docs Crystal License: MIT

Pure Crystal. Zero external C library dependencies (no ncurses). Native Windows, Linux, and macOS support.


asciicast


>> What is Opal?

Opal is an all-in-one terminal framework for Crystal designed to build world-class command-line interfaces, micro-interactive prompts, and full-screen terminal applications.

It merges the best paradigms from modern terminal engineering into a cohesive, idiomatic Crystal DSL:


[PKG] Installation

Add Opal to your project's shard.yml:

dependencies:
  opal:
    github: sol-vin/opal
    version: ~> 0.1.0

Then install dependencies:

shards install

Require Opal in your Crystal code:

require "opal"

[TOC] Table of Contents


[RUN] Quick Start

1. Build an Interactive Setup Wizard with Opal.form

require "opal"

result = Opal.form("Project Setup") do |f|
  f.text "name", "Project Name:", default: "my-app", required: true
  f.password "token", "API Token:", min_length: 8
  f.select "db", "Database:", ["PostgreSQL", "SQLite", "MySQL"]
  f.multi_select "addons", "Addons:", ["Redis", "Elasticsearch", "GraphQL"]
  f.confirm "deploy", "Auto-deploy to staging?", default: true
end

if config = result
  puts "Created project #{config["name"]} using #{config["db"]}!"
end

2. Live Fuzzy Filter with Split Preview

require "opal"

branches = ["main", "staging", "feat/auth", "feat/fuzzy-finder", "fix/timeout"]

selected = Opal.filter(
  items: branches,
  title: "Git Switcher",
  preview: ->(b : String) { "Branch #{b}\nStatus: Clean\nUpdated: 5m ago" }
)

puts "Switched to branch: #{selected}" if selected

[DEMO] Live Interactive Showcases

Real, live recordings captured directly from Opal running in terminal sessions (click any recording to open in asciinema):

| Feature Demo | Live Terminal Asciicast | | :--- | :--- | | Grand 13-Slide Interactive Showcase Tour
• System prerequisites & capabilities check (by sol.vin)
• Radial spotlight masking with feathered alpha dithering
• Scissor clipping & buffer memory ops (copy, paste, invert, rotate)
• Real-world app: OpalChat 2-pane split with unread badges
• Real-world app: Playable OpalPong with 60Hz decoupled game loop
• 3D Color Picker & Harmonies Palette Studio
• High-visibility 3x5 block digits (Digits) & digital clock
• Mermaid diagram viewer (Flowchart, Sequence, State, Class)
• TUI HTML web browser with history & OSC 8 hyperlinks
• 6 Dropdown style presets & 1/8th fractional Unicode meters
• Multi-shader compositing pipeline (Matrix + CRT + Glitch + Bloom)
• Tweens & 16 mathematical easing curves
• Gamepad controller focus traversal & virtual cursor
• [Ctrl+S] ScreenBuffer snapshot & clipboard copy toast
• [c] Source code viewer & [?] Markdown guide on every slide | asciicast | | Multi-Field Form Wizard
• Tab / Shift+Tab focus navigation
• Masked secret/password inputs
• Live inline validation feedback
• Multi-select checkboxes | asciicast | | Live Fuzzy Search & Split Preview
• Realtime sub-millisecond filtering
• Word-boundary rune highlighting
• Instant split details pane | asciicast | | Cluster Analytics Dashboard
• Rolling Unicode Sparklines
• Colorized percentage Gauges
• Horizontal BarCharts & Trees
• Catppuccin Mocha theme | asciicast | | Ghost-Text Autocomplete & Line Editor
• Modern fish/zsh inline suggestions
• Single-keystroke Tab expansion
• Full interactive line editing | asciicast | | CLI Toolchain & Auto Help
• Colored help generator
• Animated ANSI spinner
• In-place Unicode progress bar | asciicast |

[!TIP] All recordings are managed under installation IDs 2a10705a-74a3-4c4f-b46a-7fd92d0c3b93 and 93d19fef-0835-4ebf-b218-cda99edbe21b. You can permanently link them to your asciinema account via https://asciinema.org/connect/2a10705a-74a3-4c4f-b46a-7fd92d0c3b93.


[DOC] Multi-Field Form & Wizard DSL

asciicast

Traditional CLI prompts ask one question at a time and prevent reviewing earlier inputs. Opal.form presents an interactive card where all fields are visible simultaneously, users navigate using Tab / Shift+Tab, and live validation catches mistakes instantly.

result = Opal.form("New Microservice") do |f|
  f.text "service", "Service Name:", required: true
  f.text "port", "HTTP Port:", default: "8080"
  f.password "secret", "Secret Key:", min_length: 6
  f.select "tier", "Hosting Tier:", ["Small", "Medium", "Large"]
  f.multi_select "plugins", "Plugins:", ["Metrics", "Tracing", "Auth"]
  f.confirm "enabled", "Enable service immediately?", default: true

  # Custom real-time validation
  f.validate "port" do |val|
    (val.to_i? && (1024..65535).includes?(val.to_i)) ? nil : "Port must be 1024-65535"
  end
end

[SEARCH] Live Fuzzy Search & Filter

asciicast

Fast, keystroke-responsive fuzzy filtering inspired by fzf:

choice = Opal.filter(
  items: Dir["src/**/*.cr"],
  title: "Fuzzy File Finder",
  preview: ->(path : String) { File.read(path).lines.first(15).join("\n") }
)

[DIR] Interactive File Dialog & Explorer

Browse file systems with folder/file icons, formatted human-readable file sizes, live directory search filtering, and split file preview:

# Standalone interactive file picker
selected_file = Opal.file_dialog(initial_path: "./src", mode: :open_file)
puts "Chosen file: #{selected_file}" if selected_file

# Declaratively inside a layout tree
Opal.render_ui(width: 80, height: 20) do |ui|
  ui.file_dialog(initial_path: ".")
end

[UI] TrueColor 24-Bit Color Picker

Interactive color palette studio with Red, Green, Blue channel sliders, live TrueColor preview swatches, hex #RRGGBB calculations, luminance, and designer preset swatches:

# Standalone interactive color picker
picked_color = Opal.pick_color(Opal::Color.hex("#89B4FA"))
if c = picked_color
  puts "Selected color: #{c.to_hex} (RGB: #{c.to_rgb})"
end

# Declaratively in an element tree
Opal.render_ui(width: 60, height: 16) do |ui|
  ui.color_picker(initial_color: Opal::Color.cyan)
end

[3D] 2D & 3D Spatial Color Picker

Choose colors in continuous 2D and 3D geometric spaces. Features real-time pitch/yaw rotation (w/a/s/d), virtual cursor raycasting (↑/↓/←/→), depth buffering (Z-buffer), and surface sampling across multiple projection shapes:

# Launch standalone interactive 3D color picker
chosen = Opal.pick_color_3d(initial_color: Opal::Color.hex("#F38BA8"), shape: :cube_3d)
if c = chosen
  puts "Selected color: #{c.to_hex} (R: #{c.r}, G: #{c.g}, B: #{c.b})"
end

# Declaratively in a UI tree
Opal.render_ui(width: 80, height: 24) do |ui|
  ui.color_picker_3d(shape: :sphere_3d, scale: 7.0)
end

[FX] Text Shaders & Terminal FX

Manipulate the character buffer like a GPU fragment shader. Opal brings pixel-shader thinking to text terminals with normalized UV coordinates (u, v) ∈ [0, 1], double-buffered ping-pong rendering, composable multi-pass pipelines, and sub-region scoping:

# Compose a multi-pass post-processing pipeline
pipeline = Opal.shader_pipeline do |pipe|
  # Procedural plasma wave background
  pipe.plasma(speed: 2.5, scale: 6.0)

  # Custom fragment shader modifying character and colors
  pipe.fragment(label: "tint_and_dissolve") do |ctx|
    if ctx.noise(ctx.u * 10, ctx.v * 10) > 0.6
      ctx.fg = Opal::Color.hex("#89DCEB")
      ctx.char = '*'
    end
  end

  # Terminal retro scanlines and curvature vignette
  pipe.crt(scanline_opacity: 0.25, curvature: 0.15)
end

# Execute over an Opal Buffer
pipeline.render(buffer, time: 1.5)

[VIZ] Data Visualizations

asciicast

Render rich dashboards and metrics without graphics libraries:

Sparklines

# Output:  ▂▄▇▇█▆▄▃▂▂
puts Opal::UI::Sparkline.render_to_string([10.0, 15.0, 25.0, 80.0, 95.0, 60.0])

BarCharts, Gauges & Trees in UI Trees

Opal.render_ui(width: 70, height: 20) do |ui|
  ui.vstack(spacing: 1) do |v|
    # Percentage Gauge
    v.gauge 0.76, label: "Disk Usage", color: :yellow

    # Horizontal Bar Chart
    v.barchart(title: "Memory Allocation") do |bc|
      bc.bar "Web", 420, color: :green
      bc.bar "Worker", 850, color: :cyan
      bc.bar "DB", 1200, color: :red
    end

    # Hierarchical Tree
    v.tree(title: "Service Graph") do |t|
      t.node("API Gateway", icon: "[NET]") do |gateway|
        gateway.add("Auth Service", icon: "[SEC]")
        gateway.add("Search Node", icon: "[SEARCH]")
      end
    end
  end
end

[WIN] Buffer Blitting, Modals & Toasts

Floating Modal Dialog

Center a dialog box over any screen buffer with automatic background dimming:

modal = Opal::UI::Modal.new(
  title: "Confirm Deletion",
  message: "Are you sure you want to drop database 'prod'?",
  buttons: ["Cancel", "Confirm Drop"],
  selected_button: 1
)
modal.render(buffer, 0, 0, 80, 24)

Toast Notifications

Stack floating alerts in the top-right corner with auto-dismiss timers:

toasts = Opal::UI::ToastManager.new
toasts.add("Build Succeeded", "All 124 tests passed", level: :success, duration_ms: 3000)
toasts.add("Disk Warning", "Free space below 10%", level: :warning)

toasts.render_overlay(buffer, position: :top_right)

[DOC] Terminal Markdown Viewer

Convert Markdown documents into styled ANSI terminal text:

doc = <<-MD
# Opal Framework v1.0
Welcome to **Opal**! Build *beautiful* CLIs in Crystal.

### Features
- Zero external C dependencies
- 60fps delta rendering

```crystal
require "opal"
puts "Hello world!"

"Simplicity is prerequisite for reliability." MD

puts Opal.render_markdown(doc, width: 80)


---

## [UI] Theme Engine & Semantic Colors

Opal includes pre-registered designer palettes and semantic color tokens:

```crystal
# Switch theme dynamically
Opal.theme = :catppuccin_mocha # :dracula, :nord, :tokyo_night, :gruvbox, etc.

# Semantic color tokens
theme = Opal.theme
style = Opal.style
  .foreground(theme.primary)
  .background(theme.surface)
  .border_foreground(theme.border)

[SEARCH] Command Palette Overlay

Press Ctrl+P or Ctrl+K to summon an instant Spotlight action launcher:

palette = Opal::UI::CommandPalette.new
palette.add("git:commit", "Commit changes", category: "Git", shortcut: "ctrl+c") { commit_flow }
palette.add("file:open", "Open file picker", category: "File", shortcut: "ctrl+o") { open_picker }

[OSC] OSC 8 Hyperlinks & OSC 52 Clipboard

# Clickable hyperlink in modern terminals
puts Opal.hyperlink("View Source on GitHub", "https://github.com/sol-vin/opal")

# Copy directly to OS desktop clipboard over SSH and local sessions
Opal.copy_to_clipboard("API_KEY_SECRET_12345")

⏱️ Animation & Easing Engine

Smooth transitions, progress bars, and color interpolation:

# Easing functions
t = Opal::Animation.ease(:ease_in_out_cubic, 0.5)

# Color interpolation (e.g. green to red as CPU load increases)
normal_color = Opal::Color.green
alert_color  = Opal::Color.red
current_c    = Opal::Color.lerp(normal_color, alert_color, 0.75)

[CLI] CLI Application DSL

asciicast

app = Opal.cli("deployer", "Cloud deployment manager", "0.4.0") do
  option "-v", "--verbose", "Enable debug logging", type: :bool

  command "deploy", "Deploy application containers" do
    argument "service", "Service name to deploy"
    option "-c", "--concurrency=NUM", "Max concurrency", type: :int, default: 3

    run do |ctx|
      svc = ctx.argument("service")
      puts "Deploying #{svc} (concurrency: #{ctx.int("concurrency")})..."
    end
  end
end

app.run(ARGV)

[UI] Fluent Styling & Layout

Lipgloss-inspired declarative style chain with true visual string width calculation and 24-bit TrueColor:

# Composable styling
header_style = Opal.style
  .bold
  .foreground(Opal::Color.cyan)
  .background(Opal::Color.from("#1e1e2e"))
  .padding(1, 2)
  .border(:rounded)

puts header_style.render("Welcome to Opal")

# Double-buffered layout rendering
rendered = Opal::UI.render(width: 80, height: 10) do |ui|
  ui.box(border: :rounded, title: "System Info") do |b|
    b.vstack do |v|
      v.text("CPU: 8 cores active")
      v.text("Memory: 16 GB DDR5")
    end
  end
end
puts rendered

[UI] Declarative UI & Dual-Mode DSL Blending

[!NOTE] For in-depth architectural details and component tables, see the Architecture Guide: DSL & Blending.

Opal's UI DSL provides first-class support for dual-mode block execution (with builder yield builder) and OOP-DSL blending, allowing you to seamlessly mix declarative blocks with traditional component instances.

1. Concise Implicit Mode vs Explicit Receiver Mode

# Concise Implicit Syntax: No block parameters required
dashboard = Opal.render_ui(width: 80, height: 12) do
  box(title: "Status Monitor", border: :rounded) do
    vstack(spacing: 1) do
      text "Cluster Node 01", fg: :cyan
      gauge ratio: 0.85, label: "CPU: 85%"
      hstack(spacing: 2) do
        badge "HEALTHY", :green
        badge "US-EAST-1", :blue
      end
    end
  end
end

# Explicit Receiver Syntax: Full typed parameter control
explicit = Opal.render_ui(width: 80, height: 12) do |ui|
  ui.box(title: "Status Monitor", border: :rounded) do |b|
    b.vstack(spacing: 1) do |v|
      v.text "Cluster Node 01", fg: :cyan
    end
  end
end

2. Blending Traditional OOP Instances into Declarative DSL

Any component created with .new can be directly embedded into DSL blocks using add(el), << el, or custom(el):

# 1. Create components traditionally
my_chart = Opal::UI::LineGraph.new
my_chart.add_series("Traffic", [10.0, 45.0, 30.0, 90.0], :cyan)

my_log = Opal::UI::RichLog.new
my_log.log("Worker connected")

# 2. Embed into declarative DSL tree
screen = Opal::UI.build do
  vstack(spacing: 1) do
    text "System Overview"
    add my_chart        # via add
    self << my_log      # via shovel << operator
  end
end

3. Container Constructors with DSL Blocks

Container classes (Box, VStack, HStack, Screen, ModalScreen) accept declarative DSL blocks directly:

# Construct Box declaratively
box = Opal::UI::Box.new(title: "Block Box", border: :double) do
  text "Line 1"
  text "Line 2"
end

# Construct Screen with declarative root
screen = Opal::UI::Screen.new("main", title: "App Screen") do
  box(title: "Content") do
    text "Screen body"
  end
end

[PROMPT] Interactive Prompts

# Text, Confirm, Select, and Multi-Select
name = Opal.ask("Enter your username:", default: "developer")
confirmed = Opal.confirm("Continue with deployment?", default: true)
tier = Opal.select("Choose your deployment tier:", ["Small", "Medium", "Enterprise"])
features = Opal.multi_select("Select plugins:", ["Metrics", "Tracing", "RateLimiter"])

# Animated ANSI Spinner
Opal.spinner("Provisioning cluster resources...") do
  sleep 1.second
end

# In-Place Smooth Unicode Progress Bar
Opal.progress(total: 100, title: "Downloading Assets") do |bar|
  10.times do
    sleep 50.milliseconds
    bar.advance(10)
  end
end

[AUTO] Autocomplete & Ghost Text DSL

asciicast

Provide rich inline suggestions and command completion with zero terminal lag:

# Static dictionary or dynamic query provider
engine = Opal::Input::Autocomplete.new(["checkout", "commit", "push", "pull", "status", "rebase"])

# Interactive line editor with ghost-text preview
input = Opal::Input::TextInput.new(placeholder: "Type a git command...")
input.autocomplete = engine

# Readline loop with inline preview
# Press [Tab] to accept ghost completion
# Press [Enter] to submit

[CONTROL] Interactive Controls & Puppeting Engine

[!NOTE] For in-depth architectural design, sequence diagrams, and lifecycle specifications, see the Architecture Guide: Controls, Input Hooks & Engine.

Every interactive widget in Opal (Table, FilterList, ColorPicker, ColorPicker3D, FileDialog, and custom widgets) inherits from Opal::UI::Control < Opal::UI::Element. They implement standard input lifecycles, per-control input overrides, and code puppeting:

1. Default Inputs Lifecycle

Controls initialize standard keyboard and mouse bindings via setup_default_inputs:

picker = Opal::UI::ColorPicker.new
# Standard arrow keys, tabs, numbers, and mouse scrubbing work out of the box!

2. Custom Input Hooks & Interceptors (on_input)

Intercept or augment input handling per control without subclassing:

picker.on_input do |ctrl, event|
  if event.is_a?(Opal::Terminal::KeyEvent) && event.name == "x"
    ctrl.color = Opal::Color.hex("#FF0000") # Custom shortcut: Instant red
    true # Consumed!
  else
    false # Fall back to default inputs (+, -, arrows, tabs)
  end
end

3. Programmatic Puppeting from Code (puppet)

Automate controls without physical user input (e.g. background ticker or playback script):

table = Opal::UI::Table.new(headers: ["Job", "Status"])
table.puppet do |tbl, event|
  if event.is_a?(Opal::Terminal::KeyEvent) && event.name == "auto_tick"
    cur = tbl.selected_index || 0
    tbl.select((cur + 1) % tbl.rows.size)
    true
  else
    false
  end
end

# Drive the control programmatically:
table.handle_input(Opal::Terminal::KeyEvent.new("auto_tick"))

4. Multi-Control Focus Orchestration with Opal::UI::Engine

engine = Opal::UI::Engine.new([table, picker])

# Tab / Shift+Tab cycles focus automatically:
engine.handle_key(Opal::Terminal::KeyEvent.new("tab"))

# Inject synthetic inputs for headless tests:
engine.send_key("down")
engine.send_mouse(15, 10, Opal::Terminal::MouseButton::Left, Opal::Terminal::MouseAction::Press)

For a complete runnable demonstration, check out examples/13_control_input_hooks_and_puppeting.cr.


⌨️ KeyMap & MouseMap DSLs

# Declarative Keyboard Binding
key_map = Opal::Input::KeyMap.new
key_map.bind("ctrl+c", "Exit program") { exit }
key_map.bind("enter", "Confirm input") { save_data }
key_map.bind("up", "Navigate up") { cursor_up }

# Extended SGR Mouse Hit-Testing
mouse_map = Opal::Input::MouseMap.new
mouse_map.on_click(x_range: 2..15, y_range: 5..7) do
  puts "Deploy button clicked!"
end
mouse_map.on_scroll do |delta|
  scroll_view(delta)
end

[TEA] The Elm Architecture (TEA)

Build reactive terminal applications with pure state transitions:

record CounterModel, count : Int32 = 0 do
  include Opal::Tea::Model

  def init : Opal::Tea::Cmd
    Opal::Tea::Cmd.none
  end

  def update(msg : Opal::Tea::Msg) : {Opal::Tea::Model, Opal::Tea::Cmd}
    case msg
    when Opal::Tea::KeyMsg
      case msg.key
      when "up", "k"   then {CounterModel.new(count + 1), Opal::Tea::Cmd.none}
      when "down", "j" then {CounterModel.new(count - 1), Opal::Tea::Cmd.none}
      when "q"         then {self, Opal::Tea::Cmd.quit}
      else {self, Opal::Tea::Cmd.none}
      end
    else
      {self, Opal::Tea::Cmd.none}
    end
  end

  def view : String
    "Counter: #{count} (Press ↑/k, ↓/j, q to quit)"
  end
end

Opal::Tea::Program.new(CounterModel.new).run

[TEST] Testing with MockDriver

Test full TUI interactions headlessly without opening an actual terminal:

require "spec"
require "opal"

describe "Counter" do
  it "increments on keypress" do
    model = CounterModel.new
    new_model, cmd = model.update(Opal::Tea::KeyMsg.new("k"))
    new_model.as(CounterModel).count.should eq(1)
  end
end

[DEMO] Asciicast & Asciinema VCR Recording (opal/asciicast)

[!NOTE] For in-depth architectural design, format specifications, and parser details, see the Architecture Guide: Asciicast System.

Generate pixel-perfect, flicker-free terminal recordings in the standard Asciinema v2 (.cast) format with zero external dependencies.

This feature is modular and packaged as an optional require:

require "opal"
require "opal/asciicast"            # Screen recording, parsing, and driver
require "opal/asciicast/asciinema"  # VCR tape deck & playback engine

1. Tactile VCR Cassette Recording & Playback (Opal::VCR)

Control your recordings like a physical tape deck with record, capture, pause, resume, stop, save, and frame pacing:

# Block recording with automatic pacing and save
Opal::VCR.record("session.cast", width: 80, height: 24, title: "VCR Demo") do |vcr|
  # Capture initial buffer frame
  vcr.capture(buffer1, advance: 0.1)

  # Pace multi-frame transitions
  vcr.wait_frames(count: 3, delay_per_frame: 0.05)

  # Temporarily pause recording for background setup
  vcr.pause
  # ... private setup ...
  vcr.resume

  vcr.capture(buffer2, advance: 0.1)
  vcr.hold(1.5) # Freeze final frame for 1.5s
end

# Inspect and step through recorded frames
vcr = Opal::VCR.new
vcr.load("session.cast")

puts "Frames: #{vcr.total_frames} | Duration: #{vcr.duration}s"

# Step frame-by-frame
frame = vcr.next_frame
prev  = vcr.prev_frame
seek  = vcr.seek(1.0) # seek to 1 second
start = vcr.rewind

# Composite frame into a canvas while respecting existing UI overlays
target = Opal::UI::Buffer.new(100, 30)
vcr.render_frame(target, x: 2, y: 1, respect_overlays: true)

2. Direct Screen Buffer Recording (ScreenRecorder)

Record raw double-buffered frames (Opal::UI::Buffer) directly from your UI components:

recorder = Opal::Asciicast::ScreenRecorder.new("screen.cast", width: 80, height: 24)
recorder.start

# Render any UI element directly into buffer
buf = Opal::UI::Buffer.new(80, 24)
my_ui.render(buf, 0, 0, 80, 24)

# Capture exact frame without terminal flicker
recorder.capture_frame(buf, advance: 0.1)
recorder.hold(1.0)
recorder.stop.save

3. Headless Interactive Recording with Opal::Asciicast::Driver

Pass the headless driver into any Opal prompt, form wizard, or TEA program to record user interactions without an active terminal:

driver = Opal::Asciicast.create_driver(width: 80, height: 24, title: "Form Wizard Demo")

# Inject simulated keystrokes
driver.inject_key("enter")

# Run prompt headlessly
Opal.form("Setup", driver: driver) do |f|
  f.confirm "deploy", "Deploy now?", default: true
end

driver.save("demos/form.cast")

4. Parsing & Assertions with Opal::Asciicast.read

recording = Opal::Asciicast.read("session.cast")
puts "Duration: #{recording.duration}s | Outputs: #{recording.outputs.size}"

For a complete runnable demonstration, check out examples/14_asciicast_recording.cr and examples/18_dsl_blending_and_asciinema.cr.


[EXAMPLES] Examples

Explore all runnable examples in the examples/ directory:

Run any example:

crystal run examples/10_opal_tui_showcase.cr
crystal run examples/11_text_shaders.cr
crystal run examples/12_3d_color_picker.cr

[NET] Cross-Platform Support

| Platform | Terminal Backend | Colors | Mouse Support | | :--- | :--- | :--- | :--- | | Linux | POSIX termios, VT100, SGR | TrueColor, 256, ANSI 16 | Yes (SGR 1006) | | macOS | POSIX termios, VT100, SGR | TrueColor, 256, ANSI 16 | Yes (SGR 1006) | | Windows | Win32 Console API (ENABLE_VIRTUAL_TERMINAL_PROCESSING) + ANSI VT100 | TrueColor, 256, ANSI 16 | Yes (SGR 1006) |


[TOOL] Automated Versioning & Changelogs with Carbon

Opal uses Carbon for automated Git-commit-based semantic versioning, release management, and changelog orchestration. Every commit automatically synchronizes shard.yml, src/opal/version.cr, and compiles CHANGELOG.md.

# Check version alignment against Git commits
bin/carbon check

# Sync version to match Git commits
bin/carbon sync

# Synchronize changelog
bin/carbon changelog

[FILE] License

This project is licensed under the MIT License - see the LICENSE file for details.