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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();