From 6e697cf23039f74dec856be6fa888e6632c18487 Mon Sep 17 00:00:00 2001 From: Ray Zhang Date: Fri, 14 Aug 2026 22:12:31 -0400 Subject: [PATCH] add plugin Github PRs --- github-prs/README.md | 68 ++++++ github-prs/bar.luau | 82 ++++++++ github-prs/fetch.luau | 352 ++++++++++++++++++++++++++++++++ github-prs/panel.luau | 277 +++++++++++++++++++++++++ github-prs/plugin.toml | 73 +++++++ github-prs/thumbnail.webp | Bin 0 -> 42500 bytes github-prs/translations/en.json | 47 +++++ 7 files changed, 899 insertions(+) create mode 100644 github-prs/README.md create mode 100644 github-prs/bar.luau create mode 100644 github-prs/fetch.luau create mode 100644 github-prs/panel.luau create mode 100644 github-prs/plugin.toml create mode 100644 github-prs/thumbnail.webp create mode 100644 github-prs/translations/en.json diff --git a/github-prs/README.md b/github-prs/README.md new file mode 100644 index 00000000..b41dc685 --- /dev/null +++ b/github-prs/README.md @@ -0,0 +1,68 @@ +# GitHub Pull Requests + +Track the pull requests that matter to you from the Noctalia bar. The plugin uses your existing GitHub CLI authentication and shows CI, review, draft, and activity status in a compact panel. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `raycursive/github-prs` | +| Entries | Bar widget: `bar`; panel: `panel`; service: `fetch` | + +## Requirements + +- Noctalia plugin API 9 or newer (used for closure-backed panel interactions). +- `gh` on `PATH`, authenticated with `gh auth login`. +- `xdg-open` on `PATH` to open a selected pull request in the default browser. + +## Usage + +Enable `raycursive/github-prs` in **Settings → Plugins**, then add the `bar` entry from **Settings → Bar → Widgets**. Click the widget to open the `panel` entry. The `fetch` service starts automatically and refreshes the configured searches in the background. + +Each **Search rules** item is a GitHub search query fragment. The service prefixes every rule with `is:pr is:open` and, unless the rule already contains `archived:`, adds `archived:false`. Results from all rules are merged and deduplicated. + +Examples: + +| Rule | Matches | +| --- | --- | +| `author:@me` | Pull requests you created. | +| `review-requested:@me org:acme` | Reviews requested from you in one organization. | +| `assignee:@me -repo:acme/legacy draft:false` | Non-draft pull requests assigned to you, excluding one repository. | +| `involves:@me org:acme org:other-org` | Pull requests involving you across two organizations. | + +GitHub's `repo:` qualifier matches exact repositories. Use **Excluded repositories** for plugin-side glob patterns such as `legacy-*` or `acme/private-*`. A pattern without an owner matches repository names in every organization; matching is case-insensitive and `*` matches any number of characters. + +The panel groups pull requests by repository and orders them by latest activity. Select a repository header to collapse or expand it, or select a pull-request title to open it in the browser. Open the panel without the bar widget with: + +```sh +noctalia msg panel-toggle raycursive/github-prs:panel +``` + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `rules` | `string_list` | `author:@me` | GitHub search fragments; one `gh api graphql` request runs per non-empty rule. | +| `excluded_repositories` | `string_list` | empty | Case-insensitive repository-name or `owner/name` glob patterns removed after fetching. | +| `refresh_interval` | `int` | `180` seconds | Background refresh interval, limited to 30–3600 seconds. | +| `glyph` | `glyph` | `git-pull-request` | Glyph used by this bar-widget instance. | +| `hide_when_zero` | `bool` | `false` | Hides the bar widget when no matching open pull requests remain. | + +## IPC + +The `fetch` service accepts these events: + +```sh +# Fetch all configured search rules immediately. +noctalia msg plugin raycursive/github-prs:fetch all refresh + +# Write status, result count, last update, and any error to the Noctalia log. +noctalia msg plugin raycursive/github-prs:fetch all dump +``` + +## Notes + +- For every configured rule, the service spawns `gh api graphql`, which sends the resulting search query to GitHub using the GitHub CLI's current authentication. The plugin does not read or store a GitHub token itself. +- Selecting a pull request spawns `xdg-open` with the GitHub URL. The plugin does not write files. +- Each rule returns at most 50 rows. The panel reports additional matches as truncated instead of silently presenting the list as complete. +- A partial rule failure is shown alongside successful results. If every rule fails, the last successful in-memory result remains visible until the plugin reloads or a later fetch succeeds. diff --git a/github-prs/bar.luau b/github-prs/bar.luau new file mode 100644 index 00000000..fcff5d7d --- /dev/null +++ b/github-prs/bar.luau @@ -0,0 +1,82 @@ +--!nonstrict +-- Bar [[widget]] entry: glyph + open-PR count, fed entirely by the fetch +-- service through shared state. The glyph tints error-red when fetching +-- itself failed; details remain available in the panel. +-- Clicking anywhere on the capsule toggles the panel — +-- which is why the tree deliberately contains no inline ui.button (inline +-- controls would consume their own clicks). + +local PANEL_ID = "raycursive/github-prs:panel" + +local counts = noctalia.state.get("counts") +local fetchStatus = noctalia.state.get("fetch_status") or "loading" + +local function countText() + if counts ~= nil then + return tostring(counts.total) + end + if fetchStatus == "error" then + return "!" + end + return "…" +end + +local function render() + local hideWhenZero = noctalia.getConfig("hide_when_zero") == true + if hideWhenZero and fetchStatus == "ok" and counts ~= nil and counts.total == 0 then + barWidget.setVisible(false) + return + end + barWidget.setVisible(true) + + local glyph = noctalia.getConfig("glyph") + if type(glyph) ~= "string" or glyph == "" then + glyph = "git-pull-request" + end + local isVertical = barWidget.isVertical() + local container = isVertical and ui.column or ui.row + local layout = { gap = 4, align = "center" } + if isVertical then + layout.paddingV = 4 + else + layout.paddingH = 4 + end + barWidget.render(container(layout, { + ui.glyph({ + name = glyph, + size = 16, + color = fetchStatus == "error" and "error" or "on_surface", + }), + ui.label({ + text = countText(), + color = "on_surface", + }), + })) +end + +-- fetch_status is published last by the service, so one watcher on it sees a +-- consistent prs/counts snapshot; the counts watcher covers same-status updates. +noctalia.state.watch("counts", function(value) + counts = value + render() +end) + +noctalia.state.watch("fetch_status", function(value) + fetchStatus = value + render() +end) + +render() + +function update() + noctalia.setUpdateInterval(5000) + render() +end + +function onClick() + noctalia.togglePanel(PANEL_ID) +end + +function onConfigChanged() + render() +end diff --git a/github-prs/fetch.luau b/github-prs/fetch.luau new file mode 100644 index 00000000..24bf2825 --- /dev/null +++ b/github-prs/fetch.luau @@ -0,0 +1,352 @@ +--!nonstrict +-- Headless [[service]] entry — the only place that talks to GitHub. +-- +-- On every tick (and on manual refresh) it runs one `gh api graphql` search +-- per configured rule, merges the results (deduped by PR url, sorted by +-- updatedAt desc), and publishes them to the plugin's shared state for the +-- bar widget and panel to render. See the state contract below. +-- +-- State keys published: +-- prs array of { number, title, url, repo, author, isDraft, +-- reviewDecision, ci, updatedAt } — nulls normalized to "" +-- counts { total, failing, pending, approved, changes_requested, drafts } +-- error_message "" when healthy; full-failure error or partial warning +-- last_updated_text preformatted "HH:MM" of the last successful merge +-- rules_count number of configured rules (panel empty-state hint) +-- overflow_count sum of matches beyond the per-rule 50 cap +-- fetch_status "loading" | "ok" | "error" — always set LAST so watchers +-- observe a consistent snapshot +-- State keys consumed: +-- refresh_requested monotonic nonce bumped by the panel's refresh button + +local GH_TIMEOUT_MS = 30000 +local DEFAULT_INTERVAL_S = 180 +local MIN_INTERVAL_S = 30 +local RESULTS_PER_RULE = 50 + +-- Single line and free of single quotes, so argument quoting stays trivial. +local GRAPHQL_QUERY = + "query($q: String!) { search(query: $q, type: ISSUE, first: 50) { issueCount nodes { ... on PullRequest { number title url isDraft reviewDecision updatedAt repository { nameWithOwner } author { login } commits(last: 1) { nodes { commit { statusCheckRollup { state } } } } } } } }" + +-- Projects the response down to plain PR records and normalizes the nullable +-- fields (ghost author, no review decision, no CI checks) to "" so Luau never +-- sees nil holes. select(.url != null) drops nodes that failed the inline +-- fragment spread. .count keeps the raw issueCount for the overflow note. +local JQ_PROJECTION = + '{count: .data.search.issueCount, items: [.data.search.nodes[] | select(.url != null) | {number, title, url, isDraft, updatedAt, repo: .repository.nameWithOwner, author: (.author.login // ""), reviewDecision: (.reviewDecision // ""), ci: (.commits.nodes[0].commit.statusCheckRollup.state // "")}]}' + +local fetching = false +local fetchGeneration = 0 +local lastRefreshNonce = noctalia.state.get("refresh_requested") or 0 + +local function quoteShellArgument(s) + return "'" .. s:gsub("'", "'\\''") .. "'" +end + +local function readRules() + local raw = noctalia.getConfig("rules") + local rules = {} + if type(raw) == "table" then + for _, rule in ipairs(raw) do + if type(rule) == "string" then + local trimmed = noctalia.string.trim(rule) + if trimmed ~= "" then + table.insert(rules, trimmed) + end + end + end + end + return rules +end + +local function readExcludedRepositories() + local raw = noctalia.getConfig("excluded_repositories") + local patterns = {} + if type(raw) == "table" then + for _, pattern in ipairs(raw) do + if type(pattern) == "string" then + local trimmed = noctalia.string.trim(pattern) + if trimmed ~= "" then + table.insert(patterns, trimmed:lower()) + end + end + end + end + return patterns +end + +-- Small glob matcher: '*' consumes zero or more characters. Patterns without +-- an owner match the repository basename; patterns containing '/' match the +-- full owner/name, without relying on GitHub search's exact-only repo: +-- qualifier. +local function globMatches(value, pattern) + local valueIndex = 1 + local patternIndex = 1 + local starIndex = nil + local starValueIndex = nil + + while valueIndex <= #value do + local patternChar = pattern:sub(patternIndex, patternIndex) + if patternChar == "*" then + starIndex = patternIndex + starValueIndex = valueIndex + patternIndex += 1 + elseif patternIndex <= #pattern and patternChar == value:sub(valueIndex, valueIndex) then + valueIndex += 1 + patternIndex += 1 + elseif starIndex ~= nil then + starValueIndex += 1 + valueIndex = starValueIndex + patternIndex = starIndex + 1 + else + return false + end + end + + while pattern:sub(patternIndex, patternIndex) == "*" do + patternIndex += 1 + end + return patternIndex > #pattern +end + +local function repositoryExcluded(repo, patterns) + local fullName = repo:lower() + local basename = fullName:match("[^/]+$") or fullName + for _, pattern in ipairs(patterns) do + local candidate = pattern:find("/", 1, true) and fullName or basename + if globMatches(candidate, pattern) then + return true + end + end + return false +end + +local function filterExcluded(prs, patterns) + local filtered = {} + for _, pr in ipairs(prs) do + if not repositoryExcluded(pr.repo, patterns) then + table.insert(filtered, pr) + end + end + return filtered +end + +local function readIntervalMs() + local interval = tonumber(noctalia.getConfig("refresh_interval")) or DEFAULT_INTERVAL_S + return math.max(MIN_INTERVAL_S, interval) * 1000 +end + +local function buildSearchQuery(rule) + local q = "is:pr is:open" + if not rule:find("archived:", 1, true) then + q ..= " archived:false" + end + return q .. " " .. rule +end + +local function buildCommand(rule) + return "gh api graphql -f query=" + .. quoteShellArgument(GRAPHQL_QUERY) + .. " -f q=" + .. quoteShellArgument(buildSearchQuery(rule)) + .. " --jq " + .. quoteShellArgument(JQ_PROJECTION) +end + +local function computeCounts(prs) + local counts = { total = #prs, failing = 0, pending = 0, approved = 0, changes_requested = 0, drafts = 0 } + for _, pr in ipairs(prs) do + if pr.ci == "FAILURE" or pr.ci == "ERROR" then + counts.failing += 1 + elseif pr.ci == "PENDING" or pr.ci == "EXPECTED" then + counts.pending += 1 + end + if pr.reviewDecision == "APPROVED" then + counts.approved += 1 + elseif pr.reviewDecision == "CHANGES_REQUESTED" then + counts.changes_requested += 1 + end + if pr.isDraft then + counts.drafts += 1 + end + end + return counts +end + +-- fetch_status last: watchers triggering on it see prs/counts already updated. +local function publish(prs, status, errorMessage, overflow, rulesCount, updateTimestamp) + noctalia.state.set("prs", prs) + noctalia.state.set("counts", computeCounts(prs)) + noctalia.state.set("overflow_count", overflow) + noctalia.state.set("rules_count", rulesCount) + if updateTimestamp then + noctalia.state.set("last_updated_text", noctalia.formatTime("%H:%M")) + end + noctalia.state.set("error_message", errorMessage) + noctalia.state.set("fetch_status", status) +end + +local function classifyResult(result) + if result.timedOut then + return nil, noctalia.tr("error.timeout") + end + if result.exitCode ~= 0 then + local message = noctalia.string.trim(result.stderr or ""):match("^[^\r\n]+") + return nil, message or `gh exited with code {result.exitCode}` + end + if result.stdoutTruncated then + return nil, noctalia.tr("error.truncated") + end + local decoded = noctalia.json.decode(result.stdout) + if type(decoded) ~= "table" or type(decoded.items) ~= "table" then + return nil, noctalia.tr("error.parse") + end + return decoded, nil +end + +local function mergeAndPublish(ruleResults, ruleErrors, rulesCount, excludedRepositories) + local prs = {} + local seen = {} + local overflow = 0 + for _, decoded in ipairs(ruleResults) do + for _, pr in ipairs(decoded.items) do + if not seen[pr.url] and not repositoryExcluded(pr.repo, excludedRepositories) then + seen[pr.url] = true + table.insert(prs, pr) + end + end + local count = tonumber(decoded.count) or 0 + overflow += math.max(0, count - RESULTS_PER_RULE) + end + -- ISO-8601 UTC timestamps sort lexicographically; newest first. + table.sort(prs, function(a, b) + return a.updatedAt > b.updatedAt + end) + + if #ruleErrors >= rulesCount then + -- Every rule failed: keep the previous (still filtered) list visible. + local previous = filterExcluded(noctalia.state.get("prs"), excludedRepositories) + publish(previous, "error", ruleErrors[1], noctalia.state.get("overflow_count"), rulesCount, false) + elseif #ruleErrors > 0 then + local warning = noctalia.tr("panel.partial_warning", { + failed = #ruleErrors, + total = rulesCount, + message = ruleErrors[1], + }) + publish(prs, "ok", warning, overflow, rulesCount, true) + else + publish(prs, "ok", "", overflow, rulesCount, true) + end +end + +local function startFetch() + if fetching then + return + end + + local rules = readRules() + local excludedRepositories = readExcludedRepositories() + if #rules == 0 then + publish({}, "ok", "", 0, 0, true) + return + end + + if not noctalia.commandExists("gh") then + local previous = filterExcluded(noctalia.state.get("prs"), excludedRepositories) + publish(previous, "error", noctalia.tr("error.gh_missing"), 0, #rules, false) + return + end + + fetching = true + fetchGeneration += 1 + local thisGeneration = fetchGeneration + + -- Flip to loading but keep the stale list so the UI never flashes empty. + noctalia.state.set("error_message", "") + noctalia.state.set("fetch_status", "loading") + + local pending = #rules + local ruleResults = {} + local ruleErrors = {} + + local function ruleDone() + pending -= 1 + if pending == 0 then + fetching = false + mergeAndPublish(ruleResults, ruleErrors, #rules, excludedRepositories) + end + end + + for _, rule in ipairs(rules) do + local started = noctalia.runAsync(buildCommand(rule), function(result) + if thisGeneration ~= fetchGeneration then + return -- config changed mid-flight; a newer fetch owns the state + end + local decoded, err = classifyResult(result) + if decoded then + table.insert(ruleResults, decoded) + else + table.insert(ruleErrors, `{rule}: {err}`) + noctalia.log(`github-prs: rule "{rule}" failed: {err}`) + end + ruleDone() + end, GH_TIMEOUT_MS) + + if not started then + table.insert(ruleErrors, `{rule}: could not start gh`) + ruleDone() + end + end +end + +local function restartFetch() + -- Existing gh processes cannot be cancelled, so invalidate their callbacks + -- before starting the replacement fetch. + if fetching then + fetching = false + fetchGeneration += 1 + end + startFetch() +end + +noctalia.setUpdateInterval(readIntervalMs()) + +if noctalia.state.get("fetch_status") == nil then + noctalia.state.set("last_updated_text", "") + publish({}, "loading", "", 0, #readRules(), false) +end + +noctalia.state.watch("refresh_requested", function(value) + local nonce = tonumber(value) or 0 + if nonce ~= lastRefreshNonce then + lastRefreshNonce = nonce + restartFetch() + end +end) + +startFetch() + +function update() + startFetch() +end + +-- Defining this keeps the service alive across settings edits (no VM restart); +-- everything config-derived must be re-read here. +function onConfigChanged() + noctalia.setUpdateInterval(readIntervalMs()) + restartFetch() +end + +function onIpc(event, _payload) + if event == "refresh" then + restartFetch() + elseif event == "dump" then + local summary = { + status = noctalia.state.get("fetch_status"), + count = #noctalia.state.get("prs"), + error = noctalia.state.get("error_message"), + updated = noctalia.state.get("last_updated_text"), + } + noctalia.log("github-prs: " .. (noctalia.json.encode(summary) or "?")) + end +end diff --git a/github-prs/panel.luau b/github-prs/panel.luau new file mode 100644 index 00000000..2300eb3c --- /dev/null +++ b/github-prs/panel.luau @@ -0,0 +1,277 @@ +--!nonstrict +-- [[panel]] entry: pull requests grouped by repository, rendered from the +-- shared state the fetch service publishes. Each pull request uses two lines so +-- long titles cannot collide with the author or status badges; clicking the +-- title opens the PR in the browser. The header carries the last-refresh time, +-- an icon-only refresh action, and a close button. +-- +local MAX_ROWS = 80 +local CENTERED_LAYOUT = { flexGrow = 1, align = "center", justify = "center", gap = 8 } + +local CI_APPEARANCE = { + SUCCESS = { glyph = "circle-check", color = "primary" }, + FAILURE = { glyph = "circle-x", color = "error" }, + ERROR = { glyph = "alert-circle", color = "error" }, + PENDING = { glyph = "clock", color = "tertiary" }, + EXPECTED = { glyph = "clock", color = "tertiary" }, +} +local CI_NONE = { glyph = "circle-dashed", color = "on_surface_variant" } + +local REVIEW_BADGE = { + APPROVED = { key = "panel.review.approved", color = "primary" }, + CHANGES_REQUESTED = { key = "panel.review.changes_requested", color = "error" }, + REVIEW_REQUIRED = { key = "panel.review.review_required", color = "tertiary" }, +} + +local prs = {} +local fetchStatus = "loading" +local errorMessage = "" +local lastUpdatedText = "" +local rulesCount = nil +local overflowCount = 0 +local isOpen = false +local collapsedRepos = {} + +local render + +local tr = noctalia.tr + +local function syncFromState() + prs = noctalia.state.get("prs") + fetchStatus = noctalia.state.get("fetch_status") + errorMessage = noctalia.state.get("error_message") + lastUpdatedText = noctalia.state.get("last_updated_text") + rulesCount = noctalia.state.get("rules_count") + overflowCount = noctalia.state.get("overflow_count") + + local activeRepos = {} + for _, pr in ipairs(prs) do + activeRepos[pr.repo] = true + end + for repo, _ in pairs(collapsedRepos) do + if not activeRepos[repo] then + collapsedRepos[repo] = nil + end + end +end + +local function badge(text, color, fillAlpha) + return ui.row({ + fill = `{color}/{fillAlpha}`, + border = color .. "/0.24", + borderWidth = 1, + radius = 8, + paddingH = 7, + paddingV = 2, + align = "center", + }, { + ui.label({ text = text, fontSize = 11, fontWeight = "medium", color = color }), + }) +end + +local function prRow(pr) + local ci = CI_APPEARANCE[pr.ci] or CI_NONE + + local statusChildren = { + ui.glyph({ name = "user", size = 13, color = "on_surface_variant" }), + ui.label({ + text = pr.author, + fontSize = 11, + color = "on_surface_variant", + flexGrow = 1, + maxLines = 1, + }), + ui.glyph({ name = ci.glyph, size = 16, color = ci.color }), + } + if pr.isDraft then + table.insert(statusChildren, badge(tr("panel.draft"), "on_surface_variant", "0.15")) + end + local review = REVIEW_BADGE[pr.reviewDecision] + if review then + table.insert(statusChildren, badge(tr(review.key), review.color, "0.2")) + end + + return ui.column({ key = pr.url, gap = 4, paddingH = 10, paddingV = 7, align = "stretch" }, { + ui.row({ gap = 9, align = "center" }, { + ui.glyph({ name = "git-pull-request", size = 15, color = "primary" }), + ui.button({ + key = `open:{pr.url}`, + text = `#{pr.number} {pr.title}`, + variant = "ghost", + contentAlign = "start", + flexGrow = 1, + onClick = function() + local url = "'" .. pr.url:gsub("'", "'\\''") .. "'" + noctalia.runAsync("xdg-open " .. url) + end, + }), + }), + ui.row({ gap = 7, align = "center", paddingH = 24 }, statusChildren), + }) +end + +-- Group by repo and order groups by their newest PR activity. prs is globally +-- sorted by updatedAt desc, so appending in order also keeps each group recent. +local function groupedRows() + local byRepo = {} + local repoNames = {} + for _, pr in ipairs(prs) do + if byRepo[pr.repo] == nil then + byRepo[pr.repo] = {} + table.insert(repoNames, pr.repo) + end + table.insert(byRepo[pr.repo], pr) + end + + local rows = {} + local shown = 0 + local hiddenByCollapse = 0 + for _, repo in ipairs(repoNames) do + if shown >= MAX_ROWS then + break + end + local collapsed = collapsedRepos[repo] + -- Capture this iteration's repository for the click callback. + local repoName = repo + local groupChildren = { + ui.row({ + key = `repo:{repo}`, + gap = 8, + align = "center", + paddingH = 11, + paddingV = 8, + fill = "surface_variant/0.24", + onClick = function() + collapsedRepos[repoName] = not collapsedRepos[repoName] + render() + end, + }, { + ui.glyph({ name = collapsed and "chevron-right" or "chevron-down", size = 15, color = "primary" }), + ui.glyph({ name = "books", size = 16, color = "primary" }), + ui.label({ text = repo, fontSize = 13, fontWeight = "bold", color = "primary", flexGrow = 1 }), + badge(tostring(#byRepo[repo]), "primary", "0.14"), + }), + } + if collapsed then + hiddenByCollapse += #byRepo[repo] + else + for _, pr in ipairs(byRepo[repo]) do + if shown >= MAX_ROWS then + break + end + table.insert(groupChildren, ui.separator({ thickness = 1, color = "outline/0.28", spacing = 0 })) + table.insert(groupChildren, prRow(pr)) + shown += 1 + end + end + table.insert(rows, ui.column({ + key = `repo-group:{repo}`, + fill = "surface_variant/0.10", + border = "outline/0.40", + borderWidth = 1, + radius = 11, + align = "stretch", + }, groupChildren)) + end + + local omitted = #prs - shown - hiddenByCollapse + if omitted > 0 then + table.insert(rows, ui.label({ + key = "more_rows", + text = tr("panel.more_rows", { count = omitted }), + fontSize = 11, + color = "on_surface_variant", + })) + end + if overflowCount > 0 then + table.insert(rows, ui.label({ + key = "truncated_note", + text = tr("panel.truncated_note", { count = overflowCount }), + fontSize = 11, + color = "on_surface_variant", + })) + end + return rows +end + +local function body() + if #prs == 0 then + if fetchStatus == "loading" then + return ui.column(CENTERED_LAYOUT, { + ui.label({ text = tr("panel.loading"), color = "on_surface_variant" }), + }) + end + if fetchStatus == "error" then + return ui.column(CENTERED_LAYOUT, { + ui.label({ text = tr("panel.error_title"), fontWeight = "bold", color = "error" }), + ui.label({ text = errorMessage, color = "on_surface_variant", maxLines = 4, textAlign = "center" }), + ui.button({ + text = tr("panel.retry"), + variant = "primary", + onClick = function() + noctalia.state.set("refresh_requested", (noctalia.state.get("refresh_requested") or 0) + 1) + end, + }), + }) + end + local emptyKey = rulesCount == 0 and "panel.empty_no_rules" or "panel.empty" + return ui.column(CENTERED_LAYOUT, { + ui.label({ text = tr(emptyKey), color = "on_surface_variant", textAlign = "center" }), + }) + end + return ui.scroll({ flexGrow = 1, gap = 10, align = "stretch" }, groupedRows()) +end + +render = function() + if not isOpen then + return + end + + local children = { + ui.row({ align = "center", justify = "space_between", gap = 8 }, { + ui.row({ gap = 8, align = "center", flexGrow = 1 }, { + ui.glyph({ name = "brand-github", size = 20, color = "primary" }), + ui.label({ text = tr("title"), fontSize = 16, fontWeight = "bold", color = "on_surface" }), + badge(tostring(#prs), "primary", "0.14"), + }), + ui.label({ + text = lastUpdatedText ~= "" and tr("panel.updated_at", { time = lastUpdatedText }) or "", + fontSize = 11, + color = "on_surface_variant", + }), + ui.button({ + glyph = "refresh", + variant = "ghost", + enabled = fetchStatus ~= "loading", + onClick = function() + noctalia.state.set("refresh_requested", (noctalia.state.get("refresh_requested") or 0) + 1) + end, + }), + ui.button({ glyph = "close", onClick = panel.close }), + }), + } + + -- Partial rule failure: data is fresh but incomplete — warn without blocking. + if errorMessage ~= "" and fetchStatus ~= "error" then + table.insert(children, ui.label({ text = errorMessage, fontSize = 11, color = "error", maxLines = 2 })) + end + + table.insert(children, body()) + + panel.render(ui.column({ flexGrow = 1, gap = 12, align = "stretch" }, children)) +end + +noctalia.state.watch("fetch_status", function() + syncFromState() + render() +end) + +function onOpen(_context) + syncFromState() + isOpen = true + render() +end + +function onClose() + isOpen = false +end diff --git a/github-prs/plugin.toml b/github-prs/plugin.toml new file mode 100644 index 00000000..0e9a34ed --- /dev/null +++ b/github-prs/plugin.toml @@ -0,0 +1,73 @@ +# GitHub Pull Requests — pull requests in your bar, fetched via the gh CLI. +# Three entries: a headless fetch service (owns all gh calls, publishes shared +# state), a bar widget (count + status tint, click toggles the panel), and a +# panel (PRs grouped by repo with CI status and review badges). + +id = "raycursive/github-prs" +name = "GitHub Pull Requests" +version = "0.3.0" +plugin_api = 9 +author = "raycursive" +license = "MIT" +dependencies = ["gh", "xdg-open"] +tags = ["bar", "development", "panel", "productivity", "service"] +icon = "git-pull-request" +description = "Track matching GitHub pull requests from the bar, including CI and review status, using the gh CLI." + +# Each rule is a GitHub search query fragment; the service prefixes it with +# "is:pr is:open" (and "archived:false" unless the rule mentions archived:). +[[setting]] +key = "rules" +type = "string_list" +label_key = "settings.rules.label" +description_key = "settings.rules.description" +default = ["author:@me"] + +[[setting]] +key = "excluded_repositories" +type = "string_list" +label_key = "settings.excluded_repositories.label" +description_key = "settings.excluded_repositories.description" +default = [] + +[[setting]] +key = "refresh_interval" +type = "int" +label_key = "settings.refresh_interval.label" +description_key = "settings.refresh_interval.description" +default = 180 +min = 30 +max = 3600 + +# Headless service: runs one gh call per rule, merges + dedupes, publishes to +# the plugin's shared state for the widget and panel. +[[service]] +id = "fetch" +entry = "fetch.luau" + +[[widget]] +id = "bar" +entry = "bar.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + default = "git-pull-request" + + [[widget.setting]] + key = "hide_when_zero" + type = "bool" + label_key = "settings.hide_when_zero.label" + description_key = "settings.hide_when_zero.description" + default = false + +# Open with the bar widget click, or: noctalia msg panel-toggle raycursive/github-prs:panel +[[panel]] +id = "panel" +entry = "panel.luau" +width = 660 +height = 640 +placement = "attached" +position = "auto" +open_near_click = true diff --git a/github-prs/thumbnail.webp b/github-prs/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..c5ea0b512153c36aeb5094be8e6e6d469e1a4096 GIT binary patch literal 42500 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 l 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 literal 0 HcmV?d00001 diff --git a/github-prs/translations/en.json b/github-prs/translations/en.json new file mode 100644 index 00000000..a0afb1b3 --- /dev/null +++ b/github-prs/translations/en.json @@ -0,0 +1,47 @@ +{ + "title": "GitHub Pull Requests", + "settings": { + "rules": { + "label": "Search rules", + "description": "One GitHub search query fragment per entry; results are merged. Each is prefixed with 'is:pr is:open' (and 'archived:false' unless you set archived: yourself). Examples: author:@me · review-requested:@me org:acme · assignee:@me -repo:acme/legacy draft:false" + }, + "excluded_repositories": { + "label": "Excluded repositories", + "description": "Repository names to hide after fetching. * is supported; names without an owner match every organization. Examples: legacy-* · acme/private-*" + }, + "refresh_interval": { + "label": "Refresh interval (seconds)", + "description": "How often pull requests are re-fetched." + }, + "glyph": { + "label": "Bar glyph" + }, + "hide_when_zero": { + "label": "Hide when empty", + "description": "Hide the bar widget when there are no open pull requests." + } + }, + "panel": { + "updated_at": "Updated {time}", + "loading": "Loading pull requests…", + "empty": "No open pull requests. All clear!", + "empty_no_rules": "No search rules configured. Add rules in Settings → Plugins → GitHub Pull Requests.", + "error_title": "Couldn't fetch pull requests", + "retry": "Retry", + "partial_warning": "{failed} of {total} rules failed: {message}", + "truncated_note": "…{count} more matches not shown (per-rule cap is 50)", + "more_rows": "…and {count} more", + "draft": "Draft", + "review": { + "approved": "Approved", + "changes_requested": "Changes requested", + "review_required": "Review needed" + } + }, + "error": { + "gh_missing": "GitHub CLI (gh) not found in PATH. Install it, then run: gh auth login", + "timeout": "gh timed out", + "truncated": "gh output was truncated", + "parse": "Could not parse gh output" + } +}