* feat(obs-integration): add OBS integration plugin * docs(obs-integration): add README.md * chore(obs-integration): rename obs-integration-thumbnail.webp to thumbnail.webp * i18n(obs-integration): fix PR check * fix(obs-integration): honor selected rebuild architecture * fix(obs-integration): reject dot-prefixed names in validName * fix(obs-integration): treat empty checkout_dir as unset * feat(obs-integration): add repo/arch rebuild selection with confirmation * chore(obs-integration): add opensuse tag to plugin.toml * chore(obs-integration): new thumbnail * fix(obs-integration): fix rebuild controls inheriting flexGrow from reused nodes * feat(obs-integration): overhaul package actions, logging, and navigation * chore(obs-integration): new thumbnail * feat(obs-integration): label Service Run All button, move build logs * fix(obs-integration): split logs based on action source * fix(obs-integration): correct log display locations and resets * feat(obs-integration): add confirmation to remove file * feat(obs-integration): move edit package meta to header * fix(obs-integration): layout repo/arch label/select alignment * fix(obs-integration): clean rebuild log duplication * fix(obs-integration): persist logSource in nav state for panel reopen * i18n(obs-integration): change 'Repo' to 'Repositories' and 'Arch' to 'Architectures' * feat(obs-integration): add home nav button, fix header label clipping, rebuild controls layout * chore(obs-integration): change plugin's panel height * docs(obs-integration): document source-service operations in README * fix(obs-integration): correct rebuild log display and persistence, sync UI layout --------- Co-authored-by: neyfua <ogkhang.05@gmail.com>
1499 lines
40 KiB
Luau
1499 lines
40 KiB
Luau
--!nonstrict
|
|
|
|
local OSC_CONFIG = noctalia.expandPath("~/.config/osc/oscrc")
|
|
local CACHE_FILE = "projects_cache.json"
|
|
local NAV_FILE = "nav_state.json"
|
|
|
|
local projects = {}
|
|
local packages = {}
|
|
local selectedProject = nil
|
|
local selectedPackage = nil
|
|
local packageFilter = ""
|
|
local projectFilter = ""
|
|
local sortAsc = true
|
|
local editFilesOpen = false
|
|
local filesOpen = false
|
|
local filesPackage = nil
|
|
local packageFiles = {}
|
|
local rebuildArchIndex = 0
|
|
local rebuildRepos = {}
|
|
local rebuildRepoIndex = 0
|
|
local rebuildConfirm = false
|
|
local runAllConfirm = false
|
|
local repoMap = {}
|
|
local savedRebuildRepo = nil
|
|
local savedRebuildArch = nil
|
|
local rebuildLoaded = false
|
|
local isCheckingOut = false
|
|
local checkoutLog = ""
|
|
local isActionLoading = false
|
|
local actionStatus = ""
|
|
local logSource = ""
|
|
local loadPackageFiles
|
|
local resolveCheckoutDir
|
|
local maintainerMap = {}
|
|
local ownedProjects = {}
|
|
local projectsLoading = false
|
|
local packagesLoading = false
|
|
local errorText = ""
|
|
local commandLog = ""
|
|
local logPackage = nil
|
|
local pendingProject = nil
|
|
local removeConfirm = false
|
|
local removeFileConfirm = false
|
|
local removeFileTarget = nil
|
|
local restoreProject = nil
|
|
local restorePackage = nil
|
|
local restoreFilesOpen = false
|
|
local restoreFilesPackage = nil
|
|
local restoreCommandLog = nil
|
|
local restoreErrorText = nil
|
|
local restoreLogSource = nil
|
|
local saveCache
|
|
local saveNav
|
|
local maybeVerify
|
|
|
|
local ownedDone = false
|
|
local oscUser = nil
|
|
local oscWhoPending = false
|
|
|
|
local function shellQuote(value)
|
|
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
|
|
end
|
|
|
|
local function commandError(result)
|
|
return noctalia.string.trim(result.stderr ~= "" and result.stderr or result.stdout)
|
|
end
|
|
|
|
local function setCommandLog(result, refreshFiles, emptyMsg)
|
|
local err = commandError(result)
|
|
if result.exitCode ~= 0 then
|
|
errorText = err ~= "" and err or ("Command failed (exit " .. tostring(result.exitCode) .. ")")
|
|
commandLog = ""
|
|
else
|
|
errorText = ""
|
|
commandLog = noctalia.string.trim(result.stdout)
|
|
if commandLog == "" and emptyMsg then
|
|
commandLog = emptyMsg
|
|
end
|
|
if refreshFiles then
|
|
local checkoutDir = resolveCheckoutDir()
|
|
loadPackageFiles(checkoutDir .. "/" .. selectedProject .. "/" .. selectedPackage, selectedPackage)
|
|
end
|
|
end
|
|
logPackage = selectedPackage
|
|
render()
|
|
saveNav()
|
|
end
|
|
|
|
local function showCommandLog(result)
|
|
setCommandLog(result, false)
|
|
end
|
|
|
|
local function runActionAsync(cmd, statusText, refreshFiles, pkgDir, pkg, emptyMsg, source)
|
|
isActionLoading = true
|
|
actionStatus = statusText
|
|
logSource = source or ""
|
|
errorText = ""
|
|
commandLog = ""
|
|
render()
|
|
noctalia.runAsync(cmd, function(result)
|
|
isActionLoading = false
|
|
if refreshFiles and result.exitCode == 0 then
|
|
local dir = pkgDir
|
|
if not dir then
|
|
local checkoutDir = resolveCheckoutDir()
|
|
dir = checkoutDir .. "/" .. selectedProject .. "/" .. selectedPackage
|
|
end
|
|
loadPackageFiles(dir, pkg or selectedPackage)
|
|
end
|
|
setCommandLog(result, false, emptyMsg)
|
|
end, 60000)
|
|
end
|
|
|
|
-- OBS project/package identifiers: reject anything that could escape the
|
|
-- checkout dir or inject into shell commands (slashes, leading dots, spaces).
|
|
local function validName(value)
|
|
return type(value) == "string"
|
|
and value ~= ""
|
|
and not value:find("/", 1, true)
|
|
and value:match("^[%w:.+%-_]+$") ~= nil
|
|
and value:match("^%.") == nil
|
|
end
|
|
|
|
resolveCheckoutDir = function()
|
|
local checkoutDir = noctalia.getConfig("checkout_dir")
|
|
if type(checkoutDir) ~= "string" or checkoutDir:match("^%s*$") then
|
|
checkoutDir = "~/OBS"
|
|
end
|
|
return noctalia.expandPath(checkoutDir)
|
|
end
|
|
|
|
|
|
-- Estimate the combined width (px) of the two .changes buttons to decide
|
|
-- whether they fit side-by-side or must stack.
|
|
local function changesButtonsFit(pkg)
|
|
-- Rough per-char width at the button font size, plus glyph+padding budget.
|
|
local charW = 7
|
|
local base = 16 + 24
|
|
local editW = (#"Edit " + #pkg + #".changes") * charW + base
|
|
local updateW = (#"Update " + #pkg + #".changes") * charW + base
|
|
return editW + updateW + 8 <= 396
|
|
end
|
|
|
|
local function parseLines(stdout)
|
|
local out = {}
|
|
for line in tostring(stdout or ""):gmatch("[^\n]+") do
|
|
line = noctalia.string.trim(line)
|
|
if line ~= "" then
|
|
table.insert(out, line)
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function filterList(list, filter, ascending)
|
|
local lower = filter:lower()
|
|
local filtered
|
|
if filter == "" then
|
|
filtered = list
|
|
else
|
|
filtered = {}
|
|
for _, name in ipairs(list) do
|
|
if name:lower():find(lower, 1, true) then
|
|
table.insert(filtered, name)
|
|
end
|
|
end
|
|
end
|
|
table.sort(filtered, function(a, b)
|
|
if ascending then
|
|
return a < b
|
|
else
|
|
return a > b
|
|
end
|
|
end)
|
|
return filtered
|
|
end
|
|
|
|
local function configureOsc()
|
|
noctalia.runInTerminal("osc version")
|
|
end
|
|
|
|
local function checkoutPackage(project, pkg)
|
|
if not validName(project) or not validName(pkg) then
|
|
return
|
|
end
|
|
local checkoutDir = resolveCheckoutDir()
|
|
local pkgPath = checkoutDir .. "/" .. project .. "/" .. pkg
|
|
isCheckingOut = true
|
|
checkoutLog = noctalia.tr("panel.checking_out")
|
|
render()
|
|
|
|
noctalia.runAsync(
|
|
"cd " .. shellQuote(checkoutDir) .. " && osc co " .. shellQuote(project) .. " " .. shellQuote(pkg),
|
|
function(result)
|
|
isCheckingOut = false
|
|
if result.exitCode == 0 then
|
|
errorText = ""
|
|
loadPackageFiles(pkgPath, pkg)
|
|
else
|
|
errorText = commandError(result)
|
|
end
|
|
render()
|
|
saveNav()
|
|
end,
|
|
60000
|
|
)
|
|
end
|
|
|
|
local function editProjectMeta(project)
|
|
if not validName(project) then
|
|
return
|
|
end
|
|
noctalia.runInTerminal("osc meta prj " .. shellQuote(project) .. " -e")
|
|
end
|
|
|
|
local function editPackageMeta(project, pkg)
|
|
if not validName(project) or not validName(pkg) then
|
|
return
|
|
end
|
|
noctalia.runInTerminal("osc meta pkg " .. shellQuote(project) .. " " .. shellQuote(pkg) .. " -e")
|
|
end
|
|
|
|
local function rebuildPackage(project, pkg, repo, arch)
|
|
if not validName(project) or not validName(pkg) then
|
|
return
|
|
end
|
|
local cmd = "osc rebuild " .. shellQuote(project) .. " " .. shellQuote(pkg)
|
|
if repo ~= nil and repo ~= "All" and repo ~= "" then
|
|
cmd = cmd .. " -r " .. shellQuote(repo)
|
|
end
|
|
if arch ~= nil and arch ~= "All" and arch ~= "" then
|
|
cmd = cmd .. " -a " .. shellQuote(arch)
|
|
end
|
|
runActionAsync(cmd, noctalia.tr("panel.rebuilding_pkg"), false, nil, nil, nil, "rebuild")
|
|
end
|
|
|
|
loadPackageFiles = function(pkgDir, pkg)
|
|
noctalia.runAsync(
|
|
"find " .. shellQuote(pkgDir) .. " -maxdepth 1 -not -name '.*' -printf '%f\\n'",
|
|
function(result)
|
|
local list = {}
|
|
if result.exitCode == 0 then
|
|
for _, f in ipairs(parseLines(result.stdout)) do
|
|
if f ~= pkg and f ~= ".osc" then
|
|
table.insert(list, f)
|
|
end
|
|
end
|
|
end
|
|
table.sort(list)
|
|
packageFiles = list
|
|
render()
|
|
end,
|
|
5000
|
|
)
|
|
-- Re-fetch after a short delay so callers that just created the dir (checkout,
|
|
-- osc up, service runs) settle into the complete file list.
|
|
noctalia.runAsync("sleep 1", function()
|
|
noctalia.runAsync(
|
|
"find " .. shellQuote(pkgDir) .. " -maxdepth 1 -not -name '.*' -printf '%f\\n'",
|
|
function(result)
|
|
local list = {}
|
|
if result.exitCode == 0 then
|
|
for _, f in ipairs(parseLines(result.stdout)) do
|
|
if f ~= pkg and f ~= ".osc" then
|
|
table.insert(list, f)
|
|
end
|
|
end
|
|
end
|
|
table.sort(list)
|
|
packageFiles = list
|
|
render()
|
|
end,
|
|
5000
|
|
)
|
|
end, 2000)
|
|
end
|
|
|
|
local function loadRebuildArches(project, pkg)
|
|
rebuildLoaded = false
|
|
noctalia.runAsync("osc results " .. shellQuote(project) .. " " .. shellQuote(pkg), function(result)
|
|
local map = {}
|
|
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 repo ~= nil and arch ~= nil and status ~= "disabled" then
|
|
if not map[repo] then
|
|
map[repo] = {}
|
|
end
|
|
local found = false
|
|
for _, a in ipairs(map[repo]) do
|
|
if a == arch then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
if not found then
|
|
table.insert(map[repo], arch)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local repos = {}
|
|
for repo in pairs(map) do
|
|
table.insert(repos, repo)
|
|
end
|
|
table.sort(repos)
|
|
for repo in pairs(map) do
|
|
table.sort(map[repo])
|
|
end
|
|
repoMap = map
|
|
rebuildRepos = repos
|
|
local repoIndex = 0
|
|
if savedRebuildRepo then
|
|
for i, repo in ipairs(repos) do
|
|
if repo == savedRebuildRepo then
|
|
repoIndex = i
|
|
break
|
|
end
|
|
end
|
|
end
|
|
rebuildRepoIndex = repoIndex
|
|
local archIndex = 0
|
|
local repoArchs = savedRebuildRepo and map[savedRebuildRepo] or {}
|
|
if savedRebuildArch then
|
|
for i, arch in ipairs(repoArchs or {}) do
|
|
if arch == savedRebuildArch then
|
|
archIndex = i
|
|
break
|
|
end
|
|
end
|
|
end
|
|
rebuildArchIndex = archIndex
|
|
rebuildLoaded = true
|
|
render()
|
|
end, 15000)
|
|
end
|
|
|
|
local function removeFileLocal(path, pkgDir, pkg)
|
|
noctalia.runAsync("rm " .. shellQuote(path), function(result)
|
|
if result.exitCode == 0 then
|
|
loadPackageFiles(pkgDir, pkg)
|
|
else
|
|
errorText = commandError(result)
|
|
render()
|
|
end
|
|
end, 15000)
|
|
end
|
|
|
|
local function editFilePlain(path)
|
|
noctalia.runInTerminal("${EDITOR:-nano} " .. shellQuote(path))
|
|
end
|
|
|
|
local function editFile(path, pkgDir, pkg, filename)
|
|
if not validName(pkg) then
|
|
return
|
|
end
|
|
if filename:match("%.changes$") then
|
|
noctalia.runInTerminal("cd " .. shellQuote(pkgDir) .. " && osc vc")
|
|
else
|
|
editFilePlain(path)
|
|
end
|
|
end
|
|
|
|
local function oscAddRemove(pkgDir)
|
|
runActionAsync(
|
|
"cd " .. shellQuote(pkgDir) .. " && osc ar",
|
|
noctalia.tr("panel.adding_removing"),
|
|
true,
|
|
pkgDir,
|
|
nil,
|
|
noctalia.tr("panel.no_changes", { name = selectedPackage })
|
|
)
|
|
end
|
|
|
|
local function oscCommit(pkgDir)
|
|
local cmd = "cd " .. shellQuote(pkgDir) .. " && osc ci"
|
|
isActionLoading = true
|
|
actionStatus = noctalia.tr("panel.committing")
|
|
logSource = ""
|
|
errorText = ""
|
|
commandLog = ""
|
|
render()
|
|
noctalia.runAsync("cd " .. shellQuote(pkgDir) .. " && osc status", function(result)
|
|
local output = result.stdout .. "\n" .. result.stderr
|
|
local hasChanges = result.exitCode == 0 and noctalia.string.trim(output) ~= ""
|
|
isActionLoading = false
|
|
if hasChanges then
|
|
noctalia.runInTerminal(cmd)
|
|
else
|
|
noctalia.runAsync(cmd, showCommandLog, 60000)
|
|
end
|
|
end, 15000)
|
|
end
|
|
|
|
local function oscUpdate(pkgDir)
|
|
runActionAsync(
|
|
"cd " .. shellQuote(pkgDir) .. " && osc up",
|
|
noctalia.tr("panel.updating_dir"),
|
|
true,
|
|
pkgDir
|
|
)
|
|
end
|
|
|
|
local function oscServiceRun(pkgDir)
|
|
runActionAsync(
|
|
"cd " .. shellQuote(pkgDir) .. " && osc service mr",
|
|
noctalia.tr("panel.running_manual_service"),
|
|
true,
|
|
pkgDir
|
|
)
|
|
end
|
|
|
|
local function oscServiceRemoteRun(project, pkg)
|
|
runActionAsync(
|
|
"osc service rr " .. shellQuote(project) .. " " .. shellQuote(pkg),
|
|
noctalia.tr("panel.running_remote_service"),
|
|
false
|
|
)
|
|
end
|
|
|
|
local function oscServiceLocalRun(pkgDir)
|
|
runActionAsync(
|
|
"cd " .. shellQuote(pkgDir) .. " && osc service r",
|
|
noctalia.tr("panel.running_local_service"),
|
|
true,
|
|
pkgDir
|
|
)
|
|
end
|
|
|
|
local function oscServiceRunAll(pkgDir)
|
|
runActionAsync(
|
|
"cd " .. shellQuote(pkgDir) .. " && osc service ra",
|
|
noctalia.tr("panel.running_all_services"),
|
|
true,
|
|
pkgDir
|
|
)
|
|
end
|
|
|
|
local function removePackageLocal(project, pkg)
|
|
if not validName(project) or not validName(pkg) then
|
|
return
|
|
end
|
|
local checkoutDir = resolveCheckoutDir()
|
|
local projectDir = checkoutDir .. "/" .. project
|
|
local pkgDir = projectDir .. "/" .. pkg
|
|
local cmd = "rm -rf " .. shellQuote(pkgDir)
|
|
.. "\nif ! ls -A " .. shellQuote(projectDir) .. " | grep -q -v '^\\.osc$'; then rm -rf " .. shellQuote(projectDir) .. "; fi"
|
|
noctalia.runAsync(cmd, function(result)
|
|
if result.exitCode ~= 0 then
|
|
errorText = commandError(result)
|
|
end
|
|
selectedPackage = nil
|
|
removeConfirm = false
|
|
render()
|
|
saveNav()
|
|
end, 15000)
|
|
end
|
|
|
|
local function loadPackages(project)
|
|
selectedProject = project
|
|
selectedPackage = nil
|
|
packageFilter = ""
|
|
editFilesOpen = false
|
|
filesOpen = false
|
|
packageFiles = {}
|
|
removeConfirm = false
|
|
rebuildConfirm = false
|
|
runAllConfirm = false
|
|
isCheckingOut = false
|
|
checkoutLog = ""
|
|
isActionLoading = false
|
|
actionStatus = ""
|
|
logSource = ""
|
|
packages = maintainerMap[project] or {}
|
|
|
|
if #packages == 0 and projectsLoading then
|
|
pendingProject = project
|
|
packagesLoading = true
|
|
render()
|
|
return
|
|
end
|
|
|
|
if #packages == 0 and ownedProjects[project] then
|
|
packagesLoading = true
|
|
render()
|
|
noctalia.runAsync("osc ls " .. shellQuote(project), function(result)
|
|
packagesLoading = false
|
|
if result.exitCode == 0 then
|
|
packages = parseLines(result.stdout)
|
|
maintainerMap[project] = packages
|
|
saveCache()
|
|
errorText = ""
|
|
elseif errorText == "" then
|
|
errorText = commandError(result)
|
|
end
|
|
render()
|
|
end, 15000)
|
|
saveNav()
|
|
return
|
|
end
|
|
|
|
packagesLoading = false
|
|
errorText = ""
|
|
render()
|
|
saveNav()
|
|
end
|
|
|
|
local function cachePath()
|
|
local dir, _ = noctalia.pluginDataDir()
|
|
if not dir then
|
|
return nil
|
|
end
|
|
return dir .. "/" .. CACHE_FILE
|
|
end
|
|
|
|
local function navPath()
|
|
local dir, _ = noctalia.pluginDataDir()
|
|
if not dir then
|
|
return nil
|
|
end
|
|
return dir .. "/" .. NAV_FILE
|
|
end
|
|
|
|
saveNav = function()
|
|
local path = navPath()
|
|
if not path then
|
|
return
|
|
end
|
|
noctalia.writeFile(path, noctalia.json.encode({
|
|
project = selectedProject,
|
|
package = selectedPackage,
|
|
filesOpen = filesOpen,
|
|
filesPackage = filesPackage,
|
|
commandLog = commandLog,
|
|
errorText = errorText,
|
|
logSource = logSource,
|
|
}))
|
|
end
|
|
local function readNav()
|
|
local path = navPath()
|
|
if not path or not noctalia.fileExists(path) then
|
|
return nil, nil, false, nil, nil, nil
|
|
end
|
|
local data, _ = noctalia.readFile(path)
|
|
if not data then
|
|
return nil, nil, false, nil, nil, nil
|
|
end
|
|
local parsed, _ = noctalia.json.decode(data)
|
|
if type(parsed) ~= "table" then
|
|
return nil, nil, false, nil, nil, nil
|
|
end
|
|
return parsed.project, parsed.package, parsed.filesOpen == true, parsed.filesPackage, parsed.commandLog,
|
|
parsed.errorText, parsed.logSource
|
|
end
|
|
|
|
saveCache = function()
|
|
local path = cachePath()
|
|
if not path or #projects == 0 then
|
|
return
|
|
end
|
|
local ownedList = {}
|
|
for project in pairs(ownedProjects) do
|
|
table.insert(ownedList, project)
|
|
end
|
|
noctalia.writeFile(path, noctalia.json.encode({
|
|
owned = ownedList,
|
|
packages = maintainerMap,
|
|
}))
|
|
end
|
|
|
|
local function prefetchOwnedPackages()
|
|
for project in pairs(ownedProjects) do
|
|
if not maintainerMap[project] then
|
|
noctalia.runAsync("osc ls " .. shellQuote(project), function(result)
|
|
if result.exitCode == 0 and not maintainerMap[project] then
|
|
local list = parseLines(result.stdout)
|
|
maintainerMap[project] = list
|
|
saveCache()
|
|
if selectedProject == project then
|
|
packages = list
|
|
packagesLoading = false
|
|
errorText = ""
|
|
render()
|
|
end
|
|
end
|
|
end, 15000)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function readCache()
|
|
local path = cachePath()
|
|
if not path or not noctalia.fileExists(path) then
|
|
return false
|
|
end
|
|
local data, _ = noctalia.readFile(path)
|
|
if not data then
|
|
return false
|
|
end
|
|
local parsed, _ = noctalia.json.decode(data)
|
|
if type(parsed) ~= "table" then
|
|
return false
|
|
end
|
|
local cachedPkgs = parsed.packages
|
|
local cachedOwned = parsed.owned
|
|
if type(cachedPkgs) ~= "table" or type(cachedOwned) ~= "table" then
|
|
return false
|
|
end
|
|
for key, list in pairs(cachedPkgs) do
|
|
if type(key) ~= "string" or type(list) ~= "table" then
|
|
return false
|
|
end
|
|
end
|
|
maintainerMap = cachedPkgs
|
|
ownedProjects = {}
|
|
for _, project in ipairs(cachedOwned) do
|
|
ownedProjects[project] = true
|
|
end
|
|
projects = {}
|
|
local seen = {}
|
|
for project in pairs(maintainerMap) do
|
|
projects[#projects + 1] = project
|
|
seen[project] = true
|
|
end
|
|
for project in pairs(ownedProjects) do
|
|
if not seen[project] then
|
|
projects[#projects + 1] = project
|
|
end
|
|
end
|
|
table.sort(projects)
|
|
return true
|
|
end
|
|
|
|
local function finalize()
|
|
local merged = {}
|
|
for project in pairs(ownedProjects) do
|
|
merged[project] = true
|
|
end
|
|
for project in pairs(maintainerMap) do
|
|
merged[project] = true
|
|
end
|
|
projects = {}
|
|
for project in pairs(merged) do
|
|
table.insert(projects, project)
|
|
end
|
|
table.sort(projects)
|
|
|
|
saveCache()
|
|
|
|
local deferred = pendingProject
|
|
pendingProject = nil
|
|
projectsLoading = false
|
|
if deferred then
|
|
loadPackages(deferred)
|
|
return
|
|
end
|
|
render()
|
|
prefetchOwnedPackages()
|
|
end
|
|
|
|
local function loadProjects(clearNav)
|
|
readCache()
|
|
|
|
selectedProject = nil
|
|
selectedPackage = nil
|
|
packages = {}
|
|
projectFilter = ""
|
|
pendingProject = nil
|
|
projectsLoading = true
|
|
errorText = ""
|
|
ownedDone = false
|
|
oscUser = nil
|
|
oscWhoPending = false
|
|
|
|
if restoreProject then
|
|
local proj = restoreProject
|
|
local pkg = restorePackage
|
|
local filesOpenRestore = restoreFilesOpen
|
|
local filesPackageRestore = restoreFilesPackage
|
|
restoreProject = nil
|
|
restorePackage = nil
|
|
restoreFilesOpen = false
|
|
restoreFilesPackage = nil
|
|
if proj and (maintainerMap[proj] or ownedProjects[proj]) then
|
|
projectsLoading = false
|
|
loadPackages(proj)
|
|
if pkg then
|
|
selectedPackage = pkg
|
|
filesOpen = filesOpenRestore
|
|
filesPackage = filesPackageRestore
|
|
if restoreCommandLog ~= nil then
|
|
commandLog = restoreCommandLog
|
|
end
|
|
if restoreErrorText ~= nil then
|
|
errorText = restoreErrorText
|
|
end
|
|
if restoreLogSource ~= nil then
|
|
logSource = restoreLogSource
|
|
end
|
|
render()
|
|
saveNav()
|
|
loadRebuildArches(proj, pkg)
|
|
if filesOpen then
|
|
local checkoutDir = resolveCheckoutDir()
|
|
loadPackageFiles(checkoutDir .. "/" .. proj .. "/" .. pkg, pkg)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
render()
|
|
if clearNav then
|
|
saveNav()
|
|
end
|
|
|
|
noctalia.runAsync("osc search --project --maintainer --csv", function(result)
|
|
if result.exitCode == 0 then
|
|
ownedProjects = {}
|
|
for _, line in ipairs(parseLines(result.stdout)) do
|
|
ownedProjects[line] = true
|
|
end
|
|
end
|
|
ownedDone = true
|
|
maybeVerify()
|
|
end)
|
|
|
|
if oscUser == nil then
|
|
oscWhoPending = true
|
|
noctalia.runAsync("osc who | cut -d: -f1", function(userResult)
|
|
if userResult.exitCode == 0 then
|
|
oscUser = noctalia.string.trim(userResult.stdout)
|
|
else
|
|
oscUser = ""
|
|
local err = commandError(userResult)
|
|
if err ~= "" then
|
|
errorText = err
|
|
end
|
|
end
|
|
oscWhoPending = false
|
|
maybeVerify()
|
|
end, 15000)
|
|
end
|
|
end
|
|
|
|
maybeVerify = function()
|
|
if not ownedDone then
|
|
return
|
|
end
|
|
|
|
if oscWhoPending then
|
|
return
|
|
end
|
|
|
|
if oscUser == nil then
|
|
finalize()
|
|
return
|
|
end
|
|
|
|
if oscUser == "" then
|
|
finalize()
|
|
return
|
|
end
|
|
|
|
local url = "/search/package_id?match=person/@userid='" .. oscUser .. "' and person/@role='maintainer'"
|
|
noctalia.runAsync("osc api " .. shellQuote(url), function(verifyResult)
|
|
if verifyResult.exitCode == 0 then
|
|
maintainerMap = {}
|
|
for line in tostring(verifyResult.stdout):gmatch("[^\n]+") do
|
|
local project, name = line:match("<package project='([^']*)' name='([^']*)'")
|
|
if project and name then
|
|
if not maintainerMap[project] then
|
|
maintainerMap[project] = {}
|
|
end
|
|
table.insert(maintainerMap[project], name)
|
|
end
|
|
end
|
|
end
|
|
finalize()
|
|
end, 30000)
|
|
end
|
|
|
|
function render()
|
|
local body = {}
|
|
|
|
if not noctalia.commandExists("osc") then
|
|
body = {
|
|
ui.glyph({ name = "alert-triangle", color = "error", size = 32 }),
|
|
ui.label({ text = noctalia.tr("panel.osc_missing"), fontWeight = "bold", color = "error" }),
|
|
ui.label({ text = noctalia.tr("panel.osc_missing_hint"), color = "on_surface_variant" }),
|
|
}
|
|
elseif not noctalia.fileExists(OSC_CONFIG) then
|
|
body = {
|
|
ui.glyph({ name = "buildings", color = "primary", size = 32 }),
|
|
ui.label({ text = noctalia.tr("panel.osc_not_configured"), fontWeight = "bold" }),
|
|
ui.label({ text = noctalia.tr("panel.osc_not_configured_hint"), color = "on_surface_variant" }),
|
|
ui.button({ text = noctalia.tr("panel.configure_oscrc"), glyph = "terminal", onClick = configureOsc }),
|
|
}
|
|
elseif projectsLoading and #projects == 0 then
|
|
body = {
|
|
ui.glyph({ name = "loader", color = "primary", size = 28 }),
|
|
ui.label({ text = noctalia.tr("panel.loading"), color = "on_surface_variant" }),
|
|
}
|
|
elseif errorText ~= "" and #projects == 0 then
|
|
body = {
|
|
ui.glyph({ name = "alert-circle", color = "error", size = 32 }),
|
|
ui.label({ text = noctalia.tr("panel.error"), fontWeight = "bold", color = "error" }),
|
|
ui.label({ text = errorText, color = "on_surface_variant", wrap = true }),
|
|
ui.button({
|
|
text = noctalia.tr("panel.retry"),
|
|
glyph = "refresh",
|
|
variant = "default",
|
|
onClick = function()
|
|
loadProjects(true)
|
|
end,
|
|
}),
|
|
}
|
|
elseif selectedPackage then
|
|
local checkoutDir = resolveCheckoutDir()
|
|
local checkedOut = noctalia.fileExists(checkoutDir .. "/" .. selectedProject .. "/" .. selectedPackage)
|
|
|
|
local headerChildren = {
|
|
ui.button({
|
|
glyph = "arrow-left",
|
|
variant = "ghost",
|
|
onClick = function()
|
|
selectedPackage = nil
|
|
editFilesOpen = false
|
|
rebuildArchIndex = 0
|
|
rebuildRepos = {}
|
|
rebuildRepoIndex = 0
|
|
repoMap = {}
|
|
savedRebuildRepo = nil
|
|
savedRebuildArch = nil
|
|
rebuildLoaded = false
|
|
rebuildConfirm = false
|
|
removeConfirm = false
|
|
runAllConfirm = false
|
|
isCheckingOut = false
|
|
checkoutLog = ""
|
|
isActionLoading = false
|
|
actionStatus = ""
|
|
render()
|
|
saveNav()
|
|
end,
|
|
}),
|
|
ui.glyph({ name = "package", color = checkedOut and "primary" or "on_surface", size = 18 }),
|
|
ui.label({ text = selectedPackage, fontWeight = "bold", flexGrow = 1 }),
|
|
}
|
|
table.insert(
|
|
headerChildren,
|
|
ui.button({
|
|
glyph = "home-filled",
|
|
tooltip = noctalia.tr("panel.my_projects"),
|
|
variant = "default",
|
|
onClick = function()
|
|
loadProjects(true)
|
|
end,
|
|
})
|
|
)
|
|
table.insert(
|
|
headerChildren,
|
|
ui.button({
|
|
glyph = "feather-filled",
|
|
tooltip = noctalia.tr("panel.edit_meta"),
|
|
variant = "default",
|
|
onClick = function()
|
|
editPackageMeta(selectedProject, selectedPackage)
|
|
end,
|
|
})
|
|
)
|
|
if checkedOut then
|
|
table.insert(
|
|
headerChildren,
|
|
ui.button({
|
|
glyph = removeConfirm and "check" or "package-off",
|
|
tooltip = removeConfirm and noctalia.tr("panel.confirm_remove") or noctalia.tr("panel.remove"),
|
|
variant = "destructive",
|
|
onClick = function()
|
|
if removeConfirm then
|
|
removePackageLocal(selectedProject, selectedPackage)
|
|
else
|
|
removeConfirm = true
|
|
render()
|
|
end
|
|
end,
|
|
})
|
|
)
|
|
end
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8, paddingV = 4 }, headerChildren)
|
|
)
|
|
|
|
|
|
if not checkedOut then
|
|
if isCheckingOut then
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.glyph({ name = "loader", color = "primary", size = 14 }),
|
|
ui.label({ text = checkoutLog, color = "on_surface_variant" }),
|
|
})
|
|
)
|
|
else
|
|
table.insert(
|
|
body,
|
|
ui.button({
|
|
text = noctalia.tr("panel.checkout"),
|
|
glyph = "package-import",
|
|
variant = "default",
|
|
onClick = function()
|
|
checkoutPackage(selectedProject, selectedPackage)
|
|
end,
|
|
})
|
|
)
|
|
table.insert(
|
|
body,
|
|
ui.button({
|
|
text = noctalia.tr("panel.service_remote_run"),
|
|
glyph = "run",
|
|
variant = "default",
|
|
onClick = function()
|
|
oscServiceRemoteRun(selectedProject, selectedPackage)
|
|
end,
|
|
})
|
|
)
|
|
end
|
|
end
|
|
|
|
if checkedOut then
|
|
local pkgDir = checkoutDir .. "/" .. selectedProject .. "/" .. selectedPackage
|
|
local editable = {}
|
|
if noctalia.fileExists(pkgDir .. "/_service") then
|
|
table.insert(editable, "_service")
|
|
end
|
|
if noctalia.fileExists(pkgDir .. "/" .. selectedPackage .. ".spec") then
|
|
table.insert(editable, selectedPackage .. ".spec")
|
|
end
|
|
if noctalia.fileExists(pkgDir .. "/" .. selectedPackage .. ".changes") then
|
|
table.insert(editable, selectedPackage .. ".changes")
|
|
end
|
|
|
|
table.insert(
|
|
body,
|
|
ui.row({ key = "editfiles-row", align = "center", gap = 8 }, {
|
|
ui.glyph({
|
|
name = editFilesOpen and "arrow-badge-down-filled" or "arrow-badge-right-filled",
|
|
size = 16,
|
|
color = "on_surface",
|
|
}),
|
|
ui.button({
|
|
text = noctalia.tr("panel.edit_files"),
|
|
glyph = "ballpen-filled",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
align = "left",
|
|
onClick = function()
|
|
editFilesOpen = not editFilesOpen
|
|
render()
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
if editFilesOpen then
|
|
if #editable == 0 then
|
|
table.insert(body, ui.label({ text = noctalia.tr("panel.no_editable"), color = "on_surface_variant" }))
|
|
else
|
|
for _, file in ipairs(editable) do
|
|
if file:match("%.changes$") then
|
|
local editBtn = ui.button({
|
|
text = noctalia.tr("panel.edit_changes", { name = selectedPackage }),
|
|
glyph = "ballpen-filled",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
editFilePlain(pkgDir .. "/" .. file)
|
|
end,
|
|
})
|
|
local updateBtn = ui.button({
|
|
text = noctalia.tr("panel.update_changes", { name = selectedPackage }),
|
|
glyph = "square-rounded-chevron-up",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
noctalia.runInTerminal("cd " .. shellQuote(pkgDir) .. " && osc vc")
|
|
end,
|
|
})
|
|
if changesButtonsFit(selectedPackage) then
|
|
table.insert(
|
|
body,
|
|
ui.row({ key = "changes-row-" .. file, align = "center", gap = 8 }, {
|
|
editBtn, updateBtn,
|
|
})
|
|
)
|
|
else
|
|
table.insert(
|
|
body,
|
|
ui.column({ key = "changes-col-" .. file, gap = 8, align = "stretch" }, {
|
|
editBtn, updateBtn,
|
|
})
|
|
)
|
|
end
|
|
else
|
|
table.insert(
|
|
body,
|
|
ui.button({
|
|
text = file,
|
|
glyph = "file-text",
|
|
variant = "default",
|
|
onClick = function()
|
|
editFile(pkgDir .. "/" .. file, pkgDir, selectedPackage, file)
|
|
end,
|
|
})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
table.insert(
|
|
body,
|
|
ui.row({ key = "files-row", align = "center", gap = 8 }, {
|
|
ui.glyph({
|
|
name = filesOpen and "arrow-badge-down-filled" or "arrow-badge-right-filled",
|
|
size = 16,
|
|
color = "on_surface",
|
|
}),
|
|
ui.button({
|
|
text = noctalia.tr("panel.files"),
|
|
glyph = filesOpen and "folder-open" or "folder",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
align = "left",
|
|
onClick = function()
|
|
filesOpen = not filesOpen
|
|
if filesOpen then
|
|
filesPackage = selectedPackage
|
|
if #packageFiles == 0 then
|
|
loadPackageFiles(pkgDir, selectedPackage)
|
|
end
|
|
end
|
|
render()
|
|
saveNav()
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
if filesOpen then
|
|
if #packageFiles == 0 then
|
|
table.insert(body, ui.label({ text = noctalia.tr("panel.no_files"), color = "on_surface_variant" }))
|
|
else
|
|
for _, f in ipairs(packageFiles) do
|
|
table.insert(
|
|
body,
|
|
ui.row({ key = "file-row-" .. f, align = "center", gap = 8 }, {
|
|
ui.glyph({ name = "file-text", color = "on_surface_variant", size = 15 }),
|
|
ui.label({ text = f, color = "on_surface", flexGrow = 1, maxLines = 1 }),
|
|
ui.button({
|
|
glyph = (removeFileConfirm and removeFileTarget == f) and "check" or "trash",
|
|
tooltip = (removeFileConfirm and removeFileTarget == f) and noctalia.tr("panel.confirm_remove") or noctalia.tr("panel.remove_file"),
|
|
variant = (removeFileConfirm and removeFileTarget == f) and "destructive" or "ghost",
|
|
onClick = function()
|
|
if removeFileConfirm and removeFileTarget == f then
|
|
removeFileLocal(pkgDir .. "/" .. f, pkgDir, selectedPackage)
|
|
removeFileConfirm = false
|
|
removeFileTarget = nil
|
|
else
|
|
removeFileConfirm = true
|
|
removeFileTarget = f
|
|
end
|
|
render()
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.button({
|
|
text = noctalia.tr("panel.add_remove"),
|
|
glyph = "plus-minus",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
oscAddRemove(pkgDir)
|
|
end,
|
|
}),
|
|
ui.button({
|
|
text = noctalia.tr("panel.update_dir"),
|
|
glyph = "cloud-download",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
oscUpdate(pkgDir)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.button({
|
|
text = runAllConfirm and noctalia.tr("panel.confirm") or noctalia.tr("panel.service_run_all"),
|
|
glyph = runAllConfirm and "check" or "run",
|
|
tooltip = runAllConfirm and noctalia.tr("panel.confirm_run_all") or noctalia.tr("panel.service_run_all"),
|
|
variant = runAllConfirm and "secondary" or "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
if runAllConfirm then
|
|
oscServiceRunAll(pkgDir)
|
|
runAllConfirm = false
|
|
else
|
|
runAllConfirm = true
|
|
end
|
|
render()
|
|
end,
|
|
}),
|
|
ui.button({
|
|
text = noctalia.tr("panel.service_local_run"),
|
|
glyph = "run",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
oscServiceLocalRun(pkgDir)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.button({
|
|
text = noctalia.tr("panel.service_remote_run"),
|
|
glyph = "run",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
oscServiceRemoteRun(selectedProject, selectedPackage)
|
|
end,
|
|
}),
|
|
ui.button({
|
|
text = noctalia.tr("panel.service_run"),
|
|
glyph = "run",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
onClick = function()
|
|
oscServiceRun(pkgDir)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
table.insert(
|
|
body,
|
|
ui.button({
|
|
text = noctalia.tr("panel.commit"),
|
|
glyph = "package-export",
|
|
variant = "default",
|
|
onClick = function()
|
|
oscCommit(pkgDir)
|
|
end,
|
|
})
|
|
)
|
|
|
|
if logSource ~= "rebuild" then
|
|
if isActionLoading then
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.glyph({ name = "loader", color = "primary", size = 14 }),
|
|
ui.label({ text = actionStatus, color = "on_surface_variant" }),
|
|
})
|
|
)
|
|
elseif errorText ~= "" then
|
|
table.insert(body, ui.label({ text = errorText, color = "error", wrap = true }))
|
|
elseif commandLog ~= "" then
|
|
table.insert(body, ui.label({ text = commandLog, color = "on_surface_variant", wrap = true }))
|
|
end
|
|
end
|
|
end
|
|
|
|
local repoOptions = { "All" }
|
|
for _, repo in ipairs(rebuildRepos) do
|
|
table.insert(repoOptions, repo)
|
|
end
|
|
local selectedRepo = rebuildRepos[rebuildRepoIndex]
|
|
local archOptions = { "All" }
|
|
if selectedRepo then
|
|
for _, arch in ipairs(repoMap[selectedRepo] or {}) do
|
|
table.insert(archOptions, arch)
|
|
end
|
|
else
|
|
local seen = {}
|
|
for _, archs in pairs(repoMap) do
|
|
for _, arch in ipairs(archs) do
|
|
if not seen[arch] then
|
|
seen[arch] = true
|
|
table.insert(archOptions, arch)
|
|
end
|
|
end
|
|
end
|
|
table.sort(archOptions, function(a, b)
|
|
return a < b
|
|
end)
|
|
end
|
|
|
|
if not rebuildLoaded then
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.label({ text = noctalia.tr("panel.loading"), color = "on_surface_variant", flexGrow = 1 }),
|
|
ui.button({
|
|
glyph = "refresh",
|
|
tooltip = noctalia.tr("panel.rebuild"),
|
|
variant = "default",
|
|
onClick = function()
|
|
loadRebuildArches(selectedProject, selectedPackage)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
else
|
|
table.insert(
|
|
body,
|
|
ui.row({ key = "rebuild-controls", align = "center", gap = 8 }, {
|
|
ui.column({ gap = 8, align = "stretch", flexGrow = 1 }, {
|
|
ui.row({ align = "center", gap = 6 }, {
|
|
ui.label({ key = "rebuild-repo-label", text = noctalia.tr("panel.repo"), fontSize = 12, color = "on_surface_variant" }),
|
|
ui.select({
|
|
key = "rebuild-repo-" .. tostring(rebuildArchIndex),
|
|
options = repoOptions,
|
|
selectedIndex = rebuildRepoIndex,
|
|
flexGrow = 1,
|
|
onChange = function(index)
|
|
rebuildRepoIndex = tonumber(index)
|
|
savedRebuildRepo = repoOptions[rebuildRepoIndex + 1]
|
|
rebuildArchIndex = 0
|
|
savedRebuildArch = nil
|
|
render()
|
|
end,
|
|
}),
|
|
}),
|
|
ui.row({ align = "center", gap = 6 }, {
|
|
ui.label({ key = "rebuild-arch-label", text = noctalia.tr("panel.arch"), fontSize = 12, color = "on_surface_variant" }),
|
|
ui.select({
|
|
key = "rebuild-arch-" .. tostring(rebuildRepoIndex),
|
|
options = archOptions,
|
|
selectedIndex = rebuildArchIndex,
|
|
flexGrow = 1,
|
|
onChange = function(index)
|
|
rebuildArchIndex = tonumber(index)
|
|
savedRebuildArch = archOptions[rebuildArchIndex + 1]
|
|
render()
|
|
end,
|
|
}),
|
|
}),
|
|
}),
|
|
ui.button({
|
|
key = "rebuild-btn",
|
|
glyph = rebuildConfirm and "check" or "refresh",
|
|
tooltip = rebuildConfirm and noctalia.tr("panel.confirm_rebuild") or noctalia.tr("panel.rebuild"),
|
|
variant = rebuildConfirm and "secondary" or "default",
|
|
onClick = function()
|
|
if rebuildConfirm then
|
|
local repo = repoOptions[rebuildRepoIndex + 1]
|
|
local arch = archOptions[rebuildArchIndex + 1]
|
|
rebuildPackage(selectedProject, selectedPackage, repo, arch)
|
|
rebuildConfirm = false
|
|
else
|
|
rebuildConfirm = true
|
|
end
|
|
render()
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
end
|
|
|
|
if logSource == "rebuild" then
|
|
if isActionLoading then
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.glyph({ name = "loader", color = "primary", size = 14 }),
|
|
ui.label({ text = actionStatus, color = "on_surface_variant" }),
|
|
})
|
|
)
|
|
elseif errorText ~= "" then
|
|
table.insert(body, ui.label({ text = errorText, color = "error", wrap = true }))
|
|
elseif commandLog ~= "" then
|
|
table.insert(body, ui.label({ text = commandLog, color = "on_surface_variant", wrap = true }))
|
|
end
|
|
end
|
|
|
|
elseif selectedProject then
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.button({
|
|
glyph = "arrow-left",
|
|
variant = "ghost",
|
|
onClick = function()
|
|
loadProjects(true)
|
|
end,
|
|
}),
|
|
ui.glyph({ name = "folder-filled", color = "primary", size = 22 }),
|
|
ui.label({ text = selectedProject, fontWeight = "bold", fontSize = 18, flexGrow = 1 }),
|
|
})
|
|
)
|
|
|
|
if packagesLoading then
|
|
table.insert(body, ui.label({ text = noctalia.tr("panel.loading_packages"), color = "on_surface_variant" }))
|
|
elseif errorText ~= "" then
|
|
table.insert(body, ui.label({ text = errorText, color = "error", wrap = true }))
|
|
table.insert(
|
|
body,
|
|
ui.button({
|
|
text = noctalia.tr("panel.retry"),
|
|
glyph = "refresh",
|
|
variant = "default",
|
|
onClick = function()
|
|
loadPackages(selectedProject)
|
|
end,
|
|
})
|
|
)
|
|
else
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.input({
|
|
key = "packageFilter",
|
|
placeholder = noctalia.tr("panel.search_packages"),
|
|
value = packageFilter,
|
|
flexGrow = 1,
|
|
onChange = function(value)
|
|
packageFilter = value
|
|
render()
|
|
end,
|
|
}),
|
|
ui.button({
|
|
key = "pkg-sort-btn",
|
|
glyph = if sortAsc then "sort-ascending" else "sort-descending",
|
|
tooltip = if sortAsc then "A-Z" else "Z-A",
|
|
variant = "default",
|
|
onClick = function()
|
|
sortAsc = not sortAsc
|
|
render()
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
local filtered = filterList(packages, packageFilter, sortAsc)
|
|
if #filtered == 0 then
|
|
table.insert(body, ui.label({ text = noctalia.tr("panel.no_packages"), color = "on_surface_variant" }))
|
|
else
|
|
local checkoutDir = resolveCheckoutDir()
|
|
for _, name in ipairs(filtered) do
|
|
local checkedOut = noctalia.fileExists(checkoutDir .. "/" .. selectedProject .. "/" .. name)
|
|
table.insert(
|
|
body,
|
|
ui.row({ key = "pkgrow-" .. name, align = "center", gap = 8 }, {
|
|
ui.glyph({ name = "package", size = 16, color = checkedOut and "primary" or "on_surface" }),
|
|
ui.button({
|
|
key = "pkgbtn-" .. name,
|
|
text = name,
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
align = "left",
|
|
onClick = function()
|
|
if filesPackage ~= name then
|
|
filesOpen = false
|
|
packageFiles = {}
|
|
filesPackage = name
|
|
end
|
|
if logPackage ~= name then
|
|
errorText = ""
|
|
commandLog = ""
|
|
logPackage = name
|
|
end
|
|
isActionLoading = false
|
|
actionStatus = ""
|
|
selectedPackage = name
|
|
savedRebuildRepo = nil
|
|
savedRebuildArch = nil
|
|
rebuildLoaded = false
|
|
rebuildConfirm = false
|
|
runAllConfirm = false
|
|
render()
|
|
saveNav()
|
|
loadRebuildArches(selectedProject, name)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.glyph({ name = "buildings", color = "primary", size = 22 }),
|
|
ui.label({ text = noctalia.tr("panel.my_projects"), fontWeight = "bold", fontSize = 18, flexGrow = 1 }),
|
|
ui.button({
|
|
glyph = "refresh",
|
|
tooltip = noctalia.tr("panel.reload_projects"),
|
|
variant = "default",
|
|
onClick = function()
|
|
loadProjects(true)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.input({
|
|
key = "projectFilter",
|
|
placeholder = noctalia.tr("panel.search_projects"),
|
|
value = projectFilter,
|
|
flexGrow = 1,
|
|
onChange = function(value)
|
|
projectFilter = value
|
|
render()
|
|
end,
|
|
}),
|
|
ui.button({
|
|
key = "proj-sort-btn",
|
|
glyph = sortAsc and "sort-ascending" or "sort-descending",
|
|
tooltip = sortAsc and "A-Z" or "Z-A",
|
|
variant = "default",
|
|
onClick = function()
|
|
sortAsc = not sortAsc
|
|
render()
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
|
|
if #projects == 0 then
|
|
table.insert(body, ui.label({ text = noctalia.tr("panel.no_projects"), color = "on_surface_variant" }))
|
|
else
|
|
local filtered = filterList(projects, projectFilter, sortAsc)
|
|
if #filtered == 0 then
|
|
table.insert(body, ui.label({ text = noctalia.tr("panel.no_projects"), color = "on_surface_variant" }))
|
|
else
|
|
for _, name in ipairs(filtered) do
|
|
table.insert(
|
|
body,
|
|
ui.row({ align = "center", gap = 8 }, {
|
|
ui.button({
|
|
text = name,
|
|
glyph = "folder-filled",
|
|
variant = "default",
|
|
flexGrow = 1,
|
|
align = "left",
|
|
onClick = function()
|
|
loadPackages(name)
|
|
end,
|
|
}),
|
|
ui.button({
|
|
glyph = "feather-filled",
|
|
variant = "default",
|
|
tooltip = noctalia.tr("panel.edit_project_meta"),
|
|
onClick = function()
|
|
editProjectMeta(name)
|
|
end,
|
|
}),
|
|
})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
panel.render(ui.column({ flexGrow = 1, gap = 12 }, {
|
|
ui.scroll({ key = "scroll-" .. tostring(selectedProject) .. "-" .. tostring(selectedPackage), flexGrow = 1 }, {
|
|
ui.column({ gap = 10, paddingV = 6, paddingH = 12, align = "stretch" }, body),
|
|
}),
|
|
}))
|
|
end
|
|
|
|
function onOpen()
|
|
if noctalia.commandExists("osc") and noctalia.fileExists(OSC_CONFIG) then
|
|
restoreProject, restorePackage, restoreFilesOpen, restoreFilesPackage, restoreCommandLog, restoreErrorText,
|
|
restoreLogSource =
|
|
readNav()
|
|
loadProjects()
|
|
else
|
|
render()
|
|
end
|
|
end
|