Colorizes tabs by domain.
// ==UserScript==
// @name Colorful Tabs
// @description Colorizes tabs by domain.
// @version 1.0
// @namespace Surmoka
// @license MIT
// @match *://*/*
// @run-at document-idle
// @grant none
// ==/UserScript==
// ============================================
// CONFIGURATION
// ============================================
// Array of zero-width characters (our "color indices")
const MARKERS = [
'\u200B', // 0: Zero-Width Space
'\u200C', // 1: Zero-Width Non-Joiner
'\u200D', // 2: Zero-Width Joiner
'\u200E', // 3: Left-to-Right Mark
'\u2060', // 4: Word Joiner
'\u180E', // 5: Mongolian Vowel Separator
'\u034F', // 6: Combining Grapheme Joiner
'\u17B4', // 7: Khmer Vowel Inherent Aq
'\u17B5', // 8: Khmer Vowel Inherent Aa
];
const DEBUG_MODE = false; // Set to false in production
// ============================================
// HELPER FUNCTIONS
// ============================================
// Create regex pattern from MARKERS array
const MARKER_REGEX = new RegExp(`[${MARKERS.join('')}]`, 'g');
// Clean existing markers from title
function cleanTitle(title) {
return title.replace(MARKER_REGEX, '');
}
// Check if title has our marker at the start (strict checking)
function hasMarker(title) {
return MARKERS.some(marker => title.startsWith(marker));
}
// Get base domain (remove subdomains for consistency)
function getBaseDomain(hostname) {
/* const parts = hostname.split('.');
if (parts.length > 2) {
// Keep last two parts (example.com from www.example.com)
return parts.slice(-2).join('.');
} */
return hostname;
}
// Hash function to convert domain to index
function domainToColorIndex(domain) {
let hash = 0;
for (let i = 0; i < domain.length; i++) {
hash = ((hash << 5) - hash) + domain.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash) % MARKERS.length;
}
// ============================================
// DEBUG FUNCTIONS
// ============================================
function debugTitle() {
const title = document.title;
// Show first few characters as hex codes
const codes = [...title].slice(0, 5).map(c =>
'U+' + c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')
);
console.log('Title:', title);
console.log('First 5 chars:', codes);
console.log('Has marker:', hasMarker(title));
}
function fullDebug(baseDomain, colorIndex) {
console.group('🔍 Tab Marker Debug');
console.log('Title:', document.title);
console.log('First char code:', '0x' + document.title.charCodeAt(0).toString(16).toUpperCase());
console.log('Domain:', window.location.hostname);
console.log('Base domain:', baseDomain);
console.log('Color index:', colorIndex);
console.log('Marker code:', 'U+' + MARKERS[colorIndex].charCodeAt(0).toString(16).toUpperCase());
console.log('Has marker:', hasMarker(document.title));
console.groupEnd();
}
// ============================================
// MAIN FUNCTION
// ============================================
let lastCleanTitle = ''; // Track the clean version to detect real changes
let isApplying = false; // Prevent recursion
function applyDomainMarker() {
if (isApplying) return; // Prevent recursion
isApplying = true;
try {
const baseDomain = getBaseDomain(window.location.hostname);
const colorIndex = domainToColorIndex(baseDomain);
const marker = MARKERS[colorIndex];
const cleanedTitle = cleanTitle(document.title);
// Only update if the clean title actually changed
if (cleanedTitle === lastCleanTitle && hasMarker(document.title)) {
return; // Already has marker and title hasn't changed
}
lastCleanTitle = cleanedTitle;
let newTitle;
if (DEBUG_MODE) {
// Visible debug prefix
newTitle = `[${colorIndex}]${marker}${cleanedTitle}`;
} else {
// Invisible marker only
newTitle = marker + cleanedTitle;
}
document.title = newTitle;
if (DEBUG_MODE) {
fullDebug(baseDomain, colorIndex);
}
} finally {
isApplying = false;
}
}
// ============================================
// INITIALIZATION
// ============================================
// Apply immediately if title exists
if (document.title) {
applyDomainMarker();
}
// Re-apply if page changes title using MutationObserver
const titleObserver = new MutationObserver(() => {
if (!isApplying) {
applyDomainMarker();
}
});
const titleElement = document.querySelector('title');
if (titleElement) {
titleObserver.observe(titleElement, {
childList: true,
characterData: true,
subtree: true
});
}
// Aggressive periodic check for sites that bypass MutationObserver
// This will re-apply marker if it gets stripped
let checkCount = 0;
const maxChecks = 20; // Check for up to 25 seconds (50 * 500ms)
const periodicCheck = setInterval(() => {
checkCount++;
if (!hasMarker(document.title)) {
applyDomainMarker();
}
// After 25 seconds, reduce frequency
if (checkCount >= maxChecks) {
clearInterval(periodicCheck);
// Continue with less frequent checks indefinitely
// setInterval(() => {
// if (!hasMarker(document.title)) {
// applyDomainMarker();
// }
// }, 5000); // Every 5 seconds
}
}, 500); // Check every 500ms initially
// Also intercept direct title property changes (for some sites)
let originalTitle = Object.getOwnPropertyDescriptor(Document.prototype, 'title');
if (originalTitle && originalTitle.set) {
Object.defineProperty(document, 'title', {
get: originalTitle.get,
set: function(newTitle) {
// Let the original setter work
originalTitle.set.call(this, newTitle);
// Then reapply our marker after a tiny delay
setTimeout(() => {
if (!isApplying) {
applyDomainMarker();
}
}, 0);
},
configurable: true
});
}
// Debug helper - make available in console
if (DEBUG_MODE) {
window.debugTabMarker = {
showTitle: debugTitle,
showFull: () => fullDebug(
getBaseDomain(window.location.hostname),
domainToColorIndex(getBaseDomain(window.location.hostname))
),
getMarkers: () => MARKERS,
currentIndex: () => domainToColorIndex(getBaseDomain(window.location.hostname)),
reapply: applyDomainMarker
};
console.log('Debug helper available: window.debugTabMarker');
}