Last active
July 13, 2026 09:16
-
-
Save ErichDonGubler/17cdd30c4b40c93596da1d813ef327cb to your computer and use it in GitHub Desktop.
User scripts for various things Erich uses for his work on Firefox's WebGPU implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Copy PR title and link as Markdown | |
| // @namespace https://gist.github.com/ErichDonGubler/17cdd30c4b40c93596da1d813ef327cb | |
| // @version 2026-07-08.1 | |
| // @description Adds a `Copy link` button to PR title controls that copies the PR title and link as a Markdown link (i.e., `[PR Title](https://github.com/…/pull/…)`). | |
| // @author Erich Gubler <erichdongubler@gmail.com> | |
| // @match https://github.com/*/*/pull/* | |
| // @match https://github.com/*/*/issues/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com | |
| // @grant none | |
| // @sandbox DOM | |
| // @source https://gist.githubusercontent.com/ErichDonGubler/17cdd30c4b40c93596da1d813ef327cb/raw/f18629e7b36bdc4408ad86e40cb6a0f91f6054e3/CopyGitHubPROrIssueTitleAsMarkdownLink.js | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| const issueTrackerUrl = | |
| "https://gist.github.com/ErichDonGubler/17cdd30c4b40c93596da1d813ef327cb"; | |
| const bugReportHint = `Bug Erich at <${issueTrackerUrl}> to fix it!`; | |
| // GitHub renders the PR/issue header actions (`PH_Actions`) on the client: the container is | |
| // EMPTY in the initial HTML and only filled after hydration, and is re-rendered/swapped on | |
| // client-side navigation. A single observer (see the bottom of this file) drives (re)installs. | |
| function tryInstall() { | |
| // Only act on issue/PR pages; the observer below calls us on every DOM change. | |
| if (!/\/(?:issues|pull)\/\d+/.test(location.pathname)) { | |
| return; | |
| } | |
| // Anchor to `PH_Actions` itself — the stable, client-rendered actions container — rather | |
| // than a template's parent, which on PRs is a per-feature `data-loading-wrapper` React tears | |
| // down and rebuilds as merge status loads (so our button would never stick there). | |
| const actionButtonStrip = document.querySelector( | |
| '[data-component="PH_Actions"]', | |
| ); | |
| if (!actionButtonStrip) { | |
| return; | |
| } // Header not rendered yet — the observer will call us again. | |
| if (actionButtonStrip.querySelector("[data-vm-copy-link]")) { | |
| return; | |
| } // Already installed. | |
| // The labeled header button we clone for chrome may be an `<a>` (e.g. "New issue" on | |
| // issues) or a `<button>` (e.g. "View status" on PRs), so match `[data-component="Button"]` | |
| // rather than the `button` tag, and prefer one with a visible text label over an icon-only | |
| // button (e.g. a kebab menu or the copy-link icon) so the clone keeps GitHub's icon+text chrome. | |
| const templateButton = | |
| actionButtonStrip.querySelector( | |
| '[data-component="Button"]:has([data-component="text"])', | |
| ) || actionButtonStrip.querySelector('[data-component="Button"], button'); | |
| if (!templateButton) { | |
| return; | |
| } // Header not populated yet. | |
| const copyLinkButton = templateButton.cloneNode(true); | |
| copyLinkButton.setAttribute("data-vm-copy-link", ""); // Marker for the idempotency guard above. | |
| copyLinkButton.setAttribute("type", "button"); // Don't submit a surrounding form. | |
| copyLinkButton.setAttribute("data-variant", "primary"); // Match the `New issue` button. | |
| for (const attr of [ | |
| "id", | |
| "href", | |
| "target", | |
| "aria-haspopup", | |
| "aria-expanded", | |
| "aria-labelledby", | |
| // The PR "View status" template is often mid-load when cloned; shed its loading/disabled | |
| // state so our button isn't inert-looking with a spinner. | |
| "aria-disabled", | |
| "aria-describedby", | |
| "data-inactive", | |
| ]) { | |
| copyLinkButton.removeAttribute(attr); // Don't duplicate the template's id or drag along its behavior. | |
| } | |
| copyLinkButton.setAttribute("data-loading", "false"); | |
| // An `<a>` with no `href` isn't clickable/focusable on its own; make it behave like a button. | |
| if (copyLinkButton.tagName === "A") { | |
| copyLinkButton.setAttribute("role", "button"); | |
| copyLinkButton.setAttribute("tabindex", "0"); | |
| } | |
| // Drop the template's leading/trailing visuals (code icon, dropdown caret, …) up front, then | |
| // keep ONLY the stable `prc-Button-*` Primer classes on what remains, dropping feature-specific | |
| // module classes (`MergeStatusButton-*`, `PullRequestCodeButton-*`, …) that carry alien styling. | |
| copyLinkButton | |
| .querySelectorAll( | |
| '[data-component="leadingVisual"], [data-component="trailingVisual"], [data-component="loadingSpinner"]', | |
| ) | |
| .forEach((el) => el.remove()); | |
| // Use `setAttribute`, NOT `el.className = …`: on SVG elements (any octicon) `className` is a | |
| // read-only `SVGAnimatedString`, so assigning to it throws — which aborted the entire install | |
| // on every PR button (they carry icons), leaving PRs with no button at all. | |
| const keepPrcButton = (el) => { | |
| const kept = (el.getAttribute("class") || "") | |
| .split(/\s+/) | |
| .filter((c) => c.startsWith("prc-Button-")) | |
| .join(" "); | |
| el.setAttribute("class", kept); | |
| }; | |
| keepPrcButton(copyLinkButton); | |
| copyLinkButton.querySelectorAll("[class]").forEach(keepPrcButton); | |
| const textEl = copyLinkButton.querySelector('[data-component="text"]'); | |
| if (textEl) { | |
| textEl.textContent = "Copy link"; | |
| } else { | |
| copyLinkButton.textContent = "Copy link"; | |
| } | |
| // Click feedback: the resting button is green (`primary`). Snap it to grey (`default`) with | |
| // transitions off, then re-enable transitions and set the target variant so it eases back to | |
| // colour over ~1s — the motion reads as a clear "done" pulse, where a green success tint on an | |
| // already-green button would be invisible. Success eases home to green; a hard failure eases to | |
| // red (`danger`) and stays there until the next click. | |
| const FADE_MS = 1000; | |
| const flashTo = (variant) => { | |
| copyLinkButton.style.transition = "none"; | |
| copyLinkButton.setAttribute("data-variant", "default"); // Instant grey. | |
| void copyLinkButton.offsetWidth; // Commit the grey frame before animating away from it. | |
| copyLinkButton.style.transition = `background-color ${FADE_MS}ms ease, border-color ${FADE_MS}ms ease, color ${FADE_MS}ms ease`; | |
| copyLinkButton.setAttribute("data-variant", variant); // Ease back to colour. | |
| }; | |
| copyLinkButton.onclick = (event) => { | |
| // The clone can still match GitHub's delegated click handlers; keep it inert. | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| let link = new URL(document.URL); | |
| let handleTitle = (title) => { | |
| link.hash = ""; // Trim fragment. | |
| link.search = ""; // Trim query params. | |
| let markdownLink = `[${title}](${link})`; | |
| navigator.clipboard.writeText(markdownLink); | |
| copyLinkButton.title = ""; | |
| flashTo("primary"); // Success: pulse, then settle back to green. | |
| }; | |
| const [, org, repo, rawType, number] = link.pathname.split("/"); | |
| const type = rawType === "pull" ? "pulls" : rawType; | |
| fetch(`https://api.github.com/repos/${org}/${repo}/${type}/${number}`) | |
| .then((response) => response.json()) | |
| .then((responseJson) => handleTitle(responseJson.title)) | |
| .catch((e) => { | |
| console.warn( | |
| "Failed to capture issue title via GitHub's REST API:", | |
| e, | |
| ); | |
| try { | |
| console.info("Attempting fallback to editor controls…"); | |
| handleTitle( | |
| document.getElementById("issue_title").attributes.value | |
| .textContent, | |
| ); | |
| } catch (e2) { | |
| console.warn("Ugh, editor fallback for the title also failed:", e2); | |
| const msg = "Failed to capture issue title editor contents."; | |
| console.error(msg + " " + bugReportHint); | |
| copyLinkButton.title = msg; | |
| flashTo("danger"); // Failure: pulse, then settle on red. | |
| } | |
| }); | |
| }; | |
| // Place the button alongside the real action buttons (their nearest common ancestor) so it | |
| // joins the group's flex row and lands consistently — leftmost of the actions — on both issues | |
| // (whose buttons nest under a menu container) and PRs (a flat `.d-flex.gap-2` strip). Prepending | |
| // to `PH_Actions` directly put it outside that group, which each page laid out differently. | |
| const buttons = [ | |
| ...actionButtonStrip.querySelectorAll( | |
| '[data-component="Button"], button', | |
| ), | |
| ]; | |
| let group = buttons[0].parentElement; | |
| for (const button of buttons) { | |
| while (group && !group.contains(button)) { | |
| group = group.parentElement; | |
| } | |
| } | |
| (group || actionButtonStrip).prepend(copyLinkButton); | |
| } | |
| function safeInstall() { | |
| try { | |
| tryInstall(); | |
| } catch (e) { | |
| console.error( | |
| "Copy-link userscript failed to install:", | |
| e, | |
| bugReportHint, | |
| ); | |
| } | |
| } | |
| // One observer handles first paint, React re-renders, AND client-side navigation with a single | |
| // mechanism. Hooking GitHub's router is unreliable: under `@grant none` the page captured the | |
| // native `history.pushState` before us, so patching it never fired — which is why only fresh page | |
| // loads worked. `tryInstall` is a cheap no-op once our button is present. | |
| // | |
| // Use a TRAILING debounce (act only once the DOM goes quiet), NOT a leading one: after a soft | |
| // navigation React re-renders the header in a burst, and inserting mid-burst let React prune our | |
| // button right back out. That made re-install flaky — it raced React and often lost, especially | |
| // on issues, whose actions nest in a container React reconciles more aggressively than the flat | |
| // PR strip. Waiting for quiet means we always insert AFTER React has settled, so it sticks. The | |
| // max-wait guard guarantees we still run under a steady trickle of unrelated mutations (e.g. | |
| // relative-time ticks) instead of being starved forever. | |
| let quietTimer = null; | |
| let maxTimer = null; | |
| const fireInstall = () => { | |
| clearTimeout(quietTimer); | |
| clearTimeout(maxTimer); | |
| quietTimer = null; | |
| maxTimer = null; | |
| safeInstall(); | |
| }; | |
| new MutationObserver(() => { | |
| clearTimeout(quietTimer); | |
| quietTimer = setTimeout(fireInstall, 150); | |
| if (!maxTimer) maxTimer = setTimeout(fireInstall, 1000); | |
| }).observe(document.documentElement, { childList: true, subtree: true }); | |
| safeInstall(); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export default { | |
| rules: { | |
| curly: ["error", "all"], | |
| }, | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Extra Mozilla Phabricator review tools | |
| // @namespace https://gist.github.com/ErichDonGubler/f1cebb52f54dac18c3dece0bb609685c/edit | |
| // @version 2025-07-14.2 | |
| // @description Creates several convenience tools in the subheader bar underneath the title of Phabricator revision views. | |
| // @author Erich Gubler <erichdongubler@gmail.com> | |
| // @match https://phabricator.services.mozilla.com/D* | |
| // @icon data:image/png;base64,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 | |
| // @grant GM_addStyle | |
| // @sandbox DOM | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| // NOTE: Match the class for other informative elements like the `Public` callout. | |
| let loadAllFilesButton = document.createElement("span"); | |
| loadAllFilesButton.className = "policy-header-callout"; | |
| loadAllFilesButton.innerHTML = `<span class="visual-only phui-icon-view phui-font-fa fa-search-plus bluegrey" aria-hidden="true"></span>Load all files`; | |
| loadAllFilesButton.onclick = (_event) => { | |
| document | |
| .querySelectorAll('[data-sigil="differential-load"]') | |
| .forEach((el) => el.click()); | |
| }; | |
| document.querySelector(".phui-header-subheader").append(loadAllFilesButton); | |
| // NOTE: Match the class for other informative elements like the `Public` callout. | |
| let copyLinkButton = document.createElement("span"); | |
| copyLinkButton.className = "policy-header-callout"; | |
| copyLinkButton.innerHTML = `<span class="visual-only phui-icon-view phui-font-fa fa-clipboard bluegrey" aria-hidden="true"></span>Copy link`; | |
| copyLinkButton.onclick = (_event) => { | |
| copyLinkButton.classList.add("link-copied"); | |
| copyLinkButton.childNodes[0].classList.replace("bluegrey", "green"); | |
| let url = new URL(document.URL); | |
| url.hash = ""; // Erase the fragment. | |
| url.search = ""; // Erase query params. | |
| let revisionId = url.pathname.split("/").pop(); | |
| let revisionTitle = document.querySelector( | |
| ".phui-header-header > span:has(a.bug-number)", | |
| ).textContent; | |
| let link = `[${revisionId}: ${revisionTitle}](${url})`; | |
| navigator.clipboard.writeText(link); | |
| }; | |
| GM_addStyle( | |
| ".policy-header-callout.link-copied { background: greenyellow; color: green; transition: ease-in-out 0.25s }", | |
| ); | |
| document.querySelector(".phui-header-subheader").append(copyLinkButton); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment