try batch search hooking

This commit is contained in:
Jo Garnier committed 2026-06-15 21:18:11 +02:00
1 parent 17e7956cc9
commit 54b30b62fe
7 files changed
+212 -218

No files matched your search

+19 -10
View File
@@ -52,18 +52,27 @@ yay.opt.debug = false -- Enable debug logging and local init.lua lookup convenie
yay.opt.rpc = true -- Use AUR RPC for dependency/query operations.
yay.opt.double_confirm = true -- Ask for confirmation before and after builds during upgrades.
-- Search-display hooks (yay.on)
-- Search-display hook (yay.on)
--
-- Override how AUR/repo search-result lines are rendered. Return a string for
-- the full line, or nil to fall back to yay's default formatter. See
-- Override how the whole search result list is rendered. Return a string for
-- the entire menu output, or nil to fall back to yay's default formatter. See
-- doc/lua.md for the full field list and contract.
--
-- yay.on("search_aur", function(pkg)
-- local prefix = pkg.index and (pkg.index .. " ") or ""
-- return string.format("%saur/%s %s (+%d %.2f)", prefix, pkg.name, pkg.version, pkg.votes, pkg.popularity)
-- end)
-- AUR-only numeric fields (votes, popularity, first_submitted, last_modified)
-- are -1 for repo packages; use that as a guard for AUR-specific formatting.
--
-- yay.on("search_repo", function(pkg)
-- local prefix = pkg.index and (pkg.index .. " ") or ""
-- return string.format("%s%s/%s %s", prefix, pkg.source, pkg.name, pkg.version)
-- yay.on("render_search", function(results)
-- local out = {}
-- for _, pkg in ipairs(results) do
-- local prefix = pkg.index and (pkg.index .. " ") or ""
-- local line = string.format("%s%s/%s %s", prefix, pkg.source, pkg.name, pkg.version)
-- if pkg.votes >= 0 then -- AUR package: append vote count and popularity
-- line = line .. string.format(" (+%d %.2f)", pkg.votes, pkg.popularity)
-- end
-- if pkg.installed then
-- line = line .. " [installed]"
-- end
-- out[#out + 1] = line .. "\n " .. pkg.description
-- end
-- return table.concat(out, "\n")
-- end)
+31 -43
View File
@@ -50,77 +50,65 @@ lives at [`doc/init.lua`](init.lua).
## Hooks (`yay.on`)
Search-result lines can be rendered by your own Lua function. Register a
callback with `yay.on(event, fn)`. `event` is a string naming one of the
supported events; passing any other event name aborts startup (fail-fast,
like an unknown `yay.opt` key).
The entire search result list can be rendered by your own Lua function. Register
a callback with `yay.on(event, fn)`. `event` must be a supported event name;
passing any other string aborts startup (fail-fast, like an unknown `yay.opt` key).
```lua
yay.on("search_aur", function(pkg)
local prefix = pkg.index and (pkg.index .. " ") or ""
return string.format("%saur/%s %s (+%d %.2f)",
prefix, pkg.name, pkg.version, pkg.votes, pkg.popularity)
yay.on("render_search", function(results)
local out = {}
for _, pkg in ipairs(results) do
local prefix = pkg.index and (pkg.index .. " ") or ""
out[#out + 1] = string.format("%s%s/%s %s", prefix, pkg.source, pkg.name, pkg.version)
end
return table.concat(out, "\n")
end)
```
### Events
- `"search_aur"` — fired once per AUR result.
- `"search_repo"` — fired once per repo (sync) result.
- `"render_search"` — fired **once** with the whole result list; the callback
returns the entire menu output as a single string.
Hooks fire only in the detailed search view (`-Ss`) and the interactive number
menu (`yay <term>`). Name-only output (`-Sq`) and package info (`-Si`) are not
hooked, so machine-readable output stays stable.
hooked, so machine-readable output stays stable. When there are no results the
hook is not called.
Each event holds a single callback; calling `yay.on` again for the same event
replaces the previous one (last-wins).
There is a single slot per callback; calling `yay.on("render_search", …)` again
replaces the previous registration (last-wins).
### Return contract
- Return a **string**: it is printed verbatim as the entire line — yay prepends
nothing, including the number-menu index. Reproduce the index yourself from
`pkg.index` if you want it.
- Return a **string**: it is printed verbatim as the entire menu — yay prepends
nothing, including number-menu indices. Reproduce each index from `pkg.index`
if you want them.
- Return `nil`, nothing, or a non-string value: yay falls back to its built-in
formatter for that row (the documented "defer to default" path).
per-line formatter for the whole menu (the "defer to default" path).
- Raise a Lua `error(...)`: the search aborts and yay exits non-zero with
`init.lua <event> hook: ...`.
`init.lua render_search hook: ...`.
No color/format helpers are exposed. Returned text is printed as-is; embed your
own ANSI escapes if you want styling, and honor `--color=never`/`NO_COLOR`
yourself.
### `search_aur` fields
### Result entry fields
Every element of `results` uses the same schema regardless of source (AUR or repo).
AUR-only numeric fields are `-1` for repo packages.
| key | type | notes |
| --- | --- | --- |
| `source` | string | always `"aur"` |
| `source` | string | `"aur"` or repo/DB name (e.g. `"extra"`) |
| `name` | string | |
| `version` | string | |
| `description` | string | |
| `votes` | number | |
| `popularity` | number | |
| `out_of_date` | number | unix timestamp; `0` when not flagged |
| `last_modified` | number | unix timestamp of last AUR package modification |
| `package_base` | string | |
| `votes` | number | AUR vote count; **`-1` for repo packages** |
| `popularity` | number | AUR popularity; **`-1` for repo packages** |
| `first_submitted` | number | unix timestamp; **`-1` for repo packages** |
| `last_modified` | number | unix timestamp; **`-1` for repo packages** |
| `provides` | array of strings | possibly empty |
| `maintainer` | string | **`nil` when orphaned** |
| `installed` | boolean | |
| `installed_version` | string | **`nil` when not installed** |
| `count` | number | total number of result rows |
| `index` | number | 1-based selection number; **`nil` outside the number menu** |
### `search_repo` fields
| key | type | notes |
| --- | --- | --- |
| `source` | string | repo/DB name, e.g. `"extra"` |
| `name` | string | |
| `version` | string | |
| `description` | string | |
| `size` | number | download size in bytes |
| `installed_size` | number | installed size in bytes |
| `groups` | array of strings | possibly empty |
| `installed` | boolean | |
| `installed_version` | string | **`nil` when not installed** |
| `count` | number | total number of result rows |
| `index` | number | 1-based selection number; **`nil` outside the number menu** |
| `index` | number | 1-based selection number; **`nil` outside the number menu** |
+72 -41
View File
@@ -30,16 +30,10 @@ const (
Minimal
)
// Search-display event names passed to a SearchRenderer.
const (
SearchEventAUR = "search_aur"
SearchEventRepo = "search_repo"
)
// SearchRenderer overrides how a single search-result line is rendered. handled
// is false when the renderer defers to the built-in formatter for that row.
// SearchRenderer overrides how the whole search result list is rendered.
// handled is false when the renderer defers to the built-in per-line formatter.
type SearchRenderer interface {
Render(event string, pkg map[string]any) (line string, handled bool, err error)
RenderSearch(results []map[string]any) (output string, handled bool, err error)
}
type Builder interface {
@@ -95,6 +89,7 @@ func NewSourceQueryBuilder(
type abstractResult struct {
source string
name string
version string
description string
packageBase string
votes int
@@ -233,6 +228,7 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
sortableResults.results = append(sortableResults.results, abstractResult{
source: repoResults[i].DB().Name(),
name: repoResults[i].Name(),
version: repoResults[i].Version(),
description: repoResults[i].Description(),
packageBase: repoResults[i].Base(),
votes: -1,
@@ -265,6 +261,7 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
sortableResults.results = append(sortableResults.results, abstractResult{
source: dbName,
name: aurResults[i].Name,
version: aurResults[i].Version,
description: aurResults[i].Description,
packageBase: aurResults[i].PackageBase,
votes: aurResults[i].NumVotes,
@@ -289,6 +286,17 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
}
func (s *SourceQueryBuilder) Results(dbExecutor db.Executor, verboseSearch SearchVerbosity) error {
if verboseSearch != Minimal && s.renderer != nil && len(s.results) > 0 {
handled, err := s.renderViaHook(dbExecutor, verboseSearch)
if err != nil {
return err
}
if handled {
return nil
}
}
for i := range s.results {
if verboseSearch == Minimal {
s.logger.Println(s.results[i].name)
@@ -297,39 +305,7 @@ func (s *SourceQueryBuilder) Results(dbExecutor db.Executor, verboseSearch Searc
pkg := s.queryMap[s.results[i].source][s.results[i].name]
if s.renderer != nil {
var (
event string
m map[string]any
)
switch pPkg := pkg.(type) {
case aur.Pkg:
event, m = SearchEventAUR, aurPkgToMap(&pPkg, dbExecutor)
case alpm.Package:
event, m = SearchEventRepo, syncPkgToMap(pPkg, dbExecutor)
}
if m != nil {
m["count"] = len(s.results)
if verboseSearch == NumberMenu {
m["index"] = s.menuIndex(i)
}
line, handled, err := s.renderer.Render(event, m)
if err != nil {
return err
}
if handled {
s.logger.Println(line)
continue
}
}
}
var toPrint string
if verboseSearch == NumberMenu {
toPrint += text.Magenta(strconv.Itoa(s.menuIndex(i))) + " "
}
@@ -347,6 +323,61 @@ func (s *SourceQueryBuilder) Results(dbExecutor db.Executor, verboseSearch Searc
return nil
}
// renderViaHook builds one uniform map per result (display order, with index in
// number-menu mode) and hands the whole list to the renderer. Returns
// handled=false (without printing) when the renderer defers, so the caller falls
// back to the built-in formatter.
func (s *SourceQueryBuilder) renderViaHook(dbExecutor db.Executor, verboseSearch SearchVerbosity) (bool, error) {
pkgMaps := make([]map[string]any, len(s.results))
for i := range s.results {
m := resultToMap(s.results[i], dbExecutor)
if verboseSearch == NumberMenu {
m["index"] = s.menuIndex(i)
}
pkgMaps[i] = m
}
output, handled, err := s.renderer.RenderSearch(pkgMaps)
if err != nil {
return false, err
}
if !handled {
return false, nil
}
s.logger.Println(output)
return true, nil
}
// resultToMap projects a normalized search result into the Lua-facing table.
// AUR-only numeric fields (votes/popularity/first_submitted/last_modified) are
// -1 for repo packages, matching the sentinels stored in abstractResult.
func resultToMap(r abstractResult, dbExecutor db.Executor) map[string]any {
m := map[string]any{
"source": r.source,
"name": r.name,
"version": r.version,
"description": r.description,
"package_base": r.packageBase,
"votes": r.votes,
"popularity": r.popularity,
"first_submitted": r.firstSubmitted,
"last_modified": r.lastModified,
"provides": r.provides,
}
if localPkg := dbExecutor.LocalPackage(r.name); localPkg != nil {
m["installed"], m["installed_version"] = true, localPkg.Version()
} else {
m["installed"] = false
}
return m
}
// menuIndex returns the 1-based selection number shown in the number menu for
// result i, honoring bottom-up ordering.
func (s *SourceQueryBuilder) menuIndex(i int) int {
+37 -33
View File
@@ -748,19 +748,16 @@ func newYayQueryBuilderMocks() (*mock.DBExecutor, *mockaur.MockAUR) {
return mockDB, mockAUR
}
type recordedRender struct {
event string
pkg map[string]any
}
type recordingRenderer struct {
calls []recordedRender
calls int
results []map[string]any
deferDefault bool
err error
}
func (r *recordingRenderer) Render(event string, pkg map[string]any) (string, bool, error) {
r.calls = append(r.calls, recordedRender{event: event, pkg: pkg})
func (r *recordingRenderer) RenderSearch(results []map[string]any) (string, bool, error) {
r.calls++
r.results = results
if r.err != nil {
return "", false, r.err
@@ -770,13 +767,22 @@ func (r *recordingRenderer) Render(event string, pkg map[string]any) (string, bo
return "", false, nil
}
return "HOOK " + event + " " + pkg["name"].(string), true, nil
var b strings.Builder
for i, pkg := range results {
if i > 0 {
b.WriteByte('\n')
}
b.WriteString("HOOK " + pkg["source"].(string) + " " + pkg["name"].(string))
}
return b.String(), true, nil
}
func (r *recordingRenderer) byName() map[string]recordedRender {
out := make(map[string]recordedRender, len(r.calls))
for _, c := range r.calls {
out[c.pkg["name"].(string)] = c
func (r *recordingRenderer) byName() map[string]map[string]any {
out := make(map[string]map[string]any, len(r.results))
for _, pkg := range r.results {
out[pkg["name"].(string)] = pkg
}
return out
@@ -800,8 +806,9 @@ func TestSourceQueryBuilderRendererNumberMenu(t *testing.T) {
qb.Execute(context.Background(), mockDB, []string{"yay"})
require.NoError(t, qb.Results(mockDB, NumberMenu))
// One repo + two AUR results -> three hook calls and three printed lines.
require.Len(t, rr.calls, 3)
// Hook is called once for the whole menu.
require.Equal(t, 1, rr.calls)
require.Len(t, rr.results, 3)
lines := strings.Split(strings.TrimRight(w.String(), "\n"), "\n")
require.Len(t, lines, 3)
@@ -814,15 +821,12 @@ func TestSourceQueryBuilderRendererNumberMenu(t *testing.T) {
require.Contains(t, byName, "yay")
require.Contains(t, byName, "yay-git")
assert.Equal(t, SearchEventRepo, byName["ruby-yard"].event)
assert.Equal(t, "extra", byName["ruby-yard"].pkg["source"])
assert.Equal(t, SearchEventAUR, byName["yay"].event)
assert.Equal(t, "aur", byName["yay"].pkg["source"])
assert.Equal(t, "extra", byName["ruby-yard"]["source"])
assert.Equal(t, "aur", byName["yay"]["source"])
indices := map[int]bool{}
for _, c := range rr.calls {
assert.Equal(t, 3, c.pkg["count"])
idx, ok := c.pkg["index"].(int)
for _, pkg := range rr.results {
idx, ok := pkg["index"].(int)
require.True(t, ok, "index present in number menu")
indices[idx] = true
}
@@ -840,10 +844,10 @@ func TestSourceQueryBuilderRendererDetailedNoIndex(t *testing.T) {
qb.Execute(context.Background(), mockDB, []string{"yay"})
require.NoError(t, qb.Results(mockDB, Detailed))
require.Len(t, rr.calls, 3)
for _, c := range rr.calls {
assert.Equal(t, 3, c.pkg["count"])
_, hasIndex := c.pkg["index"]
require.Equal(t, 1, rr.calls)
require.Len(t, rr.results, 3)
for _, pkg := range rr.results {
_, hasIndex := pkg["index"]
assert.False(t, hasIndex, "index absent in detailed mode")
}
}
@@ -859,8 +863,8 @@ func TestSourceQueryBuilderRendererDeferFallsBackToDefault(t *testing.T) {
qb.Execute(context.Background(), mockDB, []string{"yay"})
require.NoError(t, qb.Results(mockDB, Detailed))
// Renderer was consulted for every row but deferred, so no hook output.
require.Len(t, rr.calls, 3)
// Renderer was consulted once but deferred, so no hook output.
require.Equal(t, 1, rr.calls)
assert.NotContains(t, w.String(), "HOOK ")
assert.Contains(t, w.String(), "yay")
}
@@ -894,13 +898,13 @@ func TestSourceQueryBuilderRendererAURPkgHasLastModified(t *testing.T) {
require.Contains(t, byName, "yay")
require.Contains(t, byName, "yay-git")
assert.Equal(t, 1765742501, byName["yay"].pkg["last_modified"],
assert.Equal(t, 1765742501, byName["yay"]["last_modified"],
"last_modified must match the AUR fixture value")
assert.Equal(t, 1765742519, byName["yay-git"].pkg["last_modified"],
assert.Equal(t, 1765742519, byName["yay-git"]["last_modified"],
"last_modified must match the AUR fixture value")
// Repo packages must not carry a last_modified field.
// Repo packages carry -1 sentinel (uniform schema).
require.Contains(t, byName, "ruby-yard")
_, hasLastModified := byName["ruby-yard"].pkg["last_modified"]
assert.False(t, hasLastModified, "repo packages must not have last_modified")
assert.Equal(t, -1, byName["ruby-yard"]["last_modified"],
"repo packages carry -1 sentinel for last_modified")
}
-51
View File
@@ -121,54 +121,3 @@ func syncPkgSearchString(pkg alpm.Package, dbExecutor db.Executor, singleLineRes
return toPrint
}
// aurPkgToMap builds the Lua-facing table data for an AUR search result.
func aurPkgToMap(pkg *aur.Pkg, dbExecutor db.Executor) map[string]any {
provides := make([]string, len(pkg.Provides))
copy(provides, pkg.Provides)
m := map[string]any{
"source": "aur",
"name": pkg.Name,
"version": pkg.Version,
"description": pkg.Description,
"votes": pkg.NumVotes,
"popularity": pkg.Popularity,
"out_of_date": pkg.OutOfDate,
"package_base": pkg.PackageBase,
"provides": provides,
"last_modified": pkg.LastModified,
}
if pkg.Maintainer != "" {
m["maintainer"] = pkg.Maintainer
}
if localPkg := dbExecutor.LocalPackage(pkg.Name); localPkg != nil {
m["installed"], m["installed_version"] = true, localPkg.Version()
} else {
m["installed"] = false
}
return m
}
// syncPkgToMap builds the Lua-facing table data for a repo search result.
func syncPkgToMap(pkg alpm.Package, dbExecutor db.Executor) map[string]any {
m := map[string]any{
"source": pkg.DB().Name(),
"name": pkg.Name(),
"version": pkg.Version(),
"description": pkg.Description(),
"size": pkg.Size(),
"installed_size": pkg.ISize(),
"groups": dbExecutor.PackageGroups(pkg),
}
if localPkg := dbExecutor.LocalPackage(pkg.Name()); localPkg != nil {
m["installed"], m["installed_version"] = true, localPkg.Version()
} else {
m["installed"] = false
}
return m
}
+29 -23
View File
@@ -12,19 +12,24 @@ func TestRenderHandledReturnsLine(t *testing.T) {
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.on("search_aur", function(pkg)
return pkg.source .. "/" .. pkg.name .. " " .. pkg.version
yay.on("render_search", function(results)
local out = {}
for _, pkg in ipairs(results) do
out[#out + 1] = pkg.source .. "/" .. pkg.name .. " " .. pkg.version
end
return table.concat(out, "\n")
end)
`))
assert.True(t, e.HasHooks())
line, handled, err := e.Render("search_aur", map[string]any{
"source": "aur", "name": "yay", "version": "12.0.0",
out, handled, err := e.RenderSearch([]map[string]any{
{"source": "aur", "name": "yay", "version": "12.0.0"},
{"source": "extra", "name": "bash", "version": "5.2"},
})
require.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "aur/yay 12.0.0", line)
assert.Equal(t, "aur/yay 12.0.0\nextra/bash 5.2", out)
}
func TestRenderNoHookReturnsUnhandled(t *testing.T) {
@@ -33,10 +38,10 @@ func TestRenderNoHookReturnsUnhandled(t *testing.T) {
assert.False(t, e.HasHooks())
line, handled, err := e.Render("search_aur", map[string]any{"name": "yay"})
out, handled, err := e.RenderSearch([]map[string]any{{"name": "yay"}})
require.NoError(t, err)
assert.False(t, handled)
assert.Empty(t, line)
assert.Empty(t, out)
}
func TestRenderNonStringReturnIsUnhandled(t *testing.T) {
@@ -44,15 +49,15 @@ func TestRenderNonStringReturnIsUnhandled(t *testing.T) {
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.on("search_repo", function(pkg)
yay.on("render_search", function(results)
return 42
end)
`))
line, handled, err := e.Render("search_repo", map[string]any{"name": "bash"})
out, handled, err := e.RenderSearch([]map[string]any{{"name": "bash"}})
require.NoError(t, err)
assert.False(t, handled)
assert.Empty(t, line)
assert.Empty(t, out)
}
func TestRenderNilReturnIsUnhandled(t *testing.T) {
@@ -60,12 +65,12 @@ func TestRenderNilReturnIsUnhandled(t *testing.T) {
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.on("search_repo", function(pkg)
yay.on("render_search", function(results)
return nil
end)
`))
_, handled, err := e.Render("search_repo", map[string]any{"name": "bash"})
_, handled, err := e.RenderSearch([]map[string]any{{"name": "bash"}})
require.NoError(t, err)
assert.False(t, handled)
}
@@ -75,15 +80,15 @@ func TestRenderRuntimeErrorWraps(t *testing.T) {
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.on("search_aur", function(pkg)
yay.on("render_search", function(results)
error("boom")
end)
`))
_, handled, err := e.Render("search_aur", map[string]any{"name": "yay"})
_, handled, err := e.RenderSearch([]map[string]any{{"name": "yay"}})
require.Error(t, err)
assert.False(t, handled)
assert.Contains(t, err.Error(), "init.lua search_aur hook")
assert.Contains(t, err.Error(), "init.lua render_search hook")
assert.Contains(t, err.Error(), "boom")
}
@@ -102,14 +107,14 @@ func TestOnLastWinsSingleSlot(t *testing.T) {
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.on("search_aur", function(pkg) return "first" end)
yay.on("search_aur", function(pkg) return "second" end)
yay.on("render_search", function(results) return "first" end)
yay.on("render_search", function(results) return "second" end)
`))
line, handled, err := e.Render("search_aur", map[string]any{"name": "yay"})
out, handled, err := e.RenderSearch([]map[string]any{{"name": "yay"}})
require.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "second", line)
assert.Equal(t, "second", out)
}
func TestToTableConvertsAllKinds(t *testing.T) {
@@ -117,7 +122,8 @@ func TestToTableConvertsAllKinds(t *testing.T) {
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.on("search_aur", function(pkg)
yay.on("render_search", function(results)
local pkg = results[1]
assert(pkg.name == "yay", "name")
assert(pkg.votes == 10, "votes")
assert(math.abs(pkg.popularity - 1.5) < 0.001, "popularity")
@@ -130,15 +136,15 @@ func TestToTableConvertsAllKinds(t *testing.T) {
end)
`))
line, handled, err := e.Render("search_aur", map[string]any{
out, handled, err := e.RenderSearch([]map[string]any{{
"name": "yay",
"votes": 10,
"popularity": 1.5,
"size": int64(4096),
"installed": true,
"provides": []string{"a", "b"},
})
}})
require.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "ok", line)
assert.Equal(t, "ok", out)
}
+24 -17
View File
@@ -13,17 +13,17 @@ const (
optTableName = "opt"
)
var validEvents = map[string]bool{"search_aur": true, "search_repo": true}
var validEvents = map[string]bool{"render_search": true}
type Engine struct {
L *lua.LState
hooks map[string]*lua.LFunction
L *lua.LState
renderSearch *lua.LFunction
}
func New() *Engine {
state := lua.NewState()
e := &Engine{L: state, hooks: map[string]*lua.LFunction{}}
e := &Engine{L: state}
yayTbl := state.NewTable()
state.SetGlobal(globalName, yayTbl)
@@ -33,7 +33,7 @@ func New() *Engine {
return e
}
// luaOn registers a Lua callback for a search-display event (yay.on).
// luaOn registers a Lua callback for the render_search event (yay.on).
func (e *Engine) luaOn(L *lua.LState) int {
event := L.CheckString(1)
fn := L.CheckFunction(2)
@@ -43,31 +43,29 @@ func (e *Engine) luaOn(L *lua.LState) int {
return 0
}
e.hooks[event] = fn
e.renderSearch = fn
return 0
}
// HasHooks reports whether any search-display callback has been registered.
func (e *Engine) HasHooks() bool {
return len(e.hooks) > 0
return e.renderSearch != nil
}
// Render invokes the callback registered for event with pkg. It returns the
// produced line and handled=true when a callback returned a string; handled is
// false when no callback is registered or the callback deferred to the default
// formatter (returned nil/nothing/non-string).
func (e *Engine) Render(event string, pkg map[string]any) (string, bool, error) {
fn, ok := e.hooks[event]
if !ok {
// RenderSearch invokes the render_search callback with the full result list.
// handled=true when the callback returned a string; false when no callback is
// registered or it deferred (returned nil/nothing/non-string).
func (e *Engine) RenderSearch(results []map[string]any) (string, bool, error) {
if e.renderSearch == nil {
return "", false, nil
}
e.L.Push(fn)
e.L.Push(e.toTable(pkg))
e.L.Push(e.renderSearch)
e.L.Push(e.resultsToTable(results))
if err := e.L.PCall(1, 1, nil); err != nil {
return "", false, fmt.Errorf("init.lua %s hook: %w", event, err)
return "", false, fmt.Errorf("init.lua render_search hook: %w", err)
}
ret := e.L.Get(-1)
@@ -80,6 +78,15 @@ func (e *Engine) Render(event string, pkg map[string]any) (string, bool, error)
return "", false, nil
}
func (e *Engine) resultsToTable(results []map[string]any) *lua.LTable {
tbl := e.L.NewTable()
for i := range results {
tbl.Append(e.toTable(results[i]))
}
return tbl
}
// toTable converts a Go map into a Lua table for passing to a callback.
func (e *Engine) toTable(pkg map[string]any) *lua.LTable {
tbl := e.L.NewTable()