Greasy Fork is available in English.

GitHub Top Languages

Display top programming languages on GitHub profiles.

// ==UserScript==
// @name         GitHub Top Languages
// @description  Display top programming languages on GitHub profiles.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.2
// @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';

    // Hardcode: let GITHUB_TOKEN = "your_github_personal_access_token";
    let GITHUB_TOKEN = localStorage.getItem("gh_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 fetchLanguagesGraphQL(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;
        }

        if (!GITHUB_TOKEN) {
            console.warn("GitHub GraphQL API requires a token. Please set one using window.setGitHubToken()");
            return [];
        }

        console.log(`Fetching fresh language data for ${username} using GraphQL`);

        const query = isOrg ? `
            query OrgRepoLanguages($orgName: String!, $cursor: String) {
                organization(login: $orgName) {
                    repositories(first: 100, after: $cursor, privacy: PUBLIC, isFork: false) {
                        pageInfo {
                            hasNextPage
                            endCursor
                        }
                        nodes {
                            languages(first: 100, orderBy: {field: SIZE, direction: DESC}) {
                                edges {
                                    size
                                    node {
                                        name
                                        color
                                    }
                                }
                                totalSize
                            }
                        }
                    }
                }
            }
        ` : `
            query UserRepoLanguages($login: String!, $cursor: String) {
                user(login: $login) {
                    repositories(first: 100, after: $cursor, privacy: PUBLIC, ownerAffiliations: OWNER, isFork: false) {
                        pageInfo {
                            hasNextPage
                            endCursor
                        }
                        nodes {
                            languages(first: 100, orderBy: {field: SIZE, direction: DESC}) {
                                edges {
                                    size
                                    node {
                                        name
                                        color
                                    }
                                }
                                totalSize
                            }
                        }
                    }
                }
            }
        `;

        const allLanguages = {};
        let hasNextPage = true;
        let cursor = null;

        try {
            while (hasNextPage) {
                const variables = isOrg
                    ? { orgName: username, cursor }
                    : { login: username, cursor };

                const response = await fetch("https://api.github.com/graphql", {
                    method: "POST",
                    headers: {
                        "Authorization": `Bearer ${GITHUB_TOKEN}`,
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({ query, variables })
                });

                if (!response.ok) {
                    throw new Error(`GitHub API error: ${response.status}`);
                }

                const data = await response.json();

                if (data.errors) {
                    console.error("GraphQL errors:", data.errors);
                    break;
                }

                const entityData = isOrg ? data.data?.organization : data.data?.user;
                if (!entityData) break;

                const repositories = entityData.repositories;
                const pageInfo = repositories.pageInfo;

                repositories.nodes.forEach(repo => {
                    if (!repo.languages.edges) return;
                    
                    repo.languages.edges.forEach(edge => {
                        const { name, color } = edge.node;
                        const size = edge.size;
                        
                        if (!allLanguages[name]) {
                            allLanguages[name] = { 
                                size: 0,
                                color: color
                            };
                        }
                        allLanguages[name].size += size;
                    });
                });

                hasNextPage = pageInfo.hasNextPage;
                cursor = pageInfo.endCursor;
            }

            const totalSize = Object.values(allLanguages).reduce((sum, lang) => sum + lang.size, 0);
            
            const result = Object.entries(allLanguages)
                .map(([lang, data]) => ({
                    lang,
                    color: data.color,
                    count: data.size,
                    percent: ((data.size / totalSize) * 100).toFixed(2)
                }))
                .sort((a, b) => b.count - a.count);

            setCachedData(cacheKey, result);
            return result;
        } catch (e) {
            console.error("Error fetching languages with GraphQL:", e);
            return [];
        }
    }

    function createLanguageBar(languages, colorMap) {
        const container = document.createElement("div");
        container.style.marginTop = "16px";
        container.style.width = "100%";

        const barContainer = document.createElement("div");
        barContainer.style.display = "flex";
        barContainer.style.height = "8px";
        barContainer.style.width = "100%";
        barContainer.style.borderRadius = "4px";
        barContainer.style.overflow = "hidden";
        barContainer.style.marginBottom = "8px";

        const legendContainer = document.createElement("div");
        legendContainer.style.display = "flex";
        legendContainer.style.flexWrap = "wrap";
        legendContainer.style.fontSize = "12px";

        languages.forEach((langData) => {
            const { lang, percent, color: langColor } = langData;
            const percentNum = parseFloat(percent);

            const color = langColor || (colorMap[lang] && colorMap[lang].color) || "#ccc";

            const segment = document.createElement("div");
            segment.style.backgroundColor = color;
            segment.style.width = `${percentNum}%`;
            segment.style.height = "100%";
            barContainer.appendChild(segment);

            const legendItem = document.createElement("div");
            legendItem.style.display = "flex";
            legendItem.style.alignItems = "center";
            legendItem.style.marginRight = "16px";
            legendItem.style.marginBottom = "4px";

            const colorDot = document.createElement("span");
            colorDot.style.display = "inline-block";
            colorDot.style.width = "8px";
            colorDot.style.height = "8px";
            colorDot.style.backgroundColor = color;
            colorDot.style.borderRadius = "50%";
            colorDot.style.marginRight = "6px";

            const langNameSpan = document.createElement("span");
            langNameSpan.textContent = lang;
            langNameSpan.style.fontWeight = "600";
            
            const percentSpan = document.createElement("span");
            percentSpan.textContent = ` ${percent}%`;
            percentSpan.style.fontWeight = "400";
            
            const langName = document.createElement("span");
            langName.appendChild(langNameSpan);
            langName.appendChild(percentSpan);

            legendItem.appendChild(colorDot);
            legendItem.appendChild(langName);
            legendContainer.appendChild(legendItem);
        });

        container.appendChild(barContainer);
        container.appendChild(legendContainer);

        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('#gh-lang-stats')) 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);

            if (!GITHUB_TOKEN) {
                loadingEl.textContent = "GitHub API token required for language statistics";
                const tokenNotice = document.createElement("div");
                tokenNotice.style.fontSize = "12px";
                tokenNotice.style.color = "#666";
                tokenNotice.style.marginTop = "4px";
                tokenNotice.innerHTML = "Set token with <code>window.setGitHubToken('your_token')</code> in console";
                loadingEl.appendChild(tokenNotice);
                return;
            }

            const [langs, colors] = await Promise.all([
                fetchLanguagesGraphQL(username, isOrg),
                getLanguageColors()
            ]);

            const loadingIndicator = document.getElementById("lang-stats-loading");
            if (loadingIndicator) loadingIndicator.remove();

            if (langs.length === 0) {
                const noDataEl = document.createElement("div");
                noDataEl.id = "gh-lang-stats";
                noDataEl.innerHTML = "<div style='font-size: 13px; color: #666;'>No language data available</div>";
                container.appendChild(noDataEl);
                return;
            }

            const statsWrapper = document.createElement("div");
            statsWrapper.id = "gh-lang-stats";

            const topLangs = langs.slice(0, 10);

            const langBar = createLanguageBar(topLangs, colors);
            statsWrapper.appendChild(langBar);

            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);
})();