Gemini Shortcut

Adds keyboard shortcuts to Google Gemini. Use Alt+M to open the model switcher and cycle through available models. Use Alt+G to open Gemini in a new tab. Automatically returns focus to the chatbox when the model selection menu is closed.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name                Gemini Shortcut
// @icon                https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @author              ElectroKnight22
// @namespace           electroknight22_gemini_shortcut_namespace
// @version             0.0.3
// @match               *://*/*
// @match               https://gemini.google.com/*
// @grant               none
// @run-at              document-idle
// @license             MIT
// @description         Adds keyboard shortcuts to Google Gemini. Use Alt+M to open the model switcher and cycle through available models. Use Alt+G to open Gemini in a new tab. Automatically returns focus to the chatbox when the model selection menu is closed.
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    'use strict';

    const isGeminiSite = window.location.hostname === 'gemini.google.com';

    const domCache = {
        switcherButton: null,
        promptInputBox: null,
    };

    const getModelSwitcherButton = () => {
        if (domCache.switcherButton?.isConnected) return domCache.switcherButton;
        const container = Array.from(document.querySelectorAll('bard-mode-switcher')).find(
            (el) => el.childElementCount > 1,
        );
        domCache.switcherButton = container?.querySelector('button') || null;
        return domCache.switcherButton;
    };

    const getPromptInputBox = () => {
        if (domCache.promptInputBox?.isConnected) return domCache.promptInputBox;
        const editable = document.querySelector('rich-textarea div[contenteditable="true"]');
        domCache.promptInputBox = editable || document.querySelector('rich-textarea');
        return domCache.promptInputBox;
    };

    const getModelSwitcherDropdown = () => document.querySelector('.gds-mode-switch-menu');

    const getModelSelectionButtons = () => {
        const dropdown = getModelSwitcherDropdown();
        if (!dropdown) return [];
        return Array.from(dropdown.querySelectorAll('button')).filter((btn) => btn.querySelector('.mode-title'));
    };

    const getModelNames = () => {
        return getModelSelectionButtons().map((btn) => btn.querySelector('.mode-title')?.innerText || 'Unknown');
    };

    const getCurrentModel = () => {
        const selectedButton = getModelSelectionButtons().find((btn) => btn.classList.contains('is-selected'));
        return selectedButton?.querySelector('.mode-title')?.innerText || '';
    };

    const forceClick = (el) => {
        if (!el) return;
        const eventOptions = { bubbles: true, cancelable: true, view: window, buttons: 1 };
        ['mousedown', 'mouseup', 'click'].forEach((eventType) => {
            el.dispatchEvent(new MouseEvent(eventType, eventOptions));
        });
    };

    const initMenuStateTracker = () => {
        if (!isGeminiSite) return;

        let menuWasOpen = false;

        const observer = new MutationObserver(() => {
            const menuExists = !!getModelSwitcherDropdown();

            if (menuWasOpen && !menuExists) {
                const input = getPromptInputBox();
                if (input) requestAnimationFrame(() => input.focus());
            }
            menuWasOpen = menuExists;
        });

        observer.observe(document.body, { childList: true, subtree: true });
    };

    const createHotkey = (key, callback, { ctrl = false, shift = false, alt = false } = {}) => {
        window.addEventListener('keydown', (event) => {
            const matchesKey = event.key.toLowerCase() === key.toLowerCase();
            const matchesCtrl = ctrl ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;
            const matchesShift = shift ? event.shiftKey : !event.shiftKey;
            const matchesAlt = alt ? event.altKey : !event.altKey;

            if (matchesKey && matchesCtrl && matchesShift && matchesAlt) {
                event.preventDefault();
                callback();
            }
        });
    };

    const createDefaultHotkey = (key, callback) => createHotkey(key, callback, { alt: true });

    const openGeminiInNewTab = () => window.open('https://gemini.google.com', '_blank');

    let currentFocusIndex = 0;

    const switchModel = () => {
        if (!isGeminiSite) return;

        try {
            const buttons = getModelSelectionButtons();

            if (buttons.length > 0 && buttons.includes(document.activeElement)) {
                currentFocusIndex = (currentFocusIndex + 1) % buttons.length;
                buttons[currentFocusIndex]?.focus();
                return;
            }

            const switcherBtn = getModelSwitcherButton();
            if (switcherBtn) {
                forceClick(switcherBtn);

                requestAnimationFrame(() => {
                    const freshButtons = getModelSelectionButtons();
                    const currentModel = getCurrentModel();
                    currentFocusIndex = Math.max(0, getModelNames().indexOf(currentModel));
                    if (freshButtons[currentFocusIndex]) {
                        freshButtons[currentFocusIndex].focus();
                    }
                });
            }
        } catch (e) {
            console.error('Error switching model:', e);
        }
    };

    const init = () => {
        createDefaultHotkey('/', openGeminiInNewTab);
        createDefaultHotkey('m', switchModel);
        createDefaultHotkey('s', () => {
            /* TODO: trigger microphone */
        });
        initMenuStateTracker();
    };

    init();
})();