UI¶
All of these are UiHost APIs: they need the terminal UI and raise in headless mode.
Overlays¶
An overlay is a floating or docked window. Create a buffer, attach it to a window, and mount the window in an overlay:
local buf = smelt.buf.new()
local win = smelt.win.new(buf, { surface = "inert" })
smelt.overlay.new({
title = { { text = " perf ", bold = true } },
anchor = "screen_at",
corner = "ne",
width = 44,
height = 14,
modal = false,
draggable = true,
layout = smelt.ui.layout.leaf(win),
})
Anchor decides where the overlay lives:
| Anchor | Placement |
|---|---|
dock_bottom (default) |
Docked above the statusline |
dock_top, dock_left, dock_right |
Docked to that edge (statusline row stays reserved) |
center |
Centered on screen |
screen_at |
Absolute; pair with corner, row, col |
win |
Attached to another window; pair with target, attach, row_offset, col_offset |
Size: width and height are fixed; max_width and max_height shrink to
fit up to a cap. Don't set both fixed and max on one axis. Values are a number of
cells, a percentage string like "50%", or "fill".
Dialogs¶
For a modal with a markdown panel, an option list and a text input,
smelt.dialog.open is the higher-level API and returns the user's choice. The
bundled dialogs in
runtime/lua/smelt/dialogs/
(confirm, permissions, resume, rewind) are the reference
implementations.
Dialog height is either height = "N%" (fixed) or max_height = "N%" (shrink
to fit, capped). With neither, the dialog fits its content. Setting both is an
error.
Layouts and resizable splits¶
Compose windows with hbox, vbox and leaf nodes. hsplit(first, second,
opts) puts two subtrees side by side and vsplit stacks them, each with a
draggable divider.
local layout = smelt.ui.layout
local files = smelt.win.new(smelt.buf.new(), { surface = "list_inert" })
local changes = smelt.win.new(smelt.buf.new(), { surface = "readonly_text" })
local body = layout.hsplit(layout.leaf(files), layout.leaf(changes), {
size = 34, min_first = 26, min_second = 32,
})
smelt.dialog.new({ layout = body, height = 20, focus = files })
The same body works in smelt.overlay.new({ layout = body }). size is the
first pane's initial extent in cells, a percentage, or ratio:N/M (default
"50%"). By default a user-resized split keeps its ratio when the terminal
changes; pass resize = "cells" to keep a fixed width instead.
If you rebuild the layout from a composer function, create the split once with
layout.split("horizontal", opts) and call split:layout(first, second) inside
the composer, so the user's chosen size is kept. split:size() and
split:set_size() save and restore it.
layout.frame(body, { border = "single", padding = 1, title = "details" }) adds
chrome around any subtree.
win:resize("width", 4) and win:resize("height", -1) resize the nearest split
(or the containing overlay), and win:equalize() balances splits. The built-in
keys are Ctrl-W >, <, +, - and =.
smelt.dialog.new and smelt.dialog.open accept either a composed layout or
the simpler panels / bottom_panels recipe, not both.
The main screen¶
smelt.ui.layout.set(fn) replaces the composer for the whole screen. The
default one is require("smelt.layout").compose, so a plugin can wrap it
rather than rebuild it, for example to put a sidebar beside everything:
local layout = smelt.ui.layout
local default = require("smelt.layout").compose
local side = smelt.win.new(smelt.buf.new(), { surface = "list_inert" })
layout.set(function(state)
return layout.hbox({
{ default(state), width = "fill" },
{ layout.leaf(side, { border = "rounded", title = " notes " }), width = 30 },
})
end)
The changes sidebar in the gallery is a complete example.
Large read-only views¶
For big read-only content, give a window a document instead of filling a buffer with every line. Only the visible rows are materialized, so a million-line diff stays responsive.
smelt.spawn(function()
local snapshot, err = smelt.git.diff()
if not snapshot then smelt.notify.error(err); return end
local buf = smelt.buf.new()
local win = smelt.win.new(buf, { surface = "readonly_text", vim_enabled = true })
win:document(snapshot.diff:document())
smelt.overlay.new({
anchor = "center", width = "90%", height = "80%", modal = true,
layout = smelt.ui.layout.leaf(win),
})
win:focus()
end)
smelt.document.text(text)builds a document from plain text;smelt.diff.parse(patch)from a unified patch.smelt.git.diff()loads and indexes off the UI thread.- A document can back several windows, each with its own scroll and cursor.
win:document(nil)restores the original buffer. - Syntax highlighting arrives in the background; text and navigation work immediately.
smelt.git.index(diff, index, action)stages or unstages one file ("stage","unstage"or"toggle") without touching the worktree, and returns a fresh snapshot.
The bundled
smelt.plugins.diff
plugin shows the full pattern with a file tree, folding and refresh.
Pickers¶
smelt.picker.open(opts) asks the user to choose one item. Items are strings or
{ label, description, ansi_color, search_terms } records. With placement =
"prompt_docked" the prompt filters them using smelt.fuzzy.rank. It returns
{ index, item, action }, or nil when dismissed.
smelt.spawn(function()
local choice = smelt.picker.open({
items = { "first", "second", "third" },
placement = "prompt_docked",
})
if choice then smelt.log.info("picked " .. choice.item) end
end)
Window events and marks¶
To decorate an existing buffer, subscribe to window events and draw marks in a namespace you own:
local prompt = smelt.prompt.win()
local ns = smelt.ns("my_plugin")
prompt:on("text_changed", function()
local buf = prompt:buf()
if not buf then return end
buf:clear_ns(ns):mark(ns, 1, 0, {
end_col = 999,
hl_group = "DiagnosticHint",
priority = 200,
})
end)
Event names and mark options are type-checked in your editor.
Paint leaves and pointer events¶
For canvas-like overlays that don't need a buffer, register a paint function and
handle pointer events directly. The leaf that received press keeps receiving
drag and release even if the pointer leaves its rectangle.
local paint = smelt.paint.register(draw_fn, { name = "myplugin.paint" })
paint:on("press", function(ev) smelt.notify.info("down @ "..ev.row..","..ev.col) end)
paint:on("drag", function(ev) ... end)
paint:on("release", function(ev) ... end)
Statusline segments¶
Append segments to the statusline. The handler runs on each refresh and returns a list of segments.
local statusline = require("smelt.statusline")
statusline.add("clock", function()
return { {
text = os.date(" %H:%M "),
style = { fg = { ansi = 245 } },
priority = 2,
align_right = true,
} }
end)
statusline.remove(name) removes it. Built-in segments keep rendering alongside
yours.