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 = 3
VERSION_PATCH = 0
VERSION_STATE = nil

Constructors

Instance Method Summary

Constructor Detail

def self.new(*, sync_updates : Bool = true) #

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.

Parameters:

  • sync_updates - Enable DEC mode 2026 synchronized updates (default: true). When enabled, render operations are wrapped in BSU/ESU sequences to prevent screen tearing. Unsupported terminals ignore these sequences.

[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 current_mode(*args, **options) #

Returns the current terminal mode, or nil if not yet set.


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

Returns the current terminal mode, or nil if not yet set.


[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_system_timer(interval : Time::Span = 16.milliseconds) : self #

Enables the kernel-level system timer for precise animation timing.

Uses platform-specific timers for high-precision tick events:

  • Linux: timerfd with epoll (<1ms precision)
  • macOS/BSD: kqueue EVFILT_TIMER
  • Fallback: monotonic clock with poll

The SystemTimer compensates for processing time, maintaining consistent frame rates. It also provides missed_ticks count for detecting dropped frames when processing takes longer than the interval.

Parameters:

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

Example:

termisu.enable_system_timer(16.milliseconds) # 60 FPS with kernel timing

termisu.each_event do |event|
  case event
  when Termisu::Event::Tick
    if event.missed_ticks > 0
      puts "Dropped #{event.missed_ticks} frame(s)"
    end
    termisu.render
  when Termisu::Event::Key
    break if event.key.escape?
  end
end

termisu.disable_timer

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

Enables the sleep-based timer source for animation and game loops.

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

Note: The sleep-based timer has ~5ms overhead per frame due to processing time not being compensated. For more precise timing, use #enable_system_timer which uses kernel-level timers.

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 suspend(preserve_screen : Bool, &) #

Suspends TUI mode with option to preserve alternate screen.

Parameters:

  • preserve_screen: If true, stays in alternate screen during suspension. Useful for brief prompts that don't need scrollback access.

Example:

termisu.suspend(preserve_screen: true) do
  print "Continue? [y/n]: "
  answer = gets
end

[View source]
def suspend(&) #

Suspends TUI mode for shell-out or external program execution.

This is an alias for #with_cooked_mode that makes the intent clearer when temporarily handing control to a shell or external program.

Handles:

  • Exits alternate screen (shows normal terminal with scrollback)
  • Switches to cooked mode (line buffering, echo, signals)
  • Pauses event loop input processing
  • Restores everything on block exit

Example:

termisu.suspend do
  system("vim file.txt")
end
# TUI fully restored

[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 sync_updates=(value : Bool) #

Sets whether synchronized updates are enabled.

Can be toggled at runtime. Set to false for debugging or compatibility with terminals that misbehave with sync sequences.


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

Returns true if synchronized updates (DEC mode 2026) are enabled.

When enabled, render operations are wrapped in BSU/ESU sequences to prevent screen tearing. Enabled by default.


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

Returns true if synchronized updates (DEC mode 2026) are enabled.

When enabled, render operations are wrapped in BSU/ESU sequences to prevent screen tearing. Enabled by default.


[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]
def with_cbreak_mode(preserve_screen : Bool = true, &) #

Executes a block with cbreak mode.

Cbreak mode provides character-by-character input with echo and signal handling. Useful for interactive prompts within a TUI.

Event loop input processing is paused during the block.

Example:

termisu.with_cbreak_mode do
  print "Press any key: "
  key = STDIN.read_char
end

[View source]
def with_cooked_mode(preserve_screen : Bool = false, &) #

Executes a block with cooked (shell-like) mode.

Cooked mode enables canonical input, echo, and signal handling - ideal for shell-out operations where the subprocess needs full terminal control.

Event loop input processing is paused during the block to avoid conflicts with the shell or subprocess.

Example:

termisu.with_cooked_mode do
  system("vim file.txt")
end
# Back to TUI mode, event loop active

[View source]
def with_mode(mode : Terminal::Mode, preserve_screen : Bool = false, &) #

Executes a block with specific terminal mode, restoring previous mode after.

This is the recommended way to temporarily switch modes for operations like shell-out or password input. Handles:

  • Event loop coordination (pauses input for user-interactive modes)
  • Mode switching via Terminal (which handles termios, screen, cursor)
  • Automatic restoration on block exit or exception

Parameters:

  • mode: Terminal::Mode to use within the block
  • preserve_screen: If false (default) and mode is canonical, exits alternate screen during block. If true, stays in alternate screen.

Example:

termisu.with_mode(Terminal::Mode.cooked) do
  system("vim file.txt")
end
# Terminal state fully restored

[View source]
def with_password_mode(preserve_screen : Bool = true, &) #

Executes a block with password input mode.

Password mode enables canonical (line-buffered) input with signal handling but disables echo. Perfect for secure password entry.

Example:

password = termisu.with_password_mode do
  print "Password: "
  gets
end

[View source]