* Add game-launcher plugin * auto-build: compile gamelauncher.c on first run * rename leo -> Alexander * add cc to dependencies for CI * fix CI issues: lowercase id, prefix, translations, getConfig * fix: revert prefix to g (noctalia prepends / via provider_prefix) * game-launcher: fix all 4 security audit issues, cache deletion crash, covers not showing, close button - Issue 1: Removed all system/popen/curl/wget/python3/grep from C scanner - Issue 2: Protocol URL validation + character-level filtering + shell escaping - Issue 3: Added xdg-utils to plugin.toml dependencies - Issue 4: Steampoacher opt-in setting, documented data flow in README - Fix: Plugin crash after cache deletion (pcall-wrapped all error paths) - Fix: Close button uses noctalia.togglePanel instead of panel.close - Fix: onOpen rescans if any game missing a cover - Fix: Cover display fallback path reconstruction * game-launcher: revert close button to panel.close * game-launcher: note steampoacher needed for HQ covers * game-launcher: shorten steampoacher note * 1. Refactored README by hand **Stupid AI** 2. deleted the binary , it will be built with plugin open * fix: Remove the character-by-character sanitization
83 lines
2.3 KiB
Luau
83 lines
2.3 KiB
Luau
local games = {}
|
|
local ready = false
|
|
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
|
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
|
|
|
|
local function ensureGames(cb)
|
|
local function runScan()
|
|
local ok = noctalia.runAsync(binary .. " --steam --lutris --heroic --force", function(res)
|
|
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
|
|
local parsed, err = noctalia.json.decode(res.stdout)
|
|
if parsed then
|
|
games = parsed
|
|
ready = true
|
|
end
|
|
end
|
|
cb()
|
|
end)
|
|
if not ok then cb() end
|
|
end
|
|
if ready then
|
|
cb()
|
|
return
|
|
end
|
|
if not noctalia.fileExists(binary) then
|
|
local ok = noctalia.runAsync("/usr/bin/cc -o " .. binary .. " " .. cSource .. " -lsqlite3", function(res)
|
|
if res.exitCode == 0 then
|
|
runScan()
|
|
else
|
|
cb()
|
|
end
|
|
end)
|
|
if not ok then cb() end
|
|
return
|
|
end
|
|
runScan()
|
|
end
|
|
|
|
function onQuery(text)
|
|
if text == "" then
|
|
launcher.setResults(text, {
|
|
{ id = "hint", title = "Type a game name to search", glyph = "device-gamepad-2" },
|
|
})
|
|
return
|
|
end
|
|
ensureGames(function()
|
|
local lower = text:lower()
|
|
local results = {}
|
|
for _, g in ipairs(games) do
|
|
if g.name:lower():find(lower, 1, true) then
|
|
local meta = { steam = { glyph = "brand-steam", color = "steam" }, lutris = { glyph = "device-gamepad", color = "warning" }, heroic = { glyph = "app-window", color = "heroic" } }
|
|
local m = meta[g.runner] or { glyph = "app-window", color = "on_surface" }
|
|
table.insert(results, {
|
|
id = g.id,
|
|
title = g.name,
|
|
subtitle = "Launch via " .. g.runner,
|
|
glyph = m.glyph,
|
|
})
|
|
end
|
|
end
|
|
launcher.setResults(text, results)
|
|
end)
|
|
end
|
|
|
|
local function isValidProtocol(cmd)
|
|
local protocols = { "steam://", "lutris:", "heroic://" }
|
|
for _, p in ipairs(protocols) do
|
|
if cmd:sub(1, #p) == p then
|
|
local rest = cmd:sub(#p + 1)
|
|
if rest:match("^[%w_%-%.%/]+$") then return true end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function onActivate(id)
|
|
for _, g in ipairs(games) do
|
|
if g.id == id and g.run_command and #g.run_command > 0 and isValidProtocol(g.run_command) then
|
|
noctalia.runAsync('xdg-open "' .. g.run_command .. '"')
|
|
return
|
|
end
|
|
end
|
|
end
|