Plugin gallery¶
Small plugins to copy into ~/.config/smelt/plugins/. Each one is recorded
running in smelt, with a live model doing the work.
Crate lookup¶
A new tool for the agent: the latest version, downloads and description of a crate, straight from crates.io.
-- ~/.config/smelt/plugins/crates.lua
smelt.tools.register({
name = "crate_info",
description = "Latest version and downloads of a crate on crates.io.",
parameters = {
type = "object",
properties = { name = { type = "string" } },
required = { "name" },
},
execute = function(args)
local url = "https://crates.io/api/v1/crates/" .. args.name
local headers = { ["user-agent"] = "smelt" }
local res, err = smelt.http.get(url, { headers = headers })
if not res or res.status ~= 200 then
return { content = err or "crate not found", is_error = true }
end
local c = smelt.json.decode(res.body).crate
return ("%s %s, %d downloads: %s"):format(
c.name, c.max_stable_version, c.downloads, c.description)
end,
})
Statusline¶
A lualine-style rebuild of the whole statusline: the mode, the working directory, the branch and counts of added, modified and removed files, with Nerd Font icons.
-- ~/.config/smelt/plugins/lualine.lua
local A, B, C = { 232, 100, 26 }, { 58, 47, 39 }, { 30, 26, 21 }
local FG, DARK = { 232, 223, 214 }, { 23, 20, 15 }
local DIFF = {
{ "added", " ", { 143, 191, 106 } },
{ "modified", " ", { 230, 180, 80 } },
{ "removed", " ", { 239, 107, 91 } },
}
local branch, counts = "", {}
-- Read the branch and count changed files after every tool the agent runs.
local function refresh()
smelt.spawn(function()
local snap = smelt.git.diff()
branch, counts = snap and snap.branch or "", { added = 0, modified = 0, removed = 0 }
for _, f in ipairs(snap and snap.diff:files() or {}) do
local kind = (f.status == "?" or f.status == "A") and "added"
or f.status == "D" and "removed" or "modified"
counts[kind] = counts[kind] + 1
end
smelt.statusline.invalidate()
end)
end
smelt.events.on("tool_end", refresh)
refresh()
local function text(items, s, bg, fg, bold)
table.insert(items, { text = s, style = { bg = bg, fg = fg, bold = bold } })
end
smelt.statusline.remove("core")
smelt.statusline.add("line", function()
local items = {}
local cwd = smelt.path.basename(smelt.os.cwd())
text(items, " " .. (smelt.signal.get("agent_mode") or "") .. " ", A, DARK, true)
text(items, "", B, A)
text(items, " " .. cwd .. " ", B, FG)
text(items, "", C, B)
text(items, " " .. branch .. " ", C, FG)
for _, d in ipairs(DIFF) do
local n = counts[d[1]] or 0
if n > 0 then text(items, d[2] .. n .. " ", C, d[3]) end
end
return items
end)
Changes sidebar¶
A file tree of the working copy beside the usual layout, with each file's
added and removed lines, refreshed after every tool the agent runs. It wraps
the default layout from require("smelt.layout").compose instead of rebuilding
it.
-- ~/.config/smelt/plugins/changes.lua
local layout = smelt.ui.layout
local default = require("smelt.layout").compose
local files = smelt.win.new(smelt.buf.new(), { surface = "list_inert", wrap = false, pad_left = 1 })
-- A solid panel in the scroll pill's color.
files:row_highlights({ { start = 0, ["end"] = 1000, hl_group = "SmeltScrollPillBg" } })
-- Re-read the working tree after every tool the agent runs.
local function refresh()
smelt.spawn(function()
local snap = smelt.git.diff()
files:document(snap and snap.diff:tree():document())
end)
end
smelt.events.on("tool_end", refresh)
refresh()
layout.set(function(state)
return layout.hbox({
{ default(state), width = "fill" },
{ layout.leaf(files), width = 34 },
})
end)
Snake¶
Built in: /snake (or F11) opens a snake you steer with hjkl or the arrows
while the agent works. The title keeps the score and the session's best.
snake.lua
-- /snake (or F11) - snake in a floating window. hjkl or the arrows steer,
-- space starts over, q, Esc or Ctrl-C closes.
-- The title shows the score and the best one this session.
--
-- Two board rows share one terminal row: `▀` paints the top half in its
-- foreground and the bottom half in its background.
local W, H = 32, 32
local TICK_MS = 100
local FOOD = { rgb = { 240, 80, 60 } }
local STEP = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
local OPPOSITE = { up = "down", down = "up", left = "right", right = "left" }
local session = smelt.state.get("smelt.snake")
local game, ui, show
local function key(x, y) return y * W + x end
-- A random free cell, or nil once the snake fills the board.
local function place_food(snake)
local taken, free = {}, {}
for _, p in ipairs(snake) do taken[key(p[1], p[2])] = true end
for i = 0, W * H - 1 do
if not taken[i] then free[#free + 1] = i end
end
if #free == 0 then return nil end
local i = free[math.random(#free)]
return { i % W, i // W }
end
local function new_game()
local y = H // 2
local snake = { { 7, y }, { 6, y }, { 5, y } } -- head first
game = { snake = snake, dir = "right", next_dir = "right", food = place_food(snake), score = 0, dead = false }
end
local function tick()
if game.dead then return end
game.dir = game.next_dir
local head, d = game.snake[1], STEP[game.dir]
local x, y = head[1] + d[1], head[2] + d[2]
local hit = x < 0 or y < 0 or x >= W or y >= H
-- The tail moves out of the way this tick, so it is not an obstacle.
for i = 1, #game.snake - 1 do
local p = game.snake[i]
if p[1] == x and p[2] == y then hit = true end
end
if hit then
game.dead = true
return show()
end
table.insert(game.snake, 1, { x, y })
if game.food and x == game.food[1] and y == game.food[2] then
game.score = game.score + 1
if game.score > (session.best or 0) then session.best = game.score end
game.food = place_food(game.snake)
show()
else
table.remove(game.snake)
end
end
-- The head is bright and the body fades toward the tail.
local function shade(i)
local f = 1 - (i - 1) / math.max(#game.snake, 8) * 0.7
return { rgb = { math.floor(120 * f), math.floor(220 * f), math.floor(90 * f) } }
end
local function paint(slice)
if not game then return end
local bg = smelt.theme.get("SmeltScrollPillBg").bg
local cells = {}
for i, p in ipairs(game.snake) do cells[key(p[1], p[2])] = shade(i) end
if game.food then cells[key(game.food[1], game.food[2])] = FOOD end
local cols, rows = math.min(W, slice:width()), math.min(H // 2, slice:height())
slice:fill_rect(0, 0, cols, rows, " ", { bg = bg })
for row = 0, rows - 1 do
for x = 0, cols - 1 do
local top, bottom = cells[key(x, row * 2)], cells[key(x, row * 2 + 1)]
if top or bottom then slice:set(row, x, "▀", { fg = top or bg, bg = bottom or bg }) end
end
end
end
local function turn(dir)
return function()
if not game.dead and game.dir ~= OPPOSITE[dir] then game.next_dir = dir end
end
end
local function restart()
new_game()
show()
end
local function close()
if not ui then return end
ui.timer:remove()
ui.paint:remove()
ui.win:close()
ui, game = nil, nil
end
-- Re-opening the named overlay refreshes its title with the score.
show = function()
local title = (" snake %d · best %d "):format(game.score, session.best or 0)
if game.dead then title = title .. "· game over " end
local layout = smelt.ui.layout
smelt.overlay.new({
name = "smelt.snake",
title = title,
border = { all = "Comment" },
anchor = "screen_at", corner = "ne", row = 1, col = 0,
width = W + 2, height = H // 2 + 2,
-- The empty window holds focus so the keymaps fire; the board is a paint.
layout = layout.vbox({
{ layout.leaf(ui.win), height = 0 },
{ layout.leaf(ui.paint, { measure = { W, H // 2 } }), height = "fit" },
}),
modal = false,
draggable = true,
resizable = false,
keymaps = {
{ key = "h", on_press = turn("left") },
{ key = "j", on_press = turn("down") },
{ key = "k", on_press = turn("up") },
{ key = "l", on_press = turn("right") },
{ key = "<Left>", on_press = turn("left") },
{ key = "<Down>", on_press = turn("down") },
{ key = "<Up>", on_press = turn("up") },
{ key = "<Right>", on_press = turn("right") },
{ key = "<Space>", on_press = restart },
{ key = "q", on_press = close },
{ key = "<Esc>", on_press = close },
{ key = "<C-c>", on_press = close },
},
})
end
local function open()
math.randomseed(os.time())
new_game()
ui = { win = smelt.win.new(smelt.buf.new(), { surface = "readonly_text", hide_cursor = true }) }
ui.paint = smelt.paint.register(paint, { name = "smelt.snake.paint" })
show()
ui.win:focus()
ui.timer = smelt.timer.every(TICK_MS, tick)
end
local function toggle()
if ui then close() else open() end
end
smelt.cmd.register("snake", toggle, { desc = "play snake" })
smelt.keymap.set("", "<F11>", toggle)