// ==UserScript==
// @name Gemini & AI Studio Enter Key Customizer
// @name:en Gemini & AI Studio Enter Key Customizer
// @name:ja Gemini & AI Studio Enterキーカスタマイザー
// @name:zh-TW Gemini 與 AI Studio Enter 鍵自訂器
// @namespace https://greasyfork.dpdns.org/en/users/1467948-stonedkhajiit
// @version 1.0.0
// @description Modifies Enter key behavior in Gemini and AI Studio. Gemini: Enter for newline, Modifier+Enter to send. AI Studio: Configurable send key (modifier, Enter-sends, or native). Includes a settings panel.
// @description:en Modifies Enter key behavior in Gemini and AI Studio. Gemini: Enter for newline, Modifier+Enter to send. AI Studio: Configurable send key (modifier, Enter-sends, or native). Includes a settings panel.
// @description:ja GeminiとAI StudioのEnterキー動作を変更。Gemini: Enterで改行、修飾キー+Enterで送信。AI Studio: 送信キーを選択可 (修飾キー、Enter送信、標準)。設定パネルあり。
// @description:zh-TW 調整 Gemini 與 AI Studio 的 Enter 鍵行為。Gemini:Enter 鍵換行,組合鍵送出。AI Studio:可自訂傳送鍵 (組合鍵、Enter即送、或預設)。附設定面板。
// @author StonedKhajiit
// @match https://gemini.google.com/*
// @match https://aistudio.google.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Constants and Configuration ---
const SCRIPT_ID = 'GeminiEnterNewlineMultiSite_v1.0.0';
// CSS Selectors for input fields and send buttons
const GEMINI_INPUT_SELECTOR_PRIMARY = 'div.ql-editor[contenteditable="true"]';
const GEMINI_INPUT_SELECTORS_FALLBACK = [
'textarea[enterkeyhint="send"]', 'textarea[aria-label*="Prompt"]',
'textarea[placeholder*="Message Gemini"]', 'div[role="textbox"][contenteditable="true"]'
];
const AISTUDIO_INPUT_SELECTORS = [
'ms-autosize-textarea textarea[aria-label="Type something or tab to choose an example prompt"]',
'ms-autosize-textarea textarea',
'ms-autosize-textarea textarea[aria-label="Start typing a prompt"]'
];
const GEMINI_SEND_BUTTON_SELECTORS = [
'button[aria-label*="Send"]', 'button[aria-label*="傳送"]',
'button[aria-label*="送信"]', 'button[data-test-id="send-button"]',
];
const AISTUDIO_SEND_BUTTON_SELECTORS = ['button[aria-label="Run"]'];
const AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR = 'span.secondary-key';
// GM Storage keys and default values for settings
const GM_GLOBAL_ENABLE_KEY_STORAGE = 'geminiEnterGlobalEnable';
const GM_MODIFIER_KEY_STORAGE = 'geminiEnterModifierKey';
const MODIFIER_KEYS = { NONE: 'none', CTRL: 'ctrl', SHIFT: 'shift', ALT: 'alt', NATIVE_GEMINI: 'native_gemini' };
const DEFAULT_MODIFIER_KEY = MODIFIER_KEYS.CTRL;
const GM_AISTUDIO_MODE_STORAGE = 'aiStudioKeyMode';
const AISTUDIO_KEY_MODES = {
SHIFT_SEND: 'shift_send',
ALT_SEND: 'alt_send',
AISTUDIO_SPECIFIC: 'aistudio_specific',
NATIVE_BEHAVIOR: 'native_behavior'
};
const DEFAULT_AISTUDIO_KEY_MODE = AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR;
// --- State Variables ---
let activeTextarea = null;
let isScriptGloballyEnabled = true;
let currentGlobalModifierKey = DEFAULT_MODIFIER_KEY;
let currentAIStudioKeyMode = DEFAULT_AISTUDIO_KEY_MODE;
let menuCommandIds = [];
// --- Debounce Function ---
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// --- Internationalization (i18n) ---
const i18n = {
currentLang: 'en',
strings: {
'en': {
notifySettingsSaved: 'Settings saved!',
notifyScriptEnabled: 'Custom Enter key behavior enabled. Reload page if needed.',
notifyScriptDisabled: 'Custom Enter key behavior disabled. Reload page if needed.',
settingsTitle: 'Script Settings',
geminiKeyModeLabel: 'Gemini Key Mode:',
aiStudioKeyModeLabel: 'AI Studio Key Mode:',
closeButton: 'Close',
saveButton: 'Save',
openSettingsMenu: 'Configure Enter Key Behavior...',
enableScriptMenu: 'Enable Custom Enter Key Behavior',
disableScriptMenu: 'Disable Custom Enter Key Behavior',
geminiCtrl: 'Ctrl+Enter to send',
geminiShift: 'Shift+Enter to send',
geminiAlt: 'Alt+Enter to send',
geminiNative: 'Use Gemini Native Behavior (Enter sends)',
aiStudioShift: 'Shift+Enter to send',
aiStudioAlt: 'Alt+Enter to send',
aiStudioSpecific: 'Enter to Send, Shift+Enter for Newline',
aiStudioNative: 'Use AI Studio Native Behavior (Ctrl+Enter sends)',
modifierCtrl: 'Ctrl',
modifierShift: 'Shift',
modifierAlt: 'Alt',
},
'zh-TW': {
notifySettingsSaved: '設定已儲存!',
notifyScriptEnabled: '自訂 Enter 鍵行為已啟用。若未立即生效請重載頁面。',
notifyScriptDisabled: '自訂 Enter 鍵行為已停用。若未立即生效請重載頁面。',
settingsTitle: '腳本設定',
geminiKeyModeLabel: 'Gemini 按鍵模式:',
aiStudioKeyModeLabel: 'AI Studio 按鍵模式:',
closeButton: '關閉',
saveButton: '儲存',
openSettingsMenu: '設定 Enter 鍵行為...',
enableScriptMenu: '啟用自訂 Enter 鍵行為',
disableScriptMenu: '停用自訂 Enter 鍵行為',
geminiCtrl: 'Ctrl+Enter 送出',
geminiShift: 'Shift+Enter 送出',
geminiAlt: 'Alt+Enter 送出',
geminiNative: '使用 Gemini 原生行為 (Enter 送出)',
aiStudioShift: 'Shift+Enter 送出',
aiStudioAlt: 'Alt+Enter 送出',
aiStudioSpecific: 'Enter 送出,Shift+Enter 換行',
aiStudioNative: '使用 AI Studio 原生行為 (Ctrl+Enter 送出)',
modifierCtrl: 'Ctrl',
modifierShift: 'Shift',
modifierAlt: 'Alt',
},
'ja': {
notifySettingsSaved: '設定を保存しました!',
notifyScriptEnabled: 'Enterキーのカスタム動作が有効になりました。必要に応じてページを再読み込みしてください。',
notifyScriptDisabled: 'Enterキーのカスタム動作が無効になりました。必要に応じてページを再読み込みしてください。',
settingsTitle: 'スクリプト設定',
geminiKeyModeLabel: 'Gemini のキーモード:',
aiStudioKeyModeLabel: 'AI Studio のキーモード:',
closeButton: '閉じる',
saveButton: '保存',
openSettingsMenu: 'Enterキーの動作を設定...',
enableScriptMenu: 'Enterキーのカスタム動作を有効化',
disableScriptMenu: 'Enterキーのカスタム動作を無効化',
geminiCtrl: 'Ctrl+Enter で送信',
geminiShift: 'Shift+Enter で送信',
geminiAlt: 'Alt+Enter で送信',
geminiNative: 'Gemini ネイティブ動作を使用 (Enter で送信)',
aiStudioShift: 'Shift+Enter で送信',
aiStudioAlt: 'Alt+Enter で送信',
aiStudioSpecific: 'Enter で送信、Shift+Enter で改行',
aiStudioNative: 'AI Studio ネイティブ動作を使用 (Ctrl+Enter で送信)',
modifierCtrl: 'Ctrl',
modifierShift: 'Shift',
modifierAlt: 'Alt',
}
},
detectLanguage() {
const lang = navigator.language || navigator.userLanguage;
if (lang) {
if (lang.startsWith('ja')) this.currentLang = 'ja';
else if (lang.startsWith('zh-TW') || lang.startsWith('zh-Hant')) this.currentLang = 'zh-TW';
else if (lang.startsWith('en')) this.currentLang = 'en';
else this.currentLang = 'en';
} else {
this.currentLang = 'en';
}
},
get(key, ...args) {
const langStrings = this.strings[this.currentLang] || this.strings.en;
const template = langStrings[key] || (this.strings.en && this.strings.en[key]);
if (typeof template === 'function') return template(...args);
if (typeof template === 'string') return template;
console.warn(`[${SCRIPT_ID}] Missing i18n string for key: ${key} in lang: ${this.currentLang}`);
return `Missing string: ${key}`;
}
};
// --- UI Functions ---
function createSettingsUI() {
if (document.getElementById('gemini-ai-settings-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'gemini-ai-settings-overlay';
overlay.classList.add('hidden');
const style = document.createElement('style');
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
#gemini-ai-settings-overlay {
position: fixed; inset: 0px; background-color: rgba(0, 0, 0, 0.6);
display: flex; align-items: center; justify-content: center;
z-index: 2147483647; font-family: 'Inter', Arial, sans-serif;
opacity: 0; transition: opacity 0.2s ease-in-out;
}
#gemini-ai-settings-overlay.visible { opacity: 1; }
#gemini-ai-settings-overlay.hidden { display: none !important; }
#gemini-ai-settings-panel {
background-color: #ffffff; color: #1f2937;
padding: 18px; border-radius: 8px;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
width: 90%; max-width: 420px;
position: relative; overflow-y: auto; max-height: 90vh;
}
body.userscript-dark-mode #gemini-ai-settings-panel { background-color: #2d3748; color: #e2e8f0; }
body.userscript-dark-mode #gemini-ai-settings-panel h2,
body.userscript-dark-mode #gemini-ai-settings-panel h3,
body.userscript-dark-mode #gemini-ai-settings-panel label { color: #e2e8f0; }
body.userscript-dark-mode #gemini-ai-settings-panel button#gemini-ai-close-btn { background-color: #4a5568; color: #e2e8f0; }
body.userscript-dark-mode #gemini-ai-settings-panel button#gemini-ai-close-btn:hover { background-color: #718096; }
body.userscript-dark-mode #gemini-ai-settings-panel input[type="radio"] { filter: invert(1) hue-rotate(180deg); }
#gemini-ai-settings-panel h2 { font-size: 1.15rem; font-weight: 600; margin-bottom: 0.8rem; }
#gemini-ai-settings-panel h3 { font-size: 1rem; font-weight: 500; margin-bottom: 0.5rem; margin-top: 0.7rem; }
#gemini-ai-settings-panel .section-divider { border-top: 1px solid #e5e7eb; margin-top: 1rem; margin-bottom: 1rem; }
body.userscript-dark-mode #gemini-ai-settings-panel .section-divider { border-top-color: #4a5568; }
#gemini-ai-settings-panel .options-group > div { margin-bottom: 0.3rem; }
#gemini-ai-settings-panel label { display: inline-flex; align-items: center; cursor: pointer; font-size: 0.875rem; }
#gemini-ai-settings-panel input[type="radio"] { margin-right: 0.4rem; cursor: pointer; transform: scale(0.95); }
.settings-buttons-container { display: flex; justify-content: flex-end; margin-top: 1rem; gap: 0.5rem; }
#gemini-ai-settings-panel button {
padding: 0.4rem 0.9rem; border-radius: 6px;
font-weight: 500; transition: background-color 0.2s ease, box-shadow 0.2s ease;
border: none; cursor: pointer; font-size: 0.875rem;
}
#gemini-ai-settings-panel button#gemini-ai-close-btn { background-color: #e5e7eb; color: #374151; }
#gemini-ai-settings-panel button#gemini-ai-close-btn:hover { background-color: #d1d5db; }
#gemini-ai-settings-panel button#gemini-ai-save-btn { background-color: #3b82f6; color: white; }
#gemini-ai-settings-panel button#gemini-ai-save-btn:hover { background-color: #2563eb; }
#gemini-ai-notification {
position: fixed; bottom: 25px; left: 50%;
transform: translateX(-50%);
background-color: #10b981; color: white;
padding: 0.8rem 1.5rem; border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
z-index: 2147483647; opacity: 0;
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
font-family: 'Inter', Arial, sans-serif; font-size: 0.9rem;
}
#gemini-ai-notification.visible { opacity: 1; transform: translateX(-50%) translateY(0px); }
#gemini-ai-notification.hidden { display: none !important; }
`;
document.head.appendChild(style);
const settingsPanel = document.createElement('div');
settingsPanel.id = 'gemini-ai-settings-panel';
const title = document.createElement('h2');
title.textContent = i18n.get('settingsTitle');
settingsPanel.appendChild(title);
const geminiTitle = document.createElement('h3');
geminiTitle.textContent = i18n.get('geminiKeyModeLabel');
settingsPanel.appendChild(geminiTitle);
const geminiOptionsDiv = document.createElement('div');
geminiOptionsDiv.id = 'gemini-key-options';
geminiOptionsDiv.className = 'options-group';
settingsPanel.appendChild(geminiOptionsDiv);
settingsPanel.appendChild(document.createElement('div')).className = 'section-divider';
const aistudioTitle = document.createElement('h3');
aistudioTitle.textContent = i18n.get('aiStudioKeyModeLabel');
settingsPanel.appendChild(aistudioTitle);
const aistudioOptionsDiv = document.createElement('div');
aistudioOptionsDiv.id = 'aistudio-key-options';
aistudioOptionsDiv.className = 'options-group';
settingsPanel.appendChild(aistudioOptionsDiv);
const buttonDiv = document.createElement('div');
buttonDiv.className = 'settings-buttons-container';
const closeButton = document.createElement('button');
closeButton.id = 'gemini-ai-close-btn';
closeButton.textContent = i18n.get('closeButton');
buttonDiv.appendChild(closeButton);
const saveButton = document.createElement('button');
saveButton.id = 'gemini-ai-save-btn';
saveButton.textContent = i18n.get('saveButton');
buttonDiv.appendChild(saveButton);
settingsPanel.appendChild(buttonDiv);
overlay.appendChild(settingsPanel);
document.body.appendChild(overlay);
const notificationDiv = document.createElement('div');
notificationDiv.id = 'gemini-ai-notification';
notificationDiv.classList.add('hidden');
document.body.appendChild(notificationDiv);
closeButton.addEventListener('click', closeSettings);
saveButton.addEventListener('click', saveSettingsFromUI);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSettings(); });
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('userscript-dark-mode');
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
document.body.classList.toggle('userscript-dark-mode', e.matches);
});
}
function populateSettingsUI() {
const geminiOptionsDiv = document.getElementById('gemini-key-options');
const aistudioOptionsDiv = document.getElementById('aistudio-key-options');
if (!geminiOptionsDiv || !aistudioOptionsDiv) return;
while (geminiOptionsDiv.firstChild) geminiOptionsDiv.removeChild(geminiOptionsDiv.firstChild);
while (aistudioOptionsDiv.firstChild) aistudioOptionsDiv.removeChild(aistudioOptionsDiv.firstChild);
const geminiModifierOptions = [
{ key: MODIFIER_KEYS.CTRL, labelKey: 'geminiCtrl' },
{ key: MODIFIER_KEYS.SHIFT, labelKey: 'geminiShift' },
{ key: MODIFIER_KEYS.ALT, labelKey: 'geminiAlt' },
{ key: MODIFIER_KEYS.NATIVE_GEMINI, labelKey: 'geminiNative' },
];
geminiModifierOptions.forEach(opt => {
const div = document.createElement('div');
const input = document.createElement('input');
input.type = 'radio'; input.name = 'geminiKeyMode'; input.id = `gemini-${opt.key}`; input.value = opt.key;
if (currentGlobalModifierKey === opt.key) input.checked = true;
const label = document.createElement('label');
label.htmlFor = `gemini-${opt.key}`; label.textContent = i18n.get(opt.labelKey);
div.appendChild(input); div.appendChild(label);
geminiOptionsDiv.appendChild(div);
});
const aiStudioModeOptions = [
{ mode: AISTUDIO_KEY_MODES.SHIFT_SEND, labelKey: 'aiStudioShift' },
{ mode: AISTUDIO_KEY_MODES.ALT_SEND, labelKey: 'aiStudioAlt' },
{ mode: AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC, labelKey: 'aiStudioSpecific' },
{ mode: AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR, labelKey: 'aiStudioNative' },
];
aiStudioModeOptions.forEach(opt => {
const div = document.createElement('div');
const input = document.createElement('input');
input.type = 'radio'; input.name = 'aistudioKeyMode'; input.id = `aistudio-${opt.mode}`; input.value = opt.mode;
if (currentAIStudioKeyMode === opt.mode) input.checked = true;
const label = document.createElement('label');
label.htmlFor = `aistudio-${opt.mode}`; label.textContent = i18n.get(opt.labelKey);
div.appendChild(input); div.appendChild(label);
aistudioOptionsDiv.appendChild(div);
});
}
function openSettings() {
loadSettings();
populateSettingsUI();
const overlay = document.getElementById('gemini-ai-settings-overlay');
if (overlay) {
overlay.classList.remove('hidden');
void overlay.offsetWidth;
overlay.classList.add('visible');
}
}
function closeSettings() {
const overlay = document.getElementById('gemini-ai-settings-overlay');
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => {
if (!overlay.classList.contains('visible')) {
overlay.classList.add('hidden');
}
}, 200);
}
}
function showNotification(message) {
const notificationDiv = document.getElementById('gemini-ai-notification');
if (notificationDiv) {
notificationDiv.textContent = message;
notificationDiv.classList.remove('hidden');
void notificationDiv.offsetWidth;
notificationDiv.classList.add('visible');
setTimeout(() => {
notificationDiv.classList.remove('visible');
setTimeout(() => {
if (!notificationDiv.classList.contains('visible')) {
notificationDiv.classList.add('hidden');
}
}, 300);
}, 2500);
}
}
// --- Core Logic Functions ---
function loadSettings() {
isScriptGloballyEnabled = GM_getValue(GM_GLOBAL_ENABLE_KEY_STORAGE, true);
const savedGeminiModifier = GM_getValue(GM_MODIFIER_KEY_STORAGE, DEFAULT_MODIFIER_KEY);
currentGlobalModifierKey = Object.values(MODIFIER_KEYS).includes(savedGeminiModifier) ? savedGeminiModifier : DEFAULT_MODIFIER_KEY;
const savedAIStudioMode = GM_getValue(GM_AISTUDIO_MODE_STORAGE, DEFAULT_AISTUDIO_KEY_MODE);
currentAIStudioKeyMode = Object.values(AISTUDIO_KEY_MODES).includes(savedAIStudioMode) ? savedAIStudioMode : DEFAULT_AISTUDIO_KEY_MODE;
}
function saveSettingsFromUI() {
const selectedGeminiMode = document.querySelector('input[name="geminiKeyMode"]:checked')?.value;
if (selectedGeminiMode) {
saveGeminiKeyModeSetting(selectedGeminiMode);
} else {
saveGeminiKeyModeSetting(DEFAULT_MODIFIER_KEY);
}
const selectedAIStudioMode = document.querySelector('input[name="aistudioKeyMode"]:checked')?.value;
if (selectedAIStudioMode) {
saveAIStudioKeyModeSetting(selectedAIStudioMode);
} else {
saveAIStudioKeyModeSetting(DEFAULT_AISTUDIO_KEY_MODE);
}
registerMenuCommand();
showNotification(i18n.get('notifySettingsSaved'));
closeSettings();
}
function saveGeminiKeyModeSetting(key) {
if (Object.values(MODIFIER_KEYS).includes(key) && key !== MODIFIER_KEYS.NONE) {
currentGlobalModifierKey = key;
GM_setValue(GM_MODIFIER_KEY_STORAGE, key);
updateActiveTextareaListener(); // Important to update listener status
}
}
function saveAIStudioKeyModeSetting(mode) {
if (Object.values(AISTUDIO_KEY_MODES).includes(mode)) {
currentAIStudioKeyMode = mode;
GM_setValue(GM_AISTUDIO_MODE_STORAGE, mode);
updateAIStudioButtonModifierHint();
updateActiveTextareaListener();
}
}
function updateAIStudioButtonModifierHint() {
if (!window.location.hostname.includes('aistudio.google.com')) {
return;
}
const sendButton = findSendButton();
if (sendButton) {
const hintSpan = sendButton.querySelector(AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR);
if (hintSpan) {
hintSpan.style.display = 'inline';
let hintText = i18n.get('modifierCtrl');
switch (currentAIStudioKeyMode) {
case AISTUDIO_KEY_MODES.SHIFT_SEND:
hintText = i18n.get('modifierShift');
break;
case AISTUDIO_KEY_MODES.ALT_SEND:
hintText = i18n.get('modifierAlt');
break;
case AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC:
hintSpan.style.display = 'none';
hintText = '';
break;
case AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR:
hintText = i18n.get('modifierCtrl');
break;
}
hintSpan.textContent = hintSpan.style.display !== 'none' ? hintText + ' ' : '';
}
}
}
function handleKeydown(event) {
if (window.location.hostname.includes('gemini.google.com') && !isScriptGloballyEnabled) {
return;
}
if (event.target !== activeTextarea && (!activeTextarea || !activeTextarea.contains(event.target))) {
return;
}
const currentHost = window.location.hostname;
const ctrlOnly = event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
const shiftOnly = event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey;
const altOnly = event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey;
const plainEnter = !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
if (event.key === 'Enter') {
if (currentHost.includes('gemini.google.com')) {
if (currentGlobalModifierKey === MODIFIER_KEYS.NATIVE_GEMINI) {
return;
}
applyModifierSendBehavior(event, currentGlobalModifierKey, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextarea);
} else if (currentHost.includes('aistudio.google.com')) {
if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR) {
return;
} else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC) {
if (plainEnter) {
event.preventDefault(); event.stopImmediatePropagation();
const sendButton = findSendButton();
if (sendButton && !sendButton.disabled) sendButton.click();
else { const form = event.target?.closest('form'); if (form) form.requestSubmit ? form.requestSubmit() : form.submit(); }
} else if (shiftOnly) {
event.preventDefault(); event.stopImmediatePropagation();
insertNewline(activeTextarea);
} else {
event.preventDefault(); event.stopImmediatePropagation();
}
} else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.SHIFT_SEND) {
applyModifierSendBehavior(event, MODIFIER_KEYS.SHIFT, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextarea);
} else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.ALT_SEND) {
applyModifierSendBehavior(event, MODIFIER_KEYS.ALT, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextarea);
}
}
}
}
function insertNewline(element) {
if (!element) return;
if (element.isContentEditable) {
element.focus();
let success = false;
try { success = document.execCommand('insertParagraph', false, null); } catch (e) { /* console.warn("execCommand('insertParagraph') failed:", e); */ }
if (!success) {
try {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const br = document.createElement('br');
range.deleteContents();
range.insertNode(br);
const newRange = document.createRange();
newRange.setStartAfter(br);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
} else {
document.execCommand('insertHTML', false, '<br>');
}
} catch (e) { /* console.warn("Fallback newline insertion for contentEditable failed:", e); */ }
}
} else if (element.tagName === 'TEXTAREA') {
const start = element.selectionStart; const end = element.selectionEnd;
element.value = `${element.value.substring(0, start)}\n${element.value.substring(end)}`;
element.selectionStart = element.selectionEnd = start + 1;
element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
}
}
function applyModifierSendBehavior(event, modifierKeyToSend, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextareaElement) {
let send = false;
if ((modifierKeyToSend === MODIFIER_KEYS.CTRL && ctrlOnly) ||
(modifierKeyToSend === MODIFIER_KEYS.SHIFT && shiftOnly) ||
(modifierKeyToSend === MODIFIER_KEYS.ALT && altOnly)) {
send = true;
}
if (send) {
event.preventDefault(); event.stopImmediatePropagation();
const sendButton = findSendButton();
if (sendButton && !sendButton.disabled) sendButton.click();
else {
const form = event.target?.closest('form');
if (form) form.requestSubmit ? form.requestSubmit() : form.submit();
}
} else if (plainEnter) {
event.preventDefault(); event.stopImmediatePropagation();
insertNewline(activeTextareaElement);
} else {
const isShiftEnterForNativeNewline = shiftOnly && modifierKeyToSend !== MODIFIER_KEYS.SHIFT;
if (!isShiftEnterForNativeNewline) {
event.preventDefault(); event.stopImmediatePropagation();
}
}
}
function updateActiveTextareaListener() {
if (activeTextarea) {
const listenerAttached = activeTextarea.dataset.keydownListenerAttached === 'true';
const onGemini = window.location.hostname.includes('gemini.google.com');
const onAIStudio = window.location.hostname.includes('aistudio.google.com');
let shouldCurrentlyBeAttached = false;
if (onGemini) {
shouldCurrentlyBeAttached = isScriptGloballyEnabled && currentGlobalModifierKey !== MODIFIER_KEYS.NATIVE_GEMINI;
} else if (onAIStudio) {
shouldCurrentlyBeAttached = currentAIStudioKeyMode !== AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR;
}
if (listenerAttached && !shouldCurrentlyBeAttached) {
activeTextarea.removeEventListener('keydown', handleKeydown, true);
delete activeTextarea.dataset.keydownListenerAttached;
// console.log(`[${SCRIPT_ID}] Keydown listener removed from:`, activeTextarea);
} else if (!listenerAttached && shouldCurrentlyBeAttached) {
activeTextarea.addEventListener('keydown', handleKeydown, true);
activeTextarea.dataset.keydownListenerAttached = 'true';
// console.log(`[${SCRIPT_ID}] Keydown listener attached to:`, activeTextarea);
}
}
}
function toggleScriptGlobally() {
isScriptGloballyEnabled = !isScriptGloballyEnabled;
GM_setValue(GM_GLOBAL_ENABLE_KEY_STORAGE, isScriptGloballyEnabled);
updateActiveTextareaListener();
registerMenuCommand();
showNotification(isScriptGloballyEnabled ? i18n.get('notifyScriptEnabled') : i18n.get('notifyScriptDisabled'));
}
function registerMenuCommand() {
menuCommandIds.forEach(id => { if (typeof GM_unregisterMenuCommand === 'function') try { GM_unregisterMenuCommand(id); } catch (e) { /* console.warn("Error unregistering menu command:", e); */ } });
menuCommandIds = [];
try {
const settingsCmdId = GM_registerMenuCommand(i18n.get('openSettingsMenu'), openSettings, 's');
if (settingsCmdId) menuCommandIds.push(settingsCmdId);
} catch (e) { console.error(`[${SCRIPT_ID}] Error registering 'Open Settings' menu command:`, e); }
try {
const toggleCmdText = isScriptGloballyEnabled ? i18n.get('disableScriptMenu') : i18n.get('enableScriptMenu');
const toggleCmdId = GM_registerMenuCommand(toggleCmdText, toggleScriptGlobally, 't');
if (toggleCmdId) menuCommandIds.push(toggleCmdId);
} catch (e) { console.error(`[${SCRIPT_ID}] Error registering toggle script menu command:`, e); }
}
// --- Initialization and Observation ---
function findTextarea() {
let el;
const currentHost = window.location.hostname;
if (currentHost.includes('aistudio.google.com')) {
for (const selector of AISTUDIO_INPUT_SELECTORS) {
el = document.querySelector(selector); if (el) return el;
}
} else if (currentHost.includes('gemini.google.com')) {
el = document.querySelector(GEMINI_INPUT_SELECTOR_PRIMARY); if (el) return el;
for (const selector of GEMINI_INPUT_SELECTORS_FALLBACK) {
el = document.querySelector(selector); if (el) return el;
}
}
return null;
}
function findSendButton() {
let el;
const currentHost = window.location.hostname;
if (currentHost.includes('aistudio.google.com')) {
for (const selector of AISTUDIO_SEND_BUTTON_SELECTORS) {
el = document.querySelector(selector); if (el) return el;
}
} else if (currentHost.includes('gemini.google.com')) {
for (const selector of GEMINI_SEND_BUTTON_SELECTORS) {
el = document.querySelector(selector); if (el) return el;
}
}
return null;
}
// Debounced handler for DOM changes
const debouncedDOMChangeHandler = debounce(() => {
const newTextarea = findTextarea();
if (newTextarea) {
const onGemini = window.location.hostname.includes('gemini.google.com');
const onAIStudio = window.location.hostname.includes('aistudio.google.com');
const needsListenerOnGemini = onGemini && isScriptGloballyEnabled && currentGlobalModifierKey !== MODIFIER_KEYS.NATIVE_GEMINI;
const needsListenerOnAIStudio = onAIStudio && currentAIStudioKeyMode !== AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR;
const listenerShouldBeAttached = needsListenerOnGemini || needsListenerOnAIStudio;
if (activeTextarea !== newTextarea || (activeTextarea && activeTextarea.dataset.keydownListenerAttached !== 'true' && listenerShouldBeAttached)) {
if (activeTextarea && activeTextarea.dataset.keydownListenerAttached === 'true') {
activeTextarea.removeEventListener('keydown', handleKeydown, true);
delete activeTextarea.dataset.keydownListenerAttached;
}
activeTextarea = newTextarea;
updateActiveTextareaListener(); // This will attach/detach based on current state
}
} else if (activeTextarea && activeTextarea.dataset.keydownListenerAttached === 'true') {
activeTextarea.removeEventListener('keydown', handleKeydown, true);
delete activeTextarea.dataset.keydownListenerAttached;
activeTextarea = null;
}
if (window.location.hostname.includes('aistudio.google.com')) {
updateAIStudioButtonModifierHint();
}
}, 300); // 300ms debounce time
const observeDOM = function() {
const observer = new MutationObserver((mutationsList, obs) => {
debouncedDOMChangeHandler();
});
observer.observe(document.body, { childList: true, subtree: true });
};
function init() {
i18n.detectLanguage();
loadSettings();
createSettingsUI();
registerMenuCommand();
observeDOM();
// Initial setup for textarea and button hint
activeTextarea = findTextarea();
updateActiveTextareaListener();
// Delay initial AI Studio button hint update slightly to allow page to settle
if (window.location.hostname.includes('aistudio.google.com')) {
setTimeout(updateAIStudioButtonModifierHint, 500); // Increased delay
} else {
updateAIStudioButtonModifierHint(); // For Gemini or other contexts, update immediately (though it does nothing)
}
console.log(`[${SCRIPT_ID}] Initialized. Gemini Mode: ${currentGlobalModifierKey}, AI Studio Mode: ${currentAIStudioKeyMode}, Script Enabled: ${isScriptGloballyEnabled}`);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();