From d24622c9fab674e3d872bd62fd905d11f9cbeddd Mon Sep 17 00:00:00 2001 From: Ahmed Emad <122463699+Ahmed5Emad@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:47:27 +0300 Subject: [PATCH] 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 --- game-launcher/README.md | 3 + game-launcher/panel.luau | 116 +++++++++++++++++++++-------- game-launcher/plugin.toml | 23 +++++- game-launcher/search.luau | 18 ++++- game-launcher/translations/en.json | 9 +++ 5 files changed, 135 insertions(+), 34 deletions(-) diff --git a/game-launcher/README.md b/game-launcher/README.md index a9a794c..ebae4cb 100644 --- a/game-launcher/README.md +++ b/game-launcher/README.md @@ -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 diff --git a/game-launcher/panel.luau b/game-launcher/panel.luau index 8333827..f64ab42 100644 --- a/game-launcher/panel.luau +++ b/game-launcher/panel.luau @@ -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 diff --git a/game-launcher/plugin.toml b/game-launcher/plugin.toml index b060d8f..f9ce718 100644 --- a/game-launcher/plugin.toml +++ b/game-launcher/plugin.toml @@ -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" diff --git a/game-launcher/search.luau b/game-launcher/search.luau index 5c6c074..f1560ea 100644 --- a/game-launcher/search.luau +++ b/game-launcher/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, { diff --git a/game-launcher/translations/en.json b/game-launcher/translations/en.json index 0ef458e..86789e1 100644 --- a/game-launcher/translations/en.json +++ b/game-launcher/translations/en.json @@ -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" } } }