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