tasmota-esp-web-tools
Version:
Web tools for ESP devices
231 lines (230 loc) • 8.89 kB
JavaScript
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);