From 54b30b62fe7a5196d355d4abe543f0b564bbfa36 Mon Sep 17 00:00:00 2001 From: Jo Garnier Date: Mon, 15 Jun 2026 21:18:11 +0200 Subject: [PATCH] try batch search hooking --- doc/init.lua | 29 +++++--- doc/lua.md | 74 +++++++++------------ pkg/query/query_builder.go | 113 ++++++++++++++++++++------------ pkg/query/query_builder_test.go | 70 ++++++++++---------- pkg/query/types.go | 51 -------------- pkg/settings/lua/hooks_test.go | 52 ++++++++------- pkg/settings/lua/lua.go | 41 +++++++----- 7 files changed, 212 insertions(+), 218 deletions(-) diff --git a/doc/init.lua b/doc/init.lua index e33d1fe4..2c72f966 100644 --- a/doc/init.lua +++ b/doc/init.lua @@ -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) \ No newline at end of file diff --git a/doc/lua.md b/doc/lua.md index 69155611..4a6b119e 100644 --- a/doc/lua.md +++ b/doc/lua.md @@ -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 `). 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 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** | \ No newline at end of file diff --git a/pkg/query/query_builder.go b/pkg/query/query_builder.go index 7b16518f..126c6b3f 100644 --- a/pkg/query/query_builder.go +++ b/pkg/query/query_builder.go @@ -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 { diff --git a/pkg/query/query_builder_test.go b/pkg/query/query_builder_test.go index 6474123d..ba1aae5b 100644 --- a/pkg/query/query_builder_test.go +++ b/pkg/query/query_builder_test.go @@ -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") } diff --git a/pkg/query/types.go b/pkg/query/types.go index ef04febe..69226c29 100644 --- a/pkg/query/types.go +++ b/pkg/query/types.go @@ -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 -} diff --git a/pkg/settings/lua/hooks_test.go b/pkg/settings/lua/hooks_test.go index 986b0c79..8a484ada 100644 --- a/pkg/settings/lua/hooks_test.go +++ b/pkg/settings/lua/hooks_test.go @@ -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) } diff --git a/pkg/settings/lua/lua.go b/pkg/settings/lua/lua.go index 9f940e48..2b61f1e7 100644 --- a/pkg/settings/lua/lua.go +++ b/pkg/settings/lua/lua.go @@ -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()