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.

Import: The TUI module is pre-loaded as 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
Color Markup: Use [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()

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()
TUI Best Practices:
  • 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.

Import: 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)
Returns: Response table with 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)
HTTP Best Practices:
  • 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.

Import: 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
Message Types: 1=Text, 2=Binary, 8=Close, 9=Ping, 10=Pong
-- 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)
WebSocket Best Practices:
  • 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.

🎯 Clean Model-View-Update architecture
🧩 Rich component library
🎨 Powerful styling system
⌨️ Complete keyboard and mouse support

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.