Terminal Chat

High-performance terminal-style YouTube live chat interface with custom themes, message pruning, and power-user controls.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Terminal Chat
// @namespace    https://castor-tm.neocities.org/
// @version      v0.9.55
// @description  High-performance terminal-style YouTube live chat interface with custom themes, message pruning, and power-user controls.
// @author       CastorWD
// @license      CC-BY-NC-SA-4.0
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @run-at       document-end
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    let policy = { createHTML: (s) => s };
    if (window.trustedTypes?.createPolicy) {
        try { policy = window.trustedTypes.createPolicy('chatPolicy', { createHTML: (s) => s }); } catch (e) {}
    }

    let showTimestamps = false;
    let filterTerm = "";
    let lastItems = null;
    let activeObserver = null;
    let currentChannel = "";
    let activeParticipants = new Map();
    let mutedUsers = new Set(JSON.parse(localStorage.getItem('yt-terminal-muted') || "[]"));

    let isPaused = false;
    let messageBuffer = [];
    let cmdHistory = JSON.parse(localStorage.getItem('yt-terminal-history') || "[]");
    let cmdIndex = cmdHistory.length;

    let savedGeom = JSON.parse(localStorage.getItem('yt-terminal-geom')) || { top: '100px', left: '100px', width: '450px', height: '600px' };
    savedGeom.isSnapped = true;

    let savedSizes = JSON.parse(localStorage.getItem('yt-terminal-sizes')) || { font: '13px', emo: '15px', bgLight: 0, nameColor: '#00ff00', msgColor: '#eeeeee', highlights: '', msgLimit: 500 };

    if (savedSizes.txtColor) {
        savedSizes.nameColor = savedSizes.txtColor;
        savedSizes.msgColor = '#eeeeee';
        delete savedSizes.txtColor;
        localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes));
    }

    const saveMuted = () => localStorage.setItem('yt-terminal-muted', JSON.stringify([...mutedUsers]));

    const blindfoldYouTube = (chatDoc) => {
        if (!chatDoc || chatDoc.getElementById('blinder-css')) return;
        const style = chatDoc.createElement('style');
        style.id = 'blinder-css';
        style.textContent = `
            yt-live-chat-item-list-renderer {
                content-visibility: hidden;
                contain: strict;
                opacity: 0 !important;
                pointer-events: none !important;
            }
        `;
        chatDoc.head.appendChild(style);
    };

    const purgeMuted = (user) => {
        const stream = document.getElementById('term-stream');
        if (!stream) return;
        const msgs = stream.querySelectorAll('.term-msg');
        msgs.forEach(m => {
            const auth = m.querySelector('.t-auth');
            if (auth && auth.textContent.replace(':', '').trim() === user) {
                m.remove();
            }
        });
    };

    const sendToNative = (text) => {
        const chatFrame = document.querySelector('ytd-live-chat-frame iframe');
        const chatDoc = chatFrame?.contentDocument || chatFrame?.contentWindow?.document;
        const inputField = chatDoc?.querySelector('#input.yt-live-chat-text-input-field-renderer');
        const sendBtn = chatDoc?.querySelector('#send-button button');
        if (inputField && sendBtn) {
            inputField.focus();
            inputField.textContent = text;
            inputField.dispatchEvent(new InputEvent('input', { bubbles: true, data: text }));
            setTimeout(() => {
                if (sendBtn.hasAttribute('disabled')) sendBtn.removeAttribute('disabled');
                sendBtn.click();
            }, 150);
        }
    };

    const syncStyle = (el) => {
        if (!el) return;
        el.style.fontSize = savedSizes.font;
        const hideEmo = savedSizes.emo === '0px' || savedSizes.emo === 'dot';
        const showDot = savedSizes.emo === 'dot';

        el.querySelectorAll('img, .emoji, .yt-emoji-icon').forEach(img => {
            if (hideEmo) {
                img.style.display = 'none';
            } else {
                img.style.display = 'inline-block';
                img.style.width = savedSizes.emo;
                img.style.height = savedSizes.emo;
                // FIXED: Vertical alignment optical correction
                img.style.verticalAlign = 'middle';
                img.style.position = 'relative';
                img.style.top = '-1px';
            }
        });

        el.querySelectorAll('.e-dot').forEach(dot => {
            dot.style.display = showDot ? 'inline' : 'none';
        });

        const t = el.querySelector('.t-time');
        if (t) t.style.display = showTimestamps ? 'inline' : 'none';
        if (filterTerm && !el.classList.contains('session-break')) {
            el.classList.toggle('t-hide', !el.textContent.toLowerCase().includes(filterTerm));
        }
    };

    const applyStyles = () => {
        if (document.getElementById('term-core-css')) return;
        const style = document.createElement('style');
        style.id = 'term-core-css';
        style.textContent = `
            ytd-live-chat-frame#chat { display: none !important; }
            #secondary-inner { display: flex !important; flex-direction: column !important; }

            #my-term-container, #user-menu {
                --bg-l: 0%;
                --name-c: #00ff00;
                --msg-c: #eeeeee;
                --bg-base: hsl(0, 0%, var(--bg-l));
                --bg-up: hsl(0, 0%, calc(var(--bg-l) + 10%));
                --border-c: hsl(0, 0%, calc(var(--bg-l) + 20%));
            }

            #my-term-container {
                background: var(--bg-base) !important; border: 1px solid var(--border-c) !important; border-radius: 4px;
                display: flex !important; flex-direction: column; overflow: hidden;
                font-family: 'Consolas', monospace; position: relative; z-index: 2147483647 !important;
                box-sizing: border-box;
                order: -9999 !important;
                resize: both;
                min-width: 280px; min-height: 200px;
            }
            #term-header { padding: 4px 6px; background: var(--bg-up); color: var(--name-c); border-bottom: 1px solid var(--border-c); font-size: 10px; display: flex; justify-content: space-between; align-items: center; cursor: default; user-select: none; }
            #term-stream {
                flex-grow: 1; overflow-y: auto; padding: 10px; color: var(--msg-c); line-height: 1.4;
                scrollbar-width: thin; scrollbar-color: var(--border-c) var(--bg-base);
                overscroll-behavior: contain !important;
            }
            .term-msg { display: block; color: var(--msg-c); margin-bottom: 3px; padding-left: 3px; border-left: 2px solid transparent; }
            .t-auth { color: var(--name-c); cursor: pointer; font-weight: bold; margin-right: 4px; filter: brightness(1.2); }
            .t-auth:hover { text-decoration: underline; }
            .t-time { color: gray; margin-right: 6px; font-size: 0.9em; display: none; }
            .t-hide { display: none !important; }
            .t-highlight { background: rgba(255, 50, 50, 0.2); border-left: 2px solid #ff4444; }

            .t-super { color: #ffd700 !important; font-weight: bold; }
            .t-super-tag { background: #b8860b; color: #fff; padding: 0 4px; margin-right: 5px; border-radius: 2px; }

            #term-overlay { position: absolute; top: 28px; left: 0; right: 0; bottom: 0; background: var(--bg-base); color: var(--name-c); z-index: 2147483645; display: none; overflow-y: auto; padding: 15px; font-size: 11px; overscroll-behavior: contain; }
            .overlay-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid var(--border-c); padding-bottom: 5px; }
            .overlay-item { padding: 6px 10px; border-bottom: 1px solid var(--border-c); display: flex; justify-content: space-between; align-items: center; }
            .h-btn { background: var(--bg-up); color: var(--name-c); border: 1px solid var(--border-c); font-size: 10px; cursor: pointer; padding: 1px 4px; }
            #user-menu { position: fixed; background: var(--bg-base); border: 1px solid var(--name-c); color: var(--name-c); z-index: 2147483647; display: none; font-size: 11px; min-width: 110px; box-shadow: 4px 4px 0 rgba(0,0,0,0.5); }
            .menu-opt { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--border-c); }
            .menu-opt:hover { background: var(--name-c); color: var(--bg-base); }
            #term-input-area { background: var(--bg-up); padding: 5px; border-top: 1px solid var(--border-c); display: flex; }
            #term-input {
                background: var(--bg-base); color: var(--msg-c); border: 1px solid var(--border-c);
                width: 100%; font-family: 'Consolas', monospace; resize: none; outline: none;
                padding: 6px; font-size: 13px; box-sizing: border-box;
                min-height: 28px; max-height: 150px; overflow-y: auto; line-height: 1.3;
            }
            .e-dot { color: var(--msg-c); opacity: 0.6; }
        `;
        document.head.appendChild(style);
    };

    const setupEvents = (cont) => {
        if (cont.dataset.init === "true") return;
        const overlay = cont.querySelector('#term-overlay');
        const filterInput = cont.querySelector('#term-filter');
        const chatInput = cont.querySelector('#term-input');
        const streamContainer = cont.querySelector('#term-stream');
        let currentView = "users";

        const applyTheme = () => {
            const menu = document.getElementById('user-menu');
            const bgStr = (savedSizes.bgLight || 0) + '%';

            cont.style.setProperty('--bg-l', bgStr);
            cont.style.setProperty('--name-c', savedSizes.nameColor || '#00ff00');
            cont.style.setProperty('--msg-c', savedSizes.msgColor || '#eeeeee');

            if (menu) {
                menu.style.setProperty('--bg-l', bgStr);
                menu.style.setProperty('--name-c', savedSizes.nameColor || '#00ff00');
                menu.style.setProperty('--msg-c', savedSizes.msgColor || '#eeeeee');
            }
        };

        applyTheme();

        cont.querySelector('#f-size').value = savedSizes.font;
        cont.querySelector('#e-size').value = savedSizes.emo;

        const scrollToBottom = () => {
            if (streamContainer) streamContainer.scrollTop = streamContainer.scrollHeight;
        };

        chatInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight) + 'px';
        });

        chatInput.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                const text = this.value.trim();
                if (text) {
                    sendToNative(text);
                    cmdHistory.push(text);
                    if (cmdHistory.length > 50) cmdHistory.shift();
                    localStorage.setItem('yt-terminal-history', JSON.stringify(cmdHistory));
                    cmdIndex = cmdHistory.length;
                    this.value = "";
                    this.style.height = 'auto';
                }
            }
            else if (e.key === 'ArrowUp') {
                if (cmdIndex > 0) {
                    cmdIndex--;
                    this.value = cmdHistory[cmdIndex];
                    setTimeout(() => { this.selectionStart = this.selectionEnd = this.value.length; }, 0);
                }
                e.preventDefault();
            }
            else if (e.key === 'ArrowDown') {
                if (cmdIndex < cmdHistory.length - 1) {
                    cmdIndex++;
                    this.value = cmdHistory[cmdIndex];
                } else if (cmdIndex === cmdHistory.length - 1) {
                    cmdIndex++;
                    this.value = "";
                }
                e.preventDefault();
            }
        });

        cont.querySelector('#g-pause').onclick = (e) => {
            isPaused = !isPaused;
            e.target.textContent = isPaused ? '▶' : '⏸';
            e.target.style.color = isPaused ? '#f44' : 'var(--name-c)';
            e.target.style.borderColor = isPaused ? '#f44' : 'var(--border-c)';

            if (!isPaused && messageBuffer.length > 0) {
                const fragment = document.createDocumentFragment();
                messageBuffer.forEach(msgDiv => fragment.appendChild(msgDiv));
                streamContainer.appendChild(fragment);
                messageBuffer = [];

                const limit = parseInt(savedSizes.msgLimit) || 500;
                while (streamContainer.children.length > limit) {
                    streamContainer.firstChild.remove();
                }

                setTimeout(scrollToBottom, 50);
            }
        };

        new ResizeObserver(() => {
            if (!savedGeom.isSnapped) {
                savedGeom.width = cont.style.width;
                savedGeom.height = cont.style.height;
            } else {
                savedGeom.height = cont.style.height;
            }
            localStorage.setItem('yt-terminal-geom', JSON.stringify(savedGeom));
        }).observe(cont);

        cont.addEventListener('wheel', (e) => {
            let targetScrollArea = streamContainer;
            if (overlay.style.display === 'block' && e.target.closest('#term-overlay')) {
                targetScrollArea = overlay;
            }
            const isAtTop = targetScrollArea.scrollTop === 0;
            const isAtBottom = targetScrollArea.scrollHeight - targetScrollArea.scrollTop <= targetScrollArea.clientHeight + 1;
            if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) {
                e.preventDefault();
            }
        }, { passive: false });

        window.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' || e.keyCode === 27) {
                let intercepted = false;
                if (filterInput.value || chatInput.value) {
                    filterInput.value = ""; filterTerm = ""; chatInput.value = "";
                    chatInput.style.height = 'auto';
                    cont.querySelectorAll('.term-msg').forEach(m => m.classList.remove('t-hide'));
                    intercepted = true;
                    setTimeout(scrollToBottom, 50);
                }
                if (overlay.style.display === 'block') {
                    overlay.style.display = 'none';
                    intercepted = true;
                }
                const menu = document.getElementById('user-menu');
                if (menu && menu.style.display === 'block') {
                    menu.style.display = 'none';
                    intercepted = true;
                }
                if (document.activeElement === filterInput || document.activeElement === chatInput) {
                    filterInput.blur();
                    chatInput.blur();
                    intercepted = true;
                }
                if (intercepted) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                }
            }
        }, true);

        const sanitizeGeom = () => {
            let t = parseInt(savedGeom.top) || 100;
            let l = parseInt(savedGeom.left) || 100;
            if (t < 0 || t > window.innerHeight - 50) t = 100;
            if (l < 0 || l > window.innerWidth - 100) l = 100;
            savedGeom.top = t + 'px';
            savedGeom.left = l + 'px';
        };

        cont.querySelector('#g-unsnap').onclick = () => {
            sanitizeGeom();
            Object.assign(cont.style, { position: 'fixed', top: savedGeom.top, left: savedGeom.left, width: savedGeom.width || '450px', height: savedGeom.height || '600px', margin: '0' });
            document.body.appendChild(cont);
            savedGeom.isSnapped = false;
            localStorage.setItem('yt-terminal-geom', JSON.stringify(savedGeom));
            setTimeout(scrollToBottom, 50);
        };

        cont.querySelector('#g-snap').onclick = () => {
            const t = document.querySelector('#secondary-inner');
            if (t) {
                Object.assign(cont.style, { position: 'relative', top: '0', left: '0', width: '100%', height: savedGeom.height || '600px', margin: '0' });
                t.prepend(cont);
                savedGeom.isSnapped = true;
                localStorage.setItem('yt-terminal-geom', JSON.stringify(savedGeom));
                setTimeout(scrollToBottom, 50);
            }
        };

        let isDragging = false, offset = [0,0];
        cont.addEventListener('mousedown', (e) => {
            if (e.altKey) {
                e.preventDefault();
                isDragging = true;
                offset = [cont.offsetLeft - e.clientX, cont.offsetTop - e.clientY];
                cont.style.cursor = 'grabbing';
            }
        });
        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                let newLeft = (e.clientX + offset[0]) + 'px';
                let newTop = (e.clientY + offset[1]) + 'px';
                cont.style.left = newLeft;
                cont.style.top = newTop;
                savedGeom.left = newLeft;
                savedGeom.top = newTop;
            }
        });
        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                cont.style.cursor = 'auto';
                sanitizeGeom();
                localStorage.setItem('yt-terminal-geom', JSON.stringify(savedGeom));
            }
        });

        cont.addEventListener('click', (e) => {
            const menu = document.getElementById('user-menu');
            if (e.target.classList.contains('t-auth')) {
                const user = e.target.textContent.replace(':', '').trim();
                const cleanUser = user.replace(/^@+/, '');
                const userData = activeParticipants.get(user) || {};

                menu.innerHTML = policy.createHTML(`
                    <div class="menu-opt" id="m-mention">@MENTION</div>
                    <div class="menu-opt" id="m-mute">MUTE</div>
                    <div class="menu-opt" id="m-visit">VISIT</div>
                    <div class="menu-opt" id="m-cancel" style="color:var(--msg-c); opacity:0.6;">CANCEL</div>
                `);

                applyTheme();

                menu.style.display = 'block';
                menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px';

                document.getElementById('m-mention').onclick = () => { chatInput.value += `@${cleanUser} `; chatInput.focus(); chatInput.dispatchEvent(new Event('input')); menu.style.display = 'none'; };
                document.getElementById('m-mute').onclick = () => { mutedUsers.add(user); saveMuted(); purgeMuted(user); menu.style.display = 'none'; };
                document.getElementById('m-visit').onclick = () => {
                    let finalUrl = "";
                    if (userData.cid && userData.cid.startsWith('UC')) { finalUrl = `https://www.youtube.com/channel/${userData.cid}`; }
                    else if (userData.url) { finalUrl = userData.url; }
                    else { const handle = user.startsWith('@') ? user : `@${user}`; finalUrl = `https://www.youtube.com/${handle.replace(':', '').trim()}`; }
                    if (finalUrl) window.open(finalUrl, '_blank');
                    menu.style.display = 'none';
                };
                document.getElementById('m-cancel').onclick = () => { menu.style.display = 'none'; };
            } else { if (menu) menu.style.display = 'none'; }
        });

        const renderOverlay = () => {
            if (currentView === "users") {
                const active = Array.from(activeParticipants.keys()).filter(p => !mutedUsers.has(p)).sort();
                overlay.innerHTML = policy.createHTML(`
                    <div class="overlay-header"><strong>ACTIVE (${active.length})</strong> <button id="o-toggle-muted" class="h-btn" style="color:#f44; border-color:#f44;">MUTED LIST</button></div>
                    ${active.map(p => `<div class="overlay-item"><span>${p}</span> <button class="h-btn o-mute" data-user="${p}">MUTE</button></div>`).join('')}
                `);
            } else if (currentView === "muted") {
                const muted = Array.from(mutedUsers).sort();
                overlay.innerHTML = policy.createHTML(`
                    <div class="overlay-header">
                        <strong>MUTED (${muted.length})</strong>
                        <div>
                            <button id="o-unmute-all" class="h-btn" style="color:#f44; margin-right:6px; border-color:#f44;">UNMUTE ALL</button>
                            <button id="o-toggle-users" class="h-btn">ACTIVE USERS</button>
                        </div>
                    </div>
                    ${muted.map(p => `<div class="overlay-item"><span>${p}</span> <button class="h-btn o-unmute" data-user="${p}">UNMUTE</button></div>`).join('')}
                `);
            } else if (currentView === "help") {
                overlay.innerHTML = policy.createHTML(`
                    <div class="overlay-header">
                        <strong>TERMINAL COMMANDS</strong>
                        <div>
                            <button id="o-dump" class="h-btn" style="color:#0f0; border-color:#0f0; margin-right:6px;">DUMP LOG</button>
                            <button id="o-close" class="h-btn" style="color:#f44; border-color:#f44;">CLOSE</button>
                        </div>
                    </div>
                    <div style="line-height:1.6;">
                    <strong>ENTER:</strong> Send comment<br>
                    <strong>SHIFT+ENTER:</strong> Newline<br>
                    <strong>DISPLAY:</strong> Text Size (10,13,15) | Icon Size (15,20,●,0)<br>
                    <strong>FILTER:</strong> Search comments<br>
                    <strong>▲▼:</strong> Unsnap/Snap<br>
                    <strong>👤:</strong> User list<br>
                    <strong>ALT+DRAG:</strong> Move window | <strong>RESIZE:</strong> Drag corner<br>
                    <strong>ESC:</strong> Clear text & menus | <strong>CLOCK:</strong> Toggle time<br>
                    <strong>CHAT:</strong> Up/Down Arrow for History<br>
                    <hr style="border:0; border-top:1px solid var(--border-c); margin:8px 0;">

                    <strong>MSG LIMIT:</strong> <input type="number" id="c-limit" value="${savedSizes.msgLimit || 500}" style="background:var(--bg-base); color:var(--msg-c); border:1px solid var(--border-c); width:50px; font-size:11px; padding:2px; margin-bottom:5px;"><br>

                    <strong>HIGHLIGHT WORDS:</strong> <input type="text" id="c-high" value="${savedSizes.highlights || ''}" placeholder="myname, topic..." style="background:var(--bg-base); color:var(--msg-c); border:1px solid var(--border-c); width:130px; font-size:11px; padding:2px;">
                    <div style="font-size:9px; color:gray; line-height:1; margin-bottom:5px; margin-top:2px;">(Separate multiple words with commas)</div>

                    <strong>BACKGROUND:</strong> <input type="range" id="c-range" min="0" max="100" value="${savedSizes.bgLight || 0}" style="vertical-align:middle; width:80px;"><br>
                    <div style="margin-top:5px;">
                        <strong>NAME:</strong> <input type="color" id="c-name" value="${savedSizes.nameColor || '#00ff00'}" style="vertical-align:middle; width:25px; height:20px; padding:0; border:1px solid var(--border-c); background:var(--bg-base); cursor:pointer; margin-right:15px;">
                        <strong>MSG:</strong> <input type="color" id="c-msg" value="${savedSizes.msgColor || '#eeeeee'}" style="vertical-align:middle; width:25px; height:20px; padding:0; border:1px solid var(--border-c); background:var(--bg-base); cursor:pointer;">
                    </div>
                    </div>
                `);

                const range = overlay.querySelector('#c-range');
                if (range) {
                    range.oninput = (v) => {
                        savedSizes.bgLight = v.target.value;
                        localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes));
                        applyTheme();
                    };
                }
                const pickerName = overlay.querySelector('#c-name');
                if (pickerName) {
                    pickerName.oninput = (v) => {
                        savedSizes.nameColor = v.target.value;
                        localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes));
                        applyTheme();
                    };
                }
                const pickerMsg = overlay.querySelector('#c-msg');
                if (pickerMsg) {
                    pickerMsg.oninput = (v) => {
                        savedSizes.msgColor = v.target.value;
                        localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes));
                        applyTheme();
                    };
                }
                const inputHigh = overlay.querySelector('#c-high');
                if (inputHigh) {
                    inputHigh.oninput = (e) => {
                        savedSizes.highlights = e.target.value;
                        localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes));
                    };
                }
                const inputLimit = overlay.querySelector('#c-limit');
                if (inputLimit) {
                    inputLimit.onchange = (e) => {
                        savedSizes.msgLimit = parseInt(e.target.value) || 500;
                        localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes));
                    };
                }
            }
        };

        overlay.onclick = (e) => {
            if (e.target.id === 'o-toggle-muted') { currentView = "muted"; renderOverlay(); }
            else if (e.target.id === 'o-toggle-users') { currentView = "users"; renderOverlay(); }
            else if (e.target.id === 'o-unmute-all') { mutedUsers.clear(); saveMuted(); renderOverlay(); }
            else if (e.target.id === 'o-close') { overlay.style.display = 'none'; }
            else if (e.target.id === 'o-dump') {
                const msgs = Array.from(cont.querySelectorAll('.term-msg')).map(m => {
                    const time = m.querySelector('.t-time')?.textContent || '';
                    const auth = m.querySelector('.t-auth')?.textContent || '';
                    const text = m.querySelector('span:last-child')?.textContent || '';
                    return `[${time}] ${auth} ${text}`;
                }).join('\n');

                const blob = new Blob([msgs], { type: 'text/plain' });
                const a = document.createElement('a');
                a.href = URL.createObjectURL(blob);
                a.download = `Terminal_Chat_Log_${new Date().toISOString().slice(0,10)}.txt`;
                a.click();
            }
            else if (e.target.classList.contains('o-mute')) {
                const u = e.target.dataset.user; mutedUsers.add(u); saveMuted(); purgeMuted(u); renderOverlay();
            }
            else if (e.target.classList.contains('o-unmute')) {
                const u = e.target.dataset.user; mutedUsers.delete(u); saveMuted(); renderOverlay();
            }
        };

        cont.querySelector('#g-user').onclick = () => {
            currentView = "users";
            overlay.style.display = overlay.style.display === 'block' ? 'none' : 'block';
            if (overlay.style.display === 'block') renderOverlay();
        };

        cont.querySelector('#g-help').onclick = () => {
            currentView = "help";
            overlay.style.display = overlay.style.display === 'block' ? 'none' : 'block';
            if (overlay.style.display === 'block') renderOverlay();
        };

        cont.querySelector('#f-size').onchange = (e) => { savedSizes.font = e.target.value; cont.querySelectorAll('.term-msg').forEach(syncStyle); localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes)); setTimeout(scrollToBottom, 50); };
        cont.querySelector('#e-size').onchange = (e) => { savedSizes.emo = e.target.value; cont.querySelectorAll('.term-msg').forEach(syncStyle); localStorage.setItem('yt-terminal-sizes', JSON.stringify(savedSizes)); setTimeout(scrollToBottom, 50); };
        cont.querySelector('#term-clock').onclick = () => { showTimestamps = !showTimestamps; cont.querySelectorAll('.term-msg').forEach(syncStyle); };
        filterInput.oninput = (e) => {
            filterTerm = e.target.value.toLowerCase();
            cont.querySelectorAll('.term-msg').forEach(m => {
                if (!m.classList.contains('session-break')) {
                    m.classList.toggle('t-hide', !m.textContent.toLowerCase().includes(filterTerm));
                }
            });
        };

        cont.dataset.init = "true";
    };

    const enforceTop = () => {
        const cont = document.getElementById('my-term-container');
        const target = document.querySelector('#secondary-inner');
        if (cont && target && savedGeom.isSnapped) {
            if (target.firstElementChild !== cont) {
                target.prepend(cont);
            }
        }
    };

    const injectChannelHeader = () => {
        const channelLink = document.querySelector('#upload-info #channel-name a, .ytd-video-owner-renderer #channel-name a, #owner-name a');
        const channelName = channelLink?.textContent.trim();

        if (channelName && channelName !== currentChannel) {
            currentChannel = channelName;
            const stream = document.getElementById('term-stream');
            if (stream) {
                const hr = document.createElement('div');
                hr.className = "session-break term-msg";
                hr.style.cssText = "color: var(--msg-c); border-bottom: 1px dashed var(--border-c); margin: 5px 0 10px 0; padding-bottom: 5px; text-align: center; font-size: 11px; letter-spacing: 1px; border-left:none;";
                hr.textContent = `*** CONNECTED TO: ${currentChannel.toUpperCase()} ***`;
                stream.appendChild(hr);

                setTimeout(() => { stream.scrollTop = stream.scrollHeight; }, 100);
            }
        }
    };

    const createUI = () => {
        if (document.getElementById('my-term-container')) return;
        const target = document.querySelector('#secondary-inner');
        if (!target) return;
        applyStyles();
        if (!document.getElementById('user-menu')) { const m = document.createElement('div'); m.id = 'user-menu'; document.body.appendChild(m); }

        const cont = document.createElement('div');
        cont.id = 'my-term-container';
        cont.innerHTML = policy.createHTML(`
            <div id="term-header">
                <div style="display:flex; gap:4px; align-items:center;">
                    <input type="text" id="term-filter" placeholder="FLTR" class="h-btn" style="width:35px;">
                    <select id="f-size" class="h-btn"><option value="10px">10</option><option value="13px">13</option><option value="15px">15</option></select>
                    <select id="e-size" class="h-btn"><option value="15px">15</option><option value="20px">20</option><option value="dot">●</option><option value="0px">00</option></select>
                    <button id="g-unsnap" class="h-btn">▲</button><button id="g-snap" class="h-btn">▼</button>
                    <span id="term-debug" style="font-size:9px; color:gray; margin-left:5px;">...</span>
                </div>
                <div style="display:flex; gap:6px; align-items:center;">
                    <button id="g-pause" class="h-btn" style="font-size:11px; padding:0 4px;">⏸</button>
                    <span id="term-clock" style="cursor:pointer; font-size:10px;">00:00:00</span>
                    <button id="g-user" class="h-btn">👤</button>
                    <button id="g-help" class="h-btn">?</button>
                </div>
            </div>
            <div id="term-overlay"></div>
            <div id="term-stream"></div>
            <div id="term-input-area"><textarea id="term-input" rows="1" placeholder="Chat..."></textarea></div>
        `);

        if (savedGeom.isSnapped) {
            Object.assign(cont.style, { width: '100%', height: savedGeom.height || '600px' });
            target.prepend(cont);
        } else {
            Object.assign(cont.style, { position: 'fixed', top: savedGeom.top, left: savedGeom.left, width: savedGeom.width || '450px', height: savedGeom.height || '600px' });
            document.body.appendChild(cont);
        }

        setupEvents(cont);
        setInterval(() => { const c = document.getElementById('term-clock'); if(c) c.textContent = new Date().toLocaleTimeString('en-GB'); }, 1000);
    };

    const watch = () => {
        injectChannelHeader();

        const dbg = document.getElementById('term-debug');
        const frame = document.querySelector('ytd-live-chat-frame iframe');
        if (!frame || !dbg) return;
        let chatDoc;
        try { chatDoc = frame.contentDocument || frame.contentWindow.document; } catch (e) { dbg.textContent = "SHIELD"; return; }

        blindfoldYouTube(chatDoc);

        let items = chatDoc?.getElementById('items');
        if (items && items !== lastItems) {
            dbg.textContent = "LINK"; lastItems = items;
            activeObserver?.disconnect();
            activeObserver = new MutationObserver(() => {
                const stream = document.getElementById('term-stream');
                const isSticky = stream.scrollHeight - stream.scrollTop <= stream.clientHeight + 50;

                let highWords = (savedSizes.highlights || "").split(',').map(s => s.trim().toLowerCase()).filter(s => s);
                const limit = parseInt(savedSizes.msgLimit) || 500;

                const msgs = chatDoc.querySelectorAll('yt-live-chat-text-message-renderer:not([data-cap]), yt-live-chat-paid-message-renderer:not([data-cap])');

                if (msgs.length > 0) {
                    const fragment = document.createDocumentFragment();
                    msgs.forEach(msg => {
                        msg.dataset.cap = 'true';
                        const authorEl = msg.querySelector('#author-name');
                        const auth = authorEl?.textContent || 'User';

                        const channelLink = msg.querySelector('a.yt-live-chat-author-chip, a#author-name')?.href || '';
                        const cid = msg.getAttribute('author-external-id') ||
                                    msg.querySelector('#author-name')?.closest('a')?.href?.split('/').pop() || '';

                        activeParticipants.set(auth, { url: channelLink, cid: cid });
                        if (mutedUsers.has(auth)) return;

                        let rawHtml = msg.querySelector('#message')?.innerHTML || '';
                        let msgHtml = rawHtml.replace(/<img[^>]*>/gi, match => `${match}<span class="e-dot" style="display:none; margin:0 2px;">●</span>`);

                        let isSuperchat = msg.tagName.toLowerCase() === 'yt-live-chat-paid-message-renderer';
                        let amount = isSuperchat ? (msg.querySelector('#purchase-amount')?.textContent || 'SUPER') : '';
                        let prefix = isSuperchat ? `<span class="t-super-tag">[${amount}]</span> ` : '';

                        let isHigh = false;
                        if (highWords.length > 0) {
                            const lowerMsg = (msg.querySelector('#message')?.textContent || '').toLowerCase();
                            isHigh = highWords.some(w => lowerMsg.includes(w));
                        }

                        const div = document.createElement('div');
                        div.className = 'term-msg';

                        if (isSuperchat) div.classList.add('t-super');
                        if (isHigh) div.classList.add('t-highlight');

                        div.innerHTML = policy.createHTML(`<span class="t-time">${msg.querySelector('#timestamp')?.textContent||''}</span><strong class="t-auth">${auth}:</strong> <span>${prefix}${msgHtml}</span>`);
                        syncStyle(div);

                        if (isPaused) {
                            messageBuffer.push(div);
                            if (messageBuffer.length > limit) messageBuffer.shift();
                        } else {
                            fragment.appendChild(div);
                        }
                    });

                    if (!isPaused) {
                        stream.appendChild(fragment);

                        while (stream.children.length > limit) {
                            stream.firstChild.remove();
                        }

                        if (isSticky) {
                            stream.scrollTop = stream.scrollHeight;
                        }
                    }
                }
            });
            activeObserver.observe(items, { childList: true, subtree: true });
        }
    };

    setInterval(() => { createUI(); watch(); enforceTop(); }, 1000);
})();