class Termisu

Overview

Main Termisu class - Terminal User Interface library.

Provides a clean, minimal API for terminal manipulation by delegating all logic to specialized components: Terminal and Reader.

The async event system uses Event::Loop to multiplex multiple event sources:

Example:

termisu = Termisu.new

# Set cells with colors and attributes
termisu.set_cell(10, 5, 'H', fg: Color.red, bg: Color.black, attr: Attribute::Bold)
termisu.set_cell(11, 5, 'i', fg: Color.green)
termisu.set_cell(12, 5, '!', fg: Color.blue)

# Render applies changes (diff-based rendering)
termisu.render

termisu.close

Defined in:

termisu.cr
termisu/log.cr
termisu/version.cr

Constant Summary

Log = ::Log.for("termisu")

Main log instance for Termisu library

VERSION = {{ (`shards version`).chomp.stringify }}

Full version string from shard.yml

VERSION_MAJOR = 0
VERSION_MINOR = 1
VERSION_PATCH = 0
VERSION_STATE = nil

Constructors

Instance Method Summary

Constructor Detail

def self.new #

Initializes Termisu with all required components.

Sets up terminal I/O, rendering, input reader, and async event system. Automatically enables raw mode and enters alternate screen.

The Event::Loop is started with Input and Resize sources by default. Timer source is optional and can be enabled with #enable_timer.


[View source]

Instance Method Detail

def add_event_source(source : Event::Source) : self #

Adds a custom event source to the event loop.

Custom sources must extend Event::Source and implement the abstract interface: #start(channel), #stop, #running?, and #name.

If the event loop is already running, the source is started immediately. Events from the source will appear in #poll_event alongside built-in events.

Parameters:

  • source: An Event::Source implementation

Returns self for method chaining.

Example:

class NetworkSource < Termisu::Event::Source
  def start(output)
    # Start listening for network events
  end

  def stop
    # Stop listening
  end

  def running? : Bool
    @running
  end

  def name : String
    "network"
  end
end

termisu.add_event_source(NetworkSource.new)

[View source]
def alternate_screen?(*args, **options) #

Returns true if alternate screen mode is active.


[View source]
def alternate_screen?(*args, **options, &) #

Returns true if alternate screen mode is active.


[View source]
def clear #

Clears the cell buffer (fills with spaces).

Note: This clears the buffer, not the screen. Call render() to apply.


[View source]
def close #

Closes Termisu and cleans up all resources.

Performs graceful shutdown in the correct order:

  1. Stop event loop (stops all sources, closes channel, waits for fibers)
  2. Exit alternate screen
  3. Disable raw mode
  4. Close reader and terminal

The event loop is stopped first to ensure fibers that might be using the reader are terminated before the reader is closed.


[View source]
def disable_enhanced_keyboard(*args, **options) #

Enables enhanced keyboard protocol for disambiguated key reporting.

In standard terminal mode, certain keys are indistinguishable:

  • Tab sends the same byte as Ctrl+I (0x09)
  • Enter sends the same byte as Ctrl+M (0x0D)
  • Backspace may send the same byte as Ctrl+H (0x08)

Enhanced mode enables the Kitty keyboard protocol and/or modifyOtherKeys, which report keys in a way that preserves the distinction.

Note: Not all terminals support these protocols. Unsupported terminals will silently ignore the escape sequences and continue with legacy mode. Supported terminals include: Kitty, WezTerm, foot, Ghostty, recent xterm.

Example:

termisu.enable_enhanced_keyboard
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Key
      # Now Ctrl+I and Tab are distinguishable!
      if event.ctrl? && event.key.lower_i?
        puts "Ctrl+I pressed"
      elsif event.key.tab?
        puts "Tab pressed"
      end
    end
  end
end
termisu.disable_enhanced_keyboard

[View source]
def disable_enhanced_keyboard(*args, **options, &) #

Enables enhanced keyboard protocol for disambiguated key reporting.

In standard terminal mode, certain keys are indistinguishable:

  • Tab sends the same byte as Ctrl+I (0x09)
  • Enter sends the same byte as Ctrl+M (0x0D)
  • Backspace may send the same byte as Ctrl+H (0x08)

Enhanced mode enables the Kitty keyboard protocol and/or modifyOtherKeys, which report keys in a way that preserves the distinction.

Note: Not all terminals support these protocols. Unsupported terminals will silently ignore the escape sequences and continue with legacy mode. Supported terminals include: Kitty, WezTerm, foot, Ghostty, recent xterm.

Example:

termisu.enable_enhanced_keyboard
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Key
      # Now Ctrl+I and Tab are distinguishable!
      if event.ctrl? && event.key.lower_i?
        puts "Ctrl+I pressed"
      elsif event.key.tab?
        puts "Tab pressed"
      end
    end
  end
end
termisu.disable_enhanced_keyboard

[View source]
def disable_mouse(*args, **options) #

Enables mouse input tracking.

Once enabled, mouse events will be reported via poll_event. Supports SGR extended protocol (mode 1006) for large terminals and falls back to normal mode (1000) for compatibility.

Example:

termisu.enable_mouse
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Mouse
      puts "Click at #{event.x},#{event.y}"
    end
  end
end
termisu.disable_mouse

[View source]
def disable_mouse(*args, **options, &) #

Enables mouse input tracking.

Once enabled, mouse events will be reported via poll_event. Supports SGR extended protocol (mode 1006) for large terminals and falls back to normal mode (1000) for compatibility.

Example:

termisu.enable_mouse
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Mouse
      puts "Click at #{event.x},#{event.y}"
    end
  end
end
termisu.disable_mouse

[View source]
def disable_timer : self #

Disables the timer source.

Stops Tick events from being emitted. Safe to call when timer is already disabled.


[View source]
def each_event(timeout : Time::Span, &) #

Yields each event with timeout between events.

If no event arrives within timeout, yields nothing and continues. Useful when you need to do periodic work between events.

Parameters:

  • timeout: Maximum time to wait for each event

Example:

termisu.each_event(100.milliseconds) do |event|
  # Process event
end
# Can do other work between events when timeout expires

[View source]
def each_event(timeout_ms : Int32, &) #

Yields each event with timeout in milliseconds.


[View source]
def each_event(&) #

Yields each event as it becomes available.

Blocks waiting for each event. Use this for simple event loops.

Example:

termisu.each_event do |event|
  case event
  when Termisu::Event::Key
    break if event.key.escape?
  when Termisu::Event::Tick
    # Animation frame
  end
  termisu.render
end

[View source]
def enable_enhanced_keyboard(*args, **options) #

Enables enhanced keyboard protocol for disambiguated key reporting.

In standard terminal mode, certain keys are indistinguishable:

  • Tab sends the same byte as Ctrl+I (0x09)
  • Enter sends the same byte as Ctrl+M (0x0D)
  • Backspace may send the same byte as Ctrl+H (0x08)

Enhanced mode enables the Kitty keyboard protocol and/or modifyOtherKeys, which report keys in a way that preserves the distinction.

Note: Not all terminals support these protocols. Unsupported terminals will silently ignore the escape sequences and continue with legacy mode. Supported terminals include: Kitty, WezTerm, foot, Ghostty, recent xterm.

Example:

termisu.enable_enhanced_keyboard
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Key
      # Now Ctrl+I and Tab are distinguishable!
      if event.ctrl? && event.key.lower_i?
        puts "Ctrl+I pressed"
      elsif event.key.tab?
        puts "Tab pressed"
      end
    end
  end
end
termisu.disable_enhanced_keyboard

[View source]
def enable_enhanced_keyboard(*args, **options, &) #

Enables enhanced keyboard protocol for disambiguated key reporting.

In standard terminal mode, certain keys are indistinguishable:

  • Tab sends the same byte as Ctrl+I (0x09)
  • Enter sends the same byte as Ctrl+M (0x0D)
  • Backspace may send the same byte as Ctrl+H (0x08)

Enhanced mode enables the Kitty keyboard protocol and/or modifyOtherKeys, which report keys in a way that preserves the distinction.

Note: Not all terminals support these protocols. Unsupported terminals will silently ignore the escape sequences and continue with legacy mode. Supported terminals include: Kitty, WezTerm, foot, Ghostty, recent xterm.

Example:

termisu.enable_enhanced_keyboard
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Key
      # Now Ctrl+I and Tab are distinguishable!
      if event.ctrl? && event.key.lower_i?
        puts "Ctrl+I pressed"
      elsif event.key.tab?
        puts "Tab pressed"
      end
    end
  end
end
termisu.disable_enhanced_keyboard

[View source]
def enable_mouse(*args, **options) #

Enables mouse input tracking.

Once enabled, mouse events will be reported via poll_event. Supports SGR extended protocol (mode 1006) for large terminals and falls back to normal mode (1000) for compatibility.

Example:

termisu.enable_mouse
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Mouse
      puts "Click at #{event.x},#{event.y}"
    end
  end
end
termisu.disable_mouse

[View source]
def enable_mouse(*args, **options, &) #

Enables mouse input tracking.

Once enabled, mouse events will be reported via poll_event. Supports SGR extended protocol (mode 1006) for large terminals and falls back to normal mode (1000) for compatibility.

Example:

termisu.enable_mouse
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Mouse
      puts "Click at #{event.x},#{event.y}"
    end
  end
end
termisu.disable_mouse

[View source]
def enable_timer(interval : Time::Span = 16.milliseconds) : self #

Enables the timer source for animation and game loops.

When enabled, Tick events are emitted at the specified interval. Default interval is 16ms (~60 FPS).

Parameters:

  • interval: Time between tick events (default: 16ms for 60 FPS)

Example:

termisu.enable_timer(16.milliseconds) # 60 FPS

termisu.each_event do |event|
  case event
  when Termisu::Event::Tick
    # Update animation state
    termisu.render
  when Termisu::Event::Key
    break if event.key.escape?
  end
end

termisu.disable_timer

[View source]
def enhanced_keyboard?(*args, **options) #

Enables enhanced keyboard protocol for disambiguated key reporting.

In standard terminal mode, certain keys are indistinguishable:

  • Tab sends the same byte as Ctrl+I (0x09)
  • Enter sends the same byte as Ctrl+M (0x0D)
  • Backspace may send the same byte as Ctrl+H (0x08)

Enhanced mode enables the Kitty keyboard protocol and/or modifyOtherKeys, which report keys in a way that preserves the distinction.

Note: Not all terminals support these protocols. Unsupported terminals will silently ignore the escape sequences and continue with legacy mode. Supported terminals include: Kitty, WezTerm, foot, Ghostty, recent xterm.

Example:

termisu.enable_enhanced_keyboard
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Key
      # Now Ctrl+I and Tab are distinguishable!
      if event.ctrl? && event.key.lower_i?
        puts "Ctrl+I pressed"
      elsif event.key.tab?
        puts "Tab pressed"
      end
    end
  end
end
termisu.disable_enhanced_keyboard

[View source]
def enhanced_keyboard?(*args, **options, &) #

Enables enhanced keyboard protocol for disambiguated key reporting.

In standard terminal mode, certain keys are indistinguishable:

  • Tab sends the same byte as Ctrl+I (0x09)
  • Enter sends the same byte as Ctrl+M (0x0D)
  • Backspace may send the same byte as Ctrl+H (0x08)

Enhanced mode enables the Kitty keyboard protocol and/or modifyOtherKeys, which report keys in a way that preserves the distinction.

Note: Not all terminals support these protocols. Unsupported terminals will silently ignore the escape sequences and continue with legacy mode. Supported terminals include: Kitty, WezTerm, foot, Ghostty, recent xterm.

Example:

termisu.enable_enhanced_keyboard
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Key
      # Now Ctrl+I and Tab are distinguishable!
      if event.ctrl? && event.key.lower_i?
        puts "Ctrl+I pressed"
      elsif event.key.tab?
        puts "Tab pressed"
      end
    end
  end
end
termisu.disable_enhanced_keyboard

[View source]
def hide_cursor(*args, **options) #

Sets cursor position and makes it visible. Hides the cursor (rendered on next render()). Shows the cursor (rendered on next render()).


[View source]
def hide_cursor(*args, **options, &) #

Sets cursor position and makes it visible. Hides the cursor (rendered on next render()). Shows the cursor (rendered on next render()).


[View source]
def input_available? : Bool #

Checks if input data is available.


[View source]
def mouse_enabled?(*args, **options) #

Enables mouse input tracking.

Once enabled, mouse events will be reported via poll_event. Supports SGR extended protocol (mode 1006) for large terminals and falls back to normal mode (1000) for compatibility.

Example:

termisu.enable_mouse
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Mouse
      puts "Click at #{event.x},#{event.y}"
    end
  end
end
termisu.disable_mouse

[View source]
def mouse_enabled?(*args, **options, &) #

Enables mouse input tracking.

Once enabled, mouse events will be reported via poll_event. Supports SGR extended protocol (mode 1006) for large terminals and falls back to normal mode (1000) for compatibility.

Example:

termisu.enable_mouse
loop do
  if event = termisu.poll_event(100)
    case event
    when Termisu::Event::Mouse
      puts "Click at #{event.x},#{event.y}"
    end
  end
end
termisu.disable_mouse

[View source]
def peek_byte(*args, **options) #

[View source]
def peek_byte(*args, **options, &) #

[View source]
def poll_event(timeout : Time::Span) : Event::Any | Nil #

Polls for an event with timeout.

Returns an Event or nil if timeout expires.

Parameters:

  • timeout: Maximum time to wait for an event

Example:

if event = termisu.poll_event(100.milliseconds)
  # Handle event
else
  # No event within timeout - do other work
end

[View source]
def poll_event(timeout_ms : Int32) : Event::Any | Nil #

Polls for an event with timeout in milliseconds.

Parameters:

  • timeout_ms: Timeout in milliseconds (0 for non-blocking)

[View source]
def poll_event : Event::Any #

Polls for the next event, blocking until one is available.

This is the recommended way to handle events. Returns structured Event objects (Event::Key, Event::Mouse, Event::Resize, Event::Tick) from the unified Event::Loop channel.

Blocks indefinitely until an event arrives.

Example:

loop do
  event = termisu.poll_event
  case event
  when Termisu::Event::Key
    break if event.ctrl_c? || event.key.escape?
  when Termisu::Event::Resize
    termisu.sync # Redraw after resize
  when Termisu::Event::Tick
    # Animation frame
  end
  termisu.render
end

[View source]
def raw_mode?(*args, **options) #

Returns true if raw mode is enabled.


[View source]
def raw_mode?(*args, **options, &) #

Returns true if raw mode is enabled.


[View source]
def read_byte(*args, **options) #

[View source]
def read_byte(*args, **options, &) #

[View source]
def read_bytes(*args, **options) #

[View source]
def read_bytes(*args, **options, &) #

[View source]
def remove_event_source(source : Event::Source) : self #

Removes a custom event source from the event loop.

If the source is running, it will be stopped before removal. Removing a source that isn't registered is a no-op.

Parameters:

  • source: The Event::Source to remove

Returns self for method chaining.


[View source]
def render(*args, **options) #

Renders cell buffer changes to the screen.

Only cells that have changed since the last render are redrawn (diff-based). This is more efficient than clear_screen + write for partial updates.


[View source]
def render(*args, **options, &) #

Renders cell buffer changes to the screen.

Only cells that have changed since the last render are redrawn (diff-based). This is more efficient than clear_screen + write for partial updates.


[View source]
def set_cell(*args, **options) #

Sets a cell at the specified position.

Parameters:

  • x: Column position (0-based)
  • y: Row position (0-based)
  • ch: Character to display
  • fg: Foreground color (default: white)
  • bg: Background color (default: default terminal color)
  • attr: Text attributes (default: None)

Returns false if coordinates are out of bounds.

Example:

termisu.set_cell(10, 5, 'A', fg: Color.red, attr: Attribute::Bold)
termisu.render # Apply changes

[View source]
def set_cell(*args, **options, &) #

Sets a cell at the specified position.

Parameters:

  • x: Column position (0-based)
  • y: Row position (0-based)
  • ch: Character to display
  • fg: Foreground color (default: white)
  • bg: Background color (default: default terminal color)
  • attr: Text attributes (default: None)

Returns false if coordinates are out of bounds.

Example:

termisu.set_cell(10, 5, 'A', fg: Color.red, attr: Attribute::Bold)
termisu.render # Apply changes

[View source]
def set_cursor(*args, **options) #

Sets cursor position and makes it visible. Hides the cursor (rendered on next render()). Shows the cursor (rendered on next render()).


[View source]
def set_cursor(*args, **options, &) #

Sets cursor position and makes it visible. Hides the cursor (rendered on next render()). Shows the cursor (rendered on next render()).


[View source]
def show_cursor(*args, **options) #

Sets cursor position and makes it visible. Hides the cursor (rendered on next render()). Shows the cursor (rendered on next render()).


[View source]
def show_cursor(*args, **options, &) #

Sets cursor position and makes it visible. Hides the cursor (rendered on next render()). Shows the cursor (rendered on next render()).


[View source]
def size(*args, **options) #

Returns terminal size as {width, height}.


[View source]
def size(*args, **options, &) #

Returns terminal size as {width, height}.


[View source]
def sync(*args, **options) #

Forces a full redraw of all cells.

Useful after terminal resize or screen corruption.


[View source]
def sync(*args, **options, &) #

Forces a full redraw of all cells.

Useful after terminal resize or screen corruption.


[View source]
def timer_enabled? : Bool #

Returns true if the timer is currently enabled.


[View source]
def timer_interval : Time::Span | Nil #

Returns the current timer interval, or nil if timer is disabled.


[View source]
def timer_interval=(interval : Time::Span) : Time::Span #

Sets the timer interval.

Can be called while timer is running to change the interval dynamically. Raises if timer is not enabled.

Parameters:

  • interval: New interval between tick events

Example:

termisu.enable_timer
termisu.timer_interval = 8.milliseconds # 120 FPS

[View source]
def try_poll_event : Event::Any | Nil #

Tries to poll for an event without blocking.

Returns an event if one is immediately available, or nil otherwise. This uses Crystal's select/else for true non-blocking behavior, making it ideal for game loops or fiber-based architectures.

Example:

# Game loop pattern
loop do
  while event = termisu.try_poll_event
    case event
    when Termisu::Event::Key
      break if event.key.escape?
    end
  end

  # Update game state
  update_game()
  termisu.render
  sleep 16.milliseconds
end

[View source]
def wait_event : Event::Any #

Waits for and returns the next event (blocking).

Alias for #poll_event without timeout. Blocks until an event is available from any source.

Example:

event = termisu.wait_event
puts "Got event: #{event}"

[View source]
def wait_for_input(timeout_ms : Int32) : Bool #

Waits for input data with a timeout in milliseconds.


[View source]