tui

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: BSD-3-Clause Imports: 9 Imported by: 0

README

go-ruby-widgets/tui

CI Go Reference Go Report Card

The pure-Go, Ruby-runtime-independent core of the Ruby tui gem — a terminal-cell user-interface toolkit (widgets, layout containers, frame rendering and input routing) — shaped so that go-embedded-ruby (rbgo) can bind it as require "tui".

It is a thin adapter over the terminal-cell widget toolkit go-widgets/tui:

Library Role
go-widgets/tui Terminal-cell widgets + border/split/tab layouts, ANSI frame rendering, DecodeANSI.
go-widgets/toolkit The shared Widget/Event/Rect/Theme vocabulary the widgets are built on.

It exposes them through Ruby-facing handles — Module and Widget — whose methods return Ruby-shaped values: a Hash (map[string]any), an Array ([]any) or a scalar. A single dynamic entry point, Call, dispatches a Ruby-style snake_case method name to the matching handle method and coerces the arguments, which is exactly what an rbgo binding drives from method_missing. Nothing here depends on the Ruby runtime, so it is equally usable as a standalone Go library — a sibling of go-ruby-opentype/opentype, go-ruby-regexp/regexp and go-ruby-erb/erb.

  • CGO-free, builds and tests identically on amd64, arm64, riscv64, loong64, ppc64le, s390x, plus js/wasm.
  • 100 % test coverage, race-clean, enforced in CI. Rendering is asserted at cell precision — no real terminal required.

The Ruby-facing surface

Module — the package-level receiver (the Tui module under rbgo).

Construction (each returns a Widget handle):

Method Widget
label(text) static single-line text
button(label) clickable action
entry(initial) single-line text input
check_button(label, checked) boolean toggle
list_box(items) single-selection list
progress_bar horizontal fill indicator
container border layout: fixed header + footer bands, filling body, overlays
h_split / v_split draggable split panes
notebook card layout: a tab strip selects the visible page

Layout, render and events:

Method Returns
set_size(root, cols, rows) lays the tree out to fill the terminal
bounds(w) Hash {x, y, w, h}
render(root, cols, rows) the frame as an ANSI String
render_cells(root, cols, rows) the decoded cell grid (Hash, see below)
decode_cells(ansi, cols, rows) a cell grid decoded from any ANSI string
dispatch(root, event) Hash {fired => [ids], repaint => true}

Widget — an opaque handle. Its methods mutate props (set_text, set_align, set_label, set_checked, set_fraction, set_items, set_selected, …), compose a tree (set_header / set_body / set_footer / add_overlay, set_left / set_right, set_top / set_bottom, add_tab, set_active) and wire callbacks by id (on_click, on_toggle, on_select, on_change, on_tab_changed). Every method checks the handle's kind and returns a clear error when it does not apply.

The render seam

render returns a self-contained ANSI stream a Ruby host prints straight to a terminal. render_cells (and decode_cells over any ANSI string) return the decoded grid as a Hash:

{ "cols"=>, "rows"=>,
  "cells"=> [ [ {"char"=>String, "fg"=>color|nil, "bg"=>color|nil}, ... ], ... ],
  "text" => [ "row 0 text", "row 1 text", ... ] }

where a color is {"r"=>, "g"=>, "b"=>} or nil for the terminal default. The decode path is backed by go-widgets/tui's DecodeANSI, so a test can assert the output at cell precision.

Events

dispatch routes one event Hash — {"kind"=>, "x"=>, "y"=>, "code"=>, "ctrl"=>, "shift"=>} — into the tree, whose containers translate coordinates and deliver it to the right leaf. Recognised kinds: click, key_down, key_up, char, mouse_drag, mouse_up, tick. It returns the ids of the callbacks that fired and whether a repaint is warranted.

Usage from Ruby

Under rbgo, require "tui" gives a Tui module whose snake_case methods are these operations, returning Ruby Hashes, Arrays and scalars:

require "tui"

button = Tui.button("OK")
button.on_click("ok")

root = Tui.container
root.set_header_height(1)
root.set_header(Tui.label("Title"))
root.set_body(button)

Tui.set_size(root, 40, 10)
puts Tui.render(root, 40, 10)                       # => String (ANSI frame)

fired = Tui.dispatch(button, {"kind" => "click"})  # => {"fired"=>["ok"], "repaint"=>true}

The require "tui" binding lives in rbgo (a thin method_missing shim over Call); it is pending in that repo.

Install (Go)

go get github.com/go-ruby-widgets/tui

Usage from Go

package main

import (
	"fmt"

	"github.com/go-ruby-widgets/tui"
)

func main() {
	m := tui.NewModule()

	button := m.Button("OK")
	_ = button.OnClick("ok")

	root := m.Container()
	_ = root.SetHeaderHeight(1)
	_ = root.SetHeader(m.Label("Title"))
	_ = root.SetBody(button)

	_ = m.SetSize(root, 40, 10)

	frame, _ := m.Render(root, 40, 10) // an ANSI String
	fmt.Print(frame)

	res, _ := m.Dispatch(button, map[string]any{"kind": "click"})
	fmt.Println(res["fired"]) // [ok]
}

Methods(recv) lists every snake_case name Call accepts for a handle, and Call(recv, name, args...) is the uniform dynamic entry point rbgo binds.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package tui is the pure-Go, Ruby-runtime-independent core of the Ruby `tui` gem: a terminal-cell user-interface toolkit — widgets, layout containers, frame rendering and input routing — shaped so that github.com/go-embedded-ruby/ruby (rbgo) can bind it as `require "tui"`.

It is a thin adapter over github.com/go-widgets/tui, the terminal-cell widget toolkit. It exposes that toolkit through Ruby-facing handles (Module, Widget) whose methods return Ruby-shaped values: a Hash (map[string]any), an Array ([]any) or a scalar. A single dynamic entry point, Call, dispatches a Ruby-style snake_case method name to the matching handle method and coerces the arguments, which is exactly what an rbgo binding drives from method_missing. Nothing here imports the Ruby runtime, so the package is equally usable as a standalone Go library — a sibling of go-ruby-opentype/opentype, go-ruby-regexp/regexp and go-ruby-erb/erb.

Handles

  • Module is the package-level receiver: it constructs widgets and layout containers, lays a tree out (SetSize), renders it (Render / RenderCells), decodes an ANSI frame (DecodeCells) and routes input (Dispatch).
  • Widget is an opaque handle over one widget or container. Its methods mutate props (SetText, SetFraction, …), compose a tree (SetBody, AddTab, SetLeft, …) and wire callbacks by id (OnClick, OnToggle, …).

Widgets and layouts

Leaves: label, button, entry, check_button, list_box, progress_bar. Containers: container (a border layout — fixed header + footer bands around a filling body, plus floating overlays), h_split / v_split (draggable split panes) and notebook (a card layout — a tab strip selects the visible page).

The render seam

A widget tree renders to a terminal cell grid, emitted as a self-contained ANSI stream. Render returns that stream as a String a Ruby host can print straight to a terminal. RenderCells (and DecodeCells over any ANSI string) return the decoded grid as a Ruby Hash — "cols", "rows", a "cells" Array of rows of {"char", "fg", "bg"} cell Hashes (a color is {"r","g","b"} or nil), and a "text" Array of per-row strings — so a test can assert the output at cell precision. The decode path is backed by go-widgets/tui's DecodeANSI.

Events

Dispatch routes one input event (a Ruby Hash with "kind", "x", "y", "code" and modifier flags) into the tree, whose containers translate coordinates and deliver it to the right leaf. It returns the ids of the callbacks that fired and whether a repaint is warranted.

Usage from Go

m := tui.NewModule()
root := m.Container()
_ = root.SetHeaderHeight(1)
_ = root.SetHeader(m.Label("Title"))
_ = root.SetBody(m.Button("OK"))
_ = m.SetSize(root, 40, 10)
frame, _ := m.Render(root, 40, 10)         // an ANSI String
grid, _ := m.RenderCells(root, 40, 10)     // a Ruby Hash of cells
_ = frame
_ = grid

Usage from Ruby

Under rbgo, `require "tui"` gives a Tui module whose snake_case methods are these operations, returning Ruby Hashes, Arrays and scalars:

require "tui"

root = Tui.container
root.set_header_height(1)
root.set_header(Tui.label("Title"))
root.set_body(Tui.button("OK"))
Tui.set_size(root, 40, 10)
frame = Tui.render(root, 40, 10)           # => String (ANSI)
fired = Tui.dispatch(root, {"kind" => "click", "x" => 1, "y" => 2})

The `require "tui"` binding lives in rbgo (a thin method_missing shim over Call); it is pending in that repo.

Example

Example mirrors the README: build a border-layout container with a titled header over a clickable body, render it to a cell grid, then route a click — every result a Ruby-shaped value.

package main

import (
	"fmt"

	"github.com/go-ruby-widgets/tui"
)

func main() {
	m := tui.NewModule()

	button := m.Button("OK")
	_ = button.OnClick("ok")

	root := m.Container()
	_ = root.SetHeaderHeight(1)
	_ = root.SetHeader(m.Label("Title"))
	_ = root.SetBody(button)

	_ = m.SetSize(root, 20, 5)

	grid, _ := m.RenderCells(root, 20, 5) // a Ruby Hash of decoded cells
	fmt.Println("cols:", grid["cols"], "rows:", grid["rows"])

	res, _ := m.Dispatch(button, map[string]any{"kind": "click"})
	fmt.Println("fired:", res["fired"], "repaint:", res["repaint"])

}
Output:
cols: 20 rows: 5
fired: [ok] repaint: true

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bounds

func Bounds(w *Widget) (map[string]any, error)

Bounds returns a widget's placement via the default Module.

func Call

func Call(recv any, method string, args ...any) (any, error)

Call dispatches a Ruby-style snake_case method name to the matching exported method of recv (a *Module or *Widget), coercing each Ruby-supplied argument to the Go parameter type. Trailing arguments may be omitted; they default to nil. The result is the method's Ruby-shaped return value (or nil for a method that returns nothing); a trailing error return is unwrapped into Call's own error. This is the single entry point an rbgo binding drives.

func DecodeCells

func DecodeCells(ansi string, cols, rows int) map[string]any

DecodeCells decodes an ANSI frame to a cell grid via the default Module.

func Dispatch

func Dispatch(root *Widget, ev map[string]any) (map[string]any, error)

Dispatch routes an event to root via the default Module.

func Methods

func Methods(recv any) []string

Methods lists, sorted, the Ruby-style snake_case names Call accepts for recv.

func Render

func Render(root *Widget, cols, rows int) (string, error)

Render renders root to an ANSI string via the default Module.

func RenderCells

func RenderCells(root *Widget, cols, rows int) (map[string]any, error)

RenderCells renders root to a cell grid via the default Module.

func SetSize

func SetSize(root *Widget, cols, rows int) error

SetSize lays root out on the default Module.

Types

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is the package-level Ruby receiver: the `Tui` module under rbgo. Its methods construct terminal-cell widgets and layout containers, lay them out, render them to a frame (an ANSI string and a decoded cell grid), and route input events — all returning Ruby-shaped values (a Hash is a map[string]any, an Array a []any, and scalars stay scalars).

A Module carries the per-session callback log that Module.Dispatch drains, so it is stateful and NOT safe for concurrent use: give each terminal session its own Module (the package-level convenience functions share one default Module and are likewise single-threaded).

func NewModule

func NewModule() *Module

NewModule returns a fresh Module with an empty callback log. The package-level convenience functions delegate to a shared default Module.

func (*Module) Bounds

func (m *Module) Bounds(w *Widget) (map[string]any, error)

Bounds returns a widget's current placement as a Hash with "x", "y", "w" and "h".

func (*Module) Button

func (m *Module) Button(label string) *Widget

Button builds a clickable action widget. Wire a handler with Widget.OnClick.

func (*Module) CheckButton

func (m *Module) CheckButton(label string, checked bool) *Widget

CheckButton builds a boolean toggle with the given label and initial state.

func (*Module) Container

func (m *Module) Container() *Widget

Container builds a border-layout container: a fixed-height header band at the top, a fixed-height footer band at the bottom, a body filling the middle, and floating overlays. Compose it with SetHeader/SetBody/SetFooter/AddOverlay.

func (*Module) DecodeCells

func (m *Module) DecodeCells(ansi string, cols, rows int) map[string]any

DecodeCells decodes an ANSI frame into a cell grid, returning a Ruby Hash with "cols", "rows", "cells" and "text". "cells" is an Array of rows, each an Array of cell Hashes {"char" => String, "fg" => color|nil, "bg" => color|nil}; a color is a Hash {"r","g","b"} or nil for the terminal default. "text" is an Array of per-row strings for coarse assertions. It is backed by go-widgets/tui's DecodeANSI.

func (*Module) Dispatch

func (m *Module) Dispatch(root *Widget, ev map[string]any) (map[string]any, error)

Dispatch routes one input event to root's widget tree and returns a Hash with "fired" (an Array of the callback ids that ran) and "repaint" (always true — an event may change state, so the caller should re-render). The event Hash carries "kind" (e.g. "click", "key_down", "char", "mouse_drag", "mouse_up", "tick"), "x", "y", "code"/"key"/"rune", "ctrl" and "shift"; all are optional.

func (*Module) Entry

func (m *Module) Entry(initial string) *Widget

Entry builds a single-line text input seeded with initial text.

func (*Module) HSplit

func (m *Module) HSplit() *Widget

HSplit builds a horizontal split container (a left pane and a right pane separated by a draggable grip), left pane 50% by default.

func (*Module) Label

func (m *Module) Label(text string) *Widget

Label builds a static, single-line text widget.

func (*Module) ListBox

func (m *Module) ListBox(items []any) *Widget

ListBox builds a single-selection list over the given items (a Ruby Array of strings).

func (*Module) Notebook

func (m *Module) Notebook() *Widget

Notebook builds a card-layout (tabbed) container: a tab strip selects which child page fills the body. Add pages with Widget.AddTab.

func (*Module) ProgressBar

func (m *Module) ProgressBar() *Widget

ProgressBar builds an empty horizontal progress indicator.

func (*Module) Render

func (m *Module) Render(root *Widget, cols, rows int) (string, error)

Render lays root out to (cols, rows) and returns the frame as a self-contained ANSI string a Ruby host can print straight to a terminal.

func (*Module) RenderCells

func (m *Module) RenderCells(root *Widget, cols, rows int) (map[string]any, error)

RenderCells lays root out, renders it, and returns the decoded cell grid as a Ruby Hash (see Module.DecodeCells for the shape). This is the DecodeANSI-backed form a test asserts against at cell precision.

func (*Module) SetSize

func (m *Module) SetSize(root *Widget, cols, rows int) error

SetSize lays root out to fill a (cols, rows) terminal. Non-positive dimensions fall back to the classic 80x24 default.

func (*Module) VSplit

func (m *Module) VSplit() *Widget

VSplit builds a vertical split container (a top pane over a bottom pane), top pane 50% by default.

type Widget

type Widget struct {
	// contains filtered or unexported fields
}

Widget is a Ruby-facing opaque handle over one go-widgets/tui widget or layout container. Kind names the concrete widget ("label", "button", "container", …) so a caller can branch on it, and every mutator checks it so a method that does not apply to the handle's kind returns a clear error rather than mis-typing. A Widget belongs to the Module that built it, which is where its callbacks log.

func Button

func Button(label string) *Widget

Button builds a button on the default Module.

func CheckButton

func CheckButton(label string, checked bool) *Widget

CheckButton builds a check button on the default Module.

func Container

func Container() *Widget

Container builds a border-layout container on the default Module.

func Entry

func Entry(initial string) *Widget

Entry builds an entry on the default Module.

func HSplit

func HSplit() *Widget

HSplit builds a horizontal split on the default Module.

func Label

func Label(text string) *Widget

Label builds a label on the default Module.

func ListBox

func ListBox(items []any) *Widget

ListBox builds a list box on the default Module.

func Notebook

func Notebook() *Widget

Notebook builds a notebook on the default Module.

func ProgressBar

func ProgressBar() *Widget

ProgressBar builds a progress bar on the default Module.

func VSplit

func VSplit() *Widget

VSplit builds a vertical split on the default Module.

func (*Widget) Active

func (w *Widget) Active() (int, error)

Active returns a notebook's active tab index.

func (*Widget) AddOverlay

func (w *Widget) AddOverlay(child *Widget) error

AddOverlay floats a child on top of a container's body.

func (*Widget) AddTab

func (w *Widget) AddTab(label string, page *Widget) error

AddTab appends a page to a notebook under the given tab label.

func (*Widget) Checked

func (w *Widget) Checked() (bool, error)

Checked reports a check button's state.

func (*Widget) Fraction

func (w *Widget) Fraction() (float64, error)

Fraction returns a progress bar's current fill.

func (*Widget) Items

func (w *Widget) Items() ([]any, error)

Items returns a list box's items as a Ruby Array of strings.

func (*Widget) Kind

func (w *Widget) Kind() string

Kind returns the handle's widget-kind name (e.g. "label", "notebook").

func (*Widget) OnChange

func (w *Widget) OnChange(id string) error

OnChange wires an entry so an edit records id.

func (*Widget) OnClick

func (w *Widget) OnClick(id string) error

OnClick wires a button so that a click (or Enter while focused) records id in the owning Module's callback log; Module.Dispatch returns those ids.

func (*Widget) OnSelect

func (w *Widget) OnSelect(id string) error

OnSelect wires a list box so a selection change records id.

func (*Widget) OnTabChanged

func (w *Widget) OnTabChanged(id string) error

OnTabChanged wires a notebook so a tab switch records id.

func (*Widget) OnToggle

func (w *Widget) OnToggle(id string) error

OnToggle wires a check button so a toggle records id.

func (*Widget) Selected

func (w *Widget) Selected() (int, error)

Selected returns a list box's selected row index.

func (*Widget) SetActive

func (w *Widget) SetActive(i int) error

SetActive sets a notebook's active tab index.

func (*Widget) SetAlign

func (w *Widget) SetAlign(align string) error

SetAlign sets a label's horizontal alignment ("left", "center" or "right"; any other value is treated as "left").

func (*Widget) SetBody

func (w *Widget) SetBody(child *Widget) error

SetBody sets a container's body child (the middle, filling area).

func (*Widget) SetBottom

func (w *Widget) SetBottom(child *Widget) error

SetBottom sets a vertical split's bottom pane.

func (*Widget) SetChecked

func (w *Widget) SetChecked(v bool) error

SetChecked sets a check button's state.

func (*Widget) SetFooter

func (w *Widget) SetFooter(child *Widget) error

SetFooter sets a container's fixed footer child.

func (*Widget) SetFooterHeight

func (w *Widget) SetFooterHeight(n int) error

SetFooterHeight sets a container's footer band height in rows.

func (*Widget) SetFraction

func (w *Widget) SetFraction(f float64) error

SetFraction sets a progress bar's fill in [0,1] (clamped).

func (*Widget) SetHeader

func (w *Widget) SetHeader(child *Widget) error

SetHeader sets a container's fixed header child.

func (*Widget) SetHeaderHeight

func (w *Widget) SetHeaderHeight(n int) error

SetHeaderHeight sets a container's header band height in rows.

func (*Widget) SetItems

func (w *Widget) SetItems(items []any) error

SetItems replaces a list box's items (a Ruby Array of strings) and resets the selection to the first row.

func (*Widget) SetLabel

func (w *Widget) SetLabel(s string) error

SetLabel sets the caption of a button, check button or progress bar.

func (*Widget) SetLeft

func (w *Widget) SetLeft(child *Widget) error

SetLeft sets a horizontal split's left pane.

func (*Widget) SetLeftFraction

func (w *Widget) SetLeftFraction(n int) error

SetLeftFraction sets a horizontal split's left-pane width percentage.

func (*Widget) SetPlaceholder

func (w *Widget) SetPlaceholder(s string) error

SetPlaceholder sets an entry's muted placeholder text.

func (*Widget) SetRight

func (w *Widget) SetRight(child *Widget) error

SetRight sets a horizontal split's right pane.

func (*Widget) SetSelected

func (w *Widget) SetSelected(i int) error

SetSelected sets a list box's selected row index.

func (*Widget) SetText

func (w *Widget) SetText(s string) error

SetText sets the text of a label or entry.

func (*Widget) SetTop

func (w *Widget) SetTop(child *Widget) error

SetTop sets a vertical split's top pane.

func (*Widget) SetTopFraction

func (w *Widget) SetTopFraction(n int) error

SetTopFraction sets a vertical split's top-pane height percentage.

func (*Widget) Text

func (w *Widget) Text() (string, error)

Text returns the text of a label or entry.

func (*Widget) Underlying

func (w *Widget) Underlying() toolkit.Widget

Underlying returns the wrapped go-widgets/tui widget as a toolkit.Widget, for Go callers that want to reach past the adapter. Ruby callers never need it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL