diff --git a/obs-integration/README.md b/obs-integration/README.md index d2abe5c..a1b9abd 100644 --- a/obs-integration/README.md +++ b/obs-integration/README.md @@ -64,8 +64,8 @@ its actions: - **Service Remote Run** — runs `osc service rr` on the OBS server for the package on the remote project. No local code executes; the request triggers server-side service runs, and a confirmation is not required. -- **Rebuild package** — pick an architecture (or **All**) and trigger - `osc rebuild`. Confirmation is required before it runs. +- **Rebuild package** — pick a repository and architecture (or **All**) and trigger `osc rebuild`. Confirmation is required before it runs. +- **Build Status** — Shows real-time build results for the package across all repositories and architectures. Click **Refresh status** to pull latest results. - **Edit package meta** — `osc meta pkg -e` in a terminal. - **Remove package** — deletes the checkout from disk only (never touches the OBS project); confirm before it runs. If it was the last package in the @@ -93,3 +93,7 @@ operation opens a terminal only when `osc status` reports pending changes; otherwise, it runs asynchronously in the panel. Confirmation prompts precede destructive and side-effecting operations: Rebuild package, Remove package, and Service Run All. + +Build status data is fetched from `osc results` and cached for the session. The +status display shares the same underlying `osc results` call used for rebuild +architecture selection, so enabling a package view pulls both in one query. diff --git a/obs-integration/panel.luau b/obs-integration/panel.luau index 42fd196..8e8d8dd 100644 --- a/obs-integration/panel.luau +++ b/obs-integration/panel.luau @@ -21,6 +21,7 @@ local rebuildRepoIndex = 0 local rebuildConfirm = false local runAllConfirm = false local repoMap = {} +local statusMap = {} local savedRebuildRepo = nil local savedRebuildArch = nil local rebuildLoaded = false @@ -275,17 +276,25 @@ loadPackageFiles = function(pkgDir, pkg) end, 2000) end -local function loadRebuildArches(project, pkg) + local function loadRebuildArches(project, pkg) rebuildLoaded = false + rebuildRepos = {} + repoMap = {} + statusMap = {} noctalia.runAsync("osc results " .. shellQuote(project) .. " " .. shellQuote(pkg), function(result) local map = {} + local statuses = {} if result.exitCode == 0 then for line in tostring(result.stdout):gmatch("[^\n]+") do line = noctalia.string.trim(line) local repo, arch, _, status = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)") + if status then + status = status:gsub("%*$", "") + end if repo ~= nil and arch ~= nil and status ~= "disabled" then if not map[repo] then map[repo] = {} + statuses[repo] = {} end local found = false for _, a in ipairs(map[repo]) do @@ -297,6 +306,7 @@ local function loadRebuildArches(project, pkg) if not found then table.insert(map[repo], arch) end + statuses[repo][arch] = status end end end @@ -309,6 +319,7 @@ local function loadRebuildArches(project, pkg) table.sort(map[repo]) end repoMap = map + statusMap = statuses rebuildRepos = repos local repoIndex = 0 if savedRebuildRepo then @@ -336,6 +347,50 @@ local function loadRebuildArches(project, pkg) end, 15000) end +local STATUS_STYLE = { + succeeded = { glyph = "check", color = "secondary", labelKey = "status_succeeded" }, + failure = { glyph = "x", color = "error", labelKey = "status_failed" }, + broken = { glyph = "alert-circle", color = "error", labelKey = "status_failed" }, + unresolvable = { glyph = "alert-circle", color = "error", labelKey = "status_unresolvable" }, + building = { glyph = "loader", color = "primary", labelKey = "status_building" }, + dispatching = { glyph = "loader", color = "primary", labelKey = "status_building" }, + scheduled = { glyph = "clock", color = "on_surface_variant", labelKey = "status_scheduled" }, + signing = { glyph = "lock", color = "warning", labelKey = "status_signing" }, + blocked = { glyph = "clock", color = "on_surface_variant", labelKey = "status_blocked" }, + excluded = { glyph = "ban", color = "on_surface_variant", labelKey = "status_excluded" }, + unbuildable = { glyph = "ban", color = "on_surface_variant", labelKey = "status_unbuildable" }, + finished = { glyph = "check", color = "secondary", labelKey = "status_succeeded" }, + unknown = { glyph = "help-circle", color = "on_surface_variant", labelKey = "status_unknown" }, +} + +local STATUS_GROUPS = { + ["succeeded"] = "succeeded", + ["success"] = "succeeded", + ["finished"] = "succeeded", + ["failed"] = "failure", + ["broken"] = "failure", + ["unresolvable"] = "unresolvable", + ["building"] = "building", + ["dispatching"] = "building", + ["scheduled"] = "scheduled", + ["signing"] = "signing", + ["blocked"] = "blocked", + ["excluded"] = "excluded", + ["unbuildable"] = "unbuildable", + ["unknown"] = "unknown", + ["disabled"] = "disabled", +} + +local function statusPresentation(status) + status = status or "unknown" + local group = STATUS_GROUPS[status] or status + local base = STATUS_STYLE[group] + if base then + return base.glyph, base.color, noctalia.tr("panel." .. base.labelKey), group + end + return "help-circle", "on_surface_variant", status, "unknown" +end + local function removeFileLocal(path, pkgDir, pkg) noctalia.runAsync("rm " .. shellQuote(path), function(result) if result.exitCode == 0 then @@ -831,6 +886,7 @@ function render() rebuildRepos = {} rebuildRepoIndex = 0 repoMap = {} + statusMap = {} savedRebuildRepo = nil savedRebuildArch = nil rebuildLoaded = false @@ -1262,7 +1318,7 @@ function render() }), ui.button({ key = "rebuild-btn", - glyph = rebuildConfirm and "check" or "refresh", + glyph = rebuildConfirm and "check" or "analyze-filled", tooltip = rebuildConfirm and noctalia.tr("panel.confirm_rebuild") or noctalia.tr("panel.rebuild"), variant = rebuildConfirm and "secondary" or "default", onClick = function() @@ -1297,6 +1353,58 @@ function render() end end + table.insert(body, ui.separator({ thickness = 1, spacing = 6 })) + + local statusChildren = { + ui.row({ align = "center", gap = 8 }, { + ui.glyph({ name = "activity", color = "primary", size = 16 }), + ui.label({ text = noctalia.tr("panel.build_status"), fontWeight = "bold", fontSize = 15, flexGrow = 1 }), + ui.button({ + glyph = "refresh", + tooltip = noctalia.tr("panel.status_refresh"), + variant = "ghost", + controlSize = "sm", + onClick = function() + loadRebuildArches(selectedProject, selectedPackage) + end, + }), + }), + } + if not rebuildLoaded then + table.insert(statusChildren, ui.row({ align = "center", gap = 8, paddingV = 2 }, { + ui.glyph({ name = "loader", color = "primary", size = 14 }), + ui.label({ text = noctalia.tr("panel.status_loading"), color = "on_surface_variant" }), + })) + else + local hadResults = false + for _, repo in ipairs(rebuildRepos) do + local archs = repoMap[repo] or {} + if #archs > 0 then + hadResults = true + table.insert( + statusChildren, + ui.label({ text = repo, fontWeight = "bold", fontSize = 14, color = "on_surface" }) + ) + for _, arch in ipairs(archs) do + local status = statusMap[repo] and statusMap[repo][arch] or nil + local glyph, color, label = statusPresentation(status) + table.insert( + statusChildren, + ui.row({ align = "center", gap = 8, paddingL = 12 }, { + ui.glyph({ name = glyph, color = color, size = 14 }), + ui.label({ text = arch, fontSize = 13, color = "on_surface", flexGrow = 1 }), + ui.label({ text = label, fontSize = 13, color = color }), + }) + ) + end + end + end + if not hadResults then + table.insert(statusChildren, ui.label({ text = noctalia.tr("panel.no_builds"), color = "on_surface_variant" })) + end + end + table.insert(body, ui.column({ gap = 4 }, statusChildren)) + elseif selectedProject then table.insert( body, diff --git a/obs-integration/plugin.toml b/obs-integration/plugin.toml index a9b6160..ffc2e36 100644 --- a/obs-integration/plugin.toml +++ b/obs-integration/plugin.toml @@ -1,6 +1,6 @@ id = "neyfua/obs-integration" name = "OBS Integration" -version = "1.0.0" +version = "1.1.0" plugin_api = 9 author = "neyfua" license = "MIT" diff --git a/obs-integration/translations/en.json b/obs-integration/translations/en.json index 9d0488f..4d67c6a 100644 --- a/obs-integration/translations/en.json +++ b/obs-integration/translations/en.json @@ -26,10 +26,6 @@ "no_files": "No files found.", "no_packages": "No packages found.", "no_projects": "No projects found.", - "osc_missing": "'osc' not found", - "osc_missing_hint": "Install the 'osc' package to start using this plugin.", - "osc_not_configured": "OBS account not configured", - "osc_not_configured_hint": "Create ~/.config/osc/oscrc before browsing projects.", "rebuild": "Rebuild package", "rebuilding_pkg": "Rebuilding package...", "reload_projects": "Reload projects", @@ -47,6 +43,24 @@ "service_remote_run": "Service Remote Run", "service_run": "Service Manual Run", "service_run_all": "Service Run All", + "osc_missing": "'osc' not found", + "osc_missing_hint": "Install the 'osc' package to start using this plugin.", + "osc_not_configured": "OBS account not configured", + "osc_not_configured_hint": "Create ~/.config/osc/oscrc before browsing projects.", + "build_status": "Build Status", + "status_refresh": "Refresh status", + "status_loading": "Loading status...", + "no_builds": "No build results.", + "status_succeeded": "Succeeded", + "status_failed": "Failed", + "status_unresolvable": "Unresolvable", + "status_building": "Building", + "status_scheduled": "Scheduled", + "status_signing": "Signing", + "status_blocked": "Blocked", + "status_excluded": "Excluded", + "status_unbuildable": "Unbuildable", + "status_unknown": "Unknown", "update_changes": "Update {name}.changes", "update_dir": "Update directory", "updating_dir": "Updating directory..."