Auto RTL Input

Automatically sets right-to-left (RTL) direction in ChatGPT's and Gemini's input fields when typing Arabic or Persian.

// ==UserScript==
// @name         Auto RTL Input
// @namespace    https://github.com/sinazadeh/userscripts
// @match        *://*.chatgpt.com/*
// @match        *://*.openai.com/*
// @match        *://gemini.google.com/*
// @grant        none
// @version      1.0.1
// @author       -
// @description  Automatically sets right-to-left (RTL) direction in ChatGPT's and Gemini's input fields when typing Arabic or Persian.
// @run-at       document-idle
// @license      MIT
// ==/UserScript==
/* jshint esversion: 11 */
(function () {
    'use strict';

    // This regex detects characters in the Arabic, Thaana, and other Arabic-based scripts.
    const isRTLText = text =>
        /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(text);

    function setDirection(el) {
        const text = el.innerText || el.textContent || el.value || '';
        // Set direction to 'rtl' if RTL text is detected, otherwise 'ltr'.
        const dir = isRTLText(text.trim()) ? 'rtl' : 'ltr';
        if (el.getAttribute('dir') !== dir) {
            el.setAttribute('dir', dir);
            // Also set CSS direction as backup
            el.style.direction = dir;
        }
    }

    function attachListeners(el) {
        if (!el || el.dataset.rtlAttached) return; // Do not attach more than once
        el.dataset.rtlAttached = 'true';

        console.log('RTL Script: Attached to element:', el);

        const update = () => setDirection(el);
        el.addEventListener('input', update);
        el.addEventListener('keyup', update);
        el.addEventListener('keydown', update);
        el.addEventListener('focus', update);
        // Use a small timeout on paste to allow the content to render.
        el.addEventListener('paste', () => setTimeout(update, 10));

        update(); // Run once on initial attachment
    }

    function findAndAttach() {
        // ChatGPT selectors (multiple fallbacks)
        const chatGPTSelectors = [
            "div.ProseMirror[contenteditable='true']",
            "[data-testid='user-message'] div[contenteditable='true']",
            "div[contenteditable='true'][data-id]",
        ];

        // Gemini selectors (multiple fallbacks)
        const geminiSelectors = [
            'div[role="textbox"][contenteditable="true"]',
            'div[contenteditable="true"][data-testid]',
            'div[contenteditable="true"][placeholder]',
            'textarea[placeholder*="Enter a prompt"]',
            'div[contenteditable="true"]',
            'rich-textarea div[contenteditable="true"]',
            '[data-testid="input-area"] div[contenteditable="true"]',
        ];

        const allSelectors = [...chatGPTSelectors, ...geminiSelectors];

        function processElements(selector) {
            const elements = document.querySelectorAll(selector);
            for (const el of elements) {
                if (
                    el &&
                    (el.getAttribute('contenteditable') === 'true' ||
                        el.tagName.toLowerCase() === 'textarea')
                ) {
                    const rect = el.getBoundingClientRect();
                    if (rect.width > 50 && rect.height > 20) {
                        attachListeners(el);
                    }
                }
            }
        }

        for (const selector of allSelectors) {
            processElements(selector);
        }

        // Special handling for dynamically created elements
        // Look for any contenteditable div that might be an input
        const allContentEditable = document.querySelectorAll(
            'div[contenteditable="true"]',
        );
        allContentEditable.forEach(el => {
            // Check if element looks like an input (has certain characteristics)
            const rect = el.getBoundingClientRect();
            const hasInputLikeParent =
                el.closest('[role="textbox"]') ||
                el.closest('form') ||
                el.closest('[data-testid]') ||
                el.parentElement?.className.includes('input');

            if (rect.width > 100 && rect.height > 30 && hasInputLikeParent) {
                attachListeners(el);
            }
        });
    }

    // More aggressive observation for SPAs
    const observer = new MutationObserver(mutations => {
        let shouldCheck = false;

        mutations.forEach(mutation => {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // Check if new node contains input elements
                        if (
                            node.querySelector &&
                            (node.querySelector('[contenteditable="true"]') ||
                                node.querySelector('textarea') ||
                                node.querySelector('[role="textbox"]'))
                        ) {
                            shouldCheck = true;
                        }
                    }
                });
            }
        });

        if (shouldCheck) {
            // Small delay to ensure elements are fully rendered
            setTimeout(findAndAttach, 100);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['contenteditable', 'role'],
    });

    // Initial run when the script loads
    setTimeout(findAndAttach, 500);

    // Also run on page focus (for navigation)
    window.addEventListener('focus', () => setTimeout(findAndAttach, 200));

    // Debug: Log when script loads
    console.log(
        'RTL Auto Direction Script loaded for:',
        window.location.hostname,
    );
})();