feat(search): implement render-hooks for search (#2896)
* add render hooks * re-render hook per event * add example with single_line and simplify built rendering * update docs availability * remove comments
This commit is contained in:
10 files changed
+884
-31
No files matched your search
@@ -12,21 +12,20 @@ RUN set -eux; \
|
||||
sed -i 's/^#\?DisableSandbox.*/DisableSandbox/' /etc/pacman.conf || true; \
|
||||
sed -i 's/^#\?DownloadUser.*/#DownloadUser = alpm/' /etc/pacman.conf || true; \
|
||||
pacman -Syu --noconfirm --needed \
|
||||
pacman-contrib zsh git-delta openssh bat go github-cli \
|
||||
pacman-contrib zsh git-delta openssh bat go github-cli ripgrep \
|
||||
archlinux-keyring git gcc make sudo asciidoc doxygen meson curl; \
|
||||
curl -sSfL https://golangci-lint.run/install.sh | sh -s v2.12.2; \
|
||||
go mod download; \
|
||||
rm -rf /var/lib/pacman/sync/* /var/cache/pacman/* /tmp/* /var/tmp/*; \
|
||||
rm -rf /usr/share/man/* /usr/share/doc/* || true
|
||||
|
||||
# Non-root user with passwordless sudo
|
||||
RUN useradd -m -s /bin/bash docker \
|
||||
&& echo "docker ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/docker
|
||||
|
||||
USER docker
|
||||
|
||||
# Install xgotext
|
||||
RUN go install github.com/leonelquinteros/gotext/cli/xgotext@latest
|
||||
RUN go install github.com/leonelquinteros/gotext/cli/xgotext@latest \
|
||||
&& go install golang.org/x/tools/gopls@latest
|
||||
|
||||
ENV PATH="/app/bin:/home/docker/go/bin:$PATH"
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Reproduce yay.opt.single_line_results via RenderAUR/RenderSync.
|
||||
--
|
||||
-- yay.opt.single_line_results only changes one thing in the built-in
|
||||
-- renderer: the description is joined onto the result line with a tab
|
||||
-- instead of being printed on its own indented line below. These two hooks
|
||||
-- rebuild that same layout (plus the bold/color accents) from scratch, so
|
||||
-- you have a starting point for further tweaks (drop fields, change colors,
|
||||
-- add links, etc.). yay does not expose a color/text helper API to Lua, so
|
||||
-- every ANSI escape below is built by hand with plain Lua string functions.
|
||||
|
||||
local ESC = string.char(27)
|
||||
local SEPARATOR = "\t" -- the one change that makes a result single-line
|
||||
|
||||
local MINUTE, HOUR, DAY = 60, 3600, 24 * 3600
|
||||
|
||||
local function sgr(code, s)
|
||||
return ESC .. "[" .. code .. "m" .. s .. ESC .. "[0m"
|
||||
end
|
||||
|
||||
local function bold(s) return sgr("1", s) end
|
||||
local function red(s) return sgr("31", s) end
|
||||
local function green(s) return sgr("32", s) end
|
||||
local function yellow(s) return sgr("33", s) end
|
||||
local function cyan(s) return sgr("36", s) end
|
||||
|
||||
-- A given repo/package name always renders in the same one of six colors,
|
||||
-- same hashing idea as yay's internal ColorHash.
|
||||
local function color_hash(name)
|
||||
local hash = 0
|
||||
for i = 1, #name do
|
||||
hash = (string.byte(name, i) + hash * 32 + hash) % 2147483648
|
||||
end
|
||||
return sgr(tostring(hash % 6 + 31), name)
|
||||
end
|
||||
|
||||
-- Common "repo/name version" prefix shared by AUR and sync result lines.
|
||||
local function header(repo, name, version)
|
||||
return bold(color_hash(repo)) .. "/" .. bold(name) .. " " .. cyan(version)
|
||||
end
|
||||
|
||||
-- Appends " tag" to line, unless tag is empty/nil/false.
|
||||
local function append(line, tag)
|
||||
if not tag or tag == "" then
|
||||
return line
|
||||
end
|
||||
return line .. " " .. tag
|
||||
end
|
||||
|
||||
-- "[Xm]"/"[Xh]"/"[Xd]" age badge, colored red/yellow/cyan the newer it is.
|
||||
local function age_tag(last_modified)
|
||||
if not last_modified or last_modified == 0 then
|
||||
return ""
|
||||
end
|
||||
|
||||
local age = os.time() - last_modified
|
||||
if age < HOUR then
|
||||
return bold(red("[" .. math.floor(age / MINUTE) .. "m]"))
|
||||
elseif age < DAY then
|
||||
return bold(red("[" .. math.floor(age / HOUR) .. "h]"))
|
||||
elseif age < 7 * DAY then
|
||||
return bold(yellow("[" .. math.floor(age / DAY) .. "d]"))
|
||||
end
|
||||
return cyan("[" .. math.floor(age / DAY) .. "d]")
|
||||
end
|
||||
|
||||
local function installed_tag(version, local_version)
|
||||
if local_version == "" then
|
||||
return ""
|
||||
elseif local_version == version then
|
||||
return bold(green("(Installed)"))
|
||||
end
|
||||
return bold(green("(Installed: " .. local_version .. ")"))
|
||||
end
|
||||
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
desc = "single-line, colored AUR search results (mirrors single_line_results)",
|
||||
callback = function(event)
|
||||
local d = event.data
|
||||
|
||||
local line = header("aur", d.name, d.version) ..
|
||||
bold(" (+" .. d.votes) .. " " .. bold(string.format("%.2f", d.popularity) .. ")")
|
||||
|
||||
line = append(line, age_tag(d.last_modified))
|
||||
line = append(line, d.maintainer == "" and bold(red("(Orphaned)")))
|
||||
line = append(line, d.out_of_date ~= 0 and bold(red("(Out-of-date)")))
|
||||
line = append(line, installed_tag(d.version, d.local_version))
|
||||
|
||||
return line .. SEPARATOR .. d.description
|
||||
end,
|
||||
})
|
||||
|
||||
yay.create_autocmd("RenderSync", {
|
||||
desc = "single-line, colored sync search results (mirrors single_line_results)",
|
||||
callback = function(event)
|
||||
local d = event.data
|
||||
|
||||
local line = header(d.repository, d.name, d.version)
|
||||
line = append(line, #d.groups > 0 and table.concat(d.groups, " "))
|
||||
line = append(line, installed_tag(d.version, d.local_version))
|
||||
|
||||
return line .. SEPARATOR .. d.description
|
||||
end,
|
||||
})
|
||||
@@ -131,3 +131,29 @@ yay.opt.double_confirm = true
|
||||
-- return out
|
||||
-- end,
|
||||
-- })
|
||||
--
|
||||
-- Render a custom string for each AUR result row during -Ss / -S number menu.
|
||||
-- Return a string to use it; return nil (or nothing) to fall back to default.
|
||||
--
|
||||
-- yay.create_autocmd("RenderAUR", {
|
||||
-- desc = "compact AUR search lines",
|
||||
-- callback = function(event)
|
||||
-- local d = event.data
|
||||
-- local tag = d.local_version ~= "" and (" [installed: " .. d.local_version .. "]") or ""
|
||||
-- return string.format("aur/%s %s (+%d)%s\n %s",
|
||||
-- d.name, d.version, d.votes, tag, d.description)
|
||||
-- end,
|
||||
-- })
|
||||
--
|
||||
-- Render a custom string for each sync/repo result row during -Ss / -S number menu.
|
||||
-- Return a string to use it; return nil (or nothing) to fall back to default.
|
||||
--
|
||||
-- yay.create_autocmd("RenderSync", {
|
||||
-- desc = "compact sync search lines",
|
||||
-- callback = function(event)
|
||||
-- local d = event.data
|
||||
-- local tag = d.local_version ~= "" and (" [installed: " .. d.local_version .. "]") or ""
|
||||
-- return string.format("%s/%s %s%s\n %s",
|
||||
-- d.repository, d.name, d.version, tag, d.description)
|
||||
-- end,
|
||||
-- })
|
||||
+83
@@ -483,3 +483,86 @@ yay.create_autocmd("SearchFilter", {
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
## Render hooks
|
||||
|
||||
`RenderAUR` and `RenderSync` fire once per result row during `yay -Ss` and
|
||||
the `yay <pkg>` number menu, after results are ranked, sorted, and (optionally)
|
||||
filtered. The callback receives per-package metadata and may return a `string`
|
||||
that fully replaces what yay would print for that row. Returning `nil` (or
|
||||
nothing) falls back to yay's built-in formatting.
|
||||
|
||||
The menu number prefix (magenta index shown in the number menu) is always
|
||||
prepended by yay itself; the callback controls only the package portion of the
|
||||
line. `-q`/`--quiet` (minimal verbosity) is unaffected — name-only output
|
||||
never calls these hooks. Hook errors are logged and that row falls back to
|
||||
default rendering rather than aborting the command.
|
||||
|
||||
Multiple hooks of the same event **run in registration order**; the **last**
|
||||
hook that returns a non-`nil` string wins. A later hook returning `nil` does
|
||||
not undo an earlier hook's string.
|
||||
|
||||
### RenderAUR event
|
||||
|
||||
<p class="api-since">Available from yay v13.1.0</p>
|
||||
|
||||
```lua
|
||||
{
|
||||
event = "RenderAUR",
|
||||
data = {
|
||||
name = "pkgname",
|
||||
version = "1.0-1",
|
||||
description = "A useful package",
|
||||
base = "pkgbase",
|
||||
votes = 42,
|
||||
popularity = 1.23,
|
||||
maintainer = "alice",
|
||||
out_of_date = 0, -- 0 if current; Unix timestamp if flagged
|
||||
first_submitted = 1700000000,
|
||||
last_modified = 1700000001,
|
||||
local_version = "", -- installed version, or "" when not installed
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### RenderSync event
|
||||
|
||||
<p class="api-since">Available from yay v13.1.0</p>
|
||||
|
||||
```lua
|
||||
{
|
||||
event = "RenderSync",
|
||||
data = {
|
||||
repository = "extra", -- pacman DB name (e.g. "core", "extra")
|
||||
name = "pkgname",
|
||||
description = "A useful package",
|
||||
version = "2.0-1",
|
||||
groups = {}, -- array of group strings, often empty
|
||||
local_version = "", -- installed version, or "" when not installed
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
desc = "compact AUR search lines",
|
||||
callback = function(event)
|
||||
local d = event.data
|
||||
local tag = d.local_version ~= "" and (" [installed: " .. d.local_version .. "]") or ""
|
||||
return string.format("aur/%s %s (+%d)%s\n %s",
|
||||
d.name, d.version, d.votes, tag, d.description)
|
||||
end,
|
||||
})
|
||||
|
||||
yay.create_autocmd("RenderSync", {
|
||||
desc = "compact sync search lines",
|
||||
callback = function(event)
|
||||
local d = event.data
|
||||
local tag = d.local_version ~= "" and (" [installed: " .. d.local_version .. "]") or ""
|
||||
return string.format("%s/%s %s%s\n %s",
|
||||
d.repository, d.name, d.version, tag, d.description)
|
||||
end,
|
||||
})
|
||||
```
|
||||
+44
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
---@alias yay.menuAnswer "" | "All" | "None" | "Installed" | "NotInstalled" | "abort"
|
||||
|
||||
---@alias yay.Event "AURPreInstall" | "AURPostDownload" | "UpgradeSelect" | "PostInstall" | "SearchFilter"
|
||||
---@alias yay.Event "AURPreInstall" | "AURPostDownload" | "UpgradeSelect" | "PostInstall" | "SearchFilter" | "RenderAUR" | "RenderSync"
|
||||
|
||||
-- Options: yay.opt
|
||||
|
||||
@@ -209,11 +209,54 @@
|
||||
---@field desc? string
|
||||
---@field callback fun(event: yay.SearchFilterEvent): yay.SearchResultRef[]?
|
||||
|
||||
-- Event payloads: RenderAUR
|
||||
|
||||
---@class yay.RenderAURData
|
||||
---@field name string
|
||||
---@field version string
|
||||
---@field description string
|
||||
---@field base string
|
||||
---@field votes integer
|
||||
---@field popularity number
|
||||
---@field maintainer string
|
||||
---@field out_of_date integer
|
||||
---@field first_submitted integer
|
||||
---@field last_modified integer
|
||||
---@field local_version string
|
||||
|
||||
---@class yay.RenderAUREvent
|
||||
---@field event "RenderAUR"
|
||||
---@field data yay.RenderAURData
|
||||
|
||||
---@class yay.RenderAUROpts
|
||||
---@field desc? string
|
||||
---@field callback fun(event: yay.RenderAUREvent): string?
|
||||
|
||||
-- Event payloads: RenderSync
|
||||
|
||||
---@class yay.RenderSyncData
|
||||
---@field repository string
|
||||
---@field name string
|
||||
---@field description string
|
||||
---@field version string
|
||||
---@field groups string[]
|
||||
---@field local_version string
|
||||
|
||||
---@class yay.RenderSyncEvent
|
||||
---@field event "RenderSync"
|
||||
---@field data yay.RenderSyncData
|
||||
|
||||
---@class yay.RenderSyncOpts
|
||||
---@field desc? string
|
||||
---@field callback fun(event: yay.RenderSyncEvent): string?
|
||||
|
||||
---@overload fun(event: "AURPreInstall", opts: yay.AURPreInstallOpts)
|
||||
---@overload fun(event: "AURPostDownload", opts: yay.AURPostDownloadOpts)
|
||||
---@overload fun(event: "UpgradeSelect", opts: yay.UpgradeSelectOpts)
|
||||
---@overload fun(event: "PostInstall", opts: yay.PostInstallOpts)
|
||||
---@overload fun(event: "SearchFilter", opts: yay.SearchFilterOpts)
|
||||
---@overload fun(event: "RenderAUR", opts: yay.RenderAUROpts)
|
||||
---@overload fun(event: "RenderSync", opts: yay.RenderSyncOpts)
|
||||
---@class yay.create_autocmd
|
||||
|
||||
-- The yay global
|
||||
|
||||
@@ -293,10 +293,10 @@ func (s *SourceQueryBuilder) Results(dbExecutor db.Executor, verboseSearch Searc
|
||||
|
||||
if s.results[i].source == "aur" {
|
||||
pkg := s.aurQueryMap[s.results[i].name]
|
||||
toPrint += aurPkgSearchString(pkg, dbExecutor, s.singleLineResults)
|
||||
toPrint += s.renderAUR(pkg, dbExecutor)
|
||||
} else {
|
||||
pkg := s.repoQueryMap[s.results[i].source+"/"+s.results[i].name]
|
||||
toPrint += syncPkgSearchString(pkg, dbExecutor, s.singleLineResults)
|
||||
toPrint += s.renderSync(pkg, dbExecutor)
|
||||
}
|
||||
|
||||
s.logger.Println(toPrint)
|
||||
@@ -395,3 +395,60 @@ func (s *SourceQueryBuilder) applySearchFilter(results []abstractResult) []abstr
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (s *SourceQueryBuilder) renderAUR(pkg *aur.Pkg, dbExecutor db.Executor) string {
|
||||
var localVersion string
|
||||
if localPkg := dbExecutor.LocalPackage(pkg.Name); localPkg != nil {
|
||||
localVersion = localPkg.Version()
|
||||
}
|
||||
|
||||
if s.lua != nil && s.lua.HasAutocmd(settingslua.EventRenderAUR) {
|
||||
rendered, ok, err := s.lua.RunRenderAUR(&settingslua.RenderAUREvent{
|
||||
Name: pkg.Name,
|
||||
Version: pkg.Version,
|
||||
Description: pkg.Description,
|
||||
Base: pkg.PackageBase,
|
||||
Votes: pkg.NumVotes,
|
||||
Popularity: pkg.Popularity,
|
||||
Maintainer: pkg.Maintainer,
|
||||
OutOfDate: pkg.OutOfDate,
|
||||
FirstSubmitted: pkg.FirstSubmitted,
|
||||
LastModified: pkg.LastModified,
|
||||
LocalVersion: localVersion,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Errorln(err)
|
||||
} else if ok {
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
||||
return aurPkgSearchStringResolved(pkg, localVersion, s.singleLineResults)
|
||||
}
|
||||
|
||||
func (s *SourceQueryBuilder) renderSync(pkg alpm.Package, dbExecutor db.Executor) string {
|
||||
var localVersion string
|
||||
if localPkg := dbExecutor.LocalPackage(pkg.Name()); localPkg != nil {
|
||||
localVersion = localPkg.Version()
|
||||
}
|
||||
|
||||
groups := dbExecutor.PackageGroups(pkg)
|
||||
|
||||
if s.lua != nil && s.lua.HasAutocmd(settingslua.EventRenderSync) {
|
||||
rendered, ok, err := s.lua.RunRenderSync(&settingslua.RenderSyncEvent{
|
||||
Repository: pkg.DB().Name(),
|
||||
Name: pkg.Name(),
|
||||
Description: pkg.Description(),
|
||||
Version: pkg.Version(),
|
||||
Groups: groups,
|
||||
LocalVersion: localVersion,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Errorln(err)
|
||||
} else if ok {
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
||||
return syncPkgSearchStringResolved(pkg, groups, localVersion, s.singleLineResults)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
//go:build !integration
|
||||
|
||||
package query
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
aurc "github.com/Jguer/aur"
|
||||
|
||||
dbmock "github.com/Jguer/yay/v13/pkg/db/mock"
|
||||
settingslua "github.com/Jguer/yay/v13/pkg/settings/lua"
|
||||
"github.com/Jguer/yay/v13/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v13/pkg/text"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSourceQueryBuilderRenderAURHook verifies that a RenderAUR hook replaces
|
||||
// the default AUR row rendering while sync rows keep their default formatting.
|
||||
func TestSourceQueryBuilderRenderAURHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockDB, mockAUR := newYayQueryBuilderMocks()
|
||||
|
||||
w := &strings.Builder{}
|
||||
logger := text.NewLogger(w, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
e := settingslua.New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
return "AURLINE:" .. event.data.name
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
qb := NewSourceQueryBuilder(mockAUR, logger, "", parser.ModeAny, "", false, false, false)
|
||||
qb.SetLua(e)
|
||||
qb.Execute(t.Context(), mockDB, []string{"yay"})
|
||||
|
||||
err := qb.Results(mockDB, Detailed)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := w.String()
|
||||
|
||||
// Both AUR packages should use the hook's output.
|
||||
assert.Contains(t, out, "AURLINE:yay", "hook output for yay must appear")
|
||||
assert.Contains(t, out, "AURLINE:yay-git", "hook output for yay-git must appear")
|
||||
|
||||
// Default AUR rendering includes the votes marker; hook replaces it entirely.
|
||||
assert.NotContains(t, out, "(+2461", "default AUR rendering must not appear when hook is active")
|
||||
|
||||
// Sync package should keep its default rendering (no RenderSync hook registered).
|
||||
assert.Contains(t, out, "ruby-yard", "sync package must still appear with default rendering")
|
||||
}
|
||||
|
||||
// TestSourceQueryBuilderRenderSyncHook verifies that a RenderSync hook replaces
|
||||
// the default sync row rendering.
|
||||
func TestSourceQueryBuilderRenderSyncHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockDB, mockAUR := newYayQueryBuilderMocks()
|
||||
|
||||
w := &strings.Builder{}
|
||||
logger := text.NewLogger(w, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
e := settingslua.New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderSync", {
|
||||
callback = function(event)
|
||||
return "SYNCLINE:" .. event.data.name .. "/" .. event.data.repository
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
qb := NewSourceQueryBuilder(mockAUR, logger, "", parser.ModeAny, "", false, false, false)
|
||||
qb.SetLua(e)
|
||||
qb.Execute(t.Context(), mockDB, []string{"yay"})
|
||||
|
||||
err := qb.Results(mockDB, Detailed)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := w.String()
|
||||
assert.Contains(t, out, "SYNCLINE:ruby-yard/extra", "hook output for ruby-yard must appear")
|
||||
}
|
||||
|
||||
// TestSourceQueryBuilderRenderAURNilFallsBack verifies that a RenderAUR hook
|
||||
// that returns nil causes the default formatter to be used for that row.
|
||||
func TestSourceQueryBuilderRenderAURNilFallsBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockDB, mockAUR := newYayQueryBuilderMocks()
|
||||
|
||||
w := &strings.Builder{}
|
||||
logger := text.NewLogger(w, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
e := settingslua.New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
qb := NewSourceQueryBuilder(mockAUR, logger, "", parser.ModeAny, "", false, false, false)
|
||||
qb.SetLua(e)
|
||||
qb.Execute(t.Context(), mockDB, []string{"yay"})
|
||||
|
||||
err := qb.Results(mockDB, Detailed)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := w.String()
|
||||
// Default AUR rendering must appear when hook returns nil.
|
||||
assert.Contains(t, out, "(+2461", "default AUR rendering must appear when hook returns nil")
|
||||
}
|
||||
|
||||
func TestSourceQueryBuilderRenderAURHookReceivesLocalVersionWhenSame(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := settingslua.New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
return "LOCAL:" .. event.data.local_version
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
mockDB := &dbmock.DBExecutor{
|
||||
LocalPackageFn: func(string) dbmock.IPackage {
|
||||
return &dbmock.Package{PName: "yay", PVersion: "12.5.7-1"}
|
||||
},
|
||||
}
|
||||
logger := text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
qb := NewSourceQueryBuilder(nil, logger, "", parser.ModeAny, "", false, false, false)
|
||||
qb.SetLua(e)
|
||||
|
||||
rendered := qb.renderAUR(&aurc.Pkg{Name: "yay", Version: "12.5.7-1"}, mockDB)
|
||||
require.Equal(t, "LOCAL:12.5.7-1", rendered)
|
||||
}
|
||||
|
||||
func TestSourceQueryBuilderRenderSyncHookReceivesLocalVersionWhenSame(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := settingslua.New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderSync", {
|
||||
callback = function(event)
|
||||
return "LOCAL:" .. event.data.local_version
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
mockDB := &dbmock.DBExecutor{
|
||||
LocalPackageFn: func(string) dbmock.IPackage {
|
||||
return &dbmock.Package{PName: "ruby-yard", PVersion: "0.9.34-5"}
|
||||
},
|
||||
}
|
||||
pkg := &dbmock.Package{PDB: dbmock.NewDB("extra"), PName: "ruby-yard", PVersion: "0.9.34-5"}
|
||||
logger := text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
qb := NewSourceQueryBuilder(nil, logger, "", parser.ModeAny, "", false, false, false)
|
||||
qb.SetLua(e)
|
||||
|
||||
rendered := qb.renderSync(pkg, mockDB)
|
||||
require.Equal(t, "LOCAL:0.9.34-5", rendered)
|
||||
}
|
||||
|
||||
func TestSourceQueryBuilderDefaultRenderUsesShortInstalledTagWhenSame(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockDB := &dbmock.DBExecutor{
|
||||
LocalPackageFn: func(string) dbmock.IPackage {
|
||||
return &dbmock.Package{PName: "yay", PVersion: "12.5.7-1"}
|
||||
},
|
||||
}
|
||||
logger := text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
qb := NewSourceQueryBuilder(nil, logger, "", parser.ModeAny, "", false, false, false)
|
||||
|
||||
rendered := qb.renderAUR(&aurc.Pkg{Name: "yay", Version: "12.5.7-1"}, mockDB)
|
||||
assert.Contains(t, rendered, "(Installed)")
|
||||
assert.NotContains(t, rendered, "(Installed: 12.5.7-1)")
|
||||
}
|
||||
+18
-23
@@ -8,7 +8,6 @@ import (
|
||||
alpm "github.com/Jguer/dyalpm"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/Jguer/yay/v13/pkg/db"
|
||||
"github.com/Jguer/yay/v13/pkg/text"
|
||||
)
|
||||
|
||||
@@ -47,11 +46,9 @@ func getSearchBy(value string) aur.By {
|
||||
}
|
||||
}
|
||||
|
||||
func aurPkgSearchString(
|
||||
pkg *aur.Pkg,
|
||||
dbExecutor db.Executor,
|
||||
singleLineResults bool,
|
||||
) string {
|
||||
// aurPkgSearchStringResolved renders a search result for an AUR package.
|
||||
// It accepts pre-resolved installed state to avoid a second LocalPackage call.
|
||||
func aurPkgSearchStringResolved(pkg *aur.Pkg, localVersion string, singleLineResults bool) string {
|
||||
linkText := text.Bold(text.ColorHash("aur")) + "/" + text.Bold(pkg.Name)
|
||||
toPrint := text.CreateRepoLink("aur", "", pkg.Name, linkText) +
|
||||
" " + text.Cyan(pkg.Version) +
|
||||
@@ -70,12 +67,11 @@ func aurPkgSearchString(
|
||||
toPrint += text.Bold(text.Red(gotext.Get("(Out-of-date: %s)", text.FormatTime(pkg.OutOfDate)))) + " "
|
||||
}
|
||||
|
||||
if localPkg := dbExecutor.LocalPackage(pkg.Name); localPkg != nil {
|
||||
if localPkg.Version() != pkg.Version {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localPkg.Version())))
|
||||
} else {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
}
|
||||
switch {
|
||||
case localVersion == pkg.Version:
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
case localVersion != "":
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localVersion)))
|
||||
}
|
||||
|
||||
if singleLineResults {
|
||||
@@ -89,25 +85,24 @@ func aurPkgSearchString(
|
||||
return toPrint
|
||||
}
|
||||
|
||||
// PrintSearch receives a RepoSearch type and outputs pretty text.
|
||||
func syncPkgSearchString(pkg alpm.Package, dbExecutor db.Executor, singleLineResults bool) string {
|
||||
// syncPkgSearchStringResolved renders a search result for a sync package.
|
||||
// It accepts pre-resolved groups and local version state to avoid second DB calls.
|
||||
func syncPkgSearchStringResolved(pkg alpm.Package, groups []string, localVersion string, singleLineResults bool) string {
|
||||
linkText := text.Bold(text.ColorHash(pkg.DB().Name())) + "/" + text.Bold(pkg.Name())
|
||||
toPrint := text.CreateRepoLink(pkg.DB().Name(), pkg.Architecture(), pkg.Name(), linkText) +
|
||||
" " + text.Cyan(pkg.Version()) +
|
||||
text.Bold(" ("+text.Human(pkg.Size())+
|
||||
" "+text.Human(pkg.ISize())+") ")
|
||||
|
||||
packageGroups := dbExecutor.PackageGroups(pkg)
|
||||
if len(packageGroups) != 0 {
|
||||
toPrint += fmt.Sprint(packageGroups, " ")
|
||||
if len(groups) != 0 {
|
||||
toPrint += fmt.Sprint(groups, " ")
|
||||
}
|
||||
|
||||
if localPkg := dbExecutor.LocalPackage(pkg.Name()); localPkg != nil {
|
||||
if localPkg.Version() != pkg.Version() {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localPkg.Version())))
|
||||
} else {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
}
|
||||
switch {
|
||||
case localVersion == pkg.Version():
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
case localVersion != "":
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localVersion)))
|
||||
}
|
||||
|
||||
if singleLineResults {
|
||||
|
||||
+103
-1
@@ -13,6 +13,8 @@ const (
|
||||
EventUpgradeSelect = "UpgradeSelect"
|
||||
EventPostInstall = "PostInstall"
|
||||
EventSearchFilter = "SearchFilter"
|
||||
EventRenderAUR = "RenderAUR"
|
||||
EventRenderSync = "RenderSync"
|
||||
)
|
||||
|
||||
type Autocmd struct {
|
||||
@@ -116,10 +118,34 @@ type SearchResultRef struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type RenderAUREvent struct {
|
||||
Name string
|
||||
Version string
|
||||
Description string
|
||||
Base string
|
||||
Votes int
|
||||
Popularity float64
|
||||
Maintainer string
|
||||
OutOfDate int
|
||||
FirstSubmitted int
|
||||
LastModified int
|
||||
LocalVersion string
|
||||
}
|
||||
|
||||
type RenderSyncEvent struct {
|
||||
Repository string
|
||||
Name string
|
||||
Description string
|
||||
Version string
|
||||
Groups []string
|
||||
LocalVersion string
|
||||
}
|
||||
|
||||
func (e *Engine) createAutocmd(state *glua.LState) int {
|
||||
event := state.CheckString(1)
|
||||
if event != EventAURPreInstall && event != EventAURPostDownload &&
|
||||
event != EventUpgradeSelect && event != EventPostInstall && event != EventSearchFilter {
|
||||
event != EventUpgradeSelect && event != EventPostInstall && event != EventSearchFilter &&
|
||||
event != EventRenderAUR && event != EventRenderSync {
|
||||
state.ArgError(1, fmt.Sprintf("unsupported event %q", event))
|
||||
return 0
|
||||
}
|
||||
@@ -507,6 +533,82 @@ func (e *Engine) searchResultPackagesTable(packages []SearchResultPackage) *glua
|
||||
return tbl
|
||||
}
|
||||
|
||||
func (e *Engine) renderAURTable(event *RenderAUREvent) *glua.LTable {
|
||||
eventTable, data := e.newEventTable(EventRenderAUR)
|
||||
data.RawSetString("name", glua.LString(event.Name))
|
||||
data.RawSetString("version", glua.LString(event.Version))
|
||||
data.RawSetString("description", glua.LString(event.Description))
|
||||
data.RawSetString("base", glua.LString(event.Base))
|
||||
data.RawSetString("votes", glua.LNumber(event.Votes))
|
||||
data.RawSetString("popularity", glua.LNumber(event.Popularity))
|
||||
data.RawSetString("maintainer", glua.LString(event.Maintainer))
|
||||
data.RawSetString("out_of_date", glua.LNumber(event.OutOfDate))
|
||||
data.RawSetString("first_submitted", glua.LNumber(event.FirstSubmitted))
|
||||
data.RawSetString("last_modified", glua.LNumber(event.LastModified))
|
||||
data.RawSetString("local_version", glua.LString(event.LocalVersion))
|
||||
return eventTable
|
||||
}
|
||||
|
||||
func (e *Engine) renderSyncTable(event *RenderSyncEvent) *glua.LTable {
|
||||
eventTable, data := e.newEventTable(EventRenderSync)
|
||||
data.RawSetString("repository", glua.LString(event.Repository))
|
||||
data.RawSetString("name", glua.LString(event.Name))
|
||||
data.RawSetString("description", glua.LString(event.Description))
|
||||
data.RawSetString("version", glua.LString(event.Version))
|
||||
data.RawSetString("groups", e.stringArray(event.Groups))
|
||||
data.RawSetString("local_version", glua.LString(event.LocalVersion))
|
||||
return eventTable
|
||||
}
|
||||
|
||||
func (e *Engine) RunRenderAUR(event *RenderAUREvent) (rendered string, ok bool, err error) {
|
||||
if !e.HasAutocmd(EventRenderAUR) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
return e.runRender(EventRenderAUR, func() *glua.LTable {
|
||||
return e.renderAURTable(event)
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Engine) RunRenderSync(event *RenderSyncEvent) (rendered string, ok bool, err error) {
|
||||
if !e.HasAutocmd(EventRenderSync) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
return e.runRender(EventRenderSync, func() *glua.LTable {
|
||||
return e.renderSyncTable(event)
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Engine) runRender(eventName string, newEventTable func() *glua.LTable) (rendered string, ok bool, err error) {
|
||||
for _, autocmd := range e.autocmds[eventName] {
|
||||
if err := e.L.CallByParam(glua.P{
|
||||
Fn: autocmd.callback,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, newEventTable()); err != nil {
|
||||
return "", false, fmt.Errorf("%s: %w", eventName, wrapLuaErr(err))
|
||||
}
|
||||
|
||||
value := e.L.Get(-1)
|
||||
e.L.Pop(1)
|
||||
|
||||
if value == glua.LNil {
|
||||
continue
|
||||
}
|
||||
|
||||
str, isStr := value.(glua.LString)
|
||||
if !isStr {
|
||||
return "", false, fmt.Errorf("%s: callback must return a string or nil, got %s", eventName, value.Type())
|
||||
}
|
||||
|
||||
rendered = string(str)
|
||||
ok = true
|
||||
}
|
||||
|
||||
return rendered, ok, nil
|
||||
}
|
||||
|
||||
func parseSearchFilterResult(value glua.LValue, valid map[SearchResultRef]int) ([]SearchResultRef, bool, error) {
|
||||
if value == glua.LNil {
|
||||
return nil, false, nil
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRunRenderAUREventShapeAndStringReturn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
if event.event ~= "RenderAUR" then error("bad event name: " .. tostring(event.event)) end
|
||||
local d = event.data
|
||||
if d.name ~= "pkgA" then error("bad name") end
|
||||
if d.version ~= "1.0-1" then error("bad version") end
|
||||
if d.description ~= "A great package" then error("bad description") end
|
||||
if d.base ~= "pkgA-base" then error("bad base") end
|
||||
if d.votes ~= 42 then error("bad votes") end
|
||||
if math.abs(d.popularity - 3.14) > 0.001 then error("bad popularity") end
|
||||
if d.maintainer ~= "alice" then error("bad maintainer") end
|
||||
if d.out_of_date ~= 0 then error("bad out_of_date") end
|
||||
if d.first_submitted ~= 1000 then error("bad first_submitted") end
|
||||
if d.last_modified ~= 2000 then error("bad last_modified") end
|
||||
if d.local_version ~= "" then error("bad local_version") end
|
||||
return "X:" .. d.name
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
rendered, ok, err := e.RunRenderAUR(&RenderAUREvent{
|
||||
Name: "pkgA",
|
||||
Version: "1.0-1",
|
||||
Description: "A great package",
|
||||
Base: "pkgA-base",
|
||||
Votes: 42,
|
||||
Popularity: 3.14,
|
||||
Maintainer: "alice",
|
||||
OutOfDate: 0,
|
||||
FirstSubmitted: 1000,
|
||||
LastModified: 2000,
|
||||
LocalVersion: "",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "X:pkgA", rendered)
|
||||
}
|
||||
|
||||
func TestRunRenderAURNilReturnFallsBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
rendered, ok, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, "", rendered)
|
||||
}
|
||||
|
||||
func TestRunRenderAURNonStringReturnIsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
return 42
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
_, ok, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.Error(t, err)
|
||||
require.False(t, ok)
|
||||
require.Contains(t, err.Error(), "callback must return a string or nil")
|
||||
}
|
||||
|
||||
func TestRunRenderAURHooksReceiveIndependentEventTables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
event.data.name = "mutated"
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
return event.data.name
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
rendered, ok, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "pkgA", rendered)
|
||||
}
|
||||
|
||||
func TestRunRenderAURLastHookWins(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Sub-case 1: second hook returns "B" — last non-nil wins.
|
||||
t.Run("second_returns_B", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event) return "A" end,
|
||||
})
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event) return "B" end,
|
||||
})
|
||||
`))
|
||||
|
||||
rendered, ok, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "B", rendered)
|
||||
})
|
||||
|
||||
// Sub-case 2: second hook returns nil — first hook's "A" is retained.
|
||||
t.Run("second_returns_nil_keeps_first", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event) return "A" end,
|
||||
})
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event) return nil end,
|
||||
})
|
||||
`))
|
||||
|
||||
rendered, ok, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "A", rendered)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunRenderAURAbort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderAUR", {
|
||||
callback = function(event)
|
||||
yay.abort("blocked")
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
_, _, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.EqualError(t, err, "RenderAUR: blocked")
|
||||
}
|
||||
|
||||
func TestRunRenderSyncEventShapeAndStringReturn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("RenderSync", {
|
||||
callback = function(event)
|
||||
if event.event ~= "RenderSync" then error("bad event name") end
|
||||
local d = event.data
|
||||
if d.repository ~= "extra" then error("bad repository") end
|
||||
if d.name ~= "mypkg" then error("bad name") end
|
||||
if d.version ~= "2.0-1" then error("bad version") end
|
||||
if d.groups[1] ~= "base" then error("bad groups[1]") end
|
||||
if d.local_version ~= "1.9-2" then error("bad local_version") end
|
||||
return "SYNC:" .. d.name .. "/" .. d.repository
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
rendered, ok, err := e.RunRenderSync(&RenderSyncEvent{
|
||||
Repository: "extra",
|
||||
Name: "mypkg",
|
||||
Description: "My sync package",
|
||||
Version: "2.0-1",
|
||||
Groups: []string{"base"},
|
||||
LocalVersion: "1.9-2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "SYNC:mypkg/extra", rendered)
|
||||
}
|
||||
|
||||
func TestRunRenderNoHookReturnsFalse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("aur", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
rendered, ok, err := e.RunRenderAUR(&RenderAUREvent{Name: "pkgA"})
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, "", rendered)
|
||||
})
|
||||
|
||||
t.Run("sync", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
rendered, ok, err := e.RunRenderSync(&RenderSyncEvent{Name: "mypkg"})
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, "", rendered)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user