#author("2025-12-25T12:20:53+00:00","default:iseki","iseki") #author("2025-12-25T12:21:22+00:00","default:iseki","iseki") *** Enter=Newline スクリプト **** Version 0.0.2 <pre> // ==UserScript== // @name Gemini/ChatGPT: Enter=Newline, Ctrl+Enter=Send (stable CE) // @name 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 ); })(); </pre> ***** Version 0.0.1 <pre> // ==UserScript== // @name 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.1 // @description Enter is Newline // ==/UserScript== (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>