Skip to content

Instantly share code, notes, and snippets.

@vikytech
Last active July 27, 2026 10:25
Show Gist options
  • Select an option

  • Save vikytech/cba4c758c674d1896cff4d1ac08e716a to your computer and use it in GitHub Desktop.

Select an option

Save vikytech/cba4c758c674d1896cff4d1ac08e716a to your computer and use it in GitHub Desktop.
Script to display the Name instead of the username across github (PR/Comments/Commits/Branch/Action etc).
(() => {
'use strict';
/*
* Display modes:
* "name-only" → John Smith
* "name-and-username" → John Smith (@AA47797)
*/
const DISPLAY_MODE = 'name-only';
const CACHE_PREFIX = 'github-display-name:';
const CACHE_DURATION_MS = 30 * 24 * 60 * 60 * 1000;
const pendingRequests = new Map();
const RESERVED_PATHS = new Set([
'actions',
'admin',
'apps',
'codespaces',
'dashboard',
'enterprises',
'explore',
'features',
'issues',
'login',
'logout',
'marketplace',
'new',
'notifications',
'organizations',
'orgs',
'pulls',
'search',
'security',
'settings',
'site',
'sponsors',
'topics',
]);
function normalizeLogin(value) {
return String(value || '')
.trim()
.replace(/^@/, '')
.replace(/[()[\]{},:;.!?]+$/g, '');
}
function isValidLogin(login) {
return (
typeof login === 'string' &&
/^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i.test(login) &&
!RESERVED_PATHS.has(login.toLowerCase())
);
}
function extractLoginFromHovercard(value) {
if (!value) {
return null;
}
const match = value.match(/\/users\/([^/?#]+)\/hovercard/i);
if (!match) {
return null;
}
try {
const login = decodeURIComponent(match[1]);
return isValidLogin(login) ? login : null;
} catch {
return null;
}
}
function getLoginFromElement(element) {
if (!(element instanceof Element)) {
return null;
}
const directValues = [
element.getAttribute('data-login'),
element.getAttribute('data-user-login'),
element.getAttribute('data-username'),
];
for (const value of directValues) {
const login = normalizeLogin(value);
if (isValidLogin(login)) {
return login;
}
}
const ownHovercardLogin = extractLoginFromHovercard(
element.getAttribute('data-hovercard-url')
);
if (ownHovercardLogin) {
return ownHovercardLogin;
}
if (element instanceof HTMLAnchorElement) {
try {
const url = new URL(element.href, location.origin);
if (url.origin !== location.origin) {
return null;
}
const author = normalizeLogin(url.searchParams.get('author'));
if (isValidLogin(author)) {
return author;
}
const segments = url.pathname.split('/').filter(Boolean);
if (segments.length === 1) {
const login = decodeURIComponent(segments[0]);
if (isValidLogin(login)) {
return login;
}
}
} catch {
return null;
}
}
return null;
}
function getCachedName(login) {
try {
const key = CACHE_PREFIX + login.toLowerCase();
const raw = localStorage.getItem(key);
if (!raw) {
return undefined;
}
const cached = JSON.parse(raw);
if (
!cached ||
typeof cached.savedAt !== 'number' ||
Date.now() - cached.savedAt > CACHE_DURATION_MS
) {
localStorage.removeItem(key);
return undefined;
}
return typeof cached.name === 'string' ? cached.name : '';
} catch {
return undefined;
}
}
function cacheName(login, name) {
try {
localStorage.setItem(
CACHE_PREFIX + login.toLowerCase(),
JSON.stringify({
name,
savedAt: Date.now(),
})
);
} catch {
// Continue without caching.
}
}
async function fetchDisplayName(login) {
const cachedName = getCachedName(login);
if (cachedName !== undefined) {
return cachedName;
}
const key = login.toLowerCase();
if (pendingRequests.has(key)) {
return pendingRequests.get(key);
}
const request = (async () => {
try {
const response = await fetch(`/${encodeURIComponent(login)}`, {
method: 'GET',
credentials: 'same-origin',
headers: {
Accept: 'text/html',
},
});
if (!response.ok) {
console.debug(
`[GitHub Names] Profile request failed for ${login}:`,
response.status
);
return '';
}
const html = await response.text();
const profileDocument = new DOMParser().parseFromString(
html,
'text/html'
);
const selectors = [
'[itemprop="name"]',
'.vcard-fullname',
'[data-testid="profile-name"]',
'.js-profile-editable-area .p-name',
];
let displayName = '';
for (const selector of selectors) {
const value = profileDocument
.querySelector(selector)
?.textContent?.trim();
if (value) {
displayName = value;
break;
}
}
cacheName(login, displayName);
return displayName;
} catch (error) {
console.debug(
`[GitHub Names] Could not load profile for ${login}:`,
error
);
return '';
} finally {
pendingRequests.delete(key);
}
})();
pendingRequests.set(key, request);
return request;
}
function formatDisplayName(name, login) {
return DISPLAY_MODE === 'name-and-username' ? `${name} (@${login})` : name;
}
function getUserColours(login) {
let hash = 0;
for (let index = 0; index < login.length; index++) {
hash = login.charCodeAt(index) + ((hash << 5) - hash);
hash |= 0;
}
const hue = Math.abs(hash) % 360;
return {
background: `hsl(${hue}, 72%, 86%)`,
border: `hsl(${hue}, 52%, 68%)`,
text: `hsl(${hue}, 55%, 20%)`,
};
}
function applyUserStyle(element, login) {
const colours = getUserColours(login);
element.style.setProperty(
'background-color',
colours.background,
'important'
);
element.style.setProperty('color', colours.text, 'important');
element.style.setProperty(
'border',
`1px solid ${colours.border}`,
'important'
);
element.style.setProperty('border-radius', '999px', 'important');
element.style.setProperty('padding', '1px 6px', 'important');
element.style.setProperty('margin', '1px 2px', 'important');
element.style.setProperty('font-weight', '600', 'important');
element.style.setProperty('line-height', '1', 'important');
element.style.setProperty('display', 'inline-block', 'important');
element.style.setProperty('text-decoration', 'none', 'important');
element.style.setProperty('white-space', 'nowrap', 'important');
}
function visibleTextMatchesLogin(element, login) {
const text = normalizeLogin(element.textContent);
return text.toLowerCase() === login.toLowerCase();
}
function isSuitableTarget(element, login) {
if (!(element instanceof Element)) {
return false;
}
if (element.dataset.githubDisplayNameApplied === 'true') {
return false;
}
/*
* Only modify elements whose visible content is the username.
* This prevents replacing a parent containing both name and username.
*/
return visibleTextMatchesLogin(element, login);
}
async function processElement(element) {
if (!(element instanceof Element)) {
return;
}
if (element.dataset.githubDisplayNameApplied === 'true') {
return;
}
const login = getLoginFromElement(element);
if (!login || !isSuitableTarget(element, login)) {
return;
}
/*
* Mark immediately so MutationObserver scans cannot process this
* element again while the profile request is still loading.
*/
element.dataset.githubDisplayNameApplied = 'loading';
element.dataset.originalGithubLogin = login;
const originalText = element.textContent;
const displayName = await fetchDisplayName(login);
if (!element.isConnected) {
return;
}
if (!displayName) {
delete element.dataset.githubDisplayNameApplied;
return;
}
if (
element.textContent !== originalText &&
normalizeLogin(element.textContent).toLowerCase() !== login.toLowerCase()
) {
delete element.dataset.githubDisplayNameApplied;
return;
}
element.textContent = formatDisplayName(displayName, login);
element.dataset.githubDisplayNameApplied = 'true';
applyUserStyle(element, login);
element.setAttribute(
'title',
`${displayName} — GitHub username: @${login}`
);
}
function scan(root = document) {
if (!(root instanceof Document || root instanceof Element)) {
return;
}
/*
* More specific elements come first.
* We do not process parent hovercard containers separately.
*/
const selector = [
"a[data-hovercard-url*='/users/']",
"a[data-hovercard-type='user']",
'a[data-login]',
'a[data-user-login]',
'a[data-username]',
"a[href*='?author=']",
"a[href*='&author=']",
"a[href^='/']",
].join(',');
if (root instanceof Element && root.matches(selector)) {
void processElement(root);
}
const elements = root.querySelectorAll?.(selector) || [];
for (const element of elements) {
void processElement(element);
}
}
let scanQueued = false;
function queueScan() {
if (scanQueued) {
return;
}
scanQueued = true;
requestAnimationFrame(() => {
scanQueued = false;
scan(document);
});
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
scan(node);
}
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
document.addEventListener('turbo:load', queueScan);
document.addEventListener('turbo:render', queueScan);
document.addEventListener('pjax:end', queueScan);
/*
* Supports GitHub components rendered after initial navigation.
*/
setInterval(queueScan, 3000);
scan();
console.log('[GitHub Names] Name-only display enabled without duplicates.');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment