SkillAgentSearch skills...

Termisu

🍮 Minimalistic API for writing text-based user interfaces in pure Crystal

Install / Use

/learn @omarluq/Termisu

README

Termisu

Crystal Version License: MIT Tests Version codecov Docs termisu.io Maintained CodeRabbit Pull Request Reviews Ask DeepWiki Made with Love

<!-- [![All Contributors](https://img.shields.io/github/all-contributors/omarluq/termisu?style=flat&labelColor=171717&color=7f8c8d)](https://github.com/omarluq/termisu/graphs/contributors)--> <img src="assets/termisu.png" align="right" alt="Termisu Logo" width="200"/>

Termisu (/ˌtɛr.mɪˈsuː/ — like tiramisu, but for terminals) is a library that provides a sweet and minimalistic API for writing text-based user interfaces in pure Crystal. It offers an abstraction layer over terminal capabilities through cell-based rendering with double buffering, allowing efficient and flicker-free TUI development. The API is intentionally small and focused, making it easy to learn, test, and maintain. Inspired by termbox, Termisu brings similar simplicity and elegance to the Crystal ecosystem.

[!IMPORTANT] While the API is fairly stable at this point this is still pre 1.0 software and is not battle tested, you might encounter some bugs, please help improve termisu by reporting them.

<a href="https://sonarcloud.io/project/overview?id=omarluq_termisu"><img src="https://sonarcloud.io/images/project_badges/sonarcloud-dark.svg" alt="SonarCloud Quality Gate"/></a>

Installation

  1. Add the dependency to your shard.yml:
dependencies:
  termisu:
    github: omarluq/termisu
  1. Run shards install

Usage

require "termisu"

termisu = Termisu.new

begin
  # Set cells with colors and attributes
  termisu.set_cell(0, 0, 'H', fg: Termisu::Color.red, attr: Termisu::Attribute::Bold)
  termisu.set_cell(1, 0, 'i', fg: Termisu::Color.green)
  termisu.set_cursor(2, 0)
  termisu.render

  # Event loop with keyboard and mouse support
  termisu.enable_mouse
  loop do
    if event = termisu.poll_event(100)
      case event
      when Termisu::Event::Key
        break if event.key.escape?
        break if event.key.lower_q?
      when Termisu::Event::Mouse
        termisu.set_cell(event.x, event.y, '*', fg: Termisu::Color.cyan)
        termisu.render
      end
    end
  end
ensure
  termisu.close
end

See examples/ for complete demonstrations:

  • showcase.cr - Colors, attributes, and input handling
  • animation.cr - Timer-based animation with async events

Termisu Showcase

API

Initialization

termisu = Termisu.new  # Enters raw mode + alternate screen
termisu.close          # Cleanup (always call in ensure block)

Terminal

termisu.size                  # => {width, height}
termisu.alternate_screen?     # => true/false
termisu.raw_mode?             # => true/false
termisu.current_mode          # => Mode flags or nil

Cell Buffer

termisu.set_cell(x, y, 'A', fg: Color.red, bg: Color.black, attr: Termisu::Attribute::Bold)
termisu.clear                 # Clear buffer
termisu.render                # Apply changes (diff-based)
termisu.sync                  # Force full redraw

Cursor

termisu.set_cursor(x, y)
termisu.hide_cursor
termisu.show_cursor

Events

# Blocking
event = termisu.poll_event                # Block until event
event = termisu.wait_event                # Alias

# With timeout
event = termisu.poll_event(100)           # Timeout (ms), nil on timeout
event = termisu.poll_event(100.milliseconds)

# Non-blocking (select/else pattern)
event = termisu.try_poll_event            # Returns nil immediately if no event

# Iterator
termisu.each_event do |event|
  case event
  when Termisu::Event::Key        then # keyboard
  when Termisu::Event::Mouse      then # mouse click/move
  when Termisu::Event::Resize     then # terminal resized
  when Termisu::Event::Tick       then # timer tick (if enabled)
  when Termisu::Event::ModeChange then # mode switched
  end
end

Event Types

Event::Any is the union type: Event::Key | Event::Mouse | Event::Resize | Event::Tick | Event::ModeChange

# Event::Key - Keyboard input
event.key                          # => Input::Key
event.char                         # => Char?
event.modifiers                    # => Input::Modifier
event.ctrl? / event.alt? / event.shift? / event.meta?

# Event::Mouse - Mouse input
event.x, event.y                   # Position (1-based)
event.button                       # => Mouse::Button
event.motion?                      # Mouse moved while button held
event.press?                       # Button press (not release/motion)
event.wheel?                       # Scroll wheel event
event.ctrl? / event.alt? / event.shift?

# Mouse::Button enum
event.button.left?
event.button.middle?
event.button.right?
event.button.release?
event.button.wheel_up?
event.button.wheel_down?

# Event::Resize - Terminal resized
event.width, event.height          # New dimensions
event.old_width, event.old_height  # Previous (nil if unknown)
event.changed?                     # Dimensions changed?

# Event::Tick - Timer tick (for animations)
event.frame                        # Frame counter (UInt64)
event.elapsed                      # Time since timer started
event.delta                        # Time since last tick
event.missed_ticks                 # Ticks missed due to slow processing (UInt64)

# Event::ModeChange - Terminal mode changed
event.mode                         # New mode (Terminal::Mode)
event.previous_mode                # Previous mode (Terminal::Mode?)
event.changed?                     # Did mode actually change?
event.to_raw?                      # Transitioning to raw mode?
event.from_raw?                    # Transitioning from raw mode?
event.to_user_interactive?         # Entering canonical/echo mode?
event.from_user_interactive?       # Leaving canonical/echo mode?

Mouse & Keyboard

termisu.enable_mouse               # Enable mouse tracking
termisu.disable_mouse
termisu.mouse_enabled?

termisu.enable_enhanced_keyboard   # Kitty protocol (Tab vs Ctrl+I)
termisu.disable_enhanced_keyboard
termisu.enhanced_keyboard?

Timer (for animations)

# Sleep-based timer (portable, good for most use cases)
termisu.enable_timer(16.milliseconds)    # ~60 FPS tick events

# Kernel-level timer (Linux timerfd/epoll, macOS kqueue)
# More precise timing, better for high frame rates
termisu.enable_system_timer(16.milliseconds)

termisu.disable_timer                    # Disable either timer type
termisu.timer_enabled?
termisu.timer_interval = 8.milliseconds  # Change interval at runtime

Timer Comparison

| Feature | Timer (sleep) | SystemTimer (kernel) | | --------------------- | -------------------------------- | --------------------------------------------- | | Mechanism | sleep in fiber | timerfd/epoll (Linux), kqueue (macOS) | | Precision | Good, scheduler-dependent jitter | Higher precision, kernel-driven | | Missed tick detection | Yes (channel backpressure drops) | Yes (kernel expirations + backpressure drops) | | Portability | All platforms | Linux, macOS, BSD | | Best for | Simple/portable animation loops | High-refresh loops and tighter timing |

Current Probe Results (No Render I/O)

Measured on this codebase with local probe scripts:

| Scenario | Mean Delta | Approx FPS | Missed Ticks | | --------------------------------------- | ---------- | ---------- | ------------ | | Timer (sleep) @144 idle | 7.003ms | 142.8 | 0 | | SystemTimer @144 idle | 6.944ms | 144.0 | 0 | | Timer (sleep) @144 +4ms work | 7.005ms | 142.7 | 0 | | SystemTimer @144 +4ms work | 6.945ms | 144.0 | 0 | | SystemTimer @144 + input source enabled | 6.944ms | 144.0 | 0 |

Notes:

  • These numbers isolate event-loop/timer behavior and do not include terminal render/flush costs.
  • End-to-end FPS with heav

Related Skills

View on GitHub
GitHub Stars25
CategoryContent
Updated1d ago
Forks2

Languages

Crystal

Security Score

95/100

Audited on Apr 4, 2026

No findings