// ==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.7
// @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 BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/';
const ICON_BASE_URL = `${BASE_URL}icons/`;
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 || {};
}
}
let languageMappings = {};
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) {
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: Action List Items
if (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 2: Inline Displays
else if (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 3: Repository Language Indicators
else if (element.matches('.f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"]')) {
language = normalizeLanguageName(element.textContent);
if (!availableIcons.includes(language)) return;
const parentSpan = element.parentElement;
const colorSpan = parentSpan.querySelector('.repo-language-color');
const img = document.createElement('img');
img.src = `${ICON_BASE_URL}${language}.svg`;
img.width = 16;
img.height = 16;
img.style.marginRight = '2px';
img.style.verticalAlign = 'sub';
if (colorSpan) {
colorSpan.parentNode.insertBefore(img, colorSpan);
colorSpan.remove();
}
}
// Case 4: No Wrap Language Spans
else if (element.matches('.mb-3 .no-wrap span[itemprop="programmingLanguage"]')) {
language = normalizeLanguageName(element.textContent);
if (!availableIcons.includes(language)) return;
const parentSpan = element.parentElement;
const colorSpan = parentSpan.querySelector('.repo-language-color');
const img = document.createElement('img');
img.src = `${ICON_BASE_URL}${language}.svg`;
img.width = 16;
img.height = 16;
img.style.marginRight = '4px';
const flexContainer = document.createElement('span');
flexContainer.style.display = 'inline-flex';
flexContainer.style.alignItems = 'center';
if (colorSpan) {
colorSpan.remove();
flexContainer.appendChild(img);
flexContainer.appendChild(element.cloneNode(true));
parentSpan.replaceWith(flexContainer);
}
}
// Case 5: File Type Boxes
else if (element.matches('.Box-sc-g0xbh4-0.fCvgBf')) {
const preElements = element.querySelectorAll('.Box-sc-g0xbh4-0');
preElements.forEach(preElement => {
preElement.style.display = 'none';
});
const languageSpan = element.querySelector('.Text__StyledText-sc-17v1xeu-0');
if (languageSpan && !languageSpan.dataset.iconProcessed) {
language = normalizeLanguageName(languageSpan.textContent.trim());
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 = 'middle';
languageSpan.insertAdjacentElement('beforebegin', iconImg);
languageSpan.dataset.iconProcessed = 'true';
}
}
}
// Case 6: Repository Language Colors
else if (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 7: Special File Type Boxes
else if (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 8: Other Language Icons
else if (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);
}
}
}
}
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, .f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"], .mb-3 .no-wrap span[itemprop="programmingLanguage"], .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill')) {
processElement(node);
} else {
node.querySelectorAll('.d-inline, .f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"], .mb-3 .no-wrap span[itemprop="programmingLanguage"], .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill').forEach(processElement);
}
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
document.querySelectorAll('.d-inline, .f6.color-fg-muted .repo-language-color + span[itemprop="programmingLanguage"], .mb-3 .no-wrap span[itemprop="programmingLanguage"], .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill').forEach(processElement);
}
// Case 9: File Browser Language Boxes
function hideAndReplaceDivs() {
document.querySelectorAll('span.Box-sc-g0xbh4-0.lnwIhU').forEach(spanElement => {
if (!spanElement) return;
const divToHide = spanElement.querySelector('div[class^="Box-sc-g0xbh4-0"]');
const titleElement = spanElement.closest('a')?.querySelector('div[title]');
const title = titleElement?.getAttribute('title');
if (divToHide && title && !divToHide.dataset.processed) {
const normalizedTitle = normalizeLanguageName(title);
divToHide.style.display = 'none';
divToHide.dataset.processed = 'true';
const svgElement = document.createElement('img');
svgElement.src = `${ICON_BASE_URL}${normalizedTitle}.svg`;
svgElement.alt = title;
svgElement.width = svgElement.height = 16;
divToHide.parentNode.insertBefore(svgElement, divToHide);
}
});
}
function observeDOMChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
hideAndReplaceDivs();
}
});
});
const config = { childList: true, subtree: true };
observer.observe(document.body, config);
hideAndReplaceDivs();
}
async function init() {
languageMappings = await fetchLanguageMappings();
replaceLanguageIcons();
observeDOMChanges();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();