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.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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