Colorful Tabs

Colorizes tabs by domain.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 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');
}