Crystal Version License: MIT Tests Format Ameba Version Last Commit Commit Activity Pull Requests Closed Pull Requests PRs Welcome GitHub stars GitHub forks GitHub watchers Maintained Made with Love

Termisu

Termisu Logo

Termisu is a library that provides a 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.

[!WARNING] Termisu is still in development and is considered unstable. The API is subject to change, and you may encounter bugs or incomplete features. Use it at your own risk, and contribute by reporting issues or suggesting improvements!

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::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:

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

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

Event Types

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

# 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                       # => MouseButton
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?

# MouseButton 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

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)

termisu.enable_timer(16.milliseconds)    # ~60 FPS tick events
termisu.disable_timer
termisu.timer_enabled?
termisu.timer_interval = 8.milliseconds  # Change interval

Colors

Color.red, Color.green, Color.blue       # ANSI-8
Color.bright_red, Color.bright_green     # Bright variants
Color.ansi256(208)                       # 256-color palette
Color.rgb(255, 128, 64)                  # TrueColor
Color.from_hex("#FF8040")                # Hex string
Color.grayscale(12)                      # Grayscale (0-23)

color.to_rgb                             # Convert to RGB
color.to_ansi256                         # Convert to 256
color.to_ansi8                           # Convert to 8

Attributes

Termisu::Attribute::None
Termisu::Attribute::Bold
Termisu::Attribute::Dim
Termisu::Attribute::Cursive      # Italic
Termisu::Attribute::Italic       # Alias for Cursive
Termisu::Attribute::Underline
Termisu::Attribute::Blink
Termisu::Attribute::Reverse
Termisu::Attribute::Hidden
Termisu::Attribute::Strikethrough

# Combine with |
attr = Termisu::Attribute::Bold | Termisu::Attribute::Underline
strike = Termisu::Attribute::Strikethrough | Termisu::Attribute::Dim

Keys

# Key event properties
case event
when Termisu::Event::Key
  event.key                      # => Input::Key enum
  event.char                     # => Char? (printable character)
  event.modifiers                # => Input::Modifier flags
end

# Modifier checks (on Event::Key)
event.ctrl?                      # Ctrl held
event.alt?                       # Alt/Option held
event.shift?                     # Shift held
event.meta?                      # Meta/Super/Windows held

# Common shortcuts
event.ctrl_c?
event.ctrl_d?
event.ctrl_q?
event.ctrl_z?

# Check for any modifier
event.modifiers.none?            # No modifiers held
event.modifiers.ctrl?            # Direct modifier check
event.modifiers & Input::Modifier::Ctrl  # Bitwise check

# Key matching - Special keys
event.key.escape?
event.key.enter?
event.key.tab?
event.key.back_tab?              # Shift+Tab
event.key.backspace?
event.key.space?
event.key.delete?
event.key.insert?

# Key matching - Arrow keys
event.key.up?
event.key.down?
event.key.left?
event.key.right?

# Key matching - Navigation
event.key.home?
event.key.end?
event.key.page_up?
event.key.page_down?

# Key matching - Function keys (F1-F24)
event.key.f1?
event.key.f12?

# Key matching - Letters
event.key.q?                     # Case-insensitive (a? - z?)
event.key.lower_q?               # Case-sensitive lowercase
event.key.upper_q?               # Case-sensitive uppercase

# Key matching - Numbers
event.key.num_0?                 # num_0? - num_9?

# Key predicates
event.key.letter?                # A-Z or a-z
event.key.digit?                 # 0-9
event.key.function_key?          # F1-F24
event.key.navigation?            # Arrows + Home/End/PageUp/PageDown
event.key.printable?             # Has character representation
event.key.to_char                # => Char? for printable keys

Modifiers

Input::Modifier::None
Input::Modifier::Shift
Input::Modifier::Alt             # Alt/Option
Input::Modifier::Ctrl
Input::Modifier::Meta            # Super/Windows

# Combine with |
mods = Input::Modifier::Ctrl | Input::Modifier::Shift

# Check modifiers
mods.ctrl?
mods.shift?
mods.alt?
mods.meta?

Roadmap

Current Status: v0.1.0 (async event system complete)

| Component | Status | | ------------------- | ----------- | | Terminal I/O | ✅ Complete | | Terminfo | ✅ Complete | | Double Buffering | ✅ Complete | | Colors | ✅ Complete | | Termisu::Attributes | ✅ Complete | | Keyboard Input | ✅ Complete | | Mouse Input | ✅ Complete | | Event System | ✅ Complete | | Async Event Loop | ✅ Complete | | Resize Events | ✅ Complete | | Timer/Tick Events | ✅ Complete | | Unicode/Wide Chars | 🔄 Planned |

Completed

Planned

Inspiration

Termisu is inspired by and follows some of the design philosophy of:

Contributing

See CONTRIBUTING.md for detailed guidelines.

  1. Fork it (https://github.com/omarluq/termisu/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

License

The shard is available as open source under the terms of the MIT License.

Code of conduct

Everyone interacting in this project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.