Terminal Chat

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل 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);
})();