try batch search hooking
This commit is contained in:
7 files changed
+212
-218
No files matched your search
+19
-10
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user