Force Google Dark Mode

Automatically and resiliently enables dark mode on Google, handling multiple UI versions and DOM changes.

// ==UserScript==
// @name         Force Google Dark Mode
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically and resiliently enables dark mode on Google, handling multiple UI versions and DOM changes.
// @author       HappySmacky3453
// @license      MIT
// @match        https://www.google.*/
// @match        https://www.google.*/#*
// @match        https://www.google.*/search*
// @match        https://www.google.*/preferences*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    // --- Configuration ---
    const LOG_PREFIX = '🌙 [Force Google Dark Mode]:';
    const TIMEOUT_MS = 10000;
    const CLICK_DELAY_MS = 50; // A small delay to ensure element listeners are attached.

    // --- Selectors & Heuristics ---
    const SELECTORS = {
        MENU_ITEM_REGEX: /dark theme: off|light theme|appearance: (device|light|default)/i,
    };
    const DARK_MODE_COLOR_HEURISTIC = 'rgb(32, 33, 36)';

    // --- State ---
    let observer;
    let timeoutId;

    /**
     * Checks if dark mode is already enabled using multiple reliable methods.
     * @returns {boolean}
     */
    const isDarkModeAlreadyOn = () => {
        const html = document.documentElement;
        if (html.hasAttribute('dark')) return true;
        if (html.getAttribute('data-darkreader-scheme') === 'dark') return true;

        const metaTheme = document.querySelector('meta[name="color-scheme"]');
        if (metaTheme?.content.includes('dark')) return true;

        if (document.body && getComputedStyle(document.body).backgroundColor === DARK_MODE_COLOR_HEURISTIC) return true;

        return false;
    };

    /**
     * Attempts to find and click a dark mode toggle element.
     * @returns {boolean} True if a toggle was found.
     */
    const tryToEnableDarkMode = () => {
        if (isDarkModeAlreadyOn()) {
            console.log(LOG_PREFIX, '✅ Dark mode is already active. Cleaning up.');
            cleanUp();
            return true;
        }

        let elementToClick = null;

        // Strategy 1: Find the main menu item toggle.
        const menuToggle = [...document.querySelectorAll('[role="menuitem"]')]
            .find(el => SELECTORS.MENU_ITEM_REGEX.test(el.textContent));
        if (menuToggle) {
            elementToClick = menuToggle;
        }

        // Strategy 2: Find the radio button on the preferences page.
        if (!elementToClick) {
            const radioToggle = [...document.querySelectorAll('g-radio-button')]
                .find(btn =>
                    btn.closest('[role="radiogroup"]') &&
                    /dark/i.test(btn.textContent || btn.getAttribute('aria-label') || '')
                );
            if (radioToggle && radioToggle.getAttribute('aria-checked') !== 'true') {
                elementToClick = radioToggle;
            }
        }

        if (elementToClick) {
            console.log(LOG_PREFIX, 'Found toggle element. Attempting to click...', elementToClick);
            try {
                // Use a short delay for stability on dynamic pages.
                setTimeout(() => {
                    elementToClick.click();
                    console.log(LOG_PREFIX, '✅ Successfully triggered dark mode toggle.');
                }, CLICK_DELAY_MS);
                // Once we find and trigger the element, we don't need to search again on this pass.
                // The next mutation will confirm success via isDarkModeAlreadyOn().
                return true;
            } catch (err) {
                console.error(LOG_PREFIX, '⚠️ Failed to click dark mode toggle:', err);
            }
        }

        return false;
    };

    /**
     * Stops observing DOM changes and clears the safety timeout.
     */
    const cleanUp = () => {
        if (observer) {
            observer.disconnect();
            observer = null; // Allow garbage collection
            console.log(LOG_PREFIX, 'Observer disconnected.');
        }
        if (timeoutId) {
            clearTimeout(timeoutId);
            timeoutId = null;
        }
    };

    // --- Execution ---
    if (window.top !== window.self) return; // Don't run on iframes

    console.log(LOG_PREFIX, 'Script starting.');

    // Initial attempt. If it succeeds immediately, the script will clean up and exit.
    if (tryToEnableDarkMode()) {
        // The check inside tryToEnableDarkMode() will call cleanUp() if already on.
        return;
    }

    // If not immediately found/active, observe for changes.
    observer = new MutationObserver(tryToEnableDarkMode); // Pass function reference directly
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
    });

    // Set a timeout to prevent the script from running forever.
    timeoutId = setTimeout(() => {
        if (observer) { // Only log timeout if observer is still active
            console.log(LOG_PREFIX, `⏱️ Timed out after ${TIMEOUT_MS / 1000}s. Toggle not found.`);
            cleanUp();
        }
    }, TIMEOUT_MS);
})();