tasmota-esp-web-tools 11.1.9 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ewt-console.d.ts +5 -0
- package/dist/components/ewt-console.js +100 -30
- package/dist/util/console-color.d.ts +15 -1
- package/dist/util/console-color.js +167 -19
- package/dist/util/line-break-transformer.js +13 -4
- package/dist/util/timestamp-transformer.d.ts +3 -0
- package/dist/util/timestamp-transformer.js +32 -0
- package/dist/web/install-button.js +1 -1
- package/dist/web/{install-dialog-Bd8cf0W3.js → install-dialog-DyJZE2aX.js} +43 -43
- package/js/modules/install-button.js +1 -1
- package/js/modules/{install-dialog-Bd8cf0W3.js → install-dialog-DyJZE2aX.js} +43 -43
- package/package.json +3 -3
|
@@ -6,11 +6,16 @@ export declare class EwtConsole extends HTMLElement {
|
|
|
6
6
|
onReset?: () => Promise<void>;
|
|
7
7
|
private _console?;
|
|
8
8
|
private _cancelConnection?;
|
|
9
|
+
private _commandHistory;
|
|
10
|
+
private _historyIndex;
|
|
11
|
+
private _currentInput;
|
|
9
12
|
logs(): string;
|
|
10
13
|
connectedCallback(): void;
|
|
11
14
|
private _connect;
|
|
15
|
+
private _navigateHistory;
|
|
12
16
|
private _sendCommand;
|
|
13
17
|
disconnect(): Promise<void>;
|
|
18
|
+
disconnectedCallback(): void;
|
|
14
19
|
reset(): Promise<void>;
|
|
15
20
|
}
|
|
16
21
|
declare global {
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { ColoredConsole, coloredConsoleStyles } from "../util/console-color";
|
|
2
2
|
import { sleep } from "../util/sleep";
|
|
3
3
|
import { LineBreakTransformer } from "../util/line-break-transformer";
|
|
4
|
+
import { TimestampTransformer } from "../util/timestamp-transformer";
|
|
4
5
|
export class EwtConsole extends HTMLElement {
|
|
5
6
|
constructor() {
|
|
6
7
|
super(...arguments);
|
|
7
8
|
this.allowInput = true;
|
|
9
|
+
this._commandHistory = [];
|
|
10
|
+
this._historyIndex = -1;
|
|
11
|
+
this._currentInput = "";
|
|
8
12
|
}
|
|
9
13
|
logs() {
|
|
10
14
|
var _a;
|
|
11
15
|
return ((_a = this._console) === null || _a === void 0 ? void 0 : _a.logs()) || "";
|
|
12
16
|
}
|
|
13
17
|
connectedCallback() {
|
|
18
|
+
var _a;
|
|
14
19
|
if (this._console) {
|
|
15
20
|
return;
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
// attachShadow throws if a shadow root already exists; reuse it on reattach
|
|
23
|
+
const shadowRoot = (_a = this.shadowRoot) !== null && _a !== void 0 ? _a : this.attachShadow({ mode: "open" });
|
|
18
24
|
shadowRoot.innerHTML = `
|
|
19
25
|
<style>
|
|
20
26
|
:host, input {
|
|
@@ -65,6 +71,18 @@ export class EwtConsole extends HTMLElement {
|
|
|
65
71
|
ev.stopPropagation();
|
|
66
72
|
this._sendCommand();
|
|
67
73
|
}
|
|
74
|
+
else if (ev.key === "ArrowUp") {
|
|
75
|
+
ev.preventDefault();
|
|
76
|
+
this._navigateHistory(input, 1);
|
|
77
|
+
}
|
|
78
|
+
else if (ev.key === "ArrowDown") {
|
|
79
|
+
ev.preventDefault();
|
|
80
|
+
this._navigateHistory(input, -1);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// User is editing — reset history navigation
|
|
84
|
+
this._historyIndex = -1;
|
|
85
|
+
}
|
|
68
86
|
});
|
|
69
87
|
}
|
|
70
88
|
const abortController = new AbortController();
|
|
@@ -74,64 +92,116 @@ export class EwtConsole extends HTMLElement {
|
|
|
74
92
|
return connection;
|
|
75
93
|
};
|
|
76
94
|
}
|
|
77
|
-
async _connect(
|
|
95
|
+
async _connect(signal) {
|
|
78
96
|
this.logger.debug("Starting console read loop");
|
|
79
|
-
//
|
|
97
|
+
// Capture a stable reference; addLine() becomes a no-op after destroy()
|
|
98
|
+
const consoleView = this._console;
|
|
80
99
|
if (!this.port.readable) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("");
|
|
101
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("");
|
|
102
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("Terminal disconnected: Port readable stream not available");
|
|
84
103
|
this.logger.error("Port readable stream not available - port may need to be reopened at correct baudrate");
|
|
85
104
|
return;
|
|
86
105
|
}
|
|
87
106
|
try {
|
|
88
|
-
await this.port
|
|
89
|
-
.
|
|
90
|
-
signal: abortSignal,
|
|
91
|
-
})
|
|
107
|
+
await this.port.readable
|
|
108
|
+
.pipeThrough(new TextDecoderStream(), { signal })
|
|
92
109
|
.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
|
110
|
+
.pipeThrough(new TransformStream(new TimestampTransformer()))
|
|
93
111
|
.pipeTo(new WritableStream({
|
|
94
|
-
write: (
|
|
95
|
-
|
|
112
|
+
write: (line) => {
|
|
113
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(line);
|
|
96
114
|
},
|
|
97
115
|
}));
|
|
98
|
-
if (!
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
if (!signal.aborted) {
|
|
117
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("");
|
|
118
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("");
|
|
119
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("Terminal disconnected");
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
|
-
catch (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (!signal.aborted) {
|
|
124
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("");
|
|
125
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine("");
|
|
126
|
+
consoleView === null || consoleView === void 0 ? void 0 : consoleView.addLine(`Terminal disconnected: ${err}`);
|
|
127
|
+
}
|
|
108
128
|
}
|
|
109
129
|
finally {
|
|
110
130
|
await sleep(100);
|
|
111
131
|
this.logger.debug("Finished console read loop");
|
|
112
132
|
}
|
|
113
133
|
}
|
|
134
|
+
_navigateHistory(input, direction) {
|
|
135
|
+
if (this._commandHistory.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
// Save current input before navigating away
|
|
138
|
+
if (this._historyIndex === -1) {
|
|
139
|
+
this._currentInput = input.value;
|
|
140
|
+
}
|
|
141
|
+
const newIndex = this._historyIndex + direction;
|
|
142
|
+
if (newIndex < 0) {
|
|
143
|
+
// Back to current (unsent) input
|
|
144
|
+
this._historyIndex = -1;
|
|
145
|
+
input.value = this._currentInput;
|
|
146
|
+
}
|
|
147
|
+
else if (newIndex < this._commandHistory.length) {
|
|
148
|
+
this._historyIndex = newIndex;
|
|
149
|
+
input.value = this._commandHistory[this._historyIndex];
|
|
150
|
+
}
|
|
151
|
+
// Move cursor to end
|
|
152
|
+
const len = input.value.length;
|
|
153
|
+
input.setSelectionRange(len, len);
|
|
154
|
+
}
|
|
114
155
|
async _sendCommand() {
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
156
|
+
var _a, _b;
|
|
157
|
+
const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector("input");
|
|
158
|
+
if (!input || !this.port.writable)
|
|
159
|
+
return;
|
|
160
|
+
const value = input.value;
|
|
118
161
|
const writer = this.port.writable.getWriter();
|
|
119
|
-
await writer.write(encoder.encode(command + "\r\n"));
|
|
120
|
-
this._console.addLine(`> ${command}\r\n`);
|
|
121
|
-
input.value = "";
|
|
122
|
-
input.focus();
|
|
123
162
|
try {
|
|
124
|
-
writer.
|
|
163
|
+
await writer.write(new TextEncoder().encode(`${value}\r\n`));
|
|
164
|
+
(_b = this._console) === null || _b === void 0 ? void 0 : _b.addLine(`> ${value}\r\n`);
|
|
165
|
+
if (input.isConnected) {
|
|
166
|
+
// Add to history (skip empty, skip consecutive duplicates, cap at 100)
|
|
167
|
+
if (value && value !== this._commandHistory[0]) {
|
|
168
|
+
this._commandHistory.unshift(value);
|
|
169
|
+
if (this._commandHistory.length > 100) {
|
|
170
|
+
this._commandHistory.pop();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this._historyIndex = -1;
|
|
174
|
+
this._currentInput = "";
|
|
175
|
+
input.value = "";
|
|
176
|
+
input.focus();
|
|
177
|
+
}
|
|
125
178
|
}
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
finally {
|
|
180
|
+
try {
|
|
181
|
+
writer.releaseLock();
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.error("Ignoring release lock error", err);
|
|
185
|
+
}
|
|
128
186
|
}
|
|
129
187
|
}
|
|
130
188
|
async disconnect() {
|
|
189
|
+
var _a;
|
|
131
190
|
if (this._cancelConnection) {
|
|
132
191
|
await this._cancelConnection();
|
|
133
192
|
this._cancelConnection = undefined;
|
|
134
193
|
}
|
|
194
|
+
(_a = this._console) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
195
|
+
this._console = undefined;
|
|
196
|
+
}
|
|
197
|
+
disconnectedCallback() {
|
|
198
|
+
var _a;
|
|
199
|
+
if (this._cancelConnection) {
|
|
200
|
+
this._cancelConnection();
|
|
201
|
+
this._cancelConnection = undefined;
|
|
202
|
+
}
|
|
203
|
+
(_a = this._console) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
204
|
+
this._console = undefined;
|
|
135
205
|
}
|
|
136
206
|
async reset() {
|
|
137
207
|
this.logger.debug("Triggering reset.");
|
|
@@ -6,14 +6,28 @@ interface ConsoleState {
|
|
|
6
6
|
foregroundColor: string | null;
|
|
7
7
|
backgroundColor: string | null;
|
|
8
8
|
carriageReturn: boolean;
|
|
9
|
+
lines: string[];
|
|
9
10
|
secret: boolean;
|
|
11
|
+
blink: boolean;
|
|
12
|
+
rapidBlink: boolean;
|
|
10
13
|
}
|
|
11
14
|
export declare class ColoredConsole {
|
|
12
15
|
targetElement: HTMLElement;
|
|
13
16
|
state: ConsoleState;
|
|
17
|
+
private _destroyed;
|
|
18
|
+
private _rafId;
|
|
19
|
+
private _timeoutId;
|
|
20
|
+
private _atBottom;
|
|
21
|
+
private _intersectionObserver?;
|
|
22
|
+
private _sentinel;
|
|
23
|
+
private _exportLines;
|
|
24
|
+
private _visibilityHandler;
|
|
14
25
|
constructor(targetElement: HTMLElement);
|
|
15
26
|
logs(): string;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
processLine(line: string): Element;
|
|
29
|
+
processLines(): void;
|
|
16
30
|
addLine(line: string): void;
|
|
17
31
|
}
|
|
18
|
-
export declare const coloredConsoleStyles = "\n .log {\n flex: 1;\n background-color: #1c1c1c;\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier,\n monospace;\n font-size: 12px;\n padding: 16px;\n overflow: auto;\n line-height: 1.45;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n color: #ddd;\n }\n\n .log-bold {\n font-weight: bold;\n }\n .log-italic {\n font-style: italic;\n }\n .log-underline {\n text-decoration: underline;\n }\n .log-strikethrough {\n text-decoration: line-through;\n }\n .log-underline.log-strikethrough {\n text-decoration: underline line-through;\n }\n .log-secret {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n }\n .log-secret-redacted {\n opacity: 0;\n width: 1px;\n font-size: 1px;\n }\n .log-fg-black {\n color: rgb(128, 128, 128);\n }\n .log-fg-red {\n color: rgb(255, 0, 0);\n }\n .log-fg-green {\n color: rgb(0, 255, 0);\n }\n .log-fg-yellow {\n color: rgb(255, 255, 0);\n }\n .log-fg-blue {\n color: rgb(0, 0, 255);\n }\n .log-fg-magenta {\n color: rgb(255, 0, 255);\n }\n .log-fg-cyan {\n color: rgb(0, 255, 255);\n }\n .log-fg-white {\n color: rgb(187, 187, 187);\n }\n .log-bg-black {\n background-color: rgb(0, 0, 0);\n }\n .log-bg-red {\n background-color: rgb(255, 0, 0);\n }\n .log-bg-green {\n background-color: rgb(0, 255, 0);\n }\n .log-bg-yellow {\n background-color: rgb(255, 255, 0);\n }\n .log-bg-blue {\n background-color: rgb(0, 0, 255);\n }\n .log-bg-magenta {\n background-color: rgb(255, 0, 255);\n }\n .log-bg-cyan {\n background-color: rgb(0, 255, 255);\n }\n .log-bg-white {\n background-color: rgb(255, 255, 255);\n }\n";
|
|
32
|
+
export declare const coloredConsoleStyles = "\n .log {\n flex: 1;\n background-color: #1c1c1c;\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier,\n monospace;\n font-size: 12px;\n padding: 16px;\n overflow: auto;\n line-height: 1.45;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n color: #ddd;\n }\n\n .log-bold {\n font-weight: bold;\n }\n .log-italic {\n font-style: italic;\n }\n .log-underline {\n text-decoration: underline;\n }\n .log-strikethrough {\n text-decoration: line-through;\n }\n .log-underline.log-strikethrough {\n text-decoration: underline line-through;\n }\n .log-blink {\n animation: blink 1s step-end infinite;\n }\n .log-rapid-blink {\n animation: blink 0.4s step-end infinite;\n }\n @keyframes blink {\n 50% {\n opacity: 0;\n }\n }\n .log-secret {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n }\n .log-secret-redacted {\n opacity: 0;\n width: 1px;\n font-size: 1px;\n }\n .log-fg-black {\n color: rgb(128, 128, 128);\n }\n .log-fg-red {\n color: rgb(255, 0, 0);\n }\n .log-fg-green {\n color: rgb(0, 255, 0);\n }\n .log-fg-yellow {\n color: rgb(255, 255, 0);\n }\n .log-fg-blue {\n color: rgb(0, 0, 255);\n }\n .log-fg-magenta {\n color: rgb(255, 0, 255);\n }\n .log-fg-cyan {\n color: rgb(0, 255, 255);\n }\n .log-fg-white {\n color: rgb(187, 187, 187);\n }\n .log-bg-black {\n background-color: rgb(0, 0, 0);\n }\n .log-bg-red {\n background-color: rgb(255, 0, 0);\n }\n .log-bg-green {\n background-color: rgb(0, 255, 0);\n }\n .log-bg-yellow {\n background-color: rgb(255, 255, 0);\n }\n .log-bg-blue {\n background-color: rgb(0, 0, 255);\n }\n .log-bg-magenta {\n background-color: rgb(255, 0, 255);\n }\n .log-bg-cyan {\n background-color: rgb(0, 255, 255);\n }\n .log-bg-white {\n background-color: rgb(255, 255, 255);\n }\n";
|
|
19
33
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const MAX_LINES = 2000;
|
|
1
2
|
export class ColoredConsole {
|
|
2
3
|
constructor(targetElement) {
|
|
3
4
|
this.targetElement = targetElement;
|
|
@@ -9,28 +10,74 @@ export class ColoredConsole {
|
|
|
9
10
|
foregroundColor: null,
|
|
10
11
|
backgroundColor: null,
|
|
11
12
|
carriageReturn: false,
|
|
13
|
+
lines: [],
|
|
12
14
|
secret: false,
|
|
15
|
+
blink: false,
|
|
16
|
+
rapidBlink: false,
|
|
13
17
|
};
|
|
18
|
+
this._destroyed = false;
|
|
19
|
+
this._rafId = 0;
|
|
20
|
+
this._timeoutId = 0;
|
|
21
|
+
this._atBottom = true;
|
|
22
|
+
this._sentinel = null;
|
|
23
|
+
// Full history for log export — never trimmed, unlike the DOM cap
|
|
24
|
+
this._exportLines = [];
|
|
25
|
+
this._visibilityHandler = null;
|
|
26
|
+
// Track whether the user is scrolled to the bottom via IntersectionObserver
|
|
27
|
+
// on a sentinel element, avoiding forced reflows on every processLines call.
|
|
28
|
+
const sentinel = document.createElement("div");
|
|
29
|
+
sentinel.style.height = "1px";
|
|
30
|
+
this._sentinel = sentinel;
|
|
31
|
+
targetElement.appendChild(sentinel);
|
|
32
|
+
this._intersectionObserver = new IntersectionObserver((entries) => {
|
|
33
|
+
this._atBottom = entries[0].isIntersecting;
|
|
34
|
+
}, { root: targetElement, threshold: 0 });
|
|
35
|
+
this._intersectionObserver.observe(sentinel);
|
|
36
|
+
// When the page becomes hidden, rAF is paused. Switch any pending rAF to
|
|
37
|
+
// a timeout so state.lines doesn't accumulate unbounded while backgrounded.
|
|
38
|
+
this._visibilityHandler = () => {
|
|
39
|
+
if (document.hidden && this._rafId) {
|
|
40
|
+
cancelAnimationFrame(this._rafId);
|
|
41
|
+
this._rafId = 0;
|
|
42
|
+
if (!this._timeoutId) {
|
|
43
|
+
this._timeoutId = window.setTimeout(() => this.processLines(), 50);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
document.addEventListener("visibilitychange", this._visibilityHandler);
|
|
14
48
|
}
|
|
15
49
|
logs() {
|
|
16
|
-
return this.
|
|
50
|
+
return this._exportLines.join("");
|
|
17
51
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.
|
|
52
|
+
destroy() {
|
|
53
|
+
var _a;
|
|
54
|
+
this._destroyed = true;
|
|
55
|
+
this.state.carriageReturn = false;
|
|
56
|
+
this.state.lines = [];
|
|
57
|
+
(_a = this._intersectionObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
58
|
+
if (this._visibilityHandler) {
|
|
59
|
+
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
|
60
|
+
this._visibilityHandler = null;
|
|
61
|
+
}
|
|
62
|
+
// Remove the sentinel from the DOM to avoid leaking it on teardown
|
|
63
|
+
if (this._sentinel) {
|
|
64
|
+
this._sentinel.remove();
|
|
65
|
+
this._sentinel = null;
|
|
66
|
+
}
|
|
67
|
+
if (this._rafId) {
|
|
68
|
+
cancelAnimationFrame(this._rafId);
|
|
69
|
+
this._rafId = 0;
|
|
27
70
|
}
|
|
28
|
-
if (
|
|
29
|
-
this.
|
|
71
|
+
if (this._timeoutId) {
|
|
72
|
+
clearTimeout(this._timeoutId);
|
|
73
|
+
this._timeoutId = 0;
|
|
30
74
|
}
|
|
75
|
+
}
|
|
76
|
+
processLine(line) {
|
|
77
|
+
const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g;
|
|
78
|
+
let i = 0;
|
|
31
79
|
const lineSpan = document.createElement("span");
|
|
32
80
|
lineSpan.classList.add("line");
|
|
33
|
-
this.targetElement.appendChild(lineSpan);
|
|
34
81
|
const addSpan = (content) => {
|
|
35
82
|
if (content === "")
|
|
36
83
|
return;
|
|
@@ -45,6 +92,10 @@ export class ColoredConsole {
|
|
|
45
92
|
span.classList.add("log-strikethrough");
|
|
46
93
|
if (this.state.secret)
|
|
47
94
|
span.classList.add("log-secret");
|
|
95
|
+
if (this.state.blink)
|
|
96
|
+
span.classList.add("log-blink");
|
|
97
|
+
if (this.state.rapidBlink)
|
|
98
|
+
span.classList.add("log-rapid-blink");
|
|
48
99
|
if (this.state.foregroundColor !== null)
|
|
49
100
|
span.classList.add(`log-fg-${this.state.foregroundColor}`);
|
|
50
101
|
if (this.state.backgroundColor !== null)
|
|
@@ -78,6 +129,8 @@ export class ColoredConsole {
|
|
|
78
129
|
this.state.foregroundColor = null;
|
|
79
130
|
this.state.backgroundColor = null;
|
|
80
131
|
this.state.secret = false;
|
|
132
|
+
this.state.blink = false;
|
|
133
|
+
this.state.rapidBlink = false;
|
|
81
134
|
break;
|
|
82
135
|
case 1:
|
|
83
136
|
this.state.bold = true;
|
|
@@ -89,10 +142,13 @@ export class ColoredConsole {
|
|
|
89
142
|
this.state.underline = true;
|
|
90
143
|
break;
|
|
91
144
|
case 5:
|
|
92
|
-
this.state.
|
|
145
|
+
this.state.blink = true;
|
|
93
146
|
break;
|
|
94
147
|
case 6:
|
|
95
|
-
this.state.
|
|
148
|
+
this.state.rapidBlink = true;
|
|
149
|
+
break;
|
|
150
|
+
case 8:
|
|
151
|
+
this.state.secret = true;
|
|
96
152
|
break;
|
|
97
153
|
case 9:
|
|
98
154
|
this.state.strikethrough = true;
|
|
@@ -106,6 +162,13 @@ export class ColoredConsole {
|
|
|
106
162
|
case 24:
|
|
107
163
|
this.state.underline = false;
|
|
108
164
|
break;
|
|
165
|
+
case 25:
|
|
166
|
+
this.state.blink = false;
|
|
167
|
+
this.state.rapidBlink = false;
|
|
168
|
+
break;
|
|
169
|
+
case 28:
|
|
170
|
+
this.state.secret = false;
|
|
171
|
+
break;
|
|
109
172
|
case 29:
|
|
110
173
|
this.state.strikethrough = false;
|
|
111
174
|
break;
|
|
@@ -164,14 +227,88 @@ export class ColoredConsole {
|
|
|
164
227
|
}
|
|
165
228
|
}
|
|
166
229
|
}
|
|
167
|
-
const atBottom = this.targetElement.scrollTop >
|
|
168
|
-
this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
|
|
169
230
|
addSpan(line.substring(i));
|
|
170
|
-
|
|
171
|
-
|
|
231
|
+
return lineSpan;
|
|
232
|
+
}
|
|
233
|
+
processLines() {
|
|
234
|
+
this._rafId = 0;
|
|
235
|
+
this._timeoutId = 0;
|
|
236
|
+
if (this._destroyed || this.state.lines.length === 0) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const prevCarriageReturn = this.state.carriageReturn;
|
|
240
|
+
const fragment = document.createDocumentFragment();
|
|
241
|
+
for (const line of this.state.lines) {
|
|
242
|
+
if (this.state.carriageReturn && line !== "\n") {
|
|
243
|
+
if (fragment.childElementCount) {
|
|
244
|
+
fragment.removeChild(fragment.lastChild);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const hadCarriageReturn = line.endsWith("\r");
|
|
248
|
+
fragment.appendChild(this.processLine(line.replace(/\r/g, "")));
|
|
249
|
+
this.state.carriageReturn = hadCarriageReturn;
|
|
250
|
+
}
|
|
251
|
+
// Use the tracked sentinel reference instead of lastChild! so this is
|
|
252
|
+
// safe even if the container is empty or the sentinel was removed.
|
|
253
|
+
const sentinel = this._sentinel;
|
|
254
|
+
if (!sentinel) {
|
|
255
|
+
this.state.lines = [];
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (prevCarriageReturn &&
|
|
259
|
+
this.state.lines[0] !== "\n" &&
|
|
260
|
+
sentinel.previousSibling) {
|
|
261
|
+
this.targetElement.replaceChild(fragment, sentinel.previousSibling);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
this.targetElement.insertBefore(fragment, sentinel);
|
|
265
|
+
}
|
|
266
|
+
this.state.lines = [];
|
|
267
|
+
// Trim oldest line-spans when DOM grows too large
|
|
268
|
+
const children = this.targetElement.children;
|
|
269
|
+
// -1 to exclude the sentinel div
|
|
270
|
+
const excess = children.length - 1 - MAX_LINES;
|
|
271
|
+
if (excess > 0) {
|
|
272
|
+
if (!this._atBottom) {
|
|
273
|
+
// Anchor the viewport: subtract the height of removed nodes so the
|
|
274
|
+
// visible content doesn't jump when we're not scrolled to the bottom.
|
|
275
|
+
let removedHeight = 0;
|
|
276
|
+
for (let i = 0; i < excess; i++) {
|
|
277
|
+
removedHeight += children[i].getBoundingClientRect()
|
|
278
|
+
.height;
|
|
279
|
+
}
|
|
280
|
+
for (let i = 0; i < excess; i++) {
|
|
281
|
+
this.targetElement.removeChild(children[0]);
|
|
282
|
+
}
|
|
283
|
+
this.targetElement.scrollTop -= removedHeight;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
for (let i = 0; i < excess; i++) {
|
|
287
|
+
this.targetElement.removeChild(children[0]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (this._atBottom) {
|
|
172
292
|
this.targetElement.scrollTop = this.targetElement.scrollHeight;
|
|
173
293
|
}
|
|
174
294
|
}
|
|
295
|
+
addLine(line) {
|
|
296
|
+
if (this._destroyed)
|
|
297
|
+
return;
|
|
298
|
+
this._exportLines.push(line);
|
|
299
|
+
this.state.lines.push(line);
|
|
300
|
+
// Schedule a flush if none is pending yet
|
|
301
|
+
if (!this._rafId && !this._timeoutId) {
|
|
302
|
+
if (document.hidden) {
|
|
303
|
+
// rAF is paused when the page is hidden — use a timeout fallback
|
|
304
|
+
// so state.lines doesn't accumulate unbounded while backgrounded
|
|
305
|
+
this._timeoutId = window.setTimeout(() => this.processLines(), 50);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
this._rafId = requestAnimationFrame(() => this.processLines());
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
175
312
|
}
|
|
176
313
|
export const coloredConsoleStyles = `
|
|
177
314
|
.log {
|
|
@@ -204,6 +341,17 @@ export const coloredConsoleStyles = `
|
|
|
204
341
|
.log-underline.log-strikethrough {
|
|
205
342
|
text-decoration: underline line-through;
|
|
206
343
|
}
|
|
344
|
+
.log-blink {
|
|
345
|
+
animation: blink 1s step-end infinite;
|
|
346
|
+
}
|
|
347
|
+
.log-rapid-blink {
|
|
348
|
+
animation: blink 0.4s step-end infinite;
|
|
349
|
+
}
|
|
350
|
+
@keyframes blink {
|
|
351
|
+
50% {
|
|
352
|
+
opacity: 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
207
355
|
.log-secret {
|
|
208
356
|
-webkit-user-select: none;
|
|
209
357
|
-moz-user-select: none;
|
|
@@ -5,10 +5,19 @@ export class LineBreakTransformer {
|
|
|
5
5
|
transform(chunk, controller) {
|
|
6
6
|
// Append new chunks to existing chunks.
|
|
7
7
|
this.chunks += chunk;
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// Split on \r\n, lone \r, or lone \n — capturing the separator so we can
|
|
9
|
+
// distinguish a lone \r (overwrite intent) from a normal newline.
|
|
10
|
+
const re = /\r\n|\r|\n/g;
|
|
11
|
+
let lastIndex = 0;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = re.exec(this.chunks)) !== null) {
|
|
14
|
+
const line = this.chunks.substring(lastIndex, match.index);
|
|
15
|
+
// Emit with \r suffix only for lone \r (overwrite), \n for everything else.
|
|
16
|
+
const suffix = match[0] === "\r" ? "\r" : "\n";
|
|
17
|
+
controller.enqueue(line + suffix);
|
|
18
|
+
lastIndex = re.lastIndex;
|
|
19
|
+
}
|
|
20
|
+
this.chunks = this.chunks.substring(lastIndex);
|
|
12
21
|
}
|
|
13
22
|
flush(controller) {
|
|
14
23
|
// When the stream is closed, flush any remaining chunks out.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Matches lines that already carry a wall-clock or tick timestamp so we don't
|
|
2
|
+
// add a redundant one. Does NOT match bare log-level prefixes like ESPHome's
|
|
3
|
+
// [I][tag:line]: — those have no time information.
|
|
4
|
+
//
|
|
5
|
+
// Covered formats:
|
|
6
|
+
// (123456) FreeRTOS ms-tick e.g. "(12345) "
|
|
7
|
+
// [HH:MM:SS] wall-clock bracket
|
|
8
|
+
// [HH:MM:SS.mmm] wall-clock bracket with millis
|
|
9
|
+
// I (1234) tag: ESP-IDF log level + tick e.g. "I (1234) wifi: ..."
|
|
10
|
+
// HH:MM:SS.mmm plain wall-clock
|
|
11
|
+
const DEVICE_TIMESTAMP_RE = /^\s*(?:\(\d+\)\s|\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|[DIWEACV] \(\d+\) \w|(?:\d{2}:){2}\d{2}\.\d)/;
|
|
12
|
+
export class TimestampTransformer {
|
|
13
|
+
transform(chunk, controller) {
|
|
14
|
+
// Pass through pure newline (blank-line sentinel) and empty chunks unchanged
|
|
15
|
+
// so that carriage-return overwrite logic in console-color.ts can still
|
|
16
|
+
// detect them via line !== "\n".
|
|
17
|
+
if (chunk === "" || chunk === "\n") {
|
|
18
|
+
controller.enqueue(chunk);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Skip prefixing if the line already starts with a timestamp
|
|
22
|
+
if (DEVICE_TIMESTAMP_RE.test(chunk)) {
|
|
23
|
+
controller.enqueue(chunk);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const date = new Date();
|
|
27
|
+
const h = date.getHours().toString().padStart(2, "0");
|
|
28
|
+
const m = date.getMinutes().toString().padStart(2, "0");
|
|
29
|
+
const s = date.getSeconds().toString().padStart(2, "0");
|
|
30
|
+
controller.enqueue(`[${h}:${m}:${s}]${chunk}`);
|
|
31
|
+
}
|
|
32
|
+
}
|