Skip to content

TTY

Terminal UI module for raw input events, styled output, and layout utilities.

This module only works inside terminal context. You cannot use it from regular functions—only from processes running on a Terminal Host.
local tty = require("tty")

Start the raw input reader, subscribe to events, and process them in a loop:

local tty = require("tty")
local io = require("io")
local function handler()
tty.start()
local events = tty.events()
while true do
local ev = events:receive()
if not ev then break end
if ev.type == "key" then
if ev.key == "q" or (ev.ctrl and ev.key == "c") then
break
end
io.print("Key: " .. ev.key)
elseif ev.type == "resize" then
io.print("Size: " .. ev.width .. "x" .. ev.height)
end
end
tty.stop()
end

Enable raw terminal input mode. The terminal switches to raw mode and begins emitting events.

local ok, err = tty.start()

Returns: boolean, error

Disable raw input and restore the terminal to normal mode.

local ok, err = tty.stop()

Returns: boolean, error

Subscribe to terminal events and return a channel. Events are delivered as tables with a type field.

local events = tty.events()

Returns: EventChannel, error

Query current terminal dimensions.

local width, height, err = tty.screen_size()

Returns: number, number, error

Enable or disable mouse event tracking.

local ok, err = tty.mouse(true)
ParameterTypeDescription
enablebooleantrue to enable, false to disable

Returns: boolean, error

Events are tables with a type field that determines which other fields are present.

{
type = "key",
key = "a", -- printable character or key name
key_type = "runes", -- "runes" for printable, or special key name
action = "press", -- "press" or "release"
alt = false,
ctrl = false,
shift = false
}

Requires tty.mouse(true).

{
type = "mouse",
action = "press", -- "press", "release", "motion", "wheel"
button = "left", -- button name
x = 10,
y = 5,
alt = false,
ctrl = false,
shift = false
}
{type = "resize", width = 120, height = 40}

Emitted once after tty.start() with initial dimensions.

{type = "start", width = 120, height = 40}
{type = "focus", focused = true}
{type = "paste", text = "pasted content"}

Create reusable key bindings that match against key events:

local quit = tty.bind({
keys = {"q", "ctrl+c"},
help = {key = "q/ctrl+c", desc = "quit"}
})
-- In event loop
if quit:matches(ev) then
break
end
FieldTypeDescription
keysstring[]Key patterns to match (e.g. "a", "ctrl+c", "enter")
helptableOptional. {key = "...", desc = "..."} for help text

Returns: KeyBinding

MethodReturnsDescription
matches(event)booleanTest if a key event matches this binding
set_enabled(bool)selfEnable or disable the binding
is_enabled()booleanCheck if the binding is enabled
help()tableReturns {key, desc} help info

Create styled text output using lipgloss-based styling. All style methods return a new style (immutable).

local tty = require("tty")
local io = require("io")
local title = tty.style()
:bold()
:foreground("#FF0000")
:padding(0, 1)
local box = tty.style()
:border(tty.borders.ROUNDED)
:border_foreground("#00FF00")
:width(40)
:padding(1, 2)
io.print(box:render(title:render("Hello"), "World"))

Create a new empty style.

Returns: Style

All methods return a new Style and can be chained.

MethodParameterDescription
foreground(color)stringText color (hex "#FF0000", ANSI "9", or name)
background(color)stringBackground color
bold(enable?)booleanBold text (default: true)
italic(enable?)booleanItalic text
underline(enable?)booleanUnderline text
strikethrough(enable?)booleanStrikethrough text
faint(enable?)booleanDimmed text
blink(enable?)booleanBlinking text
reverse(enable?)booleanSwap foreground/background
MethodParameterDescription
width(n)numberFixed width
height(n)numberFixed height
max_width(n)numberMaximum width
max_height(n)numberMaximum height
padding(...)numbersPadding (CSS-style: top, right, bottom, left)
margin(...)numbersMargin (CSS-style)
align(pos)numberHorizontal alignment
align_vertical(pos)numberVertical alignment
inline(enable?)booleanInline rendering mode
MethodParameterDescription
border(name, ...)string, booleansBorder style, optional per-side toggles
border_foreground(...)stringsBorder color(s)
border_background(...)stringsBorder background color(s)
MethodDescription
render(...)Render strings with this style applied
copy()Create a copy of this style
tty.borders.NORMAL
tty.borders.ROUNDED
tty.borders.THICK
tty.borders.DOUBLE
tty.borders.HIDDEN
tty.align.LEFT -- 0
tty.align.CENTER -- 0.5
tty.align.RIGHT -- 1

Layout and measurement functions for styled text. Available under tty.text.

local w = tty.text.width("hello") -- printable width (ANSI-aware)
local h = tty.text.height("a\nb\nc") -- line count
local w, h = tty.text.size("hello\nworld") -- both
-- Join side by side, aligned at top
local row = tty.text.join_horizontal(tty.text.position.TOP, left, right)
-- Stack vertically, centered
local col = tty.text.join_vertical(tty.text.position.CENTER, top, bottom)
local w = tty.text.max_width({"short", "a longer string"}) -- widest
local h = tty.text.max_height({"one\ntwo", "single"}) -- tallest

Place a string within a box of given dimensions:

-- Center in a 80x24 box
local out = tty.text.place(80, 24, tty.text.position.CENTER, tty.text.position.CENTER, content)
-- Horizontal only
local out = tty.text.place_horizontal(80, tty.text.position.RIGHT, content)
-- Vertical only
local out = tty.text.place_vertical(24, tty.text.position.BOTTOM, content)
tty.text.position.TOP -- 0
tty.text.position.LEFT -- 0
tty.text.position.CENTER -- 0.5
tty.text.position.BOTTOM -- 1
tty.text.position.RIGHT -- 1