Colorful Tabs

Colorizes tabs by domain.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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');
}