Colorful Tabs

Colorizes tabs by domain.

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

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.

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

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

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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

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