diff --git a/godot-provider/README.md b/godot-provider/README.md new file mode 100644 index 0000000..8a1f152 --- /dev/null +++ b/godot-provider/README.md @@ -0,0 +1,43 @@ +# Godot Provider + +![Godot Provider thumbnail](thumbnail.webp) + +Godot Provider integrates all projects within the project manager with the +Noctalia launcher so you can open a project quickly without leaving the shell. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `ramosdetrigo/godot-provider` | +| Entry | Launcher provider: `provider` | +| Launcher Prefix | `/gd` | + +## Requirements + +Install [Godot](https://godotengine.org/) and ensure `godot` is available on +`PATH` as `godot`. Alternatively, you can set the path to the godot binary in +the plugin's settings. The `nohup` command is required to launch Godot. + +## Usage + +Open the Noctalia launcher and type `/gd` to list local Godot projects. +Continue typing to filter projects by name, then select one to open it with +`godot`. + +## Settings + +- `sort_by` — Sort projects by last used or most opened projects in the launcher. +- `projects_path` — The path to Godot's projets.cfg file. +- `godot_command` — The shell command to open godot. + +## Notes + +Projects are read from Godot's projects.cfg file, typically at +`~/.local/share/godot/projets.cfg`. The list is cached for the launcher session +and refreshed when you clear the query. + +The `sort_by` setting sorts by writing the project's ids in a json inside the plugins' +[`pluginDataDir`](https://docs.noctalia.dev/v5/plugins/development/runtime-api/?section=filesystem#filesystem). + +Small parts of the code were taken or based upon the [zed-provider](https://github.com/noctalia-dev/community-plugins/tree/main/zed-provider) plugin. diff --git a/godot-provider/godot_provider.luau b/godot-provider/godot_provider.luau new file mode 100644 index 0000000..029b97e --- /dev/null +++ b/godot-provider/godot_provider.luau @@ -0,0 +1,170 @@ +local HOME = noctalia.getenv("HOME") or "" +local tinsert = table.insert +local tremove = table.remove +local tfind = table.find +local tsort = table.sort +local sformat = string.format + +local projectsCache +local projectsPath: string = noctalia.getConfig("projects_path") or noctalia.expandPath("~/.local/share/godot/projects.cfg") +local godot_command: string = noctalia.getConfig("godot_command") or "godot" +local sort_by: string = noctalia.getConfig("sort_by") or "history" +local dataDir = noctalia.pluginDataDir() + +if godot_command == "" then godot_command = "godot" end + +local function shellQuote(s: string): string + return "'" .. s:gsub("'", "'\"'\"'") .. "'" +end + +local function trim(s: string): string + return s:match("^%s*(.-)%s*$") :: string +end + +-- Gets the project name in project.godot - fallbacks to the folder name +local function projectName(path: string): string + local p_godot: string = noctalia.readFile(path .. "/project.godot") or "" + local project_name = p_godot:match('config/name="(.-)"%s*\n') + + return project_name and project_name:gsub("\\(.)", "%1") or path:match("([^/]+)$") or path +end + +-- Get table with projects +local function getProjects(file_string: string, query: string) + local projects = {} + + -- Gets the projects inside the projects.cfg (They're wrapped in "[]") + for project_path in file_string:gmatch("%[([^%]]+)%]") do + local project_name: string = projectName(project_path) + tinsert(projects, { + id = project_path, + title = project_name, + subtitle = project_path:gsub(HOME, "~"), + icon = "godot", + }) + end + + -- sort alphabetically + tsort(projects, function(a, b) return a.title < b.title end) + + return projects +end + +-- filter the projects based on the query with fuzzyScore +function filterProjects(query, projects) + local result = {} + for i = 1, #projects do + local project = projects[i] + + -- score from title or id + local project_score: number? + + if query == "" then + -- query empty: sort based on the sort_by key + project_score = getSortScore(project.id) + else + project_score = noctalia.fuzzyScore(query, project.title) or noctalia.fuzzyScore(query, project.id) + end + + if query == "" or project_score ~= nil then + project.score = project_score -- this will be overriden every time so no problem + tinsert(result, project) + end + end + + return result +end + +-- sends launcher results, warning the user if no projects found +function showResults(query, projects) + if #projects > 0 then + launcher.setResults(query, projects) + return + end + + launcher.setResults(query, { + { + id = "", + title = noctalia.tr("no-projects-found"), + subtitle = noctalia.tr("no-projects-subtitle"), + glyph = "loader", + }, + }) +end + +function onQuery(query: string) + query = trim(query) + + -- resets cache on new prompt + if query == "" or projectsCache == nil then + local file = noctalia.readFile(projectsPath) + projectsCache = if file then getProjects(file, query) else {} + end + + launcher.setResults(query, filterProjects(query, projectsCache)) +end + +function getSortScore(id: string): number + if id == "" then + return 0 + end + + if dataDir then + if sort_by == "history" then + local file_path = dataDir .. "/history.json" + local file = noctalia.readFile(file_path) + local data = {} + if file then + data = noctalia.json.decode(file) + local i = tfind(data, id) or #data + 1 + local score = #data - i + return score + end + else -- usage_count + local file_path = dataDir .. "/usage.json" + local file = noctalia.readFile(file_path) + if file then + return noctalia.json.decode(file)[id] or 0 + end + end + end + + return 0 +end + +-- Open godot editor on the selected project +function onActivate(id: string) + if id == "" then + return + end + + if dataDir then + local file: string? + local file_path: string = dataDir .. (sort_by == "history" and "/history.json" or "/usage.json") + file = noctalia.readFile(file_path) + + if sort_by == "history" then + local data = {} + if file then data = noctalia.json.decode(file) end + + local i: number? = tfind(data, id) + if i then + tremove(data, i) + tinsert(data, 1, id) + else + tinsert(data, 1, id) + end + + local json_str: string = noctalia.json.encode(data) :: string + noctalia.writeFile(file_path, json_str) + else -- usage_count + local data = {} + if file then data = noctalia.json.decode(file) end + data[id] = data[id] and data[id] + 1 or 1 + local json_str = noctalia.json.encode(data) + noctalia.writeFile(file_path, json_str) + end + end + + noctalia.runAsync(sformat("nohup %s -e %s &>/dev/null &", shellQuote(godot_command), shellQuote(id.."/project.godot"))) +end diff --git a/godot-provider/plugin.toml b/godot-provider/plugin.toml new file mode 100644 index 0000000..bef8d45 --- /dev/null +++ b/godot-provider/plugin.toml @@ -0,0 +1,43 @@ +id = "ramosdetrigo/godot-provider" +name = "Godot Provider" +author = "ramosdetrigo" +version = "1.0.0" +plugin_api = 3 +license = "MIT" +dependencies = ["godot", "nohup"] +icon = "folder-open" +description = "Open a recent Godot project from the launcher. Type /gd to list your projects." +tags = ["launcher", "development", "productivity"] + +[[setting]] +key = "sort_by" +type = "select" +label_key = "settings.sort_by.label" +description_key = "settings.sort_by.description" +default = "history" +options = [ + {label_key="settings.history", value = "history"}, + {label_key="settings.usage_count", value = "usage_count"}, +] + +[[setting]] +key = "projects_path" +type = "file" +label_key = "settings.projects_path.label" +description_key = "settings.projects_path.description" +default = "~/.local/share/godot/projects.cfg" + +[[setting]] +key = "godot_command" +type = "string" +label_key = "settings.godot_command.label" +description_key = "settings.godot_command.description" +default = "godot" + +[[launcher_provider]] +id = "provider" +entry = "godot_provider.luau" +prefix = "gd" +glyph = "robot" +include_in_global_search = false +debounce_ms = 0 diff --git a/godot-provider/thumbnail.webp b/godot-provider/thumbnail.webp new file mode 100644 index 0000000..c5f276d Binary files /dev/null and b/godot-provider/thumbnail.webp differ diff --git a/godot-provider/translations/en.json b/godot-provider/translations/en.json new file mode 100644 index 0000000..6a1a028 --- /dev/null +++ b/godot-provider/translations/en.json @@ -0,0 +1,20 @@ +{ + "no-projects-found": "No projects found", + "no-projects-subtitle": "No godot projects.cfg or empty file", + "settings": { + "history": "History", + "usage_count": "Usage count", + "projects_path": { + "description": "Path to Godot's projets.cfg file.", + "label": "Godot projets.cfg path" + }, + "sort_by": { + "description": "Sort by last used or most opened projects.", + "label": "Sort by" + }, + "godot_command": { + "description": "The shell command to open godot.", + "label": "Godot command" + } + } +} diff --git a/godot-provider/translations/pt-BR.json b/godot-provider/translations/pt-BR.json new file mode 100644 index 0000000..ae33e37 --- /dev/null +++ b/godot-provider/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "no-projects-found": "Nenhum projeto foi encontrado.", + "no-projects-subtitle": "O arquivo projects.cfg não existe ou está vazio.", + "settings": { + "history": "Histórico", + "usage_count": "Número de usos", + "projects_path": { + "description": "Caminho pro arquivo projets.cfg da Godot.", + "label": "Caminho do projets.cfg" + }, + "sort_by": { + "description": "Ordenar por histórico de uso ou número de usos.", + "label": "Ordenar por" + }, + "godot_command": { + "description": "O comando shell pra abrir a Godot.", + "label": "Comando godot" + } + } +}