diff options
| author | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-09-24 21:01:06 +0200 |
|---|---|---|
| committer | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-09-25 20:16:02 +0200 |
| commit | 628406f0a50e040d4661629446c620a3a495441f (patch) | |
| tree | e1a2b783296b1779a4040b5a7ea55d72547a37e8 /sourcehut-builds/panel.luau | |
Diffstat (limited to 'sourcehut-builds/panel.luau')
| -rw-r--r-- | sourcehut-builds/panel.luau | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/sourcehut-builds/panel.luau b/sourcehut-builds/panel.luau new file mode 100644 index 0000000..a720721 --- /dev/null +++ b/sourcehut-builds/panel.luau @@ -0,0 +1,204 @@ +--!nonstrict +-- Sourcehut Builds - Panel that shows the +-- +-- You can configure the glyphs shown for the different statuses, as well as +-- when there are no recent builds. + +local EmptyState = require("./empty_state.luau") +local StringUtils = require("./string_utils.luau") +local K = require("./constants.luau") + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local isOpen = false +-- I'm being lazy and not cloning this because my assumption is that I'll always +-- replace the object from the network call and not modify it. But I know I will +-- forget and break that assumption, so this comment is left here because +-- whatever bug you're finding with stale state or bad empty state is caused by +-- not cloning empty state across the files. +local buildState: SourcehutBuildsState = EmptyState +local pollerStatus: PollerStatus = K.poller.state.pending + +-------------------------------------------------------------------------------- +-- Internal Functions +-------------------------------------------------------------------------------- + +------------------------------------------------------------ Translation Helpers + +local function emptyStateLabel(): string + if pollerStatus.type == "pending" then + return noctalia.tr("panel.loading.label") + elseif pollerStatus.type == "error" then + return noctalia.tr("panel.error.label", {error = pollerStatus.error}) + else + return noctalia.tr("panel.empty.label") + end +end + +------------------------------------------------------------------------ Actions + +local function openBuild(build: SourcehutBuild, task: SourcehutBuildTask | nil) + local url = K.sourcehut.web_url .. buildState.user .. '/job/' .. build.id + + if task ~= nil then + url = url .. "#task-" .. task.name + end + + noctalia.runAsync("gio open '" .. url .. "' || xdg-open '" .. url .. "'") +end + + +--------------------------------------------------------------------- UI: Glyphs + +local function buildGlyph(status: SourcehutBuildStatus): string + if status == "PENDING" then + return "circle" + elseif status == "RUNNING" then + return "circle-caret-right" + elseif status == "FAILED" then + return "x" + elseif status == "SUCCESS" then + return "check" + elseif status == "TIMEOUT" then + return "clock-x" + elseif status == "CANCELLED" then + return "circle-off" + end + return "question-mark" +end + +local function taskGlyph(status: SourcehutBuildTaskStatus): string + if status == "PENDING" then + return "circle-dotted" + elseif status == "RUNNING" then + return "circle-caret-right" + elseif status == "FAILED" then + return "x" + elseif status == "SUCCESS" then + return "check" + elseif status == "SKIPPED" then + return "circle-dashed-minus" + end + return "question-mark" +end + +----------------------------------------------------------------- UI: Components + +local function taskButton(build: SourcehutBuild, task: SourcehutBuildTask): UiNode + return ui.button({ + glyph = taskGlyph(task.status), + controlSize = "sm", + variant = "ghost", + tooltip = noctalia.tr("panel.task.tooltip", { + task = task.name, + status = StringUtils.capitalize(task.status), + }), + onClick = function() openBuild(build, task) end + }) +end + +local function buildButton(build: SourcehutBuild): UiNode + return ui.button({ + glyph = buildGlyph(build.status), + text = noctalia.tr("panel.build.label", { + user = buildState.user, + repo = build.tags[1], + id = build.id + }), + controlSize = "sm", + variant = "ghost", + tooltip = noctalia.tr("panel.build.tooltip", { + status = StringUtils.capitalize(build.status), + }), + onClick = function() openBuild(build) end + }) +end + +local function buildRow(build: SourcehutBuild): UiNode + local tasks: {[number]: UiNode} = {} + for _k, task in ipairs(build.tasks) do + tasks[#tasks + 1] = taskButton(build, task) + end + return ui.row({align = "start", justify = "space_between", gap = 2, flexGrow = 1}, { + buildButton(build), + ui.row({justify = "end", gap = 0, flexGrow = 0}, tasks) + }) +end + +local function buildRows(builds: {[number]: SourcehutBuild}): UiNode + local build_rows: {[number]: UiNode} = {} + for _, build in ipairs(builds) do + build_rows[#build_rows+1] = buildRow(build) + end + return ui.column({ flexGrow = 1, align = "top", justify = "start" }, build_rows) +end + +-- Renders the panel +local function render() + local body + local header = ui.row({ align = "center", gap = 8 }, { + ui.label({ + text = noctalia.tr("panel.title"), + fontSize = K.panel.theme.fontSize.heading, + fontWeight = K.panel.theme.fontWeight.heading, + flexGrow = 1 + }), + }) + + if #buildState.builds == 0 then + body = ui.column({ flexGrow = 1, align = "center", justify = "center" }, { + ui.label({ text = emptyStateLabel(), color = "on_surface_variant" }), + }) + else + body = buildRows(buildState.builds) + end + + if isOpen then + panel.render(ui.column({ flexGrow = 1, gap = 10, align = "stretch" }, { header, body })) + end +end + +---------------------------------------------------------------- Event Listeners + +local function onPollerStateChanged(value: PollerStatus) + pollerStatus = value + render() +end + +local function onBuildsStateChanged(value: SourcehutBuildsState) + buildState = value + render() +end + +-------------------------------------------------------------------------------- +-- Listeners +-------------------------------------------------------------------------------- + +-- Synchronizes the panel state +noctalia.state.watch(K.events.poller_state_updated, onPollerStateChanged) +noctalia.state.watch(K.events.builds_state_updated, onBuildsStateChanged) + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- + +function onOpen(_context) + isOpen = true + noctalia.state.set(K.events.panel_state_updated, isOpen) + render() +end + +function onClose() + isOpen = false + noctalia.state.set(K.events.panel_state_updated, isOpen) +end + +function onToggle(value: boolean) + isOpen = value + render() +end + +function onConfigChanged() + render() +end |