Scripted UI
Scripts can display interactive UI elements — popups for selection and panels for structured choices — that work in both TUI and Web interfaces.
Concepts
Section titled “Concepts”PopupRequest vs InteractivePanel
Section titled “PopupRequest vs InteractivePanel”Two primitives are available:
| Type | Use Case | Features |
|---|---|---|
| PopupRequest | Quick selections with optional free-text | Simple entries with labels and descriptions |
| InteractivePanel | Structured choices with hints | Filtering, multi-select, custom data |
Use PopupRequest for simple “pick one” scenarios. Use InteractivePanel for richer interactions like confirmation dialogs, multi-select, or searchable lists.
Lua API
Section titled “Lua API”PopupRequest
Section titled “PopupRequest”local popup = require("cru.popup") -- or require("crucible.popup")
-- Create entrieslocal entries = { popup.entry("Daily Note", "Today's journal"), popup.entry("Todo List"),}
-- Basic popuplocal request = popup.request("Select a note", entries)
-- Allow free-text inputlocal search = popup.request_with_other("Search or select", entries)InteractivePanel
Section titled “InteractivePanel”local ui = require("cru.ui") -- or require("crucible.ui")
-- Create panel itemslocal items = { ui.panel_item("PostgreSQL", "Full-featured RDBMS"), ui.panel_item("SQLite", "Embedded, single-file"),}
-- Basic panellocal db_panel = ui.panel("Select database", items)
-- Convenience functionslocal confirmed = ui.confirm("Delete this file?")local choice = ui.select("Pick one", {"A", "B", "C"})local choices = ui.multi_select("Pick many", {"X", "Y"})Panel Hints
Section titled “Panel Hints”Control panel behavior with hints:
local ui = require("cru.ui") -- or require("crucible.ui")
-- Create hintslocal hints = ui.panel_hints() :filterable() -- Enable search/filter :multi_select() -- Allow multiple selections :allow_other() -- Allow free-text input
-- Panel with hintslocal panel = ui.panel_with_hints("Choose", items, hints)Handling Results
Section titled “Handling Results”PopupResponse
Section titled “PopupResponse”When user selects from a popup:
if response.selected_index then handle_selection(response.selected_index, response.selected_entry)elseif response.other then handle_text(response.other)else handle_dismiss()endPanelResult
Section titled “PanelResult”When user interacts with a panel:
if result.cancelled then handle_cancel()elseif result.other then handle_text(result.other)else for _, idx in ipairs(result.selected) do handle_selection(idx) endendExample: Database Selector Tool
Section titled “Example: Database Selector Tool”A complete example showing a panel-based tool:
-- database_selector.lualocal ui = require("cru.ui") -- or require("crucible.ui")
--- Select a database type for your project-- @tool name="choose_database" description="Select a database type for your project"function choose_database(args) local items = { ui.panel_item("PostgreSQL", "Full-featured, ACID-compliant RDBMS"), ui.panel_item("SQLite", "Embedded, zero-configuration"), ui.panel_item("MongoDB", "Document database with flexible schema"), }
local panel = ui.panel("Select database", items)
-- Display panel and get result local result = cru.show_panel(panel)
if result.cancelled then return { message = "Cancelled" } else local selected = items[result.selected[1]].label return { message = "You chose: " .. selected } endendcru.oil: Building Views (Obvious Interface Language)
Section titled “cru.oil: Building Views (Obvious Interface Language)”The cru.oil module provides a functional, React-like API for building TUI nodes. Components are functions that return node trees, and composition happens via function calls.
Basic Usage (Lua)
Section titled “Basic Usage (Lua)”local oil = cru.oil
-- Text with stylinglocal heading = oil.text("Tasks", { bold = true, fg = "blue" })
-- Layout containers (col = vertical, row = horizontal)local view = oil.col({ gap = 1, padding = 1, border = "rounded" }, oil.text("Header", { bold = true }), oil.row( oil.badge("OK", { fg = "green" }), oil.spacer(), oil.text("Status") ), oil.divider())
-- Listslocal bullets = oil.bullet_list({ "First item", "Second item" })
-- Progress indicatorslocal progress = oil.progress(0.75)local loading = oil.spinner("Loading...")Control Flow
Section titled “Control Flow”Use control flow functions for conditional and iterative rendering:
-- Conditional renderingoil.when(is_loading, oil.spinner("Loading..."))
-- Conditional with else branchoil.if_else(is_online, oil.text("Online", { fg = "green" }), oil.text("Offline", { fg = "red" }))
-- Iterate over itemsoil.each(items, function(item) return oil.text(item.name)end)Reusable Components
Section titled “Reusable Components”Create reusable components with the component factory:
-- Create a Card component with default propslocal Card = oil.component(oil.col, { padding = 2, border = "rounded" })
-- Use with additional props (merged with defaults)local view = Card({ gap = 1 }, oil.text("Card Title", { bold = true }), oil.text("Card body content"))Or define components as regular functions:
local function StatusBar(props) return oil.row({ justify = "space_between" }, oil.text(props.title, { bold = true }), oil.badge(props.status, { fg = props.color }) )end
-- UsageStatusBar({ title = "Dashboard", status = "OK", color = "green" })Available Components
Section titled “Available Components”| Function | Description |
|---|---|
oil.text(content, style?) | Styled text |
oil.col(props?, children...) | Vertical flex container |
oil.row(props?, children...) | Horizontal flex container |
oil.fragment(children...) | Invisible wrapper |
oil.spacer() | Flexible space filler |
oil.divider(char?, width?) | Horizontal line |
oil.hr() | Full-width horizontal rule |
oil.badge(label, style?) | Colored badge |
oil.spinner(label?) | Loading spinner |
oil.progress(value, width?) | Progress bar (value 0-1) |
oil.input(opts) | Text input field |
oil.popup(items, selected?, max?) | Popup menu |
oil.bullet_list(items) | Bulleted list |
oil.numbered_list(items) | Numbered list |
oil.kv(key, value) | Key-value pair row |
oil.scrollback(key, children...) | Scrollable container |
Control Flow Functions
Section titled “Control Flow Functions”| Function | Description |
|---|---|
oil.when(cond, node) | Show node if condition is truthy |
oil.if_else(cond, t, f) | Show t if true, f if false (alias: either) |
oil.each(items, fn) | Map items to nodes |
oil.component(base, defaults) | Create component with default props |
Style Options
Section titled “Style Options”local style = { fg = "red", -- Foreground: red, green, blue, yellow, etc. or "#hex" bg = "blue", -- Background color bold = true, -- Bold text dim = true, -- Dimmed text italic = true, -- Italic text underline = true, -- Underlined text}Container Options
Section titled “Container Options”local opts = { gap = 1, -- Space between children padding = 2, -- Inner padding (all sides) margin = 1, -- Outer margin (all sides) border = "rounded", -- single, double, rounded, heavy justify = "center", -- start, end, center, space_between, space_around, space_evenly align = "stretch", -- start, end, center, stretch}Markup Syntax
Section titled “Markup Syntax”For template-driven UI, parse XML-like markup:
local view = oil.markup([[ <col border="rounded" gap="1"> <text bold="true">Header</text> <divider /> <text>Content here</text> </col>]])Fennel Support
Section titled “Fennel Support”The lib/oil.fnl module provides idiomatic Fennel wrappers:
;; Load the oil module(local oil (require :oil))
;; Define a reusable component(oil.defui status-bar [{: title : status : color}] (oil.row {:justify :space-between} (oil.text title {:bold true}) (oil.badge status {:fg color})))
;; Build a view(oil.col {:gap 1 :padding 1 :border :rounded} (status-bar {:title "Dashboard" :status "OK" :color :green}) (oil.text "Welcome back!") (oil.when loading (oil.spinner "Loading...")) (oil.map-each items (fn [item] (oil.text item.name))))Fennel-Specific Features
Section titled “Fennel-Specific Features”| Macro/Function | Description |
|---|---|
(oil.defui name [props] body) | Define a component function |
(oil.cond-ui ...) | Multi-branch conditional |
(oil.when cond node) | Conditional rendering |
(oil.map-each items fn) | Iterate items (named to avoid shadowing) |
Multi-Branch Conditional
Section titled “Multi-Branch Conditional”(oil.cond-ui loading (oil.spinner "Loading...") error (oil.text error {:fg :red}) :else (oil.text "Ready" {:fg :green}))See Also
Section titled “See Also”- Language Basics - Lua reference
- Configuration - Lua configuration
- Component Architecture - UI internals
- Creating Plugins - Plugin development
- Plugin Manifest - Plugin manifest format