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 |
|
| `glyph` | `glyph` | `device-gamepad-2` | Bar widget icon |
|
||||||
| `steampoacher_enabled` | `bool` | `false` | Enable steampoacher proxy for Steam cover art |
|
| `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
|
## Security & Data Flow
|
||||||
|
|
||||||
|
|||||||
+86
-30
@@ -6,6 +6,8 @@ local errorMsg = ""
|
|||||||
local scanCounter = 0
|
local scanCounter = 0
|
||||||
local building = false
|
local building = false
|
||||||
local initOk = true
|
local initOk = true
|
||||||
|
local selectedIndex = 0
|
||||||
|
local WINDOW_SIZE = 4
|
||||||
|
|
||||||
local steamCoverDir = ""
|
local steamCoverDir = ""
|
||||||
local heroicCoverDir = ""
|
local heroicCoverDir = ""
|
||||||
@@ -46,6 +48,16 @@ local function isValidProtocol(cmd)
|
|||||||
return false
|
return false
|
||||||
end
|
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)
|
local function launchGame(id)
|
||||||
for _, g in ipairs(filtered) do
|
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
|
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
|
end
|
||||||
|
|
||||||
local function filterGames(text)
|
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
|
if text == "" then
|
||||||
filtered = games
|
filtered = pool
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local lower = text:lower()
|
local lower = text:lower()
|
||||||
local result = {}
|
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
|
if g.name:lower():find(lower, 1, true) or g.runner:lower():find(lower, 1, true) then
|
||||||
table.insert(result, g)
|
table.insert(result, g)
|
||||||
end
|
end
|
||||||
@@ -237,6 +253,7 @@ end
|
|||||||
|
|
||||||
local function renderGameRow(g, index)
|
local function renderGameRow(g, index)
|
||||||
local meta = runnerMeta[g.runner] or { glyph = "app-window", color = "on_surface_variant" }
|
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
|
local cp = (g.cover and #g.cover > 0) and g.cover or nil
|
||||||
if not cp then
|
if not cp then
|
||||||
if g.runner == "steam" then
|
if g.runner == "steam" then
|
||||||
@@ -251,15 +268,7 @@ local function renderGameRow(g, index)
|
|||||||
else
|
else
|
||||||
coverWidget = ui.box({ width = 56, height = 80, radius = 6, fill = "surface_variant" })
|
coverWidget = ui.box({ width = 56, height = 80, radius = 6, fill = "surface_variant" })
|
||||||
end
|
end
|
||||||
return ui.row({
|
local children = {
|
||||||
key = g.id .. "_" .. index,
|
|
||||||
align = "center",
|
|
||||||
gap = 14,
|
|
||||||
paddingH = 12,
|
|
||||||
paddingV = 8,
|
|
||||||
radius = 8,
|
|
||||||
border = 0,
|
|
||||||
}, {
|
|
||||||
coverWidget,
|
coverWidget,
|
||||||
ui.column({ flexGrow = 1, gap = 4 }, {
|
ui.column({ flexGrow = 1, gap = 4 }, {
|
||||||
ui.label({ text = g.name, fontWeight = "bold", fontSize = 14, maxLines = 1 }),
|
ui.label({ text = g.name, fontWeight = "bold", fontSize = 14, maxLines = 1 }),
|
||||||
@@ -274,13 +283,40 @@ local function renderGameRow(g, index)
|
|||||||
variant = "primary",
|
variant = "primary",
|
||||||
onClick = function() launchGame(g.id) end,
|
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
|
end
|
||||||
|
|
||||||
local function renderGameList()
|
local function renderGameList()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
for i, g in ipairs(filtered) do
|
local total = #filtered
|
||||||
table.insert(rows, renderGameRow(g, i))
|
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
|
end
|
||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
@@ -307,12 +343,12 @@ end
|
|||||||
|
|
||||||
local function render()
|
local function render()
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
panel.render(ui.column({ flexGrow = 1, gap = 8 }, {
|
panel.render(ui.column({ flexGrow = 1, gap = 8, padding = 12 }, {
|
||||||
ui.column({ padding = 12, gap = 8 }, {
|
ui.column({ gap = 8 }, {
|
||||||
renderHeader(),
|
renderHeader(),
|
||||||
renderSearchBar(),
|
renderSearchBar(),
|
||||||
}),
|
}),
|
||||||
ui.scroll({ flexGrow = 1, gap = 6, paddingH = 12 }, renderBody(renderGameList())),
|
ui.column({ flexGrow = 1, gap = 6 }, renderBody(renderGameList())),
|
||||||
}))
|
}))
|
||||||
end)
|
end)
|
||||||
if not ok then
|
if not ok then
|
||||||
@@ -357,7 +393,11 @@ local function scanGames()
|
|||||||
scanCounter = scanCounter + 1
|
scanCounter = scanCounter + 1
|
||||||
searchText = ""
|
searchText = ""
|
||||||
render()
|
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()
|
local ok3, err3 = pcall(function()
|
||||||
loading = false
|
loading = false
|
||||||
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
|
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
|
||||||
@@ -377,13 +417,15 @@ local function scanGames()
|
|||||||
end
|
end
|
||||||
games = {}
|
games = {}
|
||||||
end
|
end
|
||||||
filtered = games
|
selectedIndex = 0
|
||||||
|
filterGames(searchText)
|
||||||
end)
|
end)
|
||||||
if not ok3 then
|
if not ok3 then
|
||||||
loading = false
|
loading = false
|
||||||
errorMsg = "Scan error: " .. tostring(err3)
|
errorMsg = "Scan error: " .. tostring(err3)
|
||||||
games = {}
|
games = {}
|
||||||
filtered = games
|
filtered = games
|
||||||
|
selectedIndex = 0
|
||||||
noctalia.log("scan callback error: " .. tostring(err3))
|
noctalia.log("scan callback error: " .. tostring(err3))
|
||||||
end
|
end
|
||||||
render()
|
render()
|
||||||
@@ -397,18 +439,10 @@ end
|
|||||||
|
|
||||||
|
|
||||||
function onOpen(context)
|
function onOpen(context)
|
||||||
if #games == 0 then
|
selectedIndex = 0
|
||||||
scanGames()
|
filterGames(searchText)
|
||||||
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
|
|
||||||
render()
|
render()
|
||||||
|
scanGames()
|
||||||
end
|
end
|
||||||
|
|
||||||
function onClose()
|
function onClose()
|
||||||
@@ -421,6 +455,28 @@ end
|
|||||||
|
|
||||||
function onSearch(value)
|
function onSearch(value)
|
||||||
searchText = value or ""
|
searchText = value or ""
|
||||||
|
selectedIndex = 0
|
||||||
filterGames(searchText)
|
filterGames(searchText)
|
||||||
render()
|
render()
|
||||||
end
|
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"
|
id = "alexander/game-launcher"
|
||||||
name = "Game Launcher"
|
name = "Game Launcher"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
plugin_api = 9
|
plugin_api = 13
|
||||||
author = "Alexander"
|
author = "Alexander"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
icon = "device-gamepad-2"
|
icon = "device-gamepad-2"
|
||||||
@@ -27,6 +27,7 @@ height = 520
|
|||||||
placement = "floating"
|
placement = "floating"
|
||||||
position = "center"
|
position = "center"
|
||||||
open_near_click = true
|
open_near_click = true
|
||||||
|
capture_keys = ["Up", "Down", "Return"]
|
||||||
|
|
||||||
[[panel.setting]]
|
[[panel.setting]]
|
||||||
key = "steampoacher_enabled"
|
key = "steampoacher_enabled"
|
||||||
@@ -34,6 +35,24 @@ type = "bool"
|
|||||||
label_key = "settings.steampoacher_enabled.label"
|
label_key = "settings.steampoacher_enabled.label"
|
||||||
default = false
|
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]]
|
[[launcher_provider]]
|
||||||
id = "search"
|
id = "search"
|
||||||
entry = "search.luau"
|
entry = "search.luau"
|
||||||
|
|||||||
@@ -3,9 +3,23 @@ local ready = false
|
|||||||
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
||||||
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
|
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 ensureGames(cb)
|
||||||
local function runScan()
|
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
|
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
|
||||||
local parsed, err = noctalia.json.decode(res.stdout)
|
local parsed, err = noctalia.json.decode(res.stdout)
|
||||||
if parsed then
|
if parsed then
|
||||||
@@ -46,7 +60,7 @@ function onQuery(text)
|
|||||||
local lower = text:lower()
|
local lower = text:lower()
|
||||||
local results = {}
|
local results = {}
|
||||||
for _, g in ipairs(games) do
|
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 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" }
|
local m = meta[g.runner] or { glyph = "app-window", color = "on_surface" }
|
||||||
table.insert(results, {
|
table.insert(results, {
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
},
|
},
|
||||||
"steampoacher_enabled": {
|
"steampoacher_enabled": {
|
||||||
"label": "Steampoacher proxy for covers"
|
"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