Display top programming languages on GitHub profiles.
As of
// ==UserScript==
// @name GitHub Top Languages
// @description Display top programming languages on GitHub profiles.
// @icon https://github.githubassets.com/favicons/favicon-dark.svg
// @version 1.1
// @author afkarxyz
// @namespace https://github.com/afkarxyz/userscripts/
// @supportURL https://github.com/afkarxyz/userscripts/issues
// @license MIT
// @match https://github.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// Alternative method
let GITHUB_TOKEN = localStorage.getItem("gh_token") || ""; // Change it to: let GITHUB_TOKEN = "your_github_personal_access_token";
const CACHE_DURATION = 60 * 60 * 1000;
window.setGitHubToken = function(token) {
GITHUB_TOKEN = token;
localStorage.setItem("gh_token", token);
console.log("GitHub token has been set successfully!");
console.log("Refresh the page to see the changes.");
};
window.clearGitHubToken = function() {
GITHUB_TOKEN = "";
localStorage.removeItem("gh_token");
console.log("GitHub token has been cleared!");
};
function getCachedData(key) {
const cachedItem = localStorage.getItem(key);
if (!cachedItem) return null;
try {
const { data, timestamp } = JSON.parse(cachedItem);
if (Date.now() - timestamp < CACHE_DURATION) {
return data;
}
localStorage.removeItem(key);
return null;
} catch (e) {
console.error("Error parsing cached data:", e);
localStorage.removeItem(key);
return null;
}
}
function setCachedData(key, data) {
const cacheItem = {
data,
timestamp: Date.now()
};
localStorage.setItem(key, JSON.stringify(cacheItem));
}
window.clearLanguageCache = function() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('gh_langs_') || key.startsWith('gh_colors')) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
console.log("Language cache has been cleared!");
};
const COLORS_URL = "https://raw.githubusercontent.com/afkarxyz/userscripts/refs/heads/main/assets/github/colors.json";
let lastUsername = null;
async function getLanguageColors() {
const cachedColors = getCachedData('gh_colors');
if (cachedColors) {
return cachedColors;
}
try {
const res = await fetch(COLORS_URL);
const colors = await res.json();
setCachedData('gh_colors', colors);
return colors;
} catch (e) {
console.error("Failed to fetch language colors:", e);
return {};
}
}
async function fetchLanguages(username, isOrg = false) {
const cacheKey = `gh_langs_${username}_${isOrg ? 'org' : 'user'}`;
const cachedLangs = getCachedData(cacheKey);
if (cachedLangs) {
console.log(`Using cached language data for ${username}`);
return cachedLangs;
}
console.log(`Fetching fresh language data for ${username}`);
const repos = [];
let page = 1;
const headers = GITHUB_TOKEN
? { Authorization: `token ${GITHUB_TOKEN}` }
: {};
const apiUrl = isOrg
? `https://api.github.com/orgs/${username}/repos`
: `https://api.github.com/users/${username}/repos`;
while (true) {
try {
const res = await fetch(`${apiUrl}?per_page=100&page=${page}`, {
headers
});
if (!res.ok) {
console.error(`GitHub API Error: ${res.status} ${res.statusText}`);
break;
}
const data = await res.json();
if (!Array.isArray(data) || data.length === 0) break;
repos.push(...data);
page++;
const rateLimit = res.headers.get('X-RateLimit-Remaining');
if (rateLimit && parseInt(rateLimit) <= 0) {
console.warn("GitHub API rate limit reached. Set a token using window.setGitHubToken()");
break;
}
} catch (e) {
console.error("Error fetching GitHub repositories:", e);
break;
}
}
const languageCount = {};
let total = 0;
for (const repo of repos) {
if (repo.language) {
total++;
languageCount[repo.language] = (languageCount[repo.language] || 0) + 1;
}
}
const result = Object.entries(languageCount)
.map(([lang, count]) => ({
lang,
count,
percent: (count / total * 100).toFixed(2)
}))
.sort((a, b) => b.count - a.count);
setCachedData(cacheKey, result);
return result;
}
function createLangBar({ lang, percent }, colorMap, hidden = false) {
const color = (colorMap[lang] && colorMap[lang].color) || "#ccc";
const container = document.createElement("div");
container.style.marginBottom = "8px";
container.style.display = hidden ? "none" : "block";
container.className = "lang-bar";
if (hidden) container.classList.add("lang-hidden");
container.innerHTML = `
<div style="height: 8px; width: 100%; background-color: #59636e; border-radius: 4px; margin-bottom: 4px;">
<div style="width: ${percent}%; height: 100%; background-color: ${color}; border-radius: 4px;"></div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 13px;">
<div style="display: flex; align-items: center;">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 6px;"></span>
<span>${lang}</span>
</div>
<span>${percent}%</span>
</div>
`;
return container;
}
async function insertLanguageStats() {
const match = window.location.pathname.match(/^\/([^\/]+)$/);
if (!match) return;
const username = match[1];
if (username === lastUsername) return;
lastUsername = username;
try {
const userContainer = document.querySelector('.vcard-names-container');
const orgContainer = document.querySelector('.h2.lh-condensed')?.closest('.flex-1.d-flex.flex-column');
const container = userContainer || orgContainer;
if (!container) return;
const isOrg = !userContainer;
if (container.querySelector('.lang-bar')) return;
const loadingEl = document.createElement("div");
loadingEl.id = "lang-stats-loading";
loadingEl.textContent = "Loading...";
loadingEl.style.marginTop = "12px";
loadingEl.style.fontSize = "13px";
loadingEl.style.color = "#666";
container.appendChild(loadingEl);
const statsWrapper = document.createElement("div");
statsWrapper.id = "lang-stats-wrapper";
statsWrapper.style.marginTop = "12px";
statsWrapper.style.width = "100%";
const [langs, colors] = await Promise.all([
fetchLanguages(username, isOrg),
getLanguageColors()
]);
const loadingIndicator = document.getElementById("lang-stats-loading");
if (loadingIndicator) loadingIndicator.remove();
if (langs.length === 0) {
statsWrapper.innerHTML = "<div style='font-size: 13px; color: #666;'>No language data available</div>";
container.appendChild(statsWrapper);
return;
}
const topLangs = langs.slice(0, 3);
const restLangs = langs.slice(3);
topLangs.forEach(langData => {
statsWrapper.appendChild(createLangBar(langData, colors));
});
restLangs.forEach(langData => {
statsWrapper.appendChild(createLangBar(langData, colors, true));
});
if (restLangs.length > 0) {
const toggleBtn = document.createElement("button");
toggleBtn.textContent = "Show more...";
toggleBtn.style.background = "none";
toggleBtn.style.border = "none";
toggleBtn.style.color = "#4493f8";
toggleBtn.style.cursor = "pointer";
toggleBtn.style.padding = "0";
toggleBtn.style.fontSize = "13px";
toggleBtn.style.marginTop = "5px";
toggleBtn.onclick = () => {
const hidden = statsWrapper.querySelectorAll('.lang-hidden');
hidden.forEach(e => e.style.display = "block");
toggleBtn.remove();
};
statsWrapper.appendChild(toggleBtn);
}
container.appendChild(statsWrapper);
} catch (error) {
console.error("Error inserting language stats:", error);
}
}
let currentPath = location.pathname;
const observer = new MutationObserver(() => {
if (location.pathname !== currentPath) {
currentPath = location.pathname;
setTimeout(insertLanguageStats, 800);
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(insertLanguageStats, 500);
})();