Auto RTL Input

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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