UNPKG

tasmota-esp-web-tools

Version:
231 lines (230 loc) 8.89 kB
import { ColoredConsole, coloredConsoleStyles } from "../util/console-color"; import { sleep } from "../util/sleep"; import { LineBreakTransformer } from "../util/line-break-transformer"; import { TimestampTransformer } from "../util/timestamp-transformer"; export class EwtConsole extends HTMLElement { constructor() { super(...arguments); this.allowInput = true; this._commandHistory = []; this._historyIndex = -1; this._currentInput = ""; } logs() { var _a; return ((_a = this._console) === null || _a === void 0 ? void 0 : _a.logs()) || ""; } connectedCallback() { var _a; if (this._console) { return; } // attachShadow throws if a shadow root already exists; reuse it on reattach const shadowRoot = (_a = this.shadowRoot) !== null && _a !== void 0 ? _a : this.attachShadow({ mode: "open" }); shadowRoot.innerHTML = ` <style> :host, input { background-color: #1c1c1c; color: #ddd; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 1.45; display: flex; flex-direction: column; overflow: hidden; } form { display: flex; align-items: center; padding: 0 8px 0 16px; flex-shrink: 0; } input { flex: 1; padding: 4px; margin: 0 8px; border: 0; outline: none; } ${coloredConsoleStyles} </style> <div class="log"></div> ${this.allowInput ? `<form> <span aria-hidden="true">></span> <input aria-label="Serial command" autofocus> </form> ` : ""} `; this._console = new ColoredConsole(this.shadowRoot.querySelector("div")); if (this.allowInput) { const input = this.shadowRoot.querySelector("input"); this._clickHandler = () => { var _a; // Only focus input if user didn't select some text if (((_a = getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) === "") { input.focus(); } }; this.addEventListener("click", this._clickHandler); input.addEventListener("keydown", (ev) => { if (ev.key === "Enter") { ev.preventDefault(); ev.stopPropagation(); this._sendCommand(); } else if (ev.key === "ArrowUp") { ev.preventDefault(); this._navigateHistory(input, 1); } else if (ev.key === "ArrowDown") { ev.preventDefault(); this._navigateHistory(input, -1); } else { // User is editing — reset history navigation this._historyIndex = -1; } }); } const abortController = new AbortController(); const connection = this._connect(abortController.signal); this._cancelConnection = () => { abortController.abort(); return connection; }; } async _connect(signal) { this.logger.debug("Starting console read loop"); // Capture a stable reference; addLine() becomes a no-op after destroy() const consoleView = this._console; if (!this.port.readable) { consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(""); consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(""); consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("Terminal disconnected: Port readable stream not available"); this.logger.error("Port readable stream not available - port may need to be reopened at correct baudrate"); return; } try { await this.port.readable .pipeThrough(new TextDecoderStream(), { signal }) .pipeThrough(new TransformStream(new LineBreakTransformer())) .pipeThrough(new TransformStream(new TimestampTransformer())) .pipeTo(new WritableStream({ write: (line) => { consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(line); }, })); if (!signal.aborted) { consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(""); consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(""); consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("Terminal disconnected"); } } catch (err) { if (!signal.aborted) { consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(""); consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(""); consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(`Terminal disconnected: ${err}`); } } finally { await sleep(100); this.logger.debug("Finished console read loop"); } } _navigateHistory(input, direction) { if (this._commandHistory.length === 0) return; // Save current input before navigating away if (this._historyIndex === -1) { this._currentInput = input.value; } const newIndex = this._historyIndex + direction; if (newIndex < 0) { // Back to current (unsent) input this._historyIndex = -1; input.value = this._currentInput; } else if (newIndex < this._commandHistory.length) { this._historyIndex = newIndex; input.value = this._commandHistory[this._historyIndex]; } // Move cursor to end const len = input.value.length; input.setSelectionRange(len, len); } async _sendCommand() { var _a, _b; const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector("input"); if (!input || !this.port.writable) return; const value = input.value; const writer = this.port.writable.getWriter(); try { await writer.write(new TextEncoder().encode(`${value}\r\n`)); (_b = this._console) === null || _b === void 0 ? void 0 : _b.addLine(`> ${value}\r\n`); if (input.isConnected) { // Add to history (skip empty, skip consecutive duplicates, cap at 100) if (value && value !== this._commandHistory[0]) { this._commandHistory.unshift(value); if (this._commandHistory.length > 100) { this._commandHistory.pop(); } } this._historyIndex = -1; this._currentInput = ""; input.value = ""; input.focus(); } } finally { try { writer.releaseLock(); } catch (err) { this.logger.error("Ignoring release lock error", err); } } } async disconnect() { var _a; if (this._clickHandler) { this.removeEventListener("click", this._clickHandler); this._clickHandler = undefined; } if (this._cancelConnection) { await this._cancelConnection(); this._cancelConnection = undefined; } (_a = this._console) === null || _a === void 0 ? void 0 : _a.destroy(); this._console = undefined; } disconnectedCallback() { var _a; if (this._clickHandler) { this.removeEventListener("click", this._clickHandler); this._clickHandler = undefined; } if (this._cancelConnection) { this._cancelConnection(); this._cancelConnection = undefined; } (_a = this._console) === null || _a === void 0 ? void 0 : _a.destroy(); this._console = undefined; } async reset() { this.logger.debug("Triggering reset."); if (this.onReset) { try { await this.onReset(); } catch (err) { this.logger.error("Reset callback failed:", err); } } await sleep(1000); } } customElements.define("ew-console", EwtConsole);