TuiTui
TuiTui is a small terminal UI toolkit for Ruby, with no external dependencies.
It uses a lightweight, TEA-inspired (MVU) architecture: the app object is the model,
update(event) returns the next app, and view(size) renders a Canvas that the runtime paints.
Usage
An app is any object with two methods: view(size) returns a Canvas, and
update(event) returns the next app (or :quit). It may also return
[app, *commands] to request one-shot effects (see below). Runtime#run
drives the loop.
require "tui_tui"
class Counter
def initialize(count = 0) = @count = count
def view(size)
TuiTui::Canvas.blank(size).text(1, 1, "count: #{@count} (+/- to change, q to quit)")
end
def update(event)
case event
in TuiTui::KeyEvent(key: "+" | "=") then Counter.new(@count + 1)
in TuiTui::KeyEvent(key: "-" | "_") then Counter.new(@count - 1)
in TuiTui::KeyEvent(key: "q" | TuiTui::KeyCode::CTRL_C) then :quit
else self
end
end
end
TuiTui::Runtime.new(Counter.new).run
Events are Data.define values, so Ruby's pattern matching (case/in)
deconstructs them directly in update.
The runtime never inspects the model, only update's return value, so both
state styles work: the counter above returns a fresh model from each branch;
todo.rb and file_browser.rb mutate their state and return self. Prefer
immutability when you want undo or to compare models in tests; prefer
mutation when the model holds lists, caches, or widget state.
See examples/ for larger apps, each runnable with
ruby examples/<name>.rb.
Commands and subscriptions
Ongoing declarations are predicate methods the runtime polls each frame
(wants_tick?, wants_mouse?); one-shot instructions are TuiTui::Command
values returned from update, which the runtime executes before rendering.
[self, TuiTui::Command::Copy.new(path)] # write to the clipboard (OSC 52)
Command::Invalidate requests a full repaint on the next render.
Returning [:quit, *commands] performs the commands and then quits.
Testing
TuiTui::TestRuntime is the runtime with the screen removed: it folds
injected events exactly like Runtime and renders view headless. It is
opt-in — require "tui_tui/test_runtime".
require "tui_tui/test_runtime"
runtime = TuiTui::TestRuntime.new(Counter.new, rows: 3, cols: 60)
runtime.key("+").key("+").key("-")
expect(runtime.screen).to include("count: 1")
expect(runtime.commands).to eq([])
expect(runtime.key("q").quit?).to be(true)
screen returns Canvas#to_text, a style-free string projection of the
grid; canvas returns the Canvas itself for cell-level style assertions.
type("…") injects a string one grapheme at a time, and resize / tick /
mouse inject the other event kinds.
Non-functional requirements
TuiTui is built around a small set of non-functional requirements (NFRs).
N1: Minimal dependencies.
Depends only on io/console, which is a default gem.
N2: Testable without a terminal.
State transitions (update) and drawing (view) are pure functions.
Only the driver (Screen) touches the terminal.
This makes apps and widgets unit-testable and snapshot-testable in headless environments.
N3: Terminal safety.
Raw mode, the alternate screen, and cursor visibility are always restored. This applies to normal exits, exceptions, and signals.
This prevents the terminal from being left in a broken state.
The Screen.run block form guarantees this behavior through TerminalSession.
N4: No flicker.
Only the frame diff is written.
Each frame is flushed with a single write.
N5: Full-width aware.
Columns never misalign. Display width is measured using a small built-in table based on East Asian Width.
Glyphs are clipped at region edges, not split across them.
N6: Performance.
Movement and redraw stay responsive, even with large content. Only changed rows are repainted, so cost scales with the change, not the screen size.
N7: Width-safe UI chrome.
Self-drawn chrome defaults to ASCII, color, and spacing, which have a guaranteed width of 1. Unicode box-drawing has an ambiguous width that can break layouts under CJK terminal settings, so it is only used when the terminal is confirmed to render it at width 1; otherwise the chrome falls back to ASCII.
Content text, such as Japanese data, is measured with Width.
It is clipped or padded to fit the available space.
Configuration
Environment variables (all optional):
TUITUI_MOUSE— set to0/off/falseto disable mouse reporting (on by default).TUITUI_BACKGROUND—lightordarkto pick the theme for your terminal background. Without it,COLORFGBGis read if present, otherwisedarkis assumed (reliable auto-detection isn't possible on all terminals).TUITUI_BOX—ascii/unicode/autoto force or auto-detect Unicode box-drawing chrome (defaultauto: used only when the terminal renders it at width 1, else ASCII).
Installation
bundle add tui_tui
If bundler is not being used to manage dependencies, install the gem by executing:
gem install tui_tui
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.
You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
To release a new version, update the version number in version.rb, and then run bundle exec rake release,
which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/takahashim/tui_tui.