UNPKG

tasmota-esp-web-tools

Version:
672 lines (670 loc) 26.9 kB
const ANSI_256 = (() => { const t = []; // Standard colors 0-7 t[0] = "rgb(0,0,0)"; t[1] = "rgb(128,0,0)"; t[2] = "rgb(0,128,0)"; t[3] = "rgb(128,128,0)"; t[4] = "rgb(0,0,128)"; t[5] = "rgb(128,0,128)"; t[6] = "rgb(0,128,128)"; t[7] = "rgb(192,192,192)"; // Bright colors 8-15 t[8] = "rgb(128,128,128)"; t[9] = "rgb(255,0,0)"; t[10] = "rgb(0,255,0)"; t[11] = "rgb(255,255,0)"; t[12] = "rgb(99,153,255)"; t[13] = "rgb(255,0,255)"; t[14] = "rgb(0,255,255)"; t[15] = "rgb(255,255,255)"; // 6x6x6 color cube 16-231 for (let i = 0; i < 216; i++) { const r = Math.floor(i / 36); const g = Math.floor((i % 36) / 6); const b = i % 6; t[16 + i] = "rgb(" + (r ? r * 40 + 55 : 0) + "," + (g ? g * 40 + 55 : 0) + "," + (b ? b * 40 + 55 : 0) + ")"; } // Grayscale ramp 232-255 for (let i = 0; i < 24; i++) { const v = i * 10 + 8; t[232 + i] = "rgb(" + v + "," + v + "," + v + ")"; } return t; })(); // Maps 256-color indices 0–7 to the named CSS class tokens so that // \x1b[38;5;1m renders the same red as \x1b[31m. const ANSI_NAMED = [ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", ]; const MAX_LINES = 2000; export class ColoredConsole { constructor(targetElement) { this.targetElement = targetElement; this.state = { bold: false, italic: false, underline: false, strikethrough: false, foregroundColor: null, backgroundColor: null, fgRgb: null, bgRgb: null, dim: false, reverse: false, carriageReturn: false, lines: [], secret: false, blink: false, rapidBlink: false, }; this._destroyed = false; this._rafId = 0; this._timeoutId = 0; this._atBottom = true; this._sentinel = null; // Full history for log export — never trimmed, unlike the DOM cap this._exportLines = []; // Redacted plain-text version of _exportLines for logs() export this._redactedLines = []; this._visibilityHandler = null; // Track whether the user is scrolled to the bottom via IntersectionObserver // on a sentinel element, avoiding forced reflows on every processLines call. const sentinel = document.createElement("div"); sentinel.style.height = "1px"; this._sentinel = sentinel; targetElement.appendChild(sentinel); this._intersectionObserver = new IntersectionObserver((entries) => { this._atBottom = entries[0].isIntersecting; }, { root: targetElement, threshold: 0 }); this._intersectionObserver.observe(sentinel); // When the page becomes hidden, rAF is paused. Switch any pending rAF to // a timeout so state.lines doesn't accumulate unbounded while backgrounded. this._visibilityHandler = () => { if (document.hidden && this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = 0; if (!this._timeoutId) { this._timeoutId = window.setTimeout(() => this.processLines(), 50); } } }; document.addEventListener("visibilitychange", this._visibilityHandler); } logs() { return this._redactedLines.join(""); } _redactLine(line) { // Mirrors the SGR state machine in processLine but produces plain text, // replacing concealed (SGR 8) spans with "[redacted]" and stripping all // other ANSI sequences so the export never leaks hidden content. const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g; let i = 0; let secret = false; let out = ""; while (true) { const match = re.exec(line); if (match === null) break; const j = match.index; const text = line.substring(i, j); if (text) out += secret ? "[redacted]" : text; i = j + match[0].length; if (match[1] === undefined) continue; for (const colorCode of match[1].split(";")) { switch (parseInt(colorCode)) { case 0: secret = false; break; case 8: secret = true; break; case 28: secret = false; break; } } } const tail = line.substring(i); if (tail) out += secret ? "[redacted]" : tail; return out; } destroy() { var _a; this._destroyed = true; this.state.carriageReturn = false; this.state.lines = []; (_a = this._intersectionObserver) === null || _a === void 0 ? void 0 : _a.disconnect(); if (this._visibilityHandler) { document.removeEventListener("visibilitychange", this._visibilityHandler); this._visibilityHandler = null; } // Remove the sentinel from the DOM to avoid leaking it on teardown if (this._sentinel) { this._sentinel.remove(); this._sentinel = null; } if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = 0; } if (this._timeoutId) { clearTimeout(this._timeoutId); this._timeoutId = 0; } } processLine(line) { const re = /(?:\x1B|\\x1B)(?:\[(.*?)([@-~])|\].*?(?:\x07|\x1B\\))/g; let i = 0; const lineSpan = document.createElement("span"); lineSpan.classList.add("line"); const addSpan = (content) => { if (content === "") return; if (this.state.secret) { const redacted = document.createElement("span"); redacted.classList.add("log-secret-redacted"); redacted.appendChild(document.createTextNode("[redacted]")); lineSpan.appendChild(redacted); return; } const span = document.createElement("span"); if (this.state.bold) span.classList.add("log-bold"); if (this.state.dim) span.classList.add("log-dim"); if (this.state.italic) span.classList.add("log-italic"); if (this.state.underline) span.classList.add("log-underline"); if (this.state.strikethrough) span.classList.add("log-strikethrough"); if (this.state.blink) span.classList.add("log-blink"); if (this.state.rapidBlink) span.classList.add("log-rapid-blink"); // Resolve colors with reverse-video support let fgRgb = this.state.fgRgb; let bgRgb = this.state.bgRgb; let fg = this.state.foregroundColor; let bg = this.state.backgroundColor; if (this.state.reverse) { fgRgb = this.state.bgRgb; bgRgb = this.state.fgRgb; fg = this.state.backgroundColor; bg = this.state.foregroundColor; if (!fgRgb && !fg && !bgRgb && !bg) { span.classList.add("log-reverse"); } else { if (!fgRgb && !fg) fgRgb = "rgb(28,28,28)"; if (!bgRgb && !bg) bgRgb = "rgb(221,221,221)"; } } // Inline rgb() style takes priority over CSS class if (fgRgb) { span.style.color = fgRgb; } else if (fg !== null) { span.classList.add(`log-fg-${fg}`); } if (bgRgb) { span.style.backgroundColor = bgRgb; } else if (bg !== null) { span.classList.add(`log-bg-${bg}`); } span.appendChild(document.createTextNode(content)); lineSpan.appendChild(span); }; while (true) { const match = re.exec(line); if (match === null) break; const j = match.index; addSpan(line.substring(i, j)); i = j + match[0].length; // Only process SGR sequences (final byte 'm'); skip cursor, erase, etc. if (match[1] === undefined || match[2] !== "m") continue; const rawCodes = match[1] === "" ? [""] : match[1].split(";"); const codes = []; let invalidSgr = false; for (const rawCode of rawCodes) { if (rawCode === "") { codes.push(0); continue; } if (!/^\d+$/.test(rawCode)) { invalidSgr = true; break; } codes.push(Number(rawCode)); } if (invalidSgr) continue; for (let ci = 0; ci < codes.length; ci++) { const code = codes[ci]; switch (code) { case 0: this.state.bold = false; this.state.dim = false; this.state.italic = false; this.state.underline = false; this.state.strikethrough = false; this.state.foregroundColor = null; this.state.backgroundColor = null; this.state.fgRgb = null; this.state.bgRgb = null; this.state.reverse = false; this.state.secret = false; this.state.blink = false; this.state.rapidBlink = false; break; case 1: this.state.bold = true; break; case 2: this.state.dim = true; break; case 3: this.state.italic = true; break; case 4: this.state.underline = true; break; case 5: this.state.blink = true; this.state.rapidBlink = false; break; case 6: this.state.rapidBlink = true; this.state.blink = false; break; case 7: this.state.reverse = true; break; case 8: this.state.secret = true; break; case 9: this.state.strikethrough = true; break; case 22: this.state.bold = false; this.state.dim = false; break; case 23: this.state.italic = false; break; case 24: this.state.underline = false; break; case 25: this.state.blink = false; this.state.rapidBlink = false; break; case 27: this.state.reverse = false; break; case 28: this.state.secret = false; break; case 29: this.state.strikethrough = false; break; case 30: this.state.foregroundColor = "black"; this.state.fgRgb = null; break; case 31: this.state.foregroundColor = "red"; this.state.fgRgb = null; break; case 32: this.state.foregroundColor = "green"; this.state.fgRgb = null; break; case 33: this.state.foregroundColor = "yellow"; this.state.fgRgb = null; break; case 34: this.state.foregroundColor = "blue"; this.state.fgRgb = null; break; case 35: this.state.foregroundColor = "magenta"; this.state.fgRgb = null; break; case 36: this.state.foregroundColor = "cyan"; this.state.fgRgb = null; break; case 37: this.state.foregroundColor = "white"; this.state.fgRgb = null; break; case 38: // Extended foreground: 38;5;n (256-color) or 38;2;r;g;b (true-color) if (ci + 1 < codes.length) { if (codes[ci + 1] === 5) { if (ci + 2 < codes.length) { const idx = codes[ci + 2]; if (idx >= 0 && idx <= 7 && ANSI_NAMED[idx]) { this.state.foregroundColor = ANSI_NAMED[idx]; this.state.fgRgb = null; } else if (idx >= 0 && idx <= 255 && ANSI_256[idx]) { this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[idx]; } ci += 2; } else { ci += 1; } } else if (codes[ci + 1] === 2) { if (ci + 4 < codes.length) { this.state.foregroundColor = null; const r = Math.max(0, Math.min(255, codes[ci + 2])); const g = Math.max(0, Math.min(255, codes[ci + 3])); const b = Math.max(0, Math.min(255, codes[ci + 4])); this.state.fgRgb = "rgb(" + r + "," + g + "," + b + ")"; ci += 4; } else { ci = codes.length - 1; } } } break; case 39: this.state.foregroundColor = null; this.state.fgRgb = null; break; case 40: this.state.backgroundColor = "black"; this.state.bgRgb = null; break; case 41: this.state.backgroundColor = "red"; this.state.bgRgb = null; break; case 42: this.state.backgroundColor = "green"; this.state.bgRgb = null; break; case 43: this.state.backgroundColor = "yellow"; this.state.bgRgb = null; break; case 44: this.state.backgroundColor = "blue"; this.state.bgRgb = null; break; case 45: this.state.backgroundColor = "magenta"; this.state.bgRgb = null; break; case 46: this.state.backgroundColor = "cyan"; this.state.bgRgb = null; break; case 47: this.state.backgroundColor = "white"; this.state.bgRgb = null; break; case 48: // Extended background: 48;5;n (256-color) or 48;2;r;g;b (true-color) if (ci + 1 < codes.length) { if (codes[ci + 1] === 5) { if (ci + 2 < codes.length) { const idx = codes[ci + 2]; if (idx >= 0 && idx <= 7 && ANSI_NAMED[idx]) { this.state.backgroundColor = ANSI_NAMED[idx]; this.state.bgRgb = null; } else if (idx >= 0 && idx <= 255 && ANSI_256[idx]) { this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[idx]; } ci += 2; } else { ci += 1; } } else if (codes[ci + 1] === 2) { if (ci + 4 < codes.length) { this.state.backgroundColor = null; const r = Math.max(0, Math.min(255, codes[ci + 2])); const g = Math.max(0, Math.min(255, codes[ci + 3])); const b = Math.max(0, Math.min(255, codes[ci + 4])); this.state.bgRgb = "rgb(" + r + "," + g + "," + b + ")"; ci += 4; } else { ci = codes.length - 1; } } } break; case 49: this.state.backgroundColor = null; this.state.bgRgb = null; break; // Bright foreground colors case 90: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[8]; break; case 91: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[9]; break; case 92: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[10]; break; case 93: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[11]; break; case 94: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[12]; break; case 95: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[13]; break; case 96: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[14]; break; case 97: this.state.foregroundColor = null; this.state.fgRgb = ANSI_256[15]; break; // Bright background colors case 100: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[8]; break; case 101: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[9]; break; case 102: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[10]; break; case 103: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[11]; break; case 104: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[12]; break; case 105: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[13]; break; case 106: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[14]; break; case 107: this.state.backgroundColor = null; this.state.bgRgb = ANSI_256[15]; break; } } } addSpan(line.substring(i)); return lineSpan; } processLines() { this._rafId = 0; this._timeoutId = 0; if (this._destroyed || this.state.lines.length === 0) { return; } const prevCarriageReturn = this.state.carriageReturn; const fragment = document.createDocumentFragment(); for (const line of this.state.lines) { if (this.state.carriageReturn && line !== "\n") { if (fragment.childElementCount) { fragment.removeChild(fragment.lastChild); } } const hadCarriageReturn = line.endsWith("\r"); fragment.appendChild(this.processLine(line.replace(/\r/g, ""))); this.state.carriageReturn = hadCarriageReturn; } const sentinel = this._sentinel; if (!sentinel) { this.state.lines = []; return; } if (prevCarriageReturn && this.state.lines[0] !== "\n" && sentinel.previousSibling) { this.targetElement.replaceChild(fragment, sentinel.previousSibling); } else { this.targetElement.insertBefore(fragment, sentinel); } this.state.lines = []; // Trim oldest line-spans when DOM grows too large const children = this.targetElement.children; const excess = children.length - 1 - MAX_LINES; if (excess > 0) { if (!this._atBottom) { let removedHeight = 0; for (let i = 0; i < excess; i++) { removedHeight += children[i].getBoundingClientRect() .height; } for (let i = 0; i < excess; i++) { this.targetElement.removeChild(children[0]); } this.targetElement.scrollTop -= removedHeight; } else { for (let i = 0; i < excess; i++) { this.targetElement.removeChild(children[0]); } } } if (this._atBottom) { this.targetElement.scrollTop = this.targetElement.scrollHeight; } } addLine(line) { if (this._destroyed) return; this._exportLines.push(line); this._redactedLines.push(this._redactLine(line)); this.state.lines.push(line); if (!this._rafId && !this._timeoutId) { if (document.hidden) { this._timeoutId = window.setTimeout(() => this.processLines(), 50); } else { this._rafId = requestAnimationFrame(() => this.processLines()); } } } } export const coloredConsoleStyles = ` .log { flex: 1; background-color: #1c1c1c; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; padding: 16px; overflow: auto; line-height: 1.45; border-radius: 3px; white-space: pre-wrap; overflow-wrap: break-word; color: #ddd; } .log-bold { font-weight: bold; } .log-dim { opacity: 0.5; } .log-italic { font-style: italic; } .log-underline { text-decoration: underline; } .log-strikethrough { text-decoration: line-through; } .log-underline.log-strikethrough { text-decoration: underline line-through; } .log-blink { animation: blink 1s step-end infinite; } .log-rapid-blink { animation: blink 0.4s step-end infinite; } @keyframes blink { 50% { opacity: 0; } } .log-secret { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .log-secret-redacted { opacity: 0; width: 1px; font-size: 1px; } .log-reverse { background: #ddd; color: #1c1c1c; } .log-fg-black { color: rgb(128, 128, 128); } .log-fg-red { color: rgb(255, 0, 0); } .log-fg-green { color: rgb(0, 255, 0); } .log-fg-yellow { color: rgb(255, 255, 0); } .log-fg-blue { color: rgb(0, 0, 255); } .log-fg-magenta { color: rgb(255, 0, 255); } .log-fg-cyan { color: rgb(0, 255, 255); } .log-fg-white { color: rgb(187, 187, 187); } .log-bg-black { background-color: rgb(0, 0, 0); } .log-bg-red { background-color: rgb(255, 0, 0); } .log-bg-green { background-color: rgb(0, 255, 0); } .log-bg-yellow { background-color: rgb(255, 255, 0); } .log-bg-blue { background-color: rgb(0, 0, 255); } .log-bg-magenta { background-color: rgb(255, 0, 255); } .log-bg-cyan { background-color: rgb(0, 255, 255); } .log-bg-white { background-color: rgb(255, 255, 255); } `;