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.

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