High-performance terminal-style YouTube live chat interface with custom themes, message pruning, and power-user controls.
// ==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);
})();