Enhanced API Reference
Complete Guide to Hype's Built-in Modules
Deep dive into every module, method, and feature. Learn how to build professional applications with real-world examples, best practices, and performance tips.
🖥️ TUI Module - Terminal User Interface
Build beautiful, interactive terminal applications with Hype's TUI module. Based on the powerful tview library, it provides a complete set of UI components for creating professional console applications.
tui
- no require needed!
Quick Start
-- Minimal TUI app
local app = tui.newApp()
local text = tui.newTextView("Hello, TUI World! 🚀")
text:SetBorder(true):SetTitle("My App")
app:SetRoot(text, true):Run()
Core Components
tui.newApp()
local app = tui.newApp() → Application
Creates the main application instance that manages the UI event loop and component rendering.
Methods
Method | Description | Returns |
---|---|---|
SetRoot(primitive, fullscreen) |
Set the root UI component | self |
Run() |
Start the application (blocks) | nil |
Stop() |
Stop the application gracefully | nil |
Draw() |
Force screen redraw | self |
QueueUpdateDraw(func) |
Queue thread-safe UI update | self |
SetFocus(primitive) |
Set keyboard focus | self |
GetFocus() |
Get focused component | Primitive |
SetInputCapture(func) |
Set global key handler | self |
-- Basic application setup
local app = tui.newApp()
local root = tui.newTextView("Press ESC to exit")
-- Global key handler
app:SetInputCapture(function(event)
if event:Key() == 27 then -- ESC
app:Stop()
return nil -- Event handled
end
return event -- Pass through
end)
app:SetRoot(root, true):Run()
-- Advanced app with multiple components
local app = tui.newApp()
-- Create layout
local flex = tui.newFlex():SetDirection(0) -- Vertical
local header = tui.newTextView("🚀 Dashboard"):SetTextAlign(1)
local content = tui.newTextView("Loading...")
local footer = tui.newTextView("Press 'q' to quit"):SetTextAlign(1)
-- Style components
header:SetBackgroundColor(39):SetTextColor(255)
footer:SetBackgroundColor(235)
-- Build layout
flex:AddItem(header, 3, 0, false)
flex:AddItem(content, 0, 1, true) -- Takes remaining space
flex:AddItem(footer, 1, 0, false)
-- Keyboard shortcuts
app:SetInputCapture(function(event)
local key = event:Rune()
if key == 'q' or key == 'Q' then
app:Stop()
return nil
elseif key == 'r' or key == 'R' then
content:SetText("Refreshed at " .. os.date())
return nil
end
return event
end)
app:SetRoot(flex, true):Run()
-- Thread-safe UI updates from goroutines
local app = tui.newApp()
local status = tui.newTextView("Starting...")
local counter = 0
-- Background task updating UI
go(function()
while true do
sleep(1)
counter = counter + 1
-- Thread-safe UI update
app:QueueUpdateDraw(function()
status:SetText(string.format(
"Counter: %d\nTime: %s",
counter,
os.date()
))
end)
end
end)
app:SetRoot(status, true):Run()
tui.newTextView(text)
local textView = tui.newTextView(text) → TextView
Display formatted text with scrolling, colors, and dynamic content. Supports ANSI color codes and clickable regions.
Key Methods
Method | Description | Returns |
---|---|---|
SetText(text) |
Set display text (supports color codes) | self |
SetDynamicColors(bool) |
Enable [color] markup parsing | self |
SetScrollable(bool) |
Enable content scrolling | self |
SetTextAlign(align) |
0=left, 1=center, 2=right | self |
SetWordWrap(bool) |
Enable word wrapping | self |
Clear() |
Clear all content | self |
Write(p) |
Append text (io.Writer interface) | n, err |
[colorCode]text[white]
format when dynamic colors are enabled. Color codes are from the 256-color palette.
-- Basic text display
local text = tui.newTextView("Welcome to Hype!")
text:SetBorder(true)
text:SetTitle("📋 Info")
text:SetTextAlign(1) -- Center
text:SetScrollable(true)
-- Update content
text:SetText([[
Welcome to Hype TUI!
Features:
- Beautiful terminal interfaces
- Rich text formatting
- Scrollable content
- And much more!
Scroll with arrow keys or mouse wheel.
]])
-- Colored text with dynamic colors
local display = tui.newTextView("")
display:SetDynamicColors(true)
display:SetBorder(true)
display:SetTitle("🎨 Color Demo")
-- Color constants
local GREEN = 46
local RED = 196
local BLUE = 39
local YELLOW = 226
local WHITE = 255
display:SetText(string.format([[
[%d]● Success:[%d] Operation completed
[%d]● Error:[%d] Connection failed
[%d]● Warning:[%d] Low memory
[%d]● Info:[%d] System ready
[%d]Status Bar Background[%d]
[%d]━━━━━━━━━━━━━━━━━━━[%d]
]],
GREEN, WHITE,
RED, WHITE,
YELLOW, WHITE,
BLUE, WHITE,
39, 255,
39, 255
))
-- Real-time log viewer
local app = tui.newApp()
local logView = tui.newTextView("")
logView:SetDynamicColors(true)
logView:SetScrollable(true)
logView:SetBorder(true)
logView:SetTitle("📜 System Logs")
-- Log function
local function log(level, message)
local colors = {
INFO = 51, -- Cyan
WARN = 226, -- Yellow
ERROR = 196, -- Red
DEBUG = 245 -- Gray
}
local timestamp = os.date("%H:%M:%S")
local color = colors[level] or 255
local line = string.format(
"[245]%s[255] [%d][%s][255] %s\n",
timestamp, color, level, message
)
app:QueueUpdateDraw(function()
logView:Write(line)
-- Auto-scroll to bottom
logView:ScrollToEnd()
end)
end
-- Simulate log entries
go(function()
local messages = {
{level = "INFO", msg = "System initialized"},
{level = "DEBUG", msg = "Loading configuration"},
{level = "WARN", msg = "Cache miss for key: user_123"},
{level = "INFO", msg = "Connected to database"},
{level = "ERROR", msg = "Failed to load plugin: auth"},
{level = "INFO", msg = "Server started on :8080"}
}
for i, entry in ipairs(messages) do
sleep(0.5)
log(entry.level, entry.msg)
end
end)
app:SetRoot(logView, true):Run()
tui.newInputField()
local input = tui.newInputField() → InputField
Single-line text input with labels, placeholders, validation, and password masking.
Key Methods
Method | Description | Returns |
---|---|---|
SetLabel(text) |
Set field label | self |
SetPlaceholder(text) |
Set placeholder text | self |
SetText(text) |
Set field value | self |
GetText() |
Get current value | string |
SetMaskCharacter(rune) |
Mask input (e.g., '*' for passwords) | self |
SetFieldWidth(width) |
Set field width (0 = full) | self |
SetChangedFunc(func) |
On change callback | self |
SetDoneFunc(func) |
On Enter/Tab callback | self |
-- Basic input field
local input = tui.newInputField()
input:SetLabel("Username: ")
input:SetPlaceholder("Enter your username")
input:SetFieldWidth(30)
-- Password field
local password = tui.newInputField()
password:SetLabel("Password: ")
password:SetPlaceholder("Enter password")
password:SetMaskCharacter('*')
password:SetFieldWidth(30)
-- Handle submission
input:SetDoneFunc(function(field)
-- Move to password field on Enter
app:SetFocus(password)
end)
password:SetDoneFunc(function(field)
local user = input:GetText()
local pass = password:GetText()
-- Process login...
end)
-- Input with real-time validation
local app = tui.newApp()
local flex = tui.newFlex():SetDirection(0)
local emailInput = tui.newInputField()
emailInput:SetLabel("Email: ")
emailInput:SetPlaceholder("user@example.com")
local status = tui.newTextView("Enter a valid email")
status:SetDynamicColors(true)
-- Email validation
local function isValidEmail(email)
return email:match("^[%w._%+-]+@[%w.-]+%.[%w]+$") ~= nil
end
-- Real-time validation
emailInput:SetChangedFunc(function(field, text)
if text == "" then
status:SetText("[245]Enter a valid email[255]")
elseif isValidEmail(text) then
status:SetText("[46]✓ Valid email address[255]")
else
status:SetText("[196]✗ Invalid email format[255]")
end
end)
-- Submit handler
emailInput:SetDoneFunc(function(field)
local email = field:GetText()
if isValidEmail(email) then
status:SetText("[46]Email submitted: " .. email .. "[255]")
-- Process email...
else
status:SetText("[196]Please enter a valid email![255]")
end
end)
flex:AddItem(emailInput, 1, 0, true)
flex:AddItem(status, 1, 0, false)
app:SetRoot(flex, true):Run()
-- Live search implementation
local app = tui.newApp()
local flex = tui.newFlex():SetDirection(0)
local searchInput = tui.newInputField()
searchInput:SetLabel("🔍 Search: ")
searchInput:SetPlaceholder("Type to search...")
local results = tui.newList()
results:ShowSecondaryText(true)
-- Sample data
local data = {
{name = "Alice Johnson", role = "Developer"},
{name = "Bob Smith", role = "Designer"},
{name = "Charlie Brown", role = "Manager"},
{name = "Diana Prince", role = "Developer"},
{name = "Eve Wilson", role = "Analyst"}
}
-- Search function
local function search(query)
results:Clear()
if query == "" then
results:AddItem("Start typing to search...", "", 0, nil)
return
end
local found = 0
query = query:lower()
for _, item in ipairs(data) do
if item.name:lower():find(query) or
item.role:lower():find(query) then
results:AddItem(item.name, item.role, 0, function()
searchInput:SetText(item.name)
end)
found = found + 1
end
end
if found == 0 then
results:AddItem("No results found", "", 0, nil)
end
end
-- Live search on change
searchInput:SetChangedFunc(function(field, text)
search(text)
end)
-- Initial state
search("")
flex:AddItem(searchInput, 1, 0, true)
flex:AddItem(results, 0, 1, false)
app:SetRoot(flex, true):Run()
tui.newForm()
local form = tui.newForm() → Form
Complete form builder with various input types, validation, and submission handling.
Key Methods
Method | Description | Returns |
---|---|---|
AddInputField(label, initial, width, changed, done) |
Add text input | self |
AddPasswordField(label, initial, width, changed, done) |
Add password input | self |
AddDropDown(label, options, initial, selected) |
Add dropdown selector | self |
AddCheckbox(label, checked, changed) |
Add checkbox | self |
AddButton(label, selected) |
Add action button | self |
Clear(includeButtons) |
Clear form fields | self |
GetFormItem(index) |
Get item by index | FormItem |
-- Basic form example
local app = tui.newApp()
local form = tui.newForm()
-- Add form fields
form:AddInputField("Name", "", 30, nil, nil)
form:AddInputField("Email", "", 30, nil, nil)
form:AddPasswordField("Password", "", 30, nil, nil)
-- Add dropdown
form:AddDropDown("Role",
{"Developer", "Designer", "Manager"},
0, -- Initial selection
nil
)
-- Add checkbox
form:AddCheckbox("Subscribe to newsletter", false, nil)
-- Add buttons
form:AddButton("Submit", function()
-- Get form values
local name = form:GetFormItem(0):GetText()
local email = form:GetFormItem(1):GetText()
-- Process form...
end)
form:AddButton("Cancel", function()
app:Stop()
end)
app:SetRoot(form, true):Run()
-- Complete form with validation
local app = tui.newApp()
local pages = tui.newPages()
-- Registration form
local regForm = tui.newForm()
local statusText = tui.newTextView("")
statusText:SetDynamicColors(true)
-- Form data
local formData = {
username = "",
email = "",
password = "",
confirmPassword = "",
country = 0,
terms = false
}
-- Validation functions
local function validateUsername(text)
if #text < 3 then
return false, "Username must be at least 3 characters"
end
if not text:match("^[%w_]+$") then
return false, "Username can only contain letters, numbers, and underscores"
end
return true
end
local function validateEmail(text)
if not text:match("^[%w._%+-]+@[%w.-]+%.[%w]+$") then
return false, "Invalid email format"
end
return true
end
-- Add fields with validation
regForm:AddInputField("Username", "", 30,
function(text)
formData.username = text
local valid, msg = validateUsername(text)
if not valid and text ~= "" then
statusText:SetText("[196]" .. msg .. "[255]")
else
statusText:SetText("")
end
end,
nil
)
regForm:AddInputField("Email", "", 30,
function(text)
formData.email = text
local valid, msg = validateEmail(text)
if not valid and text ~= "" then
statusText:SetText("[196]" .. msg .. "[255]")
else
statusText:SetText("")
end
end,
nil
)
regForm:AddPasswordField("Password", "", 30,
function(text)
formData.password = text
if #text < 8 and text ~= "" then
statusText:SetText("[196]Password must be at least 8 characters[255]")
else
statusText:SetText("")
end
end,
nil
)
regForm:AddPasswordField("Confirm Password", "", 30,
function(text)
formData.confirmPassword = text
if text ~= formData.password and text ~= "" then
statusText:SetText("[196]Passwords do not match[255]")
else
statusText:SetText("")
end
end,
nil
)
regForm:AddDropDown("Country",
{"United States", "Canada", "United Kingdom", "Australia", "Other"},
0,
function(option, index)
formData.country = index
end
)
regForm:AddCheckbox("I agree to the terms and conditions", false,
function(checked)
formData.terms = checked
end
)
-- Form buttons
regForm:AddButton("Register", function()
-- Validate all fields
local errors = {}
local valid, msg = validateUsername(formData.username)
if not valid then table.insert(errors, msg) end
valid, msg = validateEmail(formData.email)
if not valid then table.insert(errors, msg) end
if #formData.password < 8 then
table.insert(errors, "Password too short")
end
if formData.password ~= formData.confirmPassword then
table.insert(errors, "Passwords do not match")
end
if not formData.terms then
table.insert(errors, "You must agree to the terms")
end
if #errors > 0 then
statusText:SetText("[196]Errors:\n" .. table.concat(errors, "\n") .. "[255]")
else
-- Success!
pages:SwitchToPage("success")
end
end)
regForm:AddButton("Clear", function()
regForm:Clear(false) -- Don't clear buttons
statusText:SetText("")
formData = {
username = "",
email = "",
password = "",
confirmPassword = "",
country = 0,
terms = false
}
end)
-- Success page
local successView = tui.newTextView("")
successView:SetDynamicColors(true)
successView:SetTextAlign(1)
successView:SetText([[
[46]✅ Registration Successful![255]
Welcome to Hype!
Press ESC to exit.
]])
-- Build pages
local regPage = tui.newFlex():SetDirection(0)
regPage:AddItem(regForm, 0, 1, true)
regPage:AddItem(statusText, 4, 0, false)
pages:AddPage("register", regPage, true, true)
pages:AddPage("success", successView, true, false)
-- Global key handler
app:SetInputCapture(function(event)
if event:Key() == 27 then -- ESC
app:Stop()
return nil
end
return event
end)
app:SetRoot(pages, true):Run()
Advanced TUI Patterns
Dashboard Layout
Create professional dashboard layouts with multiple panels.
-- Professional dashboard layout
local app = tui.newApp()
-- Create components
local header = tui.newTextView("🚀 System Dashboard")
header:SetTextAlign(1):SetTextColor(39):SetBackgroundColor(235)
local sidebar = tui.newList()
sidebar:SetBorder(true):SetTitle("Menu")
sidebar:AddItem("Overview", "", 0, nil)
sidebar:AddItem("Analytics", "", 0, nil)
sidebar:AddItem("Settings", "", 0, nil)
local mainContent = tui.newTextView("Select an option from the menu")
mainContent:SetBorder(true):SetTitle("Content")
local statusBar = tui.newTextView("Ready")
statusBar:SetTextAlign(2):SetBackgroundColor(39):SetTextColor(255)
-- Layout structure
local mainLayout = tui.newFlex():SetDirection(1) -- Horizontal
local contentArea = tui.newFlex():SetDirection(0) -- Vertical
mainLayout:AddItem(sidebar, 30, 0, true)
mainLayout:AddItem(contentArea, 0, 1, false)
contentArea:AddItem(mainContent, 0, 1, false)
contentArea:AddItem(statusBar, 1, 0, false)
local root = tui.newFlex():SetDirection(0) -- Vertical
root:AddItem(header, 3, 0, false)
root:AddItem(mainLayout, 0, 1, true)
-- Menu interaction
sidebar:SetSelectedFunc(function(index, main, secondary)
mainContent:SetText("Loading " .. main .. "...")
-- Load content based on selection
end)
app:SetRoot(root, true):Run()
- Use
QueueUpdateDraw
for thread-safe UI updates from goroutines - Enable dynamic colors for rich text formatting
- Set proper tab order with focus management
- Provide keyboard shortcuts for common actions
- Use borders and titles to organize complex layouts
🌐 HTTP Module - Web Services & APIs
Build HTTP clients and servers with ease. Perfect for REST APIs, webhooks, microservices, and web applications.
local http = require('http')
HTTP Client
http.get(url, headers?, timeout?)
local response, err = http.get(url, headers, timeout) → table, string
Perform HTTP GET request with optional headers and timeout.
Parameters
Parameter | Type | Description |
---|---|---|
url |
string | Target URL (required) |
headers |
table | HTTP headers (optional) |
timeout |
number | Timeout in seconds (optional, default: 30) |
status_code
, body
, headers
or nil + error
-- Basic GET request
local http = require('http')
local resp, err = http.get("https://api.github.com/users/github")
if not resp then
print("Error:", err)
return
end
print("Status:", resp.status_code)
print("Body:", resp.body)
print("Content-Type:", resp.headers["Content-Type"])
-- Parse JSON response
local json = require('json') -- Requires json plugin
local data = json.decode(resp.body)
print("Name:", data.name)
print("Followers:", data.followers)
-- Authenticated request
local http = require('http')
-- Bearer token authentication
local headers = {
["Authorization"] = "Bearer " .. os.getenv("API_TOKEN"),
["Accept"] = "application/json",
["User-Agent"] = "Hype/1.0"
}
local resp, err = http.get(
"https://api.example.com/protected/resource",
headers,
10 -- 10 second timeout
)
if not resp then
print("Request failed:", err)
return
end
if resp.status_code == 401 then
print("Authentication failed")
elseif resp.status_code == 200 then
print("Success:", resp.body)
else
print("Unexpected status:", resp.status_code)
end
-- GET with retry logic
local http = require('http')
function httpGetWithRetry(url, maxRetries, backoff)
maxRetries = maxRetries or 3
backoff = backoff or 1
for attempt = 1, maxRetries do
local resp, err = http.get(url, nil, 5)
if resp and resp.status_code == 200 then
return resp -- Success
end
-- Log attempt
print(string.format(
"Attempt %d/%d failed: %s",
attempt,
maxRetries,
err or "Status " .. (resp and resp.status_code or "unknown")
))
if attempt < maxRetries then
-- Exponential backoff
local delay = backoff * (2 ^ (attempt - 1))
print(string.format("Retrying in %.1f seconds...", delay))
sleep(delay)
end
end
return nil, "Max retries exceeded"
end
-- Usage
local resp, err = httpGetWithRetry("https://api.example.com/data", 3, 1)
if resp then
print("Success after retries:", resp.body)
else
print("Failed after all retries:", err)
end
http.post(url, body, headers?, timeout?)
local response, err = http.post(url, body, headers, timeout) → table, string
Send HTTP POST request with body data.
-- POST JSON data
local http = require('http')
local json = require('json')
-- Prepare data
local data = {
name = "John Doe",
email = "john@example.com",
age = 30,
tags = {"customer", "premium"}
}
-- Convert to JSON
local body = json.encode(data)
-- Send request
local resp, err = http.post(
"https://api.example.com/users",
body,
{
["Content-Type"] = "application/json",
["Accept"] = "application/json"
}
)
if resp and resp.status_code == 201 then
print("User created successfully")
local created = json.decode(resp.body)
print("User ID:", created.id)
else
print("Error:", err or resp.body)
end
-- POST form data
local http = require('http')
-- URL encode function
function urlencode(str)
str = string.gsub(str, "([^%w%-%.%_%~ ])", function(c)
return string.format("%%%02X", string.byte(c))
end)
str = string.gsub(str, " ", "+")
return str
end
-- Build form data
local formData = {
username = "john_doe",
password = "secure123",
remember = "true"
}
local body = {}
for k, v in pairs(formData) do
table.insert(body, urlencode(k) .. "=" .. urlencode(v))
end
body = table.concat(body, "&")
-- Send as form
local resp, err = http.post(
"https://example.com/login",
body,
{
["Content-Type"] = "application/x-www-form-urlencoded"
}
)
if resp then
-- Check for redirect or success
if resp.status_code == 302 then
print("Login successful, redirect to:", resp.headers["Location"])
elseif resp.status_code == 200 then
print("Login response:", resp.body)
else
print("Login failed:", resp.status_code)
end
end
-- Webhook implementation
local http = require('http')
local crypto = require('crypto')
local json = require('json')
function sendWebhook(url, event, data, secret)
-- Prepare payload
local payload = {
event = event,
timestamp = os.time(),
data = data
}
local body = json.encode(payload)
-- Generate signature
local signature = crypto.sha256(body .. secret)
-- Send webhook
local resp, err = http.post(url, body, {
["Content-Type"] = "application/json",
["X-Webhook-Event"] = event,
["X-Webhook-Signature"] = signature,
["X-Webhook-Timestamp"] = tostring(payload.timestamp)
}, 5) -- 5 second timeout
return resp, err
end
-- Usage example
local webhookUrl = "https://example.com/webhooks/receiver"
local secret = "webhook_secret_key"
-- Send different events
local events = {
{
type = "user.created",
data = {id = 123, email = "user@example.com"}
},
{
type = "order.completed",
data = {order_id = 456, amount = 99.99}
}
}
for _, event in ipairs(events) do
local resp, err = sendWebhook(
webhookUrl,
event.type,
event.data,
secret
)
if resp and resp.status_code == 200 then
print("Webhook delivered:", event.type)
else
print("Webhook failed:", event.type, err or resp.status_code)
end
end
HTTP Server
http.newServer()
local server = http.newServer() → Server
Create a new HTTP server instance with routing and middleware support.
Server Methods
Method | Description | Returns |
---|---|---|
handle(pattern, handler) |
Register route handler | self |
listen(port) |
Start server on port | nil |
stop() |
Stop server gracefully | nil |
Request Object
Field | Type | Description |
---|---|---|
method |
string | HTTP method (GET, POST, etc.) |
url |
string | Full request URL |
path |
string | URL path |
headers |
table | Request headers |
query |
table | Query parameters |
params |
table | URL parameters (e.g., :id) |
body |
string | Request body |
Response Methods
Method | Description | Returns |
---|---|---|
write(content) |
Write response body | self |
json(table) |
Send JSON response | self |
status(code) |
Set HTTP status code | self |
header(key, value) |
Set response header | self |
-- Basic HTTP server
local http = require('http')
local server = http.newServer()
-- Simple route
server:handle("/", function(req, res)
res:write("Welcome to Hype HTTP Server!")
end)
-- JSON response
server:handle("/api/info", function(req, res)
res:json({
server = "Hype",
version = "1.0",
timestamp = os.time()
})
end)
-- Handle different methods
server:handle("GET /users", function(req, res)
res:json({users = {"Alice", "Bob", "Charlie"}})
end)
server:handle("POST /users", function(req, res)
-- Parse JSON body
local json = require('json')
local user = json.decode(req.body)
res:status(201):json({
id = math.random(1000),
name = user.name,
created = os.date()
})
end)
-- URL parameters
server:handle("/users/:id", function(req, res)
res:json({
user_id = req.params.id,
name = "User " .. req.params.id
})
end)
-- Query parameters
server:handle("/search", function(req, res)
local query = req.query.q or ""
local limit = tonumber(req.query.limit) or 10
res:json({
query = query,
limit = limit,
results = {}
})
end)
print("Server starting on http://localhost:8080")
server:listen(8080)
-- Complete REST API
local http = require('http')
local json = require('json')
local kv = require('kv')
-- Initialize database
local db = kv.open("./api.db")
db:open_db("items")
local server = http.newServer()
-- Middleware for JSON parsing
local function parseJSON(handler)
return function(req, res)
if req.headers["Content-Type"] == "application/json" and req.body ~= "" then
local ok, data = pcall(json.decode, req.body)
if ok then
req.json = data
else
res:status(400):json({error = "Invalid JSON"})
return
end
end
handler(req, res)
end
end
-- CORS middleware
local function cors(handler)
return function(req, res)
res:header("Access-Control-Allow-Origin", "*")
res:header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
res:header("Access-Control-Allow-Headers", "Content-Type")
if req.method == "OPTIONS" then
res:status(204):write("")
return
end
handler(req, res)
end
end
-- Routes
-- GET all items
server:handle("GET /api/items", cors(function(req, res)
local items = {}
local cursor = db:cursor("items")
for key, value in cursor:iter() do
local item = json.decode(value)
item.id = key
table.insert(items, item)
end
res:json({
count = #items,
items = items
})
end))
-- GET single item
server:handle("GET /api/items/:id", cors(function(req, res)
local id = req.params.id
local data = db:get("items", id)
if data then
local item = json.decode(data)
item.id = id
res:json(item)
else
res:status(404):json({error = "Item not found"})
end
end))
-- POST create item
server:handle("POST /api/items", cors(parseJSON(function(req, res)
if not req.json or not req.json.name then
res:status(400):json({error = "Name required"})
return
end
local id = tostring(os.time() .. math.random(1000))
local item = {
name = req.json.name,
description = req.json.description or "",
created = os.date(),
updated = os.date()
}
db:put("items", id, json.encode(item))
item.id = id
res:status(201)
:header("Location", "/api/items/" .. id)
:json(item)
end)))
-- PUT update item
server:handle("PUT /api/items/:id", cors(parseJSON(function(req, res)
local id = req.params.id
local existing = db:get("items", id)
if not existing then
res:status(404):json({error = "Item not found"})
return
end
local item = json.decode(existing)
-- Update fields
if req.json.name then item.name = req.json.name end
if req.json.description then item.description = req.json.description end
item.updated = os.date()
db:put("items", id, json.encode(item))
item.id = id
res:json(item)
end)))
-- DELETE item
server:handle("DELETE /api/items/:id", cors(function(req, res)
local id = req.params.id
local existing = db:get("items", id)
if not existing then
res:status(404):json({error = "Item not found"})
return
end
db:delete("items", id)
res:status(204):write("")
end))
-- Health check
server:handle("/health", function(req, res)
res:json({
status = "healthy",
timestamp = os.time(),
database = "connected"
})
end)
print("REST API running on http://localhost:8080")
print("Try: curl http://localhost:8080/api/items")
server:listen(8080)
-- Advanced middleware patterns
local http = require('http')
local crypto = require('crypto')
local json = require('json')
local server = http.newServer()
-- Request logging middleware
local function logger(handler)
return function(req, res)
local start = os.clock()
-- Wrap response methods to capture status
local originalStatus = res.status
local statusCode = 200
res.status = function(self, code)
statusCode = code
return originalStatus(self, code)
end
-- Call handler
handler(req, res)
-- Log request
local duration = (os.clock() - start) * 1000
print(string.format(
"%s %s %s - %d - %.2fms",
os.date("%Y-%m-%d %H:%M:%S"),
req.method,
req.path,
statusCode,
duration
))
end
end
-- Authentication middleware
local function authenticate(handler)
return function(req, res)
local auth = req.headers["Authorization"]
if not auth or not auth:match("^Bearer ") then
res:status(401):json({error = "Authentication required"})
return
end
local token = auth:sub(8) -- Remove "Bearer "
-- Verify token (simplified - use proper JWT in production)
if token ~= "valid-token-123" then
res:status(403):json({error = "Invalid token"})
return
end
-- Add user to request
req.user = {id = 1, name = "John Doe"}
handler(req, res)
end
end
-- Rate limiting middleware
local requestCounts = {}
local function rateLimit(maxRequests, windowSeconds)
return function(handler)
return function(req, res)
local ip = req.headers["X-Forwarded-For"] or "unknown"
local now = os.time()
local key = ip .. ":" .. math.floor(now / windowSeconds)
requestCounts[key] = (requestCounts[key] or 0) + 1
if requestCounts[key] > maxRequests then
res:status(429):json({
error = "Too many requests",
retry_after = windowSeconds
})
return
end
handler(req, res)
end
end
end
-- Error handling middleware
local function errorHandler(handler)
return function(req, res)
local ok, err = pcall(handler, req, res)
if not ok then
print("Error:", err)
res:status(500):json({
error = "Internal server error",
message = err
})
end
end
end
-- Apply middleware to routes
-- Public route
server:handle("/", logger(function(req, res)
res:json({message = "Welcome to the API"})
end))
-- Protected route
server:handle("/api/profile", logger(authenticate(function(req, res)
res:json({
user = req.user,
message = "This is a protected endpoint"
})
end)))
-- Rate limited route
server:handle("/api/search", logger(rateLimit(10, 60)(function(req, res)
res:json({
query = req.query.q,
results = []
})
end)))
-- Route with error handling
server:handle("/api/process", logger(errorHandler(function(req, res)
-- This might throw an error
local data = json.decode(req.body)
if not data.required_field then
error("Missing required field")
end
res:json({processed = true})
end)))
-- Compose multiple middleware
local function compose(...)
local middleware = {...}
return function(handler)
for i = #middleware, 1, -1 do
handler = middleware[i](handler)
end
return handler
end
end
-- Use composed middleware
server:handle("/api/admin",
compose(logger, authenticate, rateLimit(5, 60))(function(req, res)
res:json({
message = "Admin endpoint",
user = req.user
})
end)
)
print("Server with middleware running on http://localhost:8080")
server:listen(8080)
- Always set appropriate Content-Type headers
- Use proper HTTP status codes (200, 201, 404, etc.)
- Implement request timeouts to prevent hanging
- Add CORS headers for browser compatibility
- Use middleware for cross-cutting concerns
- Validate input data before processing
- Handle errors gracefully with proper error responses
🔌 WebSocket Module - Real-Time Communication
Build real-time applications with bidirectional communication. Perfect for chat apps, live updates, collaborative tools, and IoT.
local websocket = require('websocket')
or local ws = require('ws')
WebSocket Client
websocket.dial(url, headers?)
local conn, err = websocket.dial(url, headers) → Connection, string
Connect to a WebSocket server.
Connection Methods
Method | Description | Returns |
---|---|---|
write_text(message) |
Send text message | error |
write_binary(data) |
Send binary data | error |
read() |
Read next message | type, data |
close(code?, reason?) |
Close connection | error |
ping() |
Send ping frame | error |
-- Basic WebSocket client
local ws = require('websocket')
-- Connect to server
local conn, err = ws.dial("ws://localhost:8080/ws")
if not conn then
print("Connection failed:", err)
return
end
print("Connected to WebSocket server")
-- Send message
conn:write_text("Hello, Server!")
-- Read messages
go(function()
while true do
local msg_type, msg = conn:read()
if msg_type == 1 then -- Text message
print("Received:", msg)
elseif msg_type == 2 then -- Binary message
print("Binary data:", #msg, "bytes")
elseif msg_type == 8 then -- Close
print("Server closed connection")
break
elseif msg_type == 9 then -- Ping
-- Auto-handled by most implementations
end
end
end)
-- Send periodic pings
go(function()
while true do
sleep(30)
local err = conn:ping()
if err then
print("Ping failed:", err)
break
end
end
end)
-- Keep running
sleep(60)
conn:close(1000, "Goodbye")
-- Interactive chat client
local ws = require('websocket')
local json = require('json')
-- Chat client implementation
function createChatClient(url, username)
local client = {
conn = nil,
username = username,
connected = false
}
function client:connect()
local conn, err = ws.dial(url)
if not conn then
return false, err
end
self.conn = conn
self.connected = true
-- Send join message
self:send({
type = "join",
username = self.username,
timestamp = os.time()
})
-- Start message reader
go(function()
self:readLoop()
end)
return true
end
function client:send(data)
if not self.connected then
return false, "Not connected"
end
local msg = json.encode(data)
return self.conn:write_text(msg)
end
function client:sendMessage(text)
return self:send({
type = "message",
username = self.username,
text = text,
timestamp = os.time()
})
end
function client:readLoop()
while self.connected do
local msg_type, data = self.conn:read()
if msg_type == 1 then -- Text message
local ok, msg = pcall(json.decode, data)
if ok then
self:handleMessage(msg)
end
elseif msg_type == 8 then -- Close
self.connected = false
print("\n[Disconnected from server]")
break
end
end
end
function client:handleMessage(msg)
if msg.type == "message" then
print(string.format(
"\n[%s] %s: %s",
os.date("%H:%M:%S", msg.timestamp),
msg.username,
msg.text
))
elseif msg.type == "join" then
print(string.format(
"\n[%s joined the chat]",
msg.username
))
elseif msg.type == "leave" then
print(string.format(
"\n[%s left the chat]",
msg.username
))
elseif msg.type == "users" then
print("\nOnline users:", table.concat(msg.users, ", "))
end
end
function client:disconnect()
if self.connected then
self:send({
type = "leave",
username = self.username
})
self.conn:close()
self.connected = false
end
end
return client
end
-- Usage
local username = "User" .. math.random(1000)
local client = createChatClient("ws://localhost:8080/chat", username)
local ok, err = client:connect()
if not ok then
print("Failed to connect:", err)
return
end
print("Connected as", username)
print("Type messages (or 'quit' to exit):")
-- Read user input
while true do
io.write("> ")
io.flush()
local input = io.read()
if input == "quit" then
break
elseif input == "/users" then
client:send({type = "list_users"})
elseif input ~= "" then
client:sendMessage(input)
end
end
client:disconnect()
print("Goodbye!")
-- WebSocket client with auto-reconnect
local ws = require('websocket')
function createReconnectingWebSocket(url, options)
options = options or {}
local client = {
url = url,
conn = nil,
connected = false,
reconnectInterval = options.reconnectInterval or 5,
maxReconnectInterval = options.maxReconnectInterval or 30,
reconnectAttempts = 0,
maxReconnectAttempts = options.maxReconnectAttempts or nil,
onOpen = options.onOpen or function() end,
onMessage = options.onMessage or function() end,
onClose = options.onClose or function() end,
onError = options.onError or function() end,
shouldReconnect = true
}
function client:connect()
print("Connecting to", self.url)
local conn, err = ws.dial(self.url)
if not conn then
self:onError("Connection failed: " .. err)
self:scheduleReconnect()
return false
end
self.conn = conn
self.connected = true
self.reconnectAttempts = 0
print("Connected successfully")
self:onOpen()
-- Start read loop
go(function()
self:readLoop()
end)
return true
end
function client:readLoop()
while self.connected do
local msg_type, data = self.conn:read()
if msg_type == 1 then -- Text
self:onMessage(data)
elseif msg_type == 8 then -- Close
self.connected = false
self:onClose("Server closed connection")
self:scheduleReconnect()
break
elseif msg_type == nil then -- Error
self.connected = false
self:onError("Read error: " .. (data or "unknown"))
self:scheduleReconnect()
break
end
end
end
function client:scheduleReconnect()
if not self.shouldReconnect then
return
end
if self.maxReconnectAttempts and
self.reconnectAttempts >= self.maxReconnectAttempts then
print("Max reconnection attempts reached")
return
end
self.reconnectAttempts = self.reconnectAttempts + 1
-- Exponential backoff
local interval = math.min(
self.reconnectInterval * (2 ^ (self.reconnectAttempts - 1)),
self.maxReconnectInterval
)
print(string.format(
"Reconnecting in %d seconds (attempt %d)",
interval,
self.reconnectAttempts
))
go(function()
sleep(interval)
if self.shouldReconnect then
self:connect()
end
end)
end
function client:send(message)
if not self.connected then
self:onError("Not connected")
return false
end
local err = self.conn:write_text(message)
if err then
self:onError("Send error: " .. err)
return false
end
return true
end
function client:close()
self.shouldReconnect = false
if self.connected and self.conn then
self.conn:close()
self.connected = false
end
end
return client
end
-- Usage example
local client = createReconnectingWebSocket("ws://localhost:8080/ws", {
reconnectInterval = 2,
maxReconnectInterval = 30,
maxReconnectAttempts = 10,
onOpen = function()
print("WebSocket opened")
-- Subscribe to updates
client:send(json.encode({
action = "subscribe",
channels = ["updates", "alerts"]
}))
end,
onMessage = function(data)
print("Message:", data)
-- Process message
local ok, msg = pcall(json.decode, data)
if ok then
-- Handle different message types
if msg.type == "update" then
print("Update:", msg.data)
elseif msg.type == "alert" then
print("Alert:", msg.message)
end
end
end,
onClose = function(reason)
print("WebSocket closed:", reason)
end,
onError = function(err)
print("WebSocket error:", err)
end
})
-- Connect
client:connect()
-- Send periodic heartbeat
go(function()
while true do
sleep(30)
if client.connected then
client:send(json.encode({
type = "heartbeat",
timestamp = os.time()
}))
end
end
end)
-- Keep running
print("Press Enter to quit")
io.read()
client:close()
WebSocket Server
websocket.newServer()
local server = websocket.newServer() → Server
Create a WebSocket server that can handle multiple concurrent connections.
-- Simple echo server
local ws = require('websocket')
local server = ws.newServer()
server:handle("/echo", function(conn, req)
print("New connection from", req.headers["X-Forwarded-For"] or "unknown")
-- Send welcome message
conn:write_text("Welcome to Echo Server!")
-- Echo loop
while true do
local msg_type, data = conn:read()
if msg_type == 1 then -- Text
print("Received:", data)
-- Echo back
conn:write_text("Echo: " .. data)
elseif msg_type == 2 then -- Binary
print("Binary:", #data, "bytes")
-- Echo binary
conn:write_binary(data)
elseif msg_type == 8 then -- Close
print("Client disconnected")
break
elseif msg_type == nil then -- Error
print("Read error:", data)
break
end
end
end)
print("Echo server running on ws://localhost:8080/echo")
server:listen(8080)
-- Broadcast server
local ws = require('websocket')
local json = require('json')
local server = ws.newServer()
local clients = {}
local clientId = 0
-- Broadcast to all clients
function broadcast(message, excludeConn)
local data = json.encode(message)
for id, client in pairs(clients) do
if client.conn ~= excludeConn then
local err = client.conn:write_text(data)
if err then
print("Failed to send to client", id, err)
clients[id] = nil
end
end
end
end
-- Send to specific client
function sendTo(clientId, message)
local client = clients[clientId]
if client then
local data = json.encode(message)
client.conn:write_text(data)
end
end
server:handle("/broadcast", function(conn, req)
-- Assign client ID
clientId = clientId + 1
local id = clientId
-- Store client
clients[id] = {
conn = conn,
id = id,
joined = os.time()
}
print("Client", id, "connected")
-- Notify others
broadcast({
type = "user_joined",
userId = id,
timestamp = os.time()
}, conn)
-- Send welcome message
conn:write_text(json.encode({
type = "welcome",
yourId = id,
onlineUsers = #clients
}))
-- Handle messages
while true do
local msg_type, data = conn:read()
if msg_type == 1 then
local ok, msg = pcall(json.decode, data)
if ok then
print("Client", id, "says:", msg.text)
-- Broadcast message
broadcast({
type = "message",
userId = id,
text = msg.text,
timestamp = os.time()
}, nil) -- Send to everyone including sender
end
elseif msg_type == 8 or msg_type == nil then
break
end
end
-- Clean up
clients[id] = nil
print("Client", id, "disconnected")
-- Notify others
broadcast({
type = "user_left",
userId = id,
timestamp = os.time()
}, nil)
end)
-- Status endpoint
server:handle("/status", function(conn, req)
conn:write_text(json.encode({
type = "status",
onlineUsers = #clients,
uptime = os.time()
}))
conn:close()
end)
print("Broadcast server running on ws://localhost:8080/broadcast")
server:listen(8080)
-- Chat room server
local ws = require('websocket')
local json = require('json')
local server = ws.newServer()
-- Room management
local rooms = {}
local clients = {}
local nextClientId = 1
-- Create or get room
function getRoom(name)
if not rooms[name] then
rooms[name] = {
name = name,
clients = {},
created = os.time()
}
end
return rooms[name]
end
-- Join room
function joinRoom(client, roomName)
-- Leave current room
if client.room then
leaveRoom(client)
end
-- Join new room
local room = getRoom(roomName)
room.clients[client.id] = client
client.room = roomName
-- Notify room members
broadcastToRoom(roomName, {
type = "user_joined",
userId = client.id,
username = client.username,
room = roomName
}, client.id)
-- Send room info to client
sendToClient(client.id, {
type = "room_joined",
room = roomName,
users = getRoomUsers(roomName)
})
end
-- Leave room
function leaveRoom(client)
if not client.room then return end
local room = rooms[client.room]
if room then
room.clients[client.id] = nil
-- Notify others
broadcastToRoom(client.room, {
type = "user_left",
userId = client.id,
username = client.username
}, client.id)
-- Delete empty rooms
if next(room.clients) == nil then
rooms[client.room] = nil
end
end
client.room = nil
end
-- Get users in room
function getRoomUsers(roomName)
local room = rooms[roomName]
if not room then return {} end
local users = {}
for id, client in pairs(room.clients) do
table.insert(users, {
id = id,
username = client.username
})
end
return users
end
-- Broadcast to room
function broadcastToRoom(roomName, message, excludeId)
local room = rooms[roomName]
if not room then return end
local data = json.encode(message)
for id, client in pairs(room.clients) do
if id ~= excludeId then
local err = client.conn:write_text(data)
if err then
print("Failed to send to client", id)
removeClient(id)
end
end
end
end
-- Send to specific client
function sendToClient(clientId, message)
local client = clients[clientId]
if client then
client.conn:write_text(json.encode(message))
end
end
-- Remove client
function removeClient(clientId)
local client = clients[clientId]
if client then
leaveRoom(client)
clients[clientId] = nil
end
end
-- WebSocket handler
server:handle("/chat", function(conn, req)
-- Create client
local clientId = nextClientId
nextClientId = nextClientId + 1
local client = {
id = clientId,
conn = conn,
username = "User" .. clientId,
room = nil
}
clients[clientId] = client
print("Client", clientId, "connected")
-- Send welcome
sendToClient(clientId, {
type = "welcome",
clientId = clientId,
rooms = getRoomList()
})
-- Handle messages
while true do
local msg_type, data = conn:read()
if msg_type == 1 then
local ok, msg = pcall(json.decode, data)
if ok then
handleMessage(client, msg)
end
elseif msg_type == 8 or msg_type == nil then
break
end
end
-- Clean up
print("Client", clientId, "disconnected")
removeClient(clientId)
end)
-- Handle client messages
function handleMessage(client, msg)
if msg.type == "set_username" then
client.username = msg.username
sendToClient(client.id, {
type = "username_set",
username = client.username
})
elseif msg.type == "join_room" then
joinRoom(client, msg.room)
elseif msg.type == "leave_room" then
leaveRoom(client)
elseif msg.type == "message" then
if client.room then
broadcastToRoom(client.room, {
type = "message",
userId = client.id,
username = client.username,
text = msg.text,
timestamp = os.time()
}, nil) -- Include sender
end
elseif msg.type == "list_rooms" then
sendToClient(client.id, {
type = "room_list",
rooms = getRoomList()
})
elseif msg.type == "private_message" then
local target = clients[msg.targetId]
if target then
sendToClient(msg.targetId, {
type = "private_message",
fromId = client.id,
fromUsername = client.username,
text = msg.text
})
end
end
end
-- Get list of rooms
function getRoomList()
local list = {}
for name, room in pairs(rooms) do
table.insert(list, {
name = name,
users = #room.clients
})
end
return list
end
print("Chat room server running on ws://localhost:8080/chat")
server:listen(8080)
- Implement heartbeat/ping to detect disconnections
- Add reconnection logic for resilient clients
- Use JSON for structured message passing
- Handle all message types (text, binary, close)
- Implement proper connection cleanup
- Consider message size limits
- Add authentication before accepting connections
- Use connection pooling for scalability
🛠️ Utility Functions
Helper functions and patterns for common tasks in Hype applications.
Goroutines
go(function) → nil
Run functions concurrently in lightweight threads (goroutines).
-- Run concurrent tasks
go(function()
print("Task 1 starting")
sleep(2)
print("Task 1 done")
end)
go(function()
print("Task 2 starting")
sleep(1)
print("Task 2 done")
end)
-- Output:
-- Task 1 starting
-- Task 2 starting
-- Task 2 done
-- Task 1 done
Sleep Function
sleep(seconds) → nil
Pause execution for specified number of seconds (supports decimals).
-- Sleep examples
sleep(1) -- Sleep for 1 second
sleep(0.5) -- Sleep for 500ms
sleep(0.001) -- Sleep for 1ms
-- Animation loop
go(function()
local frames = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
local i = 1
while true do
io.write("\r" .. frames[i] .. " Loading...")
io.flush()
i = (i % #frames) + 1
sleep(0.1)
end
end)
Command Line Arguments
arg[0], arg[1], ... → string
Access command line arguments passed to your script or executable.
-- Argument handling
print("Script:", arg[0])
print("Args:", #arg)
-- Parse command line flags
local options = {
port = 8080,
host = "localhost",
debug = false
}
local i = 1
while i <= #arg do
local a = arg[i]
if a == "--port" or a == "-p" then
i = i + 1
options.port = tonumber(arg[i]) or 8080
elseif a == "--host" or a == "-h" then
i = i + 1
options.host = arg[i]
elseif a == "--debug" or a == "-d" then
options.debug = true
elseif a == "--help" then
print([[
Usage: myapp [options]
Options:
-p, --port PORT Server port (default: 8080)
-h, --host HOST Server host (default: localhost)
-d, --debug Enable debug mode
--help Show this help
]])
os.exit(0)
end
i = i + 1
end
print("Starting server on " .. options.host .. ":" .. options.port)
Environment Variables
os.getenv(name) → string|nil
Access environment variables for configuration.
-- Environment configuration
local config = {
port = tonumber(os.getenv("PORT")) or 8080,
dbPath = os.getenv("DB_PATH") or "./data.db",
apiKey = os.getenv("API_KEY"),
debug = os.getenv("DEBUG") == "true"
}
-- Require certain variables
if not config.apiKey then
error("API_KEY environment variable required")
end
-- Development vs Production
local env = os.getenv("HYPE_ENV") or "development"
if env == "production" then
config.logLevel = "error"
else
config.logLevel = "debug"
end
Error Handling Patterns
Robust error handling for production applications.
-- Safe function wrapper
function safe(fn)
return function(...)
local results = {pcall(fn, ...)}
local ok = table.remove(results, 1)
if ok then
return table.unpack(results)
else
local err = results[1]
print("Error:", err)
return nil, err
end
end
end
-- Try-catch pattern
function try(fn, catch)
local ok, result = pcall(fn)
if ok then
return result
else
if catch then
return catch(result)
else
error(result)
end
end
end
-- Usage
local result = try(function()
-- Risky operation
return json.decode(data)
end, function(err)
print("JSON decode failed:", err)
return {} -- Default value
end)
🫖 Bubble Tea Plugin (NEW!)
Modern TUI framework based on The Elm Architecture for building reactive terminal interfaces.
Overview
The Bubble Tea plugin brings the power of functional reactive programming to Hype TUIs. Based on The Elm Architecture, it provides a clean separation of state, updates, and views.
Core Concepts
-- A simple counter app with Bubble Tea
local tea = require('bubbletea')
-- Model: Your application state
local function initialModel()
return {
counter = 0
}
end
-- Update: Handle messages and update the model
local function update(model, msg)
if msg.type == tea.MSG_KEY then
if msg.key == tea.KEY_CTRL_C or msg.key == "q" then
return model, tea.quit()
elseif msg.key == "+" or msg.key == tea.KEY_UP then
model.counter = model.counter + 1
elseif msg.key == "-" or msg.key == tea.KEY_DOWN then
model.counter = model.counter - 1
end
end
return model, nil
end
-- View: Render the UI
local function view(model)
return string.format([[
🫖 Bubble Tea Counter
Count: %d
Press +/↑ to increment
Press -/↓ to decrement
Press q to quit
]], model.counter)
end
-- Run the program
local program = tea.newProgram(initialModel(), update, view)
:withAltScreen()
program:run()
-- Using Bubble Tea components
local tea = require('bubbletea')
local function initialModel()
return {
-- Text input component
nameInput = tea.textinput.new()
:setPlaceholder("Enter your name...")
:setWidth(30)
:focus(),
-- List component
todoList = tea.list.new({"Buy milk", "Walk dog", "Write code"})
:setHeight(10),
-- Progress bar
progress = tea.progress.new(100)
:setWidth(40),
-- Spinner
spinner = tea.spinner.new("dots")
:setText("Loading..."),
-- Viewport for scrolling
viewport = tea.viewport.new(80, 20)
:setContent(longText)
}
end
-- Form with multiple inputs
local function createForm()
return {
username = tea.textinput.new()
:setPlaceholder("Username")
:setCharLimit(20),
password = tea.textinput.new()
:setPlaceholder("Password")
:setEchoMode("password"),
email = tea.textinput.new()
:setPlaceholder("Email")
:setValidate(function(s)
return s:match("^[%w.]+@[%w.]+$") ~= nil
end),
bio = tea.textarea.new()
:setSize(40, 5)
:setPlaceholder("Tell us about yourself...")
}
end
-- List with custom rendering
local items = {
{name = "Apple", emoji = "🍎", price = 0.5},
{name = "Banana", emoji = "🍌", price = 0.3},
{name = "Cherry", emoji = "🍒", price = 0.8}
}
local fruitList = tea.list.new(items)
:setItemRenderer(function(item, index, selected)
local prefix = selected and "▸ " or " "
return string.format("%s%s %s - $%.2f",
prefix, item.emoji, item.name, item.price)
end)
:setHeight(10)
-- Bubble Tea styling system (Lip Gloss inspired)
local tea = require('bubbletea')
local style = tea.style
-- Create styles
local titleStyle = style.new()
:foreground("cyan")
:background("blue")
:setBold(true)
:setAlign("center")
:padding(1, 2)
:border("rounded")
local errorStyle = style.new()
:foreground("red")
:setBold(true)
:setUnderline(true)
local successStyle = style.new()
:foreground("green")
:background("black")
:padding(1)
-- Apply styles
local title = titleStyle:render("🫖 Welcome to Bubble Tea!")
local error = errorStyle:render("Error: Invalid input")
local success = successStyle:render("✓ Operation completed")
-- Preset styles
local muted = style.presets.muted():render("Subtle text")
local highlight = style.presets.highlight():render("Important!")
local code = style.presets.code():render("const x = 42")
-- Complex layouts with styles
local function renderCard(title, content)
local cardStyle = style.new()
:border("normal")
:padding(1)
:margin(1)
:setWidth(40)
local titleStyle = style.new()
:foreground("yellow")
:setBold(true)
:marginBottom(1)
local card = titleStyle:render(title) .. "\n" .. content
return cardStyle:render(card)
end
-- Color utilities
local rainbow = {
style.new():foreground("red"):render("R"),
style.new():foreground("yellow"):render("A"),
style.new():foreground("green"):render("I"),
style.new():foreground("cyan"):render("N"),
style.new():foreground("blue"):render("B"),
style.new():foreground("magenta"):render("O"),
style.new():foreground("red"):render("W")
}
-- Join styled elements
local output = table.concat(rainbow) .. "\n" ..
style.new():setFaint(true):render("faded text") .. "\n" ..
style.new():setBlink(true):render("blinking!")
Architecture Pattern
The Elm Architecture provides a clean, predictable way to build interactive applications:
-- The Elm Architecture in action
local tea = require('bubbletea')
-- 1. MODEL: Define your application state
local function initialModel()
return {
todos = {},
input = tea.textinput.new():focus(),
filter = "all" -- all, active, completed
}
end
-- 2. MESSAGES: Define what can happen
local MSG_ADD_TODO = "add_todo"
local MSG_TOGGLE_TODO = "toggle_todo"
local MSG_DELETE_TODO = "delete_todo"
local MSG_FILTER_CHANGE = "filter_change"
-- 3. UPDATE: Handle messages and return new model + commands
local function update(model, msg)
-- Handle keyboard input
if msg.type == tea.MSG_KEY then
if msg.key == tea.KEY_ENTER then
-- Add new todo
local text = model.input:getValue()
if text ~= "" then
table.insert(model.todos, {
id = os.time(),
text = text,
done = false
})
model.input:setValue("")
end
elseif msg.key == tea.KEY_TAB then
-- Cycle through filters
local filters = {"all", "active", "completed"}
for i, f in ipairs(filters) do
if f == model.filter then
model.filter = filters[(i % #filters) + 1]
break
end
end
else
-- Update input component
model.input:update(msg)
end
end
-- Custom messages
if msg.type == MSG_TOGGLE_TODO then
for _, todo in ipairs(model.todos) do
if todo.id == msg.id then
todo.done = not todo.done
break
end
end
end
return model, nil
end
-- 4. VIEW: Render UI based on model
local function view(model)
local s = tea.style.new()
-- Title
local title = s:copy()
:foreground("cyan")
:setBold(true)
:render("📝 TODO MVC with Bubble Tea")
-- Input
local inputView = "New todo: " .. model.input:view()
-- Filter tabs
local filterView = ""
for _, f in ipairs({"all", "active", "completed"}) do
local style = s:copy()
if f == model.filter then
style:foreground("yellow"):setUnderline(true)
end
filterView = filterView .. style:render(f) .. " "
end
-- Todos
local todosView = {}
for _, todo in ipairs(model.todos) do
local show = model.filter == "all" or
(model.filter == "active" and not todo.done) or
(model.filter == "completed" and todo.done)
if show then
local checkbox = todo.done and "[✓]" or "[ ]"
local textStyle = s:copy()
if todo.done then
textStyle:setStrikethrough(true):foreground("gray")
end
table.insert(todosView, checkbox .. " " .. textStyle:render(todo.text))
end
end
-- Compose final view
return table.concat({
title,
"",
inputView,
"",
filterView,
"",
table.concat(todosView, "\n"),
"",
s:copy():foreground("gray"):render("Enter: add • Tab: filter • Ctrl+C: quit")
}, "\n")
end
-- 5. RUN: Start the application
local program = tea.newProgram(initialModel(), update, view)
:withAltScreen()
:withMouseCellMotion()
program:run()
Commands & Subscriptions
-- Commands trigger side effects
local tea = require('bubbletea')
-- Quit command
return model, tea.quit()
-- Timer commands
return model, tea.tick(1000, function()
return {type = "timer_tick"}
end)
-- Recurring timer
return model, tea.every(1000, function()
return {type = "clock_update", time = os.date()}
end)
-- Batch multiple commands
return model, tea.batch(
tea.tick(100, loadData),
tea.tick(200, updateUI),
tea.cmd(fetchFromAPI)
)
-- Sequence commands
return model, tea.sequence(
tea.cmd(validateInput),
tea.cmd(saveData),
tea.cmd(showSuccess)
)
-- Custom command example
local function httpCommand(url)
return function()
-- This runs asynchronously
local http = require('http')
local resp = http.get(url)
-- Return a message for update
return {
type = "http_response",
data = resp.body,
status = resp.status_code
}
end
end
-- Use in update
return model, tea.cmd(httpCommand("https://api.example.com/data"))
Best Practices
- Pure Functions: Keep update and view functions pure - no side effects
- Single State: All application state lives in the model
- Commands for I/O: Use commands for async operations, API calls, etc.
- Component Reuse: Create reusable components with their own update/view
- Message Design: Design clear, descriptive message types
Note: Bubble Tea uses a different architecture than the built-in TUI module. While TUI uses callbacks and imperative updates, Bubble Tea uses functional updates and immutable state. Choose based on your preference and project needs.