Replaces GitHub's boring round code language icons with Material Design Icons.
// ==UserScript== // @name GitHub Code Language Icons // @description Replaces GitHub's boring round code language icons with Material Design Icons. // @icon https://github.githubassets.com/favicons/favicon-dark.svg // @version 1.9 // @author afkarxyz // @namespace https://github.com/afkarxyz/misc-scripts/ // @supportURL https://github.com/afkarxyz/misc-scripts/issues // @license MIT // @match https://github.com/* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; const CONFIG = { case_01: true, // The average of all icons case_02: true, // Organization's Repositories (https://github.com/orgs/yt-dlp/repositories) case_03: true, // Languages case_04: true, // Other Languages case_05: true, // Filter by (https://github.com/search?q=spotify&type=code) case_06: true, // More languages... (https://github.com/search?q=spotify&type=repositories) case_07: true // Search's Languages (https://github.com/search?q=spotify&type=repositories) }; const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/'; const ICON_BASE_URL = `${BASE_URL}icons/`; let languageMappings = {}; async function fetchLanguageMappings() { const cacheKey = 'githubLanguageRemapCache'; const currentTime = Date.now(); const cachedData = JSON.parse(GM_getValue(cacheKey, '{}')); if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) { return cachedData.mappings; } try { const response = await fetch(`${BASE_URL}remap.json`); const data = await response.json(); GM_setValue(cacheKey, JSON.stringify({ mappings: data.iconRemap, timestamp: currentTime })); return data.iconRemap; } catch (error) { console.error('Failed to fetch language mappings:', error); return cachedData.mappings || {}; } } function normalizeLanguageName(language) { const normalizedLanguage = language.toLowerCase(); for (const [iconName, languageList] of Object.entries(languageMappings)) { if (languageList.includes(normalizedLanguage)) { return iconName; } } return normalizedLanguage; } async function fetchAvailableIcons() { const cacheKey = 'githubLanguageIconsCache'; const currentTime = Date.now(); const cachedData = JSON.parse(GM_getValue(cacheKey, '{}')); if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) { return cachedData.fileTypes; } try { const response = await fetch(`${BASE_URL}icons.json`); const data = await response.json(); GM_setValue(cacheKey, JSON.stringify({ fileTypes: data.fileTypes, timestamp: currentTime })); return data.fileTypes; } catch (error) { console.error('Failed to fetch icon list:', error); return cachedData.fileTypes || []; } } async function replaceLanguageIcons() { let availableIcons; try { availableIcons = await fetchAvailableIcons(); } catch (error) { console.error('Error getting available icons:', error); return; } const processedElements = new Set(); async function replaceOtherIcon(targetSvg) { if (!CONFIG.case_04) return; try { const response = await fetch(`${ICON_BASE_URL}other.svg`); const svgText = await response.text(); const parser = new DOMParser(); const svgDoc = parser.parseFromString(svgText, 'image/svg+xml'); const newSvg = svgDoc.querySelector('svg'); const attributes = targetSvg.attributes; for (let i = 0; i < attributes.length; i++) { const attr = attributes[i]; if (attr.name !== 'class') { newSvg.setAttribute(attr.name, attr.value); } } newSvg.setAttribute('width', '16'); newSvg.setAttribute('height', '16'); newSvg.setAttribute('viewBox', '0 0 24 24'); const innerGroup = newSvg.querySelector('g') || newSvg.querySelector('path'); if (innerGroup) { innerGroup.setAttribute('transform', 'scale(0.67)'); } const targetClasses = Array.from(targetSvg.classList); targetClasses.forEach(className => { newSvg.classList.add(className); }); newSvg.style.width = '16px'; newSvg.style.height = '16px'; newSvg.style.minWidth = '16px'; newSvg.style.minHeight = '16px'; targetSvg.parentNode.replaceChild(newSvg, targetSvg); } catch (error) { console.error('Failed to replace Other icon:', error); } } function processElement(element) { if (processedElements.has(element)) return; processedElements.add(element); let langElement, language; // Case 1: The average of all icons if (CONFIG.case_01 && element.matches('.repo-language-color')) { const languageSpan = element.parentElement.querySelector('[itemprop="programmingLanguage"]'); if (languageSpan && !languageSpan.dataset.iconProcessed) { language = normalizeLanguageName(languageSpan.textContent); if (availableIcons.includes(language)) { const iconImg = document.createElement('img'); iconImg.src = `${ICON_BASE_URL}${language}.svg`; iconImg.alt = `${language} icon`; iconImg.width = 16; iconImg.height = 16; iconImg.style.marginRight = '2px'; iconImg.style.verticalAlign = 'sub'; element.parentElement.insertBefore(iconImg, element); element.remove(); languageSpan.dataset.iconProcessed = 'true'; } } } // Case 2: Organization's Repositories else if (CONFIG.case_02 && element.matches('.Box-sc-g0xbh4-0.fCvgBf')) { const languageSpan = element.querySelector('.prc-Text-Text-0ima0'); if (languageSpan && !languageSpan.dataset.iconProcessed) { const languageText = languageSpan.textContent.trim(); language = normalizeLanguageName(languageText); element.innerHTML = ''; const newLanguageSpan = document.createElement('span'); newLanguageSpan.className = 'Box-sc-g0xbh4-0 fVplbS prc-Text-Text-0ima0'; newLanguageSpan.textContent = languageText; if (availableIcons.includes(language)) { const iconImg = document.createElement('img'); iconImg.src = `${ICON_BASE_URL}${language}.svg`; iconImg.alt = `${language} icon`; iconImg.width = 16; iconImg.height = 16; iconImg.style.verticalAlign = 'middle'; element.appendChild(iconImg); } element.appendChild(newLanguageSpan); newLanguageSpan.dataset.iconProcessed = 'true'; } } // Case 3: Languages else if (CONFIG.case_03 && element.matches('.d-inline')) { langElement = element.querySelector('.text-bold'); if (!langElement || langElement.textContent.toLowerCase() === 'other' || element.dataset.iconChecked) return; language = normalizeLanguageName(langElement.textContent); element.dataset.iconChecked = 'true'; const svg = element.querySelector('svg'); if (!svg || !availableIcons.includes(language)) return; const img = document.createElement('img'); img.src = `${ICON_BASE_URL}${language}.svg`; img.width = 16; img.height = 16; img.className = 'mr-2'; img.style.verticalAlign = 'middle'; svg.parentNode.replaceChild(img, svg); } // Case 4: Other Languages else if (CONFIG.case_04 && element.matches('.octicon-dot-fill')) { const parentElement = element.closest('.d-inline'); if (parentElement) { const languageElement = parentElement.querySelector('.text-bold'); if (languageElement && languageElement.textContent.toLowerCase() === 'other' && !element.dataset.iconProcessed) { element.dataset.iconProcessed = 'true'; replaceOtherIcon(element); } } } // Case 5: Filter by else if (CONFIG.case_05 && element.matches('.Box-sc-g0xbh4-0.hjDqIa')) { const languageSpan = element.nextElementSibling; if (languageSpan && languageSpan.getAttribute('aria-label') && !languageSpan.dataset.iconProcessed) { language = normalizeLanguageName(languageSpan.getAttribute('aria-label').replace(' language', '')); if (availableIcons.includes(language)) { const iconImg = document.createElement('img'); iconImg.src = `${ICON_BASE_URL}${language}.svg`; iconImg.alt = `${language} icon`; iconImg.width = 16; iconImg.height = 16; iconImg.style.marginRight = '4px'; iconImg.style.verticalAlign = 'middle'; element.style.display = 'none'; languageSpan.parentNode.insertBefore(iconImg, languageSpan); languageSpan.dataset.iconProcessed = 'true'; } } } // Case 6: More languages... else if (CONFIG.case_06 && element.matches('.ActionListItem-visual.ActionListItem-visual--leading')) { const languageLabel = element.closest('.ActionListContent')?.querySelector('.ActionListItem-label.text-normal'); if (!languageLabel || !languageLabel.textContent || element.dataset.iconChecked) return; language = normalizeLanguageName(languageLabel.textContent); element.dataset.iconChecked = 'true'; const colorDiv = element.querySelector('div'); if (!colorDiv || !availableIcons.includes(language)) return; const img = document.createElement('img'); img.src = `${ICON_BASE_URL}${language}.svg`; img.width = 16; img.height = 16; img.style.verticalAlign = 'middle'; colorDiv.replaceWith(img); } // Case 7: Search's Languages else if (CONFIG.case_07 && element.matches('.prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-')) { if (element.dataset.iconChecked) return; element.dataset.iconChecked = 'true'; const listItem = element.closest('.prc-ActionList-ActionListItem-uq6I7'); if (!listItem) return; const languageDiv = listItem.querySelector('.Truncate__StyledTruncate-sc-23o1d2-0'); if (!languageDiv || !languageDiv.title) return; language = normalizeLanguageName(languageDiv.title); const colorDiv = element.querySelector('.Box-sc-g0xbh4-0'); if (!colorDiv || !availableIcons.includes(language)) return; const img = document.createElement('img'); img.src = `${ICON_BASE_URL}${language}.svg`; img.alt = `${language} icon`; img.width = 16; img.height = 16; img.style.verticalAlign = 'middle'; colorDiv.replaceWith(img); } } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches('.d-inline, .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill, .prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-')) { processElement(node); } else { node.querySelectorAll('.d-inline, .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill, .prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-').forEach(processElement); } } } } }); observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('.d-inline, .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill, .prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-').forEach(processElement); } async function init() { languageMappings = await fetchLanguageMappings(); replaceLanguageIcons(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();