Oil Lua API Reference
Build custom TUI components with Lua
Oil is Crucible’s declarative UI library for building terminal interfaces. This API allows Lua plugins to create custom views, components, and interactive elements using a composable, React-like approach.
Overview
Section titled “Overview”The Oil Lua API provides:
- Layout Primitives: Build UIs with columns, rows, and flexible spacing
- Widgets: Spinners, progress bars, input fields, popups, and more
- Styling: Colors, borders, padding, and text attributes
- Conditional Rendering: Show/hide elements based on state
- Component Composition: Build reusable UI components
- Markup Syntax: Quick prototyping with XML-like syntax
All Oil functions are available under the cru.oil namespace.
Quick Start
Section titled “Quick Start”local oil = cru.oil
-- Simple textlocal hello = oil.text("Hello, World!")
-- Styled textlocal styled = oil.text("Important!", { fg = "red", bold = true })
-- Layout with columnlocal view = oil.col({ gap = 1 }, oil.text("Title", { bold = true }), oil.text("Content goes here"), oil.spinner("Loading..."))
-- Conditional renderinglocal loading_view = oil.when(is_loading, oil.spinner("Please wait..."))API Reference
Section titled “API Reference”Layout Primitives
Section titled “Layout Primitives”text(content, opts?)
Section titled “text(content, opts?)”Display text with optional styling.
Parameters:
content(string|number): Text content to displayopts(table, optional): Styling options
Returns: Node
Example:
oil.text("Hello")oil.text("Error!", { fg = "red", bold = true })oil.text(42, { fg = "cyan" })col(opts_or_children...)
Section titled “col(opts_or_children...)”Vertical layout container (column).
Parameters:
- First argument can be options table (if it contains layout keys like
gap,padding, etc.) - Remaining arguments are child nodes
Options:
gap(number): Spacing between childrenpadding(number): Internal paddingmargin(number): External marginborder(string): Border style (“single”, “double”, “rounded”, “heavy”)justify(string): Vertical alignment (“start”, “end”, “center”, “space_between”, “space_around”, “space_evenly”)align(string): Horizontal alignment (“start”, “end”, “center”, “stretch”)
Returns: Node
Example:
-- Simple columnoil.col( oil.text("Line 1"), oil.text("Line 2"))
-- Column with optionsoil.col({ gap = 2, padding = 1, border = "rounded" }, oil.text("Title"), oil.text("Content"))row(opts_or_children...)
Section titled “row(opts_or_children...)”Horizontal layout container (row).
Parameters: Same as col()
Returns: Node
Example:
-- Simple rowoil.row( oil.text("Left"), oil.spacer(), oil.text("Right"))
-- Row with gapoil.row({ gap = 2 }, oil.text("Item 1"), oil.text("Item 2"), oil.text("Item 3"))spacer()
Section titled “spacer()”Flexible space that expands to fill available space. Useful for pushing elements apart in rows.
Returns: Node
Example:
oil.row( oil.text("Left"), oil.spacer(), -- Pushes "Right" to the far right oil.text("Right"))fragment(children...)
Section titled “fragment(children...)”Container that renders children without adding layout. Useful for grouping.
Parameters:
children...: Child nodes
Returns: Node
Example:
oil.fragment( oil.text("Item 1"), oil.text("Item 2"))Widgets
Section titled “Widgets”spinner(label?)
Section titled “spinner(label?)”Animated loading spinner.
Parameters:
label(string, optional): Text to display next to spinner
Returns: Node
Example:
oil.spinner()oil.spinner("Loading data...")input(opts)
Section titled “input(opts)”Text input field with cursor.
Parameters:
opts(table):value(string): Current input valuecursor(number): Cursor positionplaceholder(string, optional): Placeholder textfocused(boolean, optional): Whether input is focused (default: true)
Returns: Node
Example:
oil.input({ value = "Hello", cursor = 5, placeholder = "Type here...", focused = true})popup(items, selected?, max_visible?)
Section titled “popup(items, selected?, max_visible?)”Popup menu with selectable items.
Parameters:
items(table): Array of items (strings or tables withlabel,desc,kind)selected(number, optional): Index of selected item (default: 0)max_visible(number, optional): Maximum visible items (default: 10)
Returns: Node
Example:
-- Simple popupoil.popup({"Option 1", "Option 2", "Option 3"}, 0, 5)
-- Popup with descriptionsoil.popup({ {label = "Save", desc = "Save current file", kind = "action"}, {label = "Load", desc = "Load from disk", kind = "action"}, {label = "Exit", desc = "Quit application", kind = "danger"}}, 0, 10)progress(value, width?)
Section titled “progress(value, width?)”Progress bar.
Parameters:
value(number): Progress value (0.0 to 1.0)width(number, optional): Bar width in characters (default: 20)
Returns: Node
Example:
oil.progress(0.5, 30) -- 50% progress, 30 chars wideoil.progress(0.75, 40) -- 75% progress, 40 chars widebadge(label, opts?)
Section titled “badge(label, opts?)”Small labeled badge.
Parameters:
label(string): Badge textopts(table, optional): Styling options
Returns: Node
Example:
oil.badge("NEW")oil.badge("ERROR", { fg = "red", bold = true })oil.badge("v1.2.3", { fg = "cyan" })divider(char?, width?)
Section titled “divider(char?, width?)”Horizontal divider line.
Parameters:
char(string, optional): Character to repeat (default: ”─”)width(number, optional): Line width (default: 80)
Returns: Node
Example:
oil.divider() -- ─────────...oil.divider("=", 40) -- ========...oil.divider("*", 20) -- ********...Horizontal rule (same as divider() with defaults).
Returns: Node
Example:
oil.hr()bullet_list(items)
Section titled “bullet_list(items)”Bulleted list.
Parameters:
items(table): Array of strings
Returns: Node
Example:
oil.bullet_list({ "First item", "Second item", "Third item"})numbered_list(items)
Section titled “numbered_list(items)”Numbered list.
Parameters:
items(table): Array of strings
Returns: Node
Example:
oil.numbered_list({ "Step one", "Step two", "Step three"})kv(key, value)
Section titled “kv(key, value)”Key-value pair display.
Parameters:
key(string): Key labelvalue(string): Value text
Returns: Node
Example:
oil.kv("Name", "John Doe")oil.kv("Status", "Active")Conditional Rendering
Section titled “Conditional Rendering”when(condition, node)
Section titled “when(condition, node)”Render node only if condition is true.
Parameters:
condition(boolean): Condition to checknode(Node): Node to render if true
Returns: Node (or empty node if false)
Example:
oil.when(is_loading, oil.spinner("Loading..."))oil.when(has_error, oil.text("Error occurred!", { fg = "red" }))either(condition, true_node, false_node)
Section titled “either(condition, true_node, false_node)”Render one of two nodes based on condition. Also available as if_else.
Parameters:
condition(boolean): Condition to checktrue_node(Node): Node to render if truefalse_node(Node): Node to render if false
Returns: Node
Example:
oil.either(is_ready, oil.text("Ready!", { fg = "green" }), oil.spinner("Initializing..."))
-- Also available as if_elseoil.if_else(has_data, oil.text(data), oil.text("No data", { fg = "yellow" }))Iteration
Section titled “Iteration”each(items, fn)
Section titled “each(items, fn)”Map over items and render each one.
Parameters:
items(table): Array of itemsfn(function): Function that takes(item, index)and returns a Node
Returns: Node (fragment containing all rendered items)
Example:
local names = {"Alice", "Bob", "Charlie"}
oil.each(names, function(name, idx) return oil.text(idx .. ". " .. name)end)Advanced
Section titled “Advanced”markup(xml_string)
Section titled “markup(xml_string)”Parse XML-like markup into nodes. Useful for quick prototyping.
Parameters:
xml_string(string): XML-like markup
Supported tags:
<div>→ col<p>→ text<ul>→ bullet_list<li>→ list item
Attributes:
gap,padding,margin,borderfg,bg,bold,dim,italic,underline
Returns: Node
Example:
oil.markup([[ <div gap="2" padding="1" border="rounded"> <p bold="true" fg="cyan">Title</p> <p>Content goes here</p> <ul> <li>Item 1</li> <li>Item 2</li> </ul> </div>]])component(base_fn, defaults)
Section titled “component(base_fn, defaults)”Create a reusable component with default props.
Parameters:
base_fn(function): Base function (likeoil.coloroil.row)defaults(table): Default options
Returns: Function that merges user props with defaults
Example:
-- Create a Card component with default stylinglocal Card = oil.component(oil.col, { border = "rounded", padding = 1, gap = 1})
-- Use it with custom propsCard({ gap = 2 }, oil.text("Title", { bold = true }), oil.text("Content"))
-- Use it with defaults onlyCard( oil.text("Simple card"))scrollback(key, children...)
Section titled “scrollback(key, children...)”Scrollable content area. Preserves scroll position across renders.
Parameters:
key(string): Unique key for this scrollback areachildren...: Child nodes
Returns: Node
Example:
oil.scrollback("chat-messages", oil.text("Message 1"), oil.text("Message 2"), oil.text("Message 3"))decrypt(content, revealed, frame?)
Section titled “decrypt(content, revealed, frame?)”Animated decrypt/scramble effect.
Parameters:
content(string): Text to decryptrevealed(number): Number of characters revealedframe(number, optional): Animation frame (default: 0)
Returns: Node
Example:
-- Gradually reveal textoil.decrypt("Secret message", 6, 0) -- "Secret█████████"oil.decrypt("Secret message", 14, 0) -- "Secret message"Node Methods
Section titled “Node Methods”All nodes support chainable methods for styling:
:with_style(opts)
Section titled “:with_style(opts)”Apply styling to a node.
Example:
oil.text("Hello"):with_style({ fg = "red", bold = true }):with_padding(n)
Section titled “:with_padding(n)”Add padding to a node.
Example:
oil.col(oil.text("Content")):with_padding(2):with_border(type)
Section titled “:with_border(type)”Add border to a node.
Example:
oil.col(oil.text("Boxed")):with_border("rounded"):with_margin(n)
Section titled “:with_margin(n)”Add margin to a node.
Example:
oil.text("Spaced"):with_margin(1):gap(n)
Section titled “:gap(n)”Set gap between children.
Example:
oil.col(child1, child2):gap(2):justify(mode)
Section titled “:justify(mode)”Set justify mode.
Example:
oil.col(child1, child2):justify("center"):align(mode)
Section titled “:align(mode)”Set align mode.
Example:
oil.row(child1, child2):align("center")Styling Options
Section titled “Styling Options”Colors
Section titled “Colors”Named colors:
"red","green","blue","cyan","yellow","magenta","white","black"
Hex colors:
"#ff0000","#00ff00","#0000ff", etc.
Usage:
{ fg = "red" } -- Foreground color{ bg = "blue" } -- Background color{ fg = "#ff5500" } -- Hex colorText Attributes
Section titled “Text Attributes”{ bold = true } -- Bold text{ dim = true } -- Dimmed text{ italic = true } -- Italic text{ underline = true } -- Underlined textLayout Options
Section titled “Layout Options”{ gap = 2 } -- Spacing between children{ padding = 1 } -- Internal padding{ margin = 1 } -- External margin{ border = "rounded" } -- Border style{ justify = "center" } -- Justify content{ align = "center" } -- Align itemsBorder styles:
"single"- Single line border"double"- Double line border"rounded"- Rounded corners"heavy"- Heavy/thick border
Justify modes:
"start"- Align to start"end"- Align to end"center"- Center items"space_between"- Space between items"space_around"- Space around items"space_evenly"- Even spacing
Align modes:
"start"- Align to start"end"- Align to end"center"- Center items"stretch"- Stretch to fill
Common Patterns
Section titled “Common Patterns”Message Block
Section titled “Message Block”local function message_block(role, content) local role_colors = { user = "green", assistant = "blue", system = "yellow" }
local color = role_colors[role] or "white"
return oil.col({ gap = 0 }, oil.text(""), oil.text(string.upper(role), { fg = color, bold = true }), oil.col({ padding = 1, border = "single" }, oil.text(content) ), oil.text("") )end
-- Usagemessage_block("user", "Hello, how are you?")message_block("assistant", "I'm doing well, thank you!")Status Bar
Section titled “Status Bar”local function status_bar(mode, model, context_pct) local mode_colors = { NORMAL = "green", PLAN = "blue", AUTO = "yellow" }
return oil.row({ gap = 2 }, oil.text(" " .. mode .. " ", { bg = mode_colors[mode], fg = "black", bold = true }), oil.text(model, { fg = "cyan" }), oil.spacer(), oil.text(string.format("%d%% ctx", context_pct), { fg = "yellow" }) )end
-- Usagestatus_bar("NORMAL", "gpt-4o", 45)Tool Call Display
Section titled “Tool Call Display”local function tool_call_display(name, status, result) local status_colors = { pending = "yellow", running = "cyan", complete = "green", error = "red" }
return oil.col({ border = "rounded", padding = 1, gap = 1 }, oil.row({ gap = 2 }, oil.text("🔧", { bold = true }), oil.text(name, { bold = true }), oil.badge(status, { fg = status_colors[status] }) ), oil.when(status == "running", oil.spinner("Processing...")), oil.when(result ~= nil, oil.col({ gap = 0 }, oil.divider("─", 40), oil.text(result) ) ) )end
-- Usagetool_call_display("search", "running", nil)tool_call_display("search", "complete", "Found 5 results")Loading State
Section titled “Loading State”local function loading_view(is_loading, error_msg, data) return oil.col({ gap = 1 }, oil.text("Data Viewer", { bold = true }), oil.hr(),
-- Show spinner while loading oil.when(is_loading, oil.col({ gap = 1 }, oil.spinner("Loading..."), oil.text("Please wait", { fg = "yellow" }) ) ),
-- Show error if present oil.when(error_msg ~= nil, oil.col({ border = "heavy", padding = 1 }, oil.text("Error", { fg = "red", bold = true }), oil.text(error_msg, { fg = "red" }) ) ),
-- Show data when ready oil.when(not is_loading and error_msg == nil and data ~= nil, oil.text(data) ) )endProgress Indicator
Section titled “Progress Indicator”local function progress_indicator(label, current, total) local percentage = math.floor((current / total) * 100)
return oil.col({ gap = 0 }, oil.row({ gap = 2 }, oil.text(label), oil.text(string.format("%d/%d", current, total), { fg = "cyan" }) ), oil.progress(current / total, 40), oil.text(string.format("%d%%", percentage), { fg = "green" }) )end
-- Usageprogress_indicator("Processing files", 7, 10)Card Component
Section titled “Card Component”local function card(title, content, opts) opts = opts or {} local border_style = opts.border or "rounded" local padding = opts.padding or 1
return oil.col({ border = border_style, padding = padding, gap = 1 }, oil.text(title, { bold = true, fg = "cyan" }), content )end
-- Usagecard("System Info", oil.col({ gap = 0 }, oil.kv("Model", "gpt-4o"), oil.kv("Status", "Ready"), oil.kv("Uptime", "2h 34m")))Best Practices
Section titled “Best Practices”Component Composition
Section titled “Component Composition”Build complex UIs from simple, reusable components:
-- Define reusable componentslocal function InfoRow(label, value) return oil.row({ gap = 2 }, oil.text(label .. ":", { fg = "cyan" }), oil.text(value) )end
local function Card(title, children) return oil.col({ border = "rounded", padding = 1, gap = 1 }, oil.text(title, { bold = true }), children )end
-- Compose themlocal view = Card("User Info", oil.col({ gap = 0 }, InfoRow("Name", "Alice"), InfoRow("Role", "Admin"), InfoRow("Status", "Active") ))Styling Conventions
Section titled “Styling Conventions”Use semantic colors and consistent spacing:
-- Good: Semantic colorslocal colors = { success = "green", error = "red", warning = "yellow", info = "blue"}
-- Good: Consistent spacinglocal SPACING = { tight = 0, normal = 1, loose = 2}
oil.col({ gap = SPACING.normal }, oil.text("Title"), oil.text("Content"))Conditional Rendering
Section titled “Conditional Rendering”Use when() and either() for clean conditional logic:
-- Good: Clear conditional renderingoil.when(is_loading, oil.spinner("Loading..."))
oil.either(has_data, oil.text(data), oil.text("No data available", { fg = "yellow" }))
-- Avoid: Lua conditionals that return nil-- Bad: if is_loading then return oil.spinner() endPerformance Tips
Section titled “Performance Tips”- Avoid deep nesting: Keep component trees shallow
- Use keys for lists: Helps with efficient re-rendering
- Memoize expensive computations: Cache results when possible
- Use
fragment()sparingly: Only when you need grouping without layout
Examples
Section titled “Examples”See examples/plugins/custom-ui.lua for comprehensive examples including:
- Chat interface with messages and tool calls
- Dashboard with cards and statistics
- Progress tracking with multiple indicators
- Conditional rendering patterns
- Markup syntax examples
- Component composition techniques