diff --git a/prismlauncher-instances/README.md b/prismlauncher-instances/README.md new file mode 100644 index 0000000..54b7844 --- /dev/null +++ b/prismlauncher-instances/README.md @@ -0,0 +1,6 @@ +# PrismLauncher Instances +![prismlauncher instances thumbnail](thumbnail.webp) +**PrismLauncher Instances** is a plugin that adds PrismLauncher instances to the noctalia launcher. + +## Requirements +- [prismlauncher](https://github.com/PrismLauncher/PrismLauncher) diff --git a/prismlauncher-instances/plugin.toml b/prismlauncher-instances/plugin.toml new file mode 100644 index 0000000..0395d42 --- /dev/null +++ b/prismlauncher-instances/plugin.toml @@ -0,0 +1,26 @@ +id = "radimous/prismlauncher-instances" +name = "PrismLauncher Instances" +version = "1.0.0" +min_noctalia = "5.0.0" +author = "radimous" +license = "MIT" +dependencies = ["prismlauncher"] +icon = "nut" +deprecated = false +description = "A launcher provider that adds PrismLauncher instances to the noctalia launcher." +tags = ["gaming", "launcher"] + +[[launcher_provider]] +id = "prismlauncher-instances" +entry = "prismlauncher-instances.luau" +prefix = "pl" +glyph = "nut" +include_in_global_search = false +debounce_ms = 0 + +[[setting]] +key = "prism_path" +type = "string" +label_key = "settings.prismlauncher_path.label" +description_key = "settings.prismlauncher_path.description" +default = "~/.local/share/PrismLauncher" diff --git a/prismlauncher-instances/prismlauncher-instances.luau b/prismlauncher-instances/prismlauncher-instances.luau new file mode 100644 index 0000000..59331c0 --- /dev/null +++ b/prismlauncher-instances/prismlauncher-instances.luau @@ -0,0 +1,177 @@ +--!nonstrict + +local function getPrismPath() + local prismPath = noctalia.getConfig("prism_path") + return noctalia.expandPath(prismPath) +end + +local function getInstancesDir() + local prismPath = getPrismPath() + + local configuredInstanceDir = getCfgValue(prismPath .. "/prismlauncher.cfg", "InstanceDir") + + local instances + if configuredInstanceDir == nil then -- default path + instances = prismPath .. "/instances" + elseif configuredInstanceDir:sub(1, 1) == "/" then -- absolute path + instances = configuredInstanceDir + else -- relative path + instances = prismPath .. "/" .. configuredInstanceDir + end + return instances +end + +local function loadInstances() + local instances = {} + local list = noctalia.listDir(getInstancesDir()) + if list ~= nil then + for _, name in ipairs(list) do + local instanceDir = getInstancesDir() .. "/" .. name + if name ~= "" and noctalia.fileExists(instanceDir .. "/instance.cfg") then -- same check as prism to only show instance dirs + table.insert(instances, name) + end + end + end + return instances +end + +local function instanceName(path) + local instanceCfg = getInstancesDir() .. "/" .. path .. "/instance.cfg" + local configuredName = getCfgValue(instanceCfg, "name") + if configuredName ~= nil then + return configuredName + end + return path:match("([^/]+)$") or path +end + +local function filterInstances(instances, query) + if query == "" then return instances end + local results = {} + for _, path in instances do + local insName = instanceName(path) + if insName:lower():find(query:lower(), 1, true) then + table.insert(results, path) + end + end + return results +end + +local function makeRows(instances) + local rows = {} + for _, path in instances do + table.insert(rows, { + id = path, + title = instanceName(path), + subtitle = path, + icon = getIcon(path), + glyph = "nut", + }) + end + return rows +end + +local function showResults(query, instances, filter) + local filtered = filterInstances(instances, filter) + if #filtered == 0 then + launcher.setResults(query, { + { id = "", title = noctalia.tr("no_instances.title"), subtitle = filter ~= "" and noctalia.tr("no_instances.subtitle") .. filter, glyph = "folder-x" } + }) + else + launcher.setResults(query, makeRows(filtered)) + end +end + +function onQuery(query) + local filter = noctalia.string.trim(query) + showResults(query, loadInstances(), filter) +end + +local function shellQuote(s) + return "'" .. s:gsub("'", "'\"'\"'") .. "'" +end + +function onActivate(id) + if id == "" then return end + noctalia.runAsync(string.format("prismlauncher --launch %s &>/dev/null", shellQuote(id))) +end + +-- doesn't handle icons that were changed to different default icon, rest is fine +-- never change icons since creating instance => OK +-- change icon to manually provided icon or to icon from mod provider => OK +-- change icon to different default icon baked into prism binary => NOK, old icon will be shown because /minecraft/icon.png isn't updated +function getIcon(instancePath) + local instanceFullPath = getInstancesDir() .. "/" .. instancePath + local defaultIcon = instanceFullPath .. "/minecraft/icon.png" + local cfgPath = instanceFullPath .. "/instance.cfg" + local iconVal = getCfgValue(cfgPath, "iconKey") + if (iconVal == nil) then + return defaultIcon + end + + local iconDir = getPrismPath() .. "/icons/" + local bestIcon = findBestIconIn(iconDir, iconVal) + + if (bestIcon == nil) then + return defaultIcon + end + return bestIcon +end + + + +-- https://github.com/PrismLauncher/PrismLauncher/blob/d2fa7cf7f7aa8fa5e3d4f5f7b474621c732cd525/launcher/icons/IconUtils.cpp +local validIconExtensions = { "svg", "png", "ico", "gif", "jpg", "jpeg", "webp" } + +local function isIconSuffix(suffix) + for _, ext in ipairs(validIconExtensions) do + if ext == suffix then + return true + end + end + return false +end + +local function splitNameAndSuffix(filename) + local dotIndex = filename:find("%.[^.]*$") + if not dotIndex then + return filename, "" + end + local base = filename:sub(1, dotIndex - 1) + local suffix = filename:sub(dotIndex + 1) + return base, suffix +end + +function findBestIconIn(folder, iconKey) + local entries = noctalia.listDir(folder) + if not entries then + return nil + end + + for _, filename in ipairs(entries) do + local baseName, suffix = splitNameAndSuffix(filename) + if (baseName == iconKey or filename == iconKey) and isIconSuffix(suffix) then + local sep = folder:sub(-1) == "/" and "" or "/" + return folder .. sep .. filename + end + end + + return nil +end + + + +function getCfgValue(cfgPath, key) -- naive INI value fetcher, doesn't handle sections, it's enough for this usecase + local content, err = noctalia.readFile(cfgPath) + if not content then + noctalia.log("Failed to read file ".. cfgPath .. " : " .. tostring(err)) + return nil + end + + for line in content:gmatch("[^\r\n]+") do + if line:sub(1, #key + 1) == key .. "=" then + return line:sub(#key + 2) + end + end + + return nil +end diff --git a/prismlauncher-instances/thumbnail.webp b/prismlauncher-instances/thumbnail.webp new file mode 100644 index 0000000..f2765e0 Binary files /dev/null and b/prismlauncher-instances/thumbnail.webp differ diff --git a/prismlauncher-instances/translations/en.json b/prismlauncher-instances/translations/en.json new file mode 100644 index 0000000..b8e4959 --- /dev/null +++ b/prismlauncher-instances/translations/en.json @@ -0,0 +1,12 @@ +{ + "settings": { + "prismlauncher_path": { + "label": "PrismLauncher path", + "description": "Path to PrismLauncher" + } + }, + "no_instances": { + "title": "No instances found", + "subtitle": "Filter: " + } +}