Skip to content

Instantly share code, notes, and snippets.

@Taik
Created August 17, 2025 20:48
Show Gist options
  • Select an option

  • Save Taik/f8cea74f527f7585a027604d862131c0 to your computer and use it in GitHub Desktop.

Select an option

Save Taik/f8cea74f527f7585a027604d862131c0 to your computer and use it in GitHub Desktop.
ChatGPT Copy Cleaner
// ==UserScript==
// @name ChatGPT Copy Cleaner
// @namespace http://tampermonkey.net/
// @version 2025-08-06
// @description Cleans text copied from ChatGPT
// @author Thinh Nguyen
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @run-at document-end
// ==/UserScript==
(() => {
/* ───────── Config ───────── */
const BUTTON_COPY_DELAY = 60; // ms – delay before we patch the clipboard
const COPY_BUTTON_SELECTOR = 'button[aria-label="Copy"],button[data-testid="copy-turn-action-button"]'; // Centralized selector
/* ───────── Utils ───────── */
// Invisible/control characters to strip entirely
const INVISIBLES = /[\u00AD\u180E\u200B-\u200D\u2060-\u2064\u2066-\u206F\u200E\u200F\u202A-\u202E\uFEFF]/g;
const VARIATION_SELECTORS = /[\uFE00-\uFE0F]/g;
// Exotic spaces to normalize to regular space
const EXOTIC_SPACES = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
// All dash/hyphen variants to normalize to regular hyphen
const DASHES = /[-]/g;
// Smart quotes to normalize to ASCII quotes
const CURLY_SINGLE = /['']/g;
const CURLY_DOUBLE = /[""]/g;
// Ellipsis to normalize to three dots
const ELLIPSIS = /\u2026/g;
const filterText = t => {
// Guard against very large strings to prevent freezing
if (!t || typeof t !== 'string' || t.length > 1000000) return t;
return t
.replace(INVISIBLES, '')
.replace(VARIATION_SELECTORS, '')
.replace(EXOTIC_SPACES, ' ')
.replace(DASHES, '-')
.replace(CURLY_SINGLE, "'")
.replace(CURLY_DOUBLE, '"')
.replace(ELLIPSIS, '...')
.replace(/ {2,}/g, ' '); // Collapse multiple spaces
};
const unchanged = (a, b) => a === b; // fast equality
/* ───────── Logging ───────── */
const log = (raw, clean, origin) => {
if (unchanged(raw, clean)) return;
console.log(`[ChatGPT-CC] COPY (${origin})\n raw : ${JSON.stringify(raw)}\n clean : ${JSON.stringify(clean)}`);
};
/* ───────── Toast ───────── */
const showToast = (() => {
let toastElement = null;
let fadeTimer = null;
let removeTimer = null;
return (message = 'Clipboard cleaned') => {
// Create toast element only once
if (!toastElement) {
toastElement = document.createElement('div');
Object.assign(toastElement.style, {
position: 'fixed',
top: '12px',
left: '50%',
transform: 'translateX(-50%)',
background: '#2ecc71',
color: '#fff',
padding: '6px 12px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
zIndex: 2147483647,
boxShadow: '0 2px 6px rgba(0,0,0,.2)',
opacity: '0',
transition: 'opacity .3s ease'
});
document.body.appendChild(toastElement);
}
// Clear existing timers
clearTimeout(fadeTimer);
clearTimeout(removeTimer);
// Update content and show
toastElement.textContent = message;
toastElement.style.opacity = '1';
// Schedule fade and hide
fadeTimer = setTimeout(() => { toastElement.style.opacity = '0' }, 1300);
};
})();
/* ───────── Selection helper ───────── */
const getSelectionText = () => {
const sel = window.getSelection();
if (sel?.toString()) return sel.toString();
const el = document.activeElement;
if (el && (el.tagName === 'TEXTAREA' ||
(el.tagName === 'INPUT' && el.type === 'text'))) {
return el.value.slice(el.selectionStart, el.selectionEnd);
}
return '';
};
/* ───────── Clipboard helper ───────── */
const setClipboardData = (event, text) => {
event.clipboardData.setData('text/plain', text);
event.stopImmediatePropagation();
event.preventDefault();
};
/* ───────── Generic copy/cut handler ───────── */
const onCopyCut = e => {
const cd = e.clipboardData;
if (!cd) return;
// Path 1: page already populated clipboardData
if (cd.types?.includes('text/plain')) {
const raw = cd.getData('text/plain');
const clean = filterText(raw);
if (unchanged(raw, clean)) return;
setClipboardData(e, clean);
log(raw, clean, 'clipboardData');
showToast();
return;
}
// Path 2: normal selection
const rawSel = getSelectionText();
if (!rawSel) return;
const cleanSel = filterText(rawSel);
if (unchanged(rawSel, cleanSel)) return;
setClipboardData(e, cleanSel);
log(rawSel, cleanSel, 'selection');
showToast();
};
document.addEventListener('copy', onCopyCut, true);
document.addEventListener('cut', onCopyCut, true);
/* ───────── “Copy”‑button fallback ───────── */
document.addEventListener('click', e => {
const btn = e.target.closest(COPY_BUTTON_SELECTOR);
if (!btn) return;
// Retry mechanism for reliable clipboard access
const attemptClipboardPatch = async (retries = 5, delay = 50) => {
// Guard against missing clipboard API
if (!navigator.clipboard) return;
// Skip if document is not focused to prevent retry loops
if (!document.hasFocus()) return;
try {
const raw = await navigator.clipboard.readText();
// If clipboard is empty, it might not be ready. Retry.
if (!raw && retries > 0) {
setTimeout(() => attemptClipboardPatch(retries - 1, delay), delay);
return;
}
const clean = filterText(raw);
if (unchanged(raw, clean)) return;
await navigator.clipboard.writeText(clean);
log(raw, clean, 'button');
showToast();
} catch (err) {
// Only log error if it's not a permission denied or not focused error
if (err.name !== 'NotAllowedError' && err.name !== 'DOMException') {
console.warn('[ChatGPT-CC] Button copy failed:', err);
}
}
};
// Start the clipboard patching process
setTimeout(() => attemptClipboardPatch(), BUTTON_COPY_DELAY);
}, true);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment