Game Launcher V1.1.0: add control over luanchers and keyboard navigation (#140)
* game-launcher: add launcher toggles, keyboard nav, auto-refresh * game-launcher: update README with runner toggle settings
This commit is contained in:
@@ -47,6 +47,9 @@ From the launcher, type `/g` followed by a game name to search. Activate a resul
|
||||
| --- | --- | --- | --- |
|
||||
| `glyph` | `glyph` | `device-gamepad-2` | Bar widget icon |
|
||||
| `steampoacher_enabled` | `bool` | `false` | Enable steampoacher proxy for Steam cover art |
|
||||
| `steam_enabled` | `bool` | `true` | Show Steam games |
|
||||
| `heroic_enabled` | `bool` | `true` | Show Heroic games |
|
||||
| `lutris_enabled` | `bool` | `true` | Show Lutris games |
|
||||
|
||||
## Security & Data Flow
|
||||
|
||||
|
||||
+86
-30
@@ -6,6 +6,8 @@ local errorMsg = ""
|
||||
local scanCounter = 0
|
||||
local building = false
|
||||
local initOk = true
|
||||
local selectedIndex = 0
|
||||
local WINDOW_SIZE = 4
|
||||
|
||||
local steamCoverDir = ""
|
||||
local heroicCoverDir = ""
|
||||
@@ -46,6 +48,16 @@ local function isValidProtocol(cmd)
|
||||
return false
|
||||
end
|
||||
|
||||
local function isRunnerEnabled(runner)
|
||||
local ok, val = pcall(noctalia.getConfig, runner .. "_enabled")
|
||||
if ok then return val == true or val == "true" end
|
||||
return true
|
||||
end
|
||||
|
||||
local function filterByRunner(g)
|
||||
return isRunnerEnabled(g.runner)
|
||||
end
|
||||
|
||||
local function launchGame(id)
|
||||
for _, g in ipairs(filtered) do
|
||||
if g.id == id and g.run_command and #g.run_command > 0 and isValidProtocol(g.run_command) then
|
||||
@@ -56,13 +68,17 @@ local function launchGame(id)
|
||||
end
|
||||
|
||||
local function filterGames(text)
|
||||
local pool = {}
|
||||
for _, g in ipairs(games) do
|
||||
if filterByRunner(g) then table.insert(pool, g) end
|
||||
end
|
||||
if text == "" then
|
||||
filtered = games
|
||||
filtered = pool
|
||||
return
|
||||
end
|
||||
local lower = text:lower()
|
||||
local result = {}
|
||||
for _, g in ipairs(games) do
|
||||
for _, g in ipairs(pool) do
|
||||
if g.name:lower():find(lower, 1, true) or g.runner:lower():find(lower, 1, true) then
|
||||
table.insert(result, g)
|
||||
end
|
||||
@@ -237,6 +253,7 @@ end
|
||||
|
||||
local function renderGameRow(g, index)
|
||||
local meta = runnerMeta[g.runner] or { glyph = "app-window", color = "on_surface_variant" }
|
||||
local isSelected = (index - 1) == selectedIndex
|
||||
local cp = (g.cover and #g.cover > 0) and g.cover or nil
|
||||
if not cp then
|
||||
if g.runner == "steam" then
|
||||
@@ -251,15 +268,7 @@ local function renderGameRow(g, index)
|
||||
else
|
||||
coverWidget = ui.box({ width = 56, height = 80, radius = 6, fill = "surface_variant" })
|
||||
end
|
||||
return ui.row({
|
||||
key = g.id .. "_" .. index,
|
||||
align = "center",
|
||||
gap = 14,
|
||||
paddingH = 12,
|
||||
paddingV = 8,
|
||||
radius = 8,
|
||||
border = 0,
|
||||
}, {
|
||||
local children = {
|
||||
coverWidget,
|
||||
ui.column({ flexGrow = 1, gap = 4 }, {
|
||||
ui.label({ text = g.name, fontWeight = "bold", fontSize = 14, maxLines = 1 }),
|
||||
@@ -274,13 +283,40 @@ local function renderGameRow(g, index)
|
||||
variant = "primary",
|
||||
onClick = function() launchGame(g.id) end,
|
||||
}),
|
||||
})
|
||||
}
|
||||
if isSelected then
|
||||
table.insert(children, 1, ui.box({ width = 2, height = 60, radius = 1, fill = "primary" }))
|
||||
return ui.row({
|
||||
key = g.id .. "_sel_" .. index,
|
||||
align = "center",
|
||||
gap = 14,
|
||||
paddingH = 12,
|
||||
paddingV = 8,
|
||||
radius = 8,
|
||||
}, children)
|
||||
end
|
||||
return ui.row({
|
||||
key = g.id .. "_" .. index,
|
||||
align = "center",
|
||||
gap = 14,
|
||||
paddingH = 12,
|
||||
paddingV = 8,
|
||||
radius = 8,
|
||||
}, children)
|
||||
end
|
||||
|
||||
local function renderGameList()
|
||||
local rows = {}
|
||||
for i, g in ipairs(filtered) do
|
||||
table.insert(rows, renderGameRow(g, i))
|
||||
local total = #filtered
|
||||
if total == 0 then return rows end
|
||||
local half = math.floor(WINDOW_SIZE / 2)
|
||||
local start = math.max(1, selectedIndex + 1 - half)
|
||||
if start + WINDOW_SIZE > total then
|
||||
start = math.max(1, total - WINDOW_SIZE + 1)
|
||||
end
|
||||
local endIdx = math.min(total, start + WINDOW_SIZE - 1)
|
||||
for i = start, endIdx do
|
||||
table.insert(rows, renderGameRow(filtered[i], i))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
@@ -307,12 +343,12 @@ end
|
||||
|
||||
local function render()
|
||||
local ok, err = pcall(function()
|
||||
panel.render(ui.column({ flexGrow = 1, gap = 8 }, {
|
||||
ui.column({ padding = 12, gap = 8 }, {
|
||||
panel.render(ui.column({ flexGrow = 1, gap = 8, padding = 12 }, {
|
||||
ui.column({ gap = 8 }, {
|
||||
renderHeader(),
|
||||
renderSearchBar(),
|
||||
}),
|
||||
ui.scroll({ flexGrow = 1, gap = 6, paddingH = 12 }, renderBody(renderGameList())),
|
||||
ui.column({ flexGrow = 1, gap = 6 }, renderBody(renderGameList())),
|
||||
}))
|
||||
end)
|
||||
if not ok then
|
||||
@@ -357,7 +393,11 @@ local function scanGames()
|
||||
scanCounter = scanCounter + 1
|
||||
searchText = ""
|
||||
render()
|
||||
local ok = noctalia.runAsync(binary .. " --steam --lutris --heroic --force", function(res)
|
||||
local flags = ""
|
||||
if isRunnerEnabled("steam") then flags = flags .. " --steam" end
|
||||
if isRunnerEnabled("lutris") then flags = flags .. " --lutris" end
|
||||
if isRunnerEnabled("heroic") then flags = flags .. " --heroic" end
|
||||
local ok = noctalia.runAsync(binary .. flags .. " --force", function(res)
|
||||
local ok3, err3 = pcall(function()
|
||||
loading = false
|
||||
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
|
||||
@@ -377,13 +417,15 @@ local function scanGames()
|
||||
end
|
||||
games = {}
|
||||
end
|
||||
filtered = games
|
||||
selectedIndex = 0
|
||||
filterGames(searchText)
|
||||
end)
|
||||
if not ok3 then
|
||||
loading = false
|
||||
errorMsg = "Scan error: " .. tostring(err3)
|
||||
games = {}
|
||||
filtered = games
|
||||
selectedIndex = 0
|
||||
noctalia.log("scan callback error: " .. tostring(err3))
|
||||
end
|
||||
render()
|
||||
@@ -397,18 +439,10 @@ end
|
||||
|
||||
|
||||
function onOpen(context)
|
||||
if #games == 0 then
|
||||
scanGames()
|
||||
return
|
||||
end
|
||||
for _, g in ipairs(games) do
|
||||
if (g.runner == "steam" or g.runner == "heroic") and (not g.cover or #g.cover == 0) then
|
||||
scanGames()
|
||||
return
|
||||
end
|
||||
end
|
||||
filtered = games
|
||||
selectedIndex = 0
|
||||
filterGames(searchText)
|
||||
render()
|
||||
scanGames()
|
||||
end
|
||||
|
||||
function onClose()
|
||||
@@ -421,6 +455,28 @@ end
|
||||
|
||||
function onSearch(value)
|
||||
searchText = value or ""
|
||||
selectedIndex = 0
|
||||
filterGames(searchText)
|
||||
render()
|
||||
end
|
||||
|
||||
function onKey(chord, pressed)
|
||||
if not pressed then return end
|
||||
if chord == "Up" then
|
||||
if #filtered > 0 then
|
||||
selectedIndex = selectedIndex - 1
|
||||
if selectedIndex < 0 then selectedIndex = #filtered - 1 end
|
||||
render()
|
||||
end
|
||||
elseif chord == "Down" then
|
||||
if #filtered > 0 then
|
||||
selectedIndex = selectedIndex + 1
|
||||
if selectedIndex >= #filtered then selectedIndex = 0 end
|
||||
render()
|
||||
end
|
||||
elseif chord == "Return" then
|
||||
if #filtered > 0 and selectedIndex >= 0 and selectedIndex < #filtered then
|
||||
launchGame(filtered[selectedIndex + 1].id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "alexander/game-launcher"
|
||||
name = "Game Launcher"
|
||||
version = "1.0.1"
|
||||
plugin_api = 9
|
||||
version = "1.1.0"
|
||||
plugin_api = 13
|
||||
author = "Alexander"
|
||||
license = "MIT"
|
||||
icon = "device-gamepad-2"
|
||||
@@ -27,6 +27,7 @@ height = 520
|
||||
placement = "floating"
|
||||
position = "center"
|
||||
open_near_click = true
|
||||
capture_keys = ["Up", "Down", "Return"]
|
||||
|
||||
[[panel.setting]]
|
||||
key = "steampoacher_enabled"
|
||||
@@ -34,6 +35,24 @@ type = "bool"
|
||||
label_key = "settings.steampoacher_enabled.label"
|
||||
default = false
|
||||
|
||||
[[panel.setting]]
|
||||
key = "steam_enabled"
|
||||
type = "bool"
|
||||
label_key = "settings.steam_enabled.label"
|
||||
default = true
|
||||
|
||||
[[panel.setting]]
|
||||
key = "heroic_enabled"
|
||||
type = "bool"
|
||||
label_key = "settings.heroic_enabled.label"
|
||||
default = true
|
||||
|
||||
[[panel.setting]]
|
||||
key = "lutris_enabled"
|
||||
type = "bool"
|
||||
label_key = "settings.lutris_enabled.label"
|
||||
default = true
|
||||
|
||||
[[launcher_provider]]
|
||||
id = "search"
|
||||
entry = "search.luau"
|
||||
|
||||
@@ -3,9 +3,23 @@ local ready = false
|
||||
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
||||
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
|
||||
|
||||
local function isRunnerEnabled(runner)
|
||||
local ok, val = pcall(noctalia.getConfig, runner .. "_enabled")
|
||||
if ok then return val == true or val == "true" end
|
||||
return true
|
||||
end
|
||||
|
||||
local function getRunnerFlags()
|
||||
local flags = ""
|
||||
if isRunnerEnabled("steam") then flags = flags .. " --steam" end
|
||||
if isRunnerEnabled("lutris") then flags = flags .. " --lutris" end
|
||||
if isRunnerEnabled("heroic") then flags = flags .. " --heroic" end
|
||||
return flags
|
||||
end
|
||||
|
||||
local function ensureGames(cb)
|
||||
local function runScan()
|
||||
local ok = noctalia.runAsync(binary .. " --steam --lutris --heroic --force", function(res)
|
||||
local ok = noctalia.runAsync(binary .. getRunnerFlags() .. " --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
|
||||
@@ -46,7 +60,7 @@ function onQuery(text)
|
||||
local lower = text:lower()
|
||||
local results = {}
|
||||
for _, g in ipairs(games) do
|
||||
if g.name:lower():find(lower, 1, true) then
|
||||
if g.name:lower():find(lower, 1, true) and isRunnerEnabled(g.runner) 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, {
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
},
|
||||
"steampoacher_enabled": {
|
||||
"label": "Steampoacher proxy for covers"
|
||||
},
|
||||
"steam_enabled": {
|
||||
"label": "Show Steam games"
|
||||
},
|
||||
"heroic_enabled": {
|
||||
"label": "Show Heroic games"
|
||||
},
|
||||
"lutris_enabled": {
|
||||
"label": "Show Lutris games"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user