// ==UserScript==
// @name Gemini/ChatGPT: Enter=Newline, Ctrl+Enter=Send (stable CE)
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @run-at document-start
// @grant none
// @version 0.0.2
// @description Enter is Newline; Ctrl+Enter is Send (Gemini/ChatGPT)
// ==/UserScript==
(function () {
'use strict';
console.log('[TM] key-remap loaded');
// ---- selectors (Gemini is often Quill / rich-textarea) ----
const GEMINI_EDITORS = [
'.ql-editor[contenteditable="true"]',
'div[contenteditable="true"][aria-label*="prompt"]',
'rich-textarea [contenteditable="true"]',
].join(',');
function isEditable(el) {
if (!el) return false;
if (el.tagName === 'TEXTAREA') return true;
if (el.tagName === 'INPUT') return false;
if (el.isContentEditable === true) return true;
// sometimes focus is inside a child node; climb up
if (el.closest && el.closest(GEMINI_EDITORS)) return true;
return false;
}
function getEditorFromEvent(e) {
// prefer event target; fallback to activeElement; then climb
let el = e.target || document.activeElement;
if (isEditable(el)) return el;
if (el && el.closest) {
const up = el.closest(GEMINI_EDITORS);
if (up) return up;
}
const any = document.querySelector(GEMINI_EDITORS);
return any || null;
}
// ---- newline insertion ----
function insertNewlineTextarea(el) {
const start = el.selectionStart ?? el.value.length;
const end = el.selectionEnd ?? el.value.length;
el.setRangeText('\n', start, end, 'end');
el.dispatchEvent(new Event('input', { bubbles: true }));
}
// For Gemini (Quill), the safest newline is: synthesize Shift+Enter and let the app handle it.
function dispatchShiftEnter(el) {
const mk = (type) =>
new KeyboardEvent(type, {
key: 'Enter',
code: 'Enter',
shiftKey: true,
bubbles: true,
cancelable: true,
});
el.dispatchEvent(mk('keydown'));
el.dispatchEvent(mk('keypress'));
el.dispatchEvent(mk('keyup'));
}
function insertNewline(el) {
if (!el) return;
if (el.tagName === 'TEXTAREA') return insertNewlineTextarea(el);
// Gemini / rich editors: prefer Shift+Enter synthesis
dispatchShiftEnter(el);
}
// ---- send button ----
function findSendButton(scope) {
const sels = [
'button[type="submit"]',
'button[data-testid="send-button"]',
'button[aria-label="Send"]',
'button[aria-label="Send message"]',
'button[aria-label*="Send"]',
'button[aria-label*="送信"]',
'button[title*="Send"]',
'button[title*="送信"]',
];
for (const sel of sels) {
const btn = scope.querySelector(sel);
if (btn && !btn.disabled) return btn;
}
// fallback: scan buttons by accessible name-ish signals
const buttons = Array.from(scope.querySelectorAll('button'));
for (const b of buttons) {
if (b.disabled) continue;
const a = (b.getAttribute('aria-label') || '').toLowerCase();
const t = (b.getAttribute('title') || '').toLowerCase();
const txt = (b.textContent || '').toLowerCase().trim();
if (a.includes('send') || t.includes('send') || txt === 'send' || a.includes('送信') || t.includes('送信')) {
return b;
}
}
return null;
}
function clickSendButtonNear(editorEl) {
// prefer nearest container (form / composer) to avoid clicking wrong "Send" elsewhere
const scopes = [];
if (editorEl && editorEl.closest) {
scopes.push(editorEl.closest('form'));
scopes.push(editorEl.closest('rich-textarea'));
scopes.push(editorEl.closest('[role="textbox"]'));
scopes.push(editorEl.parentElement);
}
scopes.push(document);
for (const s of scopes) {
if (!s) continue;
const btn = findSendButton(s);
if (btn) { btn.click(); return true; }
}
return false;
}
window.addEventListener(
'keydown',
(e) => {
// avoid fighting with our own synthetic events
if (!e.isTrusted) return;
// IME変換確定中は介入しない
if (e.isComposing) return;
if (e.key !== 'Enter') return;
const el = getEditorFromEvent(e);
if (!isEditable(el)) return;
// Ctrl+Enter → 送信
if (e.ctrlKey) {
e.preventDefault();
e.stopImmediatePropagation();
clickSendButtonNear(el);
return;
}
// Enter単体 → 改行(GeminiはShift+Enterへ“すり替え”)
if (!e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault();
e.stopImmediatePropagation();
insertNewline(el);
return;
}
// Shift+Enter 等は既定挙動に任せる
},
true
);
})();
(function () {
'use strict';
console.log('[TM] ChatGPT key-remap loaded');
function isEditable(el) {
if (!el) return false;
if (el.tagName === 'TEXTAREA') return true;
if (el.tagName === 'INPUT') return false;
return el.isContentEditable === true;
}
function insertNewlineTextarea(el) {
const start = el.selectionStart ?? el.value.length;
const end = el.selectionEnd ?? el.value.length;
el.setRangeText('\n', start, end, 'end');
el.dispatchEvent(new Event('input', { bubbles: true }));
}
function insertNewlineContentEditable(el) {
// Selection/Range で <br> と ZWSP を挿入し,カーソルを次行へ
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return;
const range = sel.getRangeAt(0);
range.deleteContents();
const br = document.createElement('br');
const zwsp = document.createTextNode('\u200B'); // 次行でタイピング可能にするおまじない
range.insertNode(br);
range.setStartAfter(br);
range.insertNode(zwsp);
// キャレットを ZWSP の後ろへ const after = document.createRange(); after.setStartAfter(zwsp); after.collapse(true); sel.removeAllRanges(); sel.addRange(after);
// React/TipTap 等に変更通知
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: '\n' }));
}
function insertNewline(el) {
if (el.tagName === 'TEXTAREA') return insertNewlineTextarea(el);
// 念のため focus
el.focus();
insertNewlineContentEditable(el);
}
function clickSendButton() {
const candidates = [
'button[type="submit"]',
'button[data-testid="send-button"]',
'button[aria-label*="Send"]',
'button[aria-label*="送信"]',
];
for (const sel of candidates) {
const btn = document.querySelector(sel);
if (btn && !btn.disabled) { btn.click(); return true; }
}
return false;
}
window.addEventListener('keydown', (e) => {
const el = document.activeElement;
if (!isEditable(el)) return;
// IME 変換確定中は介入しない if (e.isComposing) return;
if (e.key !== 'Enter') return;
// Ctrl+Enter → 送信
if (e.ctrlKey) {
e.preventDefault();
e.stopImmediatePropagation();
clickSendButton();
return;
}
// Enter 単体 → 改行(誤送信防止)
if (!e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault();
e.stopImmediatePropagation();
insertNewline(el);
return;
}
// Shift+Enter 等は既定挙動(多くは改行)
}, true);
})();
/pre>